Merge tag 'v6.7.8' into dev

# Conflicts:
#	.github/workflows/mac_packaged.yml
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/api/api_chat_invite.h
#	Telegram/SourceFiles/boxes/language_box.cpp
#	Telegram/SourceFiles/boxes/send_files_box.h
#	Telegram/SourceFiles/boxes/sticker_set_box.cpp
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/data/data_types.h
#	Telegram/SourceFiles/history/history_item.cpp
#	Telegram/SourceFiles/history/history_widget.cpp
#	Telegram/SourceFiles/history/view/history_view_chat_section.cpp
#	Telegram/SourceFiles/history/view/history_view_message.cpp
#	Telegram/SourceFiles/history/view/history_view_translate_bar.cpp
#	Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp
#	Telegram/SourceFiles/platform/mac/main_window_mac.mm
#	Telegram/SourceFiles/settings/sections/settings_main.cpp
#	Telegram/lib_ui
#	snap/snapcraft.yaml
This commit is contained in:
AlexeyZavar
2026-04-24 20:47:17 +03:00
308 changed files with 13057 additions and 2300 deletions
+17
View File
@@ -350,6 +350,8 @@ PRIVATE
api/api_statistics_data_deserialize.h
api/api_statistics_sender.cpp
api/api_statistics_sender.h
api/api_stickers_creator.cpp
api/api_stickers_creator.h
api/api_suggest_post.cpp
api/api_suggest_post.h
api/api_text_entities.cpp
@@ -515,6 +517,8 @@ PRIVATE
boxes/send_gif_with_caption_box.h
boxes/send_files_box.cpp
boxes/send_files_box.h
boxes/send_files_box_reply_header.cpp
boxes/send_files_box_reply_header.h
boxes/share_box.cpp
boxes/share_box.h
boxes/star_gift_auction_box.cpp
@@ -531,6 +535,8 @@ PRIVATE
boxes/star_gift_preview_box.h
boxes/star_gift_resale_box.cpp
boxes/star_gift_resale_box.h
boxes/sticker_creator_box.cpp
boxes/sticker_creator_box.h
boxes/sticker_set_box.cpp
boxes/sticker_set_box.h
boxes/stickers_box.cpp
@@ -626,6 +632,8 @@ PRIVATE
chat_helpers/emoji_keywords.h
chat_helpers/emoji_list_widget.cpp
chat_helpers/emoji_list_widget.h
chat_helpers/emoji_picker_overlay.cpp
chat_helpers/emoji_picker_overlay.h
chat_helpers/emoji_sets_manager.cpp
chat_helpers/emoji_sets_manager.h
chat_helpers/emoji_suggestions_widget.cpp
@@ -662,6 +670,8 @@ PRIVATE
chat_helpers/ttl_media_layer_widget.h
core/application.cpp
core/application.h
core/proxy_rotation_manager.cpp
core/proxy_rotation_manager.h
core/cached_webview_availability.h
core/bank_card_click_handler.cpp
core/bank_card_click_handler.h
@@ -1593,6 +1603,8 @@ PRIVATE
mtproto/facade.h
mtproto/mtp_instance.cpp
mtproto/mtp_instance.h
mtproto/proxy_check.cpp
mtproto/proxy_check.h
mtproto/sender.h
mtproto/session.cpp
mtproto/session.h
@@ -1656,6 +1668,8 @@ PRIVATE
platform/linux/webauthn_linux.cpp
platform/mac/file_utilities_mac.mm
platform/mac/file_utilities_mac.h
platform/mac/global_menu_mac.h
platform/mac/global_menu_mac.mm
platform/mac/launcher_mac.mm
platform/mac/launcher_mac.h
platform/mac/integration_mac.mm
@@ -1813,6 +1827,8 @@ PRIVATE
settings/sections/settings_local_passcode.h
settings/sections/settings_main.cpp
settings/sections/settings_main.h
settings/settings_recent_searches.cpp
settings/settings_recent_searches.h
settings/settings_search.cpp
settings/settings_search.h
settings/settings_faq_suggestions.cpp
@@ -2004,6 +2020,7 @@ PRIVATE
window/window_section_common.h
window/window_separate_id.cpp
window/window_separate_id.h
window/session/window_session_media.cpp
window/window_session_controller.cpp
window/window_session_controller.h
window/window_session_controller_link_info.h
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="96.5 111.3 766.5 766.5" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" fill="none" fill-rule="evenodd">
<path d="M 229.278 428.274 C 220.345 431.087 209.724 433.028 201.037 428.414 C 195.269 425.35 191.59 419.715 189.754 413.573 C 183.55 392.827 189.646 312.841 194.598 289.706 C 197.361 276.798 201.636 264.234 208.088 252.692 C 225.944 220.748 254.345 196.434 289.828 186.42 C 299.548 183.676 309.688 182.14 319.754 181.423 C 366.51 178.095 414.437 180.111 461.345 180.132 C 499.997 180.149 540.285 177.606 578.657 181.934 C 589.925 183.204 601.183 185.766 611.125 191.369 C 638.232 206.647 660.549 233.285 682.155 255.355 L 741.115 315.299 L 790.708 365.561 C 856.391 432.25 853.313 440.991 854.144 532.569 L 854.982 618.399 C 855.281 646.04 857.577 702.741 854.521 727.102 C 852.435 744.448 846.915 761.205 838.282 776.393 C 820.298 808.656 790.121 832.354 754.514 842.176 C 724.089 850.54 664.593 848.635 631.504 848.632 L 502.763 848.626 L 395.022 848.66 C 368.985 848.68 338.582 849.327 312.926 846.33 C 294.671 844.264 277.017 838.552 261.012 829.532 C 228.881 811.132 205.527 780.551 196.236 744.708 C 190.977 725.111 181.752 660.54 192.22 640.91 C 194.552 636.536 200.443 631.193 204.584 628.088 C 212.69 627.794 223.61 625.977 229.827 632.364 C 238.468 641.24 238.784 653.684 239.449 665.298 C 241.05 693.25 237.429 726.072 251.724 751.22 C 263.068 771.178 283.96 788.616 306.011 795.036 C 326.159 800.066 359.183 798.753 380.622 798.749 L 460.193 798.696 L 703.391 798.695 C 736.68 798.43 755.087 795.322 779.818 770.591 C 807.116 743.293 806.834 720.038 807.012 684.161 C 807.237 638.481 802.094 586.261 804.093 541.122 C 797.284 541.268 784.894 540.995 778.473 540.182 L 670.807 540.171 C 620.731 540.215 563.716 547.86 526.38 508.874 C 515.394 497.402 505.295 482.717 501.211 466.939 C 495.154 439.227 496.977 401.501 496.999 372.596 L 497.103 265.614 C 497.25 253.886 497.577 242.161 498.083 230.443 C 460.209 229.51 422.322 229.222 384.438 229.578 C 335.986 229.564 309.732 221.795 269.362 254.551 C 217.116 296.943 257.721 389.29 229.559 427.897 L 229.278 428.274 z M 543.987 230.048 C 569.591 229.725 580.035 227.497 600.857 244.842 C 616.603 257.958 630.079 272.64 644.427 287.248 L 711.181 355.245 L 759.072 403.921 C 769.77 414.798 785.756 430.108 793.914 442.441 C 801.17 453.41 801.179 474.426 802.716 487.848 L 663.551 488.081 C 655.173 488.09 645.943 488.449 637.627 488.107 C 619.637 487.367 594.727 490.338 577.846 484.153 C 545.853 472.432 543.843 436.102 544.247 407.389 C 544.438 393.841 544.247 379.666 544.256 365.887 C 544.495 320.607 544.405 275.326 543.987 230.048 z" fill="#FFFFFF"></path>
<path d="M 306.755 391.204 C 311.745 389.305 333.398 408.89 337.28 412.89 C 364.174 440.939 392.794 467.428 419.422 495.698 C 442.291 519.978 433.144 542.942 411.651 563.866 C 381.068 593.638 352.265 625.515 320.324 653.743 C 313.888 659.431 296.743 655.829 288.97 654.425 C 287.039 649.138 280.827 633.452 282.505 628.63 C 289.515 608.489 314.352 590.824 328.353 575.581 C 338.311 564.74 345.544 555.412 356.226 547.597 L 356.28 545.853 C 344.978 549.112 335.336 551.079 323.481 551.267 C 274.259 551.846 224.991 551.501 175.755 551.624 C 155.295 551.676 133.425 553.912 116.627 541.311 C 114.956 537.351 114 533.126 113.802 528.833 C 113.091 514.333 117.1 501.472 133.314 500.562 C 154.052 499.399 174.698 499.879 195.467 499.925 C 239.08 500.336 282.696 500.334 326.309 499.918 C 332.541 499.822 343.551 500.1 347.833 495.685 C 347.436 491.434 313.367 461.839 307.219 455.232 C 291.043 437.85 275.937 429.066 286.23 402.426 C 291.881 397.453 299.873 394.302 306.755 391.204 z" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="6px" height="6px" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
<circle cx="3" cy="3" r="3" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Menu / text_framed</title>
<g id="Icon-/-Menu-/-text_framed" stroke="none" fill="none" fill-rule="evenodd" stroke-width="1">
<g id="Group-Copy" transform="translate(2.5, 2.5)" fill="#FFFFFF">
<path d="M14,-0.599999989 C17.0927946,-0.599999989 19.6,1.9072054 19.6,5 L19.6,14 C19.6,17.0927946 17.0927946,19.6 14,19.6 L5,19.6 C1.9072054,19.6 -0.6,17.0927946 -0.6,14 L-0.6,5 C-0.6,1.9072054 1.9072054,-0.599999989 5,-0.599999989 L14,-0.599999989 Z M14,0.6 L5,0.6 L4.55012557,0.622716709 C2.33139882,0.848040676 0.6,2.72182541 0.6,5 L0.6,14 C0.6,16.4300529 2.5699471,18.4 5,18.4 L14,18.4 C16.4300529,18.4 18.4,16.4300529 18.4,14 L18.4,5 C18.4,2.5699471 16.4300529,0.6 14,0.6 Z M9.97655065,4.03401385 C10.3593484,4.17343225 10.6607815,4.47517036 10.7998126,4.85810895 L14.110978,13.9781761 C14.233488,14.3156099 14.0592578,14.688468 13.7218239,14.810978 C13.3843901,14.933488 13.011532,14.7592578 12.889022,14.4218239 L11.966,11.88 L7.036,11.88 L6.11075332,14.4224419 C5.98790207,14.7597516 5.61486788,14.9336046 5.27755814,14.8107533 C4.94024839,14.6879021 4.76639543,14.3148679 4.88924668,13.9775581 L8.21110911,4.85679966 C8.4714163,4.14208042 9.2618314,3.77370666 9.97655065,4.03401385 Z M9.43261575,5.30168339 L7.509,10.58 L11.494,10.58 L9.57785654,5.30175685 C9.57005611,5.28027185 9.55314403,5.26334264 9.53166693,5.25552049 C9.49156717,5.2409158 9.44722044,5.26158363 9.43261575,5.30168339 Z" id="Shape" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Menu / text_plain</title>
<g id="Icon-/-Menu-/-text_plain" stroke="none" fill="none" fill-rule="nonzero">
<path d="M12.5653175,4.74076496 C13.033686,4.90830287 13.4021688,5.27715336 13.5692393,5.74568882 L17.7064322,17.3481001 C17.8455527,17.7382518 17.6420516,18.1673117 17.2518999,18.3064322 C16.8617482,18.4455527 16.4326883,18.2420516 16.2935678,17.8518999 L15.1227957,14.570379 L8.87979566,14.570379 L7.70618054,17.8526045 C7.56667093,18.2426173 7.13740824,18.4456902 6.74739549,18.3061805 C6.35738275,18.1666709 6.15430984,17.7374082 6.29381946,17.3473955 L10.4443687,5.74413013 C10.7529809,4.88137467 11.702562,4.43215278 12.5653175,4.74076496 Z M11.8567298,6.24933915 L9.41579566,13.070379 L14.5877957,13.070379 L12.1563749,6.24948861 C12.1403545,6.20456056 12.1050205,6.16919133 12.0601085,6.15312605 C11.9773785,6.1235331 11.8863228,6.16660918 11.8567298,6.24933915 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Menu / text_semi</title>
<g id="Icon-/-Menu-/-text_semi" stroke="none" fill="none">
<path d="M8,2 L16,2 C19.3137085,2 22,4.6862915 22,8 L22,16 C22,19.3137085 19.3137085,22 16,22 L8,22 C4.6862915,22 2,19.3137085 2,16 L2,8 C2,4.76160306 4.56557489,2.12242824 7.77506174,2.00413847 L8,2 Z" id="Rectangle" fill="#FFFFFF" fill-rule="evenodd" opacity="0.15"></path>
<path d="M12.4765507,6.53401385 C12.8593484,6.67343225 13.1607815,6.97517036 13.2998126,7.35810895 L16.610978,16.4781761 C16.733488,16.8156099 16.5592578,17.188468 16.2218239,17.310978 C15.8843901,17.433488 15.511532,17.2592578 15.389022,16.9218239 L14.4658231,14.38048 L9.53582311,14.38048 L8.61075332,16.9224419 C8.48790207,17.2597516 8.11486788,17.4336046 7.77755814,17.3107533 C7.44024839,17.1879021 7.26639543,16.8148679 7.38924668,16.4775581 L10.7111091,7.35679966 C10.9714163,6.64208042 11.7618314,6.27370666 12.4765507,6.53401385 Z M11.9326158,7.80168339 L10.0088231,13.08048 L13.9948231,13.08048 L12.0778565,7.80175685 C12.0700561,7.78027185 12.053144,7.76334264 12.0316669,7.75552049 C11.9915672,7.7409158 11.9472204,7.76158363 11.9326158,7.80168339 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 21 21">
<g fill="none" fill-rule="evenodd" stroke="white" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" transform="translate(2 2)">
<path d="m.5 4.5c0-2.209139 1.79086-4 4-4"/>
<path d="m16.5 4.5c0-2.209139-1.79086-4-4-4"/>
<path d="m16.5 12.5c0 2.209139-1.79086 4-4 4"/>
<path d="m.5 12.5c0 2.209139 1.79086 4 4 4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 426 B

+64
View File
@@ -339,6 +339,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_invalid" = "The proxy link is invalid.";
"lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version.";
"lng_proxy_incorrect_secret" = "This proxy link uses invalid **secret** parameter. Please contact the proxy provider and ask him to update MTProxy source code and configure it with a correct **secret** value. Then let him provide a new link.";
"lng_proxy_check_ip_warning_title" = "Warning";
"lng_proxy_check_ip_warning" = "This will expose your IP address to the admin of the proxy server.";
"lng_proxy_check_ip_proceed" = "Proceed";
"lng_edit_deleted" = "This message was deleted";
"lng_edit_limit_reached#one" = "You've reached the message text limit. Please make the text shorter by {count} character.";
@@ -1236,6 +1239,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_use_system_settings" = "Use system proxy settings";
"lng_proxy_use_custom" = "Use custom proxy";
"lng_proxy_use_for_calls" = "Use proxy for calls";
"lng_proxy_auto_switch" = "Auto-switch proxies";
"lng_proxy_auto_switch_about" = "You can choose how quickly the app should auto-connect to the nearest active proxy if the current one stops working.";
"lng_proxy_auto_switch_timeout#one" = "{count} s";
"lng_proxy_auto_switch_timeout#other" = "{count} s";
"lng_proxy_about" = "Proxy servers may be helpful in accessing Telegram if there is no connection in a specific region.";
"lng_proxy_add" = "Add proxy";
"lng_proxy_share" = "Share";
@@ -1388,6 +1395,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_blocked_list_confirm_title" = "Block {name}";
"lng_blocked_list_confirm_text" = "Do you want to block {name} from messaging and calling you on Telegram?";
"lng_blocked_list_confirm_clear" = "Delete this chat";
"lng_blocked_list_confirm_reply" = "Delete this reply";
"lng_blocked_list_confirm_reply_all" = "Delete all replies from {user}";
"lng_blocked_list_confirm_ok" = "Block";
"lng_blocked_list_empty_title" = "No blocked users";
"lng_blocked_list_empty_description" = "You haven't blocked anyone yet.";
@@ -4496,16 +4505,51 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stickers_context_edit_name" = "Edit name";
"lng_stickers_context_delete" = "Delete sticker";
"lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?";
"lng_emoji_context_delete" = "Delete emoji";
"lng_emoji_context_delete_sure" = "Are you sure you want to delete this emoji from your set?";
"lng_stickers_context_delete_pack" = "Delete Pack";
"lng_stickers_context_delete_pack_everyone" = "Delete for Everyone";
"lng_stickers_context_delete_pack_self" = "Delete for Myself";
"lng_stickers_delete_pack_sure" = "Are you sure you want to delete this sticker set for everyone? This cannot be undone.";
"lng_stickers_bot_more_options" = "Check the {bot} bot for more options";
"lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
"lng_stickers_box_edit_name_about" = "Choose a name for your set.";
"lng_stickers_creator_badge" = "edit";
"lng_stickers_create_new" = "Create a New Sticker";
"lng_stickers_add_existing" = "Add an Existing Sticker";
"lng_stickers_add_to_set" = "Add to Sticker Set";
"lng_stickers_already_in_set" = "This Sticker is already in the Set.";
"lng_stickers_set_is_full" = "This Sticker Set is full.";
"lng_emoji_add_to_set" = "Add to Emoji Set";
"lng_emoji_already_in_set" = "This Emoji is already in the Set.";
"lng_emoji_set_is_full" = "This Emoji Set is full.";
"lng_emoji_added" = "Emoji added.";
"lng_stickers_pack_choose_emoji_title" = "Choose Emoji";
"lng_stickers_pack_choose_emoji_about" = "Pick an emoji that corresponds to this sticker.";
"lng_stickers_pick_existing_title" = "Choose Sticker";
"lng_stickers_pick_existing_about" = "Pick a sticker from your library to add it to this set.";
"lng_stickers_pick_existing_empty" = "You don't have any stickers yet.";
"lng_stickers_create_image_title" = "New Sticker";
"lng_stickers_create_image_about" = "Choose an image to add as a sticker.";
"lng_stickers_create_choose_image" = "Choose Image";
"lng_stickers_create_image_filter" = "Images";
"lng_stickers_create_open_failed" = "Could not load image.";
"lng_stickers_create_too_small" = "The image must be at least {size} pixels on each side.";
"lng_stickers_create_choose_emoji" = "Choose an emoji that corresponds to your sticker:";
"lng_stickers_create_emoji_about" = "Choose emojis that match your sticker";
"lng_stickers_create_emoji_required" = "Please choose an emoji.";
"lng_stickers_create_uploading" = "Uploading sticker…";
"lng_stickers_create_upload_failed" = "Sticker upload failed. Please try again.";
"lng_stickers_create_added" = "Sticker added.";
"lng_in_dlg_photo" = "Photo";
"lng_in_dlg_album" = "Album";
"lng_in_dlg_video" = "Video";
"lng_in_dlg_audio_file" = "Audio file";
"lng_in_dlg_contact" = "Contact";
"lng_in_dlg_audio" = "Voice message";
"lng_in_dlg_audio_unread" = "{emoji} Voice message";
"lng_in_dlg_video_message" = "Video message";
"lng_in_dlg_voice_message_ttl" = "One-time Voice Message";
"lng_in_dlg_video_message_ttl" = "One-time Video Message";
@@ -5120,6 +5164,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_reactions" = "Reactions contain emoji from **{name} pack**.";
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
"lng_context_animated_poll_option" = "This option contains emoji from **{name} pack**.";
"lng_context_animated_poll_option_many#one" = "This option contains emoji from **{count} pack**.";
"lng_context_animated_poll_option_many#other" = "This option contains emoji from **{count} packs**.";
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
@@ -5597,6 +5644,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mediaview_playback_speed" = "Playback speed: {speed}";
"lng_mediaview_rotate_video" = "Rotate video";
"lng_mediaview_quality_auto" = "Auto";
"lng_mediaview_quality_original" = "Original ({quality}p)";
"lng_theme_preview_title" = "Theme Preview";
"lng_theme_preview_generating" = "Generating color theme preview...";
@@ -7170,10 +7218,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_photo_editor_menu_flip" = "Flip";
"lng_photo_editor_menu_duplicate" = "Duplicate";
"lng_photo_editor_text_style_plain" = "Plain";
"lng_photo_editor_text_style_framed" = "Framed";
"lng_photo_editor_text_style_semi_transparent" = "Semi-Transparent";
"lng_photo_editor_crop_original" = "Original";
"lng_photo_editor_crop_square" = "Square";
"lng_photo_editor_crop_free" = "Free";
"lng_photo_editor_corners_about" = "Choose how rounded the sticker corners should look.";
"lng_photo_editor_corners_large" = "Large";
"lng_photo_editor_corners_medium" = "Medium";
"lng_photo_editor_corners_small" = "Small";
"lng_photo_editor_corners_none" = "None";
"lng_voice_speed_slow" = "Slow";
"lng_voice_speed_normal" = "Normal";
"lng_voice_speed_medium" = "Medium";
@@ -7838,6 +7896,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_ai_compose_apply_style" = "Apply Style";
"lng_ai_compose_style_tooltip" = "Choose Style";
"lng_send_as_file_tooltip" = "Send text as a file.";
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";
@@ -7904,6 +7964,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mac_hold_to_quit" = "Hold {text} to Quit";
"lng_sr_country_column_name" = "Country name";
"lng_sr_languages_column_native" = "Native name";
"lng_sr_languages_column_name" = "Language name";
// Keys finished
+1 -1
View File
@@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="6.7.5.0" />
Version="6.7.8.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
+4 -4
View File
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,7,5,0
PRODUCTVERSION 6,7,5,0
FILEVERSION 6,7,8,0
PRODUCTVERSION 6,7,8,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "6.7.5.0"
VALUE "FileVersion", "6.7.8.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "6.7.5.0"
VALUE "ProductVersion", "6.7.8.0"
END
END
BLOCK "VarFileInfo"
+4 -4
View File
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 6,7,5,0
PRODUCTVERSION 6,7,5,0
FILEVERSION 6,7,8,0
PRODUCTVERSION 6,7,8,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "6.7.5.0"
VALUE "FileVersion", "6.7.8.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "6.7.5.0"
VALUE "ProductVersion", "6.7.8.0"
END
END
BLOCK "VarFileInfo"
+269 -255
View File
@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_color_indices.h"
#include "styles/style_credits.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
@@ -48,6 +49,69 @@ namespace Api {
namespace {
struct InviteParticipant {
not_null<UserData*> user;
Ui::PeerUserpicView userpic;
};
struct ChatInvite {
QString title;
QString about;
PhotoData *photo = nullptr;
int participantsCount = 0;
std::vector<InviteParticipant> participants;
bool isPublic = false;
bool isChannel = false;
bool isMegagroup = false;
bool isBroadcast = false;
bool isRequestNeeded = false;
bool isFake = false;
bool isScam = false;
bool isVerified = false;
};
[[nodiscard]] ChatInvite ParseInvite(
not_null<Main::Session*> session,
const MTPDchatInvite &data) {
auto participants = std::vector<InviteParticipant>();
if (const auto list = data.vparticipants()) {
participants.reserve(list->v.size());
for (const auto &participant : list->v) {
if (const auto user = session->data().processUser(participant)) {
participants.push_back(InviteParticipant{ user });
}
}
}
const auto photo = session->data().processPhoto(data.vphoto());
return {
.title = qs(data.vtitle()),
.about = data.vabout().value_or_empty(),
.photo = (photo->isNull() ? nullptr : photo.get()),
.participantsCount = data.vparticipants_count().v,
.participants = std::move(participants),
.isPublic = data.is_public(),
.isChannel = data.is_channel(),
.isMegagroup = data.is_megagroup(),
.isBroadcast = data.is_broadcast(),
.isRequestNeeded = data.is_request_needed(),
.isFake = data.is_fake(),
.isScam = data.is_scam(),
.isVerified = data.is_verified(),
};
}
[[nodiscard]] Info::Profile::BadgeType BadgeForInvite(
const ChatInvite &invite) {
using Type = Info::Profile::BadgeType;
return invite.isVerified
? Type::Verified
: invite.isScam
? Type::Scam
: invite.isFake
? Type::Fake
: Type::None;
}
void SubmitChatInvite(
base::weak_ptr<Window::SessionController> weak,
not_null<Main::Session*> session,
@@ -200,7 +264,7 @@ void ConfirmSubscriptionBox(
}
} else {
state->photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(0),
Ui::EmptyUserpic::UserpicColor(st::colorIndexRed),
name);
}
Ui::AddSkip(content);
@@ -356,6 +420,207 @@ void ConfirmSubscriptionBox(
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void ConfirmInviteBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
const MTPDchatInvite *invitePtr,
ChannelData *invitePeekChannel,
Fn<void()> submit) {
auto invite = ParseInvite(session, *invitePtr);
const auto isChannel = invite.isChannel && !invite.isMegagroup;
const auto requestApprove = invite.isRequestNeeded;
const auto count = invite.participantsCount;
struct State {
std::shared_ptr<Data::PhotoMedia> photoMedia;
std::unique_ptr<Ui::EmptyUserpic> photoEmpty;
std::vector<InviteParticipant> participants;
};
const auto state = box->lifetime().make_state<State>();
state->participants = std::move(invite.participants);
const auto status = [&] {
return invitePeekChannel
? tr::lng_channel_invite_private(tr::now)
: (!state->participants.empty()
&& int(state->participants.size()) < count)
? tr::lng_group_invite_members(tr::now, lt_count, count)
: (count > 0 && isChannel)
? tr::lng_chat_status_subscribers(
tr::now,
lt_count_decimal,
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);
}();
box->setNoContentMargin(true);
box->setWidth(st::boxWideWidth);
const auto content = box->verticalLayout();
Ui::AddSkip(content, st::confirmInvitePhotoTop);
const auto userpic = content->add(
object_ptr<Ui::RpWidget>(content),
style::al_top);
const auto photoSize = st::confirmInvitePhotoSize;
userpic->resize(Size(photoSize));
userpic->setNaturalWidth(photoSize);
userpic->paintRequest(
) | rpl::on_next([=, small = Data::PhotoSize::Small] {
auto p = QPainter(userpic);
if (state->photoMedia) {
if (const auto image = state->photoMedia->image(small)) {
p.drawPixmap(
0,
0,
image->pix(
Size(photoSize),
{ .options = Images::Option::RoundCircle }));
}
} else if (state->photoEmpty) {
state->photoEmpty->paintCircle(
p,
0,
0,
userpic->width(),
photoSize);
}
}, userpic->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
if (const auto photo = invite.photo) {
state->photoMedia = photo->createMediaView();
state->photoMedia->wanted(
Data::PhotoSize::Small,
Data::FileOrigin());
if (!state->photoMedia->image(Data::PhotoSize::Small)) {
session->downloaderTaskFinished(
) | rpl::on_next([=] {
userpic->update();
}, userpic->lifetime());
}
} else {
state->photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(st::colorIndexRed),
invite.title);
}
Ui::AddSkip(content);
const auto title = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
invite.title,
st::confirmInviteTitle),
style::al_top);
const auto badgeType = BadgeForInvite(invite);
if (badgeType != Info::Profile::BadgeType::None) {
const auto badgeParent = title->parentWidget();
const auto badge = box->lifetime().make_state<Info::Profile::Badge>(
badgeParent,
st::infoPeerBadge,
session,
rpl::single(Info::Profile::Badge::Content{ badgeType }),
nullptr,
[] { return false; });
title->geometryValue(
) | rpl::on_next([=](const QRect &r) {
badge->move(r.x() + r.width(), r.y(), r.y() + r.height());
}, title->lifetime());
}
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
status,
st::confirmInviteStatus),
style::al_top);
if (!invite.about.isEmpty()) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
invite.about,
st::confirmInviteAbout),
st::confirmInviteAboutPadding,
style::al_top);
}
if (requestApprove) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(isChannel
? tr::lng_group_request_about_channel(tr::now)
: tr::lng_group_request_about(tr::now)),
st::confirmInviteStatus),
st::confirmInviteAboutRequestsPadding,
style::al_top);
}
if (!state->participants.empty()) {
while (state->participants.size() > 4) {
state->participants.pop_back();
}
const auto padding = (st::confirmInviteUsersWidth
- 4 * st::confirmInviteUserPhotoSize) / 10;
const auto userWidth = st::confirmInviteUserPhotoSize + 2 * padding;
auto strip = object_ptr<Ui::RpWidget>(content);
const auto rawStrip = strip.data();
rawStrip->resize(st::boxWideWidth, st::confirmInviteUserHeight);
rawStrip->setNaturalWidth(st::boxWideWidth);
const auto shown = int(state->participants.size());
const auto sumWidth = shown * userWidth;
const auto baseLeft = (st::boxWideWidth - sumWidth) / 2;
for (auto i = 0; i != shown; ++i) {
const auto &participant = state->participants[i];
const auto name = Ui::CreateChild<Ui::FlatLabel>(
rawStrip,
st::confirmInviteUserName);
name->resizeToWidth(
st::confirmInviteUserPhotoSize + padding);
name->setText(participant.user->firstName.isEmpty()
? participant.user->name()
: participant.user->firstName);
name->moveToLeft(
baseLeft + i * userWidth + (padding / 2),
st::confirmInviteUserNameTop - st::confirmInviteUserPhotoTop);
}
rawStrip->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(rawStrip);
const auto total = int(state->participants.size());
const auto totalWidth = total * userWidth;
auto left = (rawStrip->width() - totalWidth) / 2;
for (auto &participant : state->participants) {
participant.user->paintUserpicLeft(
p,
participant.userpic,
left + (userWidth - st::confirmInviteUserPhotoSize) / 2,
0,
rawStrip->width(),
st::confirmInviteUserPhotoSize);
left += userWidth;
}
}, rawStrip->lifetime());
Ui::AddSkip(content, st::boxPadding.bottom());
content->add(std::move(strip), style::margins());
}
box->addButton((requestApprove
? tr::lng_group_request_to_join()
: isChannel
? tr::lng_profile_join_channel()
: tr::lng_profile_join_group()), submit);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace
void CheckChatInvite(
@@ -399,9 +664,10 @@ void CheckChatInvite(
session,
hash,
&data))
: strong->show(Box<ConfirmInviteBox>(
: strong->show(Box(
ConfirmInviteBox,
session,
data,
&data,
invitePeekChannel,
[=] { SubmitChatInvite(weak, session, hash, isGroup); }));
if (invitePeekChannel) {
@@ -450,255 +716,3 @@ void CheckChatInvite(
}
} // namespace Api
struct ConfirmInviteBox::Participant {
not_null<UserData*> user;
Ui::PeerUserpicView userpic;
};
ConfirmInviteBox::ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
ChannelData *invitePeekChannel,
Fn<void()> submit)
: ConfirmInviteBox(
session,
Parse(session, data),
invitePeekChannel,
std::move(submit)) {
}
ConfirmInviteBox::ConfirmInviteBox(
not_null<Main::Session*> session,
ChatInvite &&invite,
ChannelData *invitePeekChannel,
Fn<void()> submit)
: _session(session)
, _submit(std::move(submit))
, _title(this, st::confirmInviteTitle)
, _badge(std::make_unique<Info::Profile::Badge>(
this,
st::infoPeerBadge,
_session,
rpl::single(Info::Profile::Badge::Content{ BadgeForInvite(invite) }),
nullptr,
[=] { return false; }))
, _status(this, st::confirmInviteStatus)
, _about(this, st::confirmInviteAbout)
, _aboutRequests(this, st::confirmInviteStatus)
, _participants(std::move(invite.participants))
, _isChannel(invite.isChannel && !invite.isMegagroup)
, _requestApprove(invite.isRequestNeeded) {
const auto count = invite.participantsCount;
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 && _isChannel)
? tr::lng_chat_status_subscribers(
tr::now,
lt_count_decimal,
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(invite.title);
_status->setText(status);
if (!invite.about.isEmpty()) {
_about->setText(invite.about);
} else {
_about.destroy();
}
if (_requestApprove) {
_aboutRequests->setText(_isChannel
? tr::lng_group_request_about_channel(tr::now)
: tr::lng_group_request_about(tr::now));
} else {
_aboutRequests.destroy();
}
if (invite.photo) {
_photo = invite.photo->createMediaView();
_photo->wanted(Data::PhotoSize::Small, Data::FileOrigin());
if (!_photo->image(Data::PhotoSize::Small)) {
_session->downloaderTaskFinished(
) | rpl::on_next([=] {
update();
}, lifetime());
}
} else {
_photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(0),
invite.title);
}
}
ConfirmInviteBox::~ConfirmInviteBox() = default;
ConfirmInviteBox::ChatInvite ConfirmInviteBox::Parse(
not_null<Main::Session*> session,
const MTPDchatInvite &data) {
auto participants = std::vector<Participant>();
if (const auto list = data.vparticipants()) {
participants.reserve(list->v.size());
for (const auto &participant : list->v) {
if (const auto user = session->data().processUser(participant)) {
participants.push_back(Participant{ user });
}
}
}
const auto photo = session->data().processPhoto(data.vphoto());
return {
.title = qs(data.vtitle()),
.about = data.vabout().value_or_empty(),
.photo = (photo->isNull() ? nullptr : photo.get()),
.participantsCount = data.vparticipants_count().v,
.participants = std::move(participants),
.isPublic = data.is_public(),
.isChannel = data.is_channel(),
.isMegagroup = data.is_megagroup(),
.isBroadcast = data.is_broadcast(),
.isRequestNeeded = data.is_request_needed(),
.isFake = data.is_fake(),
.isScam = data.is_scam(),
.isVerified = data.is_verified(),
};
}
[[nodiscard]] Info::Profile::BadgeType ConfirmInviteBox::BadgeForInvite(
const ChatInvite &invite) {
using Type = Info::Profile::BadgeType;
return invite.isVerified
? Type::Verified
: invite.isScam
? Type::Scam
: invite.isFake
? Type::Fake
: Type::None;
}
void ConfirmInviteBox::prepare() {
addButton(
(_requestApprove
? tr::lng_group_request_to_join()
: _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::confirmInviteUsersWidth - 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;
}
if (_about) {
const auto padding = st::confirmInviteAboutPadding;
_about->resizeToWidth(st::boxWideWidth - padding.left() - padding.right());
newHeight += padding.top() + _about->height() + padding.bottom();
}
if (_aboutRequests) {
const auto padding = st::confirmInviteAboutRequestsPadding;
_aboutRequests->resizeToWidth(st::boxWideWidth - padding.left() - padding.right());
newHeight += padding.top() + _aboutRequests->height() + padding.bottom();
}
setDimensions(st::boxWideWidth, newHeight);
}
void ConfirmInviteBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
const auto padding = st::boxRowPadding;
auto nameWidth = width() - padding.left() - padding.right();
auto badgeWidth = 0;
if (const auto widget = _badge->widget()) {
badgeWidth = st::infoVerifiedCheckPosition.x() + widget->width();
nameWidth -= badgeWidth;
}
_title->resizeToWidth(std::min(nameWidth, _title->textMaxWidth()));
_title->moveToLeft(
(width() - _title->width() - badgeWidth) / 2,
st::confirmInviteTitleTop);
const auto badgeLeft = _title->x() + _title->width();
const auto badgeTop = _title->y();
const auto badgeBottom = _title->y() + _title->height();
_badge->move(badgeLeft, badgeTop, badgeBottom);
_status->move(
(width() - _status->width()) / 2,
st::confirmInviteStatusTop);
auto bottom = _status->y()
+ _status->height()
+ st::boxPadding.bottom()
+ (_participants.empty() ? 0 : st::confirmInviteUserHeight);
if (_about) {
const auto padding = st::confirmInviteAboutPadding;
_about->move((width() - _about->width()) / 2, bottom + padding.top());
bottom += padding.top() + _about->height() + padding.bottom();
}
if (_aboutRequests) {
const auto padding = st::confirmInviteAboutRequestsPadding;
_aboutRequests->move((width() - _aboutRequests->width()) / 2, bottom + padding.top());
}
}
void ConfirmInviteBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
Painter p(this);
if (_photo) {
if (const auto image = _photo->image(Data::PhotoSize::Small)) {
const auto size = st::confirmInvitePhotoSize;
p.drawPixmap(
(width() - size) / 2,
st::confirmInvitePhotoTop,
image->pix(
{ size, size },
{ .options = Images::Option::RoundCircle }));
}
} else if (_photoEmpty) {
_photoEmpty->paintCircle(
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;
}
}
@@ -7,16 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/layers/box_content.h"
class UserData;
class ChannelData;
namespace Info::Profile {
class Badge;
enum class BadgeType : ushort;
} // namespace Info::Profile
namespace Main {
class Session;
} // namespace Main
@@ -25,14 +17,6 @@ namespace Window {
class SessionController;
} // namespace Window
namespace Data {
class PhotoMedia;
} // namespace Data
namespace Ui {
class EmptyUserpic;
} // namespace Ui
namespace Api {
void CheckChatInvite(
@@ -42,67 +26,3 @@ void CheckChatInvite(
Fn<void()> loaded = 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;
struct ChatInvite {
QString title;
QString about;
PhotoData *photo = nullptr;
int participantsCount = 0;
std::vector<Participant> participants;
bool isPublic = false;
bool isChannel = false;
bool isMegagroup = false;
bool isBroadcast = false;
bool isRequestNeeded = false;
bool isFake = false;
bool isScam = false;
bool isVerified = false;
};
[[nodiscard]] static ChatInvite Parse(
not_null<Main::Session*> session,
const MTPDchatInvite &data);
[[nodiscard]] Info::Profile::BadgeType BadgeForInvite(
const ChatInvite &invite);
ConfirmInviteBox(
not_null<Main::Session*> session,
ChatInvite &&invite,
ChannelData *invitePeekChannel,
Fn<void()> submit);
const not_null<Main::Session*> _session;
Fn<void()> _submit;
object_ptr<Ui::FlatLabel> _title;
std::unique_ptr<Info::Profile::Badge> _badge;
object_ptr<Ui::FlatLabel> _status;
object_ptr<Ui::FlatLabel> _about;
object_ptr<Ui::FlatLabel> _aboutRequests;
std::shared_ptr<Data::PhotoMedia> _photo;
std::unique_ptr<Ui::EmptyUserpic> _photoEmpty;
std::vector<Participant> _participants;
bool _isChannel = false;
bool _requestApprove = false;
int _userWidth = 0;
};
-2
View File
@@ -111,8 +111,6 @@ private:
const bool _isUser = false;
Data::CreditsEarnStatistics _data;
mtpRequestId _requestId = 0;
};
[[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(
@@ -58,9 +58,23 @@ constexpr auto kSearchPerPage = 50;
result += u"emoji"_q + tag.emoji();
}
}
switch (request.filter) {
case SearchFilter::NoFilter: break;
case SearchFilter::Pinned: result += u"\npinned"_q; break;
}
return result;
}
[[nodiscard]] MTPMessagesFilter PrepareFilter(SearchFilter filter) {
switch (filter) {
case SearchFilter::Pinned:
return MTP_inputMessagesFilterPinned();
case SearchFilter::NoFilter:
return MTP_inputMessagesFilterEmpty();
}
return MTP_inputMessagesFilterEmpty();
}
} // namespace
MessagesSearch::MessagesSearch(not_null<History*> history)
@@ -113,7 +127,7 @@ void MessagesSearch::searchRequest() {
Data::ReactionToMTP
)),
MTP_int(_request.topMsgId), // top_msg_id
MTP_inputMessagesFilterEmpty(),
PrepareFilter(_request.filter),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_offsetId), // offset_id
@@ -20,6 +20,11 @@ struct ReactionId;
namespace Api {
enum class SearchFilter {
NoFilter,
Pinned,
};
struct FoundMessages {
int total = -1;
MessageIdsList messages;
@@ -33,6 +38,7 @@ public:
PeerData *from = nullptr;
std::vector<Data::ReactionId> tags;
MsgId topMsgId;
SearchFilter filter = SearchFilter::NoFilter;
friend inline bool operator==(
const Request &,
@@ -75,8 +75,6 @@ private:
Data::PublicForwardsSlice _firstSlice;
mtpRequestId _requestId = 0;
};
class EarnStatistics final : public StatisticsRequestSender {
@@ -0,0 +1,543 @@
/*
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_stickers_creator.h"
#include "apiwrap.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/stickers/data_stickers.h"
#include "data/stickers/data_stickers_set.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "menu/menu_action_with_thumbnail.h"
#include "storage/file_upload.h"
#include "storage/localimageloader.h"
#include "styles/style_menu_icons.h"
#include "styles/style_widgets.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_common.h"
#include "ui/widgets/popup_menu.h"
namespace Api {
namespace {
constexpr auto kStickerSide = 512;
[[nodiscard]] MTPInputStickerSetItem InputItem(
const MTPInputDocument &document,
const QString &emoji) {
return MTP_inputStickerSetItem(
MTP_flags(0),
document,
MTP_string(emoji),
MTPMaskCoords(),
MTPstring());
}
[[nodiscard]] std::shared_ptr<FilePrepareResult> PrepareStickerWebp(
MTP::DcId dcId,
DocumentId id,
const QByteArray &bytes) {
const auto filename = u"sticker.webp"_q;
auto attributes = QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(MTP_string(filename)));
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(kStickerSide),
MTP_int(kStickerSide)));
auto result = MakePreparedFile({
.id = id,
.type = SendMediaType::File,
});
result->filename = filename;
result->filemime = u"image/webp"_q;
result->content = bytes;
result->filesize = bytes.size();
result->setFileData(bytes);
result->document = MTP_document(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_string("image/webp"),
MTP_long(bytes.size()),
MTP_vector<MTPPhotoSize>(),
MTPVector<MTPVideoSize>(),
MTP_int(dcId),
MTP_vector<MTPDocumentAttribute>(std::move(attributes)));
return result;
}
void FeedSetIfFull(
not_null<Main::Session*> session,
const MTPmessages_StickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &data) {
session->data().stickers().feedSetFull(data);
session->data().stickers().notifyUpdated(
Data::StickersType::Stickers);
}, [](const auto &) {
});
}
template <typename Callback>
void EnumerateOwnedSets(
not_null<Main::Session*> session,
Data::StickersType type,
Callback &&callback) {
const auto &stickers = session->data().stickers();
const auto &sets = stickers.sets();
const auto &order = (type == Data::StickersType::Emoji)
? stickers.emojiSetsOrder()
: stickers.setsOrder();
for (const auto setId : order) {
const auto it = sets.find(setId);
if (it == sets.end()) {
continue;
}
const auto set = it->second.get();
if (!(set->flags & Data::StickersSetFlag::AmCreator)
|| (set->type() != type)) {
continue;
}
using namespace Data;
if constexpr (std::is_same_v<
bool,
std::invoke_result_t<Callback, not_null<StickersSet*>>>) {
if (!callback(set)) {
return;
}
} else {
callback(set);
}
}
}
void FillChooseOwnedSetMenu(
not_null<Ui::PopupMenu*> menu,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::StickersType type) {
const auto session = &show->session();
const auto emoji = StickerEmojiOrDefault(document);
const auto isEmoji = (type == Data::StickersType::Emoji);
const auto maxCount = isEmoji
? kEmojiInOwnedSetMax
: kStickersInOwnedSetMax;
const auto fullMessage = isEmoji
? tr::lng_emoji_set_is_full
: tr::lng_stickers_set_is_full;
const auto addedMessage = isEmoji
? tr::lng_emoji_added
: tr::lng_stickers_create_added;
const auto alreadyMessage = isEmoji
? tr::lng_emoji_already_in_set
: tr::lng_stickers_already_in_set;
const auto failToast = [=](QString err) {
show->showToast(err.isEmpty()
? tr::lng_attach_failed(tr::now)
: err);
};
EnumerateOwnedSets(session, type, [&](not_null<Data::StickersSet*> set) {
const auto identifier = set->identifier();
const auto coverDocument = set->lookupThumbnailDocument();
auto thumbnail = coverDocument
? Ui::MakeDocumentThumbnail(
coverDocument,
Data::FileOriginStickerSet(set->id, set->accessHash))
: nullptr;
const auto targetSetId = set->id;
const auto handler = crl::guard(session, [=] {
const auto &map = session->data().stickers().sets();
const auto i = map.find(targetSetId);
if (i != map.end() && i->second->count >= maxCount) {
show->showToast(fullMessage(tr::now));
return;
}
const auto oldCount = (i != map.end())
? i->second->count
: 0;
AddExistingStickerToSet(
session,
identifier,
document,
emoji,
crl::guard(session, [=](MTPmessages_StickerSet) {
const auto &map = session->data().stickers().sets();
const auto i = map.find(targetSetId);
const auto newCount = (i != map.end())
? i->second->count
: oldCount;
show->showToast(newCount > oldCount
? addedMessage(tr::now)
: alreadyMessage(tr::now));
}),
crl::guard(session, failToast));
});
const auto rawAction = Ui::Menu::CreateAction(
menu.get(),
set->title,
handler);
auto item = base::make_unique_q<Menu::ActionWithThumbnail>(
menu->menu(),
menu->menu()->st(),
rawAction,
std::move(thumbnail),
st::menuIconStickerAdd.width());
menu->addAction(std::move(item));
});
}
} // namespace
void AddExistingStickerToSet(
not_null<Main::Session*> session,
const StickerSetIdentifier &set,
not_null<DocumentData*> document,
const QString &emoji,
Fn<void(MTPmessages_StickerSet)> done,
Fn<void(QString)> fail) {
session->api().request(MTPstickers_AddStickerToSet(
Data::InputStickerSet(set),
InputItem(document->mtpInput(), emoji))
).done([=](const MTPmessages_StickerSet &result) {
FeedSetIfFull(session, result);
if (done) {
done(result);
}
}).fail([=](const MTP::Error &error) {
if (fail) {
fail(error.type());
}
}).handleFloodErrors().send();
}
QString StickerEmojiOrDefault(not_null<DocumentData*> document) {
if (const auto sticker = document->sticker()) {
if (!sticker->alt.isEmpty()) {
return sticker->alt;
}
}
return QString::fromUtf8("\xF0\x9F\x99\x82");
}
bool HasOwnedStickerSets(not_null<Main::Session*> session) {
auto found = false;
EnumerateOwnedSets(
session,
Data::StickersType::Stickers,
[&](not_null<Data::StickersSet*>) {
found = true;
return false;
});
return found;
}
bool HasOwnedEmojiSets(not_null<Main::Session*> session) {
auto found = false;
EnumerateOwnedSets(
session,
Data::StickersType::Emoji,
[&](not_null<Data::StickersSet*>) {
found = true;
return false;
});
return found;
}
void FillChooseStickerSetMenu(
not_null<Ui::PopupMenu*> menu,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document) {
using namespace Data;
FillChooseOwnedSetMenu(menu, show, document, StickersType::Stickers);
}
void FillChooseEmojiSetMenu(
not_null<Ui::PopupMenu*> menu,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document) {
FillChooseOwnedSetMenu(menu, show, document, Data::StickersType::Emoji);
}
void AddAddToStickerSetAction(
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document) {
const auto session = &show->session();
if (!HasOwnedStickerSets(session)) {
return;
}
addAction({
.text = tr::lng_stickers_add_to_set(tr::now),
.icon = &st::menuIconStickerAdd,
.fillSubmenu = [show, document](not_null<Ui::PopupMenu*> submenu) {
FillChooseStickerSetMenu(submenu, show, document);
},
.submenuSt = &st::popupMenuWithIcons,
});
}
void AddAddToEmojiSetAction(
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document) {
const auto session = &show->session();
if (!HasOwnedEmojiSets(session)) {
return;
}
addAction({
.text = tr::lng_emoji_add_to_set(tr::now),
.icon = &st::menuIconEmoji,
.fillSubmenu = [show, document](not_null<Ui::PopupMenu*> submenu) {
FillChooseEmojiSetMenu(submenu, show, document);
},
.submenuSt = &st::popupMenuWithIcons,
});
}
void AddAddToOwnedSetAction(
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document) {
const auto sticker = document->sticker();
const auto isLottie = sticker && sticker->isLottie();
const auto size = document->dimensions;
const auto fitsEmoji = !size.isEmpty()
&& (size.width() <= kEmojiStickerSideMax)
&& (size.height() <= kEmojiStickerSideMax);
if (isLottie || !fitsEmoji) {
AddAddToStickerSetAction(addAction, show, document);
}
if (isLottie || fitsEmoji) {
AddAddToEmojiSetAction(addAction, std::move(show), document);
}
}
void DeleteStickerSet(
not_null<Main::Session*> session,
const StickerSetIdentifier &set,
Fn<void()> done,
Fn<void(QString)> fail) {
session->api().request(MTPstickers_DeleteStickerSet(
Data::InputStickerSet(set))
).done([=] {
session->data().stickers().notifyUpdated(
Data::StickersType::Stickers);
if (done) {
done();
}
}).fail([=](const MTP::Error &error) {
if (fail) {
fail(error.type());
}
}).send();
}
StickerUpload::StickerUpload(
not_null<Main::Session*> session,
StickerSetIdentifier set,
QByteArray webpBytes,
QString emoji)
: _session(session)
, _set(std::move(set))
, _bytes(std::move(webpBytes))
, _emoji(std::move(emoji))
, _api(&session->mtp()) {
}
StickerUpload::~StickerUpload() {
cancel();
}
void StickerUpload::start(
Fn<void(MTPmessages_StickerSet)> done,
Fn<void(QString)> fail,
Fn<void(int)> progress) {
Expects(!_uploadId);
_done = std::move(done);
_fail = std::move(fail);
_progress = std::move(progress);
_documentId = base::RandomValue<DocumentId>();
auto ready = PrepareStickerWebp(
_session->mtp().mainDcId(),
_documentId,
_bytes);
_uploadId = FullMsgId(
_session->userPeerId(),
_session->data().nextLocalMessageId());
const auto document = _session->data().document(_documentId);
document->uploadingData = std::make_unique<Data::UploadState>(
document->size > 0 ? document->size : int64(_bytes.size()));
_session->uploader().documentReady(
) | rpl::filter([=](const Storage::UploadedMedia &data) {
return data.fullId == _uploadId;
}) | rpl::on_next([=](const Storage::UploadedMedia &data) {
uploadReady(data.info.file);
}, _uploadLifetime);
_session->uploader().documentFailed(
) | rpl::filter([=](const FullMsgId &id) {
return id == _uploadId;
}) | rpl::on_next([=] {
uploadFailed();
}, _uploadLifetime);
if (_progress) {
_session->uploader().documentProgress(
) | rpl::filter([=](const FullMsgId &id) {
return id == _uploadId;
}) | rpl::on_next([=] {
uploadProgressed();
}, _uploadLifetime);
}
_session->uploader().upload(_uploadId, ready);
}
void StickerUpload::cancel() {
if (_uploadId) {
_session->uploader().cancel(_uploadId);
_uploadId = FullMsgId();
}
if (_addRequestId) {
_api.request(_addRequestId).cancel();
_addRequestId = 0;
}
_uploadLifetime.destroy();
_done = nullptr;
_fail = nullptr;
_progress = nullptr;
}
void StickerUpload::uploadProgressed() {
if (!_progress) {
return;
}
const auto document = _session->data().document(_documentId);
if (!document->uploading() || !document->uploadingData) {
return;
}
const auto size = document->uploadingData->size;
if (size <= 0) {
return;
}
const auto percent = int(
(document->uploadingData->offset * 100) / size);
if (percent != _lastReportedPercent) {
_lastReportedPercent = percent;
_progress(percent);
}
}
void StickerUpload::uploadFailed() {
const auto fail = std::move(_fail);
cancel();
if (fail) {
fail(QString());
}
}
void StickerUpload::uploadReady(const MTPInputFile &file) {
_uploadLifetime.destroy();
_uploadId = FullMsgId();
auto attributes = QVector<MTPDocumentAttribute>();
attributes.push_back(MTP_documentAttributeSticker(
MTP_flags(0),
MTP_string(_emoji),
MTP_inputStickerSetEmpty(),
MTPMaskCoords()));
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(kStickerSide),
MTP_int(kStickerSide)));
const auto media = MTP_inputMediaUploadedDocument(
MTP_flags(0),
file,
MTPInputFile(),
MTP_string("image/webp"),
MTP_vector<MTPDocumentAttribute>(std::move(attributes)),
MTP_vector<MTPInputDocument>(),
MTPInputPhoto(),
MTP_int(0),
MTP_int(0));
_addRequestId = _api.request(MTPmessages_UploadMedia(
MTP_flags(0),
MTPstring(),
MTP_inputPeerSelf(),
media
)).done(crl::guard(this, [=](const MTPMessageMedia &result) {
_addRequestId = 0;
auto inputDoc = (MTPInputDocument*)(nullptr);
auto storage = MTPInputDocument();
result.match([&](const MTPDmessageMediaDocument &data) {
if (const auto doc = data.vdocument()) {
doc->match([&](const MTPDdocument &d) {
storage = MTP_inputDocument(
d.vid(),
d.vaccess_hash(),
d.vfile_reference());
inputDoc = &storage;
}, [](const auto &) {
});
}
}, [](const auto &) {
});
if (inputDoc) {
requestAddSticker(*inputDoc);
} else if (const auto fail = std::move(_fail)) {
cancel();
fail(QString());
}
})).fail(crl::guard(this, [=](const MTP::Error &error) {
_addRequestId = 0;
const auto fail = std::move(_fail);
const auto type = error.type();
cancel();
if (fail) {
fail(type);
}
})).handleFloodErrors().send();
}
void StickerUpload::requestAddSticker(const MTPInputDocument &document) {
_addRequestId = _api.request(MTPstickers_AddStickerToSet(
Data::InputStickerSet(_set),
InputItem(document, _emoji))
).done(crl::guard(this, [=](const MTPmessages_StickerSet &result) {
_addRequestId = 0;
FeedSetIfFull(_session, result);
const auto done = std::move(_done);
cancel();
if (done) {
done(result);
}
})).fail(crl::guard(this, [=](const MTP::Error &error) {
_addRequestId = 0;
const auto fail = std::move(_fail);
const auto type = error.type();
cancel();
if (fail) {
fail(type);
}
})).handleFloodErrors().send();
}
} // namespace Api
@@ -0,0 +1,121 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "data/stickers/data_stickers.h"
#include "mtproto/sender.h"
class DocumentData;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class PopupMenu;
namespace Menu {
struct MenuCallback;
} // namespace Menu
} // namespace Ui
namespace Api {
inline constexpr auto kStickersInOwnedSetMax = 120;
inline constexpr auto kEmojiInOwnedSetMax = 200;
inline constexpr auto kEmojiStickerSideMax = 100;
void AddExistingStickerToSet(
not_null<Main::Session*> session,
const StickerSetIdentifier &set,
not_null<DocumentData*> document,
const QString &emoji,
Fn<void(MTPmessages_StickerSet)> done,
Fn<void(QString)> fail);
void DeleteStickerSet(
not_null<Main::Session*> session,
const StickerSetIdentifier &set,
Fn<void()> done,
Fn<void(QString)> fail);
[[nodiscard]] bool HasOwnedStickerSets(not_null<Main::Session*> session);
[[nodiscard]] bool HasOwnedEmojiSets(not_null<Main::Session*> session);
[[nodiscard]] QString StickerEmojiOrDefault(
not_null<DocumentData*> document);
void FillChooseStickerSetMenu(
not_null<Ui::PopupMenu*> menu,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document);
void FillChooseEmojiSetMenu(
not_null<Ui::PopupMenu*> menu,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document);
void AddAddToStickerSetAction(
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document);
void AddAddToEmojiSetAction(
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document);
void AddAddToOwnedSetAction(
const Ui::Menu::MenuCallback &addAction,
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document);
class StickerUpload final : public base::has_weak_ptr {
public:
StickerUpload(
not_null<Main::Session*> session,
StickerSetIdentifier set,
QByteArray webpBytes,
QString emoji);
~StickerUpload();
void start(
Fn<void(MTPmessages_StickerSet)> done,
Fn<void(QString)> fail,
Fn<void(int /*percent*/)> progress = nullptr);
void cancel();
private:
void uploadReady(const MTPInputFile &file);
void uploadFailed();
void uploadProgressed();
void requestAddSticker(const MTPInputDocument &document);
const not_null<Main::Session*> _session;
StickerSetIdentifier _set;
QByteArray _bytes;
QString _emoji;
MTP::Sender _api;
rpl::lifetime _uploadLifetime;
FullMsgId _uploadId;
DocumentId _documentId = 0;
mtpRequestId _addRequestId = 0;
Fn<void(MTPmessages_StickerSet)> _done;
Fn<void(QString)> _fail;
Fn<void(int)> _progress;
int _lastReportedPercent = -1;
};
} // namespace Api
+3 -3
View File
@@ -59,6 +59,7 @@ boxPhotoTitlePosition: point(28px, 20px);
boxPhotoPadding: margins(28px, 28px, 28px, 18px);
boxPhotoCompressedSkip: 20px;
boxPhotoCaptionSkip: 8px;
boxPhotoCaptionReplyOverlap: 5px;
defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }};
defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }};
@@ -92,7 +93,7 @@ restoreUserpicIcon: UserpicButton(defaultUserpicButton) {
}
confirmInviteTitle: FlatLabel(defaultFlatLabel) {
align: align(center);
align: align(top);
minWidth: 320px;
maxHeight: 24px;
textFg: windowBoldFg;
@@ -101,9 +102,8 @@ confirmInviteTitle: FlatLabel(defaultFlatLabel) {
}
}
confirmInviteAbout: FlatLabel(boxLabel) {
align: align(center);
align: align(top);
minWidth: 320px;
maxHeight: 60px;
style: TextStyle(boxLabelStyle) {
lineHeight: 19px;
}
@@ -1203,6 +1203,10 @@ void ComposeAiContent::request() {
return;
}
weak->_requestId = 0;
if (MTP::IgnoreError(error)) {
weak->resetState(CardState::Waiting);
return;
}
weak->showError(error.type());
});
}
+372 -151
View File
@@ -18,6 +18,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_account.h"
#include "main/main_session.h"
#include "mtproto/facade.h"
#include "mtproto/mtproto_config.h"
#include "mtproto/proxy_check.h"
#include "qr/qr_generate.h"
#include "settings/settings_common.h"
#include "storage/localstorage.h"
#include "ui/basic_click_handlers.h"
@@ -32,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/password_input.h"
@@ -46,10 +50,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "boxes/abstract_box.h" // Ui::show().
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
@@ -62,22 +67,55 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
using ProxyData = MTP::ProxyData;
[[nodiscard]] std::vector<QString> ExtractUrlsSimple(const QString &input) {
[[nodiscard]] int ClosestProxyRotationTimeoutSection(int value) {
auto result = 0;
auto bestDistance = 0;
for (auto i = 0; i != int(Core::SettingsProxy::kProxyRotationTimeouts.size()); ++i) {
const auto current = Core::SettingsProxy::kProxyRotationTimeouts[i];
const auto distance = (current > value) ? (current - value) : (value - current);
if ((i == 0) || (distance < bestDistance)) {
result = i;
bestDistance = distance;
}
}
return result;
}
[[nodiscard]] std::vector<QString> ExtractLinkCandidates(const QString &input) {
auto urls = std::vector<QString>();
static auto urlRegex = QRegularExpression(R"((https?:\/\/[^\s]+))");
static const auto urlRegex = QRegularExpression(
R"((?:https?:\/\/[^\s]+|tg:\/\/[^\s]+|(?:www\.)?(?:t\.me|telegram\.me|telegram\.dog)\/[^\s]+))",
QRegularExpression::CaseInsensitiveOption);
auto it = urlRegex.globalMatch(input);
while (it.hasNext()) {
urls.push_back(it.next().captured(1));
urls.push_back(it.next().captured(0));
}
return urls;
}
[[nodiscard]] QString ProxyDataToString(const ProxyData &proxy) {
[[nodiscard]] bool ProxyDataIsShareable(const ProxyData &proxy) {
using Type = ProxyData::Type;
return u"tg://"_q
+ (proxy.type == Type::Socks5 ? "socks" : "proxy")
return (proxy.type == Type::Socks5)
|| (proxy.type == Type::Mtproto);
}
[[nodiscard]] QString ProxyDataToQueryPath(const ProxyData &proxy) {
using Type = ProxyData::Type;
const auto path = [&] {
switch (proxy.type) {
case Type::Socks5: return u"socks"_q;
case Type::Mtproto: return u"proxy"_q;
case Type::None:
case Type::Http: return QString();
}
Unexpected("Proxy type in ProxyDataToQueryPath.");
}();
if (path.isEmpty()) {
return QString();
}
return path
+ "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
+ ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
? "&user=" + qthelp::url_encode(proxy.user) : "")
@@ -87,6 +125,148 @@ using ProxyData = MTP::ProxyData;
? "&secret=" + proxy.password : "");
}
[[nodiscard]] QString ProxyDataToLocalLink(const ProxyData &proxy) {
const auto queryPath = ProxyDataToQueryPath(proxy);
return queryPath.isEmpty() ? QString() : (u"tg://"_q + queryPath);
}
[[nodiscard]] QString ProxyDataToPublicLink(
not_null<Main::Account*> account,
const ProxyData &proxy) {
const auto queryPath = ProxyDataToQueryPath(proxy);
if (queryPath.isEmpty()) {
return QString();
}
if (const auto session = account->maybeSession()) {
return session->createInternalLinkFull(queryPath);
}
auto domain = MTP::ConfigFields(
account->mtp().environment()
).internalLinksDomain;
if (domain.endsWith('/') && queryPath.startsWith('/')) {
domain.chop(1);
} else if (!domain.endsWith('/') && !queryPath.startsWith('/')) {
domain += '/';
}
return domain + queryPath;
}
[[nodiscard]] QColor ProxyQrActiveColor() {
return QColor(0x40, 0xA7, 0xE3);
}
[[nodiscard]] QImage ProxyQr(const Qr::Data &data, int pixel, int max = 0) {
Expects(data.size > 0);
const auto available = max
? std::max(max - 2 * st::introQrBackgroundSkip, 1)
: 0;
if (available > 0 && data.size * pixel > available) {
pixel = std::max(available / data.size, 1);
}
return Qr::Generate(
data,
pixel * style::DevicePixelRatio(),
Qt::black,
Qt::white);
}
[[nodiscard]] QImage ProxyQrLogo() {
const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize);
auto result = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(ProxyQrActiveColor());
p.setPen(Qt::NoPen);
p.drawEllipse(QRect(QPoint(), size));
st::introQrPlane.paintInCenter(p, QRect(QPoint(), size));
}
return result;
}
[[nodiscard]] QImage ProxyQrTile(const QString &link, int max = 0) {
const auto data = Qr::Encode(link, Qr::Redundancy::Quartile);
const auto qr = ProxyQr(
data,
st::introQrPixel,
max);
const auto qrSize = qr.width() / style::DevicePixelRatio();
const auto skip = st::introQrBackgroundSkip;
const auto size = QSize(qrSize + 2 * skip, qrSize + 2 * skip);
auto result = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
p.drawRoundedRect(
QRect(QPoint(), size),
st::introQrBackgroundRadius,
st::introQrBackgroundRadius);
p.drawImage(QRect(skip, skip, qrSize, qrSize), qr);
const auto logo = ProxyQrLogo();
p.drawImage(
QRect(
(size.width() - st::introQrCenterSize) / 2,
(size.height() - st::introQrCenterSize) / 2,
st::introQrCenterSize,
st::introQrCenterSize),
logo);
}
return result;
}
[[nodiscard]] QImage ProxyQrForShare(const QString &link) {
return ProxyQrTile(
link,
st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right());
}
void ShowProxyQrBox(std::shared_ptr<Ui::Show> show, const QString &link) {
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
const auto copyCallback = [=, image = ProxyQrForShare(link)] {
QGuiApplication::clipboard()->setImage(image);
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
};
const auto qr = ProxyQrTile(
link,
st::boxWidth
- st::boxRowPadding.left()
- st::boxRowPadding.right());
const auto size = qr.width() / style::DevicePixelRatio();
const auto height = st::inviteLinkQrSkip * 2 + size;
const auto container = box->addRow(
object_ptr<Ui::BoxContentDivider>(box, height),
st::inviteLinkQrMargin);
const auto button = Ui::CreateChild<Ui::AbstractButton>(container);
button->resize(size, size);
button->paintRequest(
) | rpl::on_next([=] {
QPainter(button).drawImage(QRect(0, 0, size, size), qr);
}, button->lifetime());
container->widthValue(
) | rpl::on_next([=](int width) {
button->move((width - size) / 2, st::inviteLinkQrSkip);
}, button->lifetime());
button->setClickedCallback(copyCallback);
box->addLeftButton(tr::lng_group_invite_context_copy(), copyCallback);
}));
}
[[nodiscard]] ProxyData ProxyDataFromFields(
ProxyData::Type type,
const QMap<QString, QString> &fields) {
@@ -110,7 +290,7 @@ void AddProxyFromClipboard(
const auto socksString = u"socks"_q;
const auto protocol = u"tg://"_q;
const auto maybeUrls = ExtractUrlsSimple(
const auto maybeUrls = ExtractLinkCandidates(
QGuiApplication::clipboard()->text());
const auto isSingle = maybeUrls.size() == 1;
@@ -128,8 +308,8 @@ void AddProxyFromClipboard(
protocol.size(),
8192);
if (local.startsWith(protocol + proxyString)
|| local.startsWith(protocol + socksString)) {
if (local.startsWith(protocol + proxyString, Qt::CaseInsensitive)
|| local.startsWith(protocol + socksString, Qt::CaseInsensitive)) {
using namespace qthelp;
const auto options = RegExOption::CaseInsensitive;
@@ -188,7 +368,10 @@ void AddProxyFromClipboard(
auto success = Result::Failed;
for (const auto &maybeUrl : maybeUrls) {
const auto result = proceedUrl(Core::TryConvertUrlToLocal(maybeUrl));
const auto trimmed = maybeUrl.trimmed();
const auto local = Core::TryConvertUrlToLocal(trimmed);
const auto check = local.isEmpty() ? trimmed : local;
const auto result = proceedUrl(check);
if (success != Result::Success) {
success = result;
}
@@ -376,12 +559,16 @@ private:
void setupButtons(int id, not_null<ProxyRow*> button);
int rowHeight() const;
void refreshProxyForCalls();
void refreshProxyRotation();
not_null<ProxiesBoxController*> _controller;
Core::SettingsProxy &_settings;
QPointer<Ui::Checkbox> _tryIPv6;
std::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyRotation;
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _proxyRotationOptions;
QPointer<Ui::SettingsSlider> _proxyRotationTimeout;
QPointer<Ui::DividerLabel> _about;
base::unique_qptr<Ui::RpWidget> _noRows;
object_ptr<Ui::VerticalLayout> _initialWrap;
@@ -426,6 +613,7 @@ private:
not_null<Ui::VerticalLayout*> parent,
const QString &text) const;
const bool _allowShare = false;
Fn<void(ProxyData)> _callback;
Fn<void(ProxyData)> _shareCallback;
@@ -900,6 +1088,47 @@ void ProxiesBox::setupContent() {
0,
st::proxyTryIPv6Padding.right(),
st::proxyTryIPv6Padding.top()));
_proxyRotation = inner->add(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
inner,
object_ptr<Ui::Checkbox>(
inner,
tr::lng_proxy_auto_switch(tr::now),
_settings.proxyRotationEnabled()),
style::margins(
0,
st::proxyUsePadding.top(),
0,
st::proxyUsePadding.bottom())),
style::margins(
st::proxyTryIPv6Padding.left(),
0,
st::proxyTryIPv6Padding.right(),
st::proxyTryIPv6Padding.top()));
_proxyRotationOptions = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
_proxyRotationTimeout = _proxyRotationOptions->entity()->add(
object_ptr<Ui::SettingsSlider>(
_proxyRotationOptions->entity(),
st::settingsSlider),
st::settingsBigScalePadding);
for (const auto seconds : Core::SettingsProxy::kProxyRotationTimeouts) {
_proxyRotationTimeout->addSection(
tr::lng_proxy_auto_switch_timeout(
tr::now,
lt_count,
seconds));
}
_proxyRotationTimeout->setActiveSectionFast(
ClosestProxyRotationTimeoutSection(_settings.proxyRotationTimeout()));
_proxyRotationOptions->entity()->add(
object_ptr<Ui::FlatLabel>(
_proxyRotationOptions->entity(),
tr::lng_proxy_auto_switch_about(tr::now),
st::boxDividerLabel),
st::proxyAboutPadding);
_about = inner->add(
object_ptr<Ui::DividerLabel>(
@@ -922,6 +1151,7 @@ void ProxiesBox::setupContent() {
addNewProxy();
}
refreshProxyForCalls();
refreshProxyRotation();
});
_tryIPv6->checkedChanges(
) | rpl::on_next([=](bool checked) {
@@ -931,18 +1161,33 @@ void ProxiesBox::setupContent() {
_controller->proxySettingsValue(
) | rpl::on_next([=](ProxyData::Settings value) {
_proxySettings->setValue(value);
refreshProxyForCalls();
refreshProxyRotation();
}, inner->lifetime());
_proxyForCalls->entity()->checkedChanges(
) | rpl::on_next([=](bool checked) {
_controller->setProxyForCalls(checked);
}, _proxyForCalls->lifetime());
_proxyRotation->entity()->checkedChanges(
) | rpl::on_next([=](bool checked) {
_controller->setProxyRotationEnabled(checked);
refreshProxyRotation();
}, _proxyRotation->lifetime());
_proxyRotationTimeout->sectionActivated(
) | rpl::on_next([=](int section) {
_controller->setProxyRotationTimeout(
Core::SettingsProxy::kProxyRotationTimeouts[section]);
}, _proxyRotationTimeout->lifetime());
if (_rows.empty()) {
createNoRowsLabel();
}
refreshProxyForCalls();
refreshProxyRotation();
_proxyForCalls->finishAnimating();
_proxyRotation->finishAnimating();
_proxyRotationOptions->finishAnimating();
{
const auto wrap = inner->add(
@@ -987,6 +1232,20 @@ void ProxiesBox::refreshProxyForCalls() {
anim::type::normal);
}
void ProxiesBox::refreshProxyRotation() {
if (!_proxyRotation || !_proxyRotationOptions) {
return;
}
const auto visible = (_proxySettings->current()
== ProxyData::Settings::Enabled)
&& _settings.selected()
&& (_settings.list().size() > 1);
_proxyRotation->toggle(visible, anim::type::normal);
_proxyRotationOptions->toggle(
visible && _proxyRotation->entity()->checked(),
anim::type::normal);
}
int ProxiesBox::rowHeight() const {
return st::proxyRowPadding.top()
+ st::semiboldFont->height
@@ -1029,6 +1288,7 @@ void ProxiesBox::applyView(View &&view) {
} else {
i->second->updateFields(std::move(view));
}
refreshProxyRotation();
}
void ProxiesBox::createNoRowsLabel() {
@@ -1087,7 +1347,8 @@ ProxyBox::ProxyBox(
const ProxyData &data,
Fn<void(ProxyData)> callback,
Fn<void(ProxyData)> shareCallback)
: _callback(std::move(callback))
: _allowShare(data.type != Type::None)
, _callback(std::move(callback))
, _shareCallback(std::move(shareCallback))
, _content(this) {
setupControls(data);
@@ -1156,7 +1417,8 @@ void ProxyBox::refreshButtons() {
addButton(tr::lng_cancel(), [=] { closeBox(); });
const auto type = _type->current();
if (type == Type::Socks5 || type == Type::Mtproto) {
if (_allowShare
&& (type == Type::Socks5 || type == Type::Mtproto)) {
addLeftButton(tr::lng_proxy_share(), [=] { share(); });
}
}
@@ -1203,10 +1465,10 @@ ProxyData ProxyBox::collectData() {
}
void ProxyBox::setupTypes() {
const auto types = std::map<Type, QString>{
{ Type::Http, "HTTP" },
{ Type::Socks5, "SOCKS5" },
{ Type::Mtproto, "MTPROTO" },
const auto types = std::vector<std::pair<Type, QString>>{
{ Type::Mtproto, u"MTPROTO"_q },
{ Type::Socks5, u"SOCKS5"_q },
{ Type::Http, u"HTTP"_q },
};
for (const auto &[type, label] : types) {
_content->add(
@@ -1318,7 +1580,7 @@ void ProxyBox::setupMtprotoCredentials(const ProxyData &data) {
void ProxyBox::setupControls(const ProxyData &data) {
_type = std::make_shared<Ui::RadioenumGroup<Type>>(
(data.type == Type::None
? Type::Socks5
? Type::Mtproto
: data.type));
_content.create(this);
_content->resizeToWidth(st::boxWideWidth);
@@ -1359,95 +1621,7 @@ void ProxyBox::addLabel(
}
using Connection = MTP::details::AbstractConnection;
using Checker = MTP::details::ConnectionPointer;
void ResetProxyCheckers(Checker &v4, Checker &v6) {
v4 = nullptr;
v6 = nullptr;
}
void DropProxyChecker(Checker &v4, Checker &v6, not_null<Connection*> raw) {
if (v4.get() == raw) {
v4 = nullptr;
} else if (v6.get() == raw) {
v6 = nullptr;
}
}
[[nodiscard]] bool HasProxyCheckers(const Checker &v4, const Checker &v6) {
return v4 || v6;
}
void StartProxyCheck(
not_null<MTP::Instance*> mtproto,
const ProxyData &proxy,
Checker &v4,
Checker &v6,
Fn<void(Connection *raw, int ping)> done,
Fn<void(Connection *raw)> fail) {
using Variants = MTP::DcOptions::Variants;
ResetProxyCheckers(v4, v6);
const auto connType = (proxy.type == ProxyData::Type::Http)
? Variants::Http
: Variants::Tcp;
const auto dcId = mtproto->mainDcId();
const auto setup = [&](Checker &checker, const bytes::vector &secret) {
checker = Connection::Create(
mtproto,
connType,
QThread::currentThread(),
secret,
proxy);
const auto raw = checker.get();
raw->connect(raw, &Connection::connected, [=] {
if (done) {
done(raw, raw->pingTime());
}
});
const auto failed = [=] {
if (fail) {
fail(raw);
}
};
raw->connect(raw, &Connection::disconnected, failed);
raw->connect(raw, &Connection::error, failed);
};
if (proxy.type == ProxyData::Type::Mtproto) {
const auto secret = proxy.secretFromMtprotoPassword();
setup(v4, secret);
v4->connectToServer(
proxy.host,
proxy.port,
secret,
dcId,
false);
return;
}
const auto options = mtproto->dcOptions().lookup(
dcId,
MTP::DcType::Regular,
true);
const auto tryConnect = [&](Checker &checker, Variants::Address address) {
const auto &list = options.data[address][connType];
if (list.empty()
|| ((address == Variants::IPv6)
&& !Core::App().settings().proxy().tryIPv6())) {
checker = nullptr;
return;
}
const auto &endpoint = list.front();
setup(checker, endpoint.secret);
checker->connectToServer(
QString::fromStdString(endpoint.ip),
endpoint.port,
endpoint.secret,
dcId,
false);
};
tryConnect(v4, Variants::IPv4);
tryConnect(v6, Variants::IPv6);
}
using Checker = MTP::ProxyCheckConnection;
} // namespace
@@ -1609,18 +1783,19 @@ void ProxiesBoxController::ShowApplyConfirmation(
};
statusLabel->setTextColorOverride(st::proxyRowStatusFg->c);
relayout();
StartProxyCheck(
MTP::StartProxyCheck(
&account->mtp(),
proxy,
Core::App().settings().proxy().tryIPv6(),
state->v4,
state->v6,
[=](Connection *raw, int ping) {
if (!weak || state->finished) {
return;
}
DropProxyChecker(state->v4, state->v6, raw);
MTP::DropProxyChecker(state->v4, state->v6, raw);
state->finished = true;
ResetProxyCheckers(state->v4, state->v6);
MTP::ResetProxyCheckers(state->v4, state->v6);
state->statusValue = TextWithEntities{
tr::lng_proxy_box_table_available(
tr::now,
@@ -1635,19 +1810,34 @@ void ProxiesBoxController::ShowApplyConfirmation(
if (!weak || state->finished) {
return;
}
DropProxyChecker(state->v4, state->v6, raw);
if (!HasProxyCheckers(state->v4, state->v6)) {
MTP::DropProxyChecker(state->v4, state->v6, raw);
if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
state->finished = true;
setUnavailable();
}
});
if (!HasProxyCheckers(state->v4, state->v6)) {
if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
state->finished = true;
setUnavailable();
}
};
statusLabel->setClickHandlerFilter([=](const auto &...) {
runCheck();
auto &proxy = Core::App().settings().proxy();
if (proxy.checkIpWarningShown()) {
runCheck();
} else {
box->uiShow()->showBox(Ui::MakeConfirmBox({
.text = tr::lng_proxy_check_ip_warning(),
.confirmed = [=] {
auto &proxy = Core::App().settings().proxy();
proxy.setCheckIpWarningShown(true);
Local::writeSettings();
runCheck();
},
.confirmText = tr::lng_proxy_check_ip_proceed(),
.title = tr::lng_proxy_check_ip_warning_title(),
}));
}
return false;
});
}
@@ -1666,9 +1856,9 @@ void ProxiesBoxController::ShowApplyConfirmation(
const auto enableButton = box->addButton(
tr::lng_proxy_box_table_button(),
[=] {
auto &proxies = Core::App().settings().proxy().list();
if (!ranges::contains(proxies, proxy)) {
proxies.push_back(proxy);
auto &settings = Core::App().settings().proxy();
if (settings.indexInList(proxy) < 0) {
settings.addToList(proxy);
}
Core::App().setCurrentProxy(
proxy,
@@ -1704,9 +1894,10 @@ auto ProxiesBoxController::proxySettingsValue() const
void ProxiesBoxController::refreshChecker(Item &item) {
item.state = ItemState::Checking;
const auto id = item.id;
StartProxyCheck(
MTP::StartProxyCheck(
&_account->mtp(),
item.data,
Core::App().settings().proxy().tryIPv6(),
item.checker,
item.checkerv6,
[=](Connection *raw, int pingTime) {
@@ -1717,8 +1908,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
if (item == end(_list)) {
return;
}
DropProxyChecker(item->checker, item->checkerv6, raw);
ResetProxyCheckers(item->checker, item->checkerv6);
MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
MTP::ResetProxyCheckers(item->checker, item->checkerv6);
if (item->state == ItemState::Checking) {
item->state = ItemState::Available;
item->ping = pingTime;
@@ -1733,14 +1924,14 @@ void ProxiesBoxController::refreshChecker(Item &item) {
if (item == end(_list)) {
return;
}
DropProxyChecker(item->checker, item->checkerv6, raw);
if (!HasProxyCheckers(item->checker, item->checkerv6)
MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
if (!MTP::HasProxyCheckers(item->checker, item->checkerv6)
&& item->state == ItemState::Checking) {
item->state = ItemState::Unavailable;
updateView(*item);
}
});
if (!HasProxyCheckers(item.checker, item.checkerv6)) {
if (!MTP::HasProxyCheckers(item.checker, item.checkerv6)) {
item.state = ItemState::Unavailable;
}
}
@@ -1802,8 +1993,11 @@ void ProxiesBoxController::shareItem(int id, bool qr) {
void ProxiesBoxController::shareItems() {
auto result = QString();
for (const auto &item : _list) {
if (!item.deleted) {
result += ProxyDataToString(item.data) + '\n' + '\n';
if (!item.deleted && ProxyDataIsShareable(item.data)) {
const auto link = ProxyDataToPublicLink(_account, item.data);
if (!link.isEmpty()) {
result += (result.isEmpty() ? QString() : u"\n\n"_q) + link;
}
}
}
if (result.isEmpty()) {
@@ -1839,8 +2033,8 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
item->deleted = deleted;
if (deleted) {
auto &proxies = _settings.list();
proxies.erase(ranges::remove(proxies, item->data), end(proxies));
const auto removed = _settings.removeFromList(item->data);
Assert(removed);
if (item->data == _settings.selected()) {
_lastSelectedProxy = _settings.selected();
@@ -1856,16 +2050,19 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
}
}
} else {
auto &proxies = _settings.list();
if (ranges::find(proxies, item->data) == end(proxies)) {
if (_settings.indexInList(item->data) < 0) {
const auto &proxies = _settings.list();
auto insertBefore = item + 1;
while (insertBefore != end(_list) && insertBefore->deleted) {
++insertBefore;
}
auto insertBeforeIt = (insertBefore == end(_list))
? end(proxies)
: ranges::find(proxies, insertBefore->data);
proxies.insert(insertBeforeIt, item->data);
const auto foundIndex = (insertBefore == end(_list))
? int(proxies.size())
: _settings.indexInList(insertBefore->data);
const auto insertIndex = (foundIndex >= 0)
? foundIndex
: int(proxies.size());
_settings.insertToList(insertIndex, item->data);
}
if (!_settings.selected() && _lastSelectedProxy == item->data) {
@@ -1904,8 +2101,8 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::editItemBox(int id) {
void ProxiesBoxController::replaceItemWith(
std::vector<Item>::iterator which,
std::vector<Item>::iterator with) {
auto &proxies = _settings.list();
proxies.erase(ranges::remove(proxies, which->data), end(proxies));
const auto removed = _settings.removeFromList(which->data);
Assert(removed);
_views.fire({ which->id });
_list.erase(which);
@@ -1924,10 +2121,8 @@ void ProxiesBoxController::replaceItemValue(
restoreItem(which->id);
}
auto &proxies = _settings.list();
const auto i = ranges::find(proxies, which->data);
Assert(i != end(proxies));
*i = proxy;
const auto replaced = _settings.replaceInList(which->data, proxy);
Assert(replaced);
which->data = proxy;
refreshChecker(*which);
@@ -1963,8 +2158,7 @@ bool ProxiesBoxController::contains(const ProxyData &proxy) const {
}
void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
auto &proxies = _settings.list();
proxies.push_back(proxy);
_settings.addToList(proxy);
_list.push_back({ ++_idCounter, proxy });
refreshChecker(_list.back());
@@ -2001,6 +2195,22 @@ void ProxiesBoxController::setProxyForCalls(bool enabled) {
saveDelayed();
}
void ProxiesBoxController::setProxyRotationEnabled(bool enabled) {
if (_settings.proxyRotationEnabled() == enabled) {
return;
}
_settings.setProxyRotationEnabled(enabled);
saveDelayed();
}
void ProxiesBoxController::setProxyRotationTimeout(int value) {
if (_settings.proxyRotationTimeout() == value) {
return;
}
_settings.setProxyRotationTimeout(value);
saveDelayed();
}
void ProxiesBoxController::setTryIPv6(bool enabled) {
if (Core::App().settings().proxy().tryIPv6() == enabled) {
return;
@@ -2012,6 +2222,7 @@ void ProxiesBoxController::setTryIPv6(bool enabled) {
}
void ProxiesBoxController::saveDelayed() {
Core::App().proxyRotationSettingsChanged();
_saveTimer.callOnce(kSaveSettingsDelayedTimeout);
}
@@ -2022,7 +2233,7 @@ auto ProxiesBoxController::views() const -> rpl::producer<ItemView> {
rpl::producer<bool> ProxiesBoxController::listShareableChanges() const {
return _views.events_starting_with(ItemView()) | rpl::map([=] {
for (const auto &item : _list) {
if (!item.deleted) {
if (!item.deleted && ProxyDataIsShareable(item.data)) {
return true;
}
}
@@ -2049,8 +2260,7 @@ void ProxiesBoxController::updateView(const Item &item) {
}
return ItemState::Connecting;
}();
const auto supportsShare = (item.data.type == Type::Socks5)
|| (item.data.type == Type::Mtproto);
const auto supportsShare = ProxyDataIsShareable(item.data);
const auto supportsCalls = item.data.supportsCalls();
_views.fire({
item.id,
@@ -2067,18 +2277,29 @@ void ProxiesBoxController::updateView(const Item &item) {
}
void ProxiesBoxController::share(const ProxyData &proxy, bool qr) {
if (proxy.type == Type::Http) {
if (!ProxyDataIsShareable(proxy)) {
return;
}
const auto qrLink = ProxyDataToLocalLink(proxy);
if (qrLink.isEmpty()) {
return;
}
const auto link = ProxyDataToString(proxy);
if (qr) {
_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
Ui::FillPeerQrBox(box, nullptr, link, rpl::single(QString()));
box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
}));
if (_account->sessionExists()) {
_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
Ui::FillPeerQrBox(box, nullptr, qrLink, rpl::single(QString()));
box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
}));
} else {
ShowProxyQrBox(_show, qrLink);
}
return;
}
QGuiApplication::clipboard()->setText(link);
const auto shareLink = ProxyDataToPublicLink(_account, proxy);
if (shareLink.isEmpty()) {
return;
}
QGuiApplication::clipboard()->setText(shareLink);
_show->showToast(tr::lng_username_copied(tr::now));
}
@@ -85,6 +85,8 @@ public:
object_ptr<Ui::BoxContent> addNewItemBox();
bool setProxySettings(ProxyData::Settings value);
void setProxyForCalls(bool enabled);
void setProxyRotationEnabled(bool enabled);
void setProxyRotationTimeout(int value);
void setTryIPv6(bool enabled);
rpl::producer<ProxyData::Settings> proxySettingsValue() const;
@@ -1049,7 +1049,7 @@ void Options::removeDestroyed(not_null<Option*> option) {
void Options::validateState() {
checkLastOption();
_hasOptions = (ranges::count_if(_list, &Option::isGood) > 1);
_hasOptions = (ranges::count_if(_list, &Option::isGood) > 0);
_isValid = _hasOptions && ranges::none_of(_list, &Option::isTooLong);
_hasCorrect = ranges::any_of(_list, &Option::isCorrect);
@@ -211,6 +211,7 @@ auto AddButtonWithLoader(
buttonState->value() | rpl::map(StateDescription),
st::settingsUpdateState);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->show();
rpl::combine(
button->widthValue(),
@@ -344,24 +345,44 @@ void Inner::setupContent(
const auto queryStream = content->lifetime()
.make_state<rpl::event_stream<QStringView>>();
for (const auto &dict : Spellchecker::Dictionaries()) {
const auto id = dict.id;
const auto row = AddButtonWithLoader(
content,
session,
dict,
ranges::contains(enabledDictionaries, id),
queryStream->events());
row->toggledValue(
) | rpl::on_next([=](auto enabled) {
if (enabled) {
_enabledRows.push_back(id);
} else {
auto &rows = _enabledRows;
rows.erase(ranges::remove(rows, id), end(rows));
}
}, row->lifetime());
}
// Rows are created once, when Spellchecker::Dictionaries() becomes
// non-empty. Manifest is fetched lazily and may arrive after the box
// opens, so we subscribe to DictionariesChanged and populate rows
// then if we haven't already.
const auto built = content->lifetime().make_state<bool>(false);
const auto buildRows = [=] {
if (*built) {
return;
}
const auto dicts = Spellchecker::Dictionaries();
if (dicts.empty()) {
return;
}
*built = true;
for (const auto &dict : dicts) {
const auto id = dict.id;
const auto row = AddButtonWithLoader(
content,
session,
dict,
ranges::contains(enabledDictionaries, id),
queryStream->events());
row->toggledValue(
) | rpl::on_next([=](auto enabled) {
if (enabled) {
_enabledRows.push_back(id);
} else {
auto &rows = _enabledRows;
rows.erase(ranges::remove(rows, id), end(rows));
}
}, row->lifetime());
}
};
buildRows();
Spellchecker::DictionariesChanged(
) | rpl::on_next(buildRows, content->lifetime());
Spellchecker::RefreshDictionariesManifest(session);
_queryCallback = [=](const QString &query) {
if (query.size() >= kMaxQueryLength) {
@@ -403,11 +424,6 @@ void ManageDictionariesBox::prepare() {
multiSelect->setInnerFocus();
};
// The initial list of enabled rows may differ from the list of languages
// in settings, so we should store it when box opens
// and save it when box closes (don't do it when "Save" was pressed).
const auto initialEnabledRows = inner->enabledRows();
setTitle(tr::lng_settings_manage_dictionaries());
addButton(tr::lng_settings_save(), [=] {
@@ -421,8 +437,12 @@ void ManageDictionariesBox::prepare() {
addButton(tr::lng_close(), [=] { closeBox(); });
boxClosing() | rpl::on_next([=] {
Core::App().settings().setDictionariesEnabled(
FilterEnabledDict(initialEnabledRows));
const auto &current = Core::App().settings().dictionariesEnabled();
const auto filtered = FilterEnabledDict(current);
if (filtered.size() == current.size()) {
return;
}
Core::App().settings().setDictionariesEnabled(filtered);
Core::App().saveSettingsDelayed();
}, lifetime());
@@ -53,7 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kMaxOptionsCount = TodoListData::kMaxOptions;
constexpr auto kWarnTitleLimit = 12;
constexpr auto kWarnTaskLimit = 24;
constexpr auto kErrorLimit = 99;
+249 -32
View File
@@ -7,51 +7,55 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/language_box.h"
#include "data/data_peer_values.h"
#include "lang/lang_keys.h"
#include "ui/boxes/choose_language_box.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/text/text_entity.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/effects/ripple_animation.h"
#include "ui/toast/toast.h"
#include "ui/text/text_options.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "ui/ui_utility.h"
#include "storage/localstorage.h"
#include "base/platform/base_platform_info.h"
#include "boxes/abstract_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/translate_box.h"
#include "ui/boxes/confirm_box.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "core/application.h"
#include "base/platform/base_platform_info.h"
#include "lang/lang_instance.h"
#include "data/data_peer_values.h"
#include "lang/lang_cloud_manager.h"
#include "lang/lang_instance.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "platform/platform_translate_provider.h"
#include "settings/settings_common.h"
#include "spellcheck/spellcheck_types.h"
#include "storage/localstorage.h"
#include "ui/accessible/ui_accessible_item.h"
#include "ui/boxes/choose_language_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_entity.h"
#include "ui/text/text_options.h"
#include "ui/toast/toast.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/painter.h"
#include "ui/screen_reader_mode.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include "styles/style_passport.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_passport.h"
#include "styles/style_settings.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
namespace {
@@ -70,11 +74,13 @@ public:
int count() const;
int selected() const;
int chosenIndex() const;
void setSelected(int selected);
rpl::producer<bool> hasSelection() const;
rpl::producer<bool> isEmpty() const;
void activateSelected();
void selectSkip(int dir);
rpl::producer<Language> activations() const;
void changeChosen(const QString &chosen);
@@ -82,10 +88,24 @@ public:
static int DefaultRowHeight();
QAccessible::Role accessibilityRole() override;
Qt::FocusPolicy accessibilityFocusPolicy() override;
QAccessible::Role accessibilityChildRole() const override;
QAccessible::State accessibilityChildState(int index) const override;
int accessibilityChildCount() const override;
QString accessibilityChildName(int index) const override;
QRect accessibilityChildRect(int index) const override;
int accessibilityChildColumnCount(int row) const override;
QAccessible::Role accessibilityChildSubItemRole() const override;
QString accessibilityChildSubItemName(int row, int column) const override;
QString accessibilityChildSubItemValue(int row, int column) const override;
protected:
int resizeGetHeight(int newWidth) override;
void focusInEvent(QFocusEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
@@ -153,6 +173,13 @@ private:
void repaintChecked(not_null<const Row*> row);
void activateByIndex(int index);
enum class Announce {
No,
OnChange,
Always,
};
void setSelected(int index, Announce announce);
void showMenu(int index);
void setForceRippled(not_null<Row*> row, bool rippled);
bool canShare(not_null<const Row*> row) const;
@@ -182,6 +209,26 @@ private:
};
[[nodiscard]] bool ForwardListNavigation(
not_null<QKeyEvent*> e,
not_null<Rows*> rows,
int pageHeight) {
const auto key = e->key();
if (key == Qt::Key_Down) {
rows->selectSkip(1);
} else if (key == Qt::Key_Up) {
rows->selectSkip(-1);
} else if (key == Qt::Key_PageDown || key == Qt::Key_PageUp) {
const auto perPage = std::max(
pageHeight / Rows::DefaultRowHeight(),
1);
rows->selectSkip((key == Qt::Key_PageDown) ? perPage : -perPage);
} else {
return false;
}
return true;
}
class Content : public Ui::RpWidget {
public:
Content(
@@ -287,6 +334,44 @@ Rows::Rows(
resizeToWidth(width());
setAttribute(Qt::WA_MouseTracking);
update();
setAccessibleName(tr::lng_languages(tr::now));
}
void Rows::focusInEvent(QFocusEvent *e) {
if (selected() < 0 && count() > 0) {
const auto chosen = chosenIndex();
setSelected(chosen >= 0 ? chosen : 0, Announce::No);
}
RpWidget::focusInEvent(e);
const auto index = selected();
if (index >= 0) {
InvokeQueued(this, [=] {
if (selected() == index && hasFocus()) {
accessibilityChildFocused(index);
}
});
}
}
void Rows::keyPressEvent(QKeyEvent *e) {
const auto pageHeight = window() ? window()->height() : height();
if (ForwardListNavigation(e, this, pageHeight)) {
return;
}
const auto key = e->key();
if (key == Qt::Key_Home && count() > 0) {
setSelected(0, Announce::Always);
} else if (key == Qt::Key_End && count() > 0) {
setSelected(count() - 1, Announce::Always);
} else if (!e->isAutoRepeat()
&& (key == Qt::Key_Space
|| key == Qt::Key_Return
|| key == Qt::Key_Enter)) {
activateSelected();
} else {
RpWidget::keyPressEvent(e);
}
}
void Rows::mouseMoveEvent(QMouseEvent *e) {
@@ -554,7 +639,10 @@ void Rows::setForceRippled(not_null<Row*> row, bool rippled) {
}
void Rows::activateByIndex(int index) {
_chosen = rowByIndex(index).data.id;
_activations.fire_copy(rowByIndex(index).data);
accessibilityChildStateChanged(index, { .checked = true });
accessibilityChildNameChanged(index);
}
void Rows::leaveEventHook(QEvent *e) {
@@ -630,6 +718,15 @@ int Rows::selected() const {
return indexFromSelection(_selected);
}
int Rows::chosenIndex() const {
for (auto i = 0, n = count(); i < n; ++i) {
if (rowByIndex(i).data.id == _chosen) {
return i;
}
}
return -1;
}
void Rows::activateSelected() {
const auto index = selected();
if (index >= 0) {
@@ -637,24 +734,66 @@ void Rows::activateSelected() {
}
}
void Rows::selectSkip(int dir) {
const auto limit = count();
auto now = selected();
if (now < 0) {
now = chosenIndex();
}
if (now >= 0) {
const auto changed = now + dir;
if (changed < 0) {
setSelected(0, Announce::Always);
} else if (changed >= limit) {
setSelected(limit - 1, Announce::Always);
} else {
setSelected(changed, Announce::Always);
}
} else if (dir > 0) {
setSelected(0, Announce::Always);
}
}
rpl::producer<Language> Rows::activations() const {
return _activations.events();
}
void Rows::changeChosen(const QString &chosen) {
const auto oldIndex = chosenIndex();
_chosen = chosen;
for (const auto &row : _rows) {
row.check->setChecked(row.data.id == chosen, anim::type::normal);
}
const auto newIndex = chosenIndex();
if (newIndex != oldIndex && newIndex >= 0) {
accessibilityChildStateChanged(newIndex, { .checked = true });
accessibilityChildNameChanged(newIndex);
}
}
void Rows::setSelected(int selected) {
setSelected(selected, Announce::OnChange);
}
void Rows::setSelected(int selected, Announce announce) {
_mouseSelection = false;
const auto limit = count();
if (selected >= 0 && selected < limit) {
updateSelected(RowSelection{ selected });
const auto clamped = (selected >= 0 && selected < limit)
? selected
: -1;
const auto changed = (indexFromSelection(_selected) != clamped)
|| (clamped < 0 && !v::is_null(_selected));
if (clamped >= 0) {
updateSelected(RowSelection{ clamped });
} else {
updateSelected({});
}
const auto shouldAnnounce = (announce == Announce::Always)
|| (announce == Announce::OnChange && changed);
if (shouldAnnounce && clamped >= 0) {
accessibilityChildNameChanged(clamped);
accessibilityChildFocused(clamped);
}
}
rpl::producer<bool> Rows::hasSelection() const {
@@ -874,6 +1013,84 @@ void Rows::paintEvent(QPaintEvent *e) {
}
}
QAccessible::Role Rows::accessibilityRole() {
return QAccessible::List;
}
Qt::FocusPolicy Rows::accessibilityFocusPolicy() {
return Qt::TabFocus;
}
QAccessible::Role Rows::accessibilityChildRole() const {
return QAccessible::RadioButton;
}
QAccessible::State Rows::accessibilityChildState(int index) const {
QAccessible::State state;
if (Ui::ScreenReaderModeActive()) {
state.focusable = true;
}
state.checkable = true;
state.checked = (index == chosenIndex());
if (index == selected()) {
state.active = true;
if (hasFocus()) {
state.focused = true;
}
}
return state;
}
int Rows::accessibilityChildCount() const {
return count();
}
QString Rows::accessibilityChildName(int index) const {
if (index < 0 || index >= count()) {
return {};
}
const auto &row = rowByIndex(index);
return row.data.nativeName + u", "_q + row.data.name;
}
QRect Rows::accessibilityChildRect(int index) const {
if (index < 0 || index >= count()) {
return {};
}
const auto &row = rowByIndex(index);
return QRect(0, row.top, width(), row.height);
}
int Rows::accessibilityChildColumnCount(int row) const {
return 2;
}
QAccessible::Role Rows::accessibilityChildSubItemRole() const {
return QAccessible::Cell;
}
QString Rows::accessibilityChildSubItemName(int row, int column) const {
if (column == 0) {
return tr::lng_sr_languages_column_native(tr::now);
} else if (column == 1) {
return tr::lng_sr_languages_column_name(tr::now);
}
return {};
}
QString Rows::accessibilityChildSubItemValue(int row, int column) const {
if (row < 0 || row >= count()) {
return {};
}
const auto &data = rowByIndex(row).data;
if (column == 0) {
return data.nativeName;
} else if (column == 1) {
return data.name;
}
return {};
}
Content::Content(
QWidget *parent,
const Languages &recent,
@@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/edit_peer_history_visibility_box.h"
#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
@@ -35,12 +37,39 @@ void EditPeerHistoryVisibilityBox(
button->setClickedCallback([=] { historyVisibility->setValue(v); });
};
const auto tryClose = [=] {
if (historyVisibility->current() == historyVisibilitySavedValue) {
box->closeBox();
return;
}
box->uiShow()->showBox(Ui::MakeConfirmBox({
.text = tr::lng_bot_close_warning(),
.confirmed = crl::guard(box, [=](Fn<void()> close) {
close();
box->closeBox();
}),
.confirmText = tr::lng_bot_close_warning_sure(),
.cancelText = tr::lng_create_group_back(),
}));
};
box->setTitle(tr::lng_manage_history_visibility_title());
box->addButton(tr::lng_settings_save(), [=] {
savedCallback(historyVisibility->current());
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->addButton(tr::lng_cancel(), tryClose);
box->setCloseByOutsideClick(false);
box->setCloseByEscape(false);
base::install_event_filter(box, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress
&& static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {
tryClose();
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
box->addSkip(st::editPeerHistoryVisibilityTopSkip);
const auto visible = box->addRow(object_ptr<Ui::VerticalLayout>(box));
@@ -809,7 +809,7 @@ void PeerShortInfoBox::prepareRows() {
addInfoLine(std::move(label), aboutValue(), _st.labeled);
addInfoOneLine(
tr::lng_info_username_label(),
usernameValue() | rpl::map(tr::marked),
usernameValue(),
tr::lng_context_copy_mention(tr::now));
addInfoOneLine(
birthdayLabel(),
@@ -899,10 +899,12 @@ rpl::producer<QString> PeerShortInfoBox::phoneValue() const {
}) | rpl::distinct_until_changed();
}
rpl::producer<QString> PeerShortInfoBox::usernameValue() const {
rpl::producer<TextWithEntities> PeerShortInfoBox::usernameValue() const {
return _fields.value(
) | rpl::map([](const PeerShortInfoFields &fields) {
return fields.username;
return fields.usernameLink.isEmpty()
? TextWithEntities{ fields.username }
: tr::link(fields.username, fields.usernameLink);
}) | rpl::distinct_until_changed();
}
@@ -47,6 +47,7 @@ struct PeerShortInfoFields {
QString link;
TextWithEntities about;
QString username;
QString usernameLink;
Data::Birthday birthday;
TextWithEntities note;
bool isBio = false;
@@ -185,7 +186,7 @@ private:
[[nodiscard]] rpl::producer<TextWithEntities> channelValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> linkValue() const;
[[nodiscard]] rpl::producer<QString> phoneValue() const;
[[nodiscard]] rpl::producer<QString> usernameValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> usernameValue() const;
[[nodiscard]] rpl::producer<QString> birthdayLabel() const;
[[nodiscard]] rpl::producer<QString> birthdayValue() const;
[[nodiscard]] rpl::producer<TextWithEntities> aboutValue() const;
@@ -237,6 +237,9 @@ void ProcessFullPhoto(
.username = ((user && !username.isEmpty())
? ('@' + username)
: QString()),
.usernameLink = ((user && !username.isEmpty())
? Info::Profile::UsernameUrl(peer, username, true)
: QString()),
.birthday = user ? user->birthday() : Data::Birthday(),
.note = user ? user->note() : TextWithEntities(),
.isBio = (user && !user->isBot()),
+2
View File
@@ -48,6 +48,8 @@ historyPollRadio: Radio(defaultRadio) {
rippleAreaPadding: 8px;
}
historyPollCheckboxRadius: 3px;
historyPollReplyIcon: icon {{ "dialogs/dialogs_chatlist_poll-17x17", windowFg }};
historyPollReplyIconSkip: 3px;
historyPollRadioOpacity: 0.7;
historyPollRadioOpacityOver: 1.;
historyPollDuration: 300;
@@ -21,6 +21,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_fake_items.h"
#include "lang/lang_keys.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/emoji_list_widget.h"
#include "chat_helpers/tabbed_selector.h"
#include "main/main_session.h"
#include "settings/sections/settings_premium.h"
#include "ui/chat/chat_style.h"
@@ -39,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
#include "styles/style_settings.h"
@@ -454,6 +457,56 @@ void ReactionsSettingsBox(
const auto container = box->verticalLayout();
box->setTitle(tr::lng_settings_chat_reactions_title());
box->setWidth(st::boxWideWidth);
box->addButton(tr::lng_settings_save(), [=] {
const auto &data = controller->session().data();
const auto selectedId = state->selectedId.current();
if (data.reactions().favoriteId() != selectedId) {
data.reactions().setFavorite(selectedId);
}
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
if (controller->session().premium()) {
using Selector = ChatHelpers::TabbedSelector;
const auto selector = container->add(
object_ptr<Selector>(
container,
controller->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::FullReactions),
style::margins());
selector->setAllowEmojiWithoutPremium(false);
selector->setRoundRadius(0);
selector->resize(st::boxWideWidth, st::emojiPanMinHeight);
const auto docToReaction = box->lifetime().make_state<
base::flat_map<DocumentId, Data::ReactionId>>();
auto recentIds = std::vector<DocumentId>();
for (const auto &r
: reactions.list(Data::Reactions::Type::Active)) {
const auto docId = r.selectAnimation->id;
recentIds.push_back(docId);
docToReaction->emplace(docId, r.id);
}
selector->provideRecentEmoji(
ChatHelpers::DocumentListToRecent(recentIds));
selector->customEmojiChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
const auto docId = data.document->id;
const auto it = docToReaction->find(docId);
if (it != docToReaction->end()) {
state->selectedId = it->second;
} else {
state->selectedId = Data::ReactionId{ docId };
}
}, selector->lifetime());
return;
}
const auto check = Ui::CreateChild<Ui::RpWidget>(container.get());
check->resize(st::settingsReactionCornerSize);
check->setAttribute(Qt::WA_TransparentForMouseEvents);
@@ -528,16 +581,4 @@ void ReactionsSettingsBox(
}, firstCheckedButton->lifetime());
}
check->raise();
box->setTitle(tr::lng_settings_chat_reactions_title());
box->setWidth(st::boxWideWidth);
box->addButton(tr::lng_settings_save(), [=] {
const auto &data = controller->session().data();
const auto selectedId = state->selectedId.current();
if (data.reactions().favoriteId() != selectedId) {
data.reactions().setFavorite(selectedId);
}
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
+90 -11
View File
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_preview_box.h"
#include "boxes/send_gif_with_caption_box.h"
#include "boxes/send_credits_box.h"
#include "boxes/send_files_box_reply_header.h"
#include "ui/effects/scroll_content_shadow.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/checkbox.h"
@@ -441,6 +442,10 @@ int SendFilesBox::Block::fromIndex() const {
return _from;
}
bool SendFilesBox::Block::isSingleFile() const {
return !_isAlbum && !_isSingleMedia;
}
int SendFilesBox::Block::tillIndex() const {
return _till;
}
@@ -661,9 +666,58 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
, _inner(
_scroll->setOwnedWidget(
object_ptr<Ui::VerticalLayout>(_scroll.data()))) {
setReplyTo(descriptor.replyTo);
enqueueNextPrepare();
}
void SendFilesBox::setReplyTo(FullReplyTo replyTo) {
if (_replyTo == replyTo) {
return;
} else if (!replyTo.messageId || !replyTo.messageId.peer) {
_replyTo = {};
if (_replyHeader) {
_replyHeader->hideAnimated();
}
return;
}
_replyTo = replyTo;
if (_replyHeader) {
_replyHeader = nullptr;
_replyHeaderHeight = 0;
}
_replyHeader = std::make_unique<SendFiles::ReplyPillHeader>(
this,
_show,
std::move(replyTo));
_replyHeader->setRoundedShapeBelow(
!_blocks.empty() && !_blocks.front().isSingleFile());
_replyHeader->show();
_replyHeader->desiredHeight(
) | rpl::on_next([=](int height) {
if (_replyHeaderHeight.current() != height) {
_replyHeaderHeight = height;
updateBoxSize();
updateControlsGeometry();
}
}, _replyHeader->lifetime());
_replyHeader->closeRequests(
) | rpl::on_next([=] {
_replyTo = {};
if (_replyHeader) {
_replyHeader->hideAnimated();
}
}, _replyHeader->lifetime());
_replyHeader->hideFinished(
) | rpl::on_next([=] {
InvokeQueued(this, [=] {
_replyHeader = nullptr;
_replyHeaderHeight = 0;
updateBoxSize();
updateControlsGeometry();
});
}, _replyHeader->lifetime());
}
Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
const SendFilesBoxDescriptor &descriptor) {
auto initial = descriptor.sendMenuDetails;
@@ -780,7 +834,7 @@ void SendFilesBox::prepare() {
setCloseByOutsideClick(false);
boxClosing() | rpl::on_next([=] {
if (!_confirmed && _cancelledCallback) {
if (!_confirmed && !_textTaken && _cancelledCallback) {
_cancelledCallback();
}
auto text = _caption->getTextWithAppliedMarkdown();
@@ -1268,6 +1322,10 @@ void SendFilesBox::generatePreviewFrom(int fromBlock) {
if (albumStart >= 0) {
pushBlock(albumStart, _list.files.size());
}
if (_replyHeader) {
_replyHeader->setRoundedShapeBelow(
!_blocks.empty() && !_blocks.front().isSingleFile());
}
}
void SendFilesBox::pushBlock(int from, int till) {
@@ -1463,7 +1521,10 @@ void SendFilesBox::pushBlock(int from, int till) {
entry.videoCover = nullptr;
});
};
const auto showContextMenu = [=](int fileIndex, QPoint globalPosition) {
const auto showContextMenu = [=](
int fileIndex,
QPoint globalPosition,
bool forceToLeft = false) {
if (from >= till
|| fileIndex < from
|| fileIndex >= till
@@ -1473,7 +1534,10 @@ void SendFilesBox::pushBlock(int from, int till) {
state->menu = base::make_unique_q<Ui::PopupMenu>(
widget,
_st.tabbed.menu);
state->menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
if (forceToLeft) {
using Origin = Ui::PanelAnimation::Origin;
state->menu->setForcedOrigin(Origin::TopRight);
}
const auto &file = _list.files[fileIndex];
state->menu->addAction(tr::lng_attach_replace(tr::now), [=] {
replaceAttachment(fileIndex);
@@ -1534,7 +1598,9 @@ void SendFilesBox::pushBlock(int from, int till) {
&st::menuIconCaptionShow);
}
const auto canToggleSpoiler = !hasPrice()
&& _sendWay.current().sendImagesAsPhotos();
&& _sendWay.current().sendImagesAsPhotos()
&& (file.type == Ui::PreparedFile::Type::Photo
|| file.type == Ui::PreparedFile::Type::Video);
if (canToggleSpoiler) {
const auto spoilered = file.spoiler;
const auto &icons = _st.tabbed.icons;
@@ -1606,7 +1672,7 @@ void SendFilesBox::pushBlock(int from, int till) {
block.itemReplaceRequest(
) | rpl::on_next([=](int index) {
showContextMenu(index, QCursor::pos());
showContextMenu(index, QCursor::pos(), true);
}, widget->lifetime());
block.itemModifyRequest(
@@ -2129,6 +2195,7 @@ void SendFilesBox::updateBoxSize() {
if (!_caption->isHidden()) {
footerHeight += st::boxPhotoCaptionSkip + _caption->height();
}
footerHeight += _replyHeaderHeight.current();
const auto pairs = std::array<std::pair<RpWidget*, int>, 4>{ {
{ _groupFiles.data(), st::boxPhotoCompressedSkip },
{ _sendImagesAsPhotos.data(), st::boxPhotoCompressedSkip },
@@ -2216,8 +2283,14 @@ void SendFilesBox::updateControlsGeometry() {
bottom -= pair.second + pointer->heightNoMargins();
}
}
_scroll->resize(width(), bottom - _titleHeight.current());
_scroll->move(0, _titleHeight.current());
const auto replyH = _replyHeaderHeight.current();
const auto replyTopOverlap = std::min(st::boxPhotoCaptionSkip, replyH);
const auto replyTop = _titleHeight.current() - replyTopOverlap;
if (_replyHeader) {
_replyHeader->setGeometry(0, replyTop, width(), replyH);
}
_scroll->resize(width(), bottom - replyTop - replyH);
_scroll->move(0, replyTop + replyH);
}
void SendFilesBox::showFinished() {
@@ -2232,10 +2305,16 @@ rpl::producer<TextWithTags> SendFilesBox::takeTextWithTagsRequests() const {
return _textWithTagsRequests.events();
}
void SendFilesBox::requestToTakeTextWithTags() const {
if (!_caption->isHidden()) {
_textWithTagsRequests.fire_copy(_caption->getTextWithTags());
void SendFilesBox::requestToTakeTextWithTags() {
if (_caption->isHidden()) {
return;
}
const auto text = _caption->getTextWithTags();
if (!_prefilledCaptionText.text.isEmpty() && text.text.isEmpty()) {
return;
}
_textTaken = true;
_textWithTagsRequests.fire_copy(text);
}
void SendFilesBox::setInnerFocus() {
@@ -2367,7 +2446,7 @@ void SendFilesBox::send(
}
}
_confirmedCallback(std::move(bundle), options);
_confirmedCallback(std::move(bundle), options, _replyTo);
}
closeBox();
}
+16 -2
View File
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/flags.h"
#include "data/data_msg_id.h"
#include "ui/layers/box_content.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/chat/attach/attach_send_files_way.h"
@@ -62,6 +63,10 @@ class CharactersLimitLabel;
class ComposeAiButton;
} // namespace HistoryView::Controls
namespace SendFiles {
class ReplyPillHeader;
} // namespace SendFiles
enum class SendFilesAllow {
OnlyOne = (1 << 0),
Photos = (1 << 1),
@@ -91,7 +96,8 @@ using SendFilesCheck = Fn<bool(
using SendFilesConfirmed = Fn<void(
std::shared_ptr<Ui::PreparedBundle>,
Api::SendOptions)>;
Api::SendOptions,
FullReplyTo)>;
struct SendFilesBoxDescriptor {
std::shared_ptr<ChatHelpers::Show> show;
@@ -105,6 +111,7 @@ struct SendFilesBoxDescriptor {
const style::ComposeControls *stOverride = nullptr;
SendFilesConfirmed confirmed;
Fn<void()> cancelled;
FullReplyTo replyTo;
Fn<void(const TextWithTags &text)> cancelled2;
};
@@ -131,6 +138,7 @@ public:
void setCancelledCallback(Fn<void()> callback) {
_cancelledCallback = std::move(callback);
}
void setReplyTo(FullReplyTo replyTo);
[[nodiscard]] rpl::producer<TextWithTags> takeTextWithTagsRequests() const;
@@ -166,6 +174,7 @@ private:
[[nodiscard]] int fromIndex() const;
[[nodiscard]] int tillIndex() const;
[[nodiscard]] bool isSingleFile() const;
[[nodiscard]] object_ptr<Ui::RpWidget> takeWidget();
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
@@ -263,7 +272,7 @@ private:
void checkCharsLimitation();
void refreshMessagesCount();
void requestToTakeTextWithTags() const;
void requestToTakeTextWithTags();
bool validateLength(const QString &text) const;
[[nodiscard]] Fn<MenuDetails()> prepareSendMenuDetails(
@@ -297,6 +306,7 @@ private:
std::unique_ptr<Ui::RpWidget> _priceTag;
QImage _priceTagBg;
bool _confirmed = false;
bool _textTaken = false;
bool _invertCaption = false;
const object_ptr<Ui::InputField> _caption;
@@ -318,6 +328,10 @@ private:
rpl::variable<int> _footerHeight = 0;
rpl::lifetime _dimensionsLifetime;
std::unique_ptr<SendFiles::ReplyPillHeader> _replyHeader;
rpl::variable<int> _replyHeaderHeight = 0;
FullReplyTo _replyTo;
object_ptr<Ui::ScrollArea> _scroll;
QPointer<Ui::VerticalLayout> _inner;
std::deque<Block> _blocks;
@@ -0,0 +1,372 @@
/*
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 "boxes/send_files_box_reply_header.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/ui_integration.h"
#include "data/data_changes.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_reply.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/image/image.h"
#include "ui/painter.h"
#include "ui/text/text_options.h"
#include "ui/power_saving.h"
#include "ui/widgets/buttons.h"
#include "window/window_session_controller.h"
#include "apiwrap.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
namespace SendFiles {
namespace {
constexpr auto kAnimationDuration = crl::time(180);
} // namespace
ReplyPillHeader::ReplyPillHeader(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo replyTo)
: RpWidget(parent)
, _show(std::move(show))
, _data(&_show->session().data())
, _replyTo(std::move(replyTo))
, _cancel(Ui::CreateChild<Ui::IconButton>(this, st::sendFilesReplyCancel)) {
resize(
parent->width(),
st::boxPhotoCaptionSkip + st::historyReplyHeight);
_cancel->setAccessibleName(tr::lng_cancel(tr::now));
_cancel->setClickedCallback([=] {
hideAnimated();
});
setShownMessage(_data->message(_replyTo.messageId));
_data->session().changes().messageUpdates(
Data::MessageUpdate::Flag::Edited
| Data::MessageUpdate::Flag::Destroyed
) | rpl::filter([=](const Data::MessageUpdate &update) {
return (update.item == _shownMessage);
}) | rpl::on_next([=](const Data::MessageUpdate &update) {
if (update.flags & Data::MessageUpdate::Flag::Destroyed) {
_shownMessage = nullptr;
_shownMessageName.clear();
_shownMessageText.clear();
hideAnimated();
} else {
updateShownMessageText();
RpWidget::update();
}
}, lifetime());
animationCallback();
}
ReplyPillHeader::~ReplyPillHeader() = default;
rpl::producer<> ReplyPillHeader::closeRequests() const {
return _closeRequests.events();
}
rpl::producer<> ReplyPillHeader::hideFinished() const {
return _hideFinished.value()
| rpl::filter(rpl::mappers::_1)
| rpl::to_empty;
}
rpl::producer<int> ReplyPillHeader::desiredHeight() const {
return _desiredHeight.value();
}
void ReplyPillHeader::setRoundedShapeBelow(bool value) {
if (_roundedShapeBelow == value) {
return;
}
_roundedShapeBelow = value;
update();
}
void ReplyPillHeader::hideAnimated() {
if (_hiding) {
return;
}
_hiding = true;
_closeRequests.fire({});
_showAnimation.start(
[=] { animationCallback(); },
1.,
0.,
kAnimationDuration);
}
void ReplyPillHeader::animationCallback() {
const auto full = st::boxPhotoCaptionSkip + st::historyReplyHeight;
const auto value = _showAnimation.value(_hiding ? 0. : 1.);
_desiredHeight = int(base::SafeRound(full * value));
update();
if (_hiding && !_showAnimation.animating()) {
_hideFinished = true;
}
}
void ReplyPillHeader::resolveMessageData() {
const auto id = _replyTo.messageId;
if (!id || !id.peer) {
return;
}
const auto peer = _data->peer(id.peer);
const auto itemId = id.msg;
const auto callback = crl::guard(this, [=] {
if (!_shownMessage) {
if (const auto message = _data->message(peer, itemId)) {
setShownMessage(message);
} else {
hideAnimated();
}
}
});
_data->session().api().requestMessageData(peer, itemId, callback);
}
void ReplyPillHeader::setShownMessage(HistoryItem *item) {
_shownMessage = item;
if (item) {
updateShownMessageText();
const auto context = Core::TextContext({
.session = &item->history()->session(),
.customEmojiLoopLimit = 1,
});
_shownMessageName.setMarkedText(
st::fwdTextStyle,
HistoryView::Reply::ComposePreviewName(
item->history(),
item,
_replyTo),
Ui::NameTextOptions(),
context);
} else {
_shownMessageName.clear();
_shownMessageText.clear();
resolveMessageData();
}
update();
}
void ReplyPillHeader::updateShownMessageText() {
Expects(_shownMessage != nullptr);
const auto context = Core::TextContext({
.session = &_data->session(),
.repaint = [=] { customEmojiRepaint(); },
});
_shownMessageText.setMarkedText(
st::messageTextStyle,
(_replyTo.quote.empty()
? _shownMessage->inReplyText()
: _replyTo.quote),
Ui::DialogTextOptions(),
context);
}
void ReplyPillHeader::customEmojiRepaint() {
if (_repaintScheduled) {
return;
}
_repaintScheduled = true;
update();
}
void ReplyPillHeader::resizeEvent(QResizeEvent *e) {
_cancel->moveToRight(
st::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,
(st::historyReplyHeight - _cancel->height()) / 2);
}
void ReplyPillHeader::paintEvent(QPaintEvent *e) {
_repaintScheduled = false;
Painter p(this);
p.setInactive(_show->paused(Window::GifPauseReason::Layer));
const auto left = st::boxPhotoPadding.left();
const auto right = st::boxPhotoPadding.right();
const auto bottomSkip = st::boxPhotoCaptionSkip;
const auto pillHeight = height() - bottomSkip;
if (pillHeight <= 0) {
return;
}
const auto pillRect = QRect(
left,
0,
width() - left - right,
pillHeight);
if (pillRect.isEmpty()) {
return;
}
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
const auto topRadius = st::bubbleRadiusLarge;
const auto bottomRadius = _roundedShapeBelow
? st::bubbleRadiusSmall
: st::bubbleRadiusLarge;
const auto rectF = QRectF(pillRect);
auto path = QPainterPath();
path.moveTo(rectF.left() + topRadius, rectF.top());
path.lineTo(rectF.right() - topRadius, rectF.top());
path.arcTo(
rectF.right() - 2 * topRadius,
rectF.top(),
2 * topRadius,
2 * topRadius,
90, -90);
path.lineTo(rectF.right(), rectF.bottom() - bottomRadius);
path.arcTo(
rectF.right() - 2 * bottomRadius,
rectF.bottom() - 2 * bottomRadius,
2 * bottomRadius,
2 * bottomRadius,
0, -90);
path.lineTo(rectF.left() + bottomRadius, rectF.bottom());
path.arcTo(
rectF.left(),
rectF.bottom() - 2 * bottomRadius,
2 * bottomRadius,
2 * bottomRadius,
270, -90);
path.lineTo(rectF.left(), rectF.top() + topRadius);
path.arcTo(
rectF.left(),
rectF.top(),
2 * topRadius,
2 * topRadius,
180, -90);
path.closeSubpath();
p.fillPath(path, st::windowBgOver);
}
const auto iconPos = st::sendFilesReplyIconPosition
+ QPoint(pillRect.left(), pillRect.top());
if (!_replyTo.quote.empty()) {
st::historyQuoteIcon.paint(p, iconPos, width());
} else {
st::historyReplyIcon.paint(p, iconPos, width());
// Remove 'settings' mini-icon.
p.fillRect(
QRect(
QPoint(style::ConvertScale(16), style::ConvertScale(5))
+ iconPos,
QSize(style::ConvertScale(11), style::ConvertScale(8))),
st::windowBgOver);
p.fillRect(
QRect(
QPoint(style::ConvertScale(22), style::ConvertScale(13))
+ iconPos,
QSize(style::ConvertScale(5), style::ConvertScale(2))),
st::windowBgOver);
}
const auto replySkip = st::historyReplySkip;
const auto textLeft = pillRect.left() + replySkip;
const auto availableWidth = _cancel->x() - textLeft;
if (availableWidth <= 0) {
return;
}
const auto pillCenterY = pillRect.top()
+ st::historyReplyHeight / 2;
if (!_shownMessage) {
p.setFont(st::msgDateFont);
p.setPen(st::historyComposeAreaFgService);
const auto top = pillCenterY - st::msgDateFont->height / 2;
p.drawText(
textLeft,
top + st::msgDateFont->ascent,
st::msgDateFont->elided(
tr::lng_profile_loading(tr::now),
availableWidth));
return;
}
const auto media = _shownMessage->media();
const auto hasPreview = media && media->hasReplyPreview();
const auto preview = hasPreview ? media->replyPreview() : nullptr;
const auto spoilered = media && media->hasSpoiler();
if (!spoilered) {
_previewSpoiler = nullptr;
} else if (!_previewSpoiler) {
_previewSpoiler = std::make_unique<Ui::SpoilerAnimation>([=] {
update();
});
}
const auto previewSkipValue = st::historyReplyPreview + st::msgReplyBarSkip;
const auto previewSkip = (hasPreview && preview) ? previewSkipValue : 0;
const auto contentLeft = textLeft + previewSkip;
const auto contentAvailable = availableWidth - previewSkip;
if (preview) {
const auto to = QRect(
textLeft,
pillCenterY - st::historyReplyPreview / 2,
st::historyReplyPreview,
st::historyReplyPreview);
p.drawPixmap(to.x(), to.y(), preview->pixSingle(
preview->size() / style::DevicePixelRatio(),
{
.options = Images::Option::RoundSmall,
.outer = to.size(),
}));
if (_previewSpoiler) {
Ui::FillSpoilerRect(
p,
to,
Ui::DefaultImageSpoiler().frame(
_previewSpoiler->index(crl::now(), p.inactive())));
}
}
p.setPen(st::historyReplyNameFg);
p.setFont(st::msgServiceNameFont);
_shownMessageName.drawElided(
p,
contentLeft,
pillRect.top() + st::msgReplyPadding.top(),
contentAvailable);
p.setPen(st::historyComposeAreaFg);
_shownMessageText.draw(p, {
.position = QPoint(
contentLeft,
pillRect.top()
+ st::msgReplyPadding.top()
+ st::msgServiceNameFont->height),
.availableWidth = contentAvailable,
.palette = &st::historyComposeAreaPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = crl::now(),
.pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat),
.pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler),
.elisionLines = 1,
});
}
} // namespace SendFiles
@@ -0,0 +1,77 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
#include "ui/text/text.h"
class HistoryItem;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
class Session;
} // namespace Data
namespace Ui {
class IconButton;
class SpoilerAnimation;
} // namespace Ui
namespace SendFiles {
class ReplyPillHeader final : public Ui::RpWidget {
public:
ReplyPillHeader(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo replyTo);
~ReplyPillHeader();
[[nodiscard]] rpl::producer<> closeRequests() const;
[[nodiscard]] rpl::producer<> hideFinished() const;
[[nodiscard]] rpl::producer<int> desiredHeight() const;
void setRoundedShapeBelow(bool value);
void hideAnimated();
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void resolveMessageData();
void setShownMessage(HistoryItem *item);
void updateShownMessageText();
void customEmojiRepaint();
void animationCallback();
const std::shared_ptr<ChatHelpers::Show> _show;
const not_null<Data::Session*> _data;
const FullReplyTo _replyTo;
const not_null<Ui::IconButton*> _cancel;
HistoryItem *_shownMessage = nullptr;
Ui::Text::String _shownMessageName;
Ui::Text::String _shownMessageText;
std::unique_ptr<Ui::SpoilerAnimation> _previewSpoiler;
bool _repaintScheduled = false;
Ui::Animations::Simple _showAnimation;
rpl::variable<int> _desiredHeight = 0;
rpl::event_stream<> _closeRequests;
rpl::variable<bool> _hideFinished = false;
bool _hiding = false;
bool _roundedShapeBelow = true;
};
} // namespace SendFiles
+5 -3
View File
@@ -2068,16 +2068,18 @@ void FastShareMessageToSelf(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto self = show->session().user();
auto &owner = self->owner();
const auto items = owner.idsToItems(owner.itemOrItsGroup(item));
const auto donePhraseArgs = ChatHelpers::ForwardedMessagePhraseArgs{
.toCount = 1,
.singleMessage = true,
.singleMessage = (items.size() == 1),
.to1 = self,
.to2 = nullptr,
};
auto sendAction = Api::SendAction(self->owner().history(self));
auto sendAction = Api::SendAction(owner.history(self));
sendAction.clearDraft = false;
show->session().api().forwardMessages(
Data::ResolvedForwardDraft{ .items = {item} },
Data::ResolvedForwardDraft{ .items = items },
std::move(sendAction),
[=] {
auto phrase = rpl::variable<TextWithEntities>(
@@ -55,7 +55,6 @@ constexpr auto kSuccessFadeInDuration = crl::time(300);
constexpr auto kSuccessExpandDuration = crl::time(400);
constexpr auto kSuccessExpandStart = crl::time(100);
constexpr auto kProgressFadeInDuration = crl::time(300);
constexpr auto kFailureFadeInDuration = crl::time(300);
[[nodiscard]] QString FormatPercent(int permille) {
const auto rounded = (permille + 5) / 10;
@@ -120,7 +120,6 @@ private:
QColor _patternColor;
Ui::Animations::Simple _selectedAnimation;
bool _selected : 1 = false;
bool _patterned : 1 = false;
QMargins _extend;
@@ -0,0 +1,389 @@
/*
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 "boxes/sticker_creator_box.h"
#include "api/api_stickers_creator.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/emoji_picker_overlay.h"
#include "core/file_utilities.h"
#include "editor/editor_layer_widget.h"
#include "editor/photo_editor.h"
#include "editor/photo_editor_common.h"
#include "editor/scene/scene.h"
#include "editor/scene/scene_item_image.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/emoji_config.h"
#include "ui/image/image.h"
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/layers/layer_widget.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_editor.h"
#include "styles/style_layers.h"
#include <QtCore/QBuffer>
#include <QtGui/QImageReader>
namespace {
constexpr auto kStickerSide = 512;
constexpr auto kPreviewSide = 256;
constexpr auto kWebpQuality = 95;
constexpr auto kMaxEmojis = 7;
[[nodiscard]] QImage LoadImageFromFile(const QString &path) {
auto reader = QImageReader(path);
reader.setAutoTransform(true);
auto image = reader.read();
if (image.format() != QImage::Format_ARGB32_Premultiplied
&& image.format() != QImage::Format_ARGB32) {
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
return image;
}
class PreviewWidget final : public Ui::RpWidget {
public:
PreviewWidget(QWidget *parent, QImage image)
: RpWidget(parent)
, _image(std::move(image)) {
resize(kPreviewSide, kPreviewSide);
}
protected:
void paintEvent(QPaintEvent *e) override {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto target = QRect(0, 0, width(), height());
p.drawImage(target, _image);
}
private:
const QImage _image;
};
void OpenPhotoEditorForSticker(
std::shared_ptr<ChatHelpers::Show> show,
QImage image,
Fn<void(QImage&&)> onDone) {
if (image.isNull()) {
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
return;
}
const auto sessionController = show->resolveWindow();
if (!sessionController) {
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
return;
}
const auto windowController = &sessionController->window();
const auto parentWidget = sessionController->widget();
if (image.width() <= 0
|| image.height() <= 0
|| (image.width() > 10 * image.height())
|| (image.height() > 10 * image.width())) {
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
return;
}
auto canvas = QImage(
kStickerSide,
kStickerSide,
QImage::Format_ARGB32_Premultiplied);
canvas.fill(Qt::transparent);
const auto baseImage = std::make_shared<Image>(std::move(canvas));
auto scene = std::make_shared<Editor::Scene>(
QRectF(0, 0, kStickerSide, kStickerSide));
const auto userPixmap = QPixmap::fromImage(std::move(image));
const auto userSize = userPixmap.size();
const auto fitted = userSize.scaled(
QSize(kStickerSide, kStickerSide),
Qt::KeepAspectRatio);
const auto handle = st::photoEditorItemHandleSize;
const auto itemSize = (userSize.width() >= userSize.height())
? int((fitted.height() + handle)
* userSize.width() / float64(userSize.height()))
: (fitted.width() + handle);
auto itemData = Editor::ItemBase::Data{
.initialZoom = 1.0,
.zPtr = scene->lastZ(),
.size = itemSize,
.x = kStickerSide / 2,
.y = kStickerSide / 2,
.imageSize = userSize,
};
auto imageItem = std::make_shared<Editor::ItemImage>(
QPixmap(userPixmap),
std::move(itemData));
scene->addItem(std::move(imageItem));
auto modifications = Editor::PhotoModifications{
.crop = QRect(0, 0, kStickerSide, kStickerSide),
.paint = std::move(scene),
};
auto editor = base::make_unique_q<Editor::PhotoEditor>(
parentWidget,
windowController,
baseImage,
std::move(modifications),
Editor::EditorData{
.exactSize = QSize(kStickerSide, kStickerSide),
.cropType = Editor::EditorData::CropType::RoundedRect,
.keepAspectRatio = true,
.fixedCrop = true,
});
const auto raw = editor.get();
auto applyModifications = [=, done = std::move(onDone)](
const Editor::PhotoModifications &mods) mutable {
auto result = Editor::ImageModified(baseImage->original(), mods);
if (result.size() != QSize(kStickerSide, kStickerSide)) {
result = result.scaled(
kStickerSide,
kStickerSide,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
done(std::move(result));
};
auto layer = std::make_unique<Editor::LayerWidget>(
parentWidget,
std::move(editor));
Editor::InitEditorLayer(layer.get(), raw, std::move(applyModifications));
windowController->showLayer(
std::move(layer),
Ui::LayerOption::KeepOther);
}
[[nodiscard]] QByteArray EncodeWebp(QImage image) {
if (image.size() != QSize(kStickerSide, kStickerSide)) {
image = image.scaled(
kStickerSide,
kStickerSide,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
if (image.format() != QImage::Format_ARGB32) {
image = image.convertToFormat(QImage::Format_ARGB32);
}
auto bytes = QByteArray();
auto buffer = QBuffer(&bytes);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "WEBP", kWebpQuality);
return bytes;
}
} // namespace
namespace Api {
void CreateStickerBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
StickerSetIdentifier set,
QImage image,
Fn<void(MTPmessages_StickerSet)> done) {
struct State {
rpl::variable<bool> uploading = false;
std::unique_ptr<StickerUpload> upload;
QPointer<Ui::RoundButton> addButton;
};
const auto state = box->lifetime().make_state<State>();
const auto session = &show->session();
box->setTitle(tr::lng_stickers_create_image_title());
const auto inner = box->verticalLayout();
auto pickerDescriptor = ChatHelpers::EmojiPickerOverlayDescriptor{
.aboutText = tr::lng_stickers_create_emoji_about(tr::now),
.maxSelected = kMaxEmojis,
.allowExpand = true,
};
const auto metrics = ChatHelpers::EmojiPickerOverlay::EstimateMetrics(
pickerDescriptor.aboutText);
const auto pickerCollapsed = metrics.collapsedHeight;
const auto pickerTotalExpanded = metrics.totalExpandedHeight;
const auto shadowExt = metrics.shadowExtent;
constexpr auto kStickerOverlap = 24;
const auto stickerTop = shadowExt.top()
+ pickerCollapsed
- kStickerOverlap;
const auto holderHeight = std::max(
stickerTop + kPreviewSide,
pickerTotalExpanded);
const auto previewHolder = inner->add(
object_ptr<Ui::RpWidget>(inner),
QMargins(0, 0, 0, 0),
style::al_top);
previewHolder->resize(st::boxWideWidth, holderHeight);
const auto preview = Ui::CreateChild<PreviewWidget>(
previewHolder,
image);
const auto picker = Ui::CreateChild<ChatHelpers::EmojiPickerOverlay>(
previewHolder,
std::move(pickerDescriptor));
auto layoutOverlay = [=] {
const auto bubbleW = std::min(
previewHolder->width()
- 2 * st::boxRowPadding.left()
- shadowExt.left() - shadowExt.right(),
int(kPreviewSide * 1.1));
const auto totalW = bubbleW + shadowExt.left() + shadowExt.right();
const auto x = (previewHolder->width() - totalW) / 2;
picker->setGeometry(x, 0, totalW, pickerTotalExpanded);
picker->raise();
};
previewHolder->widthValue(
) | rpl::on_next([=](int width) {
preview->move((width - kPreviewSide) / 2, stickerTop);
layoutOverlay();
}, preview->lifetime());
Ui::AddSkip(inner);
const auto startUpload = [=, set = std::move(set), done = std::move(done)](
) mutable {
if (state->uploading.current()) {
return;
}
auto emoji = QString();
for (const auto one : picker->selected()) {
emoji.append(one->text());
}
if (emoji.isEmpty()) {
show->showToast(
tr::lng_stickers_create_emoji_required(tr::now));
return;
}
const auto bytes = EncodeWebp(image);
if (bytes.isEmpty()) {
show->showToast(
tr::lng_stickers_create_upload_failed(tr::now));
return;
}
const auto lockedWidth = state->addButton
? state->addButton->width()
: 0;
state->uploading = true;
if (state->addButton && lockedWidth > 0) {
state->addButton->resizeToWidth(lockedWidth);
}
state->upload = std::make_unique<StickerUpload>(
session,
set,
bytes,
emoji);
const auto doneCallback = done;
state->upload->start(
crl::guard(box, [=](MTPmessages_StickerSet result) {
state->upload = nullptr;
state->uploading = false;
show->showToast(tr::lng_stickers_create_added(tr::now));
if (doneCallback) {
doneCallback(result);
}
box->closeBox();
}),
crl::guard(box, [=](QString err) {
state->upload = nullptr;
state->uploading = false;
show->showToast(err.isEmpty()
? tr::lng_stickers_create_upload_failed(tr::now)
: err);
}));
};
const auto addButton = box->addButton(
rpl::conditional(
state->uploading.value(),
rpl::single(QString()),
tr::lng_box_done()),
startUpload);
state->addButton = addButton;
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
{
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
addButton,
addButton->height() / 2,
&st::editStickerSetNameLoading);
AddChildToWidgetCenter(addButton, loadingAnimation);
loadingAnimation->showOn(state->uploading.value());
}
box->setWidth(st::boxWideWidth);
box->boxClosing(
) | rpl::on_next([=] {
state->upload = nullptr;
}, box->lifetime());
}
void OpenCreateStickerFlow(
std::shared_ptr<ChatHelpers::Show> show,
StickerSetIdentifier set,
Fn<void(MTPmessages_StickerSet)> done) {
const auto parent = QPointer<QWidget>(show->toastParent());
const auto onChosen = [=, set = std::move(set), done = std::move(done)](
FileDialog::OpenResult &&result) mutable {
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
return;
}
const auto path = result.paths.isEmpty()
? QString()
: result.paths.front();
auto image = path.isEmpty()
? QImage::fromData(result.remoteContent)
: LoadImageFromFile(path);
OpenPhotoEditorForSticker(
show,
std::move(image),
[=, set = std::move(set), done = std::move(done)](
QImage &&prepared) mutable {
show->showBox(Box(
CreateStickerBox,
show,
std::move(set),
std::move(prepared),
std::move(done)));
});
};
FileDialog::GetOpenPath(
parent,
tr::lng_stickers_create_choose_image(tr::now),
FileDialog::ImagesFilter(),
std::move(onChosen));
}
} // namespace Api
@@ -0,0 +1,36 @@
/*
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 "data/stickers/data_stickers.h"
#include <QtGui/QImage>
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Api {
void CreateStickerBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
StickerSetIdentifier set,
QImage image,
Fn<void(MTPmessages_StickerSet)> done);
void OpenCreateStickerFlow(
std::shared_ptr<ChatHelpers::Show> show,
StickerSetIdentifier set,
Fn<void(MTPmessages_StickerSet)> done = nullptr);
} // namespace Api
+447 -27
View File
@@ -8,14 +8,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/sticker_set_box.h"
#include "api/api_common.h"
#include "api/api_stickers_creator.h"
#include "api/api_toggling_media.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/premium_preview_box.h"
#include "boxes/sticker_creator_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/stickers_list_widget.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
@@ -52,11 +55,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/gradient_round_button.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/menu/menu_multiline_action.h"
#include "base/event_filter.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "ui/widgets/inner_dropdown.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
@@ -333,6 +343,7 @@ public:
}
void applySet(const TLStickerSet &set);
void setOuterContainer(QPointer<QWidget> container);
~Inner();
@@ -390,6 +401,16 @@ private:
void startOverAnimation(int index, float64 from, float64 to);
int stickerFromGlobalPos(const QPoint &p) const;
[[nodiscard]] bool hasAddCell() const;
[[nodiscard]] int totalCellsCount() const;
[[nodiscard]] QRect addCellRect() const;
[[nodiscard]] bool addCellFromGlobalPos(const QPoint &p) const;
void setAddCellHovered(bool hovered);
void paintAddCell(QPainter &p) const;
void showAddMenu(QPoint globalPos);
void startAddExistingStickerFlow();
void startCreateNewStickerFlow();
void installDone(const MTPmessages_StickerSetInstallResult &result);
void requestReorder(not_null<DocumentData*> document, int index);
@@ -473,6 +494,8 @@ private:
mtpRequestId _installRequest = 0;
int _selected = -1;
bool _addCellHovered = false;
bool _addCellPressed = false;
base::Timer _previewTimer;
int _previewShown = -1;
@@ -480,6 +503,8 @@ private:
bool _previewLocked = false;
base::unique_qptr<Ui::PopupMenu> _menu;
base::unique_qptr<ChatHelpers::TabbedPanel> _pickerPanel;
QPointer<QWidget> _outerContainer;
rpl::event_stream<uint64> _setInstalled;
rpl::event_stream<uint64> _setArchived;
@@ -533,6 +558,7 @@ void StickerSetBox::prepare() {
_inner = setInnerWidget(
object_ptr<Inner>(this, _show, _set, _type),
st::stickersScroll);
_inner->setOuterContainer(getDelegate()->outerContainer());
if (const auto previewId = base::take(_previewDocumentId)) {
_inner->showPreviewForDocument(previewId);
}
@@ -779,7 +805,99 @@ void StickerSetBox::updateButtons() {
&st::menuIconReorder);
});
}();
const auto addPackIdActions = [=](const std::shared_ptr<base::unique_qptr<Ui::PopupMenu>> &menu)
const auto fillSetCreatorFooter = [&] {
using Filler = Fn<void(not_null<Ui::PopupMenu*>)>;
if (!_inner->amSetCreator()) {
return Filler(nullptr);
}
const auto data = &_session->data();
return Filler([=, show = _show, set = _set](
not_null<Ui::PopupMenu*> menu) {
const auto weak = base::weak_qptr<StickerSetBox>(this);
const auto deleteEveryone = [=] {
const auto confirm = [=](Fn<void()> close) {
Api::DeleteStickerSet(
&data->session(),
set,
[=] {
if (const auto strong = weak.get()) {
strong->closeBox();
}
},
[=](const QString &error) {
show->showToast(error);
});
close();
};
show->showBox(Ui::MakeConfirmBox({
.text = tr::lng_stickers_delete_pack_sure(tr::now),
.confirmed = confirm,
.confirmText
= tr::lng_stickers_remove_pack_confirm(),
.confirmStyle = &st::attentionBoxButton,
}));
};
const auto deleteSelf = [show, inner = _inner] {
const auto raw = inner.data();
if (!raw) {
return;
}
auto box = ChatHelpers::MakeConfirmRemoveSetBox(
&show->session(),
st::boxLabel,
raw->setId());
if (box) {
show->showBox(std::move(box));
}
};
const auto deleteAction = menu->addAction(
base::make_unique_q<Ui::Menu::Action>(
menu->menu(),
st::menuWithIconsAttention,
Ui::Menu::CreateAction(
menu->menu().get(),
tr::lng_stickers_context_delete_pack(tr::now),
nullptr),
&st::menuIconDeleteAttention,
&st::menuIconDeleteAttention));
deleteAction->setMenu(
Ui::CreateChild<QMenu>(menu->menu().get()));
const auto sub = menu->ensureSubmenu(
deleteAction,
st::popupMenuWithIcons);
const auto addSub = Ui::Menu::CreateAddActionCallback(sub);
addSub({
.text = tr::lng_stickers_context_delete_pack_everyone(
tr::now),
.handler = deleteEveryone,
.icon = &st::menuIconDeleteAttention,
.isAttention = true,
});
sub->addAction(
tr::lng_stickers_context_delete_pack_self(tr::now),
deleteSelf,
&st::menuIconRemove);
menu->addSeparator(&st::expandedMenuSeparator);
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
menu->menu(),
st::defaultMenu,
st::historyHasCustomEmoji,
QPoint(
st::defaultMenu.itemPadding.left(),
st::defaultMenu.itemPadding.top()),
tr::lng_stickers_bot_more_options(
tr::now,
lt_bot,
Ui::Text::Colorized(tr::bold(u"@stickers"_q)),
Ui::Text::RichLangValue));
item->clicks(
) | rpl::on_next([] {
UrlClickHandler::Open(u"https://t.me/stickers"_q);
}, item->lifetime());
menu->addAction(std::move(item));
});
}();
const auto addPackIdActions = [=](Ui::PopupMenu *menu)
{
if (type == Data::StickersType::Stickers || type == Data::StickersType::Emoji) {
const auto &settings = AyuSettings::getInstance();
@@ -788,7 +906,7 @@ void StickerSetBox::updateButtons() {
const auto setId = _inner->setId();
const auto innerId = setId >> 32;
(*menu)->addAction(
menu->addAction(
tr::ayu_MessageDetailsPackOwnerPC(tr::now),
[weak, session, innerId]
{
@@ -831,7 +949,7 @@ void StickerSetBox::updateButtons() {
&st::menuIconProfile);
if (settings.showPeerId() != PeerIdDisplay::Hidden) {
(*menu)->addAction(
menu->addAction(
tr::ayu_ContextCopyID(tr::now),
[weak, setId]
{
@@ -881,16 +999,17 @@ void StickerSetBox::updateButtons() {
if (!_inner->shortName().isEmpty()) {
const auto top = addTopButton(st::infoTopBarMenu);
const auto menu
= std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
const auto menu = top->lifetime().make_state<
base::unique_qptr<Ui::PopupMenu>>();
top->setClickedCallback([=] {
*menu = base::make_unique_q<Ui::PopupMenu>(
top,
st::popupMenuWithIcons);
const auto raw = menu->get();
if (fillSetCreatorMenu) {
fillSetCreatorMenu(*menu);
fillSetCreatorMenu(raw);
}
(*menu)->addAction(
raw->addAction(
((type == Data::StickersType::Emoji)
? tr::lng_stickers_share_emoji
: (type == Data::StickersType::Masks)
@@ -898,8 +1017,21 @@ void StickerSetBox::updateButtons() {
: tr::lng_stickers_share_pack)(tr::now),
[=] { share(); closeBox(); },
&st::menuIconShare);
addPackIdActions(menu);
(*menu)->popup(QCursor::pos());
addPackIdActions(raw);
if (fillSetCreatorFooter) {
fillSetCreatorFooter(raw);
}
raw->setForcedOrigin(
Ui::PanelAnimation::Origin::TopRight);
top->setForceRippled(true);
raw->setDestroyedCallback([=] {
if (const auto strong = top.data()) {
strong->setForceRippled(false);
}
});
raw->popup(top->mapToGlobal(QPoint(
top->width(),
top->height() - st::lineWidth * 3)));
return true;
});
}
@@ -929,30 +1061,51 @@ void StickerSetBox::updateButtons() {
_show->showBox(std::move(box));
}
};
const auto menu
= std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
const auto menu = top->lifetime().make_state<
base::unique_qptr<Ui::PopupMenu>>();
top->setClickedCallback([=] {
*menu = base::make_unique_q<Ui::PopupMenu>(
top,
st::popupMenuWithIcons);
const auto raw = menu->get();
if (type == Data::StickersType::Emoji) {
(*menu)->addAction(
tr::lng_custom_emoji_remove_pack_button(tr::now),
remove,
&st::menuIconRemove);
if (fillSetCreatorMenu) {
fillSetCreatorMenu(raw);
}
if (fillSetCreatorFooter) {
fillSetCreatorFooter(raw);
} else {
raw->addAction(
tr::lng_custom_emoji_remove_pack_button(tr::now),
remove,
&st::menuIconRemove);
}
} else {
if (fillSetCreatorMenu) {
fillSetCreatorMenu(*menu);
fillSetCreatorMenu(raw);
}
(*menu)->addAction(
raw->addAction(
(type == Data::StickersType::Masks
? tr::lng_masks_archive_pack(tr::now)
: tr::lng_stickers_archive_pack(tr::now)),
archive,
&st::menuIconArchive);
if (fillSetCreatorFooter) {
fillSetCreatorFooter(raw);
}
}
addPackIdActions(menu);
(*menu)->popup(QCursor::pos());
addPackIdActions(raw);
raw->setForcedOrigin(
Ui::PanelAnimation::Origin::TopRight);
top->setForceRippled(true);
raw->setDestroyedCallback([=] {
if (const auto strong = top.data()) {
strong->setForceRippled(false);
}
});
raw->popup(top->mapToGlobal(QPoint(
top->width(),
top->height() - st::lineWidth * 3)));
return true;
});
}
@@ -1095,7 +1248,8 @@ void StickerSetBox::Inner::applySet(const TLStickerSet &set) {
& (SetFlag::Featured
| SetFlag::NotLoaded
| SetFlag::Unread
| SetFlag::Special);
| SetFlag::Special
| SetFlag::Installed);
_setFlags |= clientFlags;
set->flags = _setFlags;
set->installDate = _setInstallDate;
@@ -1112,15 +1266,15 @@ void StickerSetBox::Inner::applySet(const TLStickerSet &set) {
_errors.fire(Error::NotFound);
return;
}
_loaded = true;
_perRow = isEmojiSet() ? kEmojiPerRow : kStickersPerRow;
_rowsCount = (_pack.size() + _perRow - 1) / _perRow;
_singleSize = isEmojiSet() ? st::emojiSetSize : st::stickersSize;
_rowsCount = (totalCellsCount() + _perRow - 1) / _perRow;
resize(
_padding.left() + _perRow * _singleSize.width(),
_padding.top() + _rowsCount * _singleSize.height() + _padding.bottom());
_loaded = true;
if (const auto previewId = base::take(_previewDocumentId)) {
showPreviewForDocument(previewId);
}
@@ -1251,6 +1405,10 @@ void StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
if (addCellFromGlobalPos(e->globalPos())) {
_addCellPressed = !_dragging.enabled;
return;
}
const auto index = stickerFromGlobalPos(e->globalPos());
if (index < 0 || index >= _pack.size()) {
return;
@@ -1389,6 +1547,7 @@ void StickerSetBox::Inner::showPreviewForDocument(DocumentId documentId) {
void StickerSetBox::Inner::leaveEventHook(QEvent *e) {
setSelected(-1);
setAddCellHovered(false);
}
void StickerSetBox::Inner::requestReorder(
@@ -1481,6 +1640,13 @@ void StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
_previewShown = -1;
return;
}
if (_addCellPressed) {
_addCellPressed = false;
if (addCellFromGlobalPos(e->globalPos())) {
showAddMenu(e->globalPos());
}
return;
}
if (!_previewTimer.isActive()) {
return;
}
@@ -1563,6 +1729,25 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
&st::menuIconCopy);
}
}
if (!amSetCreator()) {
Api::AddAddToEmojiSetAction(
Ui::Menu::CreateAddActionCallback(_menu.get()),
_show,
_pack[index]);
} else {
const auto addAction = Ui::Menu::CreateAddActionCallback(
_menu.get());
addAction({
.text = tr::lng_emoji_context_delete(tr::now),
.handler = [index, this, show = _show] {
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
fillDeleteStickerBox(box, index);
}));
},
.icon = &st::menuIconDeleteAttention,
.isAttention = true,
});
}
} else if (details.type != SendMenu::Type::Disabled) {
const auto document = _pack[index];
const auto send = crl::guard(this, [=](Api::SendOptions options) {
@@ -1594,6 +1779,12 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
(isFaved
? &st::menuIconUnfave
: &st::menuIconFave));
if (!amSetCreator()) {
Api::AddAddToStickerSetAction(
Ui::Menu::CreateAddActionCallback(_menu.get()),
_show,
document);
}
if (amSetCreator()) {
const auto addAction = Ui::Menu::CreateAddActionCallback(
_menu.get());
@@ -1629,6 +1820,8 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
const auto document = _pack[index];
const auto weak = base::make_weak(this);
const auto show = _show;
const auto type = setType();
const auto isEmoji = (type == Data::StickersType::Emoji);
const auto container = box->verticalLayout();
Ui::AddSkip(container);
@@ -1663,7 +1856,9 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
}, sticker->lifetime());
const auto label = Ui::CreateChild<Ui::FlatLabel>(
line,
tr::lng_stickers_context_delete(),
isEmoji
? tr::lng_emoji_context_delete()
: tr::lng_stickers_context_delete(),
box->getDelegate()->style().title);
line->widthValue(
) | rpl::on_next([=](int width) {
@@ -1687,7 +1882,9 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
box->addRow(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_stickers_context_delete_sure(),
isEmoji
? tr::lng_emoji_context_delete_sure()
: tr::lng_stickers_context_delete_sure(),
st::boxLabel));
const auto save = [=] {
if (state->requestId.current()) {
@@ -1702,8 +1899,7 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
)).done([=](const TLStickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &d) {
document->owner().stickers().feedSetFull(d);
document->owner().stickers().notifyUpdated(
Data::StickersType::Stickers);
document->owner().stickers().notifyUpdated(type);
}, [](const auto &) {
});
if ([[maybe_unused]] const auto strong = weak.get()) {
@@ -1746,8 +1942,27 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
}
void StickerSetBox::Inner::updateSelected() {
auto selected = stickerFromGlobalPos(QCursor::pos());
const auto cursor = QCursor::pos();
const auto onAddCell = addCellFromGlobalPos(cursor);
const auto selected = onAddCell
? -1
: stickerFromGlobalPos(cursor);
setSelected(setType() == Data::StickersType::Masks ? -1 : selected);
setAddCellHovered(onAddCell);
}
void StickerSetBox::Inner::setAddCellHovered(bool hovered) {
if (_addCellHovered == hovered) {
return;
}
_addCellHovered = hovered;
if (hasAddCell()) {
setCursor((hovered && !_dragging.enabled)
? style::cur_pointer
: style::cur_default);
const auto rect = addCellRect();
rtlupdate(rect.x(), rect.y(), rect.width(), rect.height());
}
}
void StickerSetBox::Inner::setSelected(int selected) {
@@ -1886,6 +2101,10 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) {
paintSticker(p, _dragging.index, pos, paused, now);
}
if (hasAddCell()) {
paintAddCell(p);
}
if (_lottiePlayer && !paused) {
_lottiePlayer->markFrameShown();
}
@@ -2312,4 +2531,205 @@ void StickerSetBox::Inner::repaintItems(crl::time now) {
update();
}
bool StickerSetBox::Inner::hasAddCell() const {
return _loaded
&& _amSetCreator
&& (setType() == Data::StickersType::Stickers)
&& !_pack.isEmpty()
&& (_pack.size() < Api::kStickersInOwnedSetMax);
}
int StickerSetBox::Inner::totalCellsCount() const {
return _pack.size() + (hasAddCell() ? 1 : 0);
}
QRect StickerSetBox::Inner::addCellRect() const {
const auto index = _pack.size();
const auto row = index / _perRow;
const auto column = index % _perRow;
return QRect(
_padding.left() + column * _singleSize.width(),
_padding.top() + row * _singleSize.height(),
_singleSize.width(),
_singleSize.height());
}
bool StickerSetBox::Inner::addCellFromGlobalPos(const QPoint &p) const {
if (!hasAddCell()) {
return false;
}
auto local = mapFromGlobal(p);
if (rtl()) {
local.setX(width() - local.x());
}
const auto rect = addCellRect();
return rect.contains(local);
}
void StickerSetBox::Inner::paintAddCell(QPainter &p) const {
const auto ltrRect = addCellRect();
const auto rect = rtl()
? QRect(
width() - ltrRect.x() - ltrRect.width(),
ltrRect.y(),
ltrRect.width(),
ltrRect.height())
: ltrRect;
const auto inner = QRect(
rect::center(rect) - QPoint(
st::stickersAddCellBgRadius,
st::stickersAddCellBgRadius),
Size(st::stickersAddCellBgRadius * 2));
auto hq = PainterHighQualityEnabler(p);
const auto base = st::windowSubTextFg->c;
const auto bgAlpha = (_addCellHovered && !_dragging.enabled)
? 0.22
: 0.12;
p.setPen(Qt::NoPen);
p.setBrush(anim::with_alpha(base, bgAlpha));
p.drawEllipse(inner);
const auto plusHalf = st::stickersAddCellPlusSize / 2;
const auto thickness = st::stickersAddCellPlusThickness;
const auto center = rect.center();
const auto plusH = QRectF(
center.x() - plusHalf,
center.y() - thickness / 2.,
plusHalf * 2,
thickness);
const auto plusV = QRectF(
center.x() - thickness / 2.,
center.y() - plusHalf,
thickness,
plusHalf * 2);
const auto radius = thickness / 2.;
p.setBrush(base);
p.drawRoundedRect(plusH, radius, radius);
p.drawRoundedRect(plusV, radius, radius);
}
void StickerSetBox::Inner::showAddMenu(QPoint globalPos) {
if (_dragging.enabled) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
_menu->addAction(
tr::lng_stickers_create_new(tr::now),
crl::guard(this, [=] { startCreateNewStickerFlow(); }),
&st::menuIconStickerCreate);
_menu->addAction(
tr::lng_stickers_add_existing(tr::now),
crl::guard(this, [=] { startAddExistingStickerFlow(); }),
&st::menuIconStickerAdd);
_menu->popup(globalPos);
}
void StickerSetBox::Inner::setOuterContainer(QPointer<QWidget> container) {
_outerContainer = std::move(container);
}
void StickerSetBox::Inner::startAddExistingStickerFlow() {
if (!hasAddCell() || !_outerContainer) {
return;
}
const auto container = _outerContainer.data();
const auto identifier = StickerSetIdentifier{
.id = _setId,
.accessHash = _setAccessHash,
.shortName = _setShortName,
};
const auto session = _session;
const auto show = _show;
using Selector = ChatHelpers::TabbedSelector;
_pickerPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
container,
ChatHelpers::TabbedPanelDescriptor{
.ownedSelector = object_ptr<Selector>(
nullptr,
ChatHelpers::TabbedSelectorDescriptor{
.show = _show,
.st = st::defaultComposeControls.tabbed,
.level = Window::GifPauseReason::Layer,
.mode = Selector::Mode::StickersOnly,
.excludeStickerSetId = _setId,
}),
});
const auto panel = _pickerPanel.get();
panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
panel->setDropDown(true);
panel->setShowAnimationOrigin(Ui::PanelAnimation::Origin::TopLeft);
panel->hide();
panel->selector()->fileChosen(
) | rpl::on_next([=, this](const ChatHelpers::FileChosen &chosen) {
const auto document = chosen.document;
if (_pickerPanel) {
_pickerPanel->hideAnimated();
}
const auto emoji = Api::StickerEmojiOrDefault(document);
Api::AddExistingStickerToSet(
session,
identifier,
document,
emoji,
crl::guard(this, [=, this](MTPmessages_StickerSet result) {
applySet(result);
show->showToast(
tr::lng_stickers_create_added(tr::now));
}),
crl::guard(this, [=](QString err) {
show->showToast(err.isEmpty()
? tr::lng_attach_failed(tr::now)
: err);
}));
}, panel->lifetime());
const auto reposition = [=] {
const auto size = container->size();
const auto margins = st::emojiPanMargins;
const auto panelWidth = st::emojiPanWidth
+ margins.left()
+ margins.right();
const auto panelHeight = st::emojiPanMinHeight
+ margins.top()
+ margins.bottom();
const auto top = std::max(0, (size.height() - panelHeight) / 2);
const auto right = (size.width() + panelWidth) / 2;
panel->moveTopRight(top, right);
};
base::install_event_filter(panel, container, [=](
not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Move || type == QEvent::Resize) {
crl::on_main(panel, reposition);
}
return base::EventFilterResult::Continue;
});
reposition();
panel->showAnimated();
}
void StickerSetBox::Inner::startCreateNewStickerFlow() {
if (!hasAddCell()) {
return;
}
const auto identifier = StickerSetIdentifier{
.id = _setId,
.accessHash = _setAccessHash,
.shortName = _setShortName,
};
const auto onDone = crl::guard(this, [=, this](
MTPmessages_StickerSet result) {
applySet(result);
});
Api::OpenCreateStickerFlow(_show, identifier, onDone);
}
StickerSetBox::Inner::~Inner() = default;
+4 -8
View File
@@ -414,8 +414,7 @@ StickersBox::StickersBox(
std::shared_ptr<ChatHelpers::Show> show,
Section section,
bool masks)
: _st(st::stickersRowItem)
, _show(std::move(show))
: _show(std::move(show))
, _session(&_show->session())
, _api(&_session->mtp())
, _tabs(this, st::stickersTabs)
@@ -437,8 +436,7 @@ StickersBox::StickersBox(
std::shared_ptr<ChatHelpers::Show> show,
not_null<ChannelData*> megagroup,
bool isEmoji)
: _st(st::stickersRowItem)
, _show(std::move(show))
: _show(std::move(show))
, _session(&_show->session())
, _api(&_session->mtp())
, _section(Section::Installed)
@@ -456,8 +454,7 @@ StickersBox::StickersBox(
QWidget*,
std::shared_ptr<ChatHelpers::Show> show,
const QVector<MTPStickerSetCovered> &attachedSets)
: _st(st::stickersRowItem)
, _show(std::move(show))
: _show(std::move(show))
, _session(&_show->session())
, _api(&_session->mtp())
, _section(Section::Attached)
@@ -472,8 +469,7 @@ StickersBox::StickersBox(
QWidget*,
std::shared_ptr<ChatHelpers::Show> show,
const std::vector<StickerSetIdentifier> &emojiSets)
: _st(st::stickersRowItem)
, _show(std::move(show))
: _show(std::move(show))
, _session(&_show->session())
, _api(&_session->mtp())
, _section(Section::Attached)
@@ -143,7 +143,6 @@ private:
std::array<Inner*, 5> widgets() const;
const style::PeerListItem &_st;
const std::shared_ptr<ChatHelpers::Show> _show;
const not_null<Main::Session*> _session;
MTP::Sender _api;
+1 -1
View File
@@ -409,7 +409,7 @@ void RequestButton(
url,
displayName,
session->user()->name(),
bot->firstName,
bot ? bot->firstName : QString(),
callback),
Ui::LayerOption::KeepOther);
}
+1 -2
View File
@@ -52,7 +52,7 @@ callBodyLayout: CallBodyLayout {
participantsTop: 294px;
muteStroke: 3px;
muteSize: 36px;
mutePosition: point(142px, 135px);
mutePosition: point(140px, 135px);
}
callBodyWithPreview: CallBodyLayout {
height: 185px;
@@ -631,7 +631,6 @@ groupCallMenuAbout: FlatLabel(defaultFlatLabel) {
textFg: groupCallMemberNotJoinedStatus;
palette: groupCallTextPalette;
minWidth: 200px;
maxHeight: 92px;
}
callDeviceSelectionLabel: FlatLabel(defaultSubsectionTitle) {
textFg: groupCallActiveFg;
+4 -4
View File
@@ -1196,6 +1196,10 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
raw->setIncomingVideoOutput(_videoIncoming->sink());
raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled());
_muted.value() | rpl::on_next([=](bool muted) {
Core::App().mediaDevices().setCaptureMuted(muted);
}, _instanceLifetime);
_state.value() | rpl::on_next([=](State state) {
const auto track = (state != State::FailedHangingUp)
&& (state != State::Failed)
@@ -1207,10 +1211,6 @@ void Call::createAndStartController(const MTPDphoneCall &call) {
Core::App().mediaDevices().setCaptureMuteTracker(this, track);
}, _instanceLifetime);
_muted.value() | rpl::on_next([=](bool muted) {
Core::App().mediaDevices().setCaptureMuted(muted);
}, _instanceLifetime);
#if 0
Core::App().batterySaving().value(
) | rpl::on_next([=](bool isSaving) {
@@ -101,7 +101,6 @@ private:
rpl::variable<bool> _isGroupConnecting = false;
std::vector<not_null<PeerData*>> _conferenceFirstUsers;
int _conferenceUsersCount = 0;
QBrush _groupBrush;
anim::linear_gradients<BarState> _gradients;
@@ -66,13 +66,28 @@ AboutItem::AboutItem(
, _dummyAction(new QAction(parent)) {
setPointerCursor(false);
_text->setSelectable(true);
const auto added = st.itemPadding.left() + st.itemPadding.right();
sizeValue(
) | rpl::on_next([=](const QSize &s) {
if (s.width() <= added) {
return;
}
_text->resizeToWidth(s.width() - added);
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
}, lifetime());
_text->heightValue(
) | rpl::on_next([=] {
resize(width(), contentHeight());
}, lifetime());
_text->resizeToWidth(parent->width() - added);
fitToMenuWidth();
enableMouseSelecting();
enableMouseSelecting(_text.get());
_text->setSelectable(true);
_text->resizeToWidth(st::groupCallMenuAbout.minWidth);
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
}
not_null<QAction*> AboutItem::action() const {
@@ -1606,6 +1606,7 @@ void GroupCall::rejoin(not_null<PeerData*> as) {
_joinState.action = JoinAction::Joining;
_joinState.ssrc = 0;
_initialMuteStateSent = false;
_systemMuteReconciled = false;
setState(State::Joining);
if (!tryCreateController()) {
setInstanceMode(InstanceMode::None);
@@ -1794,6 +1795,13 @@ void GroupCall::joinDone(
: State::Joined);
applyMeInCallLocally();
maybeSendMutedUpdate(wasMuteState);
_systemMuteReconciled = true;
if (!_rtmp) {
const auto state = muted();
const auto nowMuted = (state != MuteState::Active)
&& (state != MuteState::PushToTalk);
Core::App().mediaDevices().setCaptureMuted(nowMuted);
}
for (auto &state : _subchains) {
// Accept initial join blocks.
@@ -2699,8 +2707,8 @@ void GroupCall::setupMediaDevices() {
const auto muted = (state != MuteState::Active)
&& (state != MuteState::PushToTalk);
const auto track = !muted || (state == MuteState::Muted);
devices->setCaptureMuteTracker(this, track);
devices->setCaptureMuted(muted);
devices->setCaptureMuteTracker(this, track);
}, _lifetime);
}
}
@@ -2712,8 +2720,20 @@ void GroupCall::captureMuteChanged(bool mute) {
|| oldState == MuteState::RaisedHand
|| oldState == MuteState::Muted)) {
return;
} else if (!mute
&& (oldState == MuteState::ForceMuted
|| oldState == MuteState::RaisedHand)) {
crl::on_main(this, [] {
Core::App().mediaDevices().setCaptureMuted(true);
});
return;
} else if (!mute && oldState != MuteState::Muted) {
return;
} else if (!mute && !_systemMuteReconciled) {
crl::on_main(this, [] {
Core::App().mediaDevices().setCaptureMuted(true);
});
return;
}
setMutedAndUpdate(mute ? MuteState::Muted : MuteState::Active);
}
@@ -727,6 +727,7 @@ private:
rpl::variable<bool> _emptyRtmp = false;
rpl::variable<bool> _messagesEnabled = false;
bool _initialMuteStateSent = false;
bool _systemMuteReconciled = false;
bool _acceptFields = false;
rpl::event_stream<Group::ParticipantState> _otherParticipantStateValue;
@@ -37,8 +37,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Calls::Group {
namespace {
constexpr auto kPasswordCharAmount = 24;
void StartWithBox(
not_null<Ui::GenericBox*> box,
Fn<void()> done,
@@ -425,6 +425,58 @@ stickersScroll: ScrollArea(boxScroll) {
stickersRowDisabledOpacity: 0.4;
stickersRowDuration: 200;
stickersAddCellPlusSize: 22px;
stickersAddCellPlusThickness: 2px;
stickersAddCellBgRadius: 28px;
stickersEmojiPickerExpandedRadius: 20px;
stickersEmojiPickerBg: emojiPanBg;
stickersEmojiPickerShadow: windowShadowFg;
stickersEmojiPickerPadding: margins(12px, 8px, 12px, 0px);
stickersEmojiPickerItemSize: 30px;
stickersEmojiPickerItemSkip: 4px;
stickersEmojiPickerStripHeight: 40px;
stickersEmojiPickerExpandedHeight: 220px;
stickersEmojiPickerStripBubble: icon{
{ "chat/reactions_bubble_shadow", windowShadowFg },
{ "chat/reactions_bubble", emojiPanBg },
};
stickersEmojiPickerStripBubbleRight: 20px;
stickersEmojiPickerSelectedBg: windowBgActive;
stickersEmojiPickerSelectedFg: windowBgActive;
stickersEmojiPickerHeaderFg: windowSubTextFg;
stickersEmojiPickerScroll: ScrollArea(boxScroll) {
width: 14px;
deltax: 5px;
deltat: 4px;
deltab: 18px;
}
stickersEmojiPickerAbout: FlatLabel(defaultFlatLabel) {
minWidth: 100px;
align: align(top);
textFg: windowSubTextFg;
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
}
stickersEmojiPickerSectionHeader: FlatLabel(defaultFlatLabel) {
minWidth: 10px;
align: align(topleft);
textFg: windowSubTextFg;
style: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
}
stickersEmojiPickerExpandIcon: icon {{ "intro_country_dropdown", windowSubTextFg }};
stickersEmojiPickerCollapseIcon: icon {{ "intro_country_dropdown-flip_vertical", windowSubTextFg }};
stickersEmojiPickerExpandSize: 24px;
stickersEmojiPickerExpandBg: windowBgRipple;
stickersEmojiPickerBoxShadow: BoxShadow {
blurRadius: 20px;
offset: point(0px, 6px);
opacity: 0.22;
}
emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }};
filtersRemove: IconButton(stickersRemove) {
@@ -610,7 +662,7 @@ sendBoxAlbumSmallGroupSize: size(30px, 25px);
sendBoxAlbumSmallGroupCircleSize: 27px;
sendBoxFileGroupSkipTop: 2px;
sendBoxFileGroupSkipRight: 5px;
sendBoxFileGroupSkipRight: 1px;
sendBoxFileGroupEditInternalSkip: -1px;
sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {
@@ -623,7 +675,7 @@ sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_cross", me
sendBoxAlbumButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg }};
sendBoxAlbumGroupButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg, point(4px, 1px) }};
sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_cross", roundedFg, point(-2px, 1px) }};
sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_cross", roundedFg }};
defaultComposeIcons: ComposeIcons {
settings: icon {{ "emoji/emoji_settings", emojiIconFg }};
@@ -1035,6 +1087,19 @@ historyPinnedShowAll: IconButton(historyReplyCancel) {
icon: icon {{ "pinned_show_all", historyReplyCancelFg }};
iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }};
}
sendFilesReplyIconPosition: point(11px, 7px);
sendFilesReplyCancelSize: 24px;
sendFilesReplyCancel: IconButton(editMediaButton) {
width: sendFilesReplyCancelSize;
height: sendFilesReplyCancelSize;
icon: icon {{ "send_media/send_media_cross", historyReplyCancelFg }};
iconOver: icon {{ "send_media/send_media_cross", historyReplyCancelFgOver }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgRipple;
}
rippleAreaSize: sendFilesReplyCancelSize;
}
historyPinnedBotButton: RoundButton(defaultActiveButton) {
width: -34px;
height: 30px;
@@ -1445,6 +1510,10 @@ historyAiComposeButtonLetters: icon{{ "chat/ai_letters-20x20", historyComposeIco
historyAiComposeButtonStar1: icon{{ "chat/ai_star1-20x20", historyComposeIconFg }};
historyAiComposeButtonStar2: icon{{ "chat/ai_star2-20x20", historyComposeIconFg }};
historyAiComposeButtonPosition: point(-8px, -4px);
historySendAsFileButton: IconButton(historyAiComposeButton) {
icon: icon {{ "chat/text_to_file-18x18", historyComposeIconFg }};
iconOver: icon {{ "chat/text_to_file-18x18", historyComposeIconFgOver }};
}
historyAiComposeTooltipSkip: 8px;
importantTooltipHide: IconButton(defaultIconButton) {
width: 34px;
@@ -37,6 +37,8 @@ ResolveWindow ResolveWindowDefault() {
return b;
} else if (const auto c = check(app.windowFor(account))) {
return c;
} else if (Core::Quitting()) {
return nullptr;
} else if (const auto d = check(app.ensureSeparateWindowFor(
account))) {
return d;
@@ -68,8 +68,10 @@ namespace {
constexpr auto kCollapsedRows = 3;
constexpr auto kAppearDuration = 0.3;
constexpr auto kCustomSearchLimit = 256;
constexpr auto kCloudSearchPageLimit = 50;
constexpr auto kColorPickerDelay = crl::time(500);
constexpr auto kSearchRequestDelay = 400;
constexpr auto kPreloadSearchPages = 4;
using Core::RecentEmojiId;
using Core::RecentEmojiDocument;
@@ -672,12 +674,13 @@ void EmojiListWidget::applyNextSearchQuery() {
}
const auto guard = gsl::finally([&] { finish(); });
auto plain = collectPlainSearchResults();
if (_searchEmoji == _searchEmojiPrevious) {
return;
}
_searchEmoticon = QString();
for (const auto emoji : plain) {
_searchEmoticon += emoji->text();
{
auto exactSet = base::flat_set<EmojiPtr>();
const auto exact = SearchEmoji(_searchQuery, exactSet, true);
for (const auto emoji : exact) {
_searchEmoticon += emoji->text();
}
}
_searchResults.clear();
_searchCustomIds.clear();
@@ -692,9 +695,6 @@ void EmojiListWidget::applyNextSearchQuery() {
if (_mode != Mode::Full || session().premium()) {
appendPremiumSearchResults();
}
if (_mode == Mode::Full) {
appendLocalPackSearchResults();
}
_searchQueryText = ranges::accumulate(
_searchQuery,
@@ -711,13 +711,13 @@ void EmojiListWidget::applyNextSearchQuery() {
_api.request(requestId).cancel();
}
_searchNextRequestQuery = _searchQueryText;
const auto cloudCached = _searchCloudCache.find(_searchQueryText)
_searchRequestQuery = _searchQueryText;
const auto cloudCached = _searchCloudCache.find(_searchRequestQuery)
!= _searchCloudCache.cend();
const auto setsCached = _searchSetsCache.find(_searchQueryText)
const auto setsCached = _searchSetsCache.find(_searchRequestQuery)
!= _searchSetsCache.cend();
if (cloudCached || setsCached) {
_searchRequestTimer.cancel();
_searchRequestQuery = _searchQueryText;
fillCloudSearchResults();
fillCloudSearchSets();
if (!cloudCached || !setsCached) {
@@ -824,62 +824,6 @@ void EmojiListWidget::appendPremiumSearchResults() {
}
}
void EmojiListWidget::appendLocalPackSearchResults() {
const auto text = _searchQueryText.toLower();
if (text.isEmpty()) {
return;
}
const auto test = session().isTestMode();
const auto &sets = session().data().stickers().sets();
const auto processSet = [&](uint64 setId) {
const auto it = sets.find(setId);
if (it == sets.end()) {
return;
}
const auto set = it->second.get();
if (!(set->flags & Data::StickersSetFlag::Emoji)) {
return;
}
const auto title = set->title.toLower();
if (!title.startsWith(text)
&& !title.contains(u' ' + text)) {
return;
}
const auto &list = set->stickers.empty()
? set->covers
: set->stickers;
for (const auto document : list) {
if (_searchResults.size() >= kCustomSearchLimit) {
return;
}
const auto sticker = document->sticker();
if (!sticker) {
continue;
}
const auto id = document->id;
if (!_searchCustomIds.emplace(id).second) {
continue;
}
const auto statusId = EmojiStatusId{ id };
_searchResults.push_back({
.custom = resolveCustomEmoji(
statusId,
document,
SearchEmojiSectionSetId()),
.id = { RecentEmojiDocument{ .id = id, .test = test } },
});
}
};
for (const auto setId
: session().data().stickers().emojiSetsOrder()) {
processSet(setId);
}
for (const auto setId
: session().data().stickers().featuredEmojiSetsOrder()) {
processSet(setId);
}
}
void EmojiListWidget::toggleSearchLoading(bool loading) {
if (_search) {
_search->setLoading(loading);
@@ -891,74 +835,76 @@ void EmojiListWidget::toggleSearchLoading(bool loading) {
}
void EmojiListWidget::sendSearchRequest() {
_searchRequestQuery = _searchQueryText;
_searchRequestQuery = _searchNextRequestQuery;
if (_searchRequestQuery.isEmpty()) {
return;
}
const auto query = _searchRequestQuery;
const auto cloudCached = _searchCloudCache.find(
_searchRequestQuery) != _searchCloudCache.cend();
query) != _searchCloudCache.cend();
const auto setsCached = _searchSetsCache.find(
_searchRequestQuery) != _searchSetsCache.cend();
query) != _searchSetsCache.cend();
if (cloudCached && setsCached) {
toggleSearchLoading(false);
return;
}
toggleSearchLoading(true);
const auto hash = uint64(0);
if (!cloudCached) {
auto langCodes = QVector<MTPstring>();
const auto method = QGuiApplication::inputMethod();
if (method) {
for (const auto &lang : method->locale().uiLanguages()) {
langCodes.push_back(MTP_string(lang));
}
}
using Flag = MTPmessages_SearchStickers::Flag;
_searchCloudRequestId = _api.request(MTPmessages_SearchStickers(
MTP_flags(Flag::f_emojis),
MTP_string(_searchRequestQuery),
MTP_string(_searchEmoticon),
MTP_vector<MTPstring>(langCodes),
MTP_int(0),
MTP_int(50),
MTP_long(hash)
)).done([=](const MTPmessages_FoundStickers &result) {
searchCloudResultsDone(result);
}).fail([=] {
_searchCloudRequestId = 0;
_searchCloudCache.emplace(
_searchRequestQuery,
std::vector<DocumentId>());
if (!_searchSetsRequestId) {
toggleSearchLoading(false);
showSearchResults();
}
}).handleAllErrors().send();
requestSearchCloud(query, 0, true);
}
if (!setsCached) {
sendSearchSetsRequest();
sendSearchSetsRequest(query);
}
}
void EmojiListWidget::sendSearchSetsRequest() {
void EmojiListWidget::sendSearchSetsRequest(const QString &query) {
const auto hash = uint64(0);
_searchSetsRequestId = _api.request(
MTPmessages_SearchEmojiStickerSets(
MTP_flags(0),
MTP_string(_searchRequestQuery),
MTP_string(query),
MTP_long(hash))
).done([=](const MTPmessages_FoundStickerSets &result) {
searchSetsResultsDone(result);
searchSetsResultsDone(query, result);
}).fail([=] {
_searchSetsRequestId = 0;
if (!_searchCloudRequestId) {
if ((_searchRequestQuery == query) && !_searchCloudRequestId) {
toggleSearchLoading(false);
}
}).handleAllErrors().send();
}
void EmojiListWidget::requestSearchCloud(
const QString &query,
int offset,
bool fallbackToEmpty) {
using Flag = MTPmessages_SearchStickers::Flag;
const auto hash = uint64(0);
_searchCloudRequestId = _api.request(MTPmessages_SearchStickers(
MTP_flags(Flag::f_emojis),
MTP_string(query),
MTP_string(_searchEmoticon),
MTP_vector<MTPstring>(SearchStickersLangCodes()),
MTP_int(offset),
MTP_int(kCloudSearchPageLimit),
MTP_long(hash)
)).done([=](const MTPmessages_FoundStickers &result) {
searchCloudResultsDone(query, offset, result);
}).fail([=] {
_searchCloudRequestId = 0;
if (!fallbackToEmpty) {
return;
}
_searchCloudCache.emplace(query, std::vector<DocumentId>());
if ((_searchRequestQuery == query) && !_searchSetsRequestId) {
toggleSearchLoading(false);
showSearchResults();
}
}).handleAllErrors().send();
}
void EmojiListWidget::cancelSearchRequest() {
toggleSearchLoading(false);
if (const auto requestId = base::take(_searchCloudRequestId)) {
@@ -971,21 +917,48 @@ void EmojiListWidget::cancelSearchRequest() {
_searchRequestQuery = QString();
_searchNextRequestQuery = QString();
_searchCloudCache.clear();
_searchCloudNextOffset.clear();
_searchSetsCache.clear();
_searchSets.clear();
}
void EmojiListWidget::searchCloudResultsDone(
const QString &query,
int requestedOffset,
const MTPmessages_FoundStickers &result) {
_searchCloudRequestId = 0;
const auto active = (_searchRequestQuery == query);
result.match([&](const MTPDmessages_foundStickersNotModified &) {
result.match([&](const MTPDmessages_foundStickersNotModified &data) {
LOG(("API: messages.foundStickersNotModified."));
}, [&](const MTPDmessages_foundStickers &data) {
auto it = _searchCloudCache.find(_searchRequestQuery);
auto it = _searchCloudCache.find(query);
if (it == _searchCloudCache.cend()) {
it = _searchCloudCache.emplace(
_searchRequestQuery,
query,
std::vector<DocumentId>()).first;
}
if (const auto next = data.vnext_offset()) {
if (next->v > requestedOffset) {
_searchCloudNextOffset[query] = next->v;
} else {
_searchCloudNextOffset.erase(query);
}
} else {
_searchCloudNextOffset.erase(query);
}
if (!active) {
return;
}
if (!_searchSetsRequestId) {
toggleSearchLoading(false);
}
showSearchResults();
checkPaginateSearchCloud(getVisibleTop(), getVisibleBottom());
}, [&](const MTPDmessages_foundStickers &data) {
auto it = _searchCloudCache.find(query);
if (it == _searchCloudCache.cend()) {
it = _searchCloudCache.emplace(
query,
std::vector<DocumentId>()).first;
}
@@ -996,17 +969,67 @@ void EmojiListWidget::searchCloudResultsDone(
}
}
if (const auto next = data.vnext_offset()) {
if (next->v > requestedOffset) {
_searchCloudNextOffset[query] = next->v;
} else {
_searchCloudNextOffset.erase(query);
}
} else {
_searchCloudNextOffset.erase(query);
}
if (!active) {
return;
}
if (!_searchSetsRequestId) {
toggleSearchLoading(false);
}
showSearchResults();
checkPaginateSearchCloud(
getVisibleTop(),
getVisibleBottom());
});
}
void EmojiListWidget::loadMoreSearchCloud() {
if (_searchCloudRequestId
|| _searchRequestQuery.isEmpty()
|| (_searchRequestQuery != _searchNextRequestQuery)) {
return;
}
const auto query = _searchRequestQuery;
const auto offsetIt = _searchCloudNextOffset.find(query);
if (offsetIt == _searchCloudNextOffset.end()) {
return;
}
requestSearchCloud(query, offsetIt->second, false);
}
void EmojiListWidget::checkPaginateSearchCloud(
int visibleTop,
int visibleBottom) {
if (!_searchMode
|| _searchRequestQuery.isEmpty()
|| (_searchRequestQuery != _searchNextRequestQuery)
|| _searchCloudRequestId) {
return;
}
const auto visibleHeight = visibleBottom - visibleTop;
if (visibleHeight <= 0) {
return;
}
if (visibleBottom > height() - visibleHeight * kPreloadSearchPages) {
loadMoreSearchCloud();
}
}
void EmojiListWidget::searchSetsResultsDone(
const QString &query,
const MTPmessages_FoundStickerSets &result) {
_searchSetsRequestId = 0;
if (!_searchCloudRequestId) {
if ((_searchRequestQuery == query) && !_searchCloudRequestId) {
toggleSearchLoading(false);
}
@@ -1014,10 +1037,10 @@ void EmojiListWidget::searchSetsResultsDone(
LOG(("API Error: "
"messages.foundStickerSetsNotModified not expected."));
}, [&](const MTPDmessages_foundStickerSets &data) {
auto it = _searchSetsCache.find(_searchRequestQuery);
auto it = _searchSetsCache.find(query);
if (it == _searchSetsCache.cend()) {
it = _searchSetsCache.emplace(
_searchRequestQuery,
query,
std::vector<uint64>()).first;
}
for (const auto &setData : data.vsets().v) {
@@ -1028,7 +1051,9 @@ void EmojiListWidget::searchSetsResultsDone(
}
it->second.push_back(set->id);
}
showSearchResults();
if (_searchRequestQuery == query) {
showSearchResults();
}
});
}
@@ -1052,9 +1077,6 @@ void EmojiListWidget::showSearchResults() {
appendPremiumSearchResults();
}
fillCloudSearchResults();
if (_mode == Mode::Full) {
appendLocalPackSearchResults();
}
fillCloudSearchSets();
resizeToWidth(width());
@@ -1070,9 +1092,6 @@ void EmojiListWidget::fillCloudSearchResults() {
}
const auto test = session().isTestMode();
for (const auto id : it->second) {
if (_searchResults.size() >= kCustomSearchLimit) {
break;
}
if (!_searchCustomIds.emplace(id).second) {
continue;
}
@@ -1268,6 +1287,7 @@ void EmojiListWidget::visibleTopBottomUpdated(
ValidateIconAnimations::Full);
}
unloadNotSeenCustom(visibleTop, visibleBottom);
checkPaginateSearchCloud(visibleTop, visibleBottom);
}
void EmojiListWidget::unloadNotSeenCustom(
@@ -1831,6 +1851,9 @@ void EmojiListWidget::paint(
&& !_searchRequestTimer.isActive()) {
paintEmptySearchResults(p);
}
const auto badgeText = tr::lng_stickers_creator_badge(tr::now);
const auto &badgeFont = st::stickersHeaderBadgeFont;
const auto badgeWidth = badgeFont->width(badgeText);
enumerateSections([&](const SectionInfo &info) {
if (clip.top() >= info.rowsBottom) {
return true;
@@ -1843,7 +1866,7 @@ void EmojiListWidget::paint(
const auto titleLeft = (info.premiumRequired
? st().headerLockedLeft
: st().headerLeft) - st().margin.left();
const auto widthForTitle = emojiRight()
auto widthForTitle = emojiRight()
- titleLeft
- paintButtonGetWidth(p, info, buttonSelected, clip);
if (info.section > 0 && clip.top() < info.rowsTop) {
@@ -1854,6 +1877,18 @@ void EmojiListWidget::paint(
: (info.section < _staticCount)
? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now)
: _custom[info.section - _staticCount].title;
const auto titleSet = (_searchMode && info.section > 0)
? searchSetBySection(info.section).set.get()
: (info.section >= _staticCount)
? _custom[info.section - _staticCount].set.get()
: nullptr;
const auto amCreator = titleSet
&& (titleSet->flags & Data::StickersSetFlag::AmCreator);
if (amCreator) {
widthForTitle -= badgeWidth
+ st::stickersFeaturedUnreadSkip
+ st::stickersHeaderBadgeFontSkip;
}
auto titleWidth = st::emojiPanHeaderFont->width(titleText);
if (titleWidth > widthForTitle) {
titleText = st::emojiPanHeaderFont->elided(titleText, widthForTitle);
@@ -1871,6 +1906,38 @@ void EmojiListWidget::paint(
p.setFont(st::emojiPanHeaderFont);
p.setPen(st().headerFg);
p.drawText(titleLeft, textBaseline, titleText);
if (amCreator) {
const auto badgeLeft = titleLeft
+ titleWidth
+ st::stickersFeaturedUnreadSkip;
{
auto color = st().headerFg->c;
color.setAlphaF(st().headerFg->c.alphaF() * 0.15);
p.setPen(Qt::NoPen);
p.setBrush(color);
auto hq = PainterHighQualityEnabler(p);
p.drawRoundedRect(
style::rtlrect(
badgeLeft,
info.top + st::stickersHeaderBadgeFontTop,
badgeWidth + badgeFont->height,
badgeFont->height,
width()),
badgeFont->height / 2.,
badgeFont->height / 2.);
}
p.setPen(st().headerFg);
p.setBrush(Qt::NoBrush);
p.setFont(badgeFont);
p.drawText(
QRect(
badgeLeft + badgeFont->height / 2,
info.top + st::stickersHeaderBadgeFontTop,
badgeWidth,
badgeFont->height),
badgeText,
style::al_center);
}
}
if (clip.top() + clip.height() > info.rowsTop) {
ensureLoaded(info.section);
@@ -2873,6 +2940,7 @@ void EmojiListWidget::refreshCustom() {
return;
} else if (valid) {
i->thumbnailDocument = it->second->lookupThumbnailDocument();
i->title = it->second->title;
const auto premiumRequired = premium && premiumMayBeBought;
if (i->canRemove != canRemove
|| i->premiumRequired != premiumRequired) {
@@ -299,13 +299,23 @@ private:
void setupSearch();
[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();
void appendPremiumSearchResults();
void appendLocalPackSearchResults();
void sendSearchRequest();
void sendSearchSetsRequest();
void sendSearchSetsRequest(const QString &query);
void requestSearchCloud(
const QString &query,
int offset,
bool fallbackToEmpty);
void cancelSearchRequest();
void toggleSearchLoading(bool loading);
void searchCloudResultsDone(const MTPmessages_FoundStickers &result);
void searchSetsResultsDone(const MTPmessages_FoundStickerSets &result);
void searchCloudResultsDone(
const QString &query,
int requestedOffset,
const MTPmessages_FoundStickers &result);
void loadMoreSearchCloud();
void checkPaginateSearchCloud(int visibleTop, int visibleBottom);
void searchSetsResultsDone(
const QString &query,
const MTPmessages_FoundStickerSets &result);
void showSearchResults();
void fillCloudSearchResults();
void fillCloudSearchSets();
@@ -484,6 +494,7 @@ private:
std::vector<RecentOne> _searchResults;
bool _searchMode = false;
std::map<QString, std::vector<DocumentId>> _searchCloudCache;
std::map<QString, int> _searchCloudNextOffset;
std::map<QString, std::vector<uint64>> _searchSetsCache;
std::vector<CustomSet> _searchSets;
QString _searchRequestQuery;
@@ -0,0 +1,692 @@
/*
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 "chat_helpers/emoji_picker_overlay.h"
#include "ui/abstract_button.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/scroll_area.h"
#include "styles/style_chat_helpers.h"
#include <QtCore/QEvent>
#include <QtGui/QMouseEvent>
#include <QtGui/QPainter>
namespace ChatHelpers {
namespace {
[[nodiscard]] std::vector<EmojiPtr> BuildAllEmojis() {
using Section = Ui::Emoji::Section;
auto result = std::vector<EmojiPtr>();
for (auto i = int(Section::People); i <= int(Section::Symbols); ++i) {
const auto section = Ui::Emoji::GetSection(Section(i));
result.reserve(result.size() + section.size());
for (const auto emoji : section) {
result.push_back(emoji);
}
}
return result;
}
[[nodiscard]] std::vector<EmojiPtr> DefaultRecentVector() {
const auto src = Ui::Emoji::GetDefaultRecent();
return std::vector<EmojiPtr>(src.begin(), src.end());
}
} // namespace
class EmojiPickerOverlay::Strip final : public Ui::RpWidget {
public:
Strip(QWidget *parent, not_null<EmojiPickerOverlay*> owner);
void refresh();
protected:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
private:
struct Cell {
EmojiPtr emoji = nullptr;
QRect rect;
};
[[nodiscard]] int cellAtPoint(QPoint p) const;
void updateHover(int index);
const not_null<EmojiPickerOverlay*> _owner;
std::vector<Cell> _cells;
int _hover = -1;
int _pressed = -1;
};
class EmojiPickerOverlay::Grid final : public Ui::RpWidget {
public:
Grid(QWidget *parent, not_null<EmojiPickerOverlay*> owner);
void setEmojis(std::vector<EmojiPtr> emojis);
int resizeGetHeight(int newWidth) override;
void refresh();
protected:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
private:
struct Cell {
EmojiPtr emoji = nullptr;
QRect rect;
};
[[nodiscard]] int cellAtPoint(QPoint p) const;
void relayoutCells();
void updateHover(int index);
const not_null<EmojiPickerOverlay*> _owner;
std::vector<EmojiPtr> _emojis;
std::vector<Cell> _cells;
int _columns = 0;
int _hover = -1;
int _pressed = -1;
};
namespace {
void DrawEmojiCell(
QPainter &p,
const QRect &cell,
EmojiPtr emoji,
bool selected,
bool hovered) {
if (selected) {
p.setPen(Qt::NoPen);
p.setBrush(st::stickersEmojiPickerSelectedBg);
p.drawEllipse(cell);
} else if (hovered) {
p.setPen(Qt::NoPen);
p.setBrush(anim::with_alpha(st::windowSubTextFg->c, 0.12));
p.drawEllipse(cell);
}
if (!emoji) {
return;
}
const auto esize = Ui::Emoji::GetSizeLarge();
const auto dpr = style::DevicePixelRatio();
const auto pixelSize = esize / dpr;
const auto drawSize = std::min(
pixelSize,
st::stickersEmojiPickerItemSize - 4);
const auto x = cell.x() + (cell.width() - drawSize) / 2;
const auto y = cell.y() + (cell.height() - drawSize) / 2;
if (drawSize == pixelSize) {
Ui::Emoji::Draw(p, emoji, esize, x, y);
} else {
const auto target = QRect(x, y, drawSize, drawSize);
auto buffer = QImage(
QSize(pixelSize, pixelSize) * dpr,
QImage::Format_ARGB32_Premultiplied);
buffer.fill(Qt::transparent);
buffer.setDevicePixelRatio(dpr);
{
auto q = QPainter(&buffer);
Ui::Emoji::Draw(q, emoji, esize, 0, 0);
}
p.drawImage(target, buffer);
}
}
} // namespace
EmojiPickerOverlay::Strip::Strip(
QWidget *parent,
not_null<EmojiPickerOverlay*> owner)
: RpWidget(parent)
, _owner(owner) {
setMouseTracking(true);
}
void EmojiPickerOverlay::Strip::refresh() {
const auto &sel = _owner->_selectedList;
const auto &recent = _owner->_recent;
const auto item = st::stickersEmojiPickerItemSize;
const auto skip = st::stickersEmojiPickerItemSkip;
const auto w = width();
if (w <= 0 || item <= 0) {
_cells.clear();
update();
return;
}
const auto capacity = std::max(1, (w + skip) / (item + skip));
auto order = std::vector<EmojiPtr>();
order.reserve(sel.size() + recent.size());
for (const auto emoji : sel) {
order.push_back(emoji);
}
for (const auto emoji : recent) {
const auto already = std::find(sel.begin(), sel.end(), emoji)
!= sel.end();
if (!already) {
order.push_back(emoji);
}
}
if (int(order.size()) > capacity) {
order.resize(capacity);
}
_cells.clear();
_cells.reserve(order.size());
auto x = 0;
const auto y = (height() - item) / 2;
for (const auto emoji : order) {
_cells.push_back({ emoji, QRect(x, y, item, item) });
x += item + skip;
}
_hover = -1;
_pressed = -1;
update();
}
int EmojiPickerOverlay::Strip::cellAtPoint(QPoint p) const {
for (auto i = 0; i != int(_cells.size()); ++i) {
if (_cells[i].rect.contains(p)) {
return i;
}
}
return -1;
}
void EmojiPickerOverlay::Strip::updateHover(int index) {
if (_hover == index) {
return;
}
_hover = index;
update();
}
void EmojiPickerOverlay::Strip::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
for (auto i = 0; i != int(_cells.size()); ++i) {
const auto &cell = _cells[i];
const auto selected = _owner->_selectedList.end()
!= std::find(
_owner->_selectedList.begin(),
_owner->_selectedList.end(),
cell.emoji);
DrawEmojiCell(p, cell.rect, cell.emoji, selected, i == _hover);
}
}
void EmojiPickerOverlay::Strip::mouseMoveEvent(QMouseEvent *e) {
updateHover(cellAtPoint(e->pos()));
}
void EmojiPickerOverlay::Strip::mousePressEvent(QMouseEvent *e) {
_pressed = cellAtPoint(e->pos());
}
void EmojiPickerOverlay::Strip::mouseReleaseEvent(QMouseEvent *e) {
const auto released = cellAtPoint(e->pos());
const auto index = _pressed;
_pressed = -1;
if (released == index && index >= 0 && index < int(_cells.size())) {
_owner->toggleEmoji(_cells[index].emoji, false);
}
}
void EmojiPickerOverlay::Strip::leaveEventHook(QEvent *e) {
updateHover(-1);
}
EmojiPickerOverlay::Grid::Grid(
QWidget *parent,
not_null<EmojiPickerOverlay*> owner)
: RpWidget(parent)
, _owner(owner) {
setMouseTracking(true);
}
void EmojiPickerOverlay::Grid::setEmojis(std::vector<EmojiPtr> emojis) {
_emojis = std::move(emojis);
relayoutCells();
}
int EmojiPickerOverlay::Grid::resizeGetHeight(int newWidth) {
resize(newWidth, 0);
relayoutCells();
return height();
}
void EmojiPickerOverlay::Grid::refresh() {
update();
}
void EmojiPickerOverlay::Grid::relayoutCells() {
const auto item = st::stickersEmojiPickerItemSize;
const auto skip = st::stickersEmojiPickerItemSkip;
const auto w = width();
_columns = std::max(1, (w + skip) / (item + skip));
_cells.clear();
_cells.reserve(_emojis.size());
auto col = 0;
auto row = 0;
for (const auto emoji : _emojis) {
const auto x = col * (item + skip);
const auto y = row * (item + skip);
_cells.push_back({ emoji, QRect(x, y, item, item) });
if (++col >= _columns) {
col = 0;
++row;
}
}
const auto fullRows = row + (col > 0 ? 1 : 0);
const auto h = fullRows > 0
? (fullRows * item + (fullRows - 1) * skip)
: 0;
resize(w, h);
_hover = -1;
_pressed = -1;
update();
}
int EmojiPickerOverlay::Grid::cellAtPoint(QPoint p) const {
for (auto i = 0; i != int(_cells.size()); ++i) {
if (_cells[i].rect.contains(p)) {
return i;
}
}
return -1;
}
void EmojiPickerOverlay::Grid::updateHover(int index) {
if (_hover == index) {
return;
}
_hover = index;
update();
}
void EmojiPickerOverlay::Grid::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto clip = e->rect();
for (auto i = 0; i != int(_cells.size()); ++i) {
const auto &cell = _cells[i];
if (!cell.rect.intersects(clip)) {
continue;
}
const auto selected = _owner->_selectedList.end()
!= std::find(
_owner->_selectedList.begin(),
_owner->_selectedList.end(),
cell.emoji);
DrawEmojiCell(p, cell.rect, cell.emoji, selected, i == _hover);
}
}
void EmojiPickerOverlay::Grid::mouseMoveEvent(QMouseEvent *e) {
updateHover(cellAtPoint(e->pos()));
}
void EmojiPickerOverlay::Grid::mousePressEvent(QMouseEvent *e) {
_pressed = cellAtPoint(e->pos());
}
void EmojiPickerOverlay::Grid::mouseReleaseEvent(QMouseEvent *e) {
const auto released = cellAtPoint(e->pos());
const auto index = _pressed;
_pressed = -1;
if (released == index && index >= 0 && index < int(_cells.size())) {
_owner->toggleEmoji(_cells[index].emoji, true);
}
}
void EmojiPickerOverlay::Grid::leaveEventHook(QEvent *e) {
updateHover(-1);
}
EmojiPickerOverlay::Metrics EmojiPickerOverlay::EstimateMetrics(
const QString &aboutText) {
const auto tailHeight = st::stickersEmojiPickerStripBubble.height();
const auto shadowExtent = Ui::BoxShadow::ExtendFor(
st::stickersEmojiPickerBoxShadow);
const auto &pad = st::stickersEmojiPickerPadding;
auto about = Ui::FlatLabel(
nullptr,
aboutText,
st::stickersEmojiPickerAbout);
const auto collapsedHeight = pad.top()
+ about.height()
+ st::stickersEmojiPickerStripHeight
+ pad.bottom();
const auto expandedHeight = collapsedHeight
+ st::stickersEmojiPickerExpandedHeight;
const auto shadowAndTail = shadowExtent.top()
+ shadowExtent.bottom()
+ tailHeight;
return {
.shadowExtent = shadowExtent,
.tailHeight = tailHeight,
.collapsedHeight = collapsedHeight,
.expandedHeight = expandedHeight,
.totalCollapsedHeight = collapsedHeight + shadowAndTail,
.totalExpandedHeight = expandedHeight + shadowAndTail,
};
}
EmojiPickerOverlay::EmojiPickerOverlay(
QWidget *parent,
EmojiPickerOverlayDescriptor descriptor)
: RpWidget(parent)
, _aboutText(std::move(descriptor.aboutText))
, _recent(descriptor.recent.empty()
? DefaultRecentVector()
: std::move(descriptor.recent))
, _maxSelected(descriptor.maxSelected)
, _allowExpand(descriptor.allowExpand)
, _selectedList(std::move(descriptor.initialSelected))
, _shadow(st::stickersEmojiPickerBoxShadow) {
_allForGrid = BuildAllEmojis();
_about = std::make_unique<Ui::FlatLabel>(
this,
_aboutText,
st::stickersEmojiPickerAbout);
_strip = Ui::CreateChild<Strip>(this, this);
if (_allowExpand) {
_expandButton = Ui::CreateChild<Ui::AbstractButton>(this);
_expandButton->resize(
st::stickersEmojiPickerExpandSize,
st::stickersEmojiPickerExpandSize);
_expandButton->setClickedCallback([=] {
setExpanded(!_expanded.current());
});
_expandButton->paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = QPainter(_expandButton);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::stickersEmojiPickerExpandBg);
p.drawEllipse(_expandButton->rect());
const auto &icon = _expanded.current()
? st::stickersEmojiPickerCollapseIcon
: st::stickersEmojiPickerExpandIcon;
const auto x = (_expandButton->width() - icon.width()) / 2;
const auto y = (_expandButton->height() - icon.height()) / 2;
icon.paint(p, x, y, _expandButton->width());
}, _expandButton->lifetime());
_scroll = std::make_unique<Ui::ScrollArea>(
this,
st::stickersEmojiPickerScroll);
_scroll->setFrameStyle(QFrame::NoFrame);
_scroll->hide();
const auto gridPtr = _scroll->setOwnedWidget(
object_ptr<Grid>(_scroll.get(), this));
_grid = gridPtr.data();
_grid->setEmojis(_allForGrid);
}
_selectedVar = _selectedList;
resize(width(), totalExpandedHeight());
}
QMargins EmojiPickerOverlay::shadowExtent() const {
return _shadow.extend();
}
int EmojiPickerOverlay::totalCollapsedHeight() const {
const auto ext = _shadow.extend();
return collapsedHeight() + ext.top() + ext.bottom() + tailHeight();
}
int EmojiPickerOverlay::totalExpandedHeight() const {
const auto ext = _shadow.extend();
return expandedHeight() + ext.top() + ext.bottom() + tailHeight();
}
QRect EmojiPickerOverlay::bubbleRect() const {
const auto ext = _shadow.extend();
return QRect(
ext.left(),
ext.top(),
width() - ext.left() - ext.right(),
height() - ext.top() - ext.bottom() - tailHeight());
}
QRect EmojiPickerOverlay::bubbleShownRect() const {
const auto r = bubbleRect();
return QRect(r.x(), r.y(), r.width(), currentShownHeight());
}
EmojiPickerOverlay::~EmojiPickerOverlay() = default;
const std::vector<EmojiPtr> &EmojiPickerOverlay::selected() const {
return _selectedList;
}
rpl::producer<std::vector<EmojiPtr>>
EmojiPickerOverlay::selectedValue() const {
return _selectedVar.value();
}
void EmojiPickerOverlay::setExpanded(bool expanded) {
if (!_allowExpand || _expanded.current() == expanded) {
return;
}
startExpandAnimation(expanded);
_expanded = expanded;
if (_expandButton) {
_expandButton->update();
}
}
void EmojiPickerOverlay::startExpandAnimation(bool expanded) {
const auto from = _expandAnim.value(expanded ? 0. : 1.);
const auto to = expanded ? 1. : 0.;
_expandAnim.start(
[=] { applyExpandProgress(); },
from,
to,
st::slideWrapDuration,
anim::easeOutCirc);
applyExpandProgress();
}
float64 EmojiPickerOverlay::currentExpandValue() const {
return _expandAnim.value(_expanded.current() ? 1. : 0.);
}
int EmojiPickerOverlay::currentShownHeight() const {
const auto progress = currentExpandValue();
return anim::interpolate(
collapsedHeight(),
expandedHeight(),
progress);
}
void EmojiPickerOverlay::applyExpandProgress() {
if (_scroll) {
const auto progress = currentExpandValue();
_scroll->setVisible(progress > 0.);
}
relayout();
update();
}
bool EmojiPickerOverlay::expanded() const {
return _expanded.current();
}
rpl::producer<bool> EmojiPickerOverlay::expandedValue() const {
return _expanded.value();
}
int EmojiPickerOverlay::collapsedHeight() const {
const auto &pad = st::stickersEmojiPickerPadding;
const auto aboutH = _about ? _about->height() : 0;
return pad.top()
+ aboutH
+ st::stickersEmojiPickerStripHeight
+ pad.bottom();
}
int EmojiPickerOverlay::expandedHeight() const {
return collapsedHeight() + st::stickersEmojiPickerExpandedHeight;
}
void EmojiPickerOverlay::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto progress = currentExpandValue();
const auto shown = bubbleShownRect();
const auto radius = st::stickersEmojiPickerExpandedRadius;
_shadow.paint(p, shown, radius);
p.setPen(Qt::NoPen);
p.setBrush(st::stickersEmojiPickerBg);
p.drawRoundedRect(shown, radius, radius);
if (progress < 1.) {
paintTailBubble(p, shown, 1. - progress);
}
}
void EmojiPickerOverlay::paintTailBubble(
QPainter &p,
const QRect &bubble,
float64 opacity) {
const auto &icon = st::stickersEmojiPickerStripBubble;
const auto offsetRight = st::stickersEmojiPickerStripBubbleRight;
const auto x = bubble.right() + 1 - offsetRight - icon.width();
const auto y = bubble.bottom() + 1;
if (opacity >= 1.) {
icon.paint(p, x, y, width());
} else {
p.save();
p.setOpacity(opacity);
icon.paint(p, x, y, width());
p.restore();
}
}
void EmojiPickerOverlay::resizeEvent(QResizeEvent *e) {
relayout();
}
void EmojiPickerOverlay::mousePressEvent(QMouseEvent *e) {
if (!bubbleShownRect().contains(e->pos())) {
e->ignore();
}
}
int EmojiPickerOverlay::tailHeight() const {
return st::stickersEmojiPickerStripBubble.height();
}
void EmojiPickerOverlay::relayout() {
const auto &pad = st::stickersEmojiPickerPadding;
const auto bubble = bubbleRect();
const auto bubbleShown = currentShownHeight();
if (_about) {
_about->resizeToWidth(bubble.width() - pad.left() - pad.right());
_about->moveToLeft(bubble.left() + pad.left(), bubble.top() + pad.top());
}
const auto aboutBottom = _about
? (_about->y() + _about->height())
: (bubble.top() + pad.top());
const auto stripTop = aboutBottom;
const auto stripH = st::stickersEmojiPickerStripHeight;
const auto expandSize = _expandButton
? _expandButton->width()
: 0;
const auto expandGap = _expandButton
? st::stickersEmojiPickerItemSkip
: 0;
const auto stripW = bubble.width()
- pad.left()
- pad.right()
- expandSize
- expandGap;
_strip->setGeometry(bubble.left() + pad.left(), stripTop, stripW, stripH);
_strip->refresh();
if (_expandButton) {
const auto bx = bubble.right() + 1 - pad.right() - expandSize;
const auto by = stripTop + (stripH - expandSize) / 2;
_expandButton->moveToLeft(bx, by);
}
if (_scroll) {
const auto scrollTop = stripTop + stripH;
const auto bubbleBottom = bubble.top() + bubbleShown;
const auto scrollH = std::max(
0,
bubbleBottom - scrollTop - pad.bottom());
const auto scrollContentWidth = bubble.width()
- pad.left()
- pad.right();
const auto scrollAreaWidth = scrollContentWidth
+ pad.right();
_scroll->setGeometry(
bubble.left() + pad.left(),
scrollTop,
scrollAreaWidth,
scrollH);
if (_grid) {
_grid->resizeGetHeight(scrollContentWidth);
}
}
}
void EmojiPickerOverlay::toggleEmoji(EmojiPtr emoji, bool fromGrid) {
if (!emoji) {
return;
}
const auto it = std::find(
_selectedList.begin(),
_selectedList.end(),
emoji);
if (it != _selectedList.end()) {
_selectedList.erase(it);
} else {
if (_maxSelected > 0 && int(_selectedList.size()) >= _maxSelected) {
return;
}
_selectedList.push_back(emoji);
}
notifySelectionChanged();
if (fromGrid) {
setExpanded(false);
}
}
void EmojiPickerOverlay::notifySelectionChanged() {
_selectedVar = _selectedList;
if (_strip) {
_strip->refresh();
}
if (_grid) {
_grid->refresh();
}
}
} // namespace ChatHelpers
@@ -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
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "ui/emoji_config.h"
#include "ui/widgets/shadow.h"
namespace Ui {
class AbstractButton;
class FlatLabel;
class ScrollArea;
} // namespace Ui
namespace ChatHelpers {
struct EmojiPickerOverlayDescriptor {
QString aboutText;
std::vector<EmojiPtr> recent;
int maxSelected = 0;
bool allowExpand = true;
std::vector<EmojiPtr> initialSelected;
};
class EmojiPickerOverlay final : public Ui::RpWidget {
public:
EmojiPickerOverlay(
QWidget *parent,
EmojiPickerOverlayDescriptor descriptor);
~EmojiPickerOverlay();
struct Metrics {
QMargins shadowExtent;
int tailHeight = 0;
int collapsedHeight = 0;
int expandedHeight = 0;
int totalCollapsedHeight = 0;
int totalExpandedHeight = 0;
};
[[nodiscard]] static Metrics EstimateMetrics(const QString &aboutText);
[[nodiscard]] const std::vector<EmojiPtr> &selected() const;
[[nodiscard]] rpl::producer<std::vector<EmojiPtr>> selectedValue() const;
void setExpanded(bool expanded);
[[nodiscard]] bool expanded() const;
[[nodiscard]] rpl::producer<bool> expandedValue() const;
[[nodiscard]] int collapsedHeight() const;
[[nodiscard]] int expandedHeight() const;
[[nodiscard]] QMargins shadowExtent() const;
[[nodiscard]] int totalCollapsedHeight() const;
[[nodiscard]] int totalExpandedHeight() const;
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
private:
class Strip;
class Grid;
void relayout();
void toggleEmoji(EmojiPtr emoji, bool fromGrid);
void notifySelectionChanged();
void startExpandAnimation(bool expanded);
void applyExpandProgress();
void paintTailBubble(QPainter &p, const QRect &bubble, float64 opacity);
[[nodiscard]] float64 currentExpandValue() const;
[[nodiscard]] int currentShownHeight() const;
[[nodiscard]] int tailHeight() const;
[[nodiscard]] QRect bubbleRect() const;
[[nodiscard]] QRect bubbleShownRect() const;
const QString _aboutText;
const std::vector<EmojiPtr> _recent;
const int _maxSelected;
const bool _allowExpand;
std::vector<EmojiPtr> _allForGrid;
std::vector<EmojiPtr> _selectedList;
rpl::variable<std::vector<EmojiPtr>> _selectedVar;
rpl::variable<bool> _expanded = false;
std::unique_ptr<Ui::FlatLabel> _about;
Strip *_strip = nullptr;
Ui::AbstractButton *_expandButton = nullptr;
std::unique_ptr<Ui::ScrollArea> _scroll;
Grid *_grid = nullptr;
Ui::Animations::Simple _expandAnim;
Ui::BoxShadow _shadow;
};
} // namespace ChatHelpers
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "base/platform/base_platform_info.h"
#include "base/weak_ptr.h"
#include "base/zlib_help.h"
#include "data/data_session.h"
#include "lang/lang_instance.h"
@@ -18,12 +19,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mtproto/dedicated_file_loader.h"
#include "spellcheck/platform/platform_spellcheck.h"
#include "spellcheck/spellcheck_utils.h"
#include "spellcheck/spellcheck_value.h"
#include "core/application.h"
#include "core/core_settings.h"
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtGui/QGuiApplication>
#include <QtGui/QInputMethod>
@@ -59,53 +64,187 @@ inline auto LanguageFromLocale(QLocale loc) {
: int(locLang);
}
const auto kDictionaries = {
Dict{{ QLocale::English, 649, 174'516, "English" }}, // en_US
Dict{{ QLocale::Bulgarian, 594, 229'658, "\xd0\x91\xd1\x8a\xd0\xbb\xd0\xb3\xd0\xb0\xd1\x80\xd1\x81\xd0\xba\xd0\xb8" }}, // bg_BG
Dict{{ QLocale::Catalan, 595, 417'611, "\x43\x61\x74\x61\x6c\xc3\xa0" }}, // ca_ES
Dict{{ QLocale::Czech, 596, 860'286, "\xc4\x8c\x65\xc5\xa1\x74\x69\x6e\x61" }}, // cs_CZ
Dict{{ QLocale::Welsh, 597, 177'305, "\x43\x79\x6d\x72\x61\x65\x67" }}, // cy_GB
Dict{{ QLocale::Danish, 598, 345'874, "\x44\x61\x6e\x73\x6b" }}, // da_DK
Dict{{ QLocale::German, 599, 2'412'780, "\x44\x65\x75\x74\x73\x63\x68" }}, // de_DE
Dict{{ QLocale::Greek, 600, 1'389'160, "\xce\x95\xce\xbb\xce\xbb\xce\xb7\xce\xbd\xce\xb9\xce\xba\xce\xac" }}, // el_GR
Dict{{ LWC(QLocale::English, QLocale::Australia), 601, 175'266, "English (Australia)" }}, // en_AU
Dict{{ LWC(QLocale::English, QLocale::Canada), 602, 174'295, "English (Canada)" }}, // en_CA
Dict{{ LWC(QLocale::English, QLocale::UnitedKingdom), 603, 174'433, "English (United Kingdom)" }}, // en_GB
Dict{{ QLocale::Spanish, 604, 264'717, "\x45\x73\x70\x61\xc3\xb1\x6f\x6c" }}, // es_ES
Dict{{ QLocale::Estonian, 605, 757'394, "\x45\x65\x73\x74\x69" }}, // et_EE
Dict{{ QLocale::Persian, 606, 333'911, "\xd9\x81\xd8\xa7\xd8\xb1\xd8\xb3\xdb\x8c" }}, // fa_IR
Dict{{ QLocale::French, 607, 321'391, "\x46\x72\x61\x6e\xc3\xa7\x61\x69\x73" }}, // fr_FR
Dict{{ QLocale::Hebrew, 608, 622'550, "\xd7\xa2\xd7\x91\xd7\xa8\xd7\x99\xd7\xaa" }}, // he_IL
Dict{{ QLocale::Hindi, 609, 56'105, "\xe0\xa4\xb9\xe0\xa4\xbf\xe0\xa4\xa8\xe0\xa5\x8d\xe0\xa4\xa6\xe0\xa5\x80" }}, // hi_IN
Dict{{ QLocale::Croatian, 610, 668'876, "\x48\x72\x76\x61\x74\x73\x6b\x69" }}, // hr_HR
Dict{{ QLocale::Hungarian, 611, 660'402, "\x4d\x61\x67\x79\x61\x72" }}, // hu_HU
Dict{{ QLocale::Armenian, 612, 928'746, "\xd5\x80\xd5\xa1\xd5\xb5\xd5\xa5\xd6\x80\xd5\xa5\xd5\xb6" }}, // hy_AM
Dict{{ QLocale::Indonesian, 613, 100'134, "\x49\x6e\x64\x6f\x6e\x65\x73\x69\x61" }}, // id_ID
Dict{{ QLocale::Italian, 614, 324'613, "\x49\x74\x61\x6c\x69\x61\x6e\x6f" }}, // it_IT
Dict{{ QLocale::Korean, 615, 1'256'987, "\xed\x95\x9c\xea\xb5\xad\xec\x96\xb4" }}, // ko_KR
Dict{{ QLocale::Lithuanian, 616, 267'427, "\x4c\x69\x65\x74\x75\x76\x69\xc5\xb3" }}, // lt_LT
Dict{{ QLocale::Latvian, 617, 641'602, "\x4c\x61\x74\x76\x69\x65\xc5\xa1\x75" }}, // lv_LV
Dict{{ QLocale::NorwegianBokmal, 618, 588'650, "\x4e\x6f\x72\x73\x6b" }}, // nb_NO
Dict{{ QLocale::Dutch, 619, 743'406, "\x4e\x65\x64\x65\x72\x6c\x61\x6e\x64\x73" }}, // nl_NL
Dict{{ QLocale::Polish, 620, 1'015'747, "\x50\x6f\x6c\x73\x6b\x69" }}, // pl_PL
Dict{{ QLocale::Portuguese, 621, 1'231'999, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73 (Brazil)" }}, // pt_BR
Dict{{ LWC(QLocale::Portuguese, QLocale::Portugal), 622, 138'571, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73" }}, // pt_PT
Dict{{ QLocale::Romanian, 623, 455'643, "\x52\x6f\x6d\xc3\xa2\x6e\xc4\x83" }}, // ro_RO
Dict{{ QLocale::Russian, 624, 463'194, "\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\xb8\xd0\xb9" }}, // ru_RU
Dict{{ QLocale::Slovak, 625, 525'328, "\x53\x6c\x6f\x76\x65\x6e\xc4\x8d\x69\x6e\x61" }}, // sk_SK
Dict{{ QLocale::Slovenian, 626, 1'143'710, "\x53\x6c\x6f\x76\x65\x6e\xc5\xa1\xc4\x8d\x69\x6e\x61" }}, // sl_SI
Dict{{ QLocale::Albanian, 627, 583'412, "\x53\x68\x71\x69\x70" }}, // sq_AL
Dict{{ QLocale::Swedish, 628, 593'877, "\x53\x76\x65\x6e\x73\x6b\x61" }}, // sv_SE
Dict{{ QLocale::Tamil, 629, 323'193, "\xe0\xae\xa4\xe0\xae\xae\xe0\xae\xbf\xe0\xae\xb4\xe0\xaf\x8d" }}, // ta_IN
Dict{{ QLocale::Tajik, 630, 369'931, "\xd0\xa2\xd0\xbe\xd2\xb7\xd0\xb8\xd0\xba\xd3\xa3" }}, // tg_TG
Dict{{ QLocale::Turkish, 631, 4'301'099, "\x54\xc3\xbc\x72\x6b\xc3\xa7\x65" }}, // tr_TR
Dict{{ QLocale::Ukrainian, 632, 445'711, "\xd0\xa3\xd0\xba\xd1\x80\xd0\xb0\xd1\x97\xd0\xbd\xd1\x81\xd1\x8c\xd0\xba\xd0\xb0" }}, // uk_UA
Dict{{ QLocale::Vietnamese, 633, 12'949, "\x54\x69\xe1\xba\xbf\x6e\x67\x20\x56\x69\xe1\xbb\x87\x74" }}, // vi_VN
// The Tajik code is 'tg_TG' in Chromium, but QT has only 'tg_TJ'.
constexpr auto kDictionariesManifestChannel = "tdhbcfiles"_cs;
constexpr auto kDictionariesManifestPostId = 2949;
// Runtime-loaded dictionaries manifest. Kept in memory only: fetched from
// the pinned JSON post the first time something actually needs it
// (Manage Dictionaries, auto-download). Nothing persists to disk, so an
// install whose enabled dictionaries are all on disk never hits the net.
std::vector<Dict> DictionariesList;
rpl::event_stream<> DictionariesListChanged;
void EnsurePath();
bool ParseLocation(const QString &text, QString &channel, int &postId) {
const auto sep = text.indexOf('#');
if (sep <= 0 || sep == text.size() - 1) {
return false;
}
auto ok = false;
const auto parsed = text.mid(sep + 1).toInt(&ok);
if (!ok || parsed <= 0) {
return false;
}
channel = text.left(sep);
postId = parsed;
return true;
}
std::vector<Dict> ParseManifest(const QByteArray &bytes) {
auto result = std::vector<Dict>();
auto err = QJsonParseError();
const auto doc = QJsonDocument::fromJson(bytes, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
LOG(("Spellcheck Error: manifest JSON parse failed: %1"
).arg(err.errorString()));
return result;
}
const auto list = doc.object().value(u"dictionaries"_q).toArray();
result.reserve(list.size());
for (const auto &v : list) {
const auto obj = v.toObject();
auto d = Dict();
d.id = obj.value(u"id"_q).toInt();
d.size = int64(obj.value(u"size"_q).toDouble());
d.name = obj.value(u"name"_q).toString();
const auto location = obj.value(u"location"_q).toString();
if (!d.id
|| d.name.isEmpty()
|| !ParseLocation(location, d.channel, d.postId)) {
continue;
}
result.push_back(std::move(d));
}
return result;
}
class DictManifestLoader final : public base::has_weak_ptr {
public:
explicit DictManifestLoader(base::weak_ptr<Main::Session> session);
void start();
private:
void resolved(const MTPInputChannel &channel);
void received(const MTPmessages_Messages &result);
void apply(const QByteArray &bytes);
void finish();
MTP::WeakInstance _mtp;
};
std::shared_ptr<DictManifestLoader> ActiveManifestLoader;
DictManifestLoader::DictManifestLoader(base::weak_ptr<Main::Session> session)
: _mtp(session) {
}
void DictManifestLoader::start() {
if (!_mtp.valid()) {
finish();
return;
}
const auto weak = base::make_weak(this);
MTP::ResolveChannel(&_mtp, kDictionariesManifestChannel.utf16(), [=](
const MTPInputChannel &channel) {
if (const auto strong = weak.get()) {
strong->resolved(channel);
}
}, [=] {
if (const auto strong = weak.get()) {
strong->finish();
}
});
}
void DictManifestLoader::resolved(const MTPInputChannel &channel) {
const auto weak = base::make_weak(this);
_mtp.send(
MTPchannels_GetMessages(
channel,
MTP_vector<MTPInputMessage>(1,
MTP_inputMessageID(
MTP_int(kDictionariesManifestPostId)))),
[=](const MTPmessages_Messages &result) {
if (const auto strong = weak.get()) {
strong->received(result);
}
},
[=](const MTP::Error &) {
if (const auto strong = weak.get()) {
strong->finish();
}
});
}
void DictManifestLoader::received(const MTPmessages_Messages &result) {
const auto message = MTP::GetMessagesElement(result);
if (!message || message->type() != mtpc_message) {
LOG(("Spellcheck Error: manifest message not found."));
finish();
return;
}
apply(message->c_message().vmessage().v);
finish();
}
void DictManifestLoader::apply(const QByteArray &bytes) {
auto parsed = ParseManifest(bytes);
if (parsed.empty()) {
LOG(("Spellcheck Error: manifest empty or unparseable."));
return;
}
DictionariesList = std::move(parsed);
DictionariesListChanged.fire({});
}
void DictManifestLoader::finish() {
crl::on_main([] {
ActiveManifestLoader.reset();
});
}
void StartManifestRefresh(not_null<Main::Session*> session) {
if (ActiveManifestLoader) {
return;
}
ActiveManifestLoader = std::make_shared<DictManifestLoader>(
base::make_weak(session));
ActiveManifestLoader->start();
}
// Callers waiting for the first manifest arrival in this session.
// We don't cache manifest to disk, so each cold start begins empty;
// queued callbacks fire once when the MTP fetch lands.
std::vector<Fn<void()>> ManifestPending;
rpl::lifetime ManifestPendingSubscription;
void EnsureManifestThen(
not_null<Main::Session*> session,
Fn<void()> callback) {
if (!DictionariesList.empty()) {
callback();
return;
}
const auto firstCaller = ManifestPending.empty();
ManifestPending.push_back(std::move(callback));
if (firstCaller) {
DictionariesListChanged.events(
) | rpl::take(1) | rpl::on_next([] {
auto fns = std::move(ManifestPending);
ManifestPending.clear();
for (auto &fn : fns) {
fn();
}
}, ManifestPendingSubscription);
}
StartManifestRefresh(session);
}
inline auto IsSupportedLang(int lang) {
return ranges::contains(kDictionaries, lang, &Dict::id);
return ranges::contains(DictionariesList, lang, &Dict::id);
}
void EnsurePath() {
@@ -144,9 +283,12 @@ void DownloadDictionaryInBackground(
if (DictionaryExists(id)) {
auto dicts = Core::App().settings().dictionariesEnabled();
if (!ranges::contains(dicts, id)) {
if (ranges::contains(dicts, id)) {
Platform::Spellchecker::UpdateLanguages(dicts);
} else {
dicts.push_back(id);
Core::App().settings().setDictionariesEnabled(std::move(dicts));
Core::App().settings().setDictionariesEnabled(
std::move(dicts));
Core::App().saveSettingsDelayed();
}
}
@@ -230,17 +372,28 @@ void DictLoader::fail() {
}
std::vector<Dict> Dictionaries() {
return kDictionaries | ranges::to_vector;
return DictionariesList;
}
rpl::producer<> DictionariesChanged() {
return DictionariesListChanged.events();
}
void RefreshDictionariesManifest(not_null<Main::Session*> session) {
StartManifestRefresh(session);
}
int64 GetDownloadSize(int id) {
return ranges::find(kDictionaries, id, &Spellchecker::Dict::id)->size;
const auto i = ranges::find(DictionariesList, id, &Dict::id);
return (i == end(DictionariesList)) ? 0 : i->size;
}
MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
const auto username = kCloudLocationUsername.utf16();
const auto i = ranges::find(kDictionaries, id, &Spellchecker::Dict::id);
return MTP::DedicatedLoader::Location{ username, i->postId };
const auto i = ranges::find(DictionariesList, id, &Dict::id);
if (i == end(DictionariesList)) {
return MTP::DedicatedLoader::Location{};
}
return MTP::DedicatedLoader::Location{ i->channel, i->postId };
}
QString DictPathByLangId(int langId) {
@@ -256,7 +409,24 @@ QString DictionariesPath() {
bool UnpackDictionary(const QString &path, int langId) {
const auto folder = DictPathByLangId(langId);
return UnpackBlob(path, folder, IsGoodPartName);
if (!UnpackBlob(path, folder, IsGoodPartName)) {
return false;
}
// The Serbian archive ships "sr_Cyrl_RS.{dic,aff}", but
// QLocale(QLocale::Serbian).name() is "sr_RS" on our Qt, so the
// Hunspell loader and DictionaryExists miss them. Rename to the
// expected stem.
if (langId == int(QLocale::Serbian)) {
const auto dir = QDir(folder);
for (const auto &ext : kDictExtensions) {
const auto from = u"sr_Cyrl_RS.%1"_q.arg(ext);
const auto to = u"sr_RS.%1"_q.arg(ext);
if (dir.exists(from) && !dir.exists(to)) {
QFile::rename(dir.filePath(from), dir.filePath(to));
}
}
}
return true;
}
bool DictionaryExists(int langId) {
@@ -316,26 +486,26 @@ rpl::producer<QString> ButtonManageDictsState(
if (!Core::App().settings().spellcheckerEnabled()) {
return QString();
}
if (!Core::App().settings().dictionariesEnabled().size()) {
const auto dicts = Core::App().settings().dictionariesEnabled();
if (dicts.empty()) {
return QString();
}
const auto dicts = Core::App().settings().dictionariesEnabled();
const auto filtered = ranges::views::all(
dicts
) | ranges::views::filter(
DictionaryExists
) | ranges::to_vector;
const auto active = Platform::Spellchecker::ActiveLanguages();
return (active.size() == filtered.size())
? QString::number(filtered.size())
: tr::lng_contacts_loading(tr::now);
return (filtered.size() < dicts.size())
? tr::lng_contacts_loading(tr::now)
: QString::number(filtered.size());
};
return rpl::single(
computeString()
) | rpl::then(
rpl::merge(
Spellchecker::SupportedScriptsChanged(),
Spellchecker::DictionariesChanged(),
Core::App().settings().dictionariesEnabledChanges(
) | rpl::to_empty,
Core::App().settings().spellcheckerEnabledChanges(
@@ -418,12 +588,18 @@ void Start(not_null<Main::Session*> session) {
return;
}
const auto l = LanguageFromLocale(method->locale());
if (!IsSupportedLang(l) || DictionaryExists(l)) {
// Avoid pulling the manifest just because an input-method
// locale flipped; only fetch if we'd actually need to
// download something for the new locale.
if (DictionaryExists(l)) {
return;
}
crl::on_main(session, [=] {
EnsureManifestThen(session, crl::guard(session, [=] {
if (!IsSupportedLang(l) || DictionaryExists(l)) {
return;
}
DownloadDictionaryInBackground(session, 0, { l });
});
}));
};
QObject::connect(
method,
@@ -437,8 +613,17 @@ void Start(not_null<Main::Session*> session) {
if (!loaded) {
return;
}
DownloadDictionaryInBackground(session, 0, DefaultLanguages());
const auto enabled = settings->dictionariesEnabled();
if (!enabled.empty()
&& ranges::all_of(enabled, &DictionaryExists)) {
// Every previously-enabled dictionary is already on
// disk; no manifest fetch, no network traffic.
return;
}
EnsureManifestThen(session, crl::guard(session, [=] {
DownloadDictionaryInBackground(
session, 0, DefaultLanguages());
}));
}, lifetime);
connectInput();
@@ -19,6 +19,7 @@ class Session;
namespace Spellchecker {
struct Dict : public Storage::CloudBlob::Blob {
QString channel;
};
int64 GetDownloadSize(int id);
@@ -33,6 +34,8 @@ bool RemoveDictionary(int langId);
bool WriteDefaultDictionary();
std::vector<Dict> Dictionaries();
rpl::producer<> DictionariesChanged();
void RefreshDictionariesManifest(not_null<Main::Session*> session);
void Start(not_null<Main::Session*> session);
[[nodiscard]] rpl::producer<QString> ButtonManageDictsState(
@@ -122,7 +122,9 @@ EmojiPack::EmojiPack(not_null<Main::Session*> session)
EmojiPack::~EmojiPack() = default;
bool EmojiPack::add(not_null<ViewElement*> view) {
if (const auto custom = view->onlyCustomEmoji()) {
if (view->data()->textAppearing()) {
return false;
} else if (const auto custom = view->onlyCustomEmoji()) {
_onlyCustomItems.emplace(view);
return true;
} else if (const auto emoji = view->isolatedEmoji()) {
@@ -146,7 +146,8 @@ rpl::producer<std::vector<GifSection>> GifSectionsValue(
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
const std::vector<QString> &query,
base::flat_set<EmojiPtr> &outResultSet) {
base::flat_set<EmojiPtr> &outResultSet,
bool exact) {
auto result = std::vector<EmojiPtr>();
const auto pushPlain = [&](EmojiPtr emoji) {
if (result.size() < kEmojiSearchLimit
@@ -170,7 +171,7 @@ rpl::producer<std::vector<GifSection>> GifSectionsValue(
refreshed = true;
keywords.refresh();
}
const auto list = keywords.queryMine(entry);
const auto list = keywords.queryMine(entry, exact);
for (const auto &entry : list) {
pushPlain(entry.emoji);
if (result.size() >= kEmojiSearchLimit) {
@@ -70,7 +70,8 @@ struct GifSection {
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
const std::vector<QString> &query,
base::flat_set<EmojiPtr> &outResultSet);
base::flat_set<EmojiPtr> &outResultSet,
bool exact = false);
struct StickerIcon {
explicit StickerIcon(uint64 setId);
@@ -63,6 +63,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers {
[[nodiscard]] QVector<MTPstring> SearchStickersLangCodes() {
auto result = QVector<MTPstring>();
if (const auto method = QGuiApplication::inputMethod()) {
for (const auto &lang : method->locale().uiLanguages()) {
result.push_back(MTP_string(lang));
}
}
return result;
}
namespace {
constexpr auto kSearchRequestDelay = 400;
@@ -209,6 +220,7 @@ StickersListWidget::StickersListWidget(
, _section(Section::Stickers)
, _isMasks(_mode == Mode::Masks)
, _isEffects(_mode == Mode::MessageEffects)
, _excludeSetId(descriptor.excludeSetId)
, _updateItemsTimer([=] { updateItems(); })
, _updateSetsTimer([=] { updateSets(); })
, _trendingAddBgOver(
@@ -369,6 +381,9 @@ void StickersListWidget::visibleTopBottomUpdated(
checkVisibleFeatured(visibleTop, visibleBottom);
} else {
checkVisibleLottie();
if (_section == Section::Search) {
checkPaginateSearchStickers(visibleTop, visibleBottom);
}
}
if (_footer) {
_footer->validateSelectedIcon(
@@ -619,31 +634,52 @@ void StickersListWidget::sendSearchRequest() {
return;
}
requestSearchStickers(_searchQuery, 0, true);
}
void StickersListWidget::sendSearchSetsRequest(const QString &query) {
const auto hash = uint64(0);
_searchSetsRequestId = _api.request(MTPmessages_SearchStickerSets(
MTP_flags(0),
MTP_string(query),
MTP_long(hash)
)).done([=](const MTPmessages_FoundStickerSets &result) {
searchResultsDone(query, result);
}).fail([=] {
_searchSetsRequestId = 0;
if (_searchNextQuery == query) {
toggleSearchLoading(false);
}
}).handleAllErrors().send();
}
void StickersListWidget::requestSearchStickers(
const QString &query,
int offset,
bool requestSetsOnEmpty) {
const auto hash = uint64(0);
_searchStickersRequestId = _api.request(MTPmessages_SearchStickers(
MTP_flags(0),
MTP_string(_searchQuery),
MTP_string(query),
MTPstring(), // emoticon
MTP_vector<MTPstring>(), // lang_code
MTP_int(0), // offset
MTP_int(50), // limit
MTP_vector<MTPstring>(SearchStickersLangCodes()),
MTP_int(offset),
MTP_int(50),
MTP_long(hash)
)).done([=](const MTPmessages_FoundStickers &result) {
searchStickersResultsDone(result);
searchStickersResultsDone(
query,
offset,
requestSetsOnEmpty,
result);
}).fail([=] {
_searchStickersRequestId = 0;
_searchStickersCache.emplace(_searchQuery, std::vector<DocumentId>());
_searchSetsRequestId = _api.request(MTPmessages_SearchStickerSets(
MTP_flags(0),
MTP_string(_searchQuery),
MTP_long(hash)
)).done([=](const MTPmessages_FoundStickerSets &result) {
searchResultsDone(result);
}).fail([=] {
toggleSearchLoading(false);
_searchSetsRequestId = 0;
}).handleAllErrors().send();
if (requestSetsOnEmpty) {
_searchStickersCache.emplace(query, std::vector<DocumentId>());
if (_searchNextQuery == query) {
sendSearchSetsRequest(query);
}
}
}).handleAllErrors().send();
}
@@ -701,6 +737,7 @@ void StickersListWidget::cancelSetsSearch() {
_filterStickersCornerEmoji.clear();
_searchSetsCache.clear();
_searchStickersCache.clear();
_searchStickersNextOffset.clear();
refreshSearchRows(nullptr);
}
@@ -710,7 +747,7 @@ void StickersListWidget::showSearchResults() {
}
void StickersListWidget::refreshSearchRows() {
auto it = _searchSetsCache.find(_searchQuery);
auto it = _searchSetsCache.find(_searchNextQuery);
auto sets = (it != end(_searchSetsCache))
? &it->second
: nullptr;
@@ -969,16 +1006,41 @@ auto StickersListWidget::shownSets() -> std::vector<Set> & {
}
void StickersListWidget::searchStickersResultsDone(
const QString &query,
int requestedOffset,
bool requestSetsOnEmpty,
const MTPmessages_FoundStickers &result) {
_searchStickersRequestId = 0;
const auto active = (_searchNextQuery == query);
result.match([&](const MTPDmessages_foundStickersNotModified &data) {
LOG(("API: messages.foundStickersNotModified."));
if (const auto next = data.vnext_offset()) {
if (next->v > requestedOffset) {
_searchStickersNextOffset[query] = next->v;
} else {
_searchStickersNextOffset.erase(query);
}
} else {
_searchStickersNextOffset.erase(query);
}
_searchStickersCache.emplace(query, std::vector<DocumentId>());
if (!active) {
return;
}
if (requestSetsOnEmpty) {
sendSearchSetsRequest(query);
return;
}
refreshSearchRows();
checkPaginateSearchStickers(
getVisibleTop(),
getVisibleBottom());
}, [&](const MTPDmessages_foundStickers &data) {
auto it = _searchStickersCache.find(_searchQuery);
auto it = _searchStickersCache.find(query);
if (it == _searchStickersCache.cend()) {
it = _searchStickersCache.emplace(
_searchQuery,
query,
std::vector<DocumentId>()).first;
}
@@ -988,38 +1050,84 @@ void StickersListWidget::searchStickersResultsDone(
}
}
if (!it->second.empty()) {
toggleSearchLoading(false);
if (const auto next = data.vnext_offset()) {
if (next->v > requestedOffset) {
_searchStickersNextOffset[query] = next->v;
} else {
_searchStickersNextOffset.erase(query);
}
} else {
_searchStickersNextOffset.erase(query);
}
if (!active) {
return;
}
if (requestSetsOnEmpty && it->second.empty()) {
sendSearchSetsRequest(query);
return;
}
toggleSearchLoading(false);
if (requestSetsOnEmpty) {
showSearchResults();
} else {
const auto hash = uint64(0);
_searchSetsRequestId = _api.request(MTPmessages_SearchStickerSets(
MTP_flags(0),
MTP_string(_searchQuery),
MTP_long(hash)
)).done([=](const MTPmessages_FoundStickerSets &result) {
searchResultsDone(result);
}).fail([=] {
toggleSearchLoading(false);
_searchSetsRequestId = 0;
}).handleAllErrors().send();
refreshSearchRows();
}
checkPaginateSearchStickers(
getVisibleTop(),
getVisibleBottom());
});
}
void StickersListWidget::loadMoreSearchStickers() {
if (_searchStickersRequestId
|| _searchQuery.isEmpty()
|| _isEffects
|| (_searchQuery != _searchNextQuery)) {
return;
}
const auto query = _searchQuery;
const auto offsetIt = _searchStickersNextOffset.find(query);
if (offsetIt == _searchStickersNextOffset.end()) {
return;
}
requestSearchStickers(query, offsetIt->second, false);
}
void StickersListWidget::checkPaginateSearchStickers(
int visibleTop,
int visibleBottom) {
if (_section != Section::Search
|| _searchQuery.isEmpty()
|| (_searchQuery != _searchNextQuery)
|| _searchStickersRequestId) {
return;
}
const auto visibleHeight = visibleBottom - visibleTop;
if (visibleHeight <= 0) {
return;
}
if (visibleBottom > height() - visibleHeight * kPreloadOfficialPages) {
loadMoreSearchStickers();
}
}
void StickersListWidget::searchResultsDone(
const QString &query,
const MTPmessages_FoundStickerSets &result) {
toggleSearchLoading(false);
if (_searchNextQuery == query) {
toggleSearchLoading(false);
}
_searchSetsRequestId = 0;
result.match([&](const MTPDmessages_foundStickerSetsNotModified &data) {
LOG(("API Error: "
"messages.foundStickerSetsNotModified not expected."));
}, [&](const MTPDmessages_foundStickerSets &data) {
auto it = _searchSetsCache.find(_searchQuery);
auto it = _searchSetsCache.find(query);
if (it == _searchSetsCache.cend()) {
it = _searchSetsCache.emplace(
_searchQuery,
query,
std::vector<uint64>()).first;
}
for (const auto &setData : data.vsets().v) {
@@ -1029,7 +1137,9 @@ void StickersListWidget::searchResultsDone(
}
it->second.push_back(set->id);
}
showSearchResults();
if (_searchNextQuery == query) {
showSearchResults();
}
});
}
@@ -2512,6 +2622,9 @@ bool StickersListWidget::appendSet(
uint64 setId,
bool externalLayout,
AppendSkip skip) {
if (_excludeSetId && setId == _excludeSetId) {
return false;
}
const auto &sets = session().data().stickers().sets();
auto it = sets.find(setId);
if (it == sets.cend()
@@ -57,6 +57,7 @@ struct FlatLabel;
namespace ChatHelpers {
extern const char kOptionUnlimitedRecentStickers[];
[[nodiscard]] QVector<MTPstring> SearchStickersLangCodes();
struct StickerIcon;
enum class ValidateIconAnimations;
@@ -84,6 +85,7 @@ struct StickersListDescriptor {
std::vector<StickerCustomRecentDescriptor> customRecentList;
const style::EmojiPan *st = nullptr;
ComposeFeatures features;
uint64 excludeSetId = 0;
};
class StickersListWidget final : public TabbedSelector::Inner {
@@ -358,8 +360,21 @@ private:
void cancelSetsSearch();
void showSearchResults();
void searchResultsDone(const MTPmessages_FoundStickerSets &result);
void searchStickersResultsDone(const MTPmessages_FoundStickers &result);
void sendSearchSetsRequest(const QString &query);
void searchResultsDone(
const QString &query,
const MTPmessages_FoundStickerSets &result);
void requestSearchStickers(
const QString &query,
int offset,
bool requestSetsOnEmpty);
void searchStickersResultsDone(
const QString &query,
int requestedOffset,
bool requestSetsOnEmpty,
const MTPmessages_FoundStickers &result);
void loadMoreSearchStickers();
void checkPaginateSearchStickers(int visibleTop, int visibleBottom);
void refreshSearchRows();
void refreshSearchRows(const std::vector<uint64> *cloudSets);
void fillFilteredStickersRow();
@@ -407,6 +422,7 @@ private:
Section _section = Section::Stickers;
const bool _isMasks;
const bool _isEffects;
const uint64 _excludeSetId = 0;
base::Timer _updateItemsTimer;
base::Timer _updateSetsTimer;
@@ -449,6 +465,7 @@ private:
rpl::variable<int> _recentShownCount;
std::map<QString, std::vector<uint64>> _searchSetsCache;
std::map<QString, std::vector<DocumentId>> _searchStickersCache;
std::map<QString, int> _searchStickersNextOffset;
std::vector<std::pair<uint64, QStringList>> _searchIndex;
base::Timer _searchRequestTimer;
QString _searchQuery, _searchNextQuery;
@@ -349,7 +349,8 @@ not_null<DocumentData*> GenerateLocalSticker(
not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name) {
const QString &name,
bool useTextColor) {
const auto cache = [&] {
struct Session {
base::weak_ptr<Main::Session> session;
@@ -370,7 +371,8 @@ not_null<DocumentData*> GenerateLocalTgsSticker(
return &Map.back().cache;
}();
const auto i = cache->find(name);
const auto key = useTextColor ? (name + u"/:/1"_q) : name;
const auto i = cache->find(key);
if (i != end(*cache)) {
return i->second;
}
@@ -378,8 +380,11 @@ not_null<DocumentData*> GenerateLocalTgsSticker(
const auto result = GenerateLocalSticker(
session,
u":/animations/"_q + name + u".tgs"_q);
if (useTextColor) {
result->overrideEmojiUsesTextColor(true);
}
cache->emplace(name, result);
cache->emplace(key, result);
Ensures(result->sticker()->isLottie());
return result;
@@ -137,6 +137,7 @@ bool PaintStickerThumbnailPath(
[[nodiscard]] not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name);
const QString &name,
bool useTextColor = false);
} // namespace ChatHelpers
@@ -193,6 +193,10 @@ void TabbedPanel::setDesiredHeightValues(
updateContentHeight();
}
void TabbedPanel::setShowAnimationOrigin(Ui::PanelAnimation::Origin origin) {
_showAnimationOrigin = origin;
}
void TabbedPanel::setDropDown(bool dropDown) {
selector()->setDropDown(dropDown);
_dropDown = dropDown;
@@ -381,11 +385,12 @@ void TabbedPanel::startShowAnimation() {
if (!_a_show.animating()) {
auto image = grabForAnimation();
const auto origin = _showAnimationOrigin.value_or(_dropDown
? Ui::PanelAnimation::Origin::TopRight
: Ui::PanelAnimation::Origin::BottomRight);
_showAnimation = std::make_unique<Ui::PanelAnimation>(
_selector->st().showAnimation,
(_dropDown
? Ui::PanelAnimation::Origin::TopRight
: Ui::PanelAnimation::Origin::BottomRight));
origin);
auto inner = rect().marginsRemoved(st::emojiPanMargins);
_showAnimation->setFinalImage(
std::move(image),
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "ui/effects/animations.h"
#include "ui/effects/panel_animation.h"
#include "ui/rp_widget.h"
#include "ui/widgets/shadow.h"
#include "base/timer.h"
@@ -17,9 +18,6 @@ namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class PanelAnimation;
} // namespace Ui
namespace ChatHelpers {
@@ -57,6 +55,7 @@ public:
int minHeight,
int maxHeight);
void setDropDown(bool dropDown);
void setShowAnimationOrigin(Ui::PanelAnimation::Origin origin);
void hideFast();
bool hiding() const {
@@ -122,6 +121,7 @@ private:
bool _shouldFinishHide = false;
bool _dropDown = false;
std::optional<Ui::PanelAnimation::Origin> _showAnimationOrigin;
bool _hiding = false;
bool _hideAfterSlide = false;
@@ -381,6 +381,7 @@ TabbedSelector::TabbedSelector(
, _show(std::move(descriptor.show))
, _level(descriptor.level)
, _customTextColor(std::move(descriptor.customTextColor))
, _excludeStickerSetId(descriptor.excludeStickerSetId)
, _mode(descriptor.mode)
, _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg))
, _categoriesRounding(
@@ -645,6 +646,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
.paused = paused,
.st = &_st,
.features = _features,
.excludeSetId = _excludeStickerSetId,
});
}
case SelectorTab::Gifs: {
@@ -100,6 +100,7 @@ struct TabbedSelectorDescriptor {
TabbedSelectorMode mode = TabbedSelectorMode::Full;
Fn<QColor()> customTextColor;
ComposeFeatures features;
uint64 excludeStickerSetId = 0;
};
enum class TabbedSearchType {
@@ -295,6 +296,7 @@ private:
const std::shared_ptr<Show> _show;
const PauseReason _level = {};
const Fn<QColor()> _customTextColor;
const uint64 _excludeStickerSetId = 0;
Ui::Controls::SwipeBackResult _swipeBackData;
+15
View File
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/sandbox.h"
#include "core/local_url_handlers.h"
#include "core/launcher.h"
#include "core/proxy_rotation_manager.h"
#include "core/ui_integration.h"
#include "chat_helpers/emoji_keywords.h"
#include "chat_helpers/stickers_emoji_image_loader.h"
@@ -151,6 +152,7 @@ struct Application::Private {
base::Timer quitTimer;
UiIntegration uiIntegration;
Settings settings;
std::unique_ptr<ProxyRotationManager> proxyRotation;
};
Application::Application()
@@ -178,6 +180,7 @@ Application::Application()
, _setupEmailLock(false)
, _autoLockTimer([=] { checkAutoLock(); }) {
Ui::Integration::Set(&_private->uiIntegration);
_private->proxyRotation = std::make_unique<ProxyRotationManager>();
_platformIntegration->init();
@@ -239,6 +242,7 @@ Application::~Application() {
// Domain::finish() and there is a violation on Ensures(started()).
closeAdditionalWindows();
_private->proxyRotation = nullptr;
_domain->finish();
Local::finish();
@@ -843,6 +847,17 @@ void Application::setCurrentProxy(
refreshGlobalProxy();
_proxyChanges.fire({ was, now });
my.connectionTypeChangesNotify();
proxyRotationSettingsChanged();
}
void Application::proxyRotationSettingsChanged() {
_private->proxyRotation->settingsChanged();
}
void Application::checkProxyRotation(
not_null<Main::Account*> account,
int32 state) {
_private->proxyRotation->handleConnectionStateChanged(account, state);
}
auto Application::proxyChanges() const -> rpl::producer<ProxyChange> {
+2
View File
@@ -219,6 +219,8 @@ public:
void setCurrentProxy(
const MTP::ProxyData &proxy,
MTP::ProxyData::Settings settings);
void proxyRotationSettingsChanged();
void checkProxyRotation(not_null<Main::Account*> account, int32 state);
[[nodiscard]] rpl::producer<ProxyChange> proxyChanges() const;
void badMtprotoConfigurationError();
@@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/platform/base_platform_info.h"
#include "storage/serialize_common.h"
#include <algorithm>
namespace Core {
namespace {
@@ -88,6 +90,22 @@ namespace {
return result;
}
std::vector<int> NormalizeProxyRotationPreferredIndices(
std::vector<int> indices,
int listSize) {
auto filtered = std::vector<int>();
filtered.reserve(indices.size());
for (const auto index : indices) {
if (index < 0
|| index >= listSize
|| ranges::contains(filtered, index)) {
continue;
}
filtered.push_back(index);
}
return filtered;
}
} // namespace
SettingsProxy::SettingsProxy()
@@ -107,7 +125,8 @@ QByteArray SettingsProxy::serialize() const {
serializedList,
0,
ranges::plus(),
&Serialize::bytearraySize);
&Serialize::bytearraySize)
+ (4 + int(_proxyRotationPreferredIndices.size())) * sizeof(qint32);
auto stream = Serialize::ByteArrayWriter(size);
stream
<< qint32(_tryIPv6 ? 1 : 0)
@@ -118,6 +137,14 @@ QByteArray SettingsProxy::serialize() const {
for (const auto &i : serializedList) {
stream << i;
}
stream
<< qint32(_checkIpWarningShown ? 1 : 0)
<< qint32(_proxyRotationEnabled ? 1 : 0)
<< qint32(_proxyRotationTimeout)
<< qint32(_proxyRotationPreferredIndices.size());
for (const auto index : _proxyRotationPreferredIndices) {
stream << qint32(index);
}
return std::move(stream).result();
}
@@ -133,6 +160,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
auto settings = ProxySettingsToInt(_settings);
auto listCount = qint32(_list.size());
auto selectedProxy = QByteArray();
auto list = std::vector<MTP::ProxyData>();
if (!stream.atEnd()) {
stream
@@ -142,10 +170,43 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
>> selectedProxy
>> listCount;
if (stream.ok()) {
if (listCount < 0) {
return false;
}
list.reserve(listCount);
for (auto i = 0; i != listCount; ++i) {
QByteArray data;
stream >> data;
_list.push_back(DeserializeProxyData(data));
list.push_back(DeserializeProxyData(data));
}
}
}
auto checkIpWarningShown = qint32(0);
if (!stream.atEnd()) {
stream >> checkIpWarningShown;
}
auto proxyRotationEnabled = qint32(_proxyRotationEnabled ? 1 : 0);
if (!stream.atEnd()) {
stream >> proxyRotationEnabled;
}
auto proxyRotationTimeout = qint32(_proxyRotationTimeout);
if (!stream.atEnd()) {
stream >> proxyRotationTimeout;
}
auto preferredCount = qint32(0);
auto preferredIndices = std::vector<int>();
if (!stream.atEnd()) {
stream >> preferredCount;
if (stream.ok()) {
if (preferredCount < 0) {
return false;
}
preferredIndices.reserve(preferredCount);
for (auto i = 0; i != preferredCount; ++i) {
auto index = qint32(0);
stream >> index;
preferredIndices.push_back(index);
}
}
}
@@ -158,8 +219,13 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
_tryIPv6 = (tryIPv6 == 1);
_useProxyForCalls = (useProxyForCalls == 1);
_checkIpWarningShown = (checkIpWarningShown == 1);
_proxyRotationEnabled = (proxyRotationEnabled == 1);
setProxyRotationTimeout(proxyRotationTimeout);
_settings = IntToProxySettings(settings);
_selected = DeserializeProxyData(selectedProxy);
_list = std::move(list);
setProxyRotationPreferredIndices(std::move(preferredIndices));
return true;
}
@@ -176,6 +242,40 @@ bool SettingsProxy::isDisabled() const {
return _settings == MTP::ProxyData::Settings::Disabled;
}
bool SettingsProxy::checkIpWarningShown() const {
return _checkIpWarningShown;
}
void SettingsProxy::setCheckIpWarningShown(bool value) {
_checkIpWarningShown = value;
}
const std::vector<int> &SettingsProxy::proxyRotationPreferredIndices() const {
return _proxyRotationPreferredIndices;
}
void SettingsProxy::setProxyRotationPreferredIndices(std::vector<int> value) {
_proxyRotationPreferredIndices = NormalizeProxyRotationPreferredIndices(
std::move(value),
int(_list.size()));
}
bool SettingsProxy::promoteProxyRotationPreferredIndex(int index) {
if (index < 0 || index >= int(_list.size())) {
return false;
}
auto &indices = _proxyRotationPreferredIndices;
const auto i = ranges::find(indices, index);
if (i == begin(indices)) {
return false;
} else if (i != end(indices)) {
std::rotate(begin(indices), i, std::next(i));
} else {
indices.insert(begin(indices), index);
}
return true;
}
bool SettingsProxy::tryIPv6() const {
return _tryIPv6;
}
@@ -192,6 +292,24 @@ void SettingsProxy::setUseProxyForCalls(bool value) {
_useProxyForCalls = value;
}
bool SettingsProxy::proxyRotationEnabled() const {
return _proxyRotationEnabled;
}
void SettingsProxy::setProxyRotationEnabled(bool value) {
_proxyRotationEnabled = value;
}
int SettingsProxy::proxyRotationTimeout() const {
return _proxyRotationTimeout;
}
void SettingsProxy::setProxyRotationTimeout(int value) {
_proxyRotationTimeout = (value > 0)
? value
: kDefaultProxyRotationTimeout;
}
MTP::ProxyData::Settings SettingsProxy::settings() const {
return _settings;
}
@@ -216,6 +334,62 @@ std::vector<MTP::ProxyData> &SettingsProxy::list() {
return _list;
}
void SettingsProxy::setList(std::vector<MTP::ProxyData> value) {
_list = std::move(value);
_proxyRotationPreferredIndices.clear();
}
void SettingsProxy::addToList(MTP::ProxyData value) {
_list.push_back(std::move(value));
}
void SettingsProxy::insertToList(int index, MTP::ProxyData value) {
index = std::clamp(index, 0, int(_list.size()));
for (auto &existing : _proxyRotationPreferredIndices) {
if (existing >= index) {
++existing;
}
}
_list.insert(begin(_list) + index, std::move(value));
}
bool SettingsProxy::removeFromList(const MTP::ProxyData &value) {
const auto i = ranges::find(_list, value);
if (i == end(_list)) {
return false;
}
const auto index = int(i - begin(_list));
_list.erase(i);
for (auto &existing : _proxyRotationPreferredIndices) {
if (existing > index) {
--existing;
}
}
_proxyRotationPreferredIndices.erase(
std::remove(
begin(_proxyRotationPreferredIndices),
end(_proxyRotationPreferredIndices),
index),
end(_proxyRotationPreferredIndices));
return true;
}
bool SettingsProxy::replaceInList(
const MTP::ProxyData &was,
MTP::ProxyData value) {
const auto i = ranges::find(_list, was);
if (i == end(_list)) {
return false;
}
*i = std::move(value);
return true;
}
int SettingsProxy::indexInList(const MTP::ProxyData &value) const {
const auto i = ranges::find(_list, value);
return (i == end(_list)) ? -1 : int(i - begin(_list));
}
rpl::producer<> SettingsProxy::connectionTypeValue() const {
return _connectionTypeChanges.events_starting_with({});
}
@@ -9,10 +9,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/mtproto_proxy_data.h"
#include <array>
namespace Core {
class SettingsProxy final {
public:
static constexpr auto kProxyRotationTimeouts = std::array{
5,
10,
15,
30,
60,
};
static constexpr auto kDefaultProxyRotationTimeout = 10;
SettingsProxy();
[[nodiscard]] bool isEnabled() const;
@@ -29,14 +40,35 @@ public:
[[nodiscard]] bool useProxyForCalls() const;
void setUseProxyForCalls(bool value);
[[nodiscard]] bool proxyRotationEnabled() const;
void setProxyRotationEnabled(bool value);
[[nodiscard]] int proxyRotationTimeout() const;
void setProxyRotationTimeout(int value);
[[nodiscard]] MTP::ProxyData::Settings settings() const;
void setSettings(MTP::ProxyData::Settings value);
[[nodiscard]] MTP::ProxyData selected() const;
void setSelected(MTP::ProxyData value);
[[nodiscard]] bool checkIpWarningShown() const;
void setCheckIpWarningShown(bool value);
[[nodiscard]] const std::vector<int> &proxyRotationPreferredIndices() const;
void setProxyRotationPreferredIndices(std::vector<int> value);
[[nodiscard]] bool promoteProxyRotationPreferredIndex(int index);
[[nodiscard]] const std::vector<MTP::ProxyData> &list() const;
[[nodiscard]] std::vector<MTP::ProxyData> &list();
void setList(std::vector<MTP::ProxyData> value);
void addToList(MTP::ProxyData value);
void insertToList(int index, MTP::ProxyData value);
[[nodiscard]] bool removeFromList(const MTP::ProxyData &value);
[[nodiscard]] bool replaceInList(
const MTP::ProxyData &was,
MTP::ProxyData value);
[[nodiscard]] int indexInList(const MTP::ProxyData &value) const;
[[nodiscard]] QByteArray serialize() const;
bool setFromSerialized(const QByteArray &serialized);
@@ -44,13 +76,16 @@ public:
private:
bool _tryIPv6 = false;
bool _useProxyForCalls = false;
bool _proxyRotationEnabled = false;
bool _checkIpWarningShown = false;
int _proxyRotationTimeout = kDefaultProxyRotationTimeout;
MTP::ProxyData::Settings _settings = MTP::ProxyData::Settings::System;
MTP::ProxyData _selected;
std::vector<MTP::ProxyData> _list;
std::vector<int> _proxyRotationPreferredIndices;
rpl::event_stream<> _connectionTypeChanges;
};
} // namespace Core
+1 -1
View File
@@ -119,7 +119,7 @@ void ComputeDebugMode() {
auto file = QFile(debugModeSettingPath);
if (file.exists() && file.open(QIODevice::ReadOnly)) {
Logs::SetDebugEnabled(file.read(1) != "0");
#if defined _DEBUG
#if defined _DEBUG && !defined Q_OS_MAC
} else {
Logs::SetDebugEnabled(true);
#endif
@@ -0,0 +1,359 @@
/*
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 "core/proxy_rotation_manager.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "mtproto/facade.h"
#include <algorithm>
namespace Core {
namespace {
constexpr auto kProxyRotationCheckInterval = 2 * crl::time(1000);
constexpr auto kProxyRotationCheckLifetime = 20 * crl::time(1000);
constexpr auto kProxyRotationMaxActiveChecks = 10;
} // namespace
ProxyRotationManager::ProxyRotationManager()
: _checkTimer([=] { runChecks(); })
, _switchTimer([=] { switchTimerDone(); }) {
App().domain().accountsChanges(
) | rpl::on_next([=] {
stopChecking();
reevaluate();
}, _lifetime);
}
void ProxyRotationManager::settingsChanged() {
stopChecking();
pruneRemovedEntries();
reevaluate();
}
void ProxyRotationManager::handleConnectionStateChanged(
not_null<Main::Account*> account,
int32 state) {
(void)account;
(void)state;
reevaluate();
}
bool ProxyRotationManager::shouldObserve() const {
const auto &settings = App().settings().proxy();
return settings.isEnabled()
&& settings.selected()
&& settings.proxyRotationEnabled()
&& (settings.list().size() > 1);
}
std::vector<not_null<Main::Account*>> ProxyRotationManager::productionAccounts() const {
auto result = std::vector<not_null<Main::Account*>>();
for (const auto &entry : App().domain().accounts()) {
const auto account = entry.account.get();
if (!account->sessionExists() || account->mtp().isTestMode()) {
continue;
}
result.push_back(account);
}
return result;
}
not_null<Main::Account*> ProxyRotationManager::accountForChecks() const {
if (App().someSessionExists()
&& App().activeAccount().sessionExists()
&& !App().activeAccount().mtp().isTestMode()) {
return &App().activeAccount();
}
const auto accounts = productionAccounts();
Expects(!accounts.empty());
return accounts.front();
}
auto ProxyRotationManager::find(
const MTP::ProxyData &proxy) -> Entry* {
const auto i = ranges::find(
_entries,
proxy,
[](const Entry &entry) { return entry.proxy; });
return (i == end(_entries)) ? nullptr : &*i;
}
auto ProxyRotationManager::ensure(
const MTP::ProxyData &proxy) -> Entry& {
if (const auto entry = find(proxy)) {
return *entry;
}
_entries.push_back({ .proxy = proxy });
return _entries.back();
}
void ProxyRotationManager::reevaluate() {
if (!shouldObserve()) {
stopChecking();
return;
}
const auto accounts = productionAccounts();
if (accounts.empty()) {
stopChecking();
return;
}
const auto stateProj = [](not_null<Main::Account*> account) {
return account->mtp().dcstate();
};
if (ranges::contains(accounts, MTP::ConnectedState, stateProj)) {
stopChecking();
return;
}
startChecking();
}
void ProxyRotationManager::startChecking() {
if (_checking) {
return;
}
_checking = true;
_waitingToSwitch = false;
_switchStartedAt = crl::now();
updateProbeOrder();
runChecks();
const auto timeout = App().settings().proxy().proxyRotationTimeout();
_switchTimer.callOnce(timeout * crl::time(1000));
}
void ProxyRotationManager::stopChecking() {
_checkTimer.cancel();
_switchTimer.cancel();
_checking = false;
_waitingToSwitch = false;
_switchStartedAt = 0;
_probeOrder.clear();
_nextCheckIndex = 0;
clearPendingChecks();
}
void ProxyRotationManager::pruneRemovedEntries() {
const auto &settings = App().settings().proxy();
_entries.erase(
std::remove_if(begin(_entries), end(_entries), [&](const Entry &entry) {
return (settings.indexInList(entry.proxy) < 0);
}),
end(_entries));
}
void ProxyRotationManager::updateProbeOrder() {
const auto &settings = App().settings().proxy();
const auto currentIndex = settings.indexInList(settings.selected());
_probeOrder.clear();
_probeOrder.reserve(settings.list().size());
for (const auto index : settings.proxyRotationPreferredIndices()) {
if (index == currentIndex) {
continue;
}
_probeOrder.push_back(index);
}
for (auto i = 0, count = int(settings.list().size()); i != count; ++i) {
if (i == currentIndex || ranges::contains(_probeOrder, i)) {
continue;
}
_probeOrder.push_back(i);
}
_nextCheckIndex = 0;
}
void ProxyRotationManager::continueChecking(crl::time delay) {
if (!_checking) {
return;
}
if (_checkTimer.isActive()) {
_checkTimer.cancel();
}
_checkTimer.callOnce(delay);
}
void ProxyRotationManager::runChecks() {
if (!_checking) {
return;
}
if (!shouldObserve()) {
stopChecking();
return;
}
const auto accounts = productionAccounts();
if (accounts.empty()
|| ranges::contains(
accounts,
MTP::ConnectedState,
[](not_null<Main::Account*> account) {
return account->mtp().dcstate();
})) {
stopChecking();
return;
}
pruneExpiredChecks();
startNextCheck();
continueChecking(kProxyRotationCheckInterval);
}
void ProxyRotationManager::pruneExpiredChecks() {
const auto now = crl::now();
for (auto &entry : _entries) {
if (!entry.checking
|| (now - entry.startedAt < kProxyRotationCheckLifetime)) {
continue;
}
MTP::ResetProxyCheckers(entry.v4, entry.v6);
entry.checking = false;
entry.startedAt = 0;
}
}
void ProxyRotationManager::startNextCheck() {
if (_probeOrder.empty()) {
return;
}
if (ranges::count(_entries, true, &Entry::checking)
>= kProxyRotationMaxActiveChecks) {
return;
}
const auto &settings = App().settings().proxy();
auto attemptsLeft = int(_probeOrder.size());
while (attemptsLeft-- > 0) {
if (_nextCheckIndex >= int(_probeOrder.size())) {
_nextCheckIndex = 0;
}
const auto listIndex = _probeOrder[_nextCheckIndex++];
if (listIndex < 0 || listIndex >= int(settings.list().size())) {
continue;
}
const auto &proxy = settings.list()[listIndex];
auto &entry = ensure(proxy);
if (entry.checking) {
continue;
}
entry.checking = true;
entry.startedAt = crl::now();
MTP::StartProxyCheck(
&accountForChecks()->mtp(),
proxy,
settings.tryIPv6(),
entry.v4,
entry.v6,
[=](MTP::details::AbstractConnection *raw, int ping) {
checkDone(proxy, raw, ping);
},
[=](MTP::details::AbstractConnection *raw) {
checkFailed(proxy, raw);
});
break;
}
}
void ProxyRotationManager::switchTimerDone() {
if (!_checking || !shouldSwitchToAvailable()) {
return;
}
_waitingToSwitch = !switchToAvailable();
}
void ProxyRotationManager::clearPendingChecks() {
for (auto &entry : _entries) {
MTP::ResetProxyCheckers(entry.v4, entry.v6);
entry.checking = false;
entry.startedAt = 0;
}
}
void ProxyRotationManager::checkDone(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw,
int ping) {
const auto entry = find(proxy);
if (!entry
|| !entry->checking
|| ((entry->v4.get() != raw) && (entry->v6.get() != raw))) {
return;
}
MTP::DropProxyChecker(entry->v4, entry->v6, raw);
MTP::ResetProxyCheckers(entry->v4, entry->v6);
entry->checking = false;
entry->startedAt = 0;
entry->availableAt = crl::now();
const auto proxySettings = &App().settings().proxy();
if (const auto index = proxySettings->indexInList(proxy); index >= 0) {
if (proxySettings->promoteProxyRotationPreferredIndex(index)) {
App().saveSettingsDelayed();
}
}
updateProbeOrder();
if (_waitingToSwitch && shouldSwitchToAvailable()) {
_waitingToSwitch = !switchToAvailable();
}
}
void ProxyRotationManager::checkFailed(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw) {
const auto entry = find(proxy);
if (!entry
|| !entry->checking
|| ((entry->v4.get() != raw) && (entry->v6.get() != raw))) {
return;
}
MTP::DropProxyChecker(entry->v4, entry->v6, raw);
if (MTP::HasProxyCheckers(entry->v4, entry->v6)) {
return;
}
entry->checking = false;
entry->startedAt = 0;
}
bool ProxyRotationManager::switchToAvailable() {
if (!_checking) {
return false;
}
const auto &settings = App().settings().proxy();
for (const auto index : _probeOrder) {
if (index < 0 || index >= int(settings.list().size())) {
continue;
}
const auto &proxy = settings.list()[index];
const auto entry = find(proxy);
if (!entry || entry->checking || !entry->availableAt) {
continue;
}
if (entry->availableAt < _switchStartedAt) {
continue;
}
_waitingToSwitch = false;
App().setCurrentProxy(proxy, MTP::ProxyData::Settings::Enabled);
App().saveSettingsDelayed();
return true;
}
return false;
}
bool ProxyRotationManager::shouldSwitchToAvailable() const {
if (!_checking || !shouldObserve()) {
return false;
}
const auto accounts = productionAccounts();
return !accounts.empty()
&& !ranges::contains(
accounts,
MTP::ConnectedState,
[](not_null<Main::Account*> account) {
return account->mtp().dcstate();
});
}
} // namespace Core
@@ -0,0 +1,80 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "mtproto/proxy_check.h"
#include <rpl/lifetime.h>
#include <vector>
namespace Main {
class Account;
} // namespace Main
namespace Core {
class ProxyRotationManager final {
public:
ProxyRotationManager();
void settingsChanged();
void handleConnectionStateChanged(
not_null<Main::Account*> account,
int32 state);
private:
struct Entry {
MTP::ProxyData proxy;
MTP::ProxyCheckConnection v4;
MTP::ProxyCheckConnection v6;
bool checking = false;
crl::time startedAt = 0;
crl::time availableAt = 0;
};
[[nodiscard]] bool shouldObserve() const;
[[nodiscard]] std::vector<not_null<Main::Account*>> productionAccounts() const;
[[nodiscard]] not_null<Main::Account*> accountForChecks() const;
[[nodiscard]] Entry *find(const MTP::ProxyData &proxy);
[[nodiscard]] Entry &ensure(const MTP::ProxyData &proxy);
void reevaluate();
void startChecking();
void stopChecking();
void pruneRemovedEntries();
void updateProbeOrder();
void continueChecking(crl::time delay);
void runChecks();
void pruneExpiredChecks();
void startNextCheck();
void switchTimerDone();
void clearPendingChecks();
void checkDone(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw,
int ping);
void checkFailed(
const MTP::ProxyData &proxy,
not_null<MTP::details::AbstractConnection*> raw);
[[nodiscard]] bool switchToAvailable();
[[nodiscard]] bool shouldSwitchToAvailable() const;
base::Timer _checkTimer;
base::Timer _switchTimer;
std::vector<Entry> _entries;
std::vector<int> _probeOrder;
int _nextCheckIndex = 0;
bool _checking = false;
bool _waitingToSwitch = false;
crl::time _switchStartedAt = 0;
rpl::lifetime _lifetime;
};
} // namespace Core
+12 -1
View File
@@ -30,6 +30,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "ui/effects/animations.h"
#ifdef Q_OS_MAC
#include "platform/mac/global_menu_mac.h"
#endif // Q_OS_MAC
#include <QtCore/QLockFile>
#include <QtGui/QSessionManager>
#include <QtGui/QScreen>
@@ -54,6 +58,9 @@ bool Sandbox::QuitOnStartRequested = false;
Sandbox::Sandbox(int &argc, char **argv)
: QApplication(argc, argv)
, _mainThreadId(QThread::currentThreadId()) {
#ifdef Q_OS_MAC
Platform::CreateGlobalMenu();
#endif // Q_OS_MAC
}
int Sandbox::start() {
@@ -222,7 +229,11 @@ void Sandbox::setupScreenScale() {
LOG(("ScreenScale: %1").arg(cScreenScale()));
}
Sandbox::~Sandbox() = default;
Sandbox::~Sandbox() {
#ifdef Q_OS_MAC
Platform::DestroyGlobalMenu();
#endif // Q_OS_MAC
}
bool Sandbox::event(QEvent *e) {
if (e->type() == QEvent::Quit) {
+2 -2
View File
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 6007005;
constexpr auto AppVersionStr = "6.7.5";
constexpr auto AppVersion = 6007008;
constexpr auto AppVersionStr = "6.7.8";
constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
@@ -66,7 +66,6 @@ private:
rpl::variable<CreditsAmount> _nonLockedBalance;
rpl::event_stream<> _loadedChanges;
crl::time _lastLoaded = 0;
float64 _rate = 0.;
rpl::variable<CreditsAmount> _tonBalance;
rpl::event_stream<> _tonLoadedChanges;
@@ -57,7 +57,6 @@ private:
mtpRequestId _requestId = 0;
bool _disabled = false;
bool _received = false;
};
+72 -14
View File
@@ -88,6 +88,16 @@ void UpdateStickerSetIdentifier(
});
}
[[nodiscard]] int ResolveAttributeVsTranscodeQuality(
int attributesQuality,
int transcodeMax) {
return (transcodeMax > 0
&& (attributesQuality < transcodeMax
|| attributesQuality > transcodeMax * 1.5))
? transcodeMax
: attributesQuality;
}
} // namespace
QString FileNameUnsafe(
@@ -547,8 +557,7 @@ void DocumentData::setVideoQualities(
return document->isVideoFile()
&& !document->dimensions.isEmpty()
&& !document->inappPlaybackFailed()
&& document->useStreamingLoader()
&& document->canBeStreamed();
&& document->useStreamingLoader();
};
ranges::sort(
qualities,
@@ -578,18 +587,58 @@ void DocumentData::setVideoQualities(
}
qualities.erase(qualities.begin() + count, qualities.end());
if (!qualities.empty()) {
if (const auto mine = resolveVideoQuality()) {
if (mine > qualities.front()->resolveVideoQuality()) {
qualities.insert(begin(qualities), this);
auto transcodeMax = 0;
for (const auto &quality : qualities) {
const auto qres = quality->resolveVideoQuality();
if (qres > transcodeMax) {
transcodeMax = qres;
}
}
const auto attributesSize = isVideoFile() ? dimensions : QSize();
const auto attributesQuality = attributesSize.isEmpty()
? 0
: std::min(attributesSize.width(), attributesSize.height());
auto mine = ResolveAttributeVsTranscodeQuality(
attributesQuality,
transcodeMax);
if (mine) {
qualities.insert(begin(qualities), this);
}
}
data->qualities = std::move(qualities);
}
int DocumentData::resolveVideoQuality() const {
const auto size = isVideoFile() ? dimensions : QSize();
return size.isEmpty() ? 0 : std::min(size.width(), size.height());
if (const auto data = video()) {
if (!data->realVideoSize.isEmpty()) {
const auto size = data->realVideoSize;
return std::min(size.width(), size.height());
}
const auto attributesSize = isVideoFile() ? dimensions : QSize();
const auto attributesQuality = attributesSize.isEmpty()
? 0
: std::min(attributesSize.width(), attributesSize.height());
if (!data->qualities.empty()) {
auto transcodeMax = 0;
for (const auto &quality : data->qualities) {
if (quality != this) {
const auto qres = quality->resolveVideoQuality();
if (qres > transcodeMax) {
transcodeMax = qres;
}
}
}
if (transcodeMax > 0) {
return ResolveAttributeVsTranscodeQuality(
attributesQuality,
transcodeMax);
}
}
}
const auto attributesSize = isVideoFile() ? dimensions : QSize();
return attributesSize.isEmpty()
? 0
: std::min(attributesSize.width(), attributesSize.height());
}
auto DocumentData::resolveQualities(HistoryItem *context) const
@@ -611,19 +660,28 @@ not_null<DocumentData*> DocumentData::chooseQuality(
return this;
}
const auto height = int(request.height);
auto closest = this;
auto closestAbs = std::abs(height - resolveVideoQuality());
auto closestSize = size;
if (request.original) {
return this;
}
auto closest = (DocumentData*)nullptr;
auto closestAbs = -1;
auto closestSize = -1;
for (const auto &quality : list) {
const auto abs = std::abs(height - quality->resolveVideoQuality());
if (abs < closestAbs
|| (abs == closestAbs && quality->size < closestSize)) {
const auto qres = quality->resolveVideoQuality();
const auto abs = std::abs(height - qres);
if (!closest
|| abs < closestAbs
|| (abs == closestAbs && (quality->size < closestSize
|| (closest == this && quality != this)))) {
closest = quality;
closestAbs = abs;
closestSize = quality->size;
}
}
return closest;
return closest ? closest : this;
}
void DocumentData::validateLottieSticker() {
@@ -99,6 +99,7 @@ struct VoiceData : public DocumentAdditionalData {
struct VideoData : public DocumentAdditionalData {
QString codec;
std::vector<not_null<DocumentData*>> qualities;
QSize realVideoSize;
};
using RoundData = VoiceData;
@@ -181,7 +181,7 @@ Image *DocumentMedia::goodThumbnail() const {
}
void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
if (!(_flags & Flag::GoodThumbnailWanted)) {
if (!(_flags & Flag::GoodThumbnailWanted) || thumbnail.isNull()) {
return;
}
_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
@@ -285,7 +285,10 @@ void DocumentMedia::checkStickerLarge() {
void DocumentMedia::automaticLoad(
Data::FileOrigin origin,
const HistoryItem *item) {
if (_owner->status != FileReady || loaded() || _owner->cancelled()) {
if (_owner->status != FileReady
|| loaded()
|| _owner->uploading()
|| _owner->cancelled()) {
return;
} else if (!item && !_owner->sticker() && !_owner->isAnimation()) {
return;
+32 -14
View File
@@ -101,11 +101,11 @@ struct AlbumCounts {
};
[[nodiscard]] TextWithEntities WithCaptionNotificationText(
const QString &attachType,
TextWithEntities attachType,
const TextWithEntities &caption,
bool hasMiniImages = false) {
if (caption.text.isEmpty()) {
return Ui::Text::Colorized(attachType);
return Ui::Text::Colorized(std::move(attachType));
}
auto wrapped = st::wrap_rtl(caption);
return hasMiniImages
@@ -116,13 +116,23 @@ struct AlbumCounts {
tr::lng_dialogs_text_media_wrapped(
tr::now,
lt_media,
Ui::Text::Colorized(attachType),
Ui::Text::Colorized(std::move(attachType)),
tr::marked),
lt_caption,
wrapped,
tr::marked);
}
[[nodiscard]] TextWithEntities WithCaptionNotificationText(
const QString &attachType,
const TextWithEntities &caption,
bool hasMiniImages = false) {
return WithCaptionNotificationText(
Ui::Text::WithEntities(attachType),
caption,
hasMiniImages);
}
[[nodiscard]] QImage PreparePreviewImage(
not_null<const Image*> image,
ImageRoundRadius radius,
@@ -1209,27 +1219,35 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
}
}
}
const auto type = [&] {
const auto type = [&]() -> TextWithEntities {
using namespace Ui::Text;
if (_document->isVideoMessage()) {
return (item->media() && item->media()->ttlSeconds())
return WithEntities((item->media() && item->media()->ttlSeconds())
? tr::lng_in_dlg_video_message_ttl(tr::now)
: tr::lng_in_dlg_video_message(tr::now);
: tr::lng_in_dlg_video_message(tr::now));
} else if (_document->isAnimation()) {
return u"GIF"_q;
return WithEntities(u"GIF"_q);
} else if (_document->isVideoFile()) {
return tr::lng_in_dlg_video(tr::now);
return WithEntities(tr::lng_in_dlg_video(tr::now));
} else if (_document->isVoiceMessage()) {
return (item->media() && item->media()->ttlSeconds())
? tr::lng_in_dlg_voice_message_ttl(tr::now)
: tr::lng_in_dlg_audio(tr::now);
if (item->media() && item->media()->ttlSeconds()) {
return WithEntities(
tr::lng_in_dlg_voice_message_ttl(tr::now));
} else if (item->isUnreadMedia()) {
return tr::lng_in_dlg_audio_unread(
tr::now,
lt_emoji,
IconEmoji(&st::dialogsUnreadMediaDotEmoji),
tr::rich);
}
return WithEntities(tr::lng_in_dlg_audio(tr::now));
} else if (const auto name = FormatSongNameFor(_document).string();
!name.isEmpty()) {
return name;
return WithEntities(name);
} else if (_document->isAudioFile()) {
return tr::lng_in_dlg_audio_file(tr::now);
return WithEntities(tr::lng_in_dlg_audio_file(tr::now));
}
return tr::lng_in_dlg_file(tr::now);
return WithEntities(tr::lng_in_dlg_file(tr::now));
}();
const auto caption = (options.hideCaption || options.ignoreMessageText)
? TextWithEntities()
+1 -1
View File
@@ -634,7 +634,7 @@ private:
crl::time _lastFullUpdate = 0;
QString _name;
uint32 _nameVersion : 29 = 1;
uint32 _nameVersion : 16 = 1;
uint32 _sensitiveContent : 1 = 0;
uint32 _wallPaperOverriden : 1 = 0;
uint32 _checkedTrustedPayForMessage : 1 = 0;
@@ -194,8 +194,6 @@ private:
bool _restorePinnedWhenNonEmpty = false;
mtpRequestId _reloadUnreadCountRequestId = 0;
rpl::lifetime _lifetime;
};
+38 -19
View File
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_streamed_drafts.h"
#include "history/view/media/history_view_media.h"
#include "history/view/history_view_element.h"
#include "inline_bots/inline_bot_layout_item.h"
@@ -2109,6 +2110,11 @@ void Session::requestItemTextRefresh(not_null<HistoryItem*> item) {
view->itemTextUpdated();
});
requestItemResize(item);
if (item->textAppearing()) {
enumerateItemViews(item, [&](not_null<ViewElement*> view) {
view->skipInactiveTextAppearing();
});
}
};
if (const auto group = groups().find(item)) {
call(group->items.front());
@@ -2117,14 +2123,6 @@ void Session::requestItemTextRefresh(not_null<HistoryItem*> item) {
}
}
void Session::requestItemTextRefreshStreaming(
not_null<HistoryItem*> item) {
enumerateItemViews(item, [&](not_null<ViewElement*> view) {
view->itemTextUpdatedStreaming();
});
requestItemResize(item);
}
void Session::registerRestricted(
not_null<const HistoryItem*> item,
const QString &reason) {
@@ -3179,6 +3177,20 @@ HistoryItem *Session::addNewMessage(
return nullptr;
}
if (data.type() == mtpc_message) {
if (const auto h = historyLoaded(peerId)) {
if (const auto streamed = h->streamedDraftsIfExists()) {
if (const auto adopted = streamed->adoptIncoming(
data.c_message())) {
if (type == NewMessageType::Unread) {
CheckForSwitchInlineButton(adopted);
}
return adopted;
}
}
}
}
const auto result = history(peerId)->addNewMessage(
id,
data,
@@ -4206,6 +4218,21 @@ void Session::webpageApplyFields(
auto iv = (data.vcached_page() && !IgnoreIv(type))
? std::make_unique<Iv::Data>(data, *data.vcached_page())
: nullptr;
const auto resolvedPhoto = story
? story->photo()
: photo
? processPhoto(*photo).get()
: nullptr;
const auto resolvedDocument = story
? story->document()
: document
? processDocument(*document).get()
: lookupThemeDocument();
const auto photoIsVideoCover = data.is_video_cover_photo()
|| (resolvedDocument
&& resolvedPhoto
&& resolvedDocument->isVideoFile()
&& !resolvedDocument->hasThumbnail());
webpageApplyFields(
page,
type,
@@ -4215,16 +4242,8 @@ void Session::webpageApplyFields(
qs(data.vtitle().value_or_empty()),
(story ? story->caption() : description),
storyId,
(story
? story->photo()
: photo
? processPhoto(*photo).get()
: nullptr),
(story
? story->document()
: document
? processDocument(*document).get()
: lookupThemeDocument()),
resolvedPhoto,
resolvedDocument,
WebPageCollage(this, data),
std::move(iv),
lookupStickerSet(),
@@ -4233,7 +4252,7 @@ void Session::webpageApplyFields(
data.vduration().value_or_empty(),
qs(data.vauthor().value_or_empty()),
data.is_has_large_media(),
data.is_video_cover_photo(),
photoIsVideoCover,
pendingTill);
}
-1
View File
@@ -406,7 +406,6 @@ public:
void requestItemViewRefresh(not_null<const HistoryItem*> item);
[[nodiscard]] rpl::producer<not_null<const HistoryItem*>> itemViewRefreshRequest() const;
void requestItemTextRefresh(not_null<HistoryItem*> item);
void requestItemTextRefreshStreaming(not_null<HistoryItem*> item);
void requestUnreadReactionsAnimation(not_null<HistoryItem*> item);
void notifyHistoryUnloaded(not_null<const History*> history);
[[nodiscard]] rpl::producer<not_null<const History*>> historyUnloaded() const;
@@ -18,8 +18,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace {
constexpr auto kShortPollTimeout = 30 * crl::time(1000);
const TodoListItem *ItemById(const std::vector<TodoListItem> &list, int id) {
const auto i = ranges::find(list, id, &TodoListItem::id);
return (i != end(list)) ? &*i : nullptr;
+1
View File
@@ -366,6 +366,7 @@ enum class MessageFlag : uint64 {
HasUnreadPollVote = (1ULL << 59),
TextAppearing = (1ULL << 60),
TextAppearingStarted = (1ULL << 61),
AyuNoForwards = (1ULL << 63),
};
@@ -1609,7 +1609,8 @@ not_null<StickersSet*> Stickers::feedSet(const MTPStickerSet &info) {
& (SetFlag::Featured
| SetFlag::Unread
| SetFlag::NotLoaded
| SetFlag::Special);
| SetFlag::Special
| SetFlag::Installed);
set->flags = flags | clientFlags;
const auto installDate = data.vinstalled_date();
set->installDate = installDate

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