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
@@ -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 |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 769 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
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 |
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 ¤t = 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;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(); });
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -409,7 +409,7 @@ void RequestButton(
|
||||
url,
|
||||
displayName,
|
||||
session->user()->name(),
|
||||
bot->firstName,
|
||||
bot ? bot->firstName : QString(),
|
||||
callback),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||