Compare commits

..

78 Commits

Author SHA1 Message Date
John Preston
e5434ea491 Version 2.2.
- Quickly switch between different Telegram accounts
if you use multiple phone numbers.
- Share and store unlimited files of any type, now up to 2'000 MB each.
- Edit your scheduled messages.
- Use Auto-Night Mode to make Telegram night mode match
the system Dark Mode settings.
- Also added an option to switch to system window frame
in Windows and Linux.
2020-07-26 13:48:13 +04:00
John Preston
ce4338fae4 Closed alpha version 2.1.22.1. 2020-07-25 11:36:02 +04:00
John Preston
5a1a8af222 Suggest enabling archive and mute in settings. 2020-07-25 11:25:51 +04:00
John Preston
152b49c65c Reset notify settings on unarchive. 2020-07-25 11:25:22 +04:00
John Preston
5c5414b680 Improve video userpics in chat history. 2020-07-25 11:25:22 +04:00
John Preston
f99960e1f6 Play video userpics in photo change messages. 2020-07-25 11:25:22 +04:00
John Preston
e363b254f6 Add Unarchive button to ContactStatus bar. 2020-07-25 11:25:19 +04:00
John Preston
3aea9cb3ca Add archive and mute settings editing. 2020-07-25 11:24:37 +04:00
John Preston
060fe6a928 Play video userpics in profiles and settings. 2020-07-25 11:24:11 +04:00
John Preston
8c45b5e0f8 Show video userpics in media viewer. 2020-07-25 11:24:11 +04:00
John Preston
0126578dbd Allow to load video components of photos. 2020-07-25 11:24:11 +04:00
John Preston
1a9c241b96 Update API scheme to layer 116. 2020-07-25 11:24:11 +04:00
John Preston
638d4d63c5 Update API scheme to layer 115. 2020-07-25 11:24:11 +04:00
John Preston
9370e87c54 Beta version 2.1.22.
- Fix crash in web page preview display.
2020-07-24 20:00:08 +04:00
John Preston
59c38df5cc Fix crash in web page preview display. 2020-07-24 19:54:47 +04:00
John Preston
5655ad25b0 Beta version 2.1.21.
- Edit your scheduled messages.
- See the unread messages indicator for your additional accounts
on the main menu button.
- Use Auto-Night Mode to make Telegram night mode match
the system Dark Mode settings.
- Enjoy dark native window frame for Telegram night mode on Windows.
2020-07-24 17:07:54 +04:00
John Preston
c86ced8a1e Use dark Windows title bar for night mode. 2020-07-24 16:15:58 +04:00
John Preston
511067981d Forbid auto-night mode with theme editor. 2020-07-24 10:01:37 +04:00
John Preston
9a186cd8ce Indicate other accounts unread messages. 2020-07-24 09:41:51 +04:00
Ilya Fedin
385aa3eef7 Remove redudant library order hacks 2020-07-24 09:40:20 +04:00
John Preston
e065d32d28 Don't try building profile photos list by updates. 2020-07-23 17:47:04 +04:00
John Preston
d4feb16378 Don't mark as read when scheduling messages. 2020-07-23 17:46:52 +04:00
Ilya Fedin
79e6369e27 Move libatomic link to common_options 2020-07-23 14:29:56 +04:00
Ilya Fedin
e4bd89d33e Remove redudant hacks for missed dependencies
27f6c8ce62 and 3194d883d2 added missed pthread dependencies that allows to remove Threads::Threads dependency
2020-07-23 14:29:56 +04:00
Ilya Fedin
2b89700f66 libwayland headers are needed only with Qt < 5.13 2020-07-23 14:29:56 +04:00
Ilya Fedin
ab95751a66 Make gdk backend limit more permissive 2020-07-23 14:29:56 +04:00
Ilya Fedin
841908fe31 Read decoration layout property from gtk 2020-07-23 14:29:56 +04:00
23rd
9e0b046213 Fixed handling of language switch for connection type in intro settings. 2020-07-23 13:00:55 +03:00
23rd
d46b9d024e Fixed handling of language switch for theme names in intro settings. 2020-07-23 13:00:55 +03:00
23rd
52cd9f8cbf Fixed handling of language switch for checkboxes in intro settings. 2020-07-23 13:00:55 +03:00
John Preston
25d69434ec Support auto-night mode on macOS. 2020-07-23 12:30:20 +04:00
John Preston
8c4e8212cd Add 'respect system dark mode' checkbox. 2020-07-22 17:18:17 +04:00
Ilya Fedin
c24da4c3df Don't generate 64px tray icon since badge counter generator doesn't support it 2020-07-22 10:33:12 +04:00
Ilya Fedin
47a237c924 Implement system-based dark mode for Windows and Linux 2020-07-22 10:32:56 +04:00
23rd
fc3a9d98c0 Fixed phrase translate of option for native window frame. 2020-07-22 10:31:32 +04:00
23rd
acce671eb0 Added ability to jump to bottom in HistoryWidget with Ctrl key.
Fixed #7868.
2020-07-22 10:26:00 +04:00
John Preston
67b6023b32 Fix build on Windows. 2020-07-22 10:25:55 +04:00
23rd
5a46bb1770 Fixed stuck of FieldHeader when invalid link was provided. 2020-07-20 21:44:07 +03:00
23rd
01fd8aded1 Slightly refactored DragArea. 2020-07-20 21:44:07 +03:00
23rd
e0750f7b87 Added drag'n'drop area to SendFilesBox for images. 2020-07-20 21:44:07 +03:00
23rd
4eaba39a7c Added drag'n'drop area to EditCaptionBox. 2020-07-20 21:44:07 +03:00
23rd
6ac9ef34eb Moved ActivateWindow from HistoryWidget to single place. 2020-07-20 21:44:07 +03:00
23rd
42a2286230 Added implementation of drag'n'drop in section of scheduled messages. 2020-07-20 21:44:07 +03:00
23rd
24d02d5461 Replaced creating of drag'n'drop area in HistoryWidget with new way. 2020-07-20 21:44:07 +03:00
23rd
9bf2940375 Simplified setup of drag'n'drop area. 2020-07-20 21:44:07 +03:00
23rd
d98212e8b3 Fixed incorrect text in context menu item to cancel scheduled uploading. 2020-07-20 21:44:07 +03:00
23rd
3fe9c36d90 Added Esc shortcut to cancel edit in section of scheduled messages. 2020-07-20 21:44:07 +03:00
23rd
22f16caa89 Fixed multiple attempts to send request to edit message. 2020-07-20 21:44:07 +03:00
23rd
46cce57f6b Removed display post views for sent forwarded scheduled messages. 2020-07-20 21:44:07 +03:00
23rd
456244cdec Fixed blurred display of scheduled photos that were downloaded too fast. 2020-07-20 21:44:07 +03:00
23rd
69b2030c71 Added updating of WebPage preview image on download finish. 2020-07-20 21:44:07 +03:00
23rd
11018d76f1 Added Up arrow shortcut to edit scheduled messages. 2020-07-20 21:44:07 +03:00
23rd
e862215efb Added handling of group rights changes to scheduled WebPage preview.
Slightly refactored code.
2020-07-20 21:44:06 +03:00
23rd
129de6d87f Added ability to scroll to scheduled text message on edit header click. 2020-07-20 21:43:33 +03:00
23rd
3c3ce24675 Added ability to remove WebPage preview from scheduled messages section. 2020-07-20 21:43:33 +03:00
23rd
d98ac33425 Improved WebPage preview support in scheduled text messages. 2020-07-20 21:43:33 +03:00
23rd
76842792b8 Added initial support for WebPage cancelling of scheduled text messages. 2020-07-19 19:56:18 +03:00
23rd
4b01043b27 Added handling of deleting currently edited scheduled text messages. 2020-07-19 19:56:18 +03:00
23rd
0a4f3f310c Added initial implementation of editing of scheduled text messages. 2020-07-19 19:56:18 +03:00
23rd
8320feea10 Added saving local text before editing scheduled message. 2020-07-19 19:56:18 +03:00
23rd
58281023bc Moved drawing preview image from HistoryWidget to WebPageData. 2020-07-19 19:56:18 +03:00
23rd
0b655450bb Added text preview in edit header of scheduled messages section. 2020-07-19 19:56:18 +03:00
23rd
61292557bf Fixed top scroll of compose controls on change of height. 2020-07-19 19:56:18 +03:00
23rd
12ad1190ff Added initial edit message header to scheduled section. 2020-07-19 19:56:18 +03:00
23rd
42e0994581 Moved generating preview text from HistoryWidget to WebPageData. 2020-07-19 19:56:18 +03:00
23rd
69bc595e31 Fixed text loss when editing media content is canceled. 2020-07-17 18:22:38 +03:00
23rd
5c097887ef Added ability to edit media in scheduled messages. 2020-07-17 18:22:38 +03:00
23rd
b02b690747 Moved HistoryWidget::saveEditMsgDone/Fail to lambdas. 2020-07-17 18:22:38 +03:00
23rd
c52da743fd Fixed WebPageId removing from api_editing. 2020-07-17 18:22:38 +03:00
23rd
63dff9ff91 Moved edit text messages from HistoryWidget to api_editing. 2020-07-17 18:22:38 +03:00
23rd
1c41808042 Set Api::EditMessage as generic function. 2020-07-17 18:22:38 +03:00
23rd
2ebf44c166 Removed unused code from apiwrap. 2020-07-17 18:22:38 +03:00
23rd
c46b96f252 Moved edit captions from EditCaptionBox to api_editing. 2020-07-17 18:22:38 +03:00
23rd
6c89f60679 Added edit messages with uploaded file to api_editing. 2020-07-17 18:22:38 +03:00
23rd
bb73687fc5 Added api_editing as separate file of API code for edit messages. 2020-07-17 18:22:38 +03:00
23rd
31fa2d9355 Added ScheduledMessages::lookupItem for FullMsgId. 2020-07-17 18:22:37 +03:00
23rd
c350e33dd8 Moved preparing MTPInputMedia to separate file. 2020-07-17 18:22:37 +03:00
23rd
7dd9adb934 Added handling of updates for edited media in scheduled messages. 2020-07-17 18:22:37 +03:00
157 changed files with 5140 additions and 1390 deletions

View File

@@ -72,9 +72,7 @@ if (LINUX)
PRIVATE
desktop-app::external_materialdecoration
desktop-app::external_nimf_qt5
desktop-app::external_qt5ct
desktop-app::external_qt5ct_style
desktop-app::external_qt5ct_qtplugin
desktop-app::external_qt5ct_support
)
if (NOT DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
@@ -106,6 +104,7 @@ PRIVATE
tdesktop::lib_mtproto
tdesktop::lib_scheme
tdesktop::lib_export
tdesktop::lib_tgvoip
desktop-app::lib_base
desktop-app::lib_crl
desktop-app::lib_ui
@@ -122,12 +121,11 @@ PRIVATE
desktop-app::external_qr_code_generator
desktop-app::external_crash_reports
desktop-app::external_auto_updates
tdesktop::lib_tgvoip
desktop-app::external_openssl
desktop-app::external_openal
)
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
if (LINUX AND DESKTOP_APP_USE_PACKAGED AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
@@ -183,26 +181,6 @@ if (LINUX AND NOT TDESKTOP_DISABLE_GTK_INTEGRATION)
endif()
endif()
# Telegram uses long atomic types, so on some architectures libatomic is needed.
check_cxx_source_compiles("
#include <atomic>
std::atomic_int64_t foo;
int main() {return foo;}
" HAVE_LONG_ATOMIC_WITHOUT_LIB)
if (NOT HAVE_LONG_ATOMIC_WITHOUT_LIB)
target_link_libraries(Telegram PRIVATE atomic)
endif()
if (DESKTOP_APP_USE_PACKAGED)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
find_package(Threads REQUIRED)
target_link_libraries(Telegram
PRIVATE
Threads::Threads
)
endif()
target_precompile_headers(Telegram PRIVATE ${src_loc}/stdafx.h)
nice_target_sources(Telegram ${src_loc}
PRIVATE
@@ -212,9 +190,17 @@ PRIVATE
api/api_bot.h
api/api_chat_filters.cpp
api/api_chat_filters.h
api/api_chat_invite.cpp
api/api_chat_invite.h
api/api_common.h
api/api_editing.cpp
api/api_editing.h
api/api_global_privacy.cpp
api/api_global_privacy.h
api/api_hash.cpp
api/api_hash.h
api/api_media.cpp
api/api_media.h
api/api_self_destruct.cpp
api/api_self_destruct.h
api/api_send_progress.cpp
@@ -600,6 +586,8 @@ PRIVATE
history/view/history_view_service_message.h
history/view/history_view_top_bar_widget.cpp
history/view/history_view_top_bar_widget.h
history/view/history_view_webpage_preview.cpp
history/view/history_view_webpage_preview.h
history/history.cpp
history/history.h
history/history_drag_area.cpp
@@ -1083,6 +1071,7 @@ PRIVATE
window/window_connecting_widget.h
window/window_controller.cpp
window/window_controller.h
window/window_controls_layout.h
window/window_filters_menu.cpp
window/window_filters_menu.h
window/window_history_hider.cpp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 B

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

View File

@@ -424,6 +424,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_passcode_title" = "Local passcode";
"lng_settings_password_title" = "Two-step verification";
"lng_settings_sessions_title" = "Active sessions";
"lng_settings_new_unknown" = "New chats from unknown users";
"lng_settings_auto_archive" = "Archive and Mute";
"lng_settings_auto_archive_about" = "Automatically archive and mute new chats, groups and channels from non-contacts.";
"lng_settings_destroy_title" = "Delete my account";
"lng_settings_network_proxy" = "Network and proxy";
"lng_settings_version_info" = "Version and updates";
@@ -434,6 +437,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_sensitive_disable_filtering" = "Disable filtering";
"lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices.";
"lng_settings_auto_night_mode" = "Auto-Night mode";
"lng_settings_auto_night_enabled" = "Match the system settings";
"lng_settings_auto_night_warning" = "You have enabled auto-night mode. If you want to change the dark mode settings, you'll need to disable it first.";
"lng_settings_auto_night_disable" = "Disable";
"lng_suggest_hide_new_title" = "Hide new chats?";
"lng_suggest_hide_new_about" = "You are receiving lots of new chats from users who are not in your Contact List.\n\nDo you want to have such chats **automatically muted** and **archived**?";
"lng_suggest_hide_new_to_settings" = "Go to Settings";
"lng_settings_spellchecker" = "Spell checker";
"lng_settings_system_spellchecker" = "Use system spell checker";
"lng_settings_custom_spellchecker" = "Use spell checker";
@@ -1127,6 +1139,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_group_invite_members#one" = "{count} member, among them:";
"lng_group_invite_members#other" = "{count} members, among them:";
"lng_channel_invite_private" = "This channel is private.\nPlease join it to continue viewing its content.";
"lng_group_invite_create" = "Create an invite link";
"lng_group_invite_about" = "Telegram users will be able to join\nyour group by following this link.";
@@ -1281,6 +1294,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_new_contact_share_done" = "{user} can now see your phone number.";
"lng_new_contact_add_name" = "Add {user} to contacts";
"lng_new_contact_add_done" = "{user} is now in your contact list.";
"lng_new_contact_unarchive" = "Unarchive";
"lng_cant_send_to_not_contact" = "Sorry, you can only send messages to\nmutual contacts at the moment.\n{more_info}";
"lng_cant_invite_not_contact" = "Sorry, you can only add mutual contacts\nto groups at the moment.\n{more_info}";
"lng_cant_more_info" = "More info »";

View File

@@ -62,10 +62,9 @@ inputMediaUploadedPhoto#1e287d04 flags:# file:InputFile stickers:flags.0?Vector<
inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMedia;
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia;
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
@@ -75,7 +74,7 @@ inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> s
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
inputChatUploadedPhoto#c642724e flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = InputChatPhoto;
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;
inputGeoPointEmpty#e4c123d6 = InputGeoPoint;
@@ -113,7 +112,7 @@ userEmpty#200250ba id:int = User;
user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User;
userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto;
userProfilePhoto#ecd75d8c photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;
userProfilePhoto#69d3ab26 flags:# has_video:flags.0?true photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;
userStatusEmpty#9d05049 = UserStatus;
userStatusOnline#edb93949 expires:int = UserStatus;
@@ -139,7 +138,7 @@ chatParticipantsForbidden#fc900c2b flags:# chat_id:int self_participant:flags.0?
chatParticipants#3f460fed chat_id:int participants:Vector<ChatParticipant> version:int = ChatParticipants;
chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#475cdbd5 photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
messageEmpty#83e5de54 id:int = Message;
message#452c0e65 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector<RestrictionReason> = Message;
@@ -187,7 +186,7 @@ dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer t
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
photoEmpty#2331b22d id:long = Photo;
photo#d07504a5 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> dc_id:int = Photo;
photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> video_sizes:flags.1?Vector<VideoSize> dc_id:int = Photo;
photoSizeEmpty#e17e23c type:string = PhotoSize;
photoSize#77bfb61b type:string location:FileLocation w:int h:int size:int = PhotoSize;
@@ -213,7 +212,7 @@ inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags
peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings;
peerSettings#818426cd flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true = PeerSettings;
peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true geo_distance:flags.6?int = PeerSettings;
wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper;
wallPaperNoFile#8af40b25 flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;
@@ -358,6 +357,7 @@ updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;
updateDialogFilterOrder#a5d72105 order:Vector<int> = Update;
updateDialogFilters#3504914f = Update;
updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update;
updateChannelParticipant#65d2b464 flags:# channel_id:int date:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant qts:int = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@@ -395,7 +395,7 @@ help.inviteText#18cb9f78 message:string = help.InviteText;
encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat;
encryptedChatWaiting#3bf703dc id:int access_hash:long date:int admin_id:int participant_id:int = EncryptedChat;
encryptedChatRequested#c878527e id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
encryptedChatRequested#62718a82 flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
encryptedChat#fa56ce36 id:int access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long = EncryptedChat;
encryptedChatDiscarded#13d6dd27 id:int = EncryptedChat;
@@ -529,6 +529,7 @@ chatInviteExported#fc2e05bc link:string = ExportedChatInvite;
chatInviteAlready#5a686d7c chat:Chat = ChatInvite;
chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector<User> = ChatInvite;
chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite;
inputStickerSetEmpty#ffb62b95 = InputStickerSet;
inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;
@@ -619,11 +620,6 @@ channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector
help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;
foundGif#162ecc1f url:string thumb_url:string content_url:string content_type:string w:int h:int = FoundGif;
foundGifCached#9c750409 url:string photo:Photo document:Document = FoundGif;
messages.foundGifs#450a1c0a next_offset:int results:Vector<FoundGif> = messages.FoundGifs;
messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs;
messages.savedGifs#2e0709a5 hash:int gifs:Vector<Document> = messages.SavedGifs;
@@ -1141,7 +1137,17 @@ stats.broadcastStats#bdf78394 period:StatsDateRangeDays followers:StatsAbsValueA
help.promoDataEmpty#98f6ac75 expires:int = help.PromoData;
help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector<Chat> users:Vector<User> psa_type:flags.1?string psa_message:flags.2?string = help.PromoData;
videoSize#435bb987 type:string location:FileLocation w:int h:int size:int = VideoSize;
videoSize#e831c556 flags:# type:string location:FileLocation w:int h:int size:int video_start_ts:flags.0?double = VideoSize;
statsGroupTopPoster#18f3d0f7 user_id:int messages:int avg_chars:int = StatsGroupTopPoster;
statsGroupTopAdmin#6014f412 user_id:int deleted:int kicked:int banned:int = StatsGroupTopAdmin;
statsGroupTopInviter#31962a4c user_id:int invitations:int = StatsGroupTopInviter;
stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector<StatsGroupTopPoster> top_admins:Vector<StatsGroupTopAdmin> top_inviters:Vector<StatsGroupTopInviter> users:Vector<User> = stats.MegagroupStats;
globalPrivacySettings#bea2f424 flags:# archive_and_mute_new_noncontact_peers:flags.0?Bool = GlobalPrivacySettings;
---functions---
@@ -1237,6 +1243,8 @@ account.getThemes#285946f8 format:string hash:int = account.Themes;
account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool;
account.getContentSettings#8b9b4dae = account.ContentSettings;
account.getMultiWallPapers#65ad71dc wallpapers:Vector<InputWallPaper> = Vector<WallPaper>;
account.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings;
account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
@@ -1312,7 +1320,6 @@ messages.migrateChat#15a3b8e3 chat_id:int = Updates;
messages.searchGlobal#bf7225a4 flags:# folder_id:flags.0?int q:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
messages.reorderStickerSets#78337739 flags:# masks:flags.0?true order:Vector<long> = Bool;
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
messages.searchGifs#bf9a776b q:string offset:int = messages.FoundGifs;
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
@@ -1392,7 +1399,7 @@ updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
photos.updateProfilePhoto#f0bb5152 id:InputPhoto = UserProfilePhoto;
photos.uploadProfilePhoto#4f32c098 file:InputFile = photos.Photo;
photos.uploadProfilePhoto#89f30f69 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo;
photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;
@@ -1425,6 +1432,7 @@ help.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo;
help.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector<MessageEntity> = help.UserInfo;
help.getPromoData#c0977421 = help.PromoData;
help.hidePromoData#1e251c95 peer:InputPeer = Bool;
help.dismissSuggestion#77fa99f suggestion:string = Bool;
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
@@ -1501,5 +1509,6 @@ folders.deleteFolder#1c295881 folder_id:int = Updates;
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;
// LAYER 114
// LAYER 116

View File

@@ -9,7 +9,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="2.1.20.0" />
Version="2.2.0.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>

View File

@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,1,20,0
PRODUCTVERSION 2,1,20,0
FILEVERSION 2,2,0,0
PRODUCTVERSION 2,2,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "2.1.20.0"
VALUE "FileVersion", "2.2.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.1.20.0"
VALUE "ProductVersion", "2.2.0.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,1,20,0
PRODUCTVERSION 2,1,20,0
FILEVERSION 2,2,0,0
PRODUCTVERSION 2,2,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "2.1.20.0"
VALUE "FileVersion", "2.2.0.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.1.20.0"
VALUE "ProductVersion", "2.2.0.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -0,0 +1,230 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_chat_invite.h"
#include "apiwrap.h"
#include "window/window_session_controller.h"
#include "main/main_session.h"
#include "ui/empty_userpic.h"
#include "core/application.h"
#include "data/data_session.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_channel.h"
#include "data/data_user.h"
#include "data/data_file_origin.h"
#include "boxes/confirm_box.h"
#include "boxes/abstract_box.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
namespace Api {
void CheckChatInvite(
not_null<Window::SessionController*> controller,
const QString &hash,
ChannelData *invitePeekChannel) {
const auto session = &controller->session();
const auto weak = base::make_weak(controller.get());
session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {
Core::App().hideMediaView();
result.match([=](const MTPDchatInvite &data) {
const auto box = Ui::show(Box<ConfirmInviteBox>(
session,
data,
invitePeekChannel,
[=] { session->api().importChatInvite(hash); }));
if (invitePeekChannel) {
box->boxClosing(
) | rpl::filter([=] {
return !invitePeekChannel->amIn();
}) | rpl::start_with_next([=] {
if (const auto strong = weak.get()) {
strong->clearSectionStack(Window::SectionShow(
Window::SectionShow::Way::ClearStack,
anim::type::normal,
anim::activation::background));
}
}, box->lifetime());
}
}, [=](const MTPDchatInviteAlready &data) {
if (const auto chat = session->data().processChat(data.vchat())) {
if (const auto channel = chat->asChannel()) {
channel->clearInvitePeek();
}
if (const auto strong = weak.get()) {
strong->showPeerHistory(
chat,
Window::SectionShow::Way::Forward);
}
}
}, [=](const MTPDchatInvitePeek &data) {
if (const auto chat = session->data().processChat(data.vchat())) {
if (const auto channel = chat->asChannel()) {
channel->setInvitePeek(hash, data.vexpires().v);
if (const auto strong = weak.get()) {
strong->showPeerHistory(
chat,
Window::SectionShow::Way::Forward);
}
}
}
});
}, [=](const RPCError &error) {
if (error.code() != 400) {
return;
}
Core::App().hideMediaView();
Ui::show(Box<InformBox>(tr::lng_group_invite_bad_link(tr::now)));
});
}
} // namespace Api
ConfirmInviteBox::ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
ChannelData *invitePeekChannel,
Fn<void()> submit)
: _session(session)
, _submit(std::move(submit))
, _title(this, st::confirmInviteTitle)
, _status(this, st::confirmInviteStatus)
, _participants(GetParticipants(_session, data))
, _isChannel(data.is_channel() && !data.is_megagroup()) {
const auto title = qs(data.vtitle());
const auto count = data.vparticipants_count().v;
const auto status = [&] {
return invitePeekChannel
? tr::lng_channel_invite_private(tr::now)
: (!_participants.empty() && _participants.size() < count)
? tr::lng_group_invite_members(tr::now, lt_count, count)
: (count > 0)
? tr::lng_chat_status_members(tr::now, lt_count_decimal, count)
: _isChannel
? tr::lng_channel_status(tr::now)
: tr::lng_group_status(tr::now);
}();
_title->setText(title);
_status->setText(status);
const auto photo = _session->data().processPhoto(data.vphoto());
if (!photo->isNull()) {
_photo = photo->createMediaView();
_photo->wanted(Data::PhotoSize::Small, Data::FileOrigin());
if (!_photo->image(Data::PhotoSize::Small)) {
_session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
update();
}, lifetime());
}
} else {
_photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Data::PeerUserpicColor(0),
title);
}
}
ConfirmInviteBox::~ConfirmInviteBox() = default;
auto ConfirmInviteBox::GetParticipants(
not_null<Main::Session*> session,
const MTPDchatInvite &data)
-> std::vector<Participant> {
const auto participants = data.vparticipants();
if (!participants) {
return {};
}
const auto &v = participants->v;
auto result = std::vector<Participant>();
result.reserve(v.size());
for (const auto &participant : v) {
if (const auto user = session->data().processUser(participant)) {
result.push_back(Participant{ user });
}
}
return result;
}
void ConfirmInviteBox::prepare() {
addButton(
(_isChannel
? tr::lng_profile_join_channel()
: tr::lng_profile_join_group()),
_submit);
addButton(tr::lng_cancel(), [=] { closeBox(); });
while (_participants.size() > 4) {
_participants.pop_back();
}
auto newHeight = st::confirmInviteStatusTop + _status->height() + st::boxPadding.bottom();
if (!_participants.empty()) {
int skip = (st::boxWideWidth - 4 * st::confirmInviteUserPhotoSize) / 5;
int padding = skip / 2;
_userWidth = (st::confirmInviteUserPhotoSize + 2 * padding);
int sumWidth = _participants.size() * _userWidth;
int left = (st::boxWideWidth - sumWidth) / 2;
for (const auto &participant : _participants) {
auto name = new Ui::FlatLabel(this, st::confirmInviteUserName);
name->resizeToWidth(st::confirmInviteUserPhotoSize + padding);
name->setText(participant.user->firstName.isEmpty()
? participant.user->name
: participant.user->firstName);
name->moveToLeft(left + (padding / 2), st::confirmInviteUserNameTop);
left += _userWidth;
}
newHeight += st::confirmInviteUserHeight;
}
setDimensions(st::boxWideWidth, newHeight);
}
void ConfirmInviteBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_title->move((width() - _title->width()) / 2, st::confirmInviteTitleTop);
_status->move((width() - _status->width()) / 2, st::confirmInviteStatusTop);
}
void ConfirmInviteBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
Painter p(this);
if (_photo) {
if (const auto image = _photo->image(Data::PhotoSize::Small)) {
p.drawPixmap(
(width() - st::confirmInvitePhotoSize) / 2,
st::confirmInvitePhotoTop,
image->pixCircled(
st::confirmInvitePhotoSize,
st::confirmInvitePhotoSize));
}
} else if (_photoEmpty) {
_photoEmpty->paint(
p,
(width() - st::confirmInvitePhotoSize) / 2,
st::confirmInvitePhotoTop,
width(),
st::confirmInvitePhotoSize);
}
int sumWidth = _participants.size() * _userWidth;
int left = (width() - sumWidth) / 2;
for (auto &participant : _participants) {
participant.user->paintUserpicLeft(
p,
participant.userpic,
left + (_userWidth - st::confirmInviteUserPhotoSize) / 2,
st::confirmInviteUserPhotoTop,
width(),
st::confirmInviteUserPhotoSize);
left += _userWidth;
}
}

View File

@@ -0,0 +1,74 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/layers/box_content.h"
class UserData;
class ChannelData;
namespace Window {
class SessionController;
} // namespace Window
namespace Data {
class CloudImageView;
class PhotoMedia;
} // namespace Data
namespace Ui {
class EmptyUserpic;
} // namespace Ui
namespace Api {
void CheckChatInvite(
not_null<Window::SessionController*> controller,
const QString &hash,
ChannelData *invitePeekChannel = nullptr);
} // namespace Api
class ConfirmInviteBox final : public Ui::BoxContent {
public:
ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
ChannelData *invitePeekChannel,
Fn<void()> submit);
~ConfirmInviteBox();
protected:
void prepare() override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
struct Participant {
not_null<UserData*> user;
std::shared_ptr<Data::CloudImageView> userpic;
};
static std::vector<Participant> GetParticipants(
not_null<Main::Session*> session,
const MTPDchatInvite &data);
const not_null<Main::Session*> _session;
Fn<void()> _submit;
object_ptr<Ui::FlatLabel> _title;
object_ptr<Ui::FlatLabel> _status;
std::shared_ptr<Data::PhotoMedia> _photo;
std::unique_ptr<Ui::EmptyUserpic> _photoEmpty;
std::vector<Participant> _participants;
bool _isChannel = false;
int _userWidth = 0;
};

View File

@@ -0,0 +1,213 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_editing.h"
#include "apiwrap.h"
#include "api/api_media.h"
#include "api/api_text_entities.h"
#include "boxes/confirm_box.h"
#include "data/data_scheduled_messages.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mtproto/mtproto_rpc_sender.h"
namespace Api {
namespace {
using namespace rpl::details;
template <typename T>
constexpr auto WithId =
is_callable_plain_v<T, const MTPUpdates &, Fn<void()>, mtpRequestId>;
template <typename T>
constexpr auto WithoutId =
is_callable_plain_v<T, const MTPUpdates &, Fn<void()>>;
template <typename T>
constexpr auto WithoutCallback =
is_callable_plain_v<T, const MTPUpdates &>;
template <typename DoneCallback, typename FailCallback>
mtpRequestId EditMessage(
not_null<HistoryItem*> item,
const TextWithEntities &textWithEntities,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
std::optional<MTPInputMedia> inputMedia = std::nullopt) {
const auto session = &item->history()->session();
const auto api = &session->api();
const auto text = textWithEntities.text;
const auto sentEntities = EntitiesToMTP(
session,
textWithEntities.entities,
ConvertOption::SkipLocal);
const auto media = item->media();
const auto emptyFlag = MTPmessages_EditMessage::Flag(0);
const auto flags = emptyFlag
| (!text.isEmpty() || media
? MTPmessages_EditMessage::Flag::f_message
: emptyFlag)
| ((media && inputMedia.has_value())
? MTPmessages_EditMessage::Flag::f_media
: emptyFlag)
| (options.removeWebPageId
? MTPmessages_EditMessage::Flag::f_no_webpage
: emptyFlag)
| (!sentEntities.v.isEmpty()
? MTPmessages_EditMessage::Flag::f_entities
: emptyFlag)
| (options.scheduled
? MTPmessages_EditMessage::Flag::f_schedule_date
: emptyFlag);
const auto id = item->isScheduled()
? session->data().scheduledMessages().lookupId(item)
: item->id;
return api->request(MTPmessages_EditMessage(
MTP_flags(flags),
item->history()->peer->input,
MTP_int(id),
MTP_string(text),
inputMedia.value_or(MTPInputMedia()),
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled)
)).done([=](
const MTPUpdates &result,
[[maybe_unused]] mtpRequestId requestId) {
const auto apply = [=] { api->applyUpdates(result); };
if constexpr (WithId<DoneCallback>) {
done(result, apply, requestId);
} else if constexpr (WithoutId<DoneCallback>) {
done(result, apply);
} else if constexpr (WithoutCallback<DoneCallback>) {
done(result);
apply();
} else {
apply();
}
}).fail(
fail
).send();
}
template <typename DoneCallback, typename FailCallback>
mtpRequestId EditMessage(
not_null<HistoryItem*> item,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
std::optional<MTPInputMedia> inputMedia = std::nullopt) {
const auto &text = item->originalText();
return EditMessage(
item,
text,
options,
std::forward<DoneCallback>(done),
std::forward<FailCallback>(fail),
inputMedia);
}
void EditMessageWithUploadedMedia(
not_null<HistoryItem*> item,
SendOptions options,
MTPInputMedia media) {
const auto done = [=](const auto &result, Fn<void()> applyUpdates) {
if (item) {
item->clearSavedMedia();
item->setIsLocalUpdateMedia(true);
applyUpdates();
item->setIsLocalUpdateMedia(false);
}
};
const auto fail = [=](const RPCError &error) {
const auto err = error.type();
const auto session = &item->history()->session();
const auto notModified = (err == u"MESSAGE_NOT_MODIFIED"_q);
const auto mediaInvalid = (err == u"MEDIA_NEW_INVALID"_q);
if (notModified || mediaInvalid) {
item->returnSavedMedia();
session->data().sendHistoryChangeNotifications();
if (mediaInvalid) {
Ui::show(
Box<InformBox>(tr::lng_edit_media_invalid_file(tr::now)),
Ui::LayerOption::KeepOther);
}
} else {
session->api().sendMessageFail(error, item->history()->peer);
}
};
EditMessage(item, options, done, fail, media);
}
} // namespace
void RescheduleMessage(
not_null<HistoryItem*> item,
SendOptions options) {
const auto empty = [](const auto &r) {};
EditMessage(item, options, empty, empty);
}
void EditMessageWithUploadedDocument(
HistoryItem *item,
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb,
SendOptions options) {
if (!item || !item->media() || !item->media()->document()) {
return;
}
const auto media = PrepareUploadedDocument(item, file, thumb);
EditMessageWithUploadedMedia(item, options, media);
}
void EditMessageWithUploadedPhoto(
HistoryItem *item,
const MTPInputFile &file,
SendOptions options) {
if (!item || !item->media() || !item->media()->photo()) {
return;
}
const auto media = PrepareUploadedPhoto(file);
EditMessageWithUploadedMedia(item, options, media);
}
mtpRequestId EditCaption(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
SendOptions options,
Fn<void(const MTPUpdates &)> done,
Fn<void(const RPCError &)> fail) {
return EditMessage(item, caption, options, done, fail);
}
mtpRequestId EditTextMessage(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
SendOptions options,
Fn<void(const MTPUpdates &, mtpRequestId requestId)> done,
Fn<void(const RPCError &, mtpRequestId requestId)> fail) {
const auto callback = [=](
const auto &result,
Fn<void()> applyUpdates,
auto id) {
applyUpdates();
done(result, id);
};
return EditMessage(item, caption, options, callback, fail);
}
} // namespace Api

View File

@@ -0,0 +1,52 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class HistoryItem;
class RPCError;
namespace Api {
struct SendOptions;
const auto kDefaultEditMessagesErrors = {
u"MESSAGE_ID_INVALID"_q,
u"CHAT_ADMIN_REQUIRED"_q,
u"MESSAGE_EDIT_TIME_EXPIRED"_q,
};
void RescheduleMessage(
not_null<HistoryItem*> item,
SendOptions options);
void EditMessageWithUploadedDocument(
HistoryItem *item,
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb,
SendOptions options);
void EditMessageWithUploadedPhoto(
HistoryItem *item,
const MTPInputFile &file,
SendOptions options);
mtpRequestId EditCaption(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
SendOptions options,
Fn<void(const MTPUpdates &)> done,
Fn<void(const RPCError &)> fail);
mtpRequestId EditTextMessage(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
SendOptions options,
Fn<void(const MTPUpdates &, mtpRequestId requestId)> done,
Fn<void(const RPCError &, mtpRequestId requestId)> fail);
} // namespace Api

View File

@@ -0,0 +1,103 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_global_privacy.h"
#include "apiwrap.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
namespace Api {
GlobalPrivacy::GlobalPrivacy(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
void GlobalPrivacy::reload(Fn<void()> callback) {
if (callback) {
_callbacks.push_back(std::move(callback));
}
if (_requestId) {
return;
}
_requestId = _api.request(MTPaccount_GetGlobalPrivacySettings(
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
for (const auto &callback : base::take(_callbacks)) {
callback();
}
}).fail([=](const RPCError &error) {
_requestId = 0;
for (const auto &callback : base::take(_callbacks)) {
callback();
}
}).send();
_session->account().appConfig().value(
) | rpl::start_with_next([=] {
_showArchiveAndMute = _session->account().appConfig().get<bool>(
u"autoarchive_setting_available"_q,
false);
}, _session->lifetime());
}
bool GlobalPrivacy::archiveAndMuteCurrent() const {
return _archiveAndMute.current();
}
rpl::producer<bool> GlobalPrivacy::archiveAndMute() const {
return _archiveAndMute.value();
}
rpl::producer<bool> GlobalPrivacy::showArchiveAndMute() const {
using namespace rpl::mappers;
return rpl::combine(
archiveAndMute(),
_showArchiveAndMute.value(),
_1 || _2);
}
rpl::producer<> GlobalPrivacy::suggestArchiveAndMute() const {
return _session->account().appConfig().suggestionRequested(
u"AUTOARCHIVE_POPULAR"_q);
}
void GlobalPrivacy::dismissArchiveAndMuteSuggestion() {
_session->account().appConfig().dismissSuggestion(
u"AUTOARCHIVE_POPULAR"_q);
}
void GlobalPrivacy::update(bool archiveAndMute) {
using Flag = MTPDglobalPrivacySettings::Flag;
_api.request(_requestId).cancel();
_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
MTP_globalPrivacySettings(
MTP_flags(Flag::f_archive_and_mute_new_noncontact_peers),
MTP_bool(archiveAndMute))
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
}).fail([=](const RPCError &error) {
_requestId = 0;
}).send();
_archiveAndMute = archiveAndMute;
}
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &data) {
data.match([&](const MTPDglobalPrivacySettings &data) {
_archiveAndMute = data.varchive_and_mute_new_noncontact_peers()
? mtpIsTrue(*data.varchive_and_mute_new_noncontact_peers())
: false;
});
}
} // namespace Api

View File

@@ -0,0 +1,45 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class GlobalPrivacy final {
public:
explicit GlobalPrivacy(not_null<ApiWrap*> api);
void reload(Fn<void()> callback = nullptr);
void update(bool archiveAndMute);
[[nodiscard]] bool archiveAndMuteCurrent() const;
[[nodiscard]] rpl::producer<bool> archiveAndMute() const;
[[nodiscard]] rpl::producer<bool> showArchiveAndMute() const;
[[nodiscard]] rpl::producer<> suggestArchiveAndMute() const;
void dismissArchiveAndMuteSuggestion();
private:
void apply(const MTPGlobalPrivacySettings &data);
const not_null<Main::Session*> _session;
MTP::Sender _api;
mtpRequestId _requestId = 0;
rpl::variable<bool> _archiveAndMute = false;
rpl::variable<bool> _showArchiveAndMute = false;
std::vector<Fn<void()>> _callbacks;
};
} // namespace Api

View File

@@ -0,0 +1,107 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_media.h"
#include "data/data_document.h"
#include "history/history_item.h"
namespace Api {
namespace {
MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
not_null<DocumentData*> document) {
const auto filenameAttribute = MTP_documentAttributeFilename(
MTP_string(document->filename()));
const auto dimensions = document->dimensions;
auto attributes = QVector<MTPDocumentAttribute>(1, filenameAttribute);
if (dimensions.width() > 0 && dimensions.height() > 0) {
const auto duration = document->getDuration();
if (duration >= 0 && !document->hasMimeType(qstr("image/gif"))) {
auto flags = MTPDdocumentAttributeVideo::Flags(0);
using VideoFlag = MTPDdocumentAttributeVideo::Flag;
if (document->isVideoMessage()) {
flags |= VideoFlag::f_round_message;
}
if (document->supportsStreaming()) {
flags |= VideoFlag::f_supports_streaming;
}
attributes.push_back(MTP_documentAttributeVideo(
MTP_flags(flags),
MTP_int(duration),
MTP_int(dimensions.width()),
MTP_int(dimensions.height())));
} else {
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(dimensions.width()),
MTP_int(dimensions.height())));
}
}
if (document->type == AnimatedDocument) {
attributes.push_back(MTP_documentAttributeAnimated());
} else if (document->type == StickerDocument && document->sticker()) {
attributes.push_back(MTP_documentAttributeSticker(
MTP_flags(0),
MTP_string(document->sticker()->alt),
document->sticker()->set,
MTPMaskCoords()));
} else if (const auto song = document->song()) {
const auto flags = MTPDdocumentAttributeAudio::Flag::f_title
| MTPDdocumentAttributeAudio::Flag::f_performer;
attributes.push_back(MTP_documentAttributeAudio(
MTP_flags(flags),
MTP_int(song->duration),
MTP_string(song->title),
MTP_string(song->performer),
MTPstring()));
} else if (const auto voice = document->voice()) {
const auto flags = MTPDdocumentAttributeAudio::Flag::f_voice
| MTPDdocumentAttributeAudio::Flag::f_waveform;
attributes.push_back(MTP_documentAttributeAudio(
MTP_flags(flags),
MTP_int(voice->duration),
MTPstring(),
MTPstring(),
MTP_bytes(documentWaveformEncode5bit(voice->waveform))));
}
return MTP_vector<MTPDocumentAttribute>(attributes);
}
} // namespace
MTPInputMedia PrepareUploadedPhoto(const MTPInputFile &file) {
return MTP_inputMediaUploadedPhoto(
MTP_flags(0),
file,
MTPVector<MTPInputDocument>(),
MTP_int(0));
}
MTPInputMedia PrepareUploadedDocument(
not_null<HistoryItem*> item,
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb) {
if (!item || !item->media() || !item->media()->document()) {
return MTP_inputMediaEmpty();
}
const auto emptyFlag = MTPDinputMediaUploadedDocument::Flags(0);
using DocFlags = MTPDinputMediaUploadedDocument::Flag;
const auto flags = emptyFlag
| (thumb ? DocFlags::f_thumb : emptyFlag)
| (item->groupId() ? DocFlags::f_nosound_video : emptyFlag);
const auto document = item->media()->document();
return MTP_inputMediaUploadedDocument(
MTP_flags(flags),
file,
thumb.value_or(MTPInputFile()),
MTP_string(document->mimeString()),
ComposeSendingDocumentAttributes(document),
MTPVector<MTPInputDocument>(),
MTP_int(0));
}
} // namespace Api

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class HistoryItem;
namespace Api {
MTPInputMedia PrepareUploadedPhoto(const MTPInputFile &file);
MTPInputMedia PrepareUploadedDocument(
not_null<HistoryItem*> item,
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb);
} // namespace Api

View File

@@ -400,6 +400,8 @@ void SendConfirmedFile(
}
if (file->to.options.scheduled) {
flags |= MTPDmessage::Flag::f_from_scheduled;
// Scheduled messages have no the 'edited' badge.
flags |= MTPDmessage::Flag::f_edit_hide;
} else {
clientFlags |= MTPDmessage_ClientFlag::f_local_history_entry;
}
@@ -523,9 +525,13 @@ void SendConfirmedFile(
}
session->data().sendHistoryChangeNotifications();
session->changes().historyUpdated(
history,
Data::HistoryUpdate::Flag::MessageSent);
if (!itemToEdit) {
session->changes().historyUpdated(
history,
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
}
}
} // namespace Api

View File

@@ -1642,15 +1642,21 @@ void Updates::feedUpdate(const MTPUpdate &update) {
if (auto user = session().data().userLoaded(d.vuser_id().v)) {
user->setPhoto(d.vphoto());
user->loadUserpic();
if (mtpIsTrue(d.vprevious()) || !user->userpicPhotoId()) {
// After that update we don't have enough information to
// create a 'photo' with all necessary fields. So if
// we receive second such update we end up with a 'photo_id'
// in user_photos list without a loaded 'photo'.
// It fails to show in media overview if you try to open it.
//
//if (mtpIsTrue(d.vprevious()) || !user->userpicPhotoId()) {
session().storage().remove(Storage::UserPhotosRemoveAfter(
user->bareId(),
user->userpicPhotoId()));
} else {
session().storage().add(Storage::UserPhotosAddNew(
user->bareId(),
user->userpicPhotoId()));
}
//} else {
// session().storage().add(Storage::UserPhotosAddNew(
// user->bareId(),
// user->userpicPhotoId()));
//}
}
} break;

View File

@@ -8,10 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_hash.h"
#include "api/api_media.h"
#include "api/api_sending.h"
#include "api/api_text_entities.h"
#include "api/api_self_destruct.h"
#include "api/api_sensitive_content.h"
#include "api/api_global_privacy.h"
#include "api/api_updates.h"
#include "data/stickers/data_stickers.h"
#include "data/data_drafts.h"
@@ -111,63 +113,6 @@ using PhotoFileLocationId = Data::PhotoFileLocationId;
using DocumentFileLocationId = Data::DocumentFileLocationId;
using UpdatedFileReferences = Data::UpdatedFileReferences;
MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
not_null<DocumentData*> document) {
const auto filenameAttribute = MTP_documentAttributeFilename(
MTP_string(document->filename()));
const auto dimensions = document->dimensions;
auto attributes = QVector<MTPDocumentAttribute>(1, filenameAttribute);
if (dimensions.width() > 0 && dimensions.height() > 0) {
const auto duration = document->getDuration();
if (duration >= 0 && !document->hasMimeType(qstr("image/gif"))) {
auto flags = MTPDdocumentAttributeVideo::Flags(0);
if (document->isVideoMessage()) {
flags |= MTPDdocumentAttributeVideo::Flag::f_round_message;
}
if (document->supportsStreaming()) {
flags |= MTPDdocumentAttributeVideo::Flag::f_supports_streaming;
}
attributes.push_back(MTP_documentAttributeVideo(
MTP_flags(flags),
MTP_int(duration),
MTP_int(dimensions.width()),
MTP_int(dimensions.height())));
} else {
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(dimensions.width()),
MTP_int(dimensions.height())));
}
}
if (document->type == AnimatedDocument) {
attributes.push_back(MTP_documentAttributeAnimated());
} else if (document->type == StickerDocument && document->sticker()) {
attributes.push_back(MTP_documentAttributeSticker(
MTP_flags(0),
MTP_string(document->sticker()->alt),
document->sticker()->set,
MTPMaskCoords()));
} else if (const auto song = document->song()) {
const auto flags = MTPDdocumentAttributeAudio::Flag::f_title
| MTPDdocumentAttributeAudio::Flag::f_performer;
attributes.push_back(MTP_documentAttributeAudio(
MTP_flags(flags),
MTP_int(song->duration),
MTP_string(song->title),
MTP_string(song->performer),
MTPstring()));
} else if (const auto voice = document->voice()) {
const auto flags = MTPDdocumentAttributeAudio::Flag::f_voice
| MTPDdocumentAttributeAudio::Flag::f_waveform;
attributes.push_back(MTP_documentAttributeAudio(
MTP_flags(flags),
MTP_int(voice->duration),
MTPstring(),
MTPstring(),
MTP_bytes(documentWaveformEncode5bit(voice->waveform))));
}
return MTP_vector<MTPDocumentAttribute>(attributes);
}
} // namespace
MTPInputPrivacyKey ApiWrap::Privacy::Input(Key key) {
@@ -242,7 +187,8 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
, _topPromotionTimer([=] { refreshTopPromotion(); })
, _updateNotifySettingsTimer([=] { sendNotifySettingsUpdates(); })
, _selfDestruct(std::make_unique<Api::SelfDestruct>(this))
, _sensitiveContent(std::make_unique<Api::SensitiveContent>(this)) {
, _sensitiveContent(std::make_unique<Api::SensitiveContent>(this))
, _globalPrivacy(std::make_unique<Api::GlobalPrivacy>(this)) {
crl::on_main(session, [=] {
// You can't use _session->lifetime() in the constructor,
// only queued, because it is not constructed yet.
@@ -1701,7 +1647,7 @@ void ApiWrap::requestSelfParticipant(not_null<ChannelData*> channel) {
}).fail([=](const RPCError &error) {
_selfParticipantRequests.erase(channel);
if (error.type() == qstr("CHANNEL_PRIVATE")) {
channel->markForbidden();
channel->privateErrorReceived();
}
finalize(-1, 0);
}).afterDelay(kSmallDelayMs).send();
@@ -2018,6 +1964,9 @@ void ApiWrap::joinChannel(not_null<ChannelData*> channel) {
applyUpdates(result);
}).fail([=](const RPCError &error) {
if (error.type() == qstr("CHANNEL_PRIVATE")
&& channel->invitePeekExpires()) {
channel->privateErrorReceived();
} else if (error.type() == qstr("CHANNEL_PRIVATE")
|| error.type() == qstr("CHANNEL_PUBLIC_GROUP_NA")
|| error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
Ui::show(Box<InformBox>(channel->isMegagroup()
@@ -3959,8 +3908,10 @@ void ApiWrap::userPhotosDone(
//}
void ApiWrap::sendAction(const SendAction &action) {
_session->data().histories().readInbox(action.history);
action.history->getReadyFor(ShowAtTheEndMsgId);
if (!action.options.scheduled) {
_session->data().histories().readInbox(action.history);
action.history->getReadyFor(ShowAtTheEndMsgId);
}
_sendActions.fire_copy(action);
}
@@ -3980,7 +3931,9 @@ void ApiWrap::finishForwarding(const SendAction &action) {
_session->data().sendHistoryChangeNotifications();
_session->changes().historyUpdated(
history,
Data::HistoryUpdate::Flag::MessageSent);
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
}
void ApiWrap::forwardMessages(
@@ -4220,7 +4173,9 @@ void ApiWrap::sendSharedContact(
_session->data().sendHistoryChangeNotifications();
_session->changes().historyUpdated(
history,
Data::HistoryUpdate::Flag::MessageSent);
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
}
void ApiWrap::sendVoiceMessage(
@@ -4339,11 +4294,7 @@ void ApiWrap::sendUploadedPhoto(
const MTPInputFile &file,
Api::SendOptions options) {
if (const auto item = _session->data().message(localId)) {
const auto media = MTP_inputMediaUploadedPhoto(
MTP_flags(0),
file,
MTPVector<MTPInputDocument>(),
MTP_int(0));
const auto media = Api::PrepareUploadedPhoto(file);
if (const auto groupId = item->groupId()) {
uploadAlbumMedia(item, groupId, media);
} else {
@@ -4358,125 +4309,17 @@ void ApiWrap::sendUploadedDocument(
const std::optional<MTPInputFile> &thumb,
Api::SendOptions options) {
if (const auto item = _session->data().message(localId)) {
auto media = item->media();
if (auto document = media ? media->document() : nullptr) {
const auto groupId = item->groupId();
const auto flags = MTPDinputMediaUploadedDocument::Flags(0)
| (thumb
? MTPDinputMediaUploadedDocument::Flag::f_thumb
: MTPDinputMediaUploadedDocument::Flag(0))
| (groupId
? MTPDinputMediaUploadedDocument::Flag::f_nosound_video
: MTPDinputMediaUploadedDocument::Flag(0));
const auto media = MTP_inputMediaUploadedDocument(
MTP_flags(flags),
file,
thumb ? *thumb : MTPInputFile(),
MTP_string(document->mimeString()),
ComposeSendingDocumentAttributes(document),
MTPVector<MTPInputDocument>(),
MTP_int(0));
if (groupId) {
uploadAlbumMedia(item, groupId, media);
} else {
sendMedia(item, media, options);
}
if (!item->media() || !item->media()->document()) {
return;
}
}
}
void ApiWrap::editUploadedFile(
FullMsgId localId,
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb,
Api::SendOptions options,
bool isDocument) {
const auto item = _session->data().message(localId);
if (!item) {
return;
}
if (!item->media()) {
return;
}
auto sentEntities = Api::EntitiesToMTP(
_session,
item->originalText().entities,
Api::ConvertOption::SkipLocal);
auto flagsEditMsg = MTPmessages_EditMessage::Flag::f_message | 0;
flagsEditMsg |= MTPmessages_EditMessage::Flag::f_no_webpage;
flagsEditMsg |= MTPmessages_EditMessage::Flag::f_entities;
flagsEditMsg |= MTPmessages_EditMessage::Flag::f_media;
const auto media = [&]() -> std::optional<MTPInputMedia> {
if (!isDocument) {
if (!item->media()->photo()) {
return std::nullopt;
}
return MTP_inputMediaUploadedPhoto(
MTP_flags(0),
file,
MTPVector<MTPInputDocument>(),
MTP_int(0));
}
const auto document = item->media()->document();
if (!document) {
return std::nullopt;
}
const auto flags = MTPDinputMediaUploadedDocument::Flags(0)
| (thumb
? MTPDinputMediaUploadedDocument::Flag::f_thumb
: MTPDinputMediaUploadedDocument::Flag(0))
| (item->groupId()
? MTPDinputMediaUploadedDocument::Flag::f_nosound_video
: MTPDinputMediaUploadedDocument::Flag(0));
return MTP_inputMediaUploadedDocument(
MTP_flags(flags),
file,
thumb ? *thumb : MTPInputFile(),
MTP_string(document->mimeString()),
ComposeSendingDocumentAttributes(document),
MTPVector<MTPInputDocument>(),
MTP_int(0));
}();
if (!media) {
return;
}
const auto peer = item->history()->peer;
request(MTPmessages_EditMessage(
MTP_flags(flagsEditMsg),
peer->input,
MTP_int(item->id),
MTP_string(item->originalText().text),
*media,
MTPReplyMarkup(),
sentEntities,
MTP_int(0) // schedule_date
)).done([=](const MTPUpdates &result) {
item->clearSavedMedia();
item->setIsLocalUpdateMedia(true);
applyUpdates(result);
item->setIsLocalUpdateMedia(false);
}).fail([=](const RPCError &error) {
QString err = error.type();
if (err == qstr("MESSAGE_NOT_MODIFIED")) {
item->returnSavedMedia();
_session->data().sendHistoryChangeNotifications();
} else if (err == qstr("MEDIA_NEW_INVALID")) {
item->returnSavedMedia();
_session->data().sendHistoryChangeNotifications();
Ui::show(
Box<InformBox>(tr::lng_edit_media_invalid_file(tr::now)),
Ui::LayerOption::KeepOther);
const auto media = Api::PrepareUploadedDocument(item, file, thumb);
const auto groupId = item->groupId();
if (groupId) {
uploadAlbumMedia(item, groupId, media);
} else {
sendMessageFail(error, peer);
sendMedia(item, media, options);
}
}).send();
}
}
void ApiWrap::cancelLocalItem(not_null<HistoryItem*> item) {
@@ -5036,7 +4879,10 @@ void ApiWrap::photoUploadReady(
};
if (peer->isSelf()) {
request(MTPphotos_UploadProfilePhoto(
file
MTP_flags(MTPphotos_UploadProfilePhoto::Flag::f_file),
file,
MTPInputFile(), // video
MTPdouble() // video_start_ts
)).done([=](const MTPphotos_Photo &result) {
result.match([&](const MTPDphotos_photo &data) {
_session->data().processPhoto(data.vphoto());
@@ -5047,13 +4893,21 @@ void ApiWrap::photoUploadReady(
const auto history = _session->data().history(chat);
history->sendRequestId = request(MTPmessages_EditChatPhoto(
chat->inputChat,
MTP_inputChatUploadedPhoto(file)
MTP_inputChatUploadedPhoto(
MTP_flags(MTPDinputChatUploadedPhoto::Flag::f_file),
file,
MTPInputFile(), // video
MTPdouble()) // video_start_ts
)).done(applier).afterRequest(history->sendRequestId).send();
} else if (const auto channel = peer->asChannel()) {
const auto history = _session->data().history(channel);
history->sendRequestId = request(MTPchannels_EditPhoto(
channel->inputChannel,
MTP_inputChatUploadedPhoto(file)
MTP_inputChatUploadedPhoto(
MTP_flags(MTPDinputChatUploadedPhoto::Flag::f_file),
file,
MTPInputFile(), // video
MTPdouble()) // video_start_ts
)).done(applier).afterRequest(history->sendRequestId).send();
}
}
@@ -5389,6 +5243,10 @@ Api::SensitiveContent &ApiWrap::sensitiveContent() {
return *_sensitiveContent;
}
Api::GlobalPrivacy &ApiWrap::globalPrivacy() {
return *_globalPrivacy;
}
void ApiWrap::createPoll(
const PollData &data,
const SendAction &action,
@@ -5519,44 +5377,6 @@ void ApiWrap::closePoll(not_null<HistoryItem*> item) {
_pollCloseRequestIds.emplace(itemId, requestId);
}
void ApiWrap::rescheduleMessage(
not_null<HistoryItem*> item,
Api::SendOptions options) {
const auto text = item->originalText().text;
const auto sentEntities = Api::EntitiesToMTP(
_session,
item->originalText().entities,
Api::ConvertOption::SkipLocal);
const auto media = item->media();
const auto emptyFlag = MTPmessages_EditMessage::Flag(0);
const auto flags = MTPmessages_EditMessage::Flag::f_schedule_date
| (!text.isEmpty()
? MTPmessages_EditMessage::Flag::f_message
: emptyFlag)
| ((!media || !media->webpage())
? MTPmessages_EditMessage::Flag::f_no_webpage
: emptyFlag)
| (!sentEntities.v.isEmpty()
? MTPmessages_EditMessage::Flag::f_entities
: emptyFlag);
const auto id = _session->data().scheduledMessages().lookupId(item);
request(MTPmessages_EditMessage(
MTP_flags(flags),
item->history()->peer->input,
MTP_int(id),
MTP_string(text),
MTPInputMedia(),
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled)
)).done([=](const MTPUpdates &result) {
applyUpdates(result);
}).fail([](const RPCError &error) {
}).send();
}
void ApiWrap::reloadPollResults(not_null<HistoryItem*> item) {
const auto itemId = item->fullId();
if (!IsServerMsgId(item->id)

View File

@@ -50,6 +50,12 @@ struct CloudPasswordState;
} // namespace Core
namespace Api {
class Updates;
class SelfDestruct;
class SensitiveContent;
class GlobalPrivacy;
namespace details {
inline QString ToString(const QString &value) {
@@ -66,8 +72,6 @@ inline QString ToString(uint64 value) {
} // namespace details
class Updates;
template <
typename ...Types,
typename = std::enable_if_t<(sizeof...(Types) > 0)>>
@@ -86,9 +90,6 @@ QString RequestKey(Types &&...values) {
return result;
}
class SelfDestruct;
class SensitiveContent;
} // namespace Api
class ApiWrap : public MTP::Sender, private base::Subscriber {
@@ -422,12 +423,6 @@ public:
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb,
Api::SendOptions options);
void editUploadedFile(
FullMsgId localId,
const MTPInputFile &file,
const std::optional<MTPInputFile> &thumb,
Api::SendOptions options,
bool isDocument);
void cancelLocalItem(not_null<HistoryItem*> item);
@@ -466,6 +461,7 @@ public:
[[nodiscard]] Api::SelfDestruct &selfDestruct();
[[nodiscard]] Api::SensitiveContent &sensitiveContent();
[[nodiscard]] Api::GlobalPrivacy &globalPrivacy();
void createPoll(
const PollData &data,
@@ -478,10 +474,6 @@ public:
void closePoll(not_null<HistoryItem*> item);
void reloadPollResults(not_null<HistoryItem*> item);
void rescheduleMessage(
not_null<HistoryItem*> item,
Api::SendOptions options);
private:
struct MessageDataRequest {
using Callbacks = QList<RequestMessageDataCallback>;
@@ -831,6 +823,7 @@ private:
const std::unique_ptr<Api::SelfDestruct> _selfDestruct;
const std::unique_ptr<Api::SensitiveContent> _sensitiveContent;
const std::unique_ptr<Api::GlobalPrivacy> _globalPrivacy;
base::flat_map<FullMsgId, mtpRequestId> _pollVotesRequestIds;
base::flat_map<FullMsgId, mtpRequestId> _pollCloseRequestIds;

View File

@@ -94,7 +94,6 @@ confirmInviteTitle: FlatLabel(defaultFlatLabel) {
confirmInviteStatus: FlatLabel(boxLabel) {
align: align(center);
minWidth: 320px;
maxHeight: 20px;
textFg: windowSubTextFg;
}
confirmInviteTitleTop: 106px;

View File

@@ -861,146 +861,6 @@ void DeleteMessagesBox::deleteAndClear() {
session->data().sendHistoryChangeNotifications();
}
ConfirmInviteBox::ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
Fn<void()> submit)
: _session(session)
, _submit(std::move(submit))
, _title(this, st::confirmInviteTitle)
, _status(this, st::confirmInviteStatus)
, _participants(GetParticipants(_session, data))
, _isChannel(data.is_channel() && !data.is_megagroup()) {
const auto title = qs(data.vtitle());
const auto count = data.vparticipants_count().v;
const auto status = [&] {
return (!_participants.empty() && _participants.size() < count)
? tr::lng_group_invite_members(tr::now, lt_count, count)
: (count > 0)
? tr::lng_chat_status_members(tr::now, lt_count_decimal, count)
: _isChannel
? tr::lng_channel_status(tr::now)
: tr::lng_group_status(tr::now);
}();
_title->setText(title);
_status->setText(status);
const auto photo = _session->data().processPhoto(data.vphoto());
if (!photo->isNull()) {
_photo = photo->createMediaView();
_photo->wanted(Data::PhotoSize::Small, Data::FileOrigin());
if (!_photo->image(Data::PhotoSize::Small)) {
_session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
update();
}, lifetime());
}
} else {
_photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Data::PeerUserpicColor(0),
title);
}
}
auto ConfirmInviteBox::GetParticipants(
not_null<Main::Session*> session,
const MTPDchatInvite &data)
-> std::vector<Participant> {
const auto participants = data.vparticipants();
if (!participants) {
return {};
}
const auto &v = participants->v;
auto result = std::vector<Participant>();
result.reserve(v.size());
for (const auto &participant : v) {
if (const auto user = session->data().processUser(participant)) {
result.push_back(Participant{ user });
}
}
return result;
}
void ConfirmInviteBox::prepare() {
addButton(
(_isChannel
? tr::lng_profile_join_channel()
: tr::lng_profile_join_group()),
_submit);
addButton(tr::lng_cancel(), [=] { closeBox(); });
while (_participants.size() > 4) {
_participants.pop_back();
}
auto newHeight = st::confirmInviteStatusTop + _status->height() + st::boxPadding.bottom();
if (!_participants.empty()) {
int skip = (st::boxWideWidth - 4 * st::confirmInviteUserPhotoSize) / 5;
int padding = skip / 2;
_userWidth = (st::confirmInviteUserPhotoSize + 2 * padding);
int sumWidth = _participants.size() * _userWidth;
int left = (st::boxWideWidth - sumWidth) / 2;
for (const auto &participant : _participants) {
auto name = new Ui::FlatLabel(this, st::confirmInviteUserName);
name->resizeToWidth(st::confirmInviteUserPhotoSize + padding);
name->setText(participant.user->firstName.isEmpty()
? participant.user->name
: participant.user->firstName);
name->moveToLeft(left + (padding / 2), st::confirmInviteUserNameTop);
left += _userWidth;
}
newHeight += st::confirmInviteUserHeight;
}
setDimensions(st::boxWideWidth, newHeight);
}
void ConfirmInviteBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_title->move((width() - _title->width()) / 2, st::confirmInviteTitleTop);
_status->move((width() - _status->width()) / 2, st::confirmInviteStatusTop);
}
void ConfirmInviteBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
Painter p(this);
if (_photo) {
if (const auto image = _photo->image(Data::PhotoSize::Small)) {
p.drawPixmap(
(width() - st::confirmInvitePhotoSize) / 2,
st::confirmInvitePhotoTop,
image->pixCircled(
st::confirmInvitePhotoSize,
st::confirmInvitePhotoSize));
}
} else if (_photoEmpty) {
_photoEmpty->paint(
p,
(width() - st::confirmInvitePhotoSize) / 2,
st::confirmInvitePhotoTop,
width(),
st::confirmInvitePhotoSize);
}
int sumWidth = _participants.size() * _userWidth;
int left = (width() - sumWidth) / 2;
for (auto &participant : _participants) {
participant.user->paintUserpicLeft(
p,
participant.userpic,
left + (_userWidth - st::confirmInviteUserPhotoSize) / 2,
st::confirmInviteUserPhotoTop,
width(),
st::confirmInviteUserPhotoSize);
left += _userWidth;
}
}
ConfirmInviteBox::~ConfirmInviteBox() = default;
ConfirmDontWarnBox::ConfirmDontWarnBox(
QWidget*,
rpl::producer<TextWithEntities> text,

View File

@@ -206,46 +206,6 @@ private:
};
class ConfirmInviteBox final
: public Ui::BoxContent
, private base::Subscriber {
public:
ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
Fn<void()> submit);
~ConfirmInviteBox();
protected:
void prepare() override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
struct Participant {
not_null<UserData*> user;
std::shared_ptr<Data::CloudImageView> userpic;
};
static std::vector<Participant> GetParticipants(
not_null<Main::Session*> session,
const MTPDchatInvite &data);
const not_null<Main::Session*> _session;
Fn<void()> _submit;
object_ptr<Ui::FlatLabel> _title;
object_ptr<Ui::FlatLabel> _status;
std::shared_ptr<Data::PhotoMedia> _photo;
std::unique_ptr<Ui::EmptyUserpic> _photoEmpty;
std::vector<Participant> _participants;
bool _isChannel = false;
int _userWidth = 0;
};
class ConfirmDontWarnBox : public Ui::BoxContent {
public:
ConfirmDontWarnBox(

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/edit_caption_box.h"
#include "apiwrap.h"
#include "api/api_editing.h"
#include "api/api_text_entities.h"
#include "main/main_session.h"
#include "chat_helpers/emoji_suggestions_widget.h"
@@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo_media.h"
#include "data/data_document_media.h"
#include "history/history.h"
#include "history/history_drag_area.h"
#include "history/history_item.h"
#include "platform/platform_specific.h"
#include "lang/lang_keys.h"
@@ -37,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_loader_local.h"
#include "platform/platform_file_utilities.h"
#include "storage/localimageloader.h"
#include "storage/storage_media_prepare.h"
#include "mtproto/mtproto_config.h"
@@ -63,6 +66,50 @@ namespace {
using namespace ::Media::Streaming;
using Data::PhotoSize;
auto ListFromMimeData(not_null<const QMimeData*> data) {
using Error = Storage::PreparedList::Error;
auto result = data->hasUrls()
? Storage::PrepareMediaList(
// When we edit media, we need only 1 file.
data->urls().mid(0, 1),
st::sendMediaPreviewSize)
: Storage::PreparedList(Error::EmptyFile, QString());
if (result.error == Error::None) {
return result;
} else if (data->hasImage()) {
auto image = Platform::GetImageFromClipboard();
if (image.isNull()) {
image = qvariant_cast<QImage>(data->imageData());
}
if (!image.isNull()) {
return Storage::PrepareMediaFromImage(
std::move(image),
QByteArray(),
st::sendMediaPreviewSize);
}
}
return result;
}
auto CheckMimeData(not_null<const QMimeData*> data, bool isAlbum) {
if (data->urls().size() > 1) {
return false;
} else if (data->hasImage()) {
return true;
}
if (isAlbum && data->hasUrls()) {
const auto url = data->urls().front();
if (url.isLocalFile()) {
using namespace Core;
const auto info = QFileInfo(Platform::File::UrlToLocal(url));
return IsMimeAcceptedForAlbum(MimeTypeForFile(info).name());
}
}
return true;
}
} // namespace
EditCaptionBox::EditCaptionBox(
@@ -70,7 +117,6 @@ EditCaptionBox::EditCaptionBox(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item)
: _controller(controller)
, _api(&controller->session().mtp())
, _msgId(item->fullId()) {
Expects(item->media() != nullptr);
Expects(item->media()->allowsEditCaption());
@@ -617,12 +663,8 @@ void EditCaptionBox::prepare() {
if (action == Ui::InputField::MimeAction::Check) {
if (!data->hasText() && !_isAllowedEditMedia) {
return false;
} else if (data->hasImage()) {
} else if (CheckMimeData(data, _isAlbum)) {
return true;
} else if (const auto urls = data->urls(); !urls.empty()) {
if (ranges::all_of(urls, &QUrl::isLocalFile)) {
return true;
}
}
return data->hasText();
} else if (action == Ui::InputField::MimeAction::Insert) {
@@ -640,6 +682,8 @@ void EditCaptionBox::prepare() {
auto cursor = _field->textCursor();
cursor.movePosition(QTextCursor::End);
_field->setTextCursor(cursor);
setupDragArea();
}
bool EditCaptionBox::fileFromClipboard(not_null<const QMimeData*> data) {
@@ -647,50 +691,34 @@ bool EditCaptionBox::fileFromClipboard(not_null<const QMimeData*> data) {
return false;
}
using Error = Storage::PreparedList::Error;
using AlbumType = Storage::PreparedFile::AlbumType;
auto list = ListFromMimeData(data);
auto list = [&] {
auto url = QList<QUrl>();
auto canAddUrl = false;
// When we edit media, we need only 1 file.
if (data->hasUrls()) {
const auto first = data->urls().front();
url.push_front(first);
canAddUrl = first.isLocalFile();
}
auto result = canAddUrl
? Storage::PrepareMediaList(url, st::sendMediaPreviewSize)
: Storage::PreparedList(
Error::EmptyFile,
QString());
if (result.error == Error::None) {
return result;
} else if (data->hasImage()) {
auto image = Platform::GetImageFromClipboard();
if (image.isNull()) {
image = qvariant_cast<QImage>(data->imageData());
}
if (!image.isNull()) {
_isImage = true;
_photo = true;
return Storage::PrepareMediaFromImage(
std::move(image),
QByteArray(),
st::sendMediaPreviewSize);
}
}
return result;
}();
if (list.error != Error::None || list.files.empty()) {
return false;
}
if (list.files.front().type == Storage::PreparedFile::AlbumType::None
&& _isAlbum) {
Ui::show(
Box<InformBox>(tr::lng_edit_media_album_error(tr::now)),
Ui::LayerOption::KeepOther);
const auto file = &list.files.front();
if (_isAlbum && (file->type == AlbumType::None)) {
const auto imageAsDoc = [&] {
using Info = FileMediaInformation;
const auto fileMedia = &file->information->media;
if (const auto image = base::get_if<Info::Image>(fileMedia)) {
return !Storage::ValidateThumbDimensions(
image->data.width(),
image->data.height());
}
return false;
}();
if (!data->hasText() || imageAsDoc) {
Ui::show(
Box<InformBox>(tr::lng_edit_media_album_error(tr::now)),
Ui::LayerOption::KeepOther);
}
return false;
}
_photo = _isImage = (file->type == AlbumType::Photo);
_preparedList = std::move(list);
updateEditPreview();
return true;
@@ -735,6 +763,35 @@ void EditCaptionBox::setupEmojiPanel() {
});
}
void EditCaptionBox::setupDragArea() {
auto enterFilter = [=](not_null<const QMimeData*> data) {
return !_isAllowedEditMedia ? false : CheckMimeData(data, _isAlbum);
};
// Avoid both drag areas appearing at one time.
auto computeState = [=](const QMimeData *data) {
const auto state = Storage::ComputeMimeDataState(data);
return (state == Storage::MimeDataState::PhotoFiles)
? Storage::MimeDataState::Image
: state;
};
const auto areas = DragArea::SetupDragAreaToContainer(
this,
std::move(enterFilter),
[=](bool f) { _field->setAcceptDrops(f); },
nullptr,
std::move(computeState));
const auto droppedCallback = [=](bool compress) {
return [=](const QMimeData *data) {
fileFromClipboard(data);
Window::ActivateWindow(_controller);
};
};
areas.document->setDroppedCallback(droppedCallback(false));
areas.photo->setDroppedCallback(droppedCallback(true));
}
void EditCaptionBox::updateBoxSize() {
auto newHeight = st::boxPhotoPadding.top() + st::boxPhotoCaptionSkip + _field->height() + errorTopSkip() + st::normalFont->height;
if (_photo) {
@@ -759,8 +816,7 @@ int EditCaptionBox::errorTopSkip() const {
void EditCaptionBox::checkStreamedIsStarted() {
if (!_streamed) {
return;
}
if (_streamed->paused()) {
} else if (_streamed->paused()) {
_streamed->resume();
}
if (!_streamed->active() && !_streamed->failed()) {
@@ -930,88 +986,60 @@ void EditCaptionBox::save() {
return;
}
auto flags = MTPmessages_EditMessage::Flag::f_message | 0;
if (_previewCancelled) {
flags |= MTPmessages_EditMessage::Flag::f_no_webpage;
}
const auto textWithTags = _field->getTextWithAppliedMarkdown();
auto sending = TextWithEntities{
const auto sending = TextWithEntities{
textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
};
const auto prepareFlags = Ui::ItemTextOptions(
item->history(),
_controller->session().user()).flags;
TextUtilities::PrepareForSending(sending, prepareFlags);
TextUtilities::Trim(sending);
const auto sentEntities = Api::EntitiesToMTP(
&item->history()->session(),
sending.entities,
Api::ConvertOption::SkipLocal);
if (!sentEntities.v.isEmpty()) {
flags |= MTPmessages_EditMessage::Flag::f_entities;
}
auto options = Api::SendOptions();
options.scheduled = item->isScheduled() ? item->date() : 0;
if (!_preparedList.files.empty()) {
const auto textWithTags = _field->getTextWithAppliedMarkdown();
auto sending = TextWithEntities{
textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
};
item->setText(sending);
auto action = Api::SendAction(item->history());
action.options = options;
_controller->session().api().editMedia(
std::move(_preparedList),
(!_asFile && _photo) ? SendMediaType::Photo : SendMediaType::File,
_field->getTextWithAppliedMarkdown(),
Api::SendAction(item->history()),
action,
item->fullId().msg);
closeBox();
return;
}
_saveRequestId = _api.request(MTPmessages_EditMessage(
MTP_flags(flags),
item->history()->peer->input,
MTP_int(item->id),
MTP_string(sending.text),
MTPInputMedia(),
MTPReplyMarkup(),
sentEntities,
MTP_int(0)
)).done([=](const MTPUpdates &result) {
saveDone(result);
}).fail([=](const RPCError &error) {
saveFail(error);
}).send();
}
void EditCaptionBox::saveDone(const MTPUpdates &updates) {
_saveRequestId = 0;
const auto controller = _controller;
closeBox();
controller->session().api().applyUpdates(updates);
}
void EditCaptionBox::saveFail(const RPCError &error) {
_saveRequestId = 0;
const auto &type = error.type();
if (type == qstr("MESSAGE_ID_INVALID")
|| type == qstr("CHAT_ADMIN_REQUIRED")
|| type == qstr("MESSAGE_EDIT_TIME_EXPIRED")) {
_error = tr::lng_edit_error(tr::now);
update();
} else if (type == qstr("MESSAGE_NOT_MODIFIED")) {
const auto done = crl::guard(this, [=](const MTPUpdates &updates) {
_saveRequestId = 0;
closeBox();
} else if (type == qstr("MESSAGE_EMPTY")) {
_field->setFocus();
_field->showError();
update();
} else {
_error = tr::lng_edit_error(tr::now);
update();
}
});
const auto fail = crl::guard(this, [=](const RPCError &error) {
_saveRequestId = 0;
const auto &type = error.type();
if (ranges::contains(Api::kDefaultEditMessagesErrors, type)) {
_error = tr::lng_edit_error(tr::now);
update();
} else if (type == u"MESSAGE_NOT_MODIFIED"_q) {
closeBox();
} else if (type == u"MESSAGE_EMPTY"_q) {
_field->setFocus();
_field->showError();
update();
} else {
_error = tr::lng_edit_error(tr::now);
update();
}
});
lifetime().add([=] {
if (_saveRequestId) {
auto &session = _controller->session();
session.api().request(base::take(_saveRequestId)).cancel();
}
});
_saveRequestId = Api::EditCaption(item, sending, options, done, fail);
}
void EditCaptionBox::setName(QString nameString, qint64 size) {

View File

@@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/abstract_box.h"
#include "storage/storage_media_prepare.h"
#include "ui/wrap/slide_wrap.h"
#include "mtproto/sender.h"
class Image;
@@ -80,12 +79,11 @@ private:
void updateEmojiPanelGeometry();
void emojiFilterForGeometry(not_null<QEvent*> event);
void setupDragArea();
void save();
void captionResized();
void saveDone(const MTPUpdates &updates);
void saveFail(const RPCError &error);
void setName(QString nameString, qint64 size);
bool fileFromClipboard(not_null<const QMimeData*> data);
void updateEditPreview();
@@ -102,7 +100,6 @@ private:
}
const not_null<Window::SessionController*> _controller;
MTP::Sender _api;
FullMsgId _msgId;
std::shared_ptr<Data::PhotoMedia> _photoMedia;
@@ -135,7 +132,6 @@ private:
Storage::PreparedList _preparedList;
bool _previewCancelled = false;
mtpRequestId _saveRequestId = 0;
object_ptr<Ui::IconButton> _editMedia = nullptr;

View File

@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "confirm_box.h"
#include "history/history_drag_area.h"
#include "history/view/history_view_schedule_box.h"
#include "core/file_utilities.h"
#include "core/mime_type.h"
@@ -333,7 +334,7 @@ AlbumThumb::AlbumThumb(
- st::sendMediaFileThumbSize
// Right buttons.
- st::sendBoxAlbumGroupButtonFile.width * 2
- st::sendBoxAlbumGroupEditInternalSkip
- st::sendBoxAlbumGroupEditInternalSkip * 2
- st::sendBoxAlbumGroupSkipRight;
const auto filepath = file.path;
if (filepath.isEmpty()) {
@@ -1858,6 +1859,40 @@ void SendFilesBox::prepare() {
}));
updateLeftButtonVisibility();
setupDragArea();
}
void SendFilesBox::setupDragArea() {
// Avoid both drag areas appearing at one time.
auto computeState = [=](const QMimeData *data) {
const auto state = Storage::ComputeMimeDataState(data);
return (state == Storage::MimeDataState::PhotoFiles)
? Storage::MimeDataState::Image
: (state == Storage::MimeDataState::Files)
// Temporary enable drag'n'drop only for images. TODO.
? Storage::MimeDataState::None
: state;
};
const auto areas = DragArea::SetupDragAreaToContainer(
this,
[=](not_null<const QMimeData*> d) { return canAddFiles(d); },
[=](bool f) { _caption->setAcceptDrops(f); },
[=] { updateControlsGeometry(); },
std::move(computeState));
const auto droppedCallback = [=](bool compress) {
return [=](const QMimeData *data) {
addFiles(data);
Window::ActivateWindow(_controller);
};
};
areas.document->setDroppedCallback(droppedCallback(false));
areas.photo->setDroppedCallback(droppedCallback(true));
_albumChanged.events(
) | rpl::start_with_next([=] {
areas.document->raise();
areas.photo->raise();
}, lifetime());
}
void SendFilesBox::updateLeftButtonVisibility() {
@@ -1875,6 +1910,7 @@ void SendFilesBox::refreshAllAfterAlbumChanges() {
preparePreview();
captionResized();
updateLeftButtonVisibility();
_albumChanged.fire({});
}
void SendFilesBox::openDialogToAddFileToAlbum() {

View File

@@ -115,6 +115,7 @@ private:
void sendScheduled();
void captionResized();
void setupDragArea();
void setupTitleText();
void updateBoxSize();
void updateControlsGeometry();
@@ -163,6 +164,7 @@ private:
std::shared_ptr<Ui::RadioenumGroup<SendFilesWay>> _sendWay;
rpl::variable<int> _footerHeight = 0;
rpl::event_stream<> _albumChanged;
QWidget *_preview = nullptr;
AlbumPreview *_albumPreview = nullptr;

View File

@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/local_url_handlers.h"
#include "core/launcher.h"
#include "core/ui_integration.h"
#include "core/core_settings.h"
#include "chat_helpers/emoji_keywords.h"
#include "chat_helpers/stickers_emoji_image_loader.h"
#include "base/platform/base_platform_info.h"
@@ -204,6 +205,8 @@ void Application::run() {
return;
}
Core::App().settings().setWindowControlsLayout(Platform::WindowControlsLayout());
_translator = std::make_unique<Lang::Translator>();
QCoreApplication::instance()->installTranslator(_translator.get());
@@ -211,6 +214,7 @@ void Application::run() {
Ui::InitTextOptions();
Ui::Emoji::Init();
startEmojiImageLoader();
startSystemDarkModeViewer();
Media::Player::start(_audio.get());
style::ShortAnimationPlaying(
@@ -232,6 +236,7 @@ void Application::run() {
QMimeDatabase().mimeTypeForName(qsl("text/plain"));
_window = std::make_unique<Window::Controller>();
_domain->activeChanges(
) | rpl::start_with_next([=](not_null<Main::Account*> account) {
_window->showAccount(account);
@@ -248,16 +253,8 @@ void Application::run() {
// Depend on activeWindow() for now :(
startShortcuts();
App::initMedia();
const auto state = _domain->start(QByteArray());
if (state == Storage::StartResult::IncorrectPasscode) {
Global::SetLocalPasscode(true);
Global::RefLocalPasscodeChanged().notify();
lockByPasscode();
DEBUG_LOG(("Application Info: passcode needed..."));
}
startDomain();
_window->widget()->show();
@@ -279,6 +276,50 @@ void Application::run() {
}
}
void Application::startDomain() {
const auto state = _domain->start(QByteArray());
if (state != Storage::StartResult::IncorrectPasscodeLegacy) {
// In case of non-legacy passcoded app all global settings are ready.
startSettingsAndBackground();
}
if (state != Storage::StartResult::Success) {
Global::SetLocalPasscode(true);
Global::RefLocalPasscodeChanged().notify();
lockByPasscode();
DEBUG_LOG(("Application Info: passcode needed..."));
}
}
void Application::startSettingsAndBackground() {
Local::rewriteSettingsIfNeeded();
Window::Theme::Background()->start();
checkSystemDarkMode();
}
void Application::checkSystemDarkMode() {
const auto maybeDarkMode = _settings.systemDarkMode();
const auto darkModeEnabled = _settings.systemDarkModeEnabled();
const auto needToSwitch = darkModeEnabled
&& maybeDarkMode
&& (*maybeDarkMode != Window::Theme::IsNightMode());
if (needToSwitch) {
Window::Theme::ToggleNightMode();
Window::Theme::KeepApplied();
}
}
void Application::startSystemDarkModeViewer() {
if (Window::Theme::Background()->editingTheme()) {
_settings.setSystemDarkModeEnabled(false);
}
rpl::merge(
_settings.systemDarkModeChanges() | rpl::to_empty,
_settings.systemDarkModeEnabledChanges() | rpl::to_empty
) | rpl::start_with_next([=] {
checkSystemDarkMode();
}, _lifetime);
}
auto Application::prepareEmojiSourceImages()
-> std::shared_ptr<Ui::Emoji::UniversalImages> {
const auto &images = Ui::Emoji::SourceImages();

View File

@@ -140,6 +140,7 @@ public:
[[nodiscard]] QWidget *getFileDialogParent();
void notifyFileDialogShown(bool shown);
[[nodiscard]] QWidget *getModalParent();
void checkSystemDarkMode();
// Media view interface.
void checkMediaViewActivation();
@@ -161,6 +162,7 @@ public:
return _logoNoMargin;
}
void startSettingsAndBackground();
[[nodiscard]] Settings &settings() {
return _settings;
}
@@ -290,7 +292,9 @@ private:
-> std::shared_ptr<Ui::Emoji::UniversalImages>;
void startLocalStorage();
void startShortcuts();
void startDomain();
void startEmojiImageLoader();
void startSystemDarkModeViewer();
void stateChanged(Qt::ApplicationState state);

View File

@@ -85,6 +85,16 @@ std::map<int, const char*> BetaLogs() {
"- Passcode doesn't auto-lock while you're active in other apps on Linux X11."
},
{
2001021,
"- Edit your scheduled messages.\n"
"- See the unread messages indicator for your additional accounts on the main menu button.\n"
"- Use Auto-Night Mode to make Telegram night mode match the system Dark Mode settings.\n"
"- Enjoy dark native window frame for Telegram night mode on Windows.\n"
},
};
};

View File

@@ -106,7 +106,8 @@ QByteArray Settings::serialize() const {
<< qint32(_thirdColumnWidth.current())
<< qint32(_thirdSectionExtendedBy)
<< qint32(_notifyFromAll ? 1 : 0)
<< qint32(_nativeWindowFrame.current() ? 1 : 0);
<< qint32(_nativeWindowFrame.current() ? 1 : 0)
<< qint32(_systemDarkModeEnabled.current() ? 1 : 0);
}
return result;
}
@@ -171,6 +172,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
qint32 thirdSectionExtendedBy = _thirdSectionExtendedBy;
qint32 notifyFromAll = _notifyFromAll ? 1 : 0;
qint32 nativeWindowFrame = _nativeWindowFrame.current() ? 1 : 0;
qint32 systemDarkModeEnabled = _systemDarkModeEnabled.current() ? 1 : 0;
stream >> themesAccentColors;
if (!stream.atEnd()) {
@@ -248,6 +250,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
if (!stream.atEnd()) {
stream >> nativeWindowFrame;
}
if (!stream.atEnd()) {
stream >> systemDarkModeEnabled;
}
if (stream.status() != QDataStream::Ok) {
LOG(("App Error: "
"Bad data for Core::Settings::constructFromSerialized()"));
@@ -341,6 +346,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
}
_notifyFromAll = (notifyFromAll == 1);
_nativeWindowFrame = (nativeWindowFrame == 1);
_systemDarkModeEnabled = (systemDarkModeEnabled == 1);
}
bool Settings::chatWide() const {
@@ -474,6 +480,7 @@ void Settings::resetOnLastLogout() {
_thirdColumnWidth = kDefaultThirdColumnWidth; // p-w
_notifyFromAll = true;
_tabbedReplacedWithInfo = false; // per-window
_systemDarkModeEnabled = false;
}
bool Settings::ThirdColumnByDefault() {

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "window/themes/window_themes_embedded.h"
#include "window/window_controls_layout.h"
enum class SendFilesWay;
enum class RectPart;
@@ -416,6 +417,42 @@ public:
[[nodiscard]] rpl::producer<bool> nativeWindowFrameChanges() const {
return _nativeWindowFrame.changes();
}
void setSystemDarkMode(std::optional<bool> value) {
_systemDarkMode = value;
}
[[nodiscard]] std::optional<bool> systemDarkMode() const {
return _systemDarkMode.current();
}
[[nodiscard]] rpl::producer<std::optional<bool>> systemDarkModeValue() const {
return _systemDarkMode.value();
}
[[nodiscard]] rpl::producer<std::optional<bool>> systemDarkModeChanges() const {
return _systemDarkMode.changes();
}
void setSystemDarkModeEnabled(bool value) {
_systemDarkModeEnabled = value;
}
[[nodiscard]] bool systemDarkModeEnabled() const {
return _systemDarkModeEnabled.current();
}
[[nodiscard]] rpl::producer<bool> systemDarkModeEnabledValue() const {
return _systemDarkModeEnabled.value();
}
[[nodiscard]] rpl::producer<bool> systemDarkModeEnabledChanges() const {
return _systemDarkModeEnabled.changes();
}
void setWindowControlsLayout(Window::ControlsLayout value) {
_windowControlsLayout = value;
}
[[nodiscard]] Window::ControlsLayout windowControlsLayout() const {
return _windowControlsLayout.current();
}
[[nodiscard]] rpl::producer<Window::ControlsLayout> windowControlsLayoutValue() const {
return _windowControlsLayout.value();
}
[[nodiscard]] rpl::producer<Window::ControlsLayout> windowControlsLayoutChanges() const {
return _windowControlsLayout.changes();
}
[[nodiscard]] static bool ThirdColumnByDefault();
[[nodiscard]] float64 DefaultDialogsWidthRatio();
@@ -484,6 +521,9 @@ private:
rpl::variable<int> _thirdColumnWidth = kDefaultThirdColumnWidth; // p-w
bool _notifyFromAll = true;
rpl::variable<bool> _nativeWindowFrame = false;
rpl::variable<std::optional<bool>> _systemDarkMode = std::nullopt;
rpl::variable<bool> _systemDarkModeEnabled = false;
rpl::variable<Window::ControlsLayout> _windowControlsLayout;
bool _tabbedReplacedWithInfo = false; // per-window
rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/local_url_handlers.h"
#include "api/api_text_entities.h"
#include "api/api_chat_invite.h"
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "lang/lang_cloud_manager.h"
@@ -50,31 +51,7 @@ bool JoinGroupByHash(
if (!controller) {
return false;
}
const auto hash = match->captured(1);
const auto session = &controller->session();
const auto weak = base::make_weak(controller);
session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {
Core::App().hideMediaView();
result.match([=](const MTPDchatInvite &data) {
Ui::show(Box<ConfirmInviteBox>(session, data, [=] {
session->api().importChatInvite(hash);
}));
}, [=](const MTPDchatInviteAlready &data) {
if (const auto chat = session->data().processChat(data.vchat())) {
if (const auto strong = weak.get()) {
strong->showPeerHistory(
chat,
Window::SectionShow::Way::Forward);
}
}
});
}, [=](const RPCError &error) {
if (error.code() != 400) {
return;
}
Core::App().hideMediaView();
Ui::show(Box<InformBox>(tr::lng_group_invite_bad_link(tr::now)));
});
Api::CheckChatInvite(controller, match->captured(1));
return true;
}

View File

@@ -111,4 +111,11 @@ bool IsMimeSticker(const QString &mime) {
|| IsMimeStickerAnimated(mime);
}
bool IsMimeAcceptedForAlbum(const QString &mime) {
return (mime == u"image/jpeg"_q)
|| (mime == u"image/png"_q)
|| (mime == u"video/mp4"_q)
|| (mime == u"video/quicktime"_q);
}
} // namespace Core

View File

@@ -41,5 +41,6 @@ MimeType MimeTypeForData(const QByteArray &data);
bool IsMimeStickerAnimated(const QString &mime);
bool IsMimeSticker(const QString &mime);
bool IsMimeAcceptedForAlbum(const QString &mime);
} // namespace Core

View File

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

View File

@@ -309,6 +309,18 @@ bool ShouldAutoPlay(
document->size);
}
bool ShouldAutoPlay(
const Full &data,
not_null<PeerData*> peer,
not_null<PhotoData*> photo) {
const auto source = SourceFromPeer(peer);
const auto size = photo->videoByteSize();
return photo->hasVideo()
&& (data.shouldDownload(source, Type::AutoPlayGIF, size)
|| data.shouldDownload(source, Type::AutoPlayVideo, size)
|| data.shouldDownload(source, Type::AutoPlayVideoMessage, size));
}
Full WithDisabledAutoPlay(const Full &data) {
auto result = data;
for (const auto source : enums_view<Source>(kSourcesCount)) {

View File

@@ -120,6 +120,10 @@ private:
const Full &data,
not_null<PeerData*> peer,
not_null<DocumentData*> document);
[[nodiscard]] bool ShouldAutoPlay(
const Full &data,
not_null<PeerData*> peer,
not_null<PhotoData*> photo);
[[nodiscard]] Full WithDisabledAutoPlay(const Full &data);

View File

@@ -109,12 +109,13 @@ struct HistoryUpdate {
LocalMessages = (1 << 5),
ChatOccupied = (1 << 6),
MessageSent = (1 << 7),
ForwardDraft = (1 << 8),
OutboxRead = (1 << 9),
BotKeyboard = (1 << 10),
CloudDraft = (1 << 11),
ScheduledSent = (1 << 8),
ForwardDraft = (1 << 9),
OutboxRead = (1 << 10),
BotKeyboard = (1 << 11),
CloudDraft = (1 << 12),
LastUsedBit = (1 << 11),
LastUsedBit = (1 << 12),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View File

@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "history/history.h"
#include "main/main_session.h"
#include "api/api_chat_invite.h"
#include "apiwrap.h"
namespace {
@@ -632,6 +633,40 @@ void ChannelData::growSlowmodeLastMessage(TimeId when) {
session().changes().peerUpdated(this, UpdateFlag::Slowmode);
}
void ChannelData::setInvitePeek(const QString &hash, TimeId expires) {
if (!_invitePeek) {
_invitePeek = std::make_unique<InvitePeek>();
}
_invitePeek->hash = hash;
_invitePeek->expires = expires;
}
void ChannelData::clearInvitePeek() {
_invitePeek = nullptr;
}
TimeId ChannelData::invitePeekExpires() const {
return _invitePeek ? _invitePeek->expires : 0;
}
QString ChannelData::invitePeekHash() const {
return _invitePeek ? _invitePeek->hash : QString();
}
void ChannelData::privateErrorReceived() {
if (const auto expires = invitePeekExpires()) {
const auto hash = invitePeekHash();
for (const auto window : session().windows()) {
clearInvitePeek();
Api::CheckChatInvite(window, hash, this);
return;
}
_invitePeek->expires = base::unixtime::now();
} else {
markForbidden();
}
}
namespace Data {
void ApplyMigration(

View File

@@ -386,6 +386,12 @@ public:
[[nodiscard]] TimeId slowmodeLastMessage() const;
void growSlowmodeLastMessage(TimeId when);
void setInvitePeek(const QString &hash, TimeId expires);
void clearInvitePeek();
[[nodiscard]] TimeId invitePeekExpires() const;
[[nodiscard]] QString invitePeekHash() const;
void privateErrorReceived();
// Still public data members.
uint64 access = 0;
@@ -401,6 +407,11 @@ public:
TimeId inviteDate = 0;
private:
struct InvitePeek {
QString hash;
TimeId expires = 0;
};
auto unavailableReasons() const
-> const std::vector<Data::UnavailableReason> & override;
bool canEditLastAdmin(not_null<UserData*> user) const;
@@ -423,6 +434,7 @@ private:
TimeId _restrictedUntil;
std::vector<Data::UnavailableReason> _unavailableReasons;
std::unique_ptr<InvitePeek> _invitePeek;
QString _inviteLink;
ChannelData *_linkedChat = nullptr;

View File

@@ -166,8 +166,8 @@ public:
[[nodiscard]] bool videoThumbnailLoading() const;
[[nodiscard]] bool videoThumbnailFailed() const;
void loadVideoThumbnail(Data::FileOrigin origin);
const ImageLocation &videoThumbnailLocation() const;
int videoThumbnailByteSize() const;
[[nodiscard]] const ImageLocation &videoThumbnailLocation() const;
[[nodiscard]] int videoThumbnailByteSize() const;
void updateThumbnails(
const QByteArray &inlineThumbnailBytes,

View File

@@ -71,7 +71,7 @@ void Groups::refreshMessage(
unregisterMessage(item);
return;
}
if (!IsServerMsgId(item->id)) {
if (!IsServerMsgId(item->id) && !item->isScheduled()) {
return;
}
const auto groupId = item->groupId();

View File

@@ -475,6 +475,11 @@ void PeerData::setPinnedMessageId(MsgId messageId) {
}
bool PeerData::canExportChatHistory() const {
if (const auto channel = asChannel()) {
if (!channel->amIn() && channel->invitePeekExpires()) {
return false;
}
}
for (const auto &block : _owner->history(id)->blocks) {
for (const auto &message : block->messages) {
if (!message->data()->serviceMsg()) {

View File

@@ -370,7 +370,7 @@ protected:
void updateUserpic(
PhotoId photoId,
MTP::DcId dcId,
const MTPFileLocation &location);
const MTPFileLocation &small);
void clearUserpic();
private:

View File

@@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_photo_media.h"
#include "ui/image/image.h"
#include "main/main_session.h"
#include "media/streaming/media_streaming_loader_local.h"
#include "media/streaming/media_streaming_loader_mtproto.h"
#include "mainwidget.h"
#include "storage/file_download.h"
#include "core/application.h"
@@ -39,6 +41,7 @@ PhotoData::~PhotoData() {
for (auto &image : _images) {
base::take(image.loader).reset();
}
base::take(_video.loader).reset();
}
Data::Session &PhotoData::owner() const {
@@ -294,7 +297,9 @@ void PhotoData::updateImages(
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
const ImageWithLocation &thumbnail,
const ImageWithLocation &large) {
const ImageWithLocation &large,
const ImageWithLocation &video,
crl::time videoStartTime) {
if (!inlineThumbnailBytes.isEmpty()
&& _inlineThumbnailBytes.isEmpty()) {
_inlineThumbnailBytes = inlineThumbnailBytes;
@@ -315,6 +320,16 @@ void PhotoData::updateImages(
update(PhotoSize::Small, small);
update(PhotoSize::Thumbnail, thumbnail);
update(PhotoSize::Large, large);
if (video.location.valid()) {
_videoStartTime = videoStartTime;
}
Data::UpdateCloudFile(
_video,
video,
owner().cache(),
Data::kAnimationCacheTag,
[&](Data::FileOrigin origin) { loadVideo(origin); });
}
int PhotoData::width() const {
@@ -325,6 +340,76 @@ int PhotoData::height() const {
return _images[PhotoSizeIndex(PhotoSize::Large)].location.height();
}
bool PhotoData::hasVideo() const {
return _video.location.valid();
}
bool PhotoData::videoLoading() const {
return _video.loader != nullptr;
}
bool PhotoData::videoFailed() const {
return (_video.flags & Data::CloudFile::Flag::Failed);
}
void PhotoData::loadVideo(Data::FileOrigin origin) {
const auto autoLoading = false;
const auto finalCheck = [=] {
if (const auto active = activeMediaView()) {
return active->videoContent().isEmpty();
}
return true;
};
const auto done = [=](QByteArray result) {
if (const auto active = activeMediaView()) {
active->setVideo(std::move(result));
}
};
Data::LoadCloudFile(
&session(),
_video,
origin,
LoadFromCloudOrLocal,
autoLoading,
Data::kAnimationCacheTag,
finalCheck,
done);
}
const ImageLocation &PhotoData::videoLocation() const {
return _video.location;
}
int PhotoData::videoByteSize() const {
return _video.byteSize;
}
bool PhotoData::videoCanBePlayed() const {
return hasVideo() && !videoPlaybackFailed();
}
auto PhotoData::createStreamingLoader(
Data::FileOrigin origin,
bool forceRemoteLoader) const
-> std::unique_ptr<Media::Streaming::Loader> {
if (!hasVideo()) {
return nullptr;
}
if (!forceRemoteLoader) {
const auto media = activeMediaView();
if (media && !media->videoContent().isEmpty()) {
return Media::Streaming::MakeBytesLoader(media->videoContent());
}
}
return videoLocation().file().data.is<StorageFileLocation>()
? std::make_unique<Media::Streaming::LoaderMtproto>(
&session().downloader(),
videoLocation().file().data.get_unchecked<StorageFileLocation>(),
videoByteSize(),
origin)
: nullptr;
}
PhotoClickHandler::PhotoClickHandler(
not_null<PhotoData*> photo,
FullMsgId context,

View File

@@ -14,6 +14,12 @@ namespace Main {
class Session;
} // namespace Main
namespace Media {
namespace Streaming {
class Loader;
} // namespace Streaming
} // namespace Media
namespace Data {
class Session;
@@ -82,7 +88,9 @@ public:
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
const ImageWithLocation &thumbnail,
const ImageWithLocation &large);
const ImageWithLocation &large,
const ImageWithLocation &video,
crl::time videoStartTime);
[[nodiscard]] int validSizeIndex(Data::PhotoSize size) const;
[[nodiscard]] QByteArray inlineThumbnailBytes() const {
@@ -111,6 +119,27 @@ public:
[[nodiscard]] std::optional<QSize> size(Data::PhotoSize size) const;
[[nodiscard]] int imageByteSize(Data::PhotoSize size) const;
[[nodiscard]] bool hasVideo() const;
[[nodiscard]] bool videoLoading() const;
[[nodiscard]] bool videoFailed() const;
void loadVideo(Data::FileOrigin origin);
[[nodiscard]] const ImageLocation &videoLocation() const;
[[nodiscard]] int videoByteSize() const;
[[nodiscard]] crl::time videoStartPosition() const {
return _videoStartTime;
}
void setVideoPlaybackFailed() {
_videoPlaybackFailed = true;
}
[[nodiscard]] bool videoPlaybackFailed() const {
return _videoPlaybackFailed;
}
[[nodiscard]] bool videoCanBePlayed() const;
[[nodiscard]] auto createStreamingLoader(
Data::FileOrigin origin,
bool forceRemoteLoader) const
-> std::unique_ptr<Media::Streaming::Loader>;
// For now they return size of the 'large' image.
int width() const;
int height() const;
@@ -127,6 +156,9 @@ public:
private:
QByteArray _inlineThumbnailBytes;
std::array<Data::CloudFile, Data::kPhotoSizeCount> _images;
Data::CloudFile _video;
crl::time _videoStartTime = 0;
bool _videoPlaybackFailed = false;
int32 _dc = 0;
uint64 _access = 0;

View File

@@ -85,6 +85,25 @@ void PhotoMedia::set(PhotoSize size, QImage image) {
_owner->session().notifyDownloaderTaskFinished();
}
QByteArray PhotoMedia::videoContent() const {
return _videoBytes;
}
QSize PhotoMedia::videoSize() const {
const auto &location = _owner->videoLocation();
return { location.width(), location.height() };
}
void PhotoMedia::videoWanted(Data::FileOrigin origin) {
if (_videoBytes.isEmpty()) {
_owner->loadVideo(origin);
}
}
void PhotoMedia::setVideo(QByteArray content) {
_videoBytes = std::move(content);
}
bool PhotoMedia::loaded() const {
const auto index = PhotoSizeIndex(PhotoSize::Large);
return (_images[index] != nullptr);

View File

@@ -28,6 +28,11 @@ public:
void wanted(PhotoSize size, Data::FileOrigin origin);
void set(PhotoSize size, QImage image);
[[nodiscard]] QByteArray videoContent() const;
[[nodiscard]] QSize videoSize() const;
void videoWanted(Data::FileOrigin origin);
void setVideo(QByteArray content);
[[nodiscard]] bool loaded() const;
[[nodiscard]] float64 progress() const;
@@ -42,6 +47,7 @@ private:
const not_null<PhotoData*> _owner;
mutable std::unique_ptr<Image> _inlineThumbnail;
std::array<std::unique_ptr<Image>, kPhotoSizeCount> _images;
QByteArray _videoBytes;
};

View File

@@ -133,6 +133,10 @@ HistoryItem *ScheduledMessages::lookupItem(PeerId peer, MsgId msg) const {
return (*j).get();
}
HistoryItem *ScheduledMessages::lookupItem(FullMsgId itemId) const {
return lookupItem(peerFromChannel(itemId.channel), itemId.msg);
}
int ScheduledMessages::count(not_null<History*> history) const {
const auto i = _data.find(history);
return (i != end(_data)) ? i->second.items.size() : 0;
@@ -408,6 +412,12 @@ HistoryItem *ScheduledMessages::append(
if (i != end(list.itemById)) {
const auto existing = i->second;
message.match([&](const MTPDmessage &data) {
// Scheduled messages never have an edit date,
// so if we receive a flag about it,
// probably this message was edited.
if (data.is_edit_hide()) {
existing->applyEdition(data);
}
existing->updateSentContent({
qs(data.vmessage()),
Api::EntitiesFromMTP(
@@ -517,4 +527,19 @@ int32 ScheduledMessages::countListHash(const List &list) const {
return HashFinalize(hash);
}
HistoryItem *ScheduledMessages::lastSentMessage(not_null<History*> history) {
const auto i = _data.find(history);
if (i == end(_data)) {
return nullptr;
}
auto &list = i->second;
sort(list);
const auto items = ranges::view::reverse(list.items);
const auto it = ranges::find_if(
items,
&HistoryItem::canBeEditedFromHistory);
return (it == end(items)) ? nullptr : (*it).get();
}
} // namespace Data

View File

@@ -30,7 +30,9 @@ public:
[[nodiscard]] MsgId lookupId(not_null<HistoryItem*> item) const;
[[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const;
[[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const;
[[nodiscard]] int count(not_null<History*> history) const;
[[nodiscard]] HistoryItem *lastSentMessage(not_null<History*> history);
void checkEntitiesAndUpdate(const MTPDmessage &data);
void apply(const MTPDupdateNewScheduledMessage &update);

View File

@@ -190,6 +190,14 @@ std::vector<UnavailableReason> ExtractUnavailableReasons(
return FindInlineThumbnail(data.vsizes().v);
}
[[nodiscard]] int VideoStartTime(const MTPDvideoSize &data) {
return int(
std::clamp(
std::floor(data.vvideo_start_ts().value_or_empty() * 1000),
0.,
double(std::numeric_limits<int>::max())));
}
} // namespace
Session::Session(not_null<Main::Session*> session)
@@ -1008,14 +1016,16 @@ void Session::setupChannelLeavingViewer() {
PeerUpdate::Flag::ChannelAmIn
) | rpl::map([](const PeerUpdate &update) {
return update.peer->asChannel();
}) | rpl::filter([](not_null<ChannelData*> channel) {
return !(channel->amIn());
}) | rpl::start_with_next([=](not_null<ChannelData*> channel) {
// channel->clearFeed(); // #feed
if (const auto history = historyLoaded(channel->id)) {
history->removeJoinedMessage();
history->updateChatListExistence();
history->updateChatListSortPosition();
if (channel->amIn()) {
channel->clearInvitePeek();
} else {
// channel->clearFeed(); // #feed
if (const auto history = historyLoaded(channel->id)) {
history->removeJoinedMessage();
history->updateChatListExistence();
history->updateChatListSortPosition();
}
}
}, _lifetime);
}
@@ -2155,7 +2165,9 @@ not_null<PhotoData*> Session::processPhoto(
QByteArray(),
small,
thumbnail,
large);
large,
ImageWithLocation{},
crl::time(0));
}, [&](const MTPDphotoEmpty &data) {
return photo(data.vid().v);
});
@@ -2171,7 +2183,9 @@ not_null<PhotoData*> Session::photo(
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
const ImageWithLocation &thumbnail,
const ImageWithLocation &large) {
const ImageWithLocation &large,
const ImageWithLocation &video,
crl::time videoStartTime) {
const auto result = photo(id);
photoApplyFields(
result,
@@ -2183,7 +2197,9 @@ not_null<PhotoData*> Session::photo(
inlineThumbnailBytes,
small,
thumbnail,
large);
large,
video,
videoStartTime);
return result;
}
@@ -2231,7 +2247,9 @@ PhotoData *Session::photoFromWeb(
QByteArray(),
ImageWithLocation{},
ImageWithLocation{ .location = thumbnailLocation },
ImageWithLocation{ .location = large });
ImageWithLocation{ .location = large },
ImageWithLocation{},
crl::time(0));
}
void Session::photoApplyFields(
@@ -2269,8 +2287,24 @@ void Session::photoApplyFields(
? ImageWithLocation()
: Images::FromPhotoSize(_session, data, *i);
};
const auto findVideoSize = [&]() -> std::optional<MTPVideoSize> {
const auto sizes = data.vvideo_sizes();
if (!sizes || sizes->v.isEmpty()) {
return std::nullopt;
}
const auto area = [](const MTPVideoSize &size) {
return size.match([](const MTPDvideoSize &data) {
return data.vw().v * data.vh().v;
});
};
return *ranges::max_element(
sizes->v,
std::greater<>(),
area);
};
const auto large = image(LargeLevels);
if (large.location.valid()) {
const auto video = findVideoSize();
photoApplyFields(
photo,
data.vaccess_hash().v,
@@ -2281,7 +2315,14 @@ void Session::photoApplyFields(
FindPhotoInlineThumbnail(data),
image(SmallLevels),
image(ThumbnailLevels),
large);
large,
(video
? Images::FromVideoSize(_session, data, *video)
: ImageWithLocation()),
(video
? VideoStartTime(
*video->match([](const auto &data) { return &data; }))
: 0));
}
}
@@ -2295,7 +2336,9 @@ void Session::photoApplyFields(
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
const ImageWithLocation &thumbnail,
const ImageWithLocation &large) {
const ImageWithLocation &large,
const ImageWithLocation &video,
crl::time videoStartTime) {
if (!date) {
return;
}
@@ -2306,7 +2349,9 @@ void Session::photoApplyFields(
inlineThumbnailBytes,
small,
thumbnail,
large);
large,
video,
videoStartTime);
}
not_null<DocumentData*> Session::document(DocumentId id) {
@@ -3163,6 +3208,10 @@ void Session::checkPlayingAnimations() {
if (document->isAnimation() || document->isVideoFile()) {
check.emplace(view);
}
} else if (const auto photo = media->getPhoto()) {
if (photo->hasVideo()) {
check.emplace(view);
}
}
}
}
@@ -3544,6 +3593,19 @@ void Session::updateNotifySettings(
}
}
void Session::resetNotifySettingsToDefault(not_null<PeerData*> peer) {
const auto empty = MTP_peerNotifySettings(
MTP_flags(0),
MTPBool(),
MTPBool(),
MTPint(),
MTPstring());
if (peer->notifyChange(empty)) {
updateNotifySettingsLocal(peer);
_session->api().updateNotifySettingsDelayed(peer);
}
}
bool Session::notifyIsMuted(
not_null<const PeerData*> peer,
crl::time *changesIn) const {

View File

@@ -403,7 +403,9 @@ public:
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
const ImageWithLocation &thumbnail,
const ImageWithLocation &large);
const ImageWithLocation &large,
const ImageWithLocation &video,
crl::time videoStartTime);
void photoConvert(
not_null<PhotoData*> original,
const MTPPhoto &data);
@@ -594,6 +596,7 @@ public:
not_null<PeerData*> peer,
std::optional<int> muteForSeconds,
std::optional<bool> silentPosts = std::nullopt);
void resetNotifySettingsToDefault(not_null<PeerData*> peer);
bool notifyIsMuted(
not_null<const PeerData*> peer,
crl::time *changesIn = nullptr) const;
@@ -671,7 +674,9 @@ private:
const QByteArray &inlineThumbnailBytes,
const ImageWithLocation &small,
const ImageWithLocation &thumbnail,
const ImageWithLocation &large);
const ImageWithLocation &large,
const ImageWithLocation &video,
crl::time videoStartTime);
void documentApplyFields(
not_null<DocumentData*> document,

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_streaming.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
@@ -19,16 +20,16 @@ namespace {
constexpr auto kKeepAliveTimeout = 5 * crl::time(1000);
template <typename Object>
template <typename Object, typename Data>
bool PruneDestroyedAndSet(
base::flat_map<
not_null<DocumentData*>,
not_null<Data*>,
std::weak_ptr<Object>> &objects,
not_null<DocumentData*> document,
not_null<Data*> data,
const std::shared_ptr<Object> &object) {
auto result = false;
for (auto i = begin(objects); i != end(objects);) {
if (i->first == document) {
if (i->first == data) {
(i++)->second = object;
result = true;
} else if (i->second.lock() != nullptr) {
@@ -49,54 +50,64 @@ Streaming::Streaming(not_null<Session*> owner)
Streaming::~Streaming() = default;
std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
not_null<DocumentData*> document,
template <typename Data>
[[nodiscard]] std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
base::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,
not_null<Data*> data,
FileOrigin origin,
bool forceRemoteLoader) {
const auto i = _readers.find(document);
if (i != end(_readers)) {
const auto i = readers.find(data);
if (i != end(readers)) {
if (auto result = i->second.lock()) {
if (!forceRemoteLoader || result->isRemoteLoader()) {
return result;
}
}
}
auto loader = document->createStreamingLoader(origin, forceRemoteLoader);
auto loader = data->createStreamingLoader(origin, forceRemoteLoader);
if (!loader) {
return nullptr;
}
auto result = std::make_shared<Reader>(
std::move(loader),
&_owner->cacheBigFile());
if (!PruneDestroyedAndSet(_readers, document, result)) {
_readers.emplace_or_assign(document, result);
if (!PruneDestroyedAndSet(readers, data, result)) {
readers.emplace_or_assign(data, result);
}
return result;
}
std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
not_null<DocumentData*> document,
template <typename Data>
[[nodiscard]] std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
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,
FileOrigin origin) {
const auto i = _documents.find(document);
if (i != end(_documents)) {
const auto i = documents.find(data);
if (i != end(documents)) {
if (auto result = i->second.lock()) {
return result;
}
}
auto reader = sharedReader(document, origin);
auto reader = sharedReader(readers, data, origin);
if (!reader) {
return nullptr;
}
auto result = std::make_shared<Document>(document, std::move(reader));
if (!PruneDestroyedAndSet(_documents, document, result)) {
_documents.emplace_or_assign(document, result);
auto result = std::make_shared<Document>(data, std::move(reader));
if (!PruneDestroyedAndSet(documents, data, result)) {
documents.emplace_or_assign(data, result);
}
return result;
}
void Streaming::keepAlive(not_null<DocumentData*> document) {
const auto i = _documents.find(document);
if (i == end(_documents)) {
template <typename Data>
void Streaming::keepAlive(
base::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,
not_null<Data*> data) {
const auto i = documents.find(data);
if (i == end(documents)) {
return;
}
auto shared = i->second.lock();
@@ -115,6 +126,40 @@ void Streaming::keepAlive(not_null<DocumentData*> document) {
}
}
std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
not_null<DocumentData*> document,
FileOrigin origin,
bool forceRemoteLoader) {
return sharedReader(_fileReaders, document, origin, forceRemoteLoader);
}
std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
not_null<DocumentData*> document,
FileOrigin origin) {
return sharedDocument(_fileDocuments, _fileReaders, document, origin);
}
std::shared_ptr<Streaming::Reader> Streaming::sharedReader(
not_null<PhotoData*> photo,
FileOrigin origin,
bool forceRemoteLoader) {
return sharedReader(_photoReaders, photo, origin, forceRemoteLoader);
}
std::shared_ptr<Streaming::Document> Streaming::sharedDocument(
not_null<PhotoData*> photo,
FileOrigin origin) {
return sharedDocument(_photoDocuments, _photoReaders, photo, origin);
}
void Streaming::keepAlive(not_null<DocumentData*> document) {
keepAlive(_fileDocuments, document);
}
void Streaming::keepAlive(not_null<PhotoData*> photo) {
keepAlive(_photoDocuments, photo);
}
void Streaming::clearKeptAlive() {
const auto now = crl::now();
auto min = std::numeric_limits<crl::time>::max();

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer.h"
class PhotoData;
class DocumentData;
namespace Media {
@@ -41,17 +42,52 @@ public:
not_null<DocumentData*> document,
FileOrigin origin);
[[nodiscard]] std::shared_ptr<Reader> sharedReader(
not_null<PhotoData*> photo,
FileOrigin origin,
bool forceRemoteLoader = false);
[[nodiscard]] std::shared_ptr<Document> sharedDocument(
not_null<PhotoData*> photo,
FileOrigin origin);
void keepAlive(not_null<DocumentData*> document);
void keepAlive(not_null<PhotoData*> photo);
private:
void clearKeptAlive();
template <typename Data>
[[nodiscard]] std::shared_ptr<Reader> sharedReader(
base::flat_map<not_null<Data*>, std::weak_ptr<Reader>> &readers,
not_null<Data*> data,
FileOrigin origin,
bool forceRemoteLoader = false);
template <typename Data>
[[nodiscard]] std::shared_ptr<Document> sharedDocument(
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,
FileOrigin origin);
template <typename Data>
void keepAlive(
base::flat_map<not_null<Data*>, std::weak_ptr<Document>> &documents,
not_null<Data*> data);
const not_null<Session*> _owner;
base::flat_map<not_null<DocumentData*>, std::weak_ptr<Reader>> _readers;
base::flat_map<
not_null<DocumentData*>,
std::weak_ptr<Document>> _documents;
std::weak_ptr<Reader>> _fileReaders;
base::flat_map<
not_null<DocumentData*>,
std::weak_ptr<Document>> _fileDocuments;
base::flat_map<not_null<PhotoData*>, std::weak_ptr<Reader>> _photoReaders;
base::flat_map<
not_null<PhotoData*>,
std::weak_ptr<Document>> _photoDocuments;
base::flat_map<std::shared_ptr<Document>, crl::time> _keptAlive;
base::Timer _keptAliveTimer;

View File

@@ -101,7 +101,7 @@ dialogsMenuToggle: IconButton {
icon: icon {{ "dialogs_menu", dialogsMenuIconFg }};
iconOver: icon {{ "dialogs_menu", dialogsMenuIconFgOver }};
iconPosition: point(10px, 10px);
iconPosition: point(-1px, -1px);
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 40px;
@@ -109,6 +109,15 @@ dialogsMenuToggle: IconButton {
color: windowBgOver;
}
}
dialogsMenuToggleUnread: icon {
{ "dialogs_menu_unread", dialogsMenuIconFg },
{ "dialogs_menu_unread_dot", dialogsUnreadBg },
};
dialogsMenuToggleUnreadMuted: icon {
{ "dialogs_menu_unread", dialogsMenuIconFg },
{ "dialogs_menu_unread_dot", dialogsMenuIconFg },
};
dialogsLock: IconButton(dialogsMenuToggle) {
icon: icon {{ "dialogs_lock", dialogsMenuIconFg }};
iconOver: icon {{ "dialogs_lock", dialogsMenuIconFgOver }};

View File

@@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "window/window_slide_animation.h"
#include "window/window_connecting_widget.h"
#include "window/window_main_menu.h"
#include "storage/storage_media_prepare.h"
#include "storage/storage_account.h"
#include "data/data_session.h"
@@ -274,17 +275,9 @@ Widget::Widget(
Core::App().lockByPasscode();
_lockUnlock->setIconOverride(nullptr);
});
rpl::single(
rpl::empty_value()
) | rpl::then(
controller->filtersMenuChanged()
) | rpl::start_with_next([=] {
const auto filtersHidden = !controller->filtersWidth();
_mainMenuToggle->setVisible(filtersHidden);
_searchForNarrowFilters->setVisible(!filtersHidden);
updateControlsGeometry();
}, lifetime());
_mainMenuToggle->setClickedCallback([=] { showMainMenu(); });
setupMainMenuToggle();
_searchForNarrowFilters->setClickedCallback([=] { Ui::showChatsList(&session()); });
_chooseByDragTimer.setSingleShot(true);
@@ -426,6 +419,31 @@ void Widget::setupSupportMode() {
) | rpl::to_empty);
}
void Widget::setupMainMenuToggle() {
_mainMenuToggle->setClickedCallback([=] { showMainMenu(); });
rpl::single(
rpl::empty_value()
) | rpl::then(
controller()->filtersMenuChanged()
) | rpl::start_with_next([=] {
const auto filtersHidden = !controller()->filtersWidth();
_mainMenuToggle->setVisible(filtersHidden);
_searchForNarrowFilters->setVisible(!filtersHidden);
updateControlsGeometry();
}, lifetime());
Window::OtherAccountsUnreadState(
) | rpl::start_with_next([=](const Window::OthersUnreadState &state) {
const auto icon = !state.count
? nullptr
: !state.allMuted
? &st::dialogsMenuToggleUnread
: &st::dialogsMenuToggleUnreadMuted;
_mainMenuToggle->setIconOverride(icon, icon);
}, _mainMenuToggle->lifetime());
}
void Widget::fullSearchRefreshOn(rpl::producer<> events) {
std::move(
events

View File

@@ -139,6 +139,7 @@ private:
void setupSupportMode();
void setupConnectingWidget();
void setupMainMenuToggle();
bool searchForPeersRequired(const QString &query) const;
void setSearchInChat(Key chat, UserData *from = nullptr);
void showJumpToDate();

View File

@@ -1186,10 +1186,17 @@ void History::applyServiceChanges(
case mtpc_photoCachedSize: bigLoc = &bigSize.c_photoCachedSize().vlocation(); break;
}
if (smallLoc && bigLoc) {
const auto chatPhoto = MTP_chatPhoto(
MTP_flags(photo->hasVideo()
? MTPDchatPhoto::Flag::f_has_video
: MTPDchatPhoto::Flag(0)),
*smallLoc,
*bigLoc,
data.vdc_id());
if (const auto chat = peer->asChat()) {
chat->setPhoto(photo->id, MTP_chatPhoto(*smallLoc, *bigLoc, data.vdc_id()));
chat->setPhoto(photo->id, chatPhoto);
} else if (const auto channel = peer->asChannel()) {
channel->setPhoto(photo->id, MTP_chatPhoto(*smallLoc, *bigLoc, data.vdc_id()));
channel->setPhoto(photo->id, chatPhoto);
}
peer->loadUserpic();
}
@@ -2895,20 +2902,7 @@ HistoryItem *History::lastSentMessage() const {
for (const auto &block : ranges::view::reverse(blocks)) {
for (const auto &message : ranges::view::reverse(block->messages)) {
const auto item = message->data();
// Skip if message is editing media.
if (item->isEditingMedia()) {
continue;
}
// Skip if message is video message or sticker.
if (const auto media = item->media()) {
// Skip only if media is not webpage.
if (!media->webpage() && !media->allowsEditCaption()) {
continue;
}
}
if (IsServerMsgId(item->id)
&& !item->serviceMsg()
&& (item->out() || peer->isSelf())) {
if (item->canBeEditedFromHistory()) {
return item;
}
}

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/history_drag_area.h"
#include "base/event_filter.h"
#include "boxes/confirm_box.h"
#include "boxes/sticker_set_box.h"
#include "inline_bots/inline_bot_result.h"
@@ -21,10 +22,228 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "mainwidget.h"
#include "app.h"
#include "storage/storage_media_prepare.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
DragArea::DragArea(QWidget *parent) : TWidget(parent) {
namespace {
constexpr auto kDragAreaEvents = {
QEvent::DragEnter,
QEvent::DragLeave,
QEvent::Drop,
QEvent::MouseButtonRelease,
QEvent::Leave,
};
inline auto InnerRect(not_null<Ui::RpWidget*> widget) {
return QRect(
st::dragPadding.left(),
st::dragPadding.top(),
widget->width() - st::dragPadding.left() - st::dragPadding.right(),
widget->height() - st::dragPadding.top() - st::dragPadding.bottom());
}
} // namespace
DragArea::Areas DragArea::SetupDragAreaToContainer(
not_null<Ui::RpWidget*> container,
Fn<bool(not_null<const QMimeData*>)> &&dragEnterFilter,
Fn<void(bool)> &&setAcceptDropsField,
Fn<void()> &&updateControlsGeometry,
DragArea::CallbackComputeState &&computeState) {
using DragState = Storage::MimeDataState;
auto &lifetime = container->lifetime();
container->setAcceptDrops(true);
const auto attachDragDocument =
Ui::CreateChild<DragArea>(container.get());
const auto attachDragPhoto = Ui::CreateChild<DragArea>(container.get());
attachDragDocument->hide();
attachDragPhoto->hide();
attachDragDocument->raise();
attachDragPhoto->raise();
const auto attachDragState =
lifetime.make_state<DragState>(DragState::None);
const auto width = [=] {
return container->width();
};
const auto height = [=] {
return container->height();
};
const auto horizontalMargins = st::dragMargin.left()
+ st::dragMargin.right();
const auto verticalMargins = st::dragMargin.top()
+ st::dragMargin.bottom();
const auto resizeToFull = [=](not_null<DragArea*> w) {
w->resize(width() - horizontalMargins, height() - verticalMargins);
};
const auto moveToTop = [=](not_null<DragArea*> w) {
w->move(st::dragMargin.left(), st::dragMargin.top());
};
const auto updateAttachGeometry = crl::guard(container, [=] {
if (updateControlsGeometry) {
updateControlsGeometry();
}
switch (*attachDragState) {
case DragState::Files:
resizeToFull(attachDragDocument);
moveToTop(attachDragDocument);
break;
case DragState::PhotoFiles:
attachDragDocument->resize(
width() - horizontalMargins,
(height() - verticalMargins) / 2);
moveToTop(attachDragDocument);
attachDragPhoto->resize(
attachDragDocument->width(),
attachDragDocument->height());
attachDragPhoto->move(
st::dragMargin.left(),
height()
- attachDragPhoto->height()
- st::dragMargin.bottom());
break;
case DragState::Image:
resizeToFull(attachDragPhoto);
moveToTop(attachDragPhoto);
break;
}
});
const auto updateDragAreas = [=] {
if (setAcceptDropsField) {
setAcceptDropsField(*attachDragState == DragState::None);
}
updateAttachGeometry();
switch (*attachDragState) {
case DragState::None:
attachDragDocument->otherLeave();
attachDragPhoto->otherLeave();
break;
case DragState::Files:
attachDragDocument->setText(
tr::lng_drag_files_here(tr::now),
tr::lng_drag_to_send_files(tr::now));
attachDragDocument->otherEnter();
attachDragPhoto->hideFast();
break;
case DragState::PhotoFiles:
attachDragDocument->setText(
tr::lng_drag_images_here(tr::now),
tr::lng_drag_to_send_no_compression(tr::now));
attachDragPhoto->setText(
tr::lng_drag_photos_here(tr::now),
tr::lng_drag_to_send_quick(tr::now));
attachDragDocument->otherEnter();
attachDragPhoto->otherEnter();
break;
case DragState::Image:
attachDragPhoto->setText(
tr::lng_drag_images_here(tr::now),
tr::lng_drag_to_send_quick(tr::now));
attachDragDocument->hideFast();
attachDragPhoto->otherEnter();
break;
};
};
container->sizeValue(
) | rpl::start_with_next(updateAttachGeometry, lifetime);
const auto resetDragStateIfNeeded = [=] {
if (*attachDragState != DragState::None
|| !attachDragPhoto->isHidden()
|| !attachDragDocument->isHidden()) {
*attachDragState = DragState::None;
updateDragAreas();
}
};
const auto dragEnterEvent = [=](QDragEnterEvent *e) {
if (dragEnterFilter && !dragEnterFilter(e->mimeData())) {
return;
}
*attachDragState = computeState
? computeState(e->mimeData())
: Storage::ComputeMimeDataState(e->mimeData());
updateDragAreas();
if (*attachDragState != DragState::None) {
e->setDropAction(Qt::IgnoreAction);
e->accept();
}
};
const auto dragLeaveEvent = [=](QDragLeaveEvent *e) {
resetDragStateIfNeeded();
};
const auto dropEvent = [=](QDropEvent *e) {
// Hide fast to avoid visual bugs in resizable boxes.
attachDragDocument->hideFast();
attachDragPhoto->hideFast();
*attachDragState = DragState::None;
updateDragAreas();
e->acceptProposedAction();
};
const auto processDragEvents = [=](not_null<QEvent*> event) {
switch (event->type()) {
case QEvent::DragEnter:
dragEnterEvent(static_cast<QDragEnterEvent*>(event.get()));
return true;
case QEvent::DragLeave:
dragLeaveEvent(static_cast<QDragLeaveEvent*>(event.get()));
return true;
case QEvent::Drop:
dropEvent(static_cast<QDropEvent*>(event.get()));
return true;
};
return false;
};
container->events(
) | rpl::filter([=](not_null<QEvent*> event) {
return ranges::contains(kDragAreaEvents, event->type());
}) | rpl::start_with_next([=](not_null<QEvent*> event) {
const auto type = event->type();
if (processDragEvents(event)) {
return;
} else if (type == QEvent::Leave
|| type == QEvent::MouseButtonRelease) {
resetDragStateIfNeeded();
}
}, lifetime);
const auto eventFilter = [=](not_null<QEvent*> event) {
processDragEvents(event);
return base::EventFilterResult::Continue;
};
base::install_event_filter(attachDragDocument, eventFilter);
base::install_event_filter(attachDragPhoto, eventFilter);
updateDragAreas();
return {
.document = attachDragDocument,
.photo = attachDragPhoto,
};
}
DragArea::DragArea(QWidget *parent) : Ui::RpWidget(parent) {
setMouseTracking(true);
setAcceptDrops(true);
}
@@ -34,23 +253,27 @@ bool DragArea::overlaps(const QRect &globalRect) {
return false;
}
auto inner = innerRect();
auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());
return inner.marginsRemoved(QMargins(st::boxRadius, 0, st::boxRadius, 0)).contains(testRect)
|| inner.marginsRemoved(QMargins(0, st::boxRadius, 0, st::boxRadius)).contains(testRect);
const auto inner = InnerRect(this);
const auto testRect = QRect(
mapFromGlobal(globalRect.topLeft()),
globalRect.size());
const auto h = QMargins(st::boxRadius, 0, st::boxRadius, 0);
const auto v = QMargins(0, st::boxRadius, 0, st::boxRadius);
return inner.marginsRemoved(h).contains(testRect)
|| inner.marginsRemoved(v).contains(testRect);
}
void DragArea::mouseMoveEvent(QMouseEvent *e) {
if (_hiding) return;
if (_hiding) {
return;
}
auto in = QRect(st::dragPadding.left(), st::dragPadding.top(), width() - st::dragPadding.left() - st::dragPadding.right(), height() - st::dragPadding.top() - st::dragPadding.bottom()).contains(e->pos());
setIn(in);
setIn(InnerRect(this).contains(e->pos()));
}
void DragArea::dragMoveEvent(QDragMoveEvent *e) {
QRect r(st::dragPadding.left(), st::dragPadding.top(), width() - st::dragPadding.left() - st::dragPadding.right(), height() - st::dragPadding.top() - st::dragPadding.bottom());
setIn(r.contains(e->pos()));
setIn(InnerRect(this).contains(e->pos()));
e->setDropAction(_in ? Qt::CopyAction : Qt::IgnoreAction);
e->accept();
}
@@ -58,7 +281,11 @@ void DragArea::dragMoveEvent(QDragMoveEvent *e) {
void DragArea::setIn(bool in) {
if (_in != in) {
_in = in;
_a_in.start([this] { update(); }, _in ? 0. : 1., _in ? 1. : 0., st::boxDuration);
_a_in.start(
[=] { update(); },
_in ? 0. : 1.,
_in ? 1. : 0.,
st::boxDuration);
}
}
@@ -71,43 +298,57 @@ void DragArea::setText(const QString &text, const QString &subtext) {
void DragArea::paintEvent(QPaintEvent *e) {
Painter p(this);
auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
const auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
if (!_a_opacity.animating() && _hiding) {
return;
}
p.setOpacity(opacity);
auto inner = innerRect();
const auto inner = InnerRect(this);
if (!_cache.isNull()) {
p.drawPixmapLeft(inner.x() - st::boxRoundShadow.extend.left(), inner.y() - st::boxRoundShadow.extend.top(), width(), _cache);
p.drawPixmapLeft(
inner.x() - st::boxRoundShadow.extend.left(),
inner.y() - st::boxRoundShadow.extend.top(),
width(),
_cache);
return;
}
Ui::Shadow::paint(p, inner, width(), st::boxRoundShadow);
App::roundRect(p, inner, st::boxBg, BoxCorners);
p.setPen(anim::pen(st::dragColor, st::dragDropColor, _a_in.value(_in ? 1. : 0.)));
p.setPen(anim::pen(
st::dragColor,
st::dragDropColor,
_a_in.value(_in ? 1. : 0.)));
p.setFont(st::dragFont);
p.drawText(QRect(0, (height() - st::dragHeight) / 2, width(), st::dragFont->height), _text, QTextOption(style::al_top));
const auto rText = QRect(
0,
(height() - st::dragHeight) / 2,
width(),
st::dragFont->height);
p.drawText(rText, _text, QTextOption(style::al_top));
p.setFont(st::dragSubfont);
p.drawText(QRect(0, (height() + st::dragHeight) / 2 - st::dragSubfont->height, width(), st::dragSubfont->height * 2), _subtext, QTextOption(style::al_top));
const auto rSubtext = QRect(
0,
(height() + st::dragHeight) / 2 - st::dragSubfont->height,
width(),
st::dragSubfont->height * 2);
p.drawText(rSubtext, _subtext, QTextOption(style::al_top));
}
void DragArea::dragEnterEvent(QDragEnterEvent *e) {
static_cast<HistoryWidget*>(parentWidget())->dragEnterEvent(e);
e->setDropAction(Qt::IgnoreAction);
e->accept();
}
void DragArea::dragLeaveEvent(QDragLeaveEvent *e) {
static_cast<HistoryWidget*>(parentWidget())->dragLeaveEvent(e);
setIn(false);
}
void DragArea::dropEvent(QDropEvent *e) {
static_cast<HistoryWidget*>(parentWidget())->dropEvent(e);
if (e->isAccepted() && _droppedCallback) {
_droppedCallback(e->mimeData());
}
@@ -133,11 +374,15 @@ void DragArea::hideStart() {
if (_cache.isNull()) {
_cache = Ui::GrabWidget(
this,
innerRect().marginsAdded(st::boxRoundShadow.extend));
InnerRect(this).marginsAdded(st::boxRoundShadow.extend));
}
_hiding = true;
setIn(false);
_a_opacity.start([this] { opacityAnimationCallback(); }, 1., 0., st::boxDuration);
_a_opacity.start(
[=] { opacityAnimationCallback(); },
1.,
0.,
st::boxDuration);
}
void DragArea::hideFinish() {
@@ -154,10 +399,14 @@ void DragArea::showStart() {
if (_cache.isNull()) {
_cache = Ui::GrabWidget(
this,
innerRect().marginsAdded(st::boxRoundShadow.extend));
InnerRect(this).marginsAdded(st::boxRoundShadow.extend));
}
show();
_a_opacity.start([this] { opacityAnimationCallback(); }, 0., 1., st::boxDuration);
_a_opacity.start(
[=] { opacityAnimationCallback(); },
0.,
1.,
st::boxDuration);
}
void DragArea::opacityAnimationCallback() {

View File

@@ -10,12 +10,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
class DragArea : public TWidget {
Q_OBJECT
namespace Storage {
enum class MimeDataState;
} // namespace Storage
class DragArea : public Ui::RpWidget {
public:
DragArea(QWidget *parent);
struct Areas {
DragArea *document;
DragArea *photo;
};
using CallbackComputeState =
Fn<Storage::MimeDataState(const QMimeData *data)>;
static Areas SetupDragAreaToContainer(
not_null<Ui::RpWidget*> container,
Fn<bool(not_null<const QMimeData*>)> &&dragEnterFilter = nullptr,
Fn<void(bool)> &&setAcceptDropsField = nullptr,
Fn<void()> &&updateControlsGeometry = nullptr,
CallbackComputeState &&computeState = nullptr);
void setText(const QString &text, const QString &subtext);
void otherEnter();
@@ -32,28 +50,20 @@ public:
protected:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void dragMoveEvent(QDragMoveEvent *e) override;
// These events should be filtered by parent!
void dragEnterEvent(QDragEnterEvent *e) override;
void dragLeaveEvent(QDragLeaveEvent *e) override;
void dropEvent(QDropEvent *e) override;
void dragMoveEvent(QDragMoveEvent *e) override;
public slots:
private:
void hideStart();
void hideFinish();
void showStart();
private:
void setIn(bool in);
void opacityAnimationCallback();
QRect innerRect() const {
return QRect(
st::dragPadding.left(),
st::dragPadding.top(),
width() - st::dragPadding.left() - st::dragPadding.right(),
height() - st::dragPadding.top() - st::dragPadding.bottom()
);
}
bool _hiding = false;
bool _in = false;

View File

@@ -721,6 +721,26 @@ bool HistoryItem::canUpdateDate() const {
return isScheduled();
}
bool HistoryItem::canBeEditedFromHistory() const {
// Skip if message is editing media.
if (isEditingMedia()) {
return false;
}
// Skip if message is video message or sticker.
if (const auto m = media()) {
// Skip only if media is not webpage.
if (!m->webpage() && !m->allowsEditCaption()) {
return false;
}
}
if ((IsServerMsgId(id) || isScheduled())
&& !serviceMsg()
&& (out() || history()->peer->isSelf())) {
return true;
}
return false;
}
void HistoryItem::sendFailed() {
Expects(_clientFlags & MTPDmessage_ClientFlag::f_sending);
Expects(!(_clientFlags & MTPDmessage_ClientFlag::f_failed));

View File

@@ -128,13 +128,16 @@ public:
// For edit media in history_message.
virtual void returnSavedMedia() {};
void savePreviousMedia() {
_savedMedia = _media->clone(this);
_savedLocalEditMediaData = {
originalText(),
_media->clone(this),
};
}
[[nodiscard]] bool isEditingMedia() const {
return _savedMedia != nullptr;
return _savedLocalEditMediaData.media != nullptr;
}
void clearSavedMedia() {
_savedMedia = nullptr;
_savedLocalEditMediaData = {};
}
// Zero result means this message is not self-destructing right now.
@@ -332,6 +335,8 @@ public:
void updateDate(TimeId newDate);
[[nodiscard]] bool canUpdateDate() const;
[[nodiscard]] bool canBeEditedFromHistory() const;
virtual ~HistoryItem();
MsgId id;
@@ -364,7 +369,12 @@ protected:
int _textWidth = -1;
int _textHeight = 0;
std::unique_ptr<Data::Media> _savedMedia;
struct SavedMediaData {
TextWithEntities text;
std::unique_ptr<Data::Media> media;
};
SavedMediaData _savedLocalEditMediaData;
std::unique_ptr<Data::Media> _media;
private:

View File

@@ -555,7 +555,7 @@ HistoryMessage::HistoryMessage(
const auto fwdViewsCount = original->viewsCount();
if (fwdViewsCount > 0) {
config.viewsCount = fwdViewsCount;
} else if (isPost()
} else if ((isPost() && !isScheduled())
|| (original->senderOriginal()
&& original->senderOriginal()->isChannel())) {
config.viewsCount = 1;
@@ -774,11 +774,12 @@ bool HistoryMessage::allowsForward() const {
}
bool HistoryMessage::allowsSendNow() const {
return isScheduled() && !isSending() && !hasFailed();
return isScheduled() && !isSending() && !hasFailed() && !isEditingMedia();
}
bool HistoryMessage::isTooOldForEdit(TimeId now) const {
return !_history->peer->canEditMessagesIndefinitely()
&& !isScheduled()
&& (now - date() >= _history->session().serverConfig().editTimeLimit);
}
@@ -901,11 +902,13 @@ void HistoryMessage::refreshSentMedia(const MTPMessageMedia *media) {
}
void HistoryMessage::returnSavedMedia() {
if (!_savedMedia) {
if (!isEditingMedia()) {
return;
}
const auto wasGrouped = history()->owner().groups().isGrouped(this);
_media = std::move(_savedMedia);
_media = std::move(_savedLocalEditMediaData.media);
setText(_savedLocalEditMediaData.text);
clearSavedMedia();
if (wasGrouped) {
history()->owner().groups().refreshMessage(this, true);
} else {
@@ -1405,7 +1408,7 @@ std::unique_ptr<HistoryView::Element> HistoryMessage::createView(
HistoryMessage::~HistoryMessage() {
_media.reset();
_savedMedia.reset();
clearSavedMedia();
if (auto reply = Get<HistoryMessageReply>()) {
reply->clearData(this);
}

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/history_widget.h"
#include "api/api_editing.h"
#include "api/api_bot.h"
#include "api/api_sending.h"
#include "api/api_text_entities.h"
@@ -58,6 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/view/history_view_scheduled_section.h"
#include "history/view/history_view_schedule_box.h"
#include "history/view/history_view_webpage_preview.h"
#include "history/view/media/history_view_media.h"
#include "profile/profile_block_group_members.h"
#include "info/info_memento.h"
@@ -87,7 +89,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/popup_menu.h"
#include "ui/text_options.h"
#include "ui/unread_badge.h"
#include "ui/delayed_activation.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "window/themes/window_theme.h"
@@ -112,6 +113,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include <QGuiApplication> // keyboardModifiers()
#include <QtGui/QWindow>
#include <QtCore/QMimeData>
@@ -136,12 +138,6 @@ constexpr auto kCommonModifiers = 0
| Qt::ControlModifier;
const auto kPsaAboutPrefix = "cloud_lng_about_psa_";
void ActivateWindow(not_null<Window::SessionController*> controller) {
const auto window = controller->widget();
window->activateWindow();
Ui::ActivateWindowDelayed(window);
}
object_ptr<Ui::FlatButton> SetupDiscussButton(
not_null<QWidget*> parent,
not_null<Window::SessionController*> controller) {
@@ -297,9 +293,6 @@ HistoryWidget::HistoryWidget(
return recordingAnimationCallback(now);
})
, _kbScroll(this, st::botKbScroll)
, _attachDragState(DragState::None)
, _attachDragDocument(this)
, _attachDragPhoto(this)
, _topShadow(this) {
setAcceptDrops(true);
@@ -453,18 +446,22 @@ HistoryWidget::HistoryWidget(
_botKeyboardHide->addClickHandler([=] { toggleKeyboard(); });
_botCommandStart->addClickHandler([=] { startBotCommand(); });
_attachDragDocument->hide();
_attachDragPhoto->hide();
_topShadow->hide();
_attachDragDocument->setDroppedCallback([=](const QMimeData *data) {
_attachDragAreas = DragArea::SetupDragAreaToContainer(
this,
crl::guard(this, [=](not_null<const QMimeData*> d) {
return _history && _canSendMessages;
}),
crl::guard(this, [=](bool f) { _field->setAcceptDrops(f); }),
crl::guard(this, [=] { updateControlsGeometry(); }));
_attachDragAreas.document->setDroppedCallback([=](const QMimeData *data) {
confirmSendingFiles(data, CompressConfirm::No);
ActivateWindow(controller);
Window::ActivateWindow(controller);
});
_attachDragPhoto->setDroppedCallback([=](const QMimeData *data) {
_attachDragAreas.photo->setDroppedCallback([=](const QMimeData *data) {
confirmSendingFiles(data, CompressConfirm::Yes);
ActivateWindow(controller);
Window::ActivateWindow(controller);
});
subscribe(Adaptive::Changed(), [=] {
@@ -1237,8 +1234,8 @@ void HistoryWidget::orderWidgets() {
_tabbedPanel->raise();
}
_raiseEmojiSuggestions();
_attachDragDocument->raise();
_attachDragPhoto->raise();
_attachDragAreas.document->raise();
_attachDragAreas.photo->raise();
}
void HistoryWidget::updateStickersByEmoji() {
@@ -1451,7 +1448,7 @@ void HistoryWidget::onRecordDone(
qint32 samples) {
if (!canWriteMessage() || result.isEmpty()) return;
ActivateWindow(controller());
Window::ActivateWindow(controller());
const auto duration = samples / Media::Player::kDefaultFrequency;
auto action = Api::SendAction(_history);
action.replyTo = replyToId();
@@ -2066,8 +2063,8 @@ void HistoryWidget::refreshScheduledToggle() {
}
bool HistoryWidget::contentOverlapped(const QRect &globalRect) {
return (_attachDragDocument->overlaps(globalRect)
|| _attachDragPhoto->overlaps(globalRect)
return (_attachDragAreas.document->overlaps(globalRect)
|| _attachDragAreas.photo->overlaps(globalRect)
|| _fieldAutocomplete->overlaps(globalRect)
|| (_tabbedPanel && _tabbedPanel->overlaps(globalRect))
|| (_inlineResults && _inlineResults->overlaps(globalRect)));
@@ -2394,6 +2391,10 @@ void HistoryWidget::unreadCountUpdated() {
void HistoryWidget::messagesFailed(const RPCError &error, int requestId) {
if (error.type() == qstr("CHANNEL_PRIVATE")
&& _peer->isChannel()
&& _peer->asChannel()->invitePeekExpires()) {
_peer->asChannel()->privateErrorReceived();
} else if (error.type() == qstr("CHANNEL_PRIVATE")
|| error.type() == qstr("CHANNEL_PUBLIC_GROUP_NA")
|| error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
auto was = _peer;
@@ -2936,7 +2937,9 @@ void HistoryWidget::onWindowVisibleChanged() {
}
void HistoryWidget::historyDownClicked() {
if (_replyReturn && _replyReturn->history() == _history) {
if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) {
showHistory(_peer->id, ShowAtUnreadMsgId);
} else if (_replyReturn && _replyReturn->history() == _history) {
showHistory(_peer->id, _replyReturn->id);
} else if (_replyReturn && _replyReturn->history() == _migrated) {
showHistory(_peer->id, -_replyReturn->id);
@@ -2987,8 +2990,9 @@ void HistoryWidget::saveEditMsg() {
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) };
TextUtilities::PrepareForSending(left, prepareFlags);
const auto item = session().data().message(_channel, _editMsgId);
if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) {
if (const auto item = session().data().message(_channel, _editMsgId)) {
if (item) {
const auto suggestModerateActions = false;
Ui::show(Box<DeleteMessagesBox>(item, suggestModerateActions));
} else {
@@ -3001,85 +3005,55 @@ void HistoryWidget::saveEditMsg() {
return;
}
auto sendFlags = MTPmessages_EditMessage::Flag::f_message | 0;
if (webPageId == CancelledWebPageId) {
sendFlags |= MTPmessages_EditMessage::Flag::f_no_webpage;
}
auto localEntities = Api::EntitiesToMTP(&session(), sending.entities);
auto sentEntities = Api::EntitiesToMTP(
&session(),
sending.entities,
Api::ConvertOption::SkipLocal);
if (!sentEntities.v.isEmpty()) {
sendFlags |= MTPmessages_EditMessage::Flag::f_entities;
}
const auto weak = Ui::MakeWeak(this);
const auto history = _history;
_saveEditMsgRequestId = history->session().api().request(
MTPmessages_EditMessage(
MTP_flags(sendFlags),
_history->peer->input,
MTP_int(_editMsgId),
MTP_string(sending.text),
MTPInputMedia(),
MTPReplyMarkup(),
sentEntities,
MTP_int(0)
)).done([history, weak](const MTPUpdates &result, mtpRequestId requestId) {
SaveEditMsgDone(history, result, requestId);
if (const auto strong = weak.data()) {
if (requestId == strong->_saveEditMsgRequestId) {
strong->_saveEditMsgRequestId = 0;
strong->cancelEdit();
const auto done = [=](const MTPUpdates &result, mtpRequestId requestId) {
crl::guard(weak, [=] {
if (requestId == _saveEditMsgRequestId) {
_saveEditMsgRequestId = 0;
cancelEdit();
}
})();
if (auto editDraft = history->editDraft()) {
if (editDraft->saveRequestId == requestId) {
history->clearEditDraft();
history->session().local().writeDrafts(history);
}
}
}).fail([history, weak](const RPCError &error, mtpRequestId requestId) {
SaveEditMsgFail(history, error, requestId);
if (const auto strong = weak.data()) {
if (requestId == strong->_saveEditMsgRequestId) {
strong->_saveEditMsgRequestId = 0;
};
const auto fail = [=](const RPCError &error, mtpRequestId requestId) {
if (const auto editDraft = history->editDraft()) {
if (editDraft->saveRequestId == requestId) {
editDraft->saveRequestId = 0;
}
}
crl::guard(weak, [=] {
if (requestId == _saveEditMsgRequestId) {
_saveEditMsgRequestId = 0;
}
const auto &err = error.type();
if (err == qstr("MESSAGE_ID_INVALID")
|| err == qstr("CHAT_ADMIN_REQUIRED")
|| err == qstr("MESSAGE_EDIT_TIME_EXPIRED")) {
if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) {
Ui::show(Box<InformBox>(tr::lng_edit_error(tr::now)));
} else if (err == qstr("MESSAGE_NOT_MODIFIED")) {
strong->cancelEdit();
} else if (err == qstr("MESSAGE_EMPTY")) {
strong->_field->selectAll();
strong->_field->setFocus();
} else if (err == u"MESSAGE_NOT_MODIFIED"_q) {
cancelEdit();
} else if (err == u"MESSAGE_EMPTY"_q) {
_field->selectAll();
_field->setFocus();
} else {
Ui::show(Box<InformBox>(tr::lng_edit_error(tr::now)));
}
strong->update();
}
}).send();
}
update();
})();
};
void HistoryWidget::SaveEditMsgDone(
not_null<History*> history,
const MTPUpdates &updates,
mtpRequestId requestId) {
history->session().api().applyUpdates(updates);
if (auto editDraft = history->editDraft()) {
if (editDraft->saveRequestId == requestId) {
history->clearEditDraft();
history->session().local().writeDrafts(history);
}
}
}
void HistoryWidget::SaveEditMsgFail(
not_null<History*> history,
const RPCError &error,
mtpRequestId requestId) {
if (auto editDraft = history->editDraft()) {
if (editDraft->saveRequestId == requestId) {
editDraft->saveRequestId = 0;
}
}
_saveEditMsgRequestId = Api::EditTextMessage(
item,
sending,
{ .removeWebPageId = (webPageId == CancelledWebPageId) },
done,
fail);
}
void HistoryWidget::hideSelectorControlsAnimated() {
@@ -3146,7 +3120,9 @@ void HistoryWidget::send(Api::SendOptions options) {
}
session().changes().historyUpdated(
_history,
Data::HistoryUpdate::Flag::MessageSent);
(options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
}
void HistoryWidget::sendWithModifiers(Qt::KeyboardModifiers modifiers) {
@@ -3438,30 +3414,7 @@ void HistoryWidget::sendButtonClicked() {
}
}
void HistoryWidget::dragEnterEvent(QDragEnterEvent *e) {
if (!_history || !_canSendMessages) return;
_attachDragState = Storage::ComputeMimeDataState(e->mimeData());
updateDragAreas();
if (_attachDragState != DragState::None) {
e->setDropAction(Qt::IgnoreAction);
e->accept();
}
}
void HistoryWidget::dragLeaveEvent(QDragLeaveEvent *e) {
if (_attachDragState != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
_attachDragState = DragState::None;
updateDragAreas();
}
}
void HistoryWidget::leaveEventHook(QEvent *e) {
if (_attachDragState != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
_attachDragState = DragState::None;
updateDragAreas();
}
if (hasMouseTracking()) {
mouseMoveEvent(nullptr);
}
@@ -3532,10 +3485,6 @@ void HistoryWidget::mouseReleaseEvent(QMouseEvent *e) {
_replyForwardPressed = false;
update(0, _field->y() - st::historySendPadding - st::historyReplyHeight, width(), st::historyReplyHeight);
}
if (_attachDragState != DragState::None || !_attachDragPhoto->isHidden() || !_attachDragDocument->isHidden()) {
_attachDragState = DragState::None;
updateDragAreas();
}
if (_recording) {
stopRecording(_peer && _inField);
}
@@ -3712,34 +3661,6 @@ QRect HistoryWidget::floatPlayerAvailableRect() {
return _peer ? mapToGlobal(_scroll->geometry()) : mapToGlobal(rect());
}
void HistoryWidget::updateDragAreas() {
_field->setAcceptDrops(_attachDragState == DragState::None);
updateControlsGeometry();
switch (_attachDragState) {
case DragState::None:
_attachDragDocument->otherLeave();
_attachDragPhoto->otherLeave();
break;
case DragState::Files:
_attachDragDocument->setText(tr::lng_drag_files_here(tr::now), tr::lng_drag_to_send_files(tr::now));
_attachDragDocument->otherEnter();
_attachDragPhoto->hideFast();
break;
case DragState::PhotoFiles:
_attachDragDocument->setText(tr::lng_drag_images_here(tr::now), tr::lng_drag_to_send_no_compression(tr::now));
_attachDragPhoto->setText(tr::lng_drag_photos_here(tr::now), tr::lng_drag_to_send_quick(tr::now));
_attachDragDocument->otherEnter();
_attachDragPhoto->otherEnter();
break;
case DragState::Image:
_attachDragPhoto->setText(tr::lng_drag_images_here(tr::now), tr::lng_drag_to_send_quick(tr::now));
_attachDragDocument->hideFast();
_attachDragPhoto->otherEnter();
break;
};
}
bool HistoryWidget::readyToForward() const {
return _canSendMessages && !_toForward.empty();
}
@@ -3878,12 +3799,6 @@ bool HistoryWidget::kbWasHidden() const {
return _history && (_keyboard->forMsgId() == FullMsgId(_history->channelId(), _history->lastKeyboardHiddenId));
}
void HistoryWidget::dropEvent(QDropEvent *e) {
_attachDragState = DragState::None;
updateDragAreas();
e->acceptProposedAction();
}
void HistoryWidget::toggleKeyboard(bool manual) {
auto fieldEnabled = canWriteMessage() && !_a_show.animating();
if (_kbShown || _kbReplyTo) {
@@ -4384,7 +4299,7 @@ bool HistoryWidget::confirmSendingFiles(
}
}));
ActivateWindow(controller());
Window::ActivateWindow(controller());
const auto shown = Ui::show(std::move(box));
shown->setCloseByOutsideClick(false);
@@ -4625,23 +4540,6 @@ void HistoryWidget::updateControlsGeometry() {
_membersDropdown->setMaxHeight(countMembersDropdownHeightMax());
}
switch (_attachDragState) {
case DragState::Files:
_attachDragDocument->resize(width() - st::dragMargin.left() - st::dragMargin.right(), height() - st::dragMargin.top() - st::dragMargin.bottom());
_attachDragDocument->move(st::dragMargin.left(), st::dragMargin.top());
break;
case DragState::PhotoFiles:
_attachDragDocument->resize(width() - st::dragMargin.left() - st::dragMargin.right(), (height() - st::dragMargin.top() - st::dragMargin.bottom()) / 2);
_attachDragDocument->move(st::dragMargin.left(), st::dragMargin.top());
_attachDragPhoto->resize(_attachDragDocument->width(), _attachDragDocument->height());
_attachDragPhoto->move(st::dragMargin.left(), height() - _attachDragPhoto->height() - st::dragMargin.bottom());
break;
case DragState::Image:
_attachDragPhoto->resize(width() - st::dragMargin.left() - st::dragMargin.right(), height() - st::dragMargin.top() - st::dragMargin.bottom());
_attachDragPhoto->move(st::dragMargin.left(), st::dragMargin.top());
break;
}
auto topShadowLeft = (Adaptive::OneColumn() || _inGrab) ? 0 : st::lineWidth;
auto topShadowRight = (Adaptive::ThreeColumn() && !_inGrab && _peer) ? st::lineWidth : 0;
_topShadow->setGeometryToLeft(
@@ -5993,38 +5891,22 @@ void HistoryWidget::updatePreview() {
const auto timeout = (_previewData->pendingTill - base::unixtime::now());
_previewTimer.callOnce(std::max(timeout, 0) * crl::time(1000));
} else {
QString title, desc;
if (_previewData->siteName.isEmpty()) {
if (_previewData->title.isEmpty()) {
if (_previewData->description.text.isEmpty()) {
title = _previewData->author;
desc = ((_previewData->document && !_previewData->document->filename().isEmpty()) ? _previewData->document->filename() : _previewData->url);
} else {
title = _previewData->description.text;
desc = _previewData->author.isEmpty() ? ((_previewData->document && !_previewData->document->filename().isEmpty()) ? _previewData->document->filename() : _previewData->url) : _previewData->author;
}
} else {
title = _previewData->title;
desc = _previewData->description.text.isEmpty() ? (_previewData->author.isEmpty() ? ((_previewData->document && !_previewData->document->filename().isEmpty()) ? _previewData->document->filename() : _previewData->url) : _previewData->author) : _previewData->description.text;
}
} else {
title = _previewData->siteName;
desc = _previewData->title.isEmpty() ? (_previewData->description.text.isEmpty() ? (_previewData->author.isEmpty() ? ((_previewData->document && !_previewData->document->filename().isEmpty()) ? _previewData->document->filename() : _previewData->url) : _previewData->author) : _previewData->description.text) : _previewData->title;
}
if (title.isEmpty()) {
auto preview =
HistoryView::TitleAndDescriptionFromWebPage(_previewData);
if (preview.title.isEmpty()) {
if (_previewData->document) {
title = tr::lng_attach_file(tr::now);
preview.title = tr::lng_attach_file(tr::now);
} else if (_previewData->photo) {
title = tr::lng_attach_photo(tr::now);
preview.title = tr::lng_attach_photo(tr::now);
}
}
_previewTitle.setText(
st::msgNameStyle,
title,
preview.title,
Ui::NameTextOptions());
_previewDescription.setText(
st::messageTextStyle,
TextUtilities::Clean(desc),
TextUtilities::Clean(preview.description),
Ui::DialogTextOptions());
}
} else if (!readyToForward() && !replyToId() && !_editMsgId) {
@@ -6431,27 +6313,43 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) {
}
}
if (drawWebPagePreview) {
const auto textTop = backy + st::msgReplyPadding.top();
auto previewLeft = st::historyReplySkip + st::webPageLeft;
p.fillRect(st::historyReplySkip, backy + st::msgReplyPadding.top(), st::webPageBar, st::msgReplyBarSize.height(), st::msgInReplyBarColor);
if ((_previewData->photo && !_previewData->photo->isNull()) || (_previewData->document && _previewData->document->hasThumbnail() && !_previewData->document->isPatternWallPaper())) {
const auto preview = _previewData->photo
? _previewData->photo->getReplyPreview(Data::FileOrigin())
: _previewData->document->getReplyPreview(Data::FileOrigin());
if (preview) {
auto to = QRect(previewLeft, backy + st::msgReplyPadding.top(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height());
if (preview->width() == preview->height()) {
p.drawPixmap(to.x(), to.y(), preview->pix());
} else {
auto from = (preview->width() > preview->height()) ? QRect((preview->width() - preview->height()) / 2, 0, preview->height(), preview->height()) : QRect(0, (preview->height() - preview->width()) / 2, preview->width(), preview->width());
p.drawPixmap(to, preview->pix(), from);
}
}
previewLeft += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x();
p.fillRect(
st::historyReplySkip,
textTop,
st::webPageBar,
st::msgReplyBarSize.height(),
st::msgInReplyBarColor);
const auto to = QRect(
previewLeft,
textTop,
st::msgReplyBarSize.height(),
st::msgReplyBarSize.height());
if (HistoryView::DrawWebPageDataPreview(p, _previewData, to)) {
previewLeft += st::msgReplyBarSize.height()
+ st::msgReplyBarSkip
- st::msgReplyBarSize.width()
- st::msgReplyBarPos.x();
}
p.setPen(st::historyReplyNameFg);
_previewTitle.drawElided(p, previewLeft, backy + st::msgReplyPadding.top(), width() - previewLeft - _fieldBarCancel->width() - st::msgReplyPadding.right());
const auto elidedWidth = width()
- previewLeft
- _fieldBarCancel->width()
- st::msgReplyPadding.right();
_previewTitle.drawElided(
p,
previewLeft,
textTop,
elidedWidth);
p.setPen(st::historyComposeAreaFg);
_previewDescription.drawElided(p, previewLeft, backy + st::msgReplyPadding.top() + st::msgServiceNameFont->height, width() - previewLeft - _fieldBarCancel->width() - st::msgReplyPadding.right());
_previewDescription.drawElided(
p,
previewLeft,
textTop + st::msgServiceNameFont->height,
elidedWidth);
}
}

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "history/history_drag_area.h"
#include "ui/widgets/tooltip.h"
#include "mainwidget.h"
#include "chat_helpers/field_autocomplete.h"
@@ -124,9 +125,6 @@ public:
void checkHistoryActivation();
void leaveToChildEvent(QEvent *e, QWidget *child) override;
void dragEnterEvent(QDragEnterEvent *e) override;
void dragLeaveEvent(QDragLeaveEvent *e) override;
void dropEvent(QDropEvent *e) override;
bool isItemCompletelyHidden(HistoryItem *item) const;
void updateTopBarSelection();
@@ -328,7 +326,6 @@ private slots:
private:
using TabbedPanel = ChatHelpers::TabbedPanel;
using TabbedSelector = ChatHelpers::TabbedSelector;
using DragState = Storage::MimeDataState;
struct PinnedBar {
PinnedBar(MsgId msgId, HistoryWidget *parent);
~PinnedBar();
@@ -525,8 +522,6 @@ private:
void createUnreadBarAndResize();
void saveEditMsg();
static void SaveEditMsgDone(not_null<History*> history, const MTPUpdates &updates, mtpRequestId requestId);
static void SaveEditMsgFail(not_null<History*> history, const RPCError &error, mtpRequestId requestId);
void checkPreview();
void requestPreview();
@@ -569,8 +564,6 @@ private:
void animatedScrollToItem(MsgId msgId);
void animatedScrollToY(int scrollTo, HistoryItem *attachTo = nullptr);
void updateDragAreas();
// when scroll position or scroll area size changed this method
// updates the boundings of the visible area in HistoryInner
void visibleAreaUpdated();
@@ -730,8 +723,8 @@ private:
object_ptr<InlineBots::Layout::Widget> _inlineResults = { nullptr };
std::unique_ptr<TabbedPanel> _tabbedPanel;
DragState _attachDragState;
object_ptr<DragArea> _attachDragDocument, _attachDragPhoto;
DragArea::Areas _attachDragAreas;
Fn<void()> _raiseEmojiSuggestions;

View File

@@ -7,28 +7,389 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_compose_controls.h"
#include "ui/widgets/input_fields.h"
#include "ui/special_buttons.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "base/event_filter.h"
#include "base/call_delayed.h"
#include "base/qt_signal_producer.h"
#include "history/history.h"
#include "base/unixtime.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_section.h"
#include "chat_helpers/tabbed_selector.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "window/window_session_controller.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "inline_bots/inline_results_widget.h"
#include "data/data_changes.h"
#include "data/data_messages.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "facades.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_webpage_preview.h"
#include "inline_bots/inline_results_widget.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "styles/style_history.h"
#include "ui/special_buttons.h"
#include "ui/text_options.h"
#include "ui/ui_utility.h"
#include "ui/widgets/input_fields.h"
#include "window/window_session_controller.h"
namespace HistoryView {
namespace {
using MessageToEdit = ComposeControls::MessageToEdit;
constexpr auto kMouseEvent = {
QEvent::MouseMove,
QEvent::MouseButtonPress,
QEvent::MouseButtonRelease
};
[[nodiscard]] auto ShowWebPagePreview(WebPageData *page) {
return page && (page->pendingTill >= 0);
}
WebPageText ProcessWebPageData(WebPageData *page) {
auto previewText = HistoryView::TitleAndDescriptionFromWebPage(page);
if (previewText.title.isEmpty()) {
if (page->document) {
previewText.title = tr::lng_attach_file(tr::now);
} else if (page->photo) {
previewText.title = tr::lng_attach_photo(tr::now);
}
}
return previewText;
}
} // namespace
class FieldHeader : public Ui::RpWidget {
public:
FieldHeader(QWidget *parent, not_null<Data::Session*> data);
void init();
void editMessage(FullMsgId edit);
void previewRequested(
rpl::producer<QString> title,
rpl::producer<QString> description,
rpl::producer<WebPageData*> page);
bool isDisplayed() const;
bool isEditingMessage() const;
rpl::producer<FullMsgId> editMsgId() const;
rpl::producer<FullMsgId> scrollToItemRequests() const;
MessageToEdit queryToEdit();
WebPageId webPageId() const;
rpl::producer<bool> visibleChanged();
protected:
private:
void updateControlsGeometry(QSize size);
void updateVisible();
void paintWebPage(Painter &p);
void paintEditMessage(Painter &p);
struct Preview {
WebPageData *data = nullptr;
Ui::Text::String title;
Ui::Text::String description;
};
rpl::variable<QString> _title;
rpl::variable<QString> _description;
Preview _preview;
bool hasPreview() const;
Ui::Text::String _editMsgText;
rpl::variable<FullMsgId> _editMsgId;
const not_null<Data::Session*> _data;
const not_null<Ui::IconButton*> _cancel;
rpl::event_stream<bool> _visibleChanged;
rpl::event_stream<FullMsgId> _scrollToItemRequests;
};
FieldHeader::FieldHeader(QWidget *parent, not_null<Data::Session*> data)
: RpWidget(parent)
, _data(data)
, _cancel(Ui::CreateChild<Ui::IconButton>(this, st::historyReplyCancel)) {
resize(QSize(parent->width(), st::historyReplyHeight));
init();
}
void FieldHeader::init() {
sizeValue(
) | rpl::start_with_next([=](QSize size) {
updateControlsGeometry(size);
}, lifetime());
const auto leftIconPressed = lifetime().make_state<bool>(false);
paintRequest(
) | rpl::start_with_next([=] {
Painter p(this);
p.fillRect(rect(), st::historyComposeAreaBg);
if (isEditingMessage()) {
const auto position = st::historyReplyIconPosition;
st::historyEditIcon.paint(p, position, width());
}
(!ShowWebPagePreview(_preview.data) || *leftIconPressed)
? paintEditMessage(p)
: paintWebPage(p);
}, lifetime());
const auto checkPreview = [=](not_null<const HistoryItem*> item) {
_preview = {};
if (const auto media = item->media()) {
if (const auto page = media->webpage()) {
const auto preview = ProcessWebPageData(page);
_title = preview.title;
_description = preview.description;
_preview.data = page;
}
}
};
_editMsgId.value(
) | rpl::start_with_next([=] {
updateVisible();
if (const auto item = _data->message(_editMsgId.current())) {
_editMsgText.setText(
st::messageTextStyle,
item->inReplyText(),
Ui::DialogTextOptions());
checkPreview(item);
}
}, lifetime());
_cancel->addClickHandler([=] {
if (hasPreview()) {
_preview = {};
update();
} else {
_editMsgId = {};
}
updateVisible();
});
_title.value(
) | rpl::start_with_next([=](const auto &t) {
_preview.title.setText(
st::msgNameStyle,
t,
Ui::NameTextOptions());
}, lifetime());
_description.value(
) | rpl::start_with_next([=](const auto &d) {
_preview.description.setText(
st::messageTextStyle,
TextUtilities::Clean(d),
Ui::DialogTextOptions());
}, lifetime());
setMouseTracking(true);
const auto inClickable = lifetime().make_state<bool>(false);
events(
) | rpl::filter([=](not_null<QEvent*> event) {
return ranges::contains(kMouseEvent, event->type())
&& isEditingMessage();
}) | rpl::start_with_next([=](not_null<QEvent*> event) {
const auto type = event->type();
const auto e = static_cast<QMouseEvent*>(event.get());
const auto pos = e ? e->pos() : mapFromGlobal(QCursor::pos());
const auto inPreviewRect = QRect(
st::historyReplySkip,
0,
width() - st::historyReplySkip - _cancel->width(),
height()).contains(pos);
if (type == QEvent::MouseMove) {
const auto inEdit = inPreviewRect;
if (inEdit != *inClickable) {
*inClickable = inEdit;
setCursor(*inClickable
? style::cur_pointer
: style::cur_default);
}
return;
}
const auto isLeftIcon = (pos.x() < st::historyReplySkip);
const auto isLeftButton = (e->button() == Qt::LeftButton);
if (type == QEvent::MouseButtonPress) {
if (isLeftButton && isLeftIcon) {
*leftIconPressed = true;
update();
} else if (isLeftButton && inPreviewRect) {
_scrollToItemRequests.fire(_editMsgId.current());
}
} else if (type == QEvent::MouseButtonRelease) {
if (isLeftButton && *leftIconPressed) {
*leftIconPressed = false;
update();
}
}
}, lifetime());
}
void FieldHeader::previewRequested(
rpl::producer<QString> title,
rpl::producer<QString> description,
rpl::producer<WebPageData*> page) {
std::move(
title
) | rpl::start_with_next([=](const QString &t) {
_title = t;
}, lifetime());
std::move(
description
) | rpl::start_with_next([=](const QString &d) {
_description = d;
}, lifetime());
std::move(
page
) | rpl::start_with_next([=](WebPageData *p) {
_preview.data = p;
updateVisible();
}, lifetime());
}
void FieldHeader::paintWebPage(Painter &p) {
Expects(ShowWebPagePreview(_preview.data));
const auto textTop = st::msgReplyPadding.top();
auto previewLeft = st::historyReplySkip + st::webPageLeft;
p.fillRect(
st::historyReplySkip,
textTop,
st::webPageBar,
st::msgReplyBarSize.height(),
st::msgInReplyBarColor);
const QRect to(
previewLeft,
textTop,
st::msgReplyBarSize.height(),
st::msgReplyBarSize.height());
if (HistoryView::DrawWebPageDataPreview(p, _preview.data, to)) {
previewLeft += st::msgReplyBarSize.height()
+ st::msgReplyBarSkip
- st::msgReplyBarSize.width()
- st::msgReplyBarPos.x();
}
const auto elidedWidth = width()
- previewLeft
- _cancel->width()
- st::msgReplyPadding.right();
p.setPen(st::historyReplyNameFg);
_preview.title.drawElided(
p,
previewLeft,
textTop,
elidedWidth);
p.setPen(st::historyComposeAreaFg);
_preview.description.drawElided(
p,
previewLeft,
textTop + st::msgServiceNameFont->height,
elidedWidth);
}
void FieldHeader::paintEditMessage(Painter &p) {
const auto replySkip = st::historyReplySkip;
p.setPen(st::historyReplyNameFg);
p.setFont(st::msgServiceNameFont);
p.drawTextLeft(
replySkip,
st::msgReplyPadding.top(),
width(),
tr::lng_edit_message(tr::now));
p.setPen(st::historyComposeAreaFg);
p.setTextPalette(st::historyComposeAreaPalette);
_editMsgText.drawElided(
p,
replySkip,
st::msgReplyPadding.top() + st::msgServiceNameFont->height,
width() - replySkip - _cancel->width() - st::msgReplyPadding.right());
p.restoreTextPalette();
}
void FieldHeader::updateVisible() {
isDisplayed() ? show() : hide();
_visibleChanged.fire(isVisible());
}
rpl::producer<bool> FieldHeader::visibleChanged() {
return _visibleChanged.events();
}
bool FieldHeader::isDisplayed() const {
return isEditingMessage() || hasPreview();
}
bool FieldHeader::isEditingMessage() const {
return !!_editMsgId.current();
}
bool FieldHeader::hasPreview() const {
return ShowWebPagePreview(_preview.data);
}
WebPageId FieldHeader::webPageId() const {
return hasPreview() ? _preview.data->id : CancelledWebPageId;
}
void FieldHeader::updateControlsGeometry(QSize size) {
_cancel->moveToRight(0, 0);
}
void FieldHeader::editMessage(FullMsgId id) {
_editMsgId = id;
}
rpl::producer<FullMsgId> FieldHeader::editMsgId() const {
return _editMsgId.value();
}
rpl::producer<FullMsgId> FieldHeader::scrollToItemRequests() const {
return _scrollToItemRequests.events();
}
MessageToEdit FieldHeader::queryToEdit() {
const auto item = _data->message(_editMsgId.current());
if (!isEditingMessage() || !item) {
return {};
}
return {
item->fullId(),
{
item->isScheduled() ? item->date() : 0,
false,
false,
!hasPreview(),
},
};
}
ComposeControls::ComposeControls(
not_null<QWidget*> parent,
not_null<Window::SessionController*> window,
@@ -49,7 +410,10 @@ ComposeControls::ComposeControls(
_wrap.get(),
st::historyComposeField,
Ui::InputField::Mode::MultiLine,
tr::lng_message_ph())) {
tr::lng_message_ph()))
, _header(std::make_unique<FieldHeader>(
_wrap.get(),
&_window->session().data())) {
init();
}
@@ -68,6 +432,7 @@ void ComposeControls::setHistory(History *history) {
_history = history;
_window->tabbedSelector()->setCurrentPeer(
history ? history->peer.get() : nullptr);
initWebpageProcess();
}
void ComposeControls::move(int x, int y) {
@@ -95,13 +460,38 @@ rpl::producer<> ComposeControls::cancelRequests() const {
return _cancelRequests.events();
}
rpl::producer<not_null<QKeyEvent*>> ComposeControls::keyEvents() const {
return _wrap->events(
) | rpl::map([=](not_null<QEvent*> e) -> not_null<QKeyEvent*> {
return static_cast<QKeyEvent*>(e.get());
}) | rpl::filter([=](not_null<QEvent*> event) {
return (event->type() == QEvent::KeyPress);
});
}
rpl::producer<> ComposeControls::sendRequests() const {
auto filter = rpl::filter([=] {
return _send->type() == Ui::SendButton::Type::Schedule;
});
auto submits = base::qt_signal_producer(
_field.get(),
&Ui::InputField::submitted);
return rpl::merge(
_send->clicks() | rpl::to_empty,
std::move(submits) | rpl::to_empty);
_send->clicks() | filter | rpl::to_empty,
std::move(submits) | filter | rpl::to_empty);
}
rpl::producer<MessageToEdit> ComposeControls::editRequests() const {
auto toValue = rpl::map([=] { return _header->queryToEdit(); });
auto filter = rpl::filter([=] {
return _send->type() == Ui::SendButton::Type::Save;
});
auto submits = base::qt_signal_producer(
_field.get(),
&Ui::InputField::submitted);
return rpl::merge(
_send->clicks() | filter | toValue,
std::move(submits) | filter | toValue);
}
rpl::producer<> ComposeControls::attachRequests() const {
@@ -199,6 +589,53 @@ void ComposeControls::init() {
) | rpl::start_with_next([=](QRect clip) {
paintBackground(clip);
}, _wrap->lifetime());
_header->editMsgId(
) | rpl::start_with_next([=](const auto &id) {
if (_header->isEditingMessage()) {
setTextFromEditingMessage(_window->session().data().message(id));
} else {
setText(_localSavedText);
_localSavedText = {};
}
updateSendButtonType();
}, _wrap->lifetime());
_header->visibleChanged(
) | rpl::start_with_next([=] {
updateHeight();
}, _wrap->lifetime());
{
const auto lastMsgId = _wrap->lifetime().make_state<FullMsgId>();
_header->editMsgId(
) | rpl::filter([=](const auto &id) {
return !!id;
}) | rpl::start_with_next([=](const auto &id) {
*lastMsgId = id;
}, _wrap->lifetime());
_window->session().data().itemRemoved(
) | rpl::filter([=](not_null<const HistoryItem*> item) {
return item->id && ((*lastMsgId) == item->fullId());
}) | rpl::start_with_next([=] {
cancelEditMessage();
}, _wrap->lifetime());
}
}
void ComposeControls::setTextFromEditingMessage(not_null<HistoryItem*> item) {
if (!_header->isEditingMessage()) {
return;
}
_localSavedText = getTextWithAppliedMarkdown();
const auto t = item->originalText();
const auto text = TextWithTags{
t.text,
TextUtilities::ConvertEntitiesToTextTags(t.entities)
};
setText(text);
}
void ComposeControls::initField() {
@@ -262,13 +699,13 @@ void ComposeControls::initSendButton() {
void ComposeControls::updateSendButtonType() {
using Type = Ui::SendButton::Type;
const auto type = [&] {
//if (_editMsgId) {
// return Type::Save;
if (_header->isEditingMessage()) {
return Type::Save;
//} else if (_isInlineBot) {
// return Type::Cancel;
//} else if (showRecordButton()) {
// return Type::Record;
//}
}
return Type::Schedule;
}();
_send->setType(type);
@@ -312,6 +749,11 @@ void ComposeControls::updateControlsGeometry(QSize size) {
left,
size.height() - _field->height() - st::historySendPadding);
_header->resizeToWidth(size.width());
_header->moveToLeft(
0,
_field->y() - _header->height() - st::historySendPadding);
auto right = st::historySendRight;
_send->moveToRight(right, buttonsTop);
right += _send->width();
@@ -408,8 +850,209 @@ void ComposeControls::toggleTabbedSelectorMode() {
}
void ComposeControls::updateHeight() {
const auto height = _field->height() + 2 * st::historySendPadding;
_wrap->resize(_wrap->width(), height);
const auto height = _field->height()
+ (_header->isDisplayed() ? _header->height() : 0)
+ 2 * st::historySendPadding;
if (height != _wrap->height()) {
_wrap->resize(_wrap->width(), height);
}
}
void ComposeControls::editMessage(FullMsgId edit) {
cancelEditMessage();
_header->editMessage(std::move(edit));
}
void ComposeControls::cancelEditMessage() {
_header->editMessage({});
}
void ComposeControls::initWebpageProcess() {
Expects(_history);
const auto peer = _history->peer;
auto &lifetime = _wrap->lifetime();
const auto requestRepaint = crl::guard(_header.get(), [=] {
_header->update();
});
const auto parsedLinks = lifetime.make_state<QStringList>();
const auto previewLinks = lifetime.make_state<QString>();
const auto previewData = lifetime.make_state<WebPageData*>(nullptr);
using PreviewCache = std::map<QString, WebPageId>;
const auto previewCache = lifetime.make_state<PreviewCache>();
const auto previewRequest = lifetime.make_state<mtpRequestId>(0);
const auto previewCancelled = lifetime.make_state<bool>(false);
const auto mtpSender =
lifetime.make_state<MTP::Sender>(&_window->session().mtp());
const auto title = std::make_shared<rpl::event_stream<QString>>();
const auto description = std::make_shared<rpl::event_stream<QString>>();
const auto pageData = std::make_shared<rpl::event_stream<WebPageData*>>();
const auto previewTimer = lifetime.make_state<base::Timer>();
const auto updatePreview = [=] {
previewTimer->cancel();
auto t = QString();
auto d = QString();
if (ShowWebPagePreview(*previewData)) {
if (const auto till = (*previewData)->pendingTill) {
t = tr::lng_preview_loading(tr::now);
d = (*previewLinks).splitRef(' ').at(0).toString();
const auto timeout = till - base::unixtime::now();
previewTimer->callOnce(
std::max(timeout, 0) * crl::time(1000));
} else {
const auto preview = ProcessWebPageData(*previewData);
t = preview.title;
d = preview.description;
}
}
title->fire_copy(t);
description->fire_copy(d);
pageData->fire_copy(*previewData);
requestRepaint();
};
const auto gotPreview = crl::guard(_wrap.get(), [=](
const auto &result,
QString links) {
if (*previewRequest) {
*previewRequest = 0;
}
result.match([=](const MTPDmessageMediaWebPage &d) {
const auto page = _history->owner().processWebpage(d.vwebpage());
previewCache->insert({ links, page->id });
auto &till = page->pendingTill;
if (till > 0 && till <= base::unixtime::now()) {
till = -1;
}
if (links == *previewLinks && !*previewCancelled) {
*previewData = (page->id && page->pendingTill >= 0)
? page.get()
: nullptr;
updatePreview();
}
}, [=](const MTPDmessageMediaEmpty &d) {
previewCache->insert({ links, 0 });
if (links == *previewLinks && !*previewCancelled) {
*previewData = nullptr;
updatePreview();
}
}, [](const auto &d) {
});
});
const auto previewCancel = [=] {
mtpSender->request(base::take(*previewRequest)).cancel();
*previewData = nullptr;
previewLinks->clear();
updatePreview();
};
const auto getWebPagePreview = [=] {
const auto links = *previewLinks;
*previewRequest = mtpSender->request(MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(links),
MTPVector<MTPMessageEntity>()
)).done([=](const MTPMessageMedia &result) {
gotPreview(result, links);
}).send();
};
const auto checkPreview = crl::guard(_wrap.get(), [=] {
const auto previewRestricted = peer
&& peer->amRestricted(ChatRestriction::f_embed_links);
if (/*_previewCancelled ||*/ previewRestricted) {
previewCancel();
return;
}
const auto newLinks = parsedLinks->join(' ');
if (*previewLinks == newLinks) {
return;
}
mtpSender->request(base::take(*previewRequest)).cancel();
*previewLinks = newLinks;
if (previewLinks->isEmpty()) {
if (ShowWebPagePreview(*previewData)) {
previewCancel();
}
} else {
const auto i = previewCache->find(*previewLinks);
if (i == previewCache->end()) {
getWebPagePreview();
} else if (i->second) {
*previewData = _history->owner().webpage(i->second);
updatePreview();
} else if (ShowWebPagePreview(*previewData)) {
previewCancel();
}
}
});
previewTimer->setCallback([=] {
if (!ShowWebPagePreview(*previewData) || previewLinks->isEmpty()) {
return;
}
getWebPagePreview();
});
_window->session().changes().peerUpdates(
Data::PeerUpdate::Flag::Rights
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer.get() == peer);
}) | rpl::start_with_next(checkPreview, lifetime);
_window->session().downloaderTaskFinished(
) | rpl::filter([=] {
return (*previewData)
&& ((*previewData)->document || (*previewData)->photo);
}) | rpl::start_with_next((
requestRepaint
), lifetime);
_window->session().data().webPageUpdates(
) | rpl::filter([=](not_null<WebPageData*> page) {
return (*previewData == page.get());
}) | rpl::start_with_next([=] {
updatePreview();
}, lifetime);
const auto fieldLinksParser =
lifetime.make_state<MessageLinksParser>(_field);
fieldLinksParser->list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
*parsedLinks = std::move(parsed);
checkPreview();
}, lifetime);
_header->previewRequested(
title->events(),
description->events(),
pageData->events());
}
WebPageId ComposeControls::webPageId() const {
return _header->webPageId();
}
rpl::producer<Data::MessagePosition> ComposeControls::scrollRequests() const {
return _header->scrollToItemRequests(
) | rpl::map([=](FullMsgId id) -> Data::MessagePosition {
if (const auto item = _window->session().data().message(id)) {
return item->position();
}
return {};
});
}
bool ComposeControls::isEditingMessage() const {
return _header->isEditingMessage();
}
} // namespace HistoryView

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "api/api_common.h"
#include "base/unique_qptr.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
@@ -20,6 +21,10 @@ class TabbedPanel;
class TabbedSelector;
} // namespace ChatHelpers
namespace Data {
struct MessagePosition;
} // namespace Data
namespace InlineBots {
namespace Layout {
class ItemBase;
@@ -45,6 +50,8 @@ struct SectionShow;
namespace HistoryView {
class FieldHeader;
class ComposeControls final {
public:
enum class Mode {
@@ -52,6 +59,12 @@ public:
Scheduled,
};
struct MessageToEdit {
FullMsgId fullId;
Api::SendOptions options;
TextWithTags textWithTags;
};
ComposeControls(
not_null<QWidget*> parent,
not_null<Window::SessionController*> window,
@@ -70,9 +83,12 @@ public:
void focus();
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<> sendRequests() const;
[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;
[[nodiscard]] rpl::producer<> attachRequests() const;
[[nodiscard]] rpl::producer<not_null<DocumentData*>> fileChosen() const;
[[nodiscard]] rpl::producer<not_null<PhotoData*>> photoChosen() const;
[[nodiscard]] rpl::producer<Data::MessagePosition> scrollRequests() const;
[[nodiscard]] rpl::producer<not_null<QKeyEvent*>> keyEvents() const;
[[nodiscard]] auto inlineResultChosen() const
-> rpl::producer<ChatHelpers::TabbedSelector::InlineChosen>;
@@ -86,11 +102,17 @@ public:
const Window::SectionShow &params);
bool returnTabbedSelector();
bool isEditingMessage() const;
void showForGrab();
void showStarted();
void showFinished();
void editMessage(FullMsgId edit);
void cancelEditMessage();
[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
[[nodiscard]] WebPageId webPageId() const;
void clear();
void hidePanelsAnimated();
@@ -99,6 +121,7 @@ private:
void initField();
void initTabbedSelector();
void initSendButton();
void initWebpageProcess();
void updateSendButtonType();
void updateHeight();
void updateControlsGeometry(QSize size);
@@ -111,6 +134,8 @@ private:
void setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);
void setText(const TextWithTags &text);
void setTextFromEditingMessage(not_null<HistoryItem*> item);
const not_null<QWidget*> _parent;
const not_null<Window::SessionController*> _window;
History *_history = nullptr;
@@ -125,11 +150,16 @@ private:
std::unique_ptr<InlineBots::Layout::Widget> _inlineResults;
std::unique_ptr<ChatHelpers::TabbedPanel> _tabbedPanel;
friend class FieldHeader;
const std::unique_ptr<FieldHeader> _header;
rpl::event_stream<> _cancelRequests;
rpl::event_stream<not_null<DocumentData*>> _fileChosen;
rpl::event_stream<not_null<PhotoData*>> _photoChosen;
rpl::event_stream<ChatHelpers::TabbedSelector::InlineChosen> _inlineResultChosen;
TextWithTags _localSavedText;
//bool _recording = false;
//bool _inField = false;
//bool _inReplyEditForward = false;

View File

@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "window/window_peer_menu.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
@@ -62,6 +63,10 @@ ContactStatus::Bar::Bar(QWidget *parent, const QString &name)
this,
QString(),
st::historyContactStatusButton)
, _unarchive(
this,
tr::lng_new_contact_unarchive(tr::now).toUpper(),
st::historyContactStatusButton)
, _block(
this,
tr::lng_new_contact_block(tr::now).toUpper(),
@@ -72,7 +77,7 @@ ContactStatus::Bar::Bar(QWidget *parent, const QString &name)
st::historyContactStatusButton)
, _report(
this,
tr::lng_report_spam_and_leave(tr::now).toUpper(),
QString(),
st::historyContactStatusBlock)
, _close(this, st::historyReplyCancel) {
resize(_close->size());
@@ -80,15 +85,26 @@ ContactStatus::Bar::Bar(QWidget *parent, const QString &name)
void ContactStatus::Bar::showState(State state) {
_add->setVisible(state == State::AddOrBlock || state == State::Add);
_block->setVisible(state == State::AddOrBlock);
_unarchive->setVisible(state == State::UnarchiveOrBlock
|| state == State::UnarchiveOrReport);
_block->setVisible(state == State::AddOrBlock
|| state == State::UnarchiveOrBlock);
_share->setVisible(state == State::SharePhoneNumber);
_report->setVisible(state == State::ReportSpam);
_report->setVisible(state == State::ReportSpam
|| state == State::UnarchiveOrReport);
_add->setText((state == State::Add)
? tr::lng_new_contact_add_name(tr::now, lt_user, _name).toUpper()
: tr::lng_new_contact_add(tr::now).toUpper());
_report->setText((state == State::ReportSpam)
? tr::lng_report_spam_and_leave(tr::now).toUpper()
: tr::lng_report_spam(tr::now).toUpper());
updateButtonsGeometry();
}
rpl::producer<> ContactStatus::Bar::unarchiveClicks() const {
return _unarchive->clicks() | rpl::to_empty;
}
rpl::producer<> ContactStatus::Bar::addClicks() const {
return _add->clicks() | rpl::to_empty;
}
@@ -143,32 +159,34 @@ void ContactStatus::Bar::updateButtonsGeometry() {
closeWidth);
placeButton(button, full, margin);
};
if (!_add->isHidden() && !_block->isHidden()) {
const auto addWidth = buttonWidth(_add);
const auto blockWidth = buttonWidth(_block);
const auto &leftButton = _unarchive->isHidden() ? _add : _unarchive;
const auto &rightButton = _block->isHidden() ? _report : _block;
if (!leftButton->isHidden() && !rightButton->isHidden()) {
const auto leftWidth = buttonWidth(leftButton);
const auto rightWidth = buttonWidth(rightButton);
const auto half = full / 2;
if (addWidth <= half
&& blockWidth + 2 * closeWidth <= full - half) {
placeButton(_add, half);
placeButton(_block, full - half);
} else if (addWidth + blockWidth <= available) {
if (leftWidth <= half
&& rightWidth + 2 * closeWidth <= full - half) {
placeButton(leftButton, half);
placeButton(rightButton, full - half);
} else if (leftWidth + rightWidth <= available) {
const auto margin = std::clamp(
addWidth + blockWidth + closeWidth - available,
leftWidth + rightWidth + closeWidth - available,
0,
closeWidth);
const auto realBlockWidth = blockWidth + 2 * closeWidth - margin;
if (addWidth > realBlockWidth) {
placeButton(_add, addWidth);
placeButton(_block, full - addWidth, margin);
const auto realBlockWidth = rightWidth + 2 * closeWidth - margin;
if (leftWidth > realBlockWidth) {
placeButton(leftButton, leftWidth);
placeButton(rightButton, full - leftWidth, margin);
} else {
placeButton(_add, full - realBlockWidth);
placeButton(_block, realBlockWidth, margin);
placeButton(leftButton, full - realBlockWidth);
placeButton(rightButton, realBlockWidth, margin);
}
} else {
const auto forAdd = (available * addWidth)
/ (addWidth + blockWidth);
placeButton(_add, forAdd);
placeButton(_block, full - forAdd, closeWidth);
const auto forLeft = (available * leftWidth)
/ (leftWidth + rightWidth);
placeButton(leftButton, forLeft);
placeButton(rightButton, full - forLeft, closeWidth);
}
} else {
placeOne(_add);
@@ -245,6 +263,8 @@ auto ContactStatus::PeerState(not_null<PeerData*> peer)
} else {
return State::None;
}
} else if (settings.value & Setting::f_autoarchived) {
return State::UnarchiveOrBlock;
} else if (settings.value & Setting::f_block_contact) {
return State::AddOrBlock;
} else if (settings.value & Setting::f_add_contact) {
@@ -257,7 +277,9 @@ auto ContactStatus::PeerState(not_null<PeerData*> peer)
return peer->settingsValue(
) | rpl::map([=](SettingsChange settings) {
return (settings.value & Setting::f_report_spam)
return (settings.value & Setting::f_autoarchived)
? State::UnarchiveOrReport
: (settings.value & Setting::f_report_spam)
? State::ReportSpam
: State::None;
});
@@ -287,6 +309,7 @@ void ContactStatus::setupHandlers(not_null<PeerData*> peer) {
setupBlockHandler(user);
setupShareHandler(user);
}
setupUnarchiveHandler(peer);
setupReportHandler(peer);
setupCloseHandler(peer);
}
@@ -340,6 +363,21 @@ void ContactStatus::setupShareHandler(not_null<UserData*> user) {
}, _bar.lifetime());
}
void ContactStatus::setupUnarchiveHandler(not_null<PeerData*> peer) {
_bar.entity()->unarchiveClicks(
) | rpl::start_with_next([=] {
Window::ToggleHistoryArchived(peer->owner().history(peer), false);
peer->owner().resetNotifySettingsToDefault(peer);
if (const auto settings = peer->settings()) {
using Flag = MTPDpeerSettings::Flag;
const auto flags = Flag::f_autoarchived
| Flag::f_block_contact
| Flag::f_report_spam;
peer->setSettings(*settings & ~flags);
}
}, _bar.lifetime());
}
void ContactStatus::setupReportHandler(not_null<PeerData*> peer) {
_bar.entity()->reportClicks(
) | rpl::start_with_next([=] {

View File

@@ -46,6 +46,8 @@ private:
ReportSpam,
Add,
AddOrBlock,
UnarchiveOrBlock,
UnarchiveOrReport,
SharePhoneNumber,
};
@@ -55,6 +57,7 @@ private:
void showState(State state);
rpl::producer<> unarchiveClicks() const;
rpl::producer<> addClicks() const;
rpl::producer<> blockClicks() const;
rpl::producer<> shareClicks() const;
@@ -69,6 +72,7 @@ private:
QString _name;
object_ptr<Ui::FlatButton> _add;
object_ptr<Ui::FlatButton> _unarchive;
object_ptr<Ui::FlatButton> _block;
object_ptr<Ui::FlatButton> _share;
object_ptr<Ui::FlatButton> _report;
@@ -82,6 +86,7 @@ private:
void setupAddHandler(not_null<UserData*> user);
void setupBlockHandler(not_null<UserData*> user);
void setupShareHandler(not_null<UserData*> user);
void setupUnarchiveHandler(not_null<PeerData*> peer);
void setupReportHandler(not_null<PeerData*> peer);
void setupCloseHandler(not_null<PeerData*> peer);

View File

@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_context_menu.h"
#include "api/api_editing.h"
#include "base/unixtime.h"
#include "history/view/history_view_list_widget.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
@@ -73,6 +75,23 @@ MsgId ItemIdAcrossData(not_null<HistoryItem*> item) {
return session->data().scheduledMessages().lookupId(item);
}
bool HasEditScheduledMessageAction(const ContextMenuRequest &request) {
const auto item = request.item;
if (!item
|| item->isSending()
|| item->isEditingMedia()
|| !request.selectedItems.empty()) {
return false;
}
const auto peer = item->history()->peer;
if (const auto channel = peer->asChannel()) {
if (!channel->isMegagroup() && !channel->canEditMessages()) {
return false;
}
}
return true;
}
void SavePhotoToFile(not_null<PhotoData*> photo) {
const auto media = photo->activeMediaView();
if (photo->isNull() || !media || !media->loaded()) {
@@ -396,16 +415,10 @@ bool AddSendNowMessageAction(
bool AddRescheduleMessageAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request) {
const auto item = request.item;
if (!item || item->isSending() || !request.selectedItems.empty()) {
if (!HasEditScheduledMessageAction(request)) {
return false;
}
const auto peer = item->history()->peer;
if (const auto channel = peer->asChannel()) {
if (!channel->canEditMessages()) {
return false;
}
}
const auto item = request.item;
const auto owner = &item->history()->owner();
const auto itemId = item->fullId();
menu->addAction(tr::lng_context_reschedule(tr::now), [=] {
@@ -414,9 +427,13 @@ bool AddRescheduleMessageAction(
return;
}
const auto callback = [=](Api::SendOptions options) {
item->history()->session().api().rescheduleMessage(item, options);
if (!item->media() || !item->media()->webpage()) {
options.removeWebPageId = true;
}
Api::RescheduleMessage(item, options);
};
const auto peer = item->history()->peer;
const auto sendMenuType = !peer
? SendMenuType::Disabled
: peer->isSelf()
@@ -441,6 +458,29 @@ bool AddRescheduleMessageAction(
return true;
}
bool AddEditMessageAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
if (!HasEditScheduledMessageAction(request)) {
return false;
}
const auto item = request.item;
if (!item->allowsEdit(base::unixtime::now())) {
return false;
}
const auto owner = &item->history()->owner();
const auto itemId = item->fullId();
menu->addAction(tr::lng_context_edit_msg(tr::now), [=] {
const auto item = owner->message(itemId);
if (!item) {
return;
}
list->editMessageRequestNotify(item->fullId());
});
return true;
}
void AddSendNowAction(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
@@ -519,7 +559,15 @@ bool AddDeleteMessageAction(
Ui::show(Box<DeleteMessagesBox>(item, suggestModerateActions));
}
});
menu->addAction(tr::lng_context_delete_msg(tr::now), callback);
const auto text = [&] {
if (const auto message = item->toHistoryMessage()) {
if (message->uploading()) {
return tr::lng_context_cancel_upload;
}
}
return tr::lng_context_delete_msg;
}()(tr::now);
menu->addAction(text, callback);
return true;
}
@@ -586,6 +634,7 @@ void AddMessageActions(
not_null<Ui::PopupMenu*> menu,
const ContextMenuRequest &request,
not_null<ListWidget*> list) {
AddEditMessageAction(menu, request, list);
AddPostLinkAction(menu, request);
AddForwardAction(menu, request, list);
AddSendNowAction(menu, request, list);

View File

@@ -285,6 +285,12 @@ ListWidget::ListWidget(
}
}
}, lifetime());
session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
update();
}, lifetime());
session().data().itemRemoved(
) | rpl::start_with_next(
[this](auto item) { itemRemoved(item); },
@@ -2550,6 +2556,14 @@ QPoint ListWidget::mapPointToItem(
return point - QPoint(0, itemTop(view));
}
rpl::producer<FullMsgId> ListWidget::editMessageRequested() const {
return _requestedToEditMessage.events();
}
void ListWidget::editMessageRequestNotify(FullMsgId item) {
_requestedToEditMessage.fire(std::move(item));
}
ListWidget::~ListWidget() = default;
} // namespace HistoryView

View File

@@ -181,6 +181,9 @@ public:
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
rpl::producer<FullMsgId> editMessageRequested() const;
void editMessageRequestNotify(FullMsgId item);
// ElementDelegate interface.
Context elementContext() override;
std::unique_ptr<Element> elementCreate(
@@ -512,6 +515,8 @@ private:
FullMsgId _highlightedMessageId;
base::Timer _highlightTimer;
rpl::event_stream<FullMsgId> _requestedToEditMessage;
rpl::lifetime _viewerLifetime;
};

View File

@@ -12,18 +12,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_list_widget.h"
#include "history/view/history_view_schedule_box.h"
#include "history/history.h"
#include "history/history_drag_area.h"
#include "history/history_item.h"
#include "chat_helpers/message_field.h" // SendMenuType.
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/layers/generic_box.h"
#include "ui/text_options.h"
#include "ui/toast/toast.h"
#include "ui/special_buttons.h"
#include "ui/ui_utility.h"
#include "api/api_common.h"
#include "api/api_editing.h"
#include "api/api_sending.h"
#include "apiwrap.h"
#include "boxes/confirm_box.h"
#include "boxes/edit_caption_box.h"
#include "boxes/send_files_box.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
@@ -34,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_scheduled_messages.h"
#include "data/data_user.h"
#include "storage/storage_media_prepare.h"
#include "storage/storage_account.h"
#include "inline_bots/inline_bot_result.h"
@@ -130,6 +135,20 @@ ScheduledWidget::ScheduledWidget(
_scroll->show();
connect(_scroll, &Ui::ScrollArea::scrolled, [=] { onScroll(); });
_inner->editMessageRequested(
) | rpl::start_with_next([=](auto fullId) {
if (const auto item = session().data().message(fullId)) {
const auto media = item->media();
if (media && !media->webpage()) {
if (media->allowsEditCaption()) {
Ui::show(Box<EditCaptionBox>(controller, item));
}
} else {
_composeControls->editMessage(fullId);
}
}
}, _inner->lifetime());
setupScrollDownButton();
setupComposeControls();
}
@@ -141,12 +160,16 @@ void ScheduledWidget::setupComposeControls() {
_composeControls->height(
) | rpl::start_with_next([=] {
const auto wasMax = (_scroll->scrollTopMax() == _scroll->scrollTop());
updateControlsGeometry();
if (wasMax) {
listScrollTo(_scroll->scrollTopMax());
}
}, lifetime());
_composeControls->cancelRequests(
) | rpl::start_with_next([=] {
controller()->showBackFromStack();
listCancelRequest();
}, lifetime());
_composeControls->sendRequests(
@@ -154,6 +177,16 @@ void ScheduledWidget::setupComposeControls() {
send();
}, lifetime());
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
_composeControls->editRequests(
) | rpl::start_with_next([=](auto data) {
if (const auto item = session().data().message(data.fullId)) {
if (item->isScheduled()) {
edit(item, data.options, saveEditMsgRequestId);
}
}
}, lifetime());
_composeControls->attachRequests(
) | rpl::filter([=] {
return !_choosingAttach;
@@ -181,6 +214,31 @@ void ScheduledWidget::setupComposeControls() {
sendInlineResult(chosen.result, chosen.bot);
}, lifetime());
_composeControls->scrollRequests(
) | rpl::start_with_next([=](Data::MessagePosition pos) {
showAtPosition(pos);
}, lifetime());
_composeControls->keyEvents(
) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
if (e->key() == Qt::Key_Up) {
if (!_composeControls->isEditingMessage()) {
auto &messages = session().data().scheduledMessages();
if (const auto item = messages.lastSentMessage(_history)) {
_inner->editMessageRequestNotify(item->fullId());
} else {
_scroll->keyPressEvent(e);
}
} else {
_scroll->keyPressEvent(e);
}
e->accept();
} else if (e->key() == Qt::Key_Down) {
_scroll->keyPressEvent(e);
e->accept();
}
}, lifetime());
_composeControls->setMimeDataHook([=](
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action) {
@@ -463,7 +521,7 @@ void ScheduledWidget::send() {
}
void ScheduledWidget::send(Api::SendOptions options) {
const auto webPageId = 0;/* _previewCancelled
const auto webPageId = _composeControls->webPageId();/* _previewCancelled
? CancelledWebPageId
: ((_previewData && _previewData->pendingTill >= 0)
? _previewData->id
@@ -497,6 +555,79 @@ void ScheduledWidget::send(Api::SendOptions options) {
_composeControls->focus();
}
void ScheduledWidget::edit(
not_null<HistoryItem*> item,
Api::SendOptions options,
mtpRequestId *const saveEditMsgRequestId) {
if (*saveEditMsgRequestId) {
return;
}
const auto textWithTags = _composeControls->getTextWithAppliedMarkdown();
const auto prepareFlags = Ui::ItemTextOptions(
_history,
session().user()).flags;
auto sending = TextWithEntities();
auto left = TextWithEntities {
textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) };
TextUtilities::PrepareForSending(left, prepareFlags);
if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) {
if (item) {
Ui::show(Box<DeleteMessagesBox>(item, false));
} else {
_composeControls->focus();
}
return;
} else if (!left.text.isEmpty()) {
Ui::show(Box<InformBox>(tr::lng_edit_too_long(tr::now)));
return;
}
lifetime().add([=] {
if (!*saveEditMsgRequestId) {
return;
}
session().api().request(base::take(*saveEditMsgRequestId)).cancel();
});
const auto done = [=](const MTPUpdates &result, mtpRequestId requestId) {
if (requestId == *saveEditMsgRequestId) {
*saveEditMsgRequestId = 0;
_composeControls->cancelEditMessage();
}
};
const auto fail = [=](const RPCError &error, mtpRequestId requestId) {
if (requestId == *saveEditMsgRequestId) {
*saveEditMsgRequestId = 0;
}
const auto &err = error.type();
if (ranges::contains(Api::kDefaultEditMessagesErrors, err)) {
Ui::show(Box<InformBox>(tr::lng_edit_error(tr::now)));
} else if (err == u"MESSAGE_NOT_MODIFIED"_q) {
_composeControls->cancelEditMessage();
} else if (err == u"MESSAGE_EMPTY"_q) {
_composeControls->focus();
} else {
Ui::show(Box<InformBox>(tr::lng_edit_error(tr::now)));
}
update();
return true;
};
*saveEditMsgRequestId = Api::EditTextMessage(
item,
sending,
options,
crl::guard(this, done),
crl::guard(this, fail));
_composeControls->hidePanelsAnimated();
_composeControls->focus();
}
void ScheduledWidget::sendExistingDocument(
not_null<DocumentData*> document) {
const auto callback = [=](Api::SendOptions options) {
@@ -880,6 +1011,11 @@ void ScheduledWidget::showAnimatedHook(
void ScheduledWidget::showFinishedHook() {
_topBar->setAnimatingMode(false);
_composeControls->showFinished();
// We should setup the drag area only after
// the section animation is finished,
// because after that the method showChildren() is called.
setupDragArea();
}
bool ScheduledWidget::floatPlayerHandleWheelEvent(QEvent *e) {
@@ -907,6 +1043,10 @@ void ScheduledWidget::listCancelRequest() {
clearSelected();
return;
}
if (_composeControls->isEditingMessage()) {
_composeControls->cancelEditMessage();
return;
}
controller()->showBackFromStack();
}
@@ -1038,4 +1178,21 @@ void ScheduledWidget::clearSelected() {
_inner->cancelSelection();
}
void ScheduledWidget::setupDragArea() {
const auto areas = DragArea::SetupDragAreaToContainer(
this,
[=](not_null<const QMimeData*> d) { return _history; },
nullptr,
[=] { updateControlsGeometry(); });
const auto droppedCallback = [=](CompressConfirm compressed) {
return [=](const QMimeData *data) {
confirmSendingFiles(data, compressed);
Window::ActivateWindow(controller());
};
};
areas.document->setDroppedCallback(droppedCallback(CompressConfirm::No));
areas.photo->setDroppedCallback(droppedCallback(CompressConfirm::Yes));
}
} // namespace HistoryView

View File

@@ -129,6 +129,8 @@ private:
void setupComposeControls();
void setupDragArea();
void setupScrollDownButton();
void scrollDownClicked();
void scrollDownAnimationFinish();
@@ -141,6 +143,10 @@ private:
void send();
void send(Api::SendOptions options);
void edit(
not_null<HistoryItem*> item,
Api::SendOptions options,
mtpRequestId *const saveEditMsgRequestId);
void highlightSingleNewMessage(const Data::MessagesSlice &slice);
void chooseAttach();
[[nodiscard]] SendMenuType sendMenuType() const;

View File

@@ -0,0 +1,88 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_webpage_preview.h"
#include "data/data_file_origin.h"
#include "data/data_web_page.h"
namespace HistoryView {
WebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d) {
QString resultTitle, resultDescription;
const auto document = d->document;
const auto photo = d->photo;
const auto author = d->author;
const auto siteName = d->siteName;
const auto title = d->title;
const auto description = d->description;
const auto filenameOrUrl = [&] {
return ((document && !document->filename().isEmpty())
? document->filename()
: d->url);
};
const auto authorOrFilename = [&] {
return (author.isEmpty()
? filenameOrUrl()
: author);
};
const auto descriptionOrAuthor = [&] {
return (description.text.isEmpty()
? authorOrFilename()
: description.text);
};
if (siteName.isEmpty()) {
if (title.isEmpty()) {
if (description.text.isEmpty()) {
resultTitle = author;
resultDescription = filenameOrUrl();
} else {
resultTitle = description.text;
resultDescription = authorOrFilename();
}
} else {
resultTitle = title;
resultDescription = descriptionOrAuthor();
}
} else {
resultTitle = siteName;
resultDescription = title.isEmpty()
? descriptionOrAuthor()
: title;
}
return { resultTitle, resultDescription };
}
bool DrawWebPageDataPreview(Painter &p, not_null<WebPageData*> d, QRect to) {
const auto document = d->document;
const auto photo = d->photo;
if ((!photo || photo->isNull())
&& (!document
|| !document->hasThumbnail()
|| document->isPatternWallPaper())) {
return false;
}
const auto preview = photo
? photo->getReplyPreview(Data::FileOrigin())
: document->getReplyPreview(Data::FileOrigin());
if (preview) {
const auto w = preview->width();
const auto h = preview->height();
if (w == h) {
p.drawPixmap(to.x(), to.y(), preview->pix());
} else {
const auto from = (w > h)
? QRect((w - h) / 2, 0, h, h)
: QRect(0, (h - w) / 2, w, w);
p.drawPixmap(to, preview->pix(), from);
}
}
return true;
}
} // namespace HistoryView

View File

@@ -0,0 +1,20 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace HistoryView {
struct WebPageText {
QString title;
QString description;
};
WebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d);
bool DrawWebPageDataPreview(Painter &p, not_null<WebPageData*> d, QRect to);
} // namespace HistoryView

View File

@@ -99,8 +99,8 @@ Gif::~Gif() {
}
if (_dataMedia) {
_data->owner().keepAlive(base::take(_dataMedia));
_parent->checkHeavyPart();
}
_parent->checkHeavyPart();
}
}
@@ -1464,8 +1464,8 @@ void Gif::repaintStreamedContent() {
const auto own = activeOwnStreamed();
if (own && !own->frozenFrame.isNull()) {
return;
}
if (_parent->delegate()->elementIsGifPaused() && !activeRoundStreamed()) {
} else if (_parent->delegate()->elementIsGifPaused()
&& !activeRoundStreamed()) {
return;
}
history()->owner().requestViewRepaint(_parent);

View File

@@ -14,13 +14,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_media_common.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "ui/image/image.h"
#include "ui/grouped_layout.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_file_origin.h"
#include "data/data_auto_download.h"
#include "core/application.h"
#include "app.h"
#include "styles/style_history.h"
@@ -31,6 +38,17 @@ using Data::PhotoSize;
} // namespace
struct Photo::Streamed {
explicit Streamed(std::shared_ptr<::Media::Streaming::Document> shared);
::Media::Streaming::Instance instance;
QImage frozenFrame;
};
Photo::Streamed::Streamed(
std::shared_ptr<::Media::Streaming::Document> shared)
: instance(std::move(shared), nullptr) {
}
Photo::Photo(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
@@ -54,9 +72,15 @@ Photo::Photo(
}
Photo::~Photo() {
if (_dataMedia) {
_data->owner().keepAlive(base::take(_dataMedia));
_parent->checkHeavyPart();
if (_streamed || _dataMedia) {
if (_streamed) {
_data->owner().streaming().keepAlive(_data);
stopAnimation();
}
if (_dataMedia) {
_data->owner().keepAlive(base::take(_dataMedia));
_parent->checkHeavyPart();
}
}
}
@@ -93,10 +117,11 @@ void Photo::dataMediaCreated() const {
}
bool Photo::hasHeavyPart() const {
return (_dataMedia != nullptr);
return _streamed || _dataMedia;
}
void Photo::unloadHeavyPart() {
stopAnimation();
_dataMedia = nullptr;
}
@@ -209,22 +234,7 @@ void Photo::draw(Painter &p, const QRect &r, TextSelection selection, crl::time
auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
if (_serviceWidth > 0) {
const auto pix = [&] {
if (const auto large = _dataMedia->image(PhotoSize::Large)) {
return large->pixCircled(_pixw, _pixh);
} else if (const auto thumbnail = _dataMedia->image(
PhotoSize::Thumbnail)) {
return thumbnail->pixBlurredCircled(_pixw, _pixh);
} else if (const auto small = _dataMedia->image(
PhotoSize::Small)) {
return small->pixBlurredCircled(_pixw, _pixh);
} else if (const auto blurred = _dataMedia->thumbnailInline()) {
return blurred->pixBlurredCircled(_pixw, _pixh);
} else {
return QPixmap();
}
}();
p.drawPixmap(rthumb.topLeft(), pix);
paintUserpicFrame(p, rthumb.topLeft(), selected);
} else {
if (bubble) {
if (!_caption.isEmpty()) {
@@ -320,6 +330,82 @@ void Photo::draw(Painter &p, const QRect &r, TextSelection selection, crl::time
}
}
void Photo::paintUserpicFrame(
Painter &p,
QPoint photoPosition,
bool selected) const {
const auto autoplay = _data->videoCanBePlayed() && videoAutoplayEnabled();
const auto startPlay = autoplay && !_streamed;
if (startPlay) {
const_cast<Photo*>(this)->playAnimation(true);
} else {
checkStreamedIsStarted();
}
const auto size = QSize{ _pixw, _pixh };
const auto rect = QRect(photoPosition, size);
if (_streamed
&& _streamed->instance.player().ready()
&& !_streamed->instance.player().videoSize().isEmpty()) {
const auto paused = _parent->delegate()->elementIsGifPaused();
auto request = ::Media::Streaming::FrameRequest();
request.outer = size * cIntRetinaFactor();
request.resize = size * cIntRetinaFactor();
request.radius = ImageRoundRadius::Ellipse;
if (_streamed->instance.playerLocked()) {
if (_streamed->frozenFrame.isNull()) {
_streamed->frozenFrame = _streamed->instance.frame(request);
}
p.drawImage(rect, _streamed->frozenFrame);
} else {
_streamed->frozenFrame = QImage();
p.drawImage(rect, _streamed->instance.frame(request));
if (!paused) {
_streamed->instance.markFrameShown();
}
}
return;
}
const auto pix = [&] {
if (const auto large = _dataMedia->image(PhotoSize::Large)) {
return large->pixCircled(_pixw, _pixh);
} else if (const auto thumbnail = _dataMedia->image(
PhotoSize::Thumbnail)) {
return thumbnail->pixBlurredCircled(_pixw, _pixh);
} else if (const auto small = _dataMedia->image(
PhotoSize::Small)) {
return small->pixBlurredCircled(_pixw, _pixh);
} else if (const auto blurred = _dataMedia->thumbnailInline()) {
return blurred->pixBlurredCircled(_pixw, _pixh);
} else {
return QPixmap();
}
}();
p.drawPixmap(rect, pix);
if (_data->videoCanBePlayed() && !_streamed) {
auto inner = QRect(rect.x() + (rect.width() - st::msgFileSize) / 2, rect.y() + (rect.height() - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize);
p.setPen(Qt::NoPen);
if (selected) {
p.setBrush(st::msgDateImgBgSelected);
} else {
const auto over = ClickHandler::showAsActive(_openl);
p.setBrush(over ? st::msgDateImgBgOver : st::msgDateImgBg);
}
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
const auto icon = [&]() -> const style::icon * {
return &(selected ? st::historyFileThumbPlaySelected : st::historyFileThumbPlay);
}();
if (icon) {
icon->paintInCenter(p, inner);
}
}
}
TextState Photo::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
@@ -580,6 +666,131 @@ void Photo::validateGroupedCache(
*cache = image->pixNoCache(pixWidth, pixHeight, options, width, height);
}
bool Photo::createStreamingObjects() {
using namespace ::Media::Streaming;
setStreamed(std::make_unique<Streamed>(
history()->owner().streaming().sharedDocument(
_data,
_realParent->fullId())));
_streamed->instance.player().updates(
) | rpl::start_with_next_error([=](Update &&update) {
handleStreamingUpdate(std::move(update));
}, [=](Error &&error) {
handleStreamingError(std::move(error));
}, _streamed->instance.lifetime());
if (_streamed->instance.ready()) {
streamingReady(base::duplicate(_streamed->instance.info()));
}
if (!_streamed->instance.valid()) {
stopAnimation();
return false;
}
checkStreamedIsStarted();
return true;
}
void Photo::setStreamed(std::unique_ptr<Streamed> value) {
const auto removed = (_streamed && !value);
const auto set = (!_streamed && value);
_streamed = std::move(value);
if (set) {
history()->owner().registerHeavyViewPart(_parent);
} else if (removed) {
_parent->checkHeavyPart();
}
}
void Photo::handleStreamingUpdate(::Media::Streaming::Update &&update) {
using namespace ::Media::Streaming;
update.data.match([&](Information &update) {
streamingReady(std::move(update));
}, [&](const PreloadedVideo &update) {
}, [&](const UpdateVideo &update) {
repaintStreamedContent();
}, [&](const PreloadedAudio &update) {
}, [&](const UpdateAudio &update) {
}, [&](const WaitingForData &update) {
}, [&](MutedByOther) {
}, [&](Finished) {
});
}
void Photo::handleStreamingError(::Media::Streaming::Error &&error) {
_data->setVideoPlaybackFailed();
stopAnimation();
}
void Photo::repaintStreamedContent() {
if (_streamed && !_streamed->frozenFrame.isNull()) {
return;
} else if (_parent->delegate()->elementIsGifPaused()) {
return;
}
history()->owner().requestViewRepaint(_parent);
}
void Photo::streamingReady(::Media::Streaming::Information &&info) {
history()->owner().requestViewRepaint(_parent);
}
void Photo::checkAnimation() {
if (_streamed && !videoAutoplayEnabled()) {
stopAnimation();
}
}
void Photo::stopAnimation() {
setStreamed(nullptr);
}
void Photo::playAnimation(bool autoplay) {
ensureDataMediaCreated();
if (_streamed && autoplay) {
return;
} else if (_streamed && videoAutoplayEnabled()) {
Core::App().showPhoto(_data, _parent->data());
return;
}
if (_streamed) {
stopAnimation();
} else if (_data->videoCanBePlayed()) {
if (!videoAutoplayEnabled()) {
history()->owner().checkPlayingAnimations();
}
if (!createStreamingObjects()) {
_data->setVideoPlaybackFailed();
return;
}
}
}
void Photo::checkStreamedIsStarted() const {
if (!_streamed) {
return;
} else if (_streamed->instance.paused()) {
_streamed->instance.resume();
}
if (_streamed
&& !_streamed->instance.active()
&& !_streamed->instance.failed()) {
const auto position = _data->videoStartPosition();
auto options = ::Media::Streaming::PlaybackOptions();
options.position = position;
options.mode = ::Media::Streaming::Mode::Video;
options.loop = true;
_streamed->instance.play(options);
}
}
bool Photo::videoAutoplayEnabled() const {
return Data::AutoDownload::ShouldAutoPlay(
_data->session().settings().autoDownload(),
_realParent->history()->peer,
_data);
}
TextForMimeData Photo::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection);
}

View File

@@ -13,6 +13,15 @@ namespace Data {
class PhotoMedia;
} // namespace Data
namespace Media {
namespace Streaming {
class Instance;
struct Update;
enum class Error;
struct Information;
} // namespace Streaming
} // namespace Media
namespace HistoryView {
class Photo : public File {
@@ -89,8 +98,14 @@ protected:
bool dataLoaded() const override;
private:
struct Streamed;
void create(FullMsgId contextId, PeerData *chat = nullptr);
void playAnimation(bool autoplay) override;
void stopAnimation() override;
void checkAnimation() override;
void ensureDataMediaCreated() const;
void dataMediaCreated() const;
@@ -104,12 +119,26 @@ private:
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const;
bool videoAutoplayEnabled() const;
void setStreamed(std::unique_ptr<Streamed> value);
void repaintStreamedContent();
void checkStreamedIsStarted() const;
bool createStreamingObjects();
void handleStreamingUpdate(::Media::Streaming::Update &&update);
void handleStreamingError(::Media::Streaming::Error &&error);
void streamingReady(::Media::Streaming::Information &&info);
void paintUserpicFrame(
Painter &p,
QPoint photoPosition,
bool selected) const;
not_null<PhotoData*> _data;
int _serviceWidth = 0;
int _pixw = 1;
int _pixh = 1;
Ui::Text::String _caption;
mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
mutable std::unique_ptr<Streamed> _streamed;
};

View File

@@ -67,6 +67,10 @@ rpl::producer<> AppConfig::refreshed() const {
return _refreshed.events();
}
rpl::producer<> AppConfig::value() const {
return _refreshed.events_starting_with({});
}
template <typename Extractor>
auto AppConfig::getValue(const QString &key, Extractor &&extractor) const {
const auto i = _data.find(key);
@@ -126,4 +130,29 @@ std::vector<QString> AppConfig::getStringArray(
});
}
bool AppConfig::suggestionCurrent(const QString &key) const {
return !_dismissedSuggestions.contains(key)
&& ranges::contains(
get<std::vector<QString>>(
u"pending_suggestions"_q,
std::vector<QString>()),
key);
}
rpl::producer<> AppConfig::suggestionRequested(const QString &key) const {
return value(
) | rpl::filter([=] {
return suggestionCurrent(key);
});
}
void AppConfig::dismissSuggestion(const QString &key) {
if (!_dismissedSuggestions.emplace(key).second) {
return;
}
_api->request(MTPhelp_DismissSuggestion(
MTP_string(key)
)).send();
}
} // namespace Main

View File

@@ -31,6 +31,12 @@ public:
}
[[nodiscard]] rpl::producer<> refreshed() const;
[[nodiscard]] rpl::producer<> value() const;
[[nodiscard]] bool suggestionCurrent(const QString &key) const;
[[nodiscard]] rpl::producer<> suggestionRequested(
const QString &key) const;
void dismissSuggestion(const QString &key);
void refresh();
@@ -60,6 +66,7 @@ private:
mtpRequestId _requestId = 0;
base::flat_map<QString, MTPJSONValue> _data;
rpl::event_stream<> _refreshed;
base::flat_set<QString> _dismissedSuggestions;
rpl::lifetime _lifetime;
};

View File

@@ -31,7 +31,9 @@ Domain::Domain(const QString &dataName)
, _local(std::make_unique<Storage::Domain>(this, dataName)) {
_active.changes(
) | rpl::take(1) | rpl::start_with_next([] {
Local::rewriteSettingsIfNeeded();
// In case we had a legacy passcoded app we start settings here.
Core::App().startSettingsAndBackground();
Core::App().notifications().createManager();
}, _lifetime);

View File

@@ -706,16 +706,16 @@ void MainWidget::cancelUploadLayer(not_null<HistoryItem*> item) {
session().uploader().pause(itemId);
const auto stopUpload = [=] {
Ui::hideLayer();
if (const auto item = session().data().message(itemId)) {
const auto history = item->history();
if (!IsServerMsgId(itemId.msg)) {
auto &data = session().data();
if (const auto item = data.message(itemId)) {
if (!item->isEditingMedia()) {
item->destroy();
history->requestChatListMessage();
item->history()->requestChatListMessage();
} else {
item->returnSavedMedia();
session().uploader().cancel(item->fullId());
}
session().data().sendHistoryChangeNotifications();
data.sendHistoryChangeNotifications();
}
session().uploader().unpause();
};

View File

@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/streaming/media_streaming_reader.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_photo.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "main/main_session.h"
@@ -34,19 +35,29 @@ constexpr auto kGoodThumbnailQuality = 87;
Document::Document(
not_null<DocumentData*> document,
std::shared_ptr<Reader> reader)
: Document(std::move(reader), document) {
: Document(std::move(reader), document, nullptr) {
_player.fullInCache(
) | rpl::start_with_next([=](bool fullInCache) {
_document->setLoadedInMediaCache(fullInCache);
}, _player.lifetime());
}
Document::Document(std::unique_ptr<Loader> loader)
: Document(std::make_shared<Reader>(std::move(loader)), nullptr) {
Document::Document(
not_null<PhotoData*> photo,
std::shared_ptr<Reader> reader)
: Document(std::move(reader), nullptr, photo) {
}
Document::Document(std::shared_ptr<Reader> reader, DocumentData *document)
Document::Document(std::unique_ptr<Loader> loader)
: Document(std::make_shared<Reader>(std::move(loader)), nullptr, nullptr) {
}
Document::Document(
std::shared_ptr<Reader> reader,
DocumentData *document,
PhotoData *photo)
: _document(document)
, _photo(photo)
, _player(std::move(reader))
, _radial(
[=] { waitingCallback(); },
@@ -71,10 +82,6 @@ const Information &Document::info() const {
return _info;
}
//not_null<DocumentData*> Document::data() const {
// return _document;
//}
void Document::play(const PlaybackOptions &options) {
_player.play(options);
_info.audio.state.position
@@ -160,6 +167,10 @@ void Document::handleError(Error &&error) {
} else if (error == Error::OpenFailed) {
_document->setInappPlaybackFailed();
}
} else if (_photo) {
if (error == Error::NotStreamable || error == Error::OpenFailed) {
_photo->setVideoPlaybackFailed();
}
}
waitingChange(false);
}

View File

@@ -25,6 +25,9 @@ public:
Document(
not_null<DocumentData*> document,
std::shared_ptr<Reader> reader);
Document(
not_null<PhotoData*> photo,
std::shared_ptr<Reader> reader);
explicit Document(std::unique_ptr<Loader> loader);
void play(const PlaybackOptions &options);
@@ -33,14 +36,16 @@ public:
[[nodiscard]] Player &player();
[[nodiscard]] const Player &player() const;
[[nodiscard]] const Information &info() const;
// [[nodiscard]] not_null<DocumentData*> data() const;
[[nodiscard]] bool waitingShown() const;
[[nodiscard]] float64 waitingOpacity() const;
[[nodiscard]] Ui::RadialState waitingState() const;
private:
Document(std::shared_ptr<Reader> reader, DocumentData *document);
Document(
std::shared_ptr<Reader> reader,
DocumentData *document,
PhotoData *photo);
friend class Instance;
@@ -59,6 +64,7 @@ private:
void validateGoodThumbnail();
DocumentData *_document = nullptr;
PhotoData *_photo = nullptr;
Player _player;
Information _info;

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/streaming/media_streaming_document.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
@@ -35,6 +36,15 @@ Instance::Instance(
std::move(waitingCallback)) {
}
Instance::Instance(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> waitingCallback)
: Instance(
photo->owner().streaming().sharedDocument(photo, origin),
std::move(waitingCallback)) {
}
Instance::~Instance() {
if (_shared) {
unlockPlayer();

View File

@@ -34,6 +34,10 @@ public:
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> waitingCallback);
Instance(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> waitingCallback);
~Instance();
[[nodiscard]] bool valid() const;

View File

@@ -130,7 +130,7 @@ QWidget *PipDelegate::pipParentWidget() {
return _parent;
}
Images::Options VideoThumbOptions(not_null<DocumentData*> document) {
Images::Options VideoThumbOptions(DocumentData *document) {
const auto result = Images::Option::Smooth | Images::Option::Blurred;
return (document && document->isVideoMessage())
? (result | Images::Option::Circled)
@@ -240,6 +240,12 @@ struct OverlayWidget::Streamed {
QWidget *controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> waitingCallback);
Streamed(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
QWidget *controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> waitingCallback);
Streaming::Instance instance;
PlaybackControls controls;
@@ -277,6 +283,16 @@ OverlayWidget::Streamed::Streamed(
, controls(controlsParent, controlsDelegate) {
}
OverlayWidget::Streamed::Streamed(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
QWidget *controlsParent,
not_null<PlaybackControls::Delegate*> controlsDelegate,
Fn<void()> waitingCallback)
: instance(photo, origin, std::move(waitingCallback))
, controls(controlsParent, controlsDelegate) {
}
OverlayWidget::PipWrap::PipWrap(
QWidget *parent,
not_null<DocumentData*> document,
@@ -356,7 +372,7 @@ OverlayWidget::OverlayWidget()
Core::App().calls().currentCallValue(
) | rpl::start_with_next([=](Calls::Call *call) {
if (!_streamed) {
if (!_streamed || videoIsGifOrUserpic()) {
return;
} else if (call) {
playbackPauseOnCall();
@@ -441,8 +457,10 @@ QSize OverlayWidget::videoSize() const {
return flipSizeByRotation(_streamed->instance.info().video.size);
}
bool OverlayWidget::videoIsGifv() const {
return _streamed && _document->isAnimation() && !_document->isVideoMessage();
bool OverlayWidget::videoIsGifOrUserpic() const {
return _streamed
&& (!_document
|| (_document->isAnimation() && !_document->isVideoMessage()));
}
QImage OverlayWidget::videoFrame() const {
@@ -709,7 +727,7 @@ void OverlayWidget::refreshCaptionGeometry() {
_groupThumbs = nullptr;
_groupThumbsRect = QRect();
}
const auto captionBottom = (_streamed && !videoIsGifv())
const auto captionBottom = (_streamed && !videoIsGifOrUserpic())
? (_streamed->controls.y() - st::mediaviewCaptionMargin.height())
: _groupThumbs
? _groupThumbsTop
@@ -884,7 +902,7 @@ void OverlayWidget::contentSizeChanged() {
}
void OverlayWidget::resizeContentByScreenSize() {
const auto bottom = (!_streamed || videoIsGifv())
const auto bottom = (!_streamed || videoIsGifOrUserpic())
? height()
: (_streamed->controls.y()
- st::mediaviewCaptionPadding.bottom()
@@ -942,8 +960,10 @@ float64 OverlayWidget::radialProgress() const {
}
bool OverlayWidget::radialLoading() const {
if (_document) {
return _document->loading() && !_streamed;
if (_streamed) {
return false;
} else if (_document) {
return _document->loading();
} else if (_photo) {
return _photo->displayLoading();
}
@@ -1133,7 +1153,9 @@ void OverlayWidget::assignMediaPointer(not_null<PhotoData*> photo) {
_photo = photo;
_photoMedia = _photo->createMediaView();
_photoMedia->wanted(Data::PhotoSize::Small, fileOrigin());
_photo->load(fileOrigin(), LoadFromCloudOrLocal, true);
if (!_photo->hasVideo() || _photo->videoPlaybackFailed()) {
_photo->load(fileOrigin(), LoadFromCloudOrLocal, true);
}
}
}
@@ -1186,7 +1208,7 @@ void OverlayWidget::onHideControls(bool force) {
|| _menu
|| _mousePressed
|| (_fullScreenVideo
&& !videoIsGifv()
&& !videoIsGifOrUserpic()
&& _streamed->controls.geometry().contains(_lastMouseMovePos))) {
return;
}
@@ -1307,7 +1329,9 @@ void OverlayWidget::onSaveAs() {
updateOver(_lastMouseMovePos);
}
} else {
if (!_photo || !_photoMedia->loaded()) return;
if (!_photo || !_photoMedia->loaded()) {
return;
}
const auto image = _photoMedia->image(Data::PhotoSize::Large)->original();
auto filter = qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter();
@@ -1858,7 +1882,7 @@ void OverlayWidget::refreshCaption(HistoryItem *item) {
using namespace HistoryView;
_caption = Ui::Text::String(st::msgMinWidth);
const auto duration = (_streamed && !videoIsGifv())
const auto duration = (_streamed && _document && !videoIsGifOrUserpic())
? _document->getDuration()
: 0;
const auto base = duration
@@ -2045,18 +2069,29 @@ void OverlayWidget::displayPhoto(not_null<PhotoData*> photo, HistoryItem *item)
_radial.stop();
refreshMediaViewer();
_staticContent = QPixmap();
if (_photo->videoCanBePlayed()) {
initStreaming();
}
refreshCaption(item);
_zoom = 0;
_zoomToScreen = _zoomToDefault = 0;
_blurred = true;
_staticContent = QPixmap();
_down = OverNone;
const auto size = style::ConvertScale(flipSizeByRotation(QSize(
photo->width(),
photo->height())));
_w = size.width();
_h = size.height();
if (!_staticContent.isNull()) {
// Video thumbnail.
const auto size = style::ConvertScale(
flipSizeByRotation(_staticContent.size()));
_w = size.width();
_h = size.height();
} else {
const auto size = style::ConvertScale(flipSizeByRotation(QSize(
photo->width(),
photo->height())));
_w = size.width();
_h = size.height();
}
contentSizeChanged();
refreshFromLabel(item);
displayFinished();
@@ -2252,16 +2287,24 @@ void OverlayWidget::displayFinished() {
}
}
bool OverlayWidget::canInitStreaming() const {
return (_document && _documentMedia->canBePlayed())
|| (_photo && _photo->videoCanBePlayed());
}
bool OverlayWidget::initStreaming(bool continueStreaming) {
Expects(_document != nullptr);
Expects(_documentMedia->canBePlayed());
Expects(canInitStreaming());
if (_streamed) {
return true;
}
initStreamingThumbnail();
if (!createStreamingObjects()) {
_document->setInappPlaybackFailed();
if (_document) {
_document->setInappPlaybackFailed();
} else {
_photo->setVideoPlaybackFailed();
}
return false;
}
@@ -2301,22 +2344,47 @@ void OverlayWidget::startStreamingPlayer() {
const auto position = _document
? _document->session().settings().mediaLastPlaybackPosition(_document->id)
: _photo
? _photo->videoStartPosition()
: 0;
restartAtSeekPosition(position);
}
void OverlayWidget::initStreamingThumbnail() {
Expects(_document != nullptr);
Expects(_photo || _document);
_touchbarDisplay.fire(TouchBarItemType::Video);
const auto good = _documentMedia->goodThumbnail();
const auto useGood = (good != nullptr);
const auto thumbnail = _documentMedia->thumbnail();
const auto useThumb = (thumbnail != nullptr);
const auto blurred = _documentMedia->thumbnailInline();
const auto size = useGood ? good->size() : _document->dimensions;
if (!useGood && !thumbnail && !blurred) {
const auto computePhotoThumbnail = [&] {
const auto thumbnail = _photoMedia->image(Data::PhotoSize::Thumbnail);
if (thumbnail) {
return thumbnail;
} else if (_peer && _peer->userpicPhotoId() == _photo->id) {
if (const auto view = _peer->activeUserpicView()) {
if (const auto image = view->image()) {
return image;
}
}
}
return thumbnail;
};
const auto good = _document
? _documentMedia->goodThumbnail()
: _photoMedia->image(Data::PhotoSize::Large);
const auto thumbnail = _document
? _documentMedia->thumbnail()
: computePhotoThumbnail();
const auto blurred = _document
? _documentMedia->thumbnailInline()
: _photoMedia->thumbnailInline();
const auto size = _photo
? QSize(
_photo->videoLocation().width(),
_photo->videoLocation().height())
: good
? good->size()
: _document->dimensions;
if (!good && !thumbnail && !blurred) {
return;
} else if (size.isEmpty()) {
return;
@@ -2325,16 +2393,16 @@ void OverlayWidget::initStreamingThumbnail() {
const auto h = size.height();
const auto options = VideoThumbOptions(_document);
const auto goodOptions = (options & ~Images::Option::Blurred);
_staticContent = (useGood
_staticContent = (good
? good
: useThumb
: thumbnail
? thumbnail
: blurred
? blurred
: Image::BlankMedia().get())->pixNoCache(
w,
h,
useGood ? goodOptions : options,
good ? goodOptions : options,
w / cIntRetinaFactor(),
h / cIntRetinaFactor());
_staticContent.setDevicePixelRatio(cRetinaFactor());
@@ -2358,24 +2426,36 @@ void OverlayWidget::applyVideoSize() {
}
bool OverlayWidget::createStreamingObjects() {
_streamed = std::make_unique<Streamed>(
_document,
fileOrigin(),
this,
static_cast<PlaybackControls::Delegate*>(this),
[=] { waitingAnimationCallback(); });
Expects(_photo || _document);
if (_document) {
_streamed = std::make_unique<Streamed>(
_document,
fileOrigin(),
this,
static_cast<PlaybackControls::Delegate*>(this),
[=] { waitingAnimationCallback(); });
} else {
_streamed = std::make_unique<Streamed>(
_photo,
fileOrigin(),
this,
static_cast<PlaybackControls::Delegate*>(this),
[=] { waitingAnimationCallback(); });
}
if (!_streamed->instance.valid()) {
_streamed = nullptr;
return false;
}
_streamed->instance.setPriority(kOverlayLoaderPriority);
_streamed->instance.lockPlayer();
_streamed->withSound = _document->isAudioFile()
|| _document->isVideoFile()
|| _document->isVoiceMessage()
|| _document->isVideoMessage();
_streamed->withSound = _document
&& (_document->isAudioFile()
|| _document->isVideoFile()
|| _document->isVoiceMessage()
|| _document->isVideoMessage());
if (videoIsGifv()) {
if (videoIsGifOrUserpic()) {
_streamed->controls.hide();
} else {
refreshClipControllerGeometry();
@@ -2430,17 +2510,25 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
}
void OverlayWidget::handleStreamingError(Streaming::Error &&error) {
Expects(_document != nullptr);
Expects(_document || _photo);
if (error == Streaming::Error::NotStreamable) {
_document->setNotSupportsStreaming();
if (_document) {
_document->setNotSupportsStreaming();
} else {
_photo->setVideoPlaybackFailed();
}
} else if (error == Streaming::Error::OpenFailed) {
_document->setInappPlaybackFailed();
if (_document) {
_document->setInappPlaybackFailed();
} else {
_photo->setVideoPlaybackFailed();
}
}
if (!_documentMedia->canBePlayed()) {
redisplayContent();
} else {
if (canInitStreaming()) {
updatePlaybackState();
} else {
redisplayContent();
}
}
@@ -2543,7 +2631,7 @@ void OverlayWidget::initThemePreview() {
}
void OverlayWidget::refreshClipControllerGeometry() {
if (!_streamed || videoIsGifv()) {
if (!_streamed || videoIsGifOrUserpic()) {
return;
}
@@ -2578,7 +2666,7 @@ void OverlayWidget::playbackControlsFromFullScreen() {
}
void OverlayWidget::playbackControlsToPictureInPicture() {
if (!videoIsGifv()) {
if (!videoIsGifOrUserpic()) {
switchToPip();
}
}
@@ -2608,7 +2696,7 @@ void OverlayWidget::playbackPauseResume() {
_streamed->resumeOnCallEnd = false;
if (_streamed->instance.player().failed()) {
clearStreaming();
if (!_documentMedia->canBePlayed() || !initStreaming()) {
if (!canInitStreaming() || !initStreaming()) {
redisplayContent();
}
} else if (_streamed->instance.player().finished()
@@ -2627,7 +2715,6 @@ void OverlayWidget::playbackPauseResume() {
void OverlayWidget::restartAtSeekPosition(crl::time position) {
Expects(_streamed != nullptr);
Expects(_document != nullptr);
if (videoShown()) {
_streamed->instance.saveFrameToCover();
@@ -2638,11 +2725,12 @@ void OverlayWidget::restartAtSeekPosition(crl::time position) {
}
auto options = Streaming::PlaybackOptions();
options.position = position;
options.audioId = AudioMsgId(_document, _msgid);
if (!_streamed->withSound) {
options.mode = Streaming::Mode::Video;
options.loop = true;
} else {
Assert(_document != nullptr);
options.audioId = AudioMsgId(_document, _msgid);
options.speed = Core::App().settings().videoPlaybackSpeed();
if (_pip) {
_pip = nullptr;
@@ -2706,7 +2794,7 @@ void OverlayWidget::playbackControlsSpeedChanged(float64 speed) {
Core::App().settings().setVideoPlaybackSpeed(speed);
Core::App().saveSettingsDelayed();
}
if (_streamed && !videoIsGifv()) {
if (_streamed && !videoIsGifOrUserpic()) {
DEBUG_LOG(("Media playback speed: %1 to _streamed.").arg(speed));
_streamed->instance.setSpeed(speed);
}
@@ -2743,7 +2831,7 @@ void OverlayWidget::switchToPip() {
void OverlayWidget::playbackToggleFullScreen() {
Expects(_streamed != nullptr);
if (!videoShown() || (videoIsGifv() && !_fullScreenVideo)) {
if (!videoShown() || (videoIsGifOrUserpic() && !_fullScreenVideo)) {
return;
}
_fullScreenVideo = !_fullScreenVideo;
@@ -2797,7 +2885,7 @@ void OverlayWidget::playbackPauseMusic() {
void OverlayWidget::updatePlaybackState() {
Expects(_streamed != nullptr);
if (videoIsGifv()) {
if (videoIsGifOrUserpic()) {
return;
}
const auto state = _streamed->instance.player().prepareLegacyState();

View File

@@ -283,7 +283,7 @@ private:
void refreshClipControllerGeometry();
void refreshCaptionGeometry();
[[nodiscard]] bool initStreaming(bool continueStreaming = false);
bool initStreaming(bool continueStreaming = false);
void startStreamingPlayer();
void initStreamingThumbnail();
void streamingReady(Streaming::Information &&info);
@@ -346,7 +346,7 @@ private:
void applyVideoSize();
[[nodiscard]] bool videoShown() const;
[[nodiscard]] QSize videoSize() const;
[[nodiscard]] bool videoIsGifv() const;
[[nodiscard]] bool videoIsGifOrUserpic() const;
[[nodiscard]] QImage videoFrame() const;
[[nodiscard]] QImage videoFrameForDirectPaint() const;
[[nodiscard]] QImage transformVideoFrame(QImage frame) const;
@@ -356,6 +356,7 @@ private:
void paintTransformedVideoFrame(Painter &p);
void paintTransformedStaticContent(Painter &p);
void clearStreaming(bool savePosition = true);
bool canInitStreaming() const;
QBrush _transparentBrush;

View File

@@ -7,12 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/linux_libs.h"
#include "base/platform/base_platform_info.h"
#include "platform/linux/linux_xlib_helper.h"
#include "platform/linux/linux_gdk_helper.h"
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/specific_linux.h"
#include "core/sandbox.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "main/main_domain.h"
#include "mainwindow.h"
@@ -22,6 +21,7 @@ namespace Libs {
namespace {
bool gtkTriedToInit = false;
bool gtkLoaded = false;
bool loadLibrary(QLibrary &lib, const char *name, int version) {
#if defined DESKTOP_APP_USE_PACKAGED && !defined DESKTOP_APP_USE_PACKAGED_LAZY
@@ -44,21 +44,6 @@ bool loadLibrary(QLibrary &lib, const char *name, int version) {
}
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
template <typename T>
T gtkSetting(const gchar *propertyName) {
GtkSettings *settings = gtk_settings_get_default();
T value;
g_object_get(settings, propertyName, &value, nullptr);
return value;
}
QString gtkSetting(const gchar *propertyName) {
gchararray value = gtkSetting<gchararray>(propertyName);
QString str = QString::fromUtf8(value);
g_free(value);
return str;
}
void gtkMessageHandler(
const gchar *log_domain,
GLogLevelFlags log_level,
@@ -75,6 +60,7 @@ void gtkMessageHandler(
bool setupGtkBase(QLibrary &lib_gtk) {
if (!LOAD_SYMBOL(lib_gtk, "gtk_init_check", gtk_init_check)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_check_version", gtk_check_version)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_settings_get_default", gtk_settings_get_default)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_show", gtk_widget_show)) return false;
@@ -124,17 +110,12 @@ bool setupGtkBase(QLibrary &lib_gtk) {
if (!LOAD_SYMBOL(lib_gtk, "gdk_atom_intern", gdk_atom_intern)) return false;
if (LOAD_SYMBOL(lib_gtk, "gdk_set_allowed_backends", gdk_set_allowed_backends)) {
// We work only with X11 GDK backend.
// We work only with Wayland and X11 GDK backends.
// Otherwise we get segfault in Ubuntu 17.04 in gtk_init_check() call.
// See https://github.com/telegramdesktop/tdesktop/issues/3176
// See https://github.com/telegramdesktop/tdesktop/issues/3162
if(Platform::IsWayland() && !lib_gtk.fileName().contains("gtk-x11-2.0")) {
DEBUG_LOG(("Limit allowed GDK backends to wayland"));
gdk_set_allowed_backends("wayland");
} else {
DEBUG_LOG(("Limit allowed GDK backends to x11"));
gdk_set_allowed_backends("x11");
}
DEBUG_LOG(("Limit allowed GDK backends to wayland and x11"));
gdk_set_allowed_backends("wayland,x11");
}
// gtk_init will reset the Xlib error handler, and that causes
@@ -173,10 +154,12 @@ bool IconThemeShouldBeSet() {
void SetIconTheme() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
if (IconThemeShouldBeSet()) {
if (GtkSettingSupported()
&& GtkLoaded()
&& IconThemeShouldBeSet()) {
DEBUG_LOG(("Set GTK icon theme"));
QIcon::setThemeName(gtkSetting("gtk-icon-theme-name"));
QIcon::setFallbackThemeName(gtkSetting("gtk-fallback-icon-theme"));
QIcon::setThemeName(GtkSetting("gtk-icon-theme-name"));
QIcon::setFallbackThemeName(GtkSetting("gtk-fallback-icon-theme"));
Platform::SetApplicationIcon(Window::CreateIcon());
if (App::wnd()) {
App::wnd()->setWindowIcon(Window::CreateIcon());
@@ -185,12 +168,25 @@ void SetIconTheme() {
}
});
}
void DarkModeChanged() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().settings().setSystemDarkMode(Platform::IsDarkMode());
});
}
void DecorationLayoutChanged() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().settings().setWindowControlsLayout(Platform::WindowControlsLayout());
});
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
f_gtk_init_check gtk_init_check = nullptr;
f_gtk_check_version gtk_check_version = nullptr;
f_gtk_settings_get_default gtk_settings_get_default = nullptr;
f_gtk_widget_show gtk_widget_show = nullptr;
f_gtk_widget_hide gtk_widget_hide = nullptr;
@@ -245,6 +241,10 @@ f_gdk_pixbuf_get_pixels gdk_pixbuf_get_pixels = nullptr;
f_gdk_pixbuf_get_width gdk_pixbuf_get_width = nullptr;
f_gdk_pixbuf_get_height gdk_pixbuf_get_height = nullptr;
f_gdk_pixbuf_get_rowstride gdk_pixbuf_get_rowstride = nullptr;
bool GtkLoaded() {
return gtkLoaded;
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
void start() {
@@ -255,7 +255,6 @@ void start() {
DEBUG_LOG(("Loading libraries"));
bool gtkLoaded = false;
QLibrary lib_gtk;
lib_gtk.setLoadHints(QLibrary::DeepBindHint);
@@ -284,6 +283,15 @@ void start() {
const auto settings = gtk_settings_get_default();
g_signal_connect(settings, "notify::gtk-icon-theme-name", G_CALLBACK(SetIconTheme), nullptr);
g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(DarkModeChanged), nullptr);
if (!gtk_check_version(3, 0, 0)) {
g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(DarkModeChanged), nullptr);
}
if (!gtk_check_version(3, 12, 0)) {
g_signal_connect(settings, "notify::gtk-decoration-layout", G_CALLBACK(DecorationLayoutChanged), nullptr);
}
} else {
LOG(("Could not load gtk-3 or gtk-x11-2.0!"));
}

View File

@@ -29,6 +29,10 @@ extern "C" {
namespace Platform {
namespace Libs {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
bool GtkLoaded();
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
void start();
template <typename Function>
@@ -50,6 +54,9 @@ bool load(QLibrary &lib, const char *name, Function &func) {
typedef gboolean (*f_gtk_init_check)(int *argc, char ***argv);
extern f_gtk_init_check gtk_init_check;
typedef const gchar* (*f_gtk_check_version)(guint required_major, guint required_minor, guint required_micro);
extern f_gtk_check_version gtk_check_version;
typedef GtkSettings* (*f_gtk_settings_get_default)(void);
extern f_gtk_settings_get_default gtk_settings_get_default;
@@ -252,6 +259,25 @@ extern f_gdk_pixbuf_get_height gdk_pixbuf_get_height;
typedef int (*f_gdk_pixbuf_get_rowstride)(const GdkPixbuf *pixbuf);
extern f_gdk_pixbuf_get_rowstride gdk_pixbuf_get_rowstride;
inline bool GtkSettingSupported() {
return gtk_settings_get_default != nullptr;
}
template <typename T>
inline T GtkSetting(const gchar *propertyName) {
GtkSettings *settings = gtk_settings_get_default();
T value;
g_object_get(settings, propertyName, &value, nullptr);
return value;
}
inline QString GtkSetting(const gchar *propertyName) {
gchararray value = GtkSetting<gchararray>(propertyName);
QString str = QString::fromUtf8(value);
g_free(value);
return str;
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace Libs

View File

@@ -151,7 +151,6 @@ QIcon TrayIconGen(int counter, bool muted) {
24,
32,
48,
64
};
for (const auto iconSize : iconSizes) {
@@ -427,7 +426,7 @@ void MainWindow::initHook() {
|| QSystemTrayIcon::isSystemTrayAvailable();
LOG(("System tray available: %1").arg(Logs::b(trayAvailable)));
cSetSupportTray(trayAvailable);
Platform::SetTrayIconSupported(trayAvailable);
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
auto sniWatcher = new QDBusServiceWatcher(
@@ -592,9 +591,9 @@ void MainWindow::onSNIOwnerChanged(
const auto trayAvailable = SNIAvailable
|| QSystemTrayIcon::isSystemTrayAvailable();
cSetSupportTray(trayAvailable);
Platform::SetTrayIconSupported(trayAvailable);
if (cSupportTray()) {
if (trayAvailable) {
psSetupTrayIcon();
} else {
LOG(("System tray is not available."));
@@ -655,9 +654,9 @@ void MainWindow::psSetupTrayIcon() {
}
void MainWindow::workmodeUpdated(DBIWorkMode mode) {
if (!cSupportTray()) return;
if (mode == dbiwmWindowOnly) {
if (!Platform::TrayIconSupported()) {
return;
} else if (mode == dbiwmWindowOnly) {
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
if (_sniTrayIcon) {
_sniTrayIcon->setContextMenu(0);

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