Merge tag 'v6.7.8' into dev

# Conflicts:
#	.github/workflows/mac_packaged.yml
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/api/api_chat_invite.h
#	Telegram/SourceFiles/boxes/language_box.cpp
#	Telegram/SourceFiles/boxes/send_files_box.h
#	Telegram/SourceFiles/boxes/sticker_set_box.cpp
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/data/data_types.h
#	Telegram/SourceFiles/history/history_item.cpp
#	Telegram/SourceFiles/history/history_widget.cpp
#	Telegram/SourceFiles/history/view/history_view_chat_section.cpp
#	Telegram/SourceFiles/history/view/history_view_message.cpp
#	Telegram/SourceFiles/history/view/history_view_translate_bar.cpp
#	Telegram/SourceFiles/history/view/history_view_translate_tracker.cpp
#	Telegram/SourceFiles/platform/mac/main_window_mac.mm
#	Telegram/SourceFiles/settings/sections/settings_main.cpp
#	Telegram/lib_ui
#	snap/snapcraft.yaml
This commit is contained in:
AlexeyZavar
2026-04-24 20:47:17 +03:00
308 changed files with 13057 additions and 2300 deletions
+2 -2
View File
@@ -127,7 +127,7 @@ Locate the render tool (`codegen_style` with `--render-svg` mode):
```bash
if [[ "$OSTYPE" == darwin* ]]; then
ls out/Debug/codegen_style
ls out/Telegram/codegen/codegen/style/Debug/codegen_style
else
ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe
fi
@@ -137,7 +137,7 @@ If missing, build it: `cmake --build out --config Debug --target codegen_style`
Test on a known good SVG (use the appropriate binary path for the OS):
```bash
CODEGEN=$(if [[ "$OSTYPE" == darwin* ]]; then echo out/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)
CODEGEN=$(if [[ "$OSTYPE" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)
$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512
```
+8 -2
View File
@@ -27,9 +27,11 @@ Read `changelog.txt` from the repository root. Note the **latest version number*
### 3. Determine the new version number
- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given).
**Important:** version numbers are shared across the beta and stable tracks — the sequence advances through both. The same `major.minor.patch` cannot be released as both beta and stable. A beta release "uses up" that number; the next stable must bump to a new patch.
- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given). If the latest changelog entry already used this exact number (regardless of beta/stable), warn the user — they likely want a bumped patch.
- **If no version was provided**, auto-increment from the latest changelog version:
- If it was a beta, and the new release is **not** beta, reuse the same version number but drop "beta".
- If it was a beta, and the new release is **not** beta, bump the patch component by 1 (do **not** reuse the beta's number for stable).
- If the new release is beta and the latest was also beta with the same major.minor, bump patch.
- Otherwise bump the patch component by 1.
- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.
@@ -65,6 +67,10 @@ Use this exact format (date is today in DD.MM.YY):
Prepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.
**Never delete or edit existing entries**, even if the new stable entry merges bullets from prior beta(s). Prior beta blocks remain as historical record for beta-track users.
**For stable releases spanning prior beta(s):** the entry should cover everything since the last **stable** release (not just since the last beta), including noteworthy beta-shipped features, trimmed as needed to stay within 412 bullets.
### 8. Wait for approval
After writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.
+1 -1
View File
@@ -44,4 +44,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
@@ -0,0 +1,16 @@
name: Unused styles updater.
on:
repository_dispatch:
types: ["Restart unused_styles_updater workflow."]
schedule:
- cron: "0 3 1 * *"
workflow_dispatch:
jobs:
Unused-styles:
runs-on: ubuntu-latest
steps:
- uses: desktop-app/action_code_updater@master
with:
type: "unused-styles"
+1
View File
@@ -212,6 +212,7 @@ api().request(MTPnamespace_MethodName(
```
- For single constructors, use `.data()` shortcut
- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling
- Silently ignore HTTP 406 errors in UI: the server uses 406 to mean "show nothing to the user". Guard toasts with `MTP::IgnoreError(error)` or use `MTP::ShowErrorFallback(show, error)` (both in `mtproto/mtproto_response.h`) which shows `error.type()` as a toast unless the error should be ignored.
## UI Styling
+17
View File
@@ -350,6 +350,8 @@ PRIVATE
api/api_statistics_data_deserialize.h
api/api_statistics_sender.cpp
api/api_statistics_sender.h
api/api_stickers_creator.cpp
api/api_stickers_creator.h
api/api_suggest_post.cpp
api/api_suggest_post.h
api/api_text_entities.cpp
@@ -515,6 +517,8 @@ PRIVATE
boxes/send_gif_with_caption_box.h
boxes/send_files_box.cpp
boxes/send_files_box.h
boxes/send_files_box_reply_header.cpp
boxes/send_files_box_reply_header.h
boxes/share_box.cpp
boxes/share_box.h
boxes/star_gift_auction_box.cpp
@@ -531,6 +535,8 @@ PRIVATE
boxes/star_gift_preview_box.h
boxes/star_gift_resale_box.cpp
boxes/star_gift_resale_box.h
boxes/sticker_creator_box.cpp
boxes/sticker_creator_box.h
boxes/sticker_set_box.cpp
boxes/sticker_set_box.h
boxes/stickers_box.cpp
@@ -626,6 +632,8 @@ PRIVATE
chat_helpers/emoji_keywords.h
chat_helpers/emoji_list_widget.cpp
chat_helpers/emoji_list_widget.h
chat_helpers/emoji_picker_overlay.cpp
chat_helpers/emoji_picker_overlay.h
chat_helpers/emoji_sets_manager.cpp
chat_helpers/emoji_sets_manager.h
chat_helpers/emoji_suggestions_widget.cpp
@@ -662,6 +670,8 @@ PRIVATE
chat_helpers/ttl_media_layer_widget.h
core/application.cpp
core/application.h
core/proxy_rotation_manager.cpp
core/proxy_rotation_manager.h
core/cached_webview_availability.h
core/bank_card_click_handler.cpp
core/bank_card_click_handler.h
@@ -1593,6 +1603,8 @@ PRIVATE
mtproto/facade.h
mtproto/mtp_instance.cpp
mtproto/mtp_instance.h
mtproto/proxy_check.cpp
mtproto/proxy_check.h
mtproto/sender.h
mtproto/session.cpp
mtproto/session.h
@@ -1656,6 +1668,8 @@ PRIVATE
platform/linux/webauthn_linux.cpp
platform/mac/file_utilities_mac.mm
platform/mac/file_utilities_mac.h
platform/mac/global_menu_mac.h
platform/mac/global_menu_mac.mm
platform/mac/launcher_mac.mm
platform/mac/launcher_mac.h
platform/mac/integration_mac.mm
@@ -1813,6 +1827,8 @@ PRIVATE
settings/sections/settings_local_passcode.h
settings/sections/settings_main.cpp
settings/sections/settings_main.h
settings/settings_recent_searches.cpp
settings/settings_recent_searches.h
settings/settings_search.cpp
settings/settings_search.h
settings/settings_faq_suggestions.cpp
@@ -2004,6 +2020,7 @@ PRIVATE
window/window_section_common.h
window/window_separate_id.cpp
window/window_separate_id.h
window/session/window_session_media.cpp
window/window_session_controller.cpp
window/window_session_controller.h
window/window_session_controller_link_info.h
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="96.5 111.3 766.5 766.5" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" fill="none" fill-rule="evenodd">
<path d="M 229.278 428.274 C 220.345 431.087 209.724 433.028 201.037 428.414 C 195.269 425.35 191.59 419.715 189.754 413.573 C 183.55 392.827 189.646 312.841 194.598 289.706 C 197.361 276.798 201.636 264.234 208.088 252.692 C 225.944 220.748 254.345 196.434 289.828 186.42 C 299.548 183.676 309.688 182.14 319.754 181.423 C 366.51 178.095 414.437 180.111 461.345 180.132 C 499.997 180.149 540.285 177.606 578.657 181.934 C 589.925 183.204 601.183 185.766 611.125 191.369 C 638.232 206.647 660.549 233.285 682.155 255.355 L 741.115 315.299 L 790.708 365.561 C 856.391 432.25 853.313 440.991 854.144 532.569 L 854.982 618.399 C 855.281 646.04 857.577 702.741 854.521 727.102 C 852.435 744.448 846.915 761.205 838.282 776.393 C 820.298 808.656 790.121 832.354 754.514 842.176 C 724.089 850.54 664.593 848.635 631.504 848.632 L 502.763 848.626 L 395.022 848.66 C 368.985 848.68 338.582 849.327 312.926 846.33 C 294.671 844.264 277.017 838.552 261.012 829.532 C 228.881 811.132 205.527 780.551 196.236 744.708 C 190.977 725.111 181.752 660.54 192.22 640.91 C 194.552 636.536 200.443 631.193 204.584 628.088 C 212.69 627.794 223.61 625.977 229.827 632.364 C 238.468 641.24 238.784 653.684 239.449 665.298 C 241.05 693.25 237.429 726.072 251.724 751.22 C 263.068 771.178 283.96 788.616 306.011 795.036 C 326.159 800.066 359.183 798.753 380.622 798.749 L 460.193 798.696 L 703.391 798.695 C 736.68 798.43 755.087 795.322 779.818 770.591 C 807.116 743.293 806.834 720.038 807.012 684.161 C 807.237 638.481 802.094 586.261 804.093 541.122 C 797.284 541.268 784.894 540.995 778.473 540.182 L 670.807 540.171 C 620.731 540.215 563.716 547.86 526.38 508.874 C 515.394 497.402 505.295 482.717 501.211 466.939 C 495.154 439.227 496.977 401.501 496.999 372.596 L 497.103 265.614 C 497.25 253.886 497.577 242.161 498.083 230.443 C 460.209 229.51 422.322 229.222 384.438 229.578 C 335.986 229.564 309.732 221.795 269.362 254.551 C 217.116 296.943 257.721 389.29 229.559 427.897 L 229.278 428.274 z M 543.987 230.048 C 569.591 229.725 580.035 227.497 600.857 244.842 C 616.603 257.958 630.079 272.64 644.427 287.248 L 711.181 355.245 L 759.072 403.921 C 769.77 414.798 785.756 430.108 793.914 442.441 C 801.17 453.41 801.179 474.426 802.716 487.848 L 663.551 488.081 C 655.173 488.09 645.943 488.449 637.627 488.107 C 619.637 487.367 594.727 490.338 577.846 484.153 C 545.853 472.432 543.843 436.102 544.247 407.389 C 544.438 393.841 544.247 379.666 544.256 365.887 C 544.495 320.607 544.405 275.326 543.987 230.048 z" fill="#FFFFFF"></path>
<path d="M 306.755 391.204 C 311.745 389.305 333.398 408.89 337.28 412.89 C 364.174 440.939 392.794 467.428 419.422 495.698 C 442.291 519.978 433.144 542.942 411.651 563.866 C 381.068 593.638 352.265 625.515 320.324 653.743 C 313.888 659.431 296.743 655.829 288.97 654.425 C 287.039 649.138 280.827 633.452 282.505 628.63 C 289.515 608.489 314.352 590.824 328.353 575.581 C 338.311 564.74 345.544 555.412 356.226 547.597 L 356.28 545.853 C 344.978 549.112 335.336 551.079 323.481 551.267 C 274.259 551.846 224.991 551.501 175.755 551.624 C 155.295 551.676 133.425 553.912 116.627 541.311 C 114.956 537.351 114 533.126 113.802 528.833 C 113.091 514.333 117.1 501.472 133.314 500.562 C 154.052 499.399 174.698 499.879 195.467 499.925 C 239.08 500.336 282.696 500.334 326.309 499.918 C 332.541 499.822 343.551 500.1 347.833 495.685 C 347.436 491.434 313.367 461.839 307.219 455.232 C 291.043 437.85 275.937 429.066 286.23 402.426 C 291.881 397.453 299.873 394.302 306.755 391.204 z" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

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

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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

After

Width:  |  Height:  |  Size: 1.6 KiB

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

After

Width:  |  Height:  |  Size: 1.1 KiB

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

After

Width:  |  Height:  |  Size: 1.4 KiB

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

After

Width:  |  Height:  |  Size: 426 B

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

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