Updated to 6.8.1.
Esse commit está contido em:
@@ -4,8 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [dev]
|
branches: [dev]
|
||||||
paths: [changelog.txt]
|
paths: [changelog.txt]
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
linux:
|
linux:
|
||||||
name: Rocky Linux 8
|
name: Rocky Linux 8
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: MacOS
|
name: MacOS
|
||||||
runs-on: macos-latest
|
runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-macos-latest' || 'macos-latest' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: MacOS
|
name: MacOS
|
||||||
runs-on: macos-latest
|
runs-on: ${{ (github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-macos-latest' || 'macos-latest' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Windows
|
name: Windows
|
||||||
runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || 'windows-latest' }}
|
runs-on: ${{ matrix.arch == 'arm64' && 'windows-11-arm' || ((github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/')) && 'depot-windows-latest-16' || 'windows-latest') }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
git config --global user.email "you@example.com"
|
git config --global user.email "you@example.com"
|
||||||
git config --global user.name "Sample"
|
git config --global user.name "Sample"
|
||||||
|
|
||||||
- uses: ilammy/msvc-dev-cmd@v1.13.0
|
- uses: Eden-CI/msvc-dev-cmd@master
|
||||||
name: Native Tools Command Prompt.
|
name: Native Tools Command Prompt.
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|||||||
+12
@@ -156,6 +156,18 @@ QString currentTitle = tr::lng_settings_title(tr::now);
|
|||||||
rpl::producer<QString> nameProducer = GetNameProducer();
|
rpl::producer<QString> nameProducer = GetNameProducer();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Use `_q` for QString literals:**
|
||||||
|
|
||||||
|
Prefer the project literal `u"..."_q` instead of the verbose `QStringLiteral("...")` macro when creating `QString` values:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Prefer this:
|
||||||
|
auto text = u"Settings"_q;
|
||||||
|
|
||||||
|
// Instead of this:
|
||||||
|
auto text = QStringLiteral("Settings");
|
||||||
|
```
|
||||||
|
|
||||||
## API Usage
|
## API Usage
|
||||||
|
|
||||||
### API Schema Files
|
### API Schema Files
|
||||||
|
|||||||
@@ -391,6 +391,10 @@ PRIVATE
|
|||||||
boxes/transfer_gift_box.h
|
boxes/transfer_gift_box.h
|
||||||
boxes/compose_ai_box.cpp
|
boxes/compose_ai_box.cpp
|
||||||
boxes/compose_ai_box.h
|
boxes/compose_ai_box.h
|
||||||
|
boxes/create_ai_tone_box.cpp
|
||||||
|
boxes/create_ai_tone_box.h
|
||||||
|
boxes/preview_ai_tone_box.cpp
|
||||||
|
boxes/preview_ai_tone_box.h
|
||||||
boxes/translate_box.cpp
|
boxes/translate_box.cpp
|
||||||
boxes/translate_box.h
|
boxes/translate_box.h
|
||||||
boxes/url_auth_box.cpp
|
boxes/url_auth_box.cpp
|
||||||
@@ -595,6 +599,8 @@ PRIVATE
|
|||||||
data/components/passkeys.h
|
data/components/passkeys.h
|
||||||
data/components/promo_suggestions.cpp
|
data/components/promo_suggestions.cpp
|
||||||
data/components/promo_suggestions.h
|
data/components/promo_suggestions.h
|
||||||
|
data/components/recent_inline_bots.cpp
|
||||||
|
data/components/recent_inline_bots.h
|
||||||
data/components/recent_peers.cpp
|
data/components/recent_peers.cpp
|
||||||
data/components/recent_peers.h
|
data/components/recent_peers.h
|
||||||
data/components/recent_shared_media_gifts.cpp
|
data/components/recent_shared_media_gifts.cpp
|
||||||
@@ -620,6 +626,8 @@ PRIVATE
|
|||||||
data/data_abstract_sparse_ids.h
|
data/data_abstract_sparse_ids.h
|
||||||
data/data_abstract_structure.cpp
|
data/data_abstract_structure.cpp
|
||||||
data/data_abstract_structure.h
|
data/data_abstract_structure.h
|
||||||
|
data/data_ai_compose_tones.cpp
|
||||||
|
data/data_ai_compose_tones.h
|
||||||
data/data_audio_msg_id.cpp
|
data/data_audio_msg_id.cpp
|
||||||
data/data_audio_msg_id.h
|
data/data_audio_msg_id.h
|
||||||
data/data_auto_download.cpp
|
data/data_auto_download.cpp
|
||||||
@@ -1802,6 +1810,8 @@ PRIVATE
|
|||||||
ui/chat/sponsored_message_bar.h
|
ui/chat/sponsored_message_bar.h
|
||||||
ui/controls/compose_ai_button_factory.cpp
|
ui/controls/compose_ai_button_factory.cpp
|
||||||
ui/controls/compose_ai_button_factory.h
|
ui/controls/compose_ai_button_factory.h
|
||||||
|
ui/controls/custom_emoji_toast_icon.cpp
|
||||||
|
ui/controls/custom_emoji_toast_icon.h
|
||||||
ui/controls/emoji_button_factory.cpp
|
ui/controls/emoji_button_factory.cpp
|
||||||
ui/controls/emoji_button_factory.h
|
ui/controls/emoji_button_factory.h
|
||||||
ui/controls/location_picker.cpp
|
ui/controls/location_picker.cpp
|
||||||
@@ -1812,6 +1822,8 @@ PRIVATE
|
|||||||
ui/controls/table_rows.h
|
ui/controls/table_rows.h
|
||||||
ui/controls/userpic_button.cpp
|
ui/controls/userpic_button.cpp
|
||||||
ui/controls/userpic_button.h
|
ui/controls/userpic_button.h
|
||||||
|
ui/controls/warning_tooltip.cpp
|
||||||
|
ui/controls/warning_tooltip.h
|
||||||
ui/effects/credits_graphics.cpp
|
ui/effects/credits_graphics.cpp
|
||||||
ui/effects/credits_graphics.h
|
ui/effects/credits_graphics.h
|
||||||
ui/effects/emoji_fly_animation.cpp
|
ui/effects/emoji_fly_animation.cpp
|
||||||
|
|||||||
Arquivo binário não exibido.
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="64px" height="64px" viewBox="86 88 1428 1428" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke="none" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M 958.553 1299.97 C 934.769 1305.39 906.43 1312.64 882.948 1315.92 C 779.669 1330.36 677.454 1318.71 583.292 1272.15 C 563.463 1262.34 547.132 1254.86 527.944 1242.75 C 435.019 1184.11 363.007 1097.59 322.199 995.568 C 270.377 868.326 271.778 723.336 327.271 597.348 C 339.315 570.005 347.862 549.146 364.195 523.501 C 440.059 406.442 559.396 324.393 695.857 295.468 C 793.87 273.039 896.365 280.647 989.993 317.299 C 1023.84 330.856 1064.3 352.862 1095.4 372.043 C 1131.57 394.356 1165.91 427.689 1193.83 459.345 C 1252.08 524.213 1295 608.113 1312.6 693.716 C 1330.75 781.951 1326.27 882.203 1296.79 967.484 C 1292.99 978.458 1283.28 1006.98 1276.51 1015.69 C 1273.57 1022.68 1268.1 1032.56 1264.61 1040.23 C 1265.07 1028.72 1265.56 1020.16 1264.79 1008.68 C 1254.79 973.218 1242.44 960.874 1212.87 941.129 C 1218.75 927.097 1222.13 912.02 1225.38 897.254 C 1243.43 814.997 1237.73 723.116 1206.19 644.774 C 1159.36 528.464 1073.88 445.913 958.19 399.511 C 767.873 323.175 536.448 406.112 432.131 581.107 C 420.192 601.135 411.979 624.034 402.565 645.324 C 360.668 747.59 363.066 865.571 405.417 967.139 C 441.894 1054.37 505.485 1127.53 586.779 1175.81 C 604.994 1186.4 634.545 1199.51 654.867 1207.68 C 712.277 1230.79 779.141 1236.79 840.406 1232.3 C 860.115 1230.86 879.512 1226.97 898.933 1225.24 C 900.582 1248.18 911.027 1266.69 928.362 1281.83 C 937.274 1289.62 947.496 1295.76 958.553 1299.97 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 1128.69 1164.62 C 1126.89 1138.94 1129.74 1110.05 1128.91 1083.97 C 1129.53 1062.87 1126.32 1039.59 1128.73 1018.82 C 1131.19 997.636 1150.52 980.952 1171.68 980.635 C 1227.95 979.793 1219.16 1038.37 1219.11 1074.53 L 1219 1164.55 C 1251.14 1165.51 1283.43 1164.42 1315.58 1164.78 C 1333.04 1164.98 1353.69 1163.41 1370.55 1166.9 C 1403.29 1173.68 1416.22 1221.5 1391.21 1244.31 C 1384.38 1250.42 1376.05 1254.61 1367.07 1256.44 C 1352.27 1259.6 1327.65 1258.18 1311.86 1258.37 C 1281.15 1258.74 1249.63 1257.71 1218.96 1258.75 L 1219.1 1356.51 C 1219.12 1377.54 1223.48 1414.32 1207.25 1428.23 C 1172.49 1458.02 1132.23 1439.9 1127.9 1395.53 C 1122.53 1372.19 1117.35 1310 1126.65 1288.84 L 1127.08 1289.83 L 1128.41 1287.22 C 1128.27 1278.01 1127.93 1267.97 1128.08 1258.84 C 1083.86 1257.02 1039.03 1259.47 994.752 1258.25 C 989.7 1258.11 984.635 1257.5 979.69 1256.56 C 956.617 1253.73 941.757 1229.5 943.148 1207.43 C 943.828 1195.73 949.179 1184.79 958 1177.08 C 976.352 1161.03 1014.24 1164.71 1037.73 1164.72 L 1128.69 1164.62 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 610.973 895.133 C 643.186 892.884 648.43 905.548 667.386 929.45 C 675.615 939.699 684.891 949.062 695.063 957.386 C 730.715 986.644 776.693 1000.22 822.52 995.027 C 865.303 990.469 904.474 969.266 933.418 937.876 C 940.853 929.813 946.51 922.315 953.186 913.72 C 962.084 902.256 970.413 896.331 984.914 894.729 C 1008.49 892.123 1030.8 911.274 1032.5 934.785 C 1034.05 956.385 1017.23 974.884 1003.86 990.297 C 965.325 1032.63 927.972 1059.61 871.965 1074.67 C 801.761 1093.54 733.018 1083.12 670.277 1046.06 C 635.22 1024.35 591.125 986.858 576.88 947.553 C 568.425 924.222 588.241 900.643 610.973 895.133 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 961.019 634.145 C 970.488 633.512 981.532 632.792 990.535 636.285 C 1051.66 660 1046.51 761.342 981.201 775.898 C 899.252 786.262 882.791 651.991 961.019 634.145 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 624.902 634.175 C 641.093 632.775 651.579 633.112 666.22 641.676 C 700.598 661.786 706.956 712.893 687.854 745.391 C 679.285 759.97 664.152 771.962 647.31 775.873 C 566.74 787.124 546.445 655.7 624.902 634.175 z" fill="#FFFFFF"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Depois Largura: | Altura: | Tamanho: 3.8 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="20px" height="20px" viewBox="-54 -126.5 1300 1300" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke="none" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M 293.256 246.66 C 303.57 231.316 346.963 193.508 362.121 182.857 C 382.415 168.598 439.629 137.351 462.441 132.86 C 470.936 131.188 479.696 129.931 487.895 127.07 C 500.501 122.671 512.121 116.238 525.425 114.002 C 555.645 108.924 640.89 107.622 669.916 114.173 C 683.279 117.189 695.566 124.379 708.747 128.119 C 718.506 130.889 728.902 131.327 738.592 134.243 C 757.682 139.988 804.12 165.473 821.912 177.449 C 867.145 207.897 919.822 260.531 948.249 307.519 C 956.862 321.754 962.746 336.715 969.862 351.626 C 975.201 362.815 983.166 373.039 985.555 385.407 C 987.846 397.263 988.468 408.699 992.437 420.254 C 996.946 433.384 1004.11 445.487 1007.96 458.861 C 1016.96 457.67 1040.85 458.039 1050.9 458.099 C 1071.24 458.22 1096.29 457.055 1116.12 458.887 C 1113.9 472.586 1109.75 486.906 1099.93 497.379 C 1091.13 509.049 1081.18 520.048 1071.87 531.335 C 1040.42 569.475 1005.46 609.384 978.799 651.023 C 958.481 658.332 946.002 656.045 925.372 650.899 C 923.434 638.943 906.187 620.514 898.648 610.127 C 870.778 571.724 838.388 534.317 807.162 498.633 C 796.457 486.112 794.628 475.55 791.043 460.243 C 806.428 457.256 856.509 458.12 874.563 458.212 C 883.566 458.092 893.22 457.323 902.256 456.764 C 903.74 449.659 902.471 448.74 901.714 441.787 C 900.687 415.298 900.595 400.329 884.444 377.489 C 881.097 372.755 877.002 361.881 873.678 356.511 C 843.98 308.538 807.536 273.71 760.358 243.338 C 755.347 240.111 745.152 237.841 739.751 234.55 C 722.189 218.469 706.178 219.541 684.402 213.641 C 674.021 210.828 662.702 201.355 650.37 200.448 C 611.58 197.595 558.962 193.272 522.048 205.721 C 504.105 211.771 495.028 218.663 475.513 221.182 C 456.615 227.903 431.787 244.276 414.511 254.977 C 394.757 269.256 370.747 294.197 349.204 304.094 C 335.659 310.316 317.151 305.429 305.81 295.04 C 301.065 290.655 296.833 285.745 293.196 280.405 C 292.284 268.047 292.218 259.075 293.256 246.66 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 186.21 590.217 C 165.947 586.351 91.3706 591.299 77.8568 585.809 C 75.5685 582.498 76.2555 583.019 75.838 578.51 C 77.5916 574.02 91.758 559.627 96.0945 554.009 C 111.556 535.17 124.332 514.736 139.966 496.003 C 166.731 463.932 193.103 430.125 220.887 399.027 C 225.297 394.866 236.541 390.072 242.554 391.758 C 263.746 397.702 280.201 429.811 293.903 445.212 C 325.984 481.268 354.299 517.854 382.164 557.04 C 386.544 563.2 392.893 568.595 397.499 574.66 C 400.763 579.027 401.107 578.847 400.448 583.758 C 398.359 586.15 396.178 586.68 392.934 587.013 C 358.35 590.56 322.483 586.695 287.998 590.332 C 286.888 594.013 288.348 594.999 287.802 597.634 C 289.784 611.55 290.883 626.521 294.274 640.12 C 295.861 649.754 305.692 659.151 308.895 667.473 C 316.255 686.601 325.546 698.041 337.263 714.289 C 376.585 768.818 438.026 814.138 503.435 831.266 C 515.296 834.373 527.363 841.503 539.704 843.839 C 561.158 848.014 583.548 847.455 605.323 847.395 C 618.707 846.567 639.142 847.471 651.643 843.722 C 674.039 837.007 696.296 828.486 718.987 822.786 C 728.912 820.292 758.859 801.739 768.491 796.066 C 792.188 782.109 823.117 749.603 849.588 741.891 C 853.848 742.617 857.997 743.565 861.98 745.286 C 877.229 751.876 890.039 765.465 896.003 780.961 C 898.307 786.949 898.876 792.855 896.197 798.806 C 887.925 817.181 839.079 856.675 821.137 868.231 C 804.479 878.961 785.773 887.555 768.037 896.353 C 758.813 900.928 749.665 906.4 740.065 910.114 C 729.664 914.14 717.667 914.007 706.836 916.831 C 697.251 919.33 688.426 924.652 679.192 928.197 C 650.723 939.126 543.279 936.774 512.519 929.224 C 502.551 926.777 493.919 920.224 484.247 916.818 C 473.53 913.044 461.504 913.434 450.587 909.918 C 441.609 907.026 434.012 901.262 425.66 897.078 C 417.434 892.958 408.696 889.753 400.299 885.987 C 347.46 862.286 271.215 782.467 241.089 732.394 C 233.035 719.007 229.024 704.264 222.018 690.519 C 218.612 683.836 212.373 678.567 209.546 671.603 C 204.416 658.964 206.387 644.372 203.035 631.286 C 199.386 617.039 191.425 603.959 186.21 590.217 z" fill="#FFFFFF"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Depois Largura: | Altura: | Tamanho: 4.1 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="75 74 885 885" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke="none" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M 485.872 154.421 C 489.97 154.046 494.423 154.058 498.509 153.897 C 550.78 151.838 601.802 157.649 650.431 177.877 C 667.745 185.111 693.731 190.279 692.097 214.216 C 691.384 224.651 681.86 237.281 671.016 236.327 C 653.387 234.776 634.559 225.215 617.678 219.534 C 596.3 212.246 574.213 207.234 551.783 204.582 C 469.584 195.653 387.24 220.083 323.211 272.396 C 261.614 321.783 217.09 396.821 208.415 475.761 C 206.956 482.079 206.614 495.229 206.281 501.694 C 204.396 538.381 211.27 573.459 219.719 608.938 C 224.462 628.854 221.439 642.528 198.953 647.477 C 194.156 648.533 186.809 645.768 183.151 642.302 C 163.893 619.135 161.813 578.405 157.609 549.212 C 156.007 530.135 155.785 487.372 158.155 469.629 C 167.611 398.838 196.617 332.034 243.259 278.161 C 304.622 206.501 391.836 162.019 485.872 154.421 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 835.576 387.119 C 846.257 386.476 859.055 391.76 862.457 402.727 C 952.916 694.355 683.31 955.692 392.557 856.088 C 373.491 849.469 359.657 835.857 369.781 815.187 C 380.661 792.975 424.62 813.919 442.89 818.575 C 668.956 876.181 881.625 663.172 819.977 436.576 C 814.533 416.562 809.795 393.853 835.576 387.119 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 506.655 356.237 C 507.253 356.193 507.852 356.164 508.452 356.149 C 514.915 355.965 524.437 356.269 529.019 361.243 C 541.438 374.727 538.608 402.515 538.558 419.699 C 538.336 440.88 538.437 462.063 538.862 483.24 C 564.166 484.738 591.325 483.308 616.786 483.757 C 628.64 484.306 640.998 482.703 652.742 484.688 C 679.097 489.144 678.77 502.3 675.383 523.737 C 658.032 540.009 627.072 535.994 604.59 535.956 C 582.68 535.868 560.769 535.934 538.86 536.153 C 538.481 557.735 538.358 579.321 538.491 600.907 C 538.51 618.768 539.533 641.312 536.012 658.614 C 534.681 665.156 528.901 669.767 523.287 672.977 C 479.344 684.02 486.343 635.046 486.378 606.695 L 486.523 536.072 C 464.79 535.891 443.057 535.869 421.324 536.008 C 401.889 536.033 361.672 541.339 359.068 513.56 C 355.669 477.301 396.449 483.784 422.364 483.781 C 443.752 483.956 465.14 483.733 486.52 483.113 C 486.442 461.785 482.477 380.51 492.947 364.316 C 496.294 359.139 500.844 357.43 506.655 356.237 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 273.483 635.716 C 279.402 635.318 282.607 638.76 287.122 642.37 C 290.955 650.759 294.158 665.727 296.611 675.355 C 300.185 689.387 303.286 700.322 308.344 714.042 C 328.986 722.065 347.188 725.311 368.296 730.924 C 378.676 733.685 382.203 737.899 386.734 746.651 C 377.464 767.083 349.327 764.448 330.321 771.075 C 322.237 773.894 315.391 775.635 307.166 778.987 C 302.219 793.4 299.145 808.432 295.467 823.202 C 291.842 837.76 290.715 847.938 277.759 857.105 C 273.015 857.333 267.834 852.625 264.059 849.727 C 257.052 830.72 252.102 802.131 244.156 778.901 C 227.676 772.036 210.016 768.699 192.777 764.177 C 183.037 761.623 168.221 758.973 166.7 746.721 C 166.027 741.295 170.305 736.719 173.617 733.049 C 196.08 726.576 219.674 721.849 242.955 715.166 C 254.629 690.133 254.04 665.986 265.651 642.555 C 266.876 640.083 271.122 637.274 273.483 635.716 z" fill="#FFFFFF"></path>
|
||||||
|
<path d="M 778.555 201.873 C 779.561 201.996 780.856 202.153 782.44 202.346 C 793.311 209.755 797.518 249.625 804.521 257.463 C 819.052 273.725 842.541 270.313 863.58 283.156 L 863.691 283.532 C 863.946 284.417 865.096 288.479 865.225 289.089 C 869.022 307.019 825.422 305.382 811.604 315.971 C 793.451 329.882 798.069 353.434 786.988 372.313 C 784.987 375.721 782.996 376.704 779.456 377.455 C 778.406 377.433 773.501 377.273 772.932 376.82 C 761.115 367.408 760.692 337.63 752.125 324.294 C 746.888 313.426 728.414 309.537 716.752 306.692 C 680.1 297.753 681.643 280.476 717.76 272.848 C 723.122 271.716 733.685 268.125 739.553 266.177 C 766.027 257.386 754.066 210.621 778.555 201.873 z" fill="#FFFFFF"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Depois Largura: | Altura: | Tamanho: 4.0 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>Filled / filled_poll_country</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="filled_poll_country_gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0BC4BA"/>
|
||||||
|
<stop offset="100%" stop-color="#1196E4"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="Filled-/-filled_poll_country" stroke="none" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M36,9.33333333 C24.8,9.33333333 14.6666667,17.92 14.6666667,31.2 C14.6666667,39.68 21.2,51.7866667 34.24,63.28 C35.2533333,64.16 36.7733333,64.16 37.7866667,63.28 C50.8,51.7866667 57.3333333,39.68 57.3333333,31.2 C57.3333333,17.92 47.2,9.33333333 36,9.33333333 Z M36,37 C32.15,37 29,33.85 29,30 C29,26.15 32.15,23 36,23 C39.85,23 43,26.15 43,30 C43,33.85 39.85,37 36,37 Z" id="Shape" fill="url(#filled_poll_country_gradient)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Depois Largura: | Altura: | Tamanho: 988 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>Filled / filled_poll_subscribers</title>
|
||||||
|
<g id="Filled-/-filled_poll_subscribers" stroke="none" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M37.8519595,55.9901058 L12.721202,55.9901058 C10.9567905,55.9901058 9.72882062,55.6937689 9.03729237,55.1010952 C8.34576412,54.5084214 8,53.6705918 8,52.5876063 C8,51.1691958 8.40371843,49.6771286 9.21115528,48.1114047 C10.0185921,46.5456808 11.1756044,45.083174 12.6821921,43.7238842 C14.1887799,42.3645945 16.0058771,41.2577897 18.1334839,40.4034698 C20.2610906,39.5491498 22.6449518,39.1219899 25.2850674,39.1219899 C27.9251831,39.1219899 30.3074749,39.5491498 32.4319429,40.4034698 C34.556411,41.2577897 36.375526,42.3645945 37.8892879,43.7238842 C39.4030498,45.083174 40.5621359,46.5456808 41.3665462,48.1114047 C42.1709564,49.6771286 42.5731615,51.1691958 42.5731615,52.5876063 C42.5731615,53.6705918 42.2273413,54.5084214 41.535701,55.1010952 C40.8440606,55.6937689 39.6161468,55.9901058 37.8519595,55.9901058 Z M24.5707386,35.2383399 C22.9441021,35.2383399 21.4612492,34.8051733 20.1221797,33.9388401 C18.7831101,33.072507 17.7140307,31.905829 16.9149414,30.4388063 C16.115852,28.9717836 15.7163074,27.3235967 15.7163074,25.4942457 C15.7163074,23.7307072 16.1196783,22.1312815 16.9264201,20.6959685 C17.733162,19.2606555 18.8077417,18.1191658 20.1501591,17.2714995 C21.4925766,16.4238332 22.9661031,16 24.5707386,16 C26.1753741,16 27.6488408,16.4282606 28.9911387,17.2847817 C30.3334366,18.1413028 31.4080163,19.2909891 32.2148777,20.7338406 C33.0217391,22.1766922 33.4251698,23.7755795 33.4251698,25.5305024 C33.4184739,27.3356823 33.0155813,28.9717836 32.2164919,30.4388063 C31.4174026,31.905829 30.3461111,33.072507 29.0026175,33.9388401 C27.6591239,34.8051733 26.1818309,35.2383399 24.5707386,35.2383399 Z M60.001153,56 L45.0075786,56 C45.5691861,55.1381067 45.8683177,54.1102111 45.9049735,52.916313 C45.9416293,51.7224149 45.7426006,50.4576696 45.3078872,49.1220771 C44.8731739,47.7864846 44.2320334,46.4733066 43.3844657,45.1825433 C42.536898,43.89178 41.5086855,42.7183421 40.2998283,41.6622297 C41.3965882,40.9205324 42.6701257,40.3033064 44.1204405,39.8105517 C45.5707554,39.317797 47.1795759,39.0714196 48.9469019,39.0714196 C51.193556,39.0714196 53.2382668,39.4701796 55.0810343,40.2676996 C56.9238019,41.0652196 58.5113238,42.1123539 59.8436002,43.4091026 C61.1758767,44.7058514 62.2016791,46.1210249 62.9210074,47.6546232 C63.6403358,49.1882216 64,50.6843808 64,52.1431009 C64,53.3641164 63.7023256,54.3121868 63.1069769,54.9873121 C62.5116281,55.6624374 61.4763535,56 60.001153,56 Z M48.8634231,35.7209561 C47.4577528,35.7209561 46.1711747,35.3756832 45.0036888,34.6851375 C43.8362029,33.9945917 42.9026566,33.061349 42.2030498,31.8854093 C41.5034431,30.7094695 41.1536398,29.3936836 41.1536398,27.9380514 C41.1536398,26.520679 41.5051171,25.237106 42.2080718,24.0873325 C42.9110265,22.937559 43.8462468,22.0239409 45.0137327,21.3464782 C46.1812186,20.6690156 47.4644487,20.3302842 48.8634231,20.3302842 C50.2626365,20.3302842 51.5476004,20.6730284 52.7183147,21.3585169 C53.889029,22.0440053 54.8264016,22.9636702 55.5304324,24.1175116 C56.2344632,25.271353 56.5832503,26.5559704 56.5769671,27.9713638 C56.5769671,29.4127035 56.2263922,30.7193643 55.5255898,31.8913461 C54.8247874,33.0633279 53.8906432,33.9945917 52.7231573,34.6851375 C51.5556714,35.3756832 50.2690933,35.7209561 48.8634231,35.7209561 Z" id="Shape" fill="#FFFFFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Depois Largura: | Altura: | Tamanho: 3.5 KiB |
@@ -3317,6 +3317,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_credits_box_history_entry_gift_sold_to" = "To";
|
"lng_credits_box_history_entry_gift_sold_to" = "To";
|
||||||
"lng_credits_box_history_entry_gift_full_price" = "Full Price";
|
"lng_credits_box_history_entry_gift_full_price" = "Full Price";
|
||||||
"lng_credits_box_history_entry_gift_bought_from" = "From";
|
"lng_credits_box_history_entry_gift_bought_from" = "From";
|
||||||
|
"lng_credits_box_history_entry_gift_offer" = "Gift Offer";
|
||||||
|
|
||||||
"lng_credits_subscription_section" = "My subscriptions";
|
"lng_credits_subscription_section" = "My subscriptions";
|
||||||
"lng_credits_box_subscription_title" = "Subscription";
|
"lng_credits_box_subscription_title" = "Subscription";
|
||||||
@@ -3449,11 +3450,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_business_limit_reached#one" = "Limit of {count} message reached.";
|
"lng_business_limit_reached#one" = "Limit of {count} message reached.";
|
||||||
"lng_business_limit_reached#other" = "Limit of {count} messages reached.";
|
"lng_business_limit_reached#other" = "Limit of {count} messages reached.";
|
||||||
|
|
||||||
"lng_chatbots_title" = "Chatbots";
|
|
||||||
"lng_chatbots_about" = "Add a bot to your account to help you automatically process and respond to the messages you receive. {link}";
|
|
||||||
"lng_chatbots_about_link" = "Learn more...";
|
|
||||||
"lng_chatbots_placeholder" = "Enter bot URL or username";
|
"lng_chatbots_placeholder" = "Enter bot URL or username";
|
||||||
"lng_chatbots_add_about" = "Enter the link to the Telegram bot that you want to automatically process your chats.";
|
|
||||||
"lng_chatbots_access_title" = "Chats accessible for the bot";
|
"lng_chatbots_access_title" = "Chats accessible for the bot";
|
||||||
"lng_chatbots_all_except" = "All 1-to-1 Chats Except...";
|
"lng_chatbots_all_except" = "All 1-to-1 Chats Except...";
|
||||||
"lng_chatbots_selected" = "Only Selected Chats";
|
"lng_chatbots_selected" = "Only Selected Chats";
|
||||||
@@ -3491,11 +3488,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
"lng_chatbots_manage_stories" = "Manage Stories";
|
"lng_chatbots_manage_stories" = "Manage Stories";
|
||||||
|
|
||||||
"lng_chatbots_remove" = "Remove Bot";
|
|
||||||
"lng_chatbots_not_found" = "Chatbot not found.";
|
"lng_chatbots_not_found" = "Chatbot not found.";
|
||||||
"lng_chatbots_not_supported" = "This bot doesn't support Telegram Business yet.";
|
"lng_chatbots_not_supported" = "This bot doesn't support Telegram Business yet.";
|
||||||
"lng_chatbots_add" = "Add";
|
"lng_chatbots_add" = "Add";
|
||||||
"lng_chatbots_info_url" = "https://telegram.org/blog/telegram-business#chatbots-for-business";
|
"lng_chatbots_remove_bot" = "Remove Bot";
|
||||||
|
"lng_chatbots_leave_without_added_title" = "No Bot Added";
|
||||||
|
"lng_chatbots_leave_without_added_text" = "You haven't added a bot to manage your account. Leave anyway?";
|
||||||
|
"lng_chatbots_added_success" = "{bot} now manages your account.";
|
||||||
|
|
||||||
|
"lng_chat_automation_title" = "Chat Automation";
|
||||||
|
"lng_chat_automation_about" = "Add a bot to answer messages on your behalf.";
|
||||||
|
"lng_chat_automation_add_about" = "Choose a bot to manage your chats automatically.";
|
||||||
|
"lng_settings_chat_automation_label" = "Chat automation";
|
||||||
|
"lng_settings_chat_automation_off" = "Off";
|
||||||
"lng_chatbot_status_can_reply" = "bot manages this chat";
|
"lng_chatbot_status_can_reply" = "bot manages this chat";
|
||||||
"lng_chatbot_status_paused" = "bot paused";
|
"lng_chatbot_status_paused" = "bot paused";
|
||||||
"lng_chatbot_status_views" = "bot has access to this chat";
|
"lng_chatbot_status_views" = "bot has access to this chat";
|
||||||
@@ -4501,6 +4506,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
"lng_inline_bot_no_results" = "No results.";
|
"lng_inline_bot_no_results" = "No results.";
|
||||||
"lng_inline_bot_via" = "via {inline_bot}";
|
"lng_inline_bot_via" = "via {inline_bot}";
|
||||||
|
"lng_guest_chat_for" = "for {user}";
|
||||||
|
|
||||||
"lng_box_remove" = "Remove";
|
"lng_box_remove" = "Remove";
|
||||||
|
|
||||||
@@ -4528,6 +4534,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_masks_count#other" = "{count} masks";
|
"lng_masks_count#other" = "{count} masks";
|
||||||
"lng_custom_emoji_count#one" = "{count} emoji";
|
"lng_custom_emoji_count#one" = "{count} emoji";
|
||||||
"lng_custom_emoji_count#other" = "{count} emoji";
|
"lng_custom_emoji_count#other" = "{count} emoji";
|
||||||
|
"lng_search_back_to_results" = "Back to search";
|
||||||
|
"lng_search_results_header" = "Search Result";
|
||||||
"lng_stickers_attached_sets" = "Sets of attached stickers";
|
"lng_stickers_attached_sets" = "Sets of attached stickers";
|
||||||
"lng_custom_emoji_used_sets" = "Sets of used emoji";
|
"lng_custom_emoji_used_sets" = "Sets of used emoji";
|
||||||
"lng_custom_emoji_remove_pack_button" = "Remove Emoji";
|
"lng_custom_emoji_remove_pack_button" = "Remove Emoji";
|
||||||
@@ -4618,10 +4626,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_in_dlg_audio_count#other" = "{count} audio";
|
"lng_in_dlg_audio_count#other" = "{count} audio";
|
||||||
|
|
||||||
"lng_ban_user" = "Ban User";
|
"lng_ban_user" = "Ban User";
|
||||||
|
"lng_ban_specific_user" = "Ban {user}";
|
||||||
"lng_ban_users" = "Ban users";
|
"lng_ban_users" = "Ban users";
|
||||||
"lng_restrict_users" = "Restrict users";
|
"lng_restrict_users" = "Restrict users";
|
||||||
"lng_delete_all_from_user" = "Delete all from {user}";
|
"lng_delete_all_from_user" = "Delete all from {user}";
|
||||||
"lng_delete_all_from_users" = "Delete all from users";
|
|
||||||
"lng_restrict_user#one" = "Restrict user";
|
"lng_restrict_user#one" = "Restrict user";
|
||||||
"lng_restrict_user#other" = "Restrict users";
|
"lng_restrict_user#other" = "Restrict users";
|
||||||
"lng_restrict_user_part" = "Partially restrict this user {emoji}";
|
"lng_restrict_user_part" = "Partially restrict this user {emoji}";
|
||||||
@@ -5595,6 +5603,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_selected_delete_sure_this" = "Do you want to delete this message?";
|
"lng_selected_delete_sure_this" = "Do you want to delete this message?";
|
||||||
"lng_selected_delete_sure#one" = "Do you want to delete {count} message?";
|
"lng_selected_delete_sure#one" = "Do you want to delete {count} message?";
|
||||||
"lng_selected_delete_sure#other" = "Do you want to delete {count} messages?";
|
"lng_selected_delete_sure#other" = "Do you want to delete {count} messages?";
|
||||||
|
"lng_delete_title_message_one" = "Delete this message";
|
||||||
|
"lng_delete_title_message_many#one" = "Delete {count} message";
|
||||||
|
"lng_delete_title_message_many#other" = "Delete {count} messages";
|
||||||
|
"lng_delete_title_reaction_this" = "Delete this reaction";
|
||||||
|
"lng_delete_title_reaction_all" = "Delete all reactions";
|
||||||
|
"lng_delete_label_also_this_reaction" = "Also delete this reaction.";
|
||||||
|
"lng_delete_label_also_some_reactions" = "Also delete all reactions from some participants.";
|
||||||
|
"lng_delete_label_also_all_reactions" = "Also delete all reactions.";
|
||||||
|
"lng_delete_sub_messages" = "Delete all messages";
|
||||||
|
"lng_delete_sub_reactions" = "Delete all reactions";
|
||||||
|
"lng_context_delete_this_reaction" = "Delete this reaction";
|
||||||
"lng_selected_remove_saved_music" = "Do you want to remove this file from your profile?";
|
"lng_selected_remove_saved_music" = "Do you want to remove this file from your profile?";
|
||||||
"lng_saved_music_added" = "Audio added to your Profile.";
|
"lng_saved_music_added" = "Audio added to your Profile.";
|
||||||
"lng_saved_music_removed" = "Audio removed from your Profile.";
|
"lng_saved_music_removed" = "Audio removed from your Profile.";
|
||||||
@@ -6368,7 +6387,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_rights_chat_send_media" = "Send media";
|
"lng_rights_chat_send_media" = "Send media";
|
||||||
"lng_rights_chat_send_stickers" = "Send stickers & GIFs";
|
"lng_rights_chat_send_stickers" = "Send stickers & GIFs";
|
||||||
"lng_rights_chat_send_links" = "Embed links";
|
"lng_rights_chat_send_links" = "Embed links";
|
||||||
"lng_rights_chat_send_polls" = "Send polls";
|
"lng_rights_chat_send_reactions" = "Reactions";
|
||||||
|
"lng_rights_chat_send_polls" = "Polls";
|
||||||
"lng_rights_chat_add_members" = "Add members";
|
"lng_rights_chat_add_members" = "Add members";
|
||||||
"lng_rights_chat_photos" = "Photos";
|
"lng_rights_chat_photos" = "Photos";
|
||||||
"lng_rights_chat_videos" = "Video files";
|
"lng_rights_chat_videos" = "Video files";
|
||||||
@@ -6419,6 +6439,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_restricted_send_gifs" = "The admins of this group have restricted your ability to send GIFs.";
|
"lng_restricted_send_gifs" = "The admins of this group have restricted your ability to send GIFs.";
|
||||||
"lng_restricted_send_inline" = "The admins of this group have restricted your ability to send inline content.";
|
"lng_restricted_send_inline" = "The admins of this group have restricted your ability to send inline content.";
|
||||||
"lng_restricted_send_polls" = "The admins of this group have restricted your ability to send polls.";
|
"lng_restricted_send_polls" = "The admins of this group have restricted your ability to send polls.";
|
||||||
|
"lng_restricted_send_reactions_click" = "You cannot send reactions in this chat.";
|
||||||
"lng_restricted_boost_group" = "Boost this group to send messages";
|
"lng_restricted_boost_group" = "Boost this group to send messages";
|
||||||
|
|
||||||
"lng_restricted_send_message_until" = "The admins of this group have restricted you from sending messages until {date}, {time}.";
|
"lng_restricted_send_message_until" = "The admins of this group have restricted you from sending messages until {date}, {time}.";
|
||||||
@@ -6550,6 +6571,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_admin_log_edited_message" = "{from} edited message:";
|
"lng_admin_log_edited_message" = "{from} edited message:";
|
||||||
"lng_admin_log_previous_message" = "Original message";
|
"lng_admin_log_previous_message" = "Original message";
|
||||||
"lng_admin_log_deleted_message" = "{from} deleted message:";
|
"lng_admin_log_deleted_message" = "{from} deleted message:";
|
||||||
|
"lng_admin_log_deleted_messages_collapsed#one" = "{from} deleted {count} message from {names} ({link}).";
|
||||||
|
"lng_admin_log_deleted_messages_collapsed#other" = "{from} deleted {count} messages from {names} ({link}).";
|
||||||
|
"lng_admin_log_show_all" = "Show all";
|
||||||
|
"lng_admin_log_hide_all" = "Hide all";
|
||||||
|
"lng_admin_log_expand_more#one" = "Show {count} More Message";
|
||||||
|
"lng_admin_log_expand_more#other" = "Show {count} More Messages";
|
||||||
"lng_admin_log_sent_message" = "{from} sent this message:";
|
"lng_admin_log_sent_message" = "{from} sent this message:";
|
||||||
"lng_admin_log_participant_joined" = "{from} joined the group";
|
"lng_admin_log_participant_joined" = "{from} joined the group";
|
||||||
"lng_admin_log_participant_joined_channel" = "{from} joined the channel";
|
"lng_admin_log_participant_joined_channel" = "{from} joined the channel";
|
||||||
@@ -6662,6 +6689,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_admin_log_banned_send_stickers" = "Send stickers & GIFs";
|
"lng_admin_log_banned_send_stickers" = "Send stickers & GIFs";
|
||||||
"lng_admin_log_banned_embed_links" = "Embed links";
|
"lng_admin_log_banned_embed_links" = "Embed links";
|
||||||
"lng_admin_log_banned_send_polls" = "Send polls";
|
"lng_admin_log_banned_send_polls" = "Send polls";
|
||||||
|
"lng_admin_log_banned_send_reactions" = "Send reactions";
|
||||||
"lng_admin_log_admin_change_info" = "Change info";
|
"lng_admin_log_admin_change_info" = "Change info";
|
||||||
"lng_admin_log_admin_post_messages" = "Post messages";
|
"lng_admin_log_admin_post_messages" = "Post messages";
|
||||||
"lng_admin_log_admin_edit_messages" = "Edit messages";
|
"lng_admin_log_admin_edit_messages" = "Edit messages";
|
||||||
@@ -6990,7 +7018,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_polls_answers_count#other" = "{count} answers";
|
"lng_polls_answers_count#other" = "{count} answers";
|
||||||
"lng_polls_answers_none" = "No answers";
|
"lng_polls_answers_none" = "No answers";
|
||||||
"lng_polls_submit_votes" = "Vote";
|
"lng_polls_submit_votes" = "Vote";
|
||||||
|
"lng_polls_view_stats" = "View Stats";
|
||||||
"lng_polls_view_results" = "View results";
|
"lng_polls_view_results" = "View results";
|
||||||
|
"lng_polls_stats_title" = "Poll Stats";
|
||||||
"lng_polls_view_votes#one" = "View Votes ({count})";
|
"lng_polls_view_votes#one" = "View Votes ({count})";
|
||||||
"lng_polls_view_votes#other" = "View Votes ({count})";
|
"lng_polls_view_votes#other" = "View Votes ({count})";
|
||||||
"lng_polls_admin_votes#one" = "{count} vote {arrow}";
|
"lng_polls_admin_votes#one" = "{count} vote {arrow}";
|
||||||
@@ -7038,6 +7068,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_polls_create_poll_ends" = "Poll ends";
|
"lng_polls_create_poll_ends" = "Poll ends";
|
||||||
"lng_polls_create_hide_results" = "Hide results";
|
"lng_polls_create_hide_results" = "Hide results";
|
||||||
"lng_polls_create_hide_results_about" = "If you switch this on, results will appear only after the poll closes.";
|
"lng_polls_create_hide_results_about" = "If you switch this on, results will appear only after the poll closes.";
|
||||||
|
"lng_polls_create_restrict_to_subscribers" = "Restrict to Subscribers";
|
||||||
|
"lng_polls_create_restrict_to_subscribers_about" = "Only subscribers who joined 24+ hours ago can vote.";
|
||||||
|
"lng_polls_create_limit_by_country" = "Limit by Country";
|
||||||
|
"lng_polls_create_limit_by_country_about" = "Only users from selected countries can vote.";
|
||||||
|
"lng_polls_create_allowed_countries" = "Allowed Countries";
|
||||||
|
"lng_polls_create_countries_count#one" = "{count} country";
|
||||||
|
"lng_polls_create_countries_count#other" = "{count} countries";
|
||||||
|
"lng_polls_create_choose_country" = "Please choose at least one country.";
|
||||||
|
"lng_polls_create_countries_limit#one" = "You can choose up to {count} country.";
|
||||||
|
"lng_polls_create_countries_limit#other" = "You can choose up to {count} countries.";
|
||||||
"lng_polls_create_duration_custom" = "Custom";
|
"lng_polls_create_duration_custom" = "Custom";
|
||||||
"lng_polls_create_deadline_title" = "Deadline";
|
"lng_polls_create_deadline_title" = "Deadline";
|
||||||
"lng_polls_create_deadline_button" = "Set Deadline";
|
"lng_polls_create_deadline_button" = "Set Deadline";
|
||||||
@@ -7055,6 +7095,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes.";
|
"lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes.";
|
||||||
"lng_polls_media_uploading_toast_title" = "Please wait";
|
"lng_polls_media_uploading_toast_title" = "Please wait";
|
||||||
"lng_polls_media_uploading_toast" = "Poll media is still uploading...";
|
"lng_polls_media_uploading_toast" = "Poll media is still uploading...";
|
||||||
|
"lng_polls_vote_restricted_subscribers_channel" = "Only subscribers of {channel} can vote.";
|
||||||
|
"lng_polls_vote_restricted_subscribers" = "Only subscribers can vote.";
|
||||||
|
"lng_polls_vote_restricted_subscribers_recent" = "Only subscribers who joined more than 24 hours ago can vote.";
|
||||||
|
"lng_polls_vote_restricted_countries_list" = "Only users from {countries} can vote.";
|
||||||
|
"lng_polls_vote_restricted_countries" = "Only users from selected countries can vote.";
|
||||||
"lng_polls_ends_toast" = "Results will appear after the poll ends.";
|
"lng_polls_ends_toast" = "Results will appear after the poll ends.";
|
||||||
|
|
||||||
"lng_polls_poll_results_title" = "Poll results";
|
"lng_polls_poll_results_title" = "Poll results";
|
||||||
@@ -7308,6 +7353,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_view_button_iv" = "Instant View";
|
"lng_view_button_iv" = "Instant View";
|
||||||
"lng_view_button_stickerset" = "View stickers";
|
"lng_view_button_stickerset" = "View stickers";
|
||||||
"lng_view_button_emojipack" = "View emoji";
|
"lng_view_button_emojipack" = "View emoji";
|
||||||
|
"lng_view_button_style" = "View Style";
|
||||||
"lng_view_button_collectible" = "View collectible";
|
"lng_view_button_collectible" = "View collectible";
|
||||||
"lng_view_button_call" = "Join call";
|
"lng_view_button_call" = "Join call";
|
||||||
"lng_view_button_storyalbum" = "View Album";
|
"lng_view_button_storyalbum" = "View Album";
|
||||||
@@ -7933,11 +7979,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_ai_compose_tab_fix" = "Fix";
|
"lng_ai_compose_tab_fix" = "Fix";
|
||||||
"lng_ai_compose_original" = "Original";
|
"lng_ai_compose_original" = "Original";
|
||||||
"lng_ai_compose_result" = "Result";
|
"lng_ai_compose_result" = "Result";
|
||||||
|
"lng_ai_compose_before" = "Before";
|
||||||
|
"lng_ai_compose_after" = "After";
|
||||||
"lng_ai_compose_to_language" = "To {language}";
|
"lng_ai_compose_to_language" = "To {language}";
|
||||||
"lng_ai_compose_name_style" = "{name} ({style})";
|
"lng_ai_compose_name_style" = "{name} ({style})";
|
||||||
"lng_ai_compose_style_neutral" = "Neutral";
|
"lng_ai_compose_style_neutral" = "Neutral";
|
||||||
"lng_ai_compose_emojify" = "emojify";
|
"lng_ai_compose_emojify" = "emojify";
|
||||||
"lng_ai_compose_error" = "AI request failed.";
|
"lng_ai_compose_error" = "AI request failed.";
|
||||||
|
"lng_ai_compose_error_too_long" = "Sorry, this text is too long.";
|
||||||
|
"lng_ai_compose_tone_invalid" = "This style was deleted or the link is invalid.";
|
||||||
"lng_ai_compose_tooltip" = "Rewrite, translate, or correct your text using AI.";
|
"lng_ai_compose_tooltip" = "Rewrite, translate, or correct your text using AI.";
|
||||||
"lng_ai_compose_flood_title" = "Daily limit reached";
|
"lng_ai_compose_flood_title" = "Daily limit reached";
|
||||||
"lng_ai_compose_flood_text" = "Get {link} for **50x** more AI text transformations per day.";
|
"lng_ai_compose_flood_text" = "Get {link} for **50x** more AI text transformations per day.";
|
||||||
@@ -7946,6 +7996,49 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_ai_compose_select_style" = "Select Style";
|
"lng_ai_compose_select_style" = "Select Style";
|
||||||
"lng_ai_compose_apply_style" = "Apply Style";
|
"lng_ai_compose_apply_style" = "Apply Style";
|
||||||
"lng_ai_compose_style_tooltip" = "Choose Style";
|
"lng_ai_compose_style_tooltip" = "Choose Style";
|
||||||
|
"lng_ai_compose_create_tone_title" = "New Style";
|
||||||
|
"lng_ai_compose_edit_tone_title" = "Edit Style";
|
||||||
|
"lng_ai_compose_tone_icon_title" = "Style Icon";
|
||||||
|
"lng_ai_compose_tone_name" = "Name";
|
||||||
|
"lng_ai_compose_tone_prompt" = "Prompt";
|
||||||
|
"lng_ai_compose_tone_save" = "Save";
|
||||||
|
"lng_ai_compose_tone_create" = "Create";
|
||||||
|
"lng_ai_compose_tone_author" = "Add a link to my account";
|
||||||
|
"lng_ai_compose_tone_name_placeholder" = "Style Name (for example, \"Pirate\")";
|
||||||
|
"lng_ai_compose_tone_prompt_placeholder" = "Instructions (for example \"write in bold, nautical tone, light slang (aye, matey), vivid sea imagery, playful swagger, rhythmic phrasing, and adventurous mood\")";
|
||||||
|
"lng_ai_compose_tone_edit" = "Edit Style";
|
||||||
|
"lng_ai_compose_tone_share" = "Share Style";
|
||||||
|
"lng_ai_compose_tone_remove" = "Remove Style";
|
||||||
|
"lng_ai_compose_tone_delete" = "Delete Style";
|
||||||
|
"lng_ai_compose_tone_remove_sure" = "Are you sure you want to remove this style?";
|
||||||
|
"lng_ai_compose_tone_delete_sure" = "Are you sure you want to delete this style? It will be removed for everyone who installed it.";
|
||||||
|
"lng_ai_compose_tone_link_copied" = "Style link copied.";
|
||||||
|
"lng_ai_compose_author" = "Style by {user}";
|
||||||
|
"lng_ai_compose_tone_warn_icon" = "Please choose an icon.";
|
||||||
|
"lng_ai_compose_tone_warn_name" = "Please choose a name.";
|
||||||
|
"lng_ai_compose_tone_warn_prompt" = "Please enter instructions.";
|
||||||
|
"lng_ai_compose_tone_created" = "{title} Style Created!";
|
||||||
|
"lng_ai_compose_tone_updated" = "{title} Style Updated!";
|
||||||
|
"lng_ai_compose_tone_created_description" = "Right click the style to edit or share the link.";
|
||||||
|
"lng_ai_compose_tone_preview_about" = "Add this style to instantly rewrite your messages.";
|
||||||
|
"lng_ai_compose_tone_preview_add" = "Add Style";
|
||||||
|
"lng_ai_compose_tone_preview_add_example" = "Another example";
|
||||||
|
"lng_ai_compose_tone_preview_used_by#one" = "Used by {count} person.";
|
||||||
|
"lng_ai_compose_tone_preview_used_by#other" = "Used by {count} people.";
|
||||||
|
"lng_ai_compose_tone_preview_created_by" = "Created by {user}";
|
||||||
|
"lng_ai_compose_tone_added" = "Style Added";
|
||||||
|
"lng_ai_compose_tone_removed" = "Style removed.";
|
||||||
|
"lng_ai_compose_tone_deleted" = "Style deleted.";
|
||||||
|
"lng_ai_compose_tone_added_description" = "Tap \"AI\" → \"{name}\" when typing your next long message.";
|
||||||
|
"lng_ai_compose_tone_saved_limit#one" = "Subscribe to {link} to save up to {premium_count} styles, or delete one of your **{count}** style to add another.";
|
||||||
|
"lng_ai_compose_tone_saved_limit#other" = "Subscribe to {link} to save up to {premium_count} styles, or delete one of your **{count}** styles to add another.";
|
||||||
|
"lng_ai_compose_tone_saved_limit_link" = "Premium";
|
||||||
|
"lng_ai_compose_tone_saved_limit_final#one" = "You can save up to **{count}** style. Delete one to add another.";
|
||||||
|
"lng_ai_compose_tone_saved_limit_final#other" = "You can save up to **{count}** styles. Delete one to add another.";
|
||||||
|
"lng_sr_ai_compose_info" = "About AI Editor";
|
||||||
|
"lng_sr_ai_compose_copy_result" = "Copy result";
|
||||||
|
"lng_sr_ai_compose_expand_original" = "Expand original";
|
||||||
|
"lng_sr_ai_compose_collapse_original" = "Collapse original";
|
||||||
|
|
||||||
"lng_send_as_file_tooltip" = "Send text as a file.";
|
"lng_send_as_file_tooltip" = "Send text as a file.";
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<file alias="sleep.tgs">../../animations/sleep.tgs</file>
|
<file alias="sleep.tgs">../../animations/sleep.tgs</file>
|
||||||
<file alias="greeting.tgs">../../animations/greeting.tgs</file>
|
<file alias="greeting.tgs">../../animations/greeting.tgs</file>
|
||||||
<file alias="location.tgs">../../animations/location.tgs</file>
|
<file alias="location.tgs">../../animations/location.tgs</file>
|
||||||
<file alias="robot.tgs">../../animations/robot.tgs</file>
|
<file alias="settings/chat_automation.tgs">../../animations/settings/chat_automation.tgs</file>
|
||||||
<file alias="writing.tgs">../../animations/writing.tgs</file>
|
<file alias="writing.tgs">../../animations/writing.tgs</file>
|
||||||
<file alias="hours.tgs">../../animations/hours.tgs</file>
|
<file alias="hours.tgs">../../animations/hours.tgs</file>
|
||||||
<file alias="phone.tgs">../../animations/phone.tgs</file>
|
<file alias="phone.tgs">../../animations/phone.tgs</file>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||||
ProcessorArchitecture="ARCHITECTURE"
|
ProcessorArchitecture="ARCHITECTURE"
|
||||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||||
Version="6.7.9.0" />
|
Version="6.8.1.0" />
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Telegram Desktop</DisplayName>
|
<DisplayName>Telegram Desktop</DisplayName>
|
||||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
|||||||
//
|
//
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION 6,7,9,0
|
FILEVERSION 6,8,1,0
|
||||||
PRODUCTVERSION 6,7,9,0
|
PRODUCTVERSION 6,8,1,0
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
FILEFLAGS 0x1L
|
FILEFLAGS 0x1L
|
||||||
@@ -62,10 +62,10 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", ""
|
VALUE "CompanyName", ""
|
||||||
VALUE "FileDescription", "Telegram Desktop"
|
VALUE "FileDescription", "Telegram Desktop"
|
||||||
VALUE "FileVersion", "6.7.9.0"
|
VALUE "FileVersion", "6.8.1.0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
||||||
VALUE "ProductName", "Telegram Desktop"
|
VALUE "ProductName", "Telegram Desktop"
|
||||||
VALUE "ProductVersion", "6.7.9.0"
|
VALUE "ProductVersion", "6.8.1.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|||||||
//
|
//
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION 6,7,9,0
|
FILEVERSION 6,8,1,0
|
||||||
PRODUCTVERSION 6,7,9,0
|
PRODUCTVERSION 6,8,1,0
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
FILEFLAGS 0x1L
|
FILEFLAGS 0x1L
|
||||||
@@ -53,10 +53,10 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", ""
|
VALUE "CompanyName", ""
|
||||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||||
VALUE "FileVersion", "6.7.9.0"
|
VALUE "FileVersion", "6.8.1.0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
||||||
VALUE "ProductName", "Telegram Desktop"
|
VALUE "ProductName", "Telegram Desktop"
|
||||||
VALUE "ProductVersion", "6.7.9.0"
|
VALUE "ProductVersion", "6.8.1.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ mtpRequestId ComposeWithAi::request(
|
|||||||
if (!request.translateToLang.isEmpty()) {
|
if (!request.translateToLang.isEmpty()) {
|
||||||
flags |= Flag::f_translate_to_lang;
|
flags |= Flag::f_translate_to_lang;
|
||||||
}
|
}
|
||||||
if (!request.changeTone.isEmpty()) {
|
if (request.tone) {
|
||||||
flags |= Flag::f_change_tone;
|
flags |= Flag::f_tone;
|
||||||
}
|
}
|
||||||
if (request.emojify) {
|
if (request.emojify) {
|
||||||
flags |= Flag::f_emojify;
|
flags |= Flag::f_emojify;
|
||||||
@@ -53,9 +53,14 @@ mtpRequestId ComposeWithAi::request(
|
|||||||
request.translateToLang.isEmpty()
|
request.translateToLang.isEmpty()
|
||||||
? MTPstring()
|
? MTPstring()
|
||||||
: MTP_string(request.translateToLang),
|
: MTP_string(request.translateToLang),
|
||||||
request.changeTone.isEmpty()
|
request.tone
|
||||||
? MTPstring()
|
? (request.tone->id
|
||||||
: MTP_string(request.changeTone)
|
? MTP_inputAiComposeToneID(
|
||||||
|
MTP_long(request.tone->id),
|
||||||
|
MTP_long(request.tone->accessHash))
|
||||||
|
: MTP_inputAiComposeToneDefault(
|
||||||
|
MTP_string(request.tone->defaultTone)))
|
||||||
|
: MTPInputAiComposeTone()
|
||||||
)).done([=, done = std::move(done)](
|
)).done([=, done = std::move(done)](
|
||||||
const MTPmessages_ComposedMessageWithAI &result) mutable {
|
const MTPmessages_ComposedMessageWithAI &result) mutable {
|
||||||
const auto &data = result.data();
|
const auto &data = result.data();
|
||||||
|
|||||||
@@ -23,12 +23,25 @@ namespace Api {
|
|||||||
|
|
||||||
class ComposeWithAi final {
|
class ComposeWithAi final {
|
||||||
public:
|
public:
|
||||||
|
struct ToneRef {
|
||||||
|
QString defaultTone;
|
||||||
|
uint64 id = 0;
|
||||||
|
uint64 accessHash = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct Request {
|
struct Request {
|
||||||
TextWithEntities text;
|
TextWithEntities text;
|
||||||
QString translateToLang;
|
QString translateToLang;
|
||||||
QString changeTone;
|
std::optional<ToneRef> tone;
|
||||||
bool proofread = false;
|
bool proofread = false;
|
||||||
bool emojify = false;
|
bool emojify = false;
|
||||||
|
|
||||||
|
void setDefaultTone(const QString &type) {
|
||||||
|
tone = ToneRef{ .defaultTone = type };
|
||||||
|
}
|
||||||
|
void setCustomTone(uint64 id, uint64 accessHash) {
|
||||||
|
tone = ToneRef{ .id = id, .accessHash = accessHash };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DiffEntity {
|
struct DiffEntity {
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ Data::CreditsHistoryEntry CreditsHistoryEntryFromTL(
|
|||||||
.postsSearch = tl.data().is_posts_search(),
|
.postsSearch = tl.data().is_posts_search(),
|
||||||
.giftUpgraded = tl.data().is_stargift_upgrade(),
|
.giftUpgraded = tl.data().is_stargift_upgrade(),
|
||||||
.giftResale = tl.data().is_stargift_resale(),
|
.giftResale = tl.data().is_stargift_resale(),
|
||||||
|
.giftOffer = tl.data().is_offer(),
|
||||||
.reaction = tl.data().is_reaction(),
|
.reaction = tl.data().is_reaction(),
|
||||||
.refunded = tl.data().is_refund(),
|
.refunded = tl.data().is_refund(),
|
||||||
.pending = tl.data().is_pending(),
|
.pending = tl.data().is_pending(),
|
||||||
|
|||||||
@@ -8,19 +8,207 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "api/api_polls.h"
|
#include "api/api_polls.h"
|
||||||
|
|
||||||
#include "api/api_common.h"
|
#include "api/api_common.h"
|
||||||
|
#include "api/api_statistics_data_deserialize.h"
|
||||||
#include "api/api_text_entities.h"
|
#include "api/api_text_entities.h"
|
||||||
#include "api/api_updates.h"
|
#include "api/api_updates.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
|
#include "base/call_delayed.h"
|
||||||
|
#include "base/qt/qt_key_modifiers.h"
|
||||||
#include "base/random.h"
|
#include "base/random.h"
|
||||||
#include "data/business/data_shortcut_messages.h"
|
#include "data/business/data_shortcut_messages.h"
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
#include "data/data_histories.h"
|
#include "data/data_histories.h"
|
||||||
#include "data/data_poll.h"
|
#include "data/data_poll.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
|
#include "data/data_statistics_chart.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
#include "history/history_item.h"
|
#include "history/history_item.h"
|
||||||
#include "history/history_item_helpers.h" // ShouldSendSilent
|
#include "history/history_item_helpers.h" // ShouldSendSilent
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
|
#include "styles/style_polls.h"
|
||||||
|
#include "ui/toast/toast.h"
|
||||||
|
#include "window/window_session_controller.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kVoteRestrictionToastDuration = 5 * crl::time(1000);
|
||||||
|
|
||||||
|
const auto kSubscribersOnlyVoteErrorPatterns = std::array{
|
||||||
|
u"POLL_SUBSCRIBERS_ONLY"_q,
|
||||||
|
u"POLL_MEMBER_RESTRICTED"_q,
|
||||||
|
u"VOTE_SUBSCRIBERS_ONLY"_q,
|
||||||
|
u"SUBSCRIBERS_ONLY"_q,
|
||||||
|
u"SUBSCRIBER_REQUIRED"_q,
|
||||||
|
u"SUBSCRIBER_ONLY"_q,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto kSubscribersJoinedTooRecentlyVoteErrorPatterns = std::array{
|
||||||
|
u"POLL_SUBSCRIBERS_TOO_RECENT"_q,
|
||||||
|
u"VOTE_SUBSCRIBERS_TOO_RECENT"_q,
|
||||||
|
u"SUBSCRIBERS_TOO_RECENT"_q,
|
||||||
|
u"SUBSCRIBER_TOO_RECENT"_q,
|
||||||
|
u"JOINED_TOO_RECENTLY"_q,
|
||||||
|
u"24_HOURS"_q,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto kCountriesVoteErrorPatterns = std::array{
|
||||||
|
u"POLL_COUNTRIES_ISO2"_q,
|
||||||
|
u"VOTE_COUNTRIES_ISO2"_q,
|
||||||
|
u"COUNTRIES_ISO2"_q,
|
||||||
|
u"COUNTRY_RESTRICTED"_q,
|
||||||
|
u"COUNTRY_ISO2"_q,
|
||||||
|
};
|
||||||
|
|
||||||
|
template <size_t Size>
|
||||||
|
[[nodiscard]] bool MatchesErrorPattern(
|
||||||
|
const QString &type,
|
||||||
|
const std::array<QString, Size> &patterns) {
|
||||||
|
for (const auto &pattern : patterns) {
|
||||||
|
if (!pattern.isEmpty()
|
||||||
|
&& type.contains(pattern, Qt::CaseInsensitive)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] PollData::VoteRestriction ParseVoteRestrictionError(
|
||||||
|
const QString &type) {
|
||||||
|
if (MatchesErrorPattern(
|
||||||
|
type,
|
||||||
|
kSubscribersJoinedTooRecentlyVoteErrorPatterns)) {
|
||||||
|
return PollData::VoteRestriction::SubscribersJoinedTooRecently;
|
||||||
|
} else if (MatchesErrorPattern(
|
||||||
|
type,
|
||||||
|
kSubscribersOnlyVoteErrorPatterns)) {
|
||||||
|
return PollData::VoteRestriction::SubscribersOnly;
|
||||||
|
} else if (MatchesErrorPattern(
|
||||||
|
type,
|
||||||
|
kCountriesVoteErrorPatterns)) {
|
||||||
|
return PollData::VoteRestriction::Countries;
|
||||||
|
}
|
||||||
|
return PollData::VoteRestriction::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowVoteRestrictionToast(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
not_null<const PollData*> poll,
|
||||||
|
PollData::VoteRestriction restriction) {
|
||||||
|
if (restriction == PollData::VoteRestriction::None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto text = PollVoteRestrictionText(restriction, peer, poll);
|
||||||
|
if (text.text.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto window = peer->session().tryResolveWindow(peer)) {
|
||||||
|
window->showToast({
|
||||||
|
.text = std::move(text),
|
||||||
|
.iconLottie = u"ban"_q,
|
||||||
|
.iconLottieSize = st::pollToastIconSize,
|
||||||
|
.duration = kVoteRestrictionToastDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
[[nodiscard]] Data::StatisticalGraph GenerateMockupPollStats(
|
||||||
|
const PollData &poll) {
|
||||||
|
auto chart = Data::StatisticalChart();
|
||||||
|
const auto colorKeys = std::array<QString, 10>{
|
||||||
|
u"BLUE"_q,
|
||||||
|
u"GREEN"_q,
|
||||||
|
u"RED"_q,
|
||||||
|
u"GOLDEN"_q,
|
||||||
|
u"LIGHTBLUE"_q,
|
||||||
|
u"LIGHTGREEN"_q,
|
||||||
|
u"ORANGE"_q,
|
||||||
|
u"INDIGO"_q,
|
||||||
|
u"PURPLE"_q,
|
||||||
|
u"CYAN"_q,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr auto kPoints = 14;
|
||||||
|
constexpr auto kOneDay = float64(24 * 60 * 60 * 1000);
|
||||||
|
constexpr auto kStart = float64(1704067200000);
|
||||||
|
chart.x.reserve(kPoints);
|
||||||
|
for (auto i = 0; i != kPoints; ++i) {
|
||||||
|
chart.x.push_back(kStart + i * kOneDay);
|
||||||
|
}
|
||||||
|
chart.timeStep = kOneDay;
|
||||||
|
|
||||||
|
auto lineId = 0;
|
||||||
|
chart.lines.reserve(poll.answers.size());
|
||||||
|
for (const auto &answer : poll.answers) {
|
||||||
|
auto line = Data::StatisticalChart::Line();
|
||||||
|
line.id = ++lineId;
|
||||||
|
line.idString = u"answer_%1"_q.arg(line.id);
|
||||||
|
line.name = answer.text.text.trimmed();
|
||||||
|
if (line.name.isEmpty()) {
|
||||||
|
line.name = QString("#%1").arg(line.id);
|
||||||
|
}
|
||||||
|
line.colorKey = colorKeys[(line.id - 1) % int(colorKeys.size())];
|
||||||
|
line.y.reserve(kPoints);
|
||||||
|
|
||||||
|
auto seed = int64(13 * line.id + 17);
|
||||||
|
for (const auto byte : answer.option) {
|
||||||
|
seed += uchar(byte);
|
||||||
|
}
|
||||||
|
const auto base = std::max(int64(answer.votes), int64(1));
|
||||||
|
for (auto i = 0; i != kPoints; ++i) {
|
||||||
|
const auto wave = int64(
|
||||||
|
((i + line.id) % 5) * ((i + 2 * line.id) % 4));
|
||||||
|
const auto trend = int64((i * (line.id + 1)) / 3);
|
||||||
|
const auto noise = int64((seed + i * 7 + line.id * 11) % 6);
|
||||||
|
const auto value = std::max(
|
||||||
|
base + wave + trend + noise - 2,
|
||||||
|
int64(1));
|
||||||
|
line.y.push_back(value);
|
||||||
|
line.maxValue = std::max(line.maxValue, value);
|
||||||
|
line.minValue = std::min(line.minValue, value);
|
||||||
|
}
|
||||||
|
chart.lines.push_back(std::move(line));
|
||||||
|
}
|
||||||
|
if (chart.lines.empty()) {
|
||||||
|
auto line = Data::StatisticalChart::Line();
|
||||||
|
line.id = 1;
|
||||||
|
line.idString = u"votes"_q;
|
||||||
|
line.name = tr::lng_notification_reactions_poll_votes(tr::now);
|
||||||
|
line.colorKey = u"BLUE"_q;
|
||||||
|
line.y.reserve(kPoints);
|
||||||
|
|
||||||
|
const auto base = std::max(int64(poll.totalVoters), int64(1));
|
||||||
|
for (auto i = 0; i != kPoints; ++i) {
|
||||||
|
const auto value = std::max(
|
||||||
|
base + i * 2 + ((i * 5) % 7),
|
||||||
|
int64(1));
|
||||||
|
line.y.push_back(value);
|
||||||
|
line.maxValue = std::max(line.maxValue, value);
|
||||||
|
line.minValue = std::min(line.minValue, value);
|
||||||
|
}
|
||||||
|
chart.lines.push_back(std::move(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.defaultZoomXIndex = {
|
||||||
|
.min = std::max(0, kPoints - 8),
|
||||||
|
.max = kPoints - 1,
|
||||||
|
};
|
||||||
|
chart.measure();
|
||||||
|
if (chart.maxValue == chart.minValue) {
|
||||||
|
if (chart.minValue) {
|
||||||
|
chart.minValue = 0;
|
||||||
|
} else {
|
||||||
|
chart.maxValue = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
.chart = std::move(chart),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace Api {
|
namespace Api {
|
||||||
|
|
||||||
@@ -151,6 +339,7 @@ void Polls::sendVotes(
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto peer = item->history()->peer;
|
||||||
|
|
||||||
const auto showSending = poll && !options.empty();
|
const auto showSending = poll && !options.empty();
|
||||||
const auto hideSending = [=] {
|
const auto hideSending = [=] {
|
||||||
@@ -164,6 +353,12 @@ void Polls::sendVotes(
|
|||||||
if (showSending) {
|
if (showSending) {
|
||||||
poll->sendingVotes = options;
|
poll->sendingVotes = options;
|
||||||
_session->data().requestItemRepaint(item);
|
_session->data().requestItemRepaint(item);
|
||||||
|
} else if (poll && options.empty() && poll->voted()) {
|
||||||
|
for (auto &answer : poll->answers) {
|
||||||
|
answer.chosen = false;
|
||||||
|
}
|
||||||
|
++poll->version;
|
||||||
|
_session->data().notifyPollUpdateDelayed(poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto prepared = QVector<MTPbytes>();
|
auto prepared = QVector<MTPbytes>();
|
||||||
@@ -173,16 +368,33 @@ void Polls::sendVotes(
|
|||||||
ranges::back_inserter(prepared),
|
ranges::back_inserter(prepared),
|
||||||
[](const QByteArray &option) { return MTP_bytes(option); });
|
[](const QByteArray &option) { return MTP_bytes(option); });
|
||||||
const auto requestId = _api.request(MTPmessages_SendVote(
|
const auto requestId = _api.request(MTPmessages_SendVote(
|
||||||
item->history()->peer->input(),
|
peer->input(),
|
||||||
MTP_int(item->id),
|
MTP_int(item->id),
|
||||||
MTP_vector<MTPbytes>(prepared)
|
MTP_vector<MTPbytes>(prepared)
|
||||||
)).done([=](const MTPUpdates &result) {
|
)).done([=](const MTPUpdates &result) {
|
||||||
_pollVotesRequestIds.erase(itemId);
|
_pollVotesRequestIds.erase(itemId);
|
||||||
hideSending();
|
hideSending();
|
||||||
|
if (poll) {
|
||||||
|
if (poll->voteRestriction() != PollData::VoteRestriction::None) {
|
||||||
|
poll->setVoteRestriction(PollData::VoteRestriction::None);
|
||||||
|
_session->data().notifyPollUpdateDelayed(poll);
|
||||||
|
}
|
||||||
|
}
|
||||||
_session->updates().applyUpdates(result);
|
_session->updates().applyUpdates(result);
|
||||||
}).fail([=] {
|
}).fail([=](const MTP::Error &error) {
|
||||||
_pollVotesRequestIds.erase(itemId);
|
_pollVotesRequestIds.erase(itemId);
|
||||||
hideSending();
|
hideSending();
|
||||||
|
if (poll) {
|
||||||
|
const auto restriction = ParseVoteRestrictionError(error.type());
|
||||||
|
if (restriction != PollData::VoteRestriction::None) {
|
||||||
|
poll->setVoteRestriction(restriction);
|
||||||
|
_session->data().notifyPollUpdateDelayed(poll);
|
||||||
|
if (const auto item = _session->data().message(itemId)) {
|
||||||
|
_session->data().requestItemResize(item);
|
||||||
|
}
|
||||||
|
ShowVoteRestrictionToast(peer, poll, restriction);
|
||||||
|
}
|
||||||
|
}
|
||||||
}).send();
|
}).send();
|
||||||
_pollVotesRequestIds.emplace(itemId, requestId);
|
_pollVotesRequestIds.emplace(itemId, requestId);
|
||||||
}
|
}
|
||||||
@@ -305,4 +517,65 @@ void Polls::reloadResults(not_null<HistoryItem*> item) {
|
|||||||
_pollReloadRequestIds.emplace(itemId, requestId);
|
_pollReloadRequestIds.emplace(itemId, requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Polls::requestStats(
|
||||||
|
FullMsgId itemId,
|
||||||
|
Fn<void(Data::StatisticalGraph)> done,
|
||||||
|
Fn<void(QString)> fail) {
|
||||||
|
const auto item = _session->data().message(itemId);
|
||||||
|
const auto media = item ? item->media() : nullptr;
|
||||||
|
const auto poll = media ? media->poll() : nullptr;
|
||||||
|
if (!item || !item->isRegular() || !poll) {
|
||||||
|
if (fail) {
|
||||||
|
fail(QString());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#ifdef _DEBUG
|
||||||
|
if (base::IsCtrlPressed()) {
|
||||||
|
auto callback = std::move(done);
|
||||||
|
if (callback) {
|
||||||
|
constexpr auto kMockupStatsDelay = 2 * crl::time(1000);
|
||||||
|
auto graph = GenerateMockupPollStats(*poll);
|
||||||
|
base::call_delayed(kMockupStatsDelay, _session, [=]() mutable {
|
||||||
|
callback(std::move(graph));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
const auto requestGraph = [=](const QString &token) {
|
||||||
|
_api.request(MTPstats_LoadAsyncGraph(
|
||||||
|
MTP_flags(MTPstats_LoadAsyncGraph::Flag(0)),
|
||||||
|
MTP_string(token),
|
||||||
|
MTP_long(0)
|
||||||
|
)).done([=](const MTPStatsGraph &result) {
|
||||||
|
if (done) {
|
||||||
|
done(Api::StatisticalGraphFromTL(result));
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error.type());
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
};
|
||||||
|
_api.request(MTPstats_GetPollStats(
|
||||||
|
MTP_flags(MTPstats_GetPollStats::Flags(0)),
|
||||||
|
item->history()->peer->input(),
|
||||||
|
MTP_int(item->id)
|
||||||
|
)).done([=](const MTPstats_PollStats &result) {
|
||||||
|
auto graph = Api::StatisticalGraphFromTL(result.data().vvotes_graph());
|
||||||
|
if (graph.chart || !graph.error.isEmpty() || graph.zoomToken.isEmpty()) {
|
||||||
|
if (done) {
|
||||||
|
done(std::move(graph));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestGraph(graph.zoomToken);
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error.type());
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Api
|
} // namespace Api
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ class ApiWrap;
|
|||||||
class HistoryItem;
|
class HistoryItem;
|
||||||
struct PollData;
|
struct PollData;
|
||||||
struct PollMedia;
|
struct PollMedia;
|
||||||
|
namespace Data {
|
||||||
|
struct StatisticalGraph;
|
||||||
|
} // namespace Data
|
||||||
|
|
||||||
namespace Main {
|
namespace Main {
|
||||||
class Session;
|
class Session;
|
||||||
@@ -45,6 +48,10 @@ public:
|
|||||||
void deleteAnswer(FullMsgId itemId, const QByteArray &option);
|
void deleteAnswer(FullMsgId itemId, const QByteArray &option);
|
||||||
void close(not_null<HistoryItem*> item);
|
void close(not_null<HistoryItem*> item);
|
||||||
void reloadResults(not_null<HistoryItem*> item);
|
void reloadResults(not_null<HistoryItem*> item);
|
||||||
|
void requestStats(
|
||||||
|
FullMsgId itemId,
|
||||||
|
Fn<void(Data::StatisticalGraph)> done,
|
||||||
|
Fn<void(QString)> fail);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const not_null<Main::Session*> _session;
|
const not_null<Main::Session*> _session;
|
||||||
|
|||||||
@@ -144,6 +144,34 @@ auto CreateReportMessagesOrStoriesCallback(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReactionReportCapabilities GetReactionReportCapabilities(
|
||||||
|
not_null<PeerData*> group,
|
||||||
|
not_null<PeerData*> participant) {
|
||||||
|
const auto channel = group->asMegagroup();
|
||||||
|
return channel
|
||||||
|
? ReactionReportCapabilities{
|
||||||
|
.canReport = channel->isPublic() && !participant->isSelf(),
|
||||||
|
.canBan = channel->canRestrictParticipant(participant),
|
||||||
|
}
|
||||||
|
: ReactionReportCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportReaction(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<PeerData*> group,
|
||||||
|
MsgId messageId,
|
||||||
|
not_null<PeerData*> participant) {
|
||||||
|
group->session().api().request(MTPmessages_ReportReaction(
|
||||||
|
group->input(),
|
||||||
|
MTP_int(messageId.bare),
|
||||||
|
participant->input()
|
||||||
|
)).done([=] {
|
||||||
|
if (show) {
|
||||||
|
show->showToast(tr::lng_report_thanks(tr::now));
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
void ReportSpam(
|
void ReportSpam(
|
||||||
not_null<PeerData*> sender,
|
not_null<PeerData*> sender,
|
||||||
const MessageIdsList &ids) {
|
const MessageIdsList &ids) {
|
||||||
|
|||||||
@@ -53,6 +53,21 @@ void SendPhotoReport(
|
|||||||
not_null<PeerData*> peer)
|
not_null<PeerData*> peer)
|
||||||
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)>;
|
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)>;
|
||||||
|
|
||||||
|
struct ReactionReportCapabilities final {
|
||||||
|
bool canReport = false;
|
||||||
|
bool canBan = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ReactionReportCapabilities GetReactionReportCapabilities(
|
||||||
|
not_null<PeerData*> group,
|
||||||
|
not_null<PeerData*> participant);
|
||||||
|
|
||||||
|
void ReportReaction(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<PeerData*> group,
|
||||||
|
MsgId messageId,
|
||||||
|
not_null<PeerData*> participant);
|
||||||
|
|
||||||
void ReportSpam(
|
void ReportSpam(
|
||||||
not_null<PeerData*> sender,
|
not_null<PeerData*> sender,
|
||||||
const MessageIdsList &ids);
|
const MessageIdsList &ids);
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ void FillChooseOwnedSetMenu(
|
|||||||
const auto identifier = set->identifier();
|
const auto identifier = set->identifier();
|
||||||
const auto coverDocument = set->lookupThumbnailDocument();
|
const auto coverDocument = set->lookupThumbnailDocument();
|
||||||
auto thumbnail = coverDocument
|
auto thumbnail = coverDocument
|
||||||
? Ui::MakeDocumentThumbnail(
|
? Ui::MakeDocumentThumbnailFit(
|
||||||
coverDocument,
|
coverDocument,
|
||||||
Data::FileOriginStickerSet(set->id, set->accessHash))
|
Data::FileOriginStickerSet(set->id, set->accessHash))
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/components/top_peers.h"
|
#include "data/components/top_peers.h"
|
||||||
#include "data/notify/data_notify_settings.h"
|
#include "data/notify/data_notify_settings.h"
|
||||||
#include "data/stickers/data_stickers.h"
|
#include "data/stickers/data_stickers.h"
|
||||||
|
#include "data/data_ai_compose_tones.h"
|
||||||
#include "data/data_saved_messages.h"
|
#include "data/data_saved_messages.h"
|
||||||
#include "data/data_saved_sublist.h"
|
#include "data/data_saved_sublist.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
@@ -1209,6 +1210,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
|
|||||||
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
|
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
|
||||||
MTP_long(d.vvia_bot_id().value_or_empty()),
|
MTP_long(d.vvia_bot_id().value_or_empty()),
|
||||||
MTPlong(), // via_business_bot_id
|
MTPlong(), // via_business_bot_id
|
||||||
|
MTPPeer(), // guestchat_via_from
|
||||||
d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),
|
d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),
|
||||||
d.vdate(),
|
d.vdate(),
|
||||||
d.vmessage(),
|
d.vmessage(),
|
||||||
@@ -1252,6 +1254,7 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
|
|||||||
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
|
d.vfwd_from() ? *d.vfwd_from() : MTPMessageFwdHeader(),
|
||||||
MTP_long(d.vvia_bot_id().value_or_empty()),
|
MTP_long(d.vvia_bot_id().value_or_empty()),
|
||||||
MTPlong(), // via_business_bot_id
|
MTPlong(), // via_business_bot_id
|
||||||
|
MTPPeer(), // guestchat_via_from
|
||||||
d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),
|
d.vreply_to() ? *d.vreply_to() : MTPMessageReplyHeader(),
|
||||||
d.vdate(),
|
d.vdate(),
|
||||||
d.vmessage(),
|
d.vmessage(),
|
||||||
@@ -2777,6 +2780,10 @@ void Updates::feedUpdate(const MTPUpdate &update) {
|
|||||||
session().api().ringtones().applyUpdate();
|
session().api().ringtones().applyUpdate();
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
case mtpc_updateAiComposeTones: {
|
||||||
|
session().data().aiComposeTones().applyUpdate();
|
||||||
|
} break;
|
||||||
|
|
||||||
case mtpc_updateTranscribedAudio: {
|
case mtpc_updateTranscribedAudio: {
|
||||||
const auto &data = update.c_updateTranscribedAudio();
|
const auto &data = update.c_updateTranscribedAudio();
|
||||||
_session->api().transcribes().apply(data);
|
_session->api().transcribes().apply(data);
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ struct Userpic {
|
|||||||
TimeId date = 0;
|
TimeId date = 0;
|
||||||
bool dateReacted = false;
|
bool dateReacted = false;
|
||||||
QString customEntityData;
|
QString customEntityData;
|
||||||
|
ReactionId reaction;
|
||||||
mutable Ui::PeerUserpicView view;
|
mutable Ui::PeerUserpicView view;
|
||||||
mutable InMemoryKey uniqueKey;
|
mutable InMemoryKey uniqueKey;
|
||||||
};
|
};
|
||||||
@@ -128,6 +129,26 @@ struct State {
|
|||||||
bool scheduled = false;
|
bool scheduled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool ApplyReactionsRemovedToCachedData(
|
||||||
|
PeersWithReactions &data,
|
||||||
|
const Data::ReactionsRemoved &update) {
|
||||||
|
const auto was = data.list.size();
|
||||||
|
data.list.erase(
|
||||||
|
ranges::remove_if(data.list, [&](const PeerWithReaction &entry) {
|
||||||
|
return !entry.reaction.empty()
|
||||||
|
&& entry.peerWithDate.peer == update.participant->id;
|
||||||
|
}),
|
||||||
|
end(data.list));
|
||||||
|
const auto removed = int(was - data.list.size());
|
||||||
|
if (!removed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data.fullReactionsCount = (data.fullReactionsCount > removed)
|
||||||
|
? (data.fullReactionsCount - removed)
|
||||||
|
: 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] auto Contexts()
|
[[nodiscard]] auto Contexts()
|
||||||
-> base::flat_map<not_null<QWidget*>, std::unique_ptr<Context>> & {
|
-> base::flat_map<not_null<QWidget*>, std::unique_ptr<Context>> & {
|
||||||
static auto result = base::flat_map<
|
static auto result = base::flat_map<
|
||||||
@@ -188,6 +209,22 @@ struct State {
|
|||||||
context->cachedReacted.erase(j);
|
context->cachedReacted.erase(j);
|
||||||
}
|
}
|
||||||
}, context->subscriptions[session]);
|
}, context->subscriptions[session]);
|
||||||
|
session->data().reactionsRemoved(
|
||||||
|
) | rpl::on_next([=](const Data::ReactionsRemoved &update) {
|
||||||
|
for (auto &[item, map] : context->cachedReacted) {
|
||||||
|
if (item->history()->peer->id != update.peer->id) {
|
||||||
|
continue;
|
||||||
|
} else if (update.msgId && item->id != update.msgId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (auto &entry : map) {
|
||||||
|
auto data = entry.second.data.current();
|
||||||
|
if (ApplyReactionsRemovedToCachedData(data, update)) {
|
||||||
|
entry.second.data = std::move(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, context->subscriptions[session]);
|
||||||
Data::AmPremiumValue(
|
Data::AmPremiumValue(
|
||||||
session
|
session
|
||||||
) | rpl::skip(1) | rpl::filter(
|
) | rpl::skip(1) | rpl::filter(
|
||||||
@@ -443,12 +480,22 @@ bool UpdateUserpics(
|
|||||||
return resolved.peer != nullptr;
|
return resolved.peer != nullptr;
|
||||||
}) | ranges::to_vector;
|
}) | ranges::to_vector;
|
||||||
|
|
||||||
const auto same = ranges::equal(
|
const auto same = [&] {
|
||||||
state->userpics,
|
if (state->userpics.size() != peers.size()) {
|
||||||
peers,
|
return false;
|
||||||
ranges::equal_to(),
|
}
|
||||||
[](const Userpic &u) { return std::pair(u.peer.get(), u.date); },
|
const auto count = state->userpics.size();
|
||||||
[](const ResolvedPeer &r) { return std::pair(r.peer, r.date); });
|
for (auto i = size_t(); i != count; ++i) {
|
||||||
|
const auto &userpic = state->userpics[i];
|
||||||
|
const auto &resolved = peers[i];
|
||||||
|
if ((userpic.peer.get() != resolved.peer)
|
||||||
|
|| (userpic.date != resolved.date)
|
||||||
|
|| (userpic.reaction != resolved.reaction)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
if (same) {
|
if (same) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -461,6 +508,7 @@ bool UpdateUserpics(
|
|||||||
if (i != end(was) && i->view.cloud) {
|
if (i != end(was) && i->view.cloud) {
|
||||||
i->date = resolved.date;
|
i->date = resolved.date;
|
||||||
i->dateReacted = resolved.dateReacted;
|
i->dateReacted = resolved.dateReacted;
|
||||||
|
i->reaction = resolved.reaction;
|
||||||
now.push_back(std::move(*i));
|
now.push_back(std::move(*i));
|
||||||
now.back().customEntityData = data;
|
now.back().customEntityData = data;
|
||||||
continue;
|
continue;
|
||||||
@@ -470,6 +518,7 @@ bool UpdateUserpics(
|
|||||||
.date = resolved.date,
|
.date = resolved.date,
|
||||||
.dateReacted = resolved.dateReacted,
|
.dateReacted = resolved.dateReacted,
|
||||||
.customEntityData = data,
|
.customEntityData = data,
|
||||||
|
.reaction = resolved.reaction,
|
||||||
});
|
});
|
||||||
auto &userpic = now.back();
|
auto &userpic = now.back();
|
||||||
userpic.uniqueKey = peer->userpicUniqueKey(userpic.view);
|
userpic.uniqueKey = peer->userpicUniqueKey(userpic.view);
|
||||||
@@ -512,11 +561,15 @@ void RegenerateParticipants(not_null<State*> state, int small, int large) {
|
|||||||
const auto peer = userpic.peer;
|
const auto peer = userpic.peer;
|
||||||
const auto date = userpic.date;
|
const auto date = userpic.date;
|
||||||
const auto id = peer->id.value;
|
const auto id = peer->id.value;
|
||||||
|
const auto self = peer->isSelf();
|
||||||
const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
|
const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
|
||||||
if (was != end(old)) {
|
if (was != end(old)) {
|
||||||
was->name = peer->name();
|
was->name = peer->name();
|
||||||
was->date = FormatReadDate(date, currentDate);
|
was->date = FormatReadDate(date, currentDate);
|
||||||
was->dateReacted = userpic.dateReacted;
|
was->dateReacted = userpic.dateReacted;
|
||||||
|
was->self = self;
|
||||||
|
was->customEntityData = userpic.customEntityData;
|
||||||
|
was->reaction = userpic.reaction;
|
||||||
now.push_back(std::move(*was));
|
now.push_back(std::move(*was));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -524,7 +577,9 @@ void RegenerateParticipants(not_null<State*> state, int small, int large) {
|
|||||||
.name = peer->name(),
|
.name = peer->name(),
|
||||||
.date = FormatReadDate(date, currentDate),
|
.date = FormatReadDate(date, currentDate),
|
||||||
.dateReacted = userpic.dateReacted,
|
.dateReacted = userpic.dateReacted,
|
||||||
|
.self = self,
|
||||||
.customEntityData = userpic.customEntityData,
|
.customEntityData = userpic.customEntityData,
|
||||||
|
.reaction = userpic.reaction,
|
||||||
.userpicLarge = GenerateUserpic(userpic, large),
|
.userpicLarge = GenerateUserpic(userpic, large),
|
||||||
.userpicKey = userpic.uniqueKey,
|
.userpicKey = userpic.uniqueKey,
|
||||||
.id = id,
|
.id = id,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/data_folder.h"
|
#include "data/data_folder.h"
|
||||||
#include "data/data_forum_topic.h"
|
#include "data/data_forum_topic.h"
|
||||||
#include "data/data_forum.h"
|
#include "data/data_forum.h"
|
||||||
|
#include "data/data_message_reaction_id.h"
|
||||||
#include "data/data_saved_messages.h"
|
#include "data/data_saved_messages.h"
|
||||||
#include "data/data_saved_music.h"
|
#include "data/data_saved_music.h"
|
||||||
#include "data/data_saved_sublist.h"
|
#include "data/data_saved_sublist.h"
|
||||||
@@ -1422,6 +1423,43 @@ void ApiWrap::deleteAllFromParticipantSend(
|
|||||||
}).send();
|
}).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ApiWrap::deleteAllReactionsFromParticipant(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
not_null<PeerData*> participant,
|
||||||
|
MsgId originMsgId,
|
||||||
|
const Data::ReactionId &originReaction) {
|
||||||
|
_session->data().removeReactionsFromParticipant(
|
||||||
|
peer,
|
||||||
|
0,
|
||||||
|
participant,
|
||||||
|
originReaction,
|
||||||
|
originMsgId);
|
||||||
|
request(MTPmessages_DeleteParticipantReactions(
|
||||||
|
peer->input(),
|
||||||
|
participant->input()
|
||||||
|
)).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiWrap::deleteParticipantReaction(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
MsgId msgId,
|
||||||
|
not_null<PeerData*> participant,
|
||||||
|
const Data::ReactionId &reaction) {
|
||||||
|
_session->data().removeReactionsFromParticipant(
|
||||||
|
peer,
|
||||||
|
msgId,
|
||||||
|
participant,
|
||||||
|
reaction,
|
||||||
|
0);
|
||||||
|
request(MTPmessages_DeleteParticipantReaction(
|
||||||
|
peer->input(),
|
||||||
|
MTP_int(msgId.bare),
|
||||||
|
participant->input()
|
||||||
|
)).done([=](const MTPUpdates &result) {
|
||||||
|
applyUpdates(result);
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
void ApiWrap::deleteSublistHistory(
|
void ApiWrap::deleteSublistHistory(
|
||||||
not_null<ChannelData*> channel,
|
not_null<ChannelData*> channel,
|
||||||
not_null<PeerData*> sublistPeer) {
|
not_null<PeerData*> sublistPeer) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Session;
|
|||||||
} // namespace Main
|
} // namespace Main
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
|
struct ReactionId;
|
||||||
struct UpdatedFileReferences;
|
struct UpdatedFileReferences;
|
||||||
class WallPaper;
|
class WallPaper;
|
||||||
struct ResolvedForwardDraft;
|
struct ResolvedForwardDraft;
|
||||||
@@ -237,6 +238,16 @@ public:
|
|||||||
void deleteAllFromParticipant(
|
void deleteAllFromParticipant(
|
||||||
not_null<ChannelData*> channel,
|
not_null<ChannelData*> channel,
|
||||||
not_null<PeerData*> from);
|
not_null<PeerData*> from);
|
||||||
|
void deleteAllReactionsFromParticipant(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
not_null<PeerData*> participant,
|
||||||
|
MsgId originMsgId,
|
||||||
|
const Data::ReactionId &originReaction);
|
||||||
|
void deleteParticipantReaction(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
MsgId msgId,
|
||||||
|
not_null<PeerData*> participant,
|
||||||
|
const Data::ReactionId &reaction);
|
||||||
void deleteSublistHistory(
|
void deleteSublistHistory(
|
||||||
not_null<ChannelData*> parentChat,
|
not_null<ChannelData*> parentChat,
|
||||||
not_null<PeerData*> sublistPeer);
|
not_null<PeerData*> sublistPeer);
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ boxPhotoTitlePosition: point(28px, 20px);
|
|||||||
boxPhotoPadding: margins(28px, 28px, 28px, 18px);
|
boxPhotoPadding: margins(28px, 28px, 28px, 18px);
|
||||||
boxPhotoCompressedSkip: 20px;
|
boxPhotoCompressedSkip: 20px;
|
||||||
boxPhotoCaptionSkip: 8px;
|
boxPhotoCaptionSkip: 8px;
|
||||||
boxPhotoCaptionReplyOverlap: 5px;
|
|
||||||
|
|
||||||
defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }};
|
defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }};
|
||||||
defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }};
|
defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }};
|
||||||
@@ -115,10 +114,8 @@ confirmInviteStatus: FlatLabel(confirmInviteAbout) {
|
|||||||
}
|
}
|
||||||
confirmInviteAboutPadding: margins(36px, 4px, 36px, 10px);
|
confirmInviteAboutPadding: margins(36px, 4px, 36px, 10px);
|
||||||
confirmInviteAboutRequestsPadding: margins(36px, 9px, 36px, 15px);
|
confirmInviteAboutRequestsPadding: margins(36px, 9px, 36px, 15px);
|
||||||
confirmInviteTitleTop: 141px;
|
|
||||||
confirmInvitePhotoSize: 96px;
|
confirmInvitePhotoSize: 96px;
|
||||||
confirmInvitePhotoTop: 33px;
|
confirmInvitePhotoTop: 33px;
|
||||||
confirmInviteStatusTop: 164px;
|
|
||||||
confirmInviteUserHeight: 100px;
|
confirmInviteUserHeight: 100px;
|
||||||
confirmInviteUserPhotoSize: 50px;
|
confirmInviteUserPhotoSize: 50px;
|
||||||
confirmInviteUserPhotoTop: 210px;
|
confirmInviteUserPhotoTop: 210px;
|
||||||
@@ -349,7 +346,6 @@ themeWarningHeight: 150px;
|
|||||||
themeWarningTextTop: 60px;
|
themeWarningTextTop: 60px;
|
||||||
|
|
||||||
aboutWidth: 390px;
|
aboutWidth: 390px;
|
||||||
aboutVersionTop: -3px;
|
|
||||||
aboutVersionLink: LinkButton(defaultLinkButton) {
|
aboutVersionLink: LinkButton(defaultLinkButton) {
|
||||||
color: windowSubTextFg;
|
color: windowSubTextFg;
|
||||||
overColor: windowSubTextFg;
|
overColor: windowSubTextFg;
|
||||||
@@ -386,7 +382,6 @@ connectionUserInputField: InputField(defaultInputField) {
|
|||||||
}
|
}
|
||||||
connectionPasswordInputField: InputField(defaultInputField) {
|
connectionPasswordInputField: InputField(defaultInputField) {
|
||||||
}
|
}
|
||||||
connectionIPv6Skip: 11px;
|
|
||||||
|
|
||||||
autolockWidth: 256px;
|
autolockWidth: 256px;
|
||||||
autolockButton: Checkbox(defaultBoxCheckbox) {
|
autolockButton: Checkbox(defaultBoxCheckbox) {
|
||||||
@@ -529,7 +524,6 @@ colorResultInput: InputField(colorValueInput) {
|
|||||||
changePhoneButton: RoundButton(defaultActiveButton) {
|
changePhoneButton: RoundButton(defaultActiveButton) {
|
||||||
width: 256px;
|
width: 256px;
|
||||||
}
|
}
|
||||||
changePhoneButtonPadding: margins(0px, 32px, 0px, 44px);
|
|
||||||
changePhoneTitle: FlatLabel(boxTitle) {
|
changePhoneTitle: FlatLabel(boxTitle) {
|
||||||
}
|
}
|
||||||
changePhoneTitlePadding: margins(0px, 8px, 0px, 8px);
|
changePhoneTitlePadding: margins(0px, 8px, 0px, 8px);
|
||||||
@@ -550,18 +544,6 @@ changePhoneError: FlatLabel(changePhoneLabel) {
|
|||||||
|
|
||||||
normalBoxLottieSize: size(120px, 120px);
|
normalBoxLottieSize: size(120px, 120px);
|
||||||
|
|
||||||
adminLogFilterUserpicLeft: 15px;
|
|
||||||
adminLogFilterLittleSkip: 16px;
|
|
||||||
adminLogFilterCheckbox: Checkbox(defaultBoxCheckbox) {
|
|
||||||
style: TextStyle(boxTextStyle) {
|
|
||||||
font: font(boxFontSize semibold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adminLogFilterSkip: 32px;
|
|
||||||
adminLogFilterUserCheckbox: Checkbox(defaultBoxCheckbox) {
|
|
||||||
margin: margins(8px, 6px, 8px, 6px);
|
|
||||||
checkPosition: point(8px, 6px);
|
|
||||||
}
|
|
||||||
rightsCheckbox: Checkbox(defaultCheckbox) {
|
rightsCheckbox: Checkbox(defaultCheckbox) {
|
||||||
textPosition: point(10px, 1px);
|
textPosition: point(10px, 1px);
|
||||||
rippleBg: attentionButtonBgOver;
|
rippleBg: attentionButtonBgOver;
|
||||||
@@ -588,7 +570,6 @@ rightsButtonToggleWidth: 70px;
|
|||||||
rightsDividerMargin: margins(0px, 0px, 0px, 20px);
|
rightsDividerMargin: margins(0px, 0px, 0px, 20px);
|
||||||
rightsHeaderMargin: margins(22px, 13px, 22px, 7px);
|
rightsHeaderMargin: margins(22px, 13px, 22px, 7px);
|
||||||
rightsToggleMargin: margins(22px, 8px, 22px, 8px);
|
rightsToggleMargin: margins(22px, 8px, 22px, 8px);
|
||||||
rightsAboutMargin: margins(22px, 8px, 22px, 8px);
|
|
||||||
rightsPhotoButton: UserpicButton(defaultUserpicButton) {
|
rightsPhotoButton: UserpicButton(defaultUserpicButton) {
|
||||||
size: size(60px, 60px);
|
size: size(60px, 60px);
|
||||||
photoSize: 60px;
|
photoSize: 60px;
|
||||||
@@ -1001,8 +982,6 @@ contactsWithStories: PeerList(peerListBox) {
|
|||||||
nameFgChecked: contactsNameFg;
|
nameFgChecked: contactsNameFg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storiesReadLineTwice: 2px;
|
|
||||||
storiesUnreadLineTwice: 4px;
|
|
||||||
requestsAcceptButton: RoundButton(defaultActiveButton) {
|
requestsAcceptButton: RoundButton(defaultActiveButton) {
|
||||||
width: -28px;
|
width: -28px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -1087,7 +1066,6 @@ collectibleHeaderPadding: margins(24px, 16px, 24px, 12px);
|
|||||||
collectibleOwnerPadding: margins(24px, 4px, 24px, 8px);
|
collectibleOwnerPadding: margins(24px, 4px, 24px, 8px);
|
||||||
collectibleInfo: inviteForbiddenInfo;
|
collectibleInfo: inviteForbiddenInfo;
|
||||||
collectibleInfoPadding: margins(24px, 12px, 24px, 12px);
|
collectibleInfoPadding: margins(24px, 12px, 24px, 12px);
|
||||||
collectibleInfoTonMargins: margins(0px, 3px, 0px, 0px);
|
|
||||||
collectibleMore: RoundButton(defaultActiveButton) {
|
collectibleMore: RoundButton(defaultActiveButton) {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
textTop: 9px;
|
textTop: 9px;
|
||||||
@@ -1110,11 +1088,18 @@ moderateBoxUserpic: UserpicButton(defaultUserpicButton) {
|
|||||||
photoSize: 34px;
|
photoSize: 34px;
|
||||||
photoPosition: point(0px, 4px);
|
photoPosition: point(0px, 4px);
|
||||||
}
|
}
|
||||||
moderateBoxExpand: icon {{ "chat/reply_type_group", boxTextFg }};
|
moderateBoxExpand: IconEmoji {
|
||||||
|
icon: icon {{ "chat/reply_type_group", boxTextFg }};
|
||||||
|
padding: margins(1px, 3px, 1px, 0px);
|
||||||
|
useIconColor: true;
|
||||||
|
}
|
||||||
moderateBoxExpandHeight: 20px;
|
moderateBoxExpandHeight: 20px;
|
||||||
moderateBoxExpandRight: 10px;
|
moderateBoxExpandRight: 10px;
|
||||||
moderateBoxExpandInnerSkip: 2px;
|
moderateBoxExpandInnerSkip: 2px;
|
||||||
moderateBoxExpandFont: font(11px);
|
moderateBoxExpandFont: font(11px);
|
||||||
|
moderateBoxExpandTextStyle: TextStyle(boxTextStyle) {
|
||||||
|
font: moderateBoxExpandFont;
|
||||||
|
}
|
||||||
moderateBoxExpandToggleSize: 4px;
|
moderateBoxExpandToggleSize: 4px;
|
||||||
moderateBoxExpandToggleFourStrokes: 3px;
|
moderateBoxExpandToggleFourStrokes: 3px;
|
||||||
moderateBoxExpandIcon: IconEmoji{
|
moderateBoxExpandIcon: IconEmoji{
|
||||||
@@ -1134,7 +1119,6 @@ moderateBoxDividerLabel: FlatLabel(boxDividerLabel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
profileQrFont: font(fsize bold);
|
profileQrFont: font(fsize bold);
|
||||||
profileQrCenterSize: 34px;
|
|
||||||
profileQrBackgroundRadius: 12px;
|
profileQrBackgroundRadius: 12px;
|
||||||
profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
|
profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
|
||||||
profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px);
|
profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px);
|
||||||
@@ -1147,13 +1131,6 @@ foldersMenu: PopupMenu(popupMenuWithIcons) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fakeUserpicButton: UserpicButton(defaultUserpicButton) {
|
|
||||||
size: size(1px, 1px);
|
|
||||||
photoSize: 1px;
|
|
||||||
changeIcon: icon {{ "settings/photo", transparent }};
|
|
||||||
uploadBg: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
moderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {
|
moderateCommonGroupsCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) {
|
||||||
imageRadius: 12px;
|
imageRadius: 12px;
|
||||||
imageSmallRadius: 11px;
|
imageSmallRadius: 11px;
|
||||||
@@ -1213,7 +1190,6 @@ createBotStatusLabel: FlatLabel(aboutRevokePublicLabel) {
|
|||||||
maxHeight: 20px;
|
maxHeight: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
aiComposeBoxSectionSkip: 8px;
|
|
||||||
aiComposeBoxStyleTabsSkip: 8px;
|
aiComposeBoxStyleTabsSkip: 8px;
|
||||||
aiComposeContentMargin: margins(16px, 0px, 16px, 0px);
|
aiComposeContentMargin: margins(16px, 0px, 16px, 0px);
|
||||||
|
|
||||||
@@ -1277,12 +1253,121 @@ aiComposeBadge: RoundButton(customEmojiTextBadge) {
|
|||||||
}
|
}
|
||||||
aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px);
|
aiComposeBadgeMargin: margins(0px, 2px, 0px, 0px);
|
||||||
|
|
||||||
|
aiComposeAddStyleIcon: icon {{ "menu/edit_stars_add", aiComposeButtonFg }};
|
||||||
|
aiComposeAddStyleIconOver: icon {{ "menu/edit_stars_add", aiComposeButtonFgActive }};
|
||||||
|
|
||||||
|
aiToneIconPreviewSize: 80px;
|
||||||
|
aiToneIconPreviewBottomSkip: 12px;
|
||||||
|
aiToneIconPreviewTopSkip: 4px;
|
||||||
|
aiToneIconPreviewBg: boxBg;
|
||||||
|
aiToneIconPreviewInnerSize: 54px;
|
||||||
|
|
||||||
|
aiToneIconPreviewPlaceholder: icon {{ "chat/ai_style_tone", windowSubTextFg }};
|
||||||
|
|
||||||
|
aiToneFieldBg: boxBg;
|
||||||
|
aiToneFieldRadius: 12px;
|
||||||
|
aiToneFieldPadding: margins(16px, 12px, 16px, 12px);
|
||||||
|
aiToneFieldsMargin: margins(16px, 0px, 16px, 0px);
|
||||||
|
aiToneFieldsSkip: 8px;
|
||||||
|
|
||||||
|
aiToneNameField: InputField(defaultInputField) {
|
||||||
|
textBg: transparent;
|
||||||
|
textBgActive: transparent;
|
||||||
|
textMargins: margins(16px, 12px, 16px, 12px);
|
||||||
|
border: 0px;
|
||||||
|
borderActive: 0px;
|
||||||
|
heightMin: 44px;
|
||||||
|
}
|
||||||
|
aiTonePromptField: InputField(newGroupDescription) {
|
||||||
|
textBg: transparent;
|
||||||
|
textBgActive: transparent;
|
||||||
|
textMargins: margins(16px, 12px, 16px, 12px);
|
||||||
|
border: 0px;
|
||||||
|
borderActive: 0px;
|
||||||
|
heightMin: 140px;
|
||||||
|
heightMax: 240px;
|
||||||
|
}
|
||||||
|
aiTonePlaceholderLabel: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
minWidth: 80px;
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(14px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aiToneAuthorCheckboxMargin: margins(0px, 12px, 0px, 8px);
|
||||||
|
aiToneDeleteButton: RoundButton(defaultActiveButton) {
|
||||||
|
textFg: attentionButtonFg;
|
||||||
|
textFgOver: attentionButtonFg;
|
||||||
|
textBg: boxBg;
|
||||||
|
textBgOver: boxBg;
|
||||||
|
height: 42px;
|
||||||
|
textTop: 12px;
|
||||||
|
style: semiboldTextStyle;
|
||||||
|
ripple: defaultRippleAnimation;
|
||||||
|
}
|
||||||
|
aiToneDeleteButtonMargin: margins(16px, 8px, 16px, 0px);
|
||||||
|
aiComposeToneToastIconSize: size(32px, 32px);
|
||||||
|
aiComposeToneToastIconPadding: margins(12px, 6px, 12px, 6px);
|
||||||
|
|
||||||
|
aiTonePreviewTitleLabel: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowFg;
|
||||||
|
minWidth: 0px;
|
||||||
|
maxHeight: 28px;
|
||||||
|
align: align(top);
|
||||||
|
style: TextStyle(semiboldTextStyle) {
|
||||||
|
font: font(17px semibold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aiTonePreviewTitleMargin: margins(16px, 6px, 16px, 0px);
|
||||||
|
|
||||||
|
aiTonePreviewAboutLabel: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
minWidth: 20px;
|
||||||
|
align: align(top);
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(14px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aiTonePreviewAboutMargin: margins(28px, 6px, 28px, 6px);
|
||||||
|
|
||||||
|
aiTonePreviewAttributionLabel: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
minWidth: 0px;
|
||||||
|
align: align(top);
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(12px);
|
||||||
|
linkUnderline: kLinkUnderlineActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aiTonePreviewAttributionMargin: margins(16px, 10px, 16px, 8px);
|
||||||
|
|
||||||
|
aiTonePreviewExampleCardBg: boxBg;
|
||||||
|
aiTonePreviewExampleCardRadius: 22px;
|
||||||
|
aiTonePreviewExampleCardPadding: margins(14px, 12px, 16px, 12px);
|
||||||
|
aiTonePreviewExampleCardSectionSkip: 24px;
|
||||||
|
aiTonePreviewExampleCardTitleSkip: 6px;
|
||||||
|
aiTonePreviewExampleCardMargin: margins(16px, 6px, 16px, 0px);
|
||||||
|
aiTonePreviewBottomSkip: 6px;
|
||||||
|
|
||||||
|
aiTonePreviewAnotherExampleButton: RoundButton(defaultLightButton) {
|
||||||
|
width: -12px;
|
||||||
|
height: 26px;
|
||||||
|
radius: 8px;
|
||||||
|
textTop: 4px;
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(12px semibold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aiTonePreviewAnotherExampleIcon: IconEmoji {
|
||||||
|
icon: icon {{ "chat/refresh-18x18", lightButtonFg }};
|
||||||
|
padding: margins(0px, 1px, 4px, 0px);
|
||||||
|
}
|
||||||
|
aiComposeToneRemovedToastIcon: icon {{ "menu/delete", toastFg }};
|
||||||
|
|
||||||
aiComposeCardBg: boxBg;
|
aiComposeCardBg: boxBg;
|
||||||
aiComposeCardRadius: 22px;
|
aiComposeCardRadius: 22px;
|
||||||
aiComposeCardPadding: margins(12px, 16px, 16px, 16px);
|
aiComposeCardPadding: margins(12px, 16px, 16px, 16px);
|
||||||
aiComposeCardDivider: shadowFg;
|
|
||||||
aiComposeCardSectionSkip: 12px;
|
aiComposeCardSectionSkip: 12px;
|
||||||
aiComposeCardTextSkip: 0px;
|
|
||||||
aiComposeCardControlSkip: 8px;
|
aiComposeCardControlSkip: 8px;
|
||||||
aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) {
|
aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) {
|
||||||
textFg: windowFg;
|
textFg: windowFg;
|
||||||
@@ -1290,6 +1375,8 @@ aiComposeCardTitle: FlatLabel(defaultSubsectionTitle) {
|
|||||||
maxHeight: 22px;
|
maxHeight: 22px;
|
||||||
}
|
}
|
||||||
aiComposeEmojifyCheckbox: Checkbox(defaultBoxCheckbox) {
|
aiComposeEmojifyCheckbox: Checkbox(defaultBoxCheckbox) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
textFgActive: windowSubTextFg;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1312,6 +1399,17 @@ aiComposeCopyButton: IconButton(aiComposeExpandButton) {
|
|||||||
iconOver: aiComposeCopyIcon;
|
iconOver: aiComposeCopyIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aiComposeAuthorLabel: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
minWidth: 0px;
|
||||||
|
maxHeight: 20px;
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(12px);
|
||||||
|
linkUnderline: kLinkUnderlineActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aiComposeAuthorLabelTop: 8px;
|
||||||
|
|
||||||
aiComposeBoxButton: RoundButton(defaultActiveButton) {
|
aiComposeBoxButton: RoundButton(defaultActiveButton) {
|
||||||
height: 42px;
|
height: 42px;
|
||||||
textTop: 12px;
|
textTop: 12px;
|
||||||
@@ -1337,3 +1435,4 @@ aiComposeBoxInfoButton: IconButton(boxTitleClose) {
|
|||||||
iconOver: icon {{ "menu/info", boxTitleCloseFgOver }};
|
iconOver: icon {{ "menu/info", boxTitleCloseFgOver }};
|
||||||
ripple: defaultRippleAnimation;
|
ripple: defaultRippleAnimation;
|
||||||
}
|
}
|
||||||
|
aiComposeShadowOpacity: 0.3;
|
||||||
|
|||||||
@@ -9,20 +9,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
#include "api/api_compose_with_ai.h"
|
#include "api/api_compose_with_ai.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
|
#include "boxes/create_ai_tone_box.h"
|
||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
|
#include "boxes/share_box.h"
|
||||||
#include "chat_helpers/compose/compose_show.h"
|
#include "chat_helpers/compose/compose_show.h"
|
||||||
#include "chat_helpers/stickers_lottie.h"
|
#include "chat_helpers/stickers_lottie.h"
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
#include "core/click_handler_types.h"
|
#include "core/click_handler_types.h"
|
||||||
#include "core/core_settings.h"
|
#include "core/core_settings.h"
|
||||||
#include "core/ui_integration.h"
|
#include "core/ui_integration.h"
|
||||||
|
#include "data/data_ai_compose_tones.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "data/stickers/data_custom_emoji.h"
|
#include "data/stickers/data_custom_emoji.h"
|
||||||
#include "data/data_session.h"
|
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "main/session/session_show.h"
|
#include "main/session/session_show.h"
|
||||||
#include "main/main_app_config.h"
|
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "settings/sections/settings_premium.h"
|
#include "settings/sections/settings_premium.h"
|
||||||
#include "spellcheck/platform/platform_language.h"
|
#include "spellcheck/platform/platform_language.h"
|
||||||
@@ -46,11 +48,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
#include "ui/widgets/labels.h"
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/widgets/menu/menu_action.h"
|
||||||
|
#include "ui/widgets/popup_menu.h"
|
||||||
#include "ui/widgets/tooltip.h"
|
#include "ui/widgets/tooltip.h"
|
||||||
#include "styles/style_basic.h"
|
#include "styles/style_basic.h"
|
||||||
#include "styles/style_boxes.h"
|
#include "styles/style_boxes.h"
|
||||||
#include "styles/style_chat_helpers.h"
|
#include "styles/style_chat_helpers.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
|
#include "styles/style_menu_icons.h"
|
||||||
#include "styles/style_widgets.h"
|
#include "styles/style_widgets.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -214,20 +219,22 @@ enum class CardState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor(
|
[[nodiscard]] Ui::LabeledEmojiTab ResolveStyleDescriptor(
|
||||||
const Main::AppConfig::AiComposeStyle &style) {
|
const Data::AiComposeTone &tone) {
|
||||||
return {
|
return {
|
||||||
.id = style.type,
|
.id = tone.isDefault ? tone.defaultType : QString::number(tone.id),
|
||||||
.label = style.title,
|
.label = tone.title,
|
||||||
.customEmojiData = Data::SerializeCustomEmojiId(style.emojiId),
|
.customEmojiData = tone.emojiId
|
||||||
|
? Data::SerializeCustomEmojiId(tone.emojiId)
|
||||||
|
: QString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Ui::LabeledEmojiTab> ResolveStyleDescriptors(
|
[[nodiscard]] std::vector<Ui::LabeledEmojiTab> ResolveStyleDescriptors(
|
||||||
const std::vector<Main::AppConfig::AiComposeStyle> &styles) {
|
const std::vector<Data::AiComposeTone> &tones) {
|
||||||
auto result = std::vector<Ui::LabeledEmojiTab>();
|
auto result = std::vector<Ui::LabeledEmojiTab>();
|
||||||
result.reserve(styles.size());
|
result.reserve(tones.size());
|
||||||
for (const auto &style : styles) {
|
for (const auto &tone : tones) {
|
||||||
result.push_back(ResolveStyleDescriptor(style));
|
result.push_back(ResolveStyleDescriptor(tone));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -249,6 +256,17 @@ enum class CardState {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto WithAddStyleTab(std::vector<Ui::LabeledEmojiTab> tabs)
|
||||||
|
-> std::vector<Ui::LabeledEmojiTab> {
|
||||||
|
tabs.push_back({
|
||||||
|
.id = u"_add_style"_q,
|
||||||
|
.label = tr::lng_ai_compose_tone_create(tr::now),
|
||||||
|
.icon = &st::aiComposeAddStyleIcon,
|
||||||
|
.iconActive = &st::aiComposeAddStyleIconOver,
|
||||||
|
});
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] TextWithEntities LoadingTitleSparkle(
|
[[nodiscard]] TextWithEntities LoadingTitleSparkle(
|
||||||
not_null<Main::Session*> session) {
|
not_null<Main::Session*> session) {
|
||||||
const auto sparkles = ChatHelpers::GenerateLocalTgsSticker(
|
const auto sparkles = ChatHelpers::GenerateLocalTgsSticker(
|
||||||
@@ -364,6 +382,7 @@ public:
|
|||||||
[[nodiscard]] bool hasResult() const;
|
[[nodiscard]] bool hasResult() const;
|
||||||
[[nodiscard]] const TextWithEntities &result() const;
|
[[nodiscard]] const TextWithEntities &result() const;
|
||||||
[[nodiscard]] const std::vector<Ui::LabeledEmojiTab> &stylesData() const;
|
[[nodiscard]] const std::vector<Ui::LabeledEmojiTab> &stylesData() const;
|
||||||
|
[[nodiscard]] const std::vector<Data::AiComposeTone> &tones() const;
|
||||||
void setReadyChangedCallback(Fn<void(bool)> callback);
|
void setReadyChangedCallback(Fn<void(bool)> callback);
|
||||||
void setLoadingChangedCallback(Fn<void(bool)> callback);
|
void setLoadingChangedCallback(Fn<void(bool)> callback);
|
||||||
void setPremiumFloodCallback(Fn<void()> callback);
|
void setPremiumFloodCallback(Fn<void()> callback);
|
||||||
@@ -373,6 +392,8 @@ public:
|
|||||||
[[nodiscard]] bool hasStyleSelection() const;
|
[[nodiscard]] bool hasStyleSelection() const;
|
||||||
void setModeTabs(not_null<ComposeAiModeTabs*> tabs);
|
void setModeTabs(not_null<ComposeAiModeTabs*> tabs);
|
||||||
void setStyleTabs(not_null<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>*> stylesWrap);
|
void setStyleTabs(not_null<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>*> stylesWrap);
|
||||||
|
void refreshTones();
|
||||||
|
void selectToneById(uint64 id);
|
||||||
void start();
|
void start();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -390,6 +411,7 @@ private:
|
|||||||
void resetState(CardState state);
|
void resetState(CardState state);
|
||||||
void applyResult(Api::ComposeWithAi::Result &&result);
|
void applyResult(Api::ComposeWithAi::Result &&result);
|
||||||
void showError(const QString &error = {});
|
void showError(const QString &error = {});
|
||||||
|
void setAuthorId(UserId authorId);
|
||||||
void notifyLoadingChanged();
|
void notifyLoadingChanged();
|
||||||
void notifyReadyChanged();
|
void notifyReadyChanged();
|
||||||
[[nodiscard]] QString currentTranslateStyle() const;
|
[[nodiscard]] QString currentTranslateStyle() const;
|
||||||
@@ -400,12 +422,14 @@ private:
|
|||||||
const TextWithEntities _original;
|
const TextWithEntities _original;
|
||||||
const LanguageId _detectedFrom;
|
const LanguageId _detectedFrom;
|
||||||
LanguageId _to;
|
LanguageId _to;
|
||||||
const std::vector<Ui::LabeledEmojiTab> _stylesData;
|
std::vector<Data::AiComposeTone> _tones;
|
||||||
const std::vector<Ui::LabeledEmojiTab> _translateStylesData;
|
std::vector<Ui::LabeledEmojiTab> _stylesData;
|
||||||
|
std::vector<Ui::LabeledEmojiTab> _translateStylesData;
|
||||||
QPointer<ComposeAiModeTabs> _tabs;
|
QPointer<ComposeAiModeTabs> _tabs;
|
||||||
QPointer<Ui::LabeledEmojiScrollTabs> _styles;
|
QPointer<Ui::LabeledEmojiScrollTabs> _styles;
|
||||||
QPointer<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>> _stylesWrap;
|
QPointer<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>> _stylesWrap;
|
||||||
const not_null<ComposeAiPreviewCard*> _preview;
|
const not_null<ComposeAiPreviewCard*> _preview;
|
||||||
|
const not_null<Ui::FlatLabel*> _authorLabel;
|
||||||
Fn<void(bool)> _readyChanged;
|
Fn<void(bool)> _readyChanged;
|
||||||
Fn<void(bool)> _loadingChanged;
|
Fn<void(bool)> _loadingChanged;
|
||||||
Fn<void()> _premiumFlood;
|
Fn<void()> _premiumFlood;
|
||||||
@@ -414,6 +438,7 @@ private:
|
|||||||
ComposeAiMode _mode = ComposeAiMode::Style;
|
ComposeAiMode _mode = ComposeAiMode::Style;
|
||||||
int _styleIndex = -1;
|
int _styleIndex = -1;
|
||||||
int _translateStyleIndex = 0;
|
int _translateStyleIndex = 0;
|
||||||
|
UserId _authorId = UserId(0);
|
||||||
bool _emojify = false;
|
bool _emojify = false;
|
||||||
CardState _state = CardState::Waiting;
|
CardState _state = CardState::Waiting;
|
||||||
mtpRequestId _requestId = 0;
|
mtpRequestId _requestId = 0;
|
||||||
@@ -432,6 +457,7 @@ ComposeAiModeButton::ComposeAiModeButton(
|
|||||||
, _mode(mode)
|
, _mode(mode)
|
||||||
, _label(std::move(label)) {
|
, _label(std::move(label)) {
|
||||||
setCursor(style::cur_pointer);
|
setCursor(style::cur_pointer);
|
||||||
|
setAccessibleName(_label);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeAiModeButton::setSelected(bool selected) {
|
void ComposeAiModeButton::setSelected(bool selected) {
|
||||||
@@ -638,6 +664,7 @@ ComposeAiPreviewCard::ComposeAiPreviewCard(
|
|||||||
_copyCallback();
|
_copyCallback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_copy->setAccessibleName(tr::lng_sr_ai_compose_copy_result(tr::now));
|
||||||
_emojify->checkedChanges(
|
_emojify->checkedChanges(
|
||||||
) | rpl::on_next([=](bool checked) {
|
) | rpl::on_next([=](bool checked) {
|
||||||
if (_emojifyChanged) {
|
if (_emojifyChanged) {
|
||||||
@@ -863,9 +890,7 @@ int ComposeAiPreviewCard::resizeGetHeight(int newWidth) {
|
|||||||
? _resultBody->st().style.lineHeight
|
? _resultBody->st().style.lineHeight
|
||||||
: _resultBody->st().style.font->height;
|
: _resultBody->st().style.font->height;
|
||||||
if (!_copy->isHidden()) {
|
if (!_copy->isHidden()) {
|
||||||
_resultBody->setSkipBlock(
|
_resultBody->setSkipBlock(_copy->width(), lineHeight);
|
||||||
_copy->width(),
|
|
||||||
lineHeight);
|
|
||||||
} else {
|
} else {
|
||||||
_resultBody->setSkipBlock(0, 0);
|
_resultBody->setSkipBlock(0, 0);
|
||||||
}
|
}
|
||||||
@@ -898,7 +923,9 @@ void ComposeAiPreviewCard::paintEvent(QPaintEvent *e) {
|
|||||||
st::aiComposeCardRadius);
|
st::aiComposeCardRadius);
|
||||||
if (_dividerVisible) {
|
if (_dividerVisible) {
|
||||||
p.setBrush(Qt::NoBrush);
|
p.setBrush(Qt::NoBrush);
|
||||||
p.setPen(st::aiComposeCardDivider);
|
auto color = st::windowSubTextFg->c;
|
||||||
|
color.setAlphaF(st::aiComposeShadowOpacity);
|
||||||
|
p.setPen(color);
|
||||||
p.drawLine(
|
p.drawLine(
|
||||||
st::aiComposeCardPadding.left(),
|
st::aiComposeCardPadding.left(),
|
||||||
_dividerTop,
|
_dividerTop,
|
||||||
@@ -920,6 +947,9 @@ void ComposeAiPreviewCard::updateOriginalToggleIcon() {
|
|||||||
_originalToggle->setIconOverride(
|
_originalToggle->setIconOverride(
|
||||||
_originalExpanded ? &st::aiComposeCollapseIcon : nullptr,
|
_originalExpanded ? &st::aiComposeCollapseIcon : nullptr,
|
||||||
_originalExpanded ? &st::aiComposeCollapseIcon : nullptr);
|
_originalExpanded ? &st::aiComposeCollapseIcon : nullptr);
|
||||||
|
_originalToggle->setAccessibleName(_originalExpanded
|
||||||
|
? tr::lng_sr_ai_compose_collapse_original(tr::now)
|
||||||
|
: tr::lng_sr_ai_compose_expand_original(tr::now));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeAiContent
|
// ComposeAiContent
|
||||||
@@ -934,15 +964,18 @@ ComposeAiContent::ComposeAiContent(
|
|||||||
, _original(std::move(args.text))
|
, _original(std::move(args.text))
|
||||||
, _detectedFrom(Platform::Language::Recognize(_original.text))
|
, _detectedFrom(Platform::Language::Recognize(_original.text))
|
||||||
, _to(DefaultAiTranslateTo(_detectedFrom))
|
, _to(DefaultAiTranslateTo(_detectedFrom))
|
||||||
, _stylesData(ResolveStyleDescriptors(
|
, _tones(_session->data().aiComposeTones().list())
|
||||||
_session->appConfig().aiComposeStyles()))
|
, _stylesData(ResolveStyleDescriptors(_tones))
|
||||||
, _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData))
|
, _translateStylesData(ResolveTranslateStyleDescriptors(_session, _stylesData))
|
||||||
, _preview(
|
, _preview(
|
||||||
Ui::CreateChild<ComposeAiPreviewCard>(
|
Ui::CreateChild<ComposeAiPreviewCard>(
|
||||||
this,
|
this,
|
||||||
_session,
|
_session,
|
||||||
_original,
|
_original,
|
||||||
args.chatStyle)) {
|
args.chatStyle))
|
||||||
|
, _authorLabel(Ui::CreateChild<Ui::FlatLabel>(
|
||||||
|
this,
|
||||||
|
st::aiComposeAuthorLabel)) {
|
||||||
_preview->setResizeCallback([=] { refreshLayout(); });
|
_preview->setResizeCallback([=] { refreshLayout(); });
|
||||||
_preview->setChooseCallback([=] { chooseLanguage(); });
|
_preview->setChooseCallback([=] { chooseLanguage(); });
|
||||||
_preview->setCopyCallback([=] { copyResult(); });
|
_preview->setCopyCallback([=] { copyResult(); });
|
||||||
@@ -953,6 +986,26 @@ ComposeAiContent::ComposeAiContent(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
_preview->setShow(_box->uiShow());
|
_preview->setShow(_box->uiShow());
|
||||||
|
_authorLabel->setVisible(false);
|
||||||
|
_authorLabel->heightValue(
|
||||||
|
) | rpl::skip(1) | rpl::on_next([=] {
|
||||||
|
refreshLayout();
|
||||||
|
}, lifetime());
|
||||||
|
const auto show = _box->uiShow();
|
||||||
|
_authorLabel->setClickHandlerFilter([=](
|
||||||
|
const ClickHandlerPtr &handler,
|
||||||
|
Qt::MouseButton button) {
|
||||||
|
if (dynamic_cast<Ui::Text::PreClickHandler*>(handler.get())) {
|
||||||
|
ActivateClickHandler(_authorLabel, handler, ClickContext{
|
||||||
|
.button = button,
|
||||||
|
.other = QVariant::fromValue(ClickHandlerContext{
|
||||||
|
.show = show,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeAiContent::~ComposeAiContent() {
|
ComposeAiContent::~ComposeAiContent() {
|
||||||
@@ -971,6 +1024,10 @@ const std::vector<Ui::LabeledEmojiTab> &ComposeAiContent::stylesData() const {
|
|||||||
return _stylesData;
|
return _stylesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<Data::AiComposeTone> &ComposeAiContent::tones() const {
|
||||||
|
return _tones;
|
||||||
|
}
|
||||||
|
|
||||||
void ComposeAiContent::setReadyChangedCallback(Fn<void(bool)> callback) {
|
void ComposeAiContent::setReadyChangedCallback(Fn<void(bool)> callback) {
|
||||||
_readyChanged = std::move(callback);
|
_readyChanged = std::move(callback);
|
||||||
}
|
}
|
||||||
@@ -994,7 +1051,7 @@ void ComposeAiContent::setStyleTabs(
|
|||||||
_stylesWrap->setDuration(0);
|
_stylesWrap->setDuration(0);
|
||||||
_styles = stylesWrap->entity();
|
_styles = stylesWrap->entity();
|
||||||
_styles->setChangedCallback([=](int index) {
|
_styles->setChangedCallback([=](int index) {
|
||||||
if (index >= 0 && index < int(_stylesData.size())) {
|
if (index >= 0 && index < int(_tones.size())) {
|
||||||
const auto wasNoSelection = (_styleIndex < 0);
|
const auto wasNoSelection = (_styleIndex < 0);
|
||||||
_styleIndex = index;
|
_styleIndex = index;
|
||||||
updateTitles();
|
updateTitles();
|
||||||
@@ -1004,12 +1061,76 @@ void ComposeAiContent::setStyleTabs(
|
|||||||
_styleSelected();
|
_styleSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (index == int(_tones.size())) {
|
||||||
|
_styles->setActive(_styleIndex);
|
||||||
|
_box->uiShow()->show(Box(
|
||||||
|
CreateAiToneBox,
|
||||||
|
_session,
|
||||||
|
crl::guard(this, [=](Data::AiComposeTone tone) {
|
||||||
|
selectToneById(tone.id);
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_styles->setActive(_styleIndex);
|
_styles->setActive(_styleIndex);
|
||||||
_stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant);
|
_stylesWrap->toggle(_mode == ComposeAiMode::Style, anim::type::instant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComposeAiContent::refreshTones() {
|
||||||
|
auto previousKey = QString();
|
||||||
|
auto hadSelection = false;
|
||||||
|
if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) {
|
||||||
|
const auto &prev = _tones[_styleIndex];
|
||||||
|
previousKey = prev.isDefault
|
||||||
|
? prev.defaultType
|
||||||
|
: QString::number(prev.id);
|
||||||
|
hadSelection = true;
|
||||||
|
}
|
||||||
|
_tones = _session->data().aiComposeTones().list();
|
||||||
|
_stylesData = ResolveStyleDescriptors(_tones);
|
||||||
|
_translateStylesData = ResolveTranslateStyleDescriptors(
|
||||||
|
_session,
|
||||||
|
_stylesData);
|
||||||
|
auto remapped = -1;
|
||||||
|
if (hadSelection) {
|
||||||
|
for (auto i = 0; i != int(_tones.size()); ++i) {
|
||||||
|
const auto &tone = _tones[i];
|
||||||
|
const auto key = tone.isDefault
|
||||||
|
? tone.defaultType
|
||||||
|
: QString::number(tone.id);
|
||||||
|
if (key == previousKey) {
|
||||||
|
remapped = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_styleIndex = remapped;
|
||||||
|
if (_mode == ComposeAiMode::Style && hadSelection && _styleIndex < 0) {
|
||||||
|
request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComposeAiContent::selectToneById(uint64 id) {
|
||||||
|
for (auto i = 0; i != int(_tones.size()); ++i) {
|
||||||
|
const auto &tone = _tones[i];
|
||||||
|
if (!tone.isDefault && tone.id == id) {
|
||||||
|
const auto wasNoSelection = (_styleIndex < 0);
|
||||||
|
_styleIndex = i;
|
||||||
|
updateTitles();
|
||||||
|
if (_styles) {
|
||||||
|
_styles->setActive(_styleIndex);
|
||||||
|
_styles->scrollToActive();
|
||||||
|
}
|
||||||
|
if (_mode == ComposeAiMode::Style) {
|
||||||
|
request();
|
||||||
|
if (wasNoSelection && _styleSelected) {
|
||||||
|
_styleSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ComposeAiContent::start() {
|
void ComposeAiContent::start() {
|
||||||
updatePinnedTabs(anim::type::instant);
|
updatePinnedTabs(anim::type::instant);
|
||||||
updateTitles();
|
updateTitles();
|
||||||
@@ -1019,7 +1140,16 @@ void ComposeAiContent::start() {
|
|||||||
int ComposeAiContent::resizeGetHeight(int newWidth) {
|
int ComposeAiContent::resizeGetHeight(int newWidth) {
|
||||||
_preview->resizeToWidth(newWidth);
|
_preview->resizeToWidth(newWidth);
|
||||||
_preview->moveToLeft(0, 0, newWidth);
|
_preview->moveToLeft(0, 0, newWidth);
|
||||||
return _preview->height();
|
auto y = _preview->height();
|
||||||
|
if (!_authorLabel->isHidden()) {
|
||||||
|
_authorLabel->resizeToWidth(newWidth);
|
||||||
|
_authorLabel->moveToLeft(
|
||||||
|
0,
|
||||||
|
y + st::aiComposeAuthorLabelTop,
|
||||||
|
newWidth);
|
||||||
|
y += st::aiComposeAuthorLabelTop + _authorLabel->height();
|
||||||
|
}
|
||||||
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeAiContent::refreshLayout() {
|
void ComposeAiContent::refreshLayout() {
|
||||||
@@ -1105,6 +1235,7 @@ void ComposeAiContent::setMode(ComposeAiMode mode) {
|
|||||||
_mode = mode;
|
_mode = mode;
|
||||||
_state = CardState::Waiting;
|
_state = CardState::Waiting;
|
||||||
_preview->setState(CardState::Waiting);
|
_preview->setState(CardState::Waiting);
|
||||||
|
setAuthorId(UserId(0));
|
||||||
notifyLoadingChanged();
|
notifyLoadingChanged();
|
||||||
if (_modeChanged) {
|
if (_modeChanged) {
|
||||||
_modeChanged(_mode);
|
_modeChanged(_mode);
|
||||||
@@ -1173,13 +1304,21 @@ void ComposeAiContent::request() {
|
|||||||
.emojify = (_mode != ComposeAiMode::Fix) && _emojify,
|
.emojify = (_mode != ComposeAiMode::Fix) && _emojify,
|
||||||
};
|
};
|
||||||
switch (_mode) {
|
switch (_mode) {
|
||||||
case ComposeAiMode::Translate:
|
case ComposeAiMode::Translate: {
|
||||||
request.translateToLang = _to.twoLetterCode();
|
request.translateToLang = _to.twoLetterCode();
|
||||||
request.changeTone = currentTranslateStyle();
|
const auto style = currentTranslateStyle();
|
||||||
break;
|
if (!style.isEmpty()) {
|
||||||
|
request.setDefaultTone(style);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
case ComposeAiMode::Style:
|
case ComposeAiMode::Style:
|
||||||
if (_styleIndex >= 0) {
|
if (_styleIndex >= 0 && _styleIndex < int(_tones.size())) {
|
||||||
request.changeTone = _stylesData[_styleIndex].id;
|
const auto &tone = _tones[_styleIndex];
|
||||||
|
if (tone.isDefault) {
|
||||||
|
request.setDefaultTone(tone.defaultType);
|
||||||
|
} else {
|
||||||
|
request.setCustomTone(tone.id, tone.accessHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ComposeAiMode::Fix:
|
case ComposeAiMode::Fix:
|
||||||
@@ -1211,9 +1350,43 @@ void ComposeAiContent::request() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComposeAiContent::setAuthorId(UserId authorId) {
|
||||||
|
if (_authorId == authorId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_authorId = authorId;
|
||||||
|
if (const auto user = _session->data().userLoaded(authorId)) {
|
||||||
|
const auto name = user->shortName();
|
||||||
|
auto mention = tr::marked(name);
|
||||||
|
mention.entities.push_back(EntityInText(
|
||||||
|
EntityType::MentionName,
|
||||||
|
0,
|
||||||
|
name.size(),
|
||||||
|
TextUtilities::MentionNameDataFromFields({
|
||||||
|
.selfId = _session->userId().bare,
|
||||||
|
.userId = authorId.bare,
|
||||||
|
.accessHash = user->accessHash(),
|
||||||
|
})));
|
||||||
|
_authorLabel->setMarkedText(
|
||||||
|
tr::lng_ai_compose_author(
|
||||||
|
tr::now,
|
||||||
|
lt_user,
|
||||||
|
std::move(mention),
|
||||||
|
tr::marked),
|
||||||
|
Core::TextContext({ .session = _session }));
|
||||||
|
_authorLabel->setVisible(true);
|
||||||
|
} else {
|
||||||
|
_authorLabel->setMarkedText({});
|
||||||
|
_authorLabel->setVisible(false);
|
||||||
|
_authorId = UserId(0);
|
||||||
|
}
|
||||||
|
refreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
void ComposeAiContent::resetState(CardState state) {
|
void ComposeAiContent::resetState(CardState state) {
|
||||||
_state = state;
|
_state = state;
|
||||||
_result = {};
|
_result = {};
|
||||||
|
setAuthorId(UserId(0));
|
||||||
_preview->setState(state);
|
_preview->setState(state);
|
||||||
notifyLoadingChanged();
|
notifyLoadingChanged();
|
||||||
updateTitles();
|
updateTitles();
|
||||||
@@ -1234,6 +1407,13 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) {
|
|||||||
notifyLoadingChanged();
|
notifyLoadingChanged();
|
||||||
if (_state == CardState::Ready) {
|
if (_state == CardState::Ready) {
|
||||||
_preview->setResultText(std::move(display));
|
_preview->setResultText(std::move(display));
|
||||||
|
if (_mode == ComposeAiMode::Style
|
||||||
|
&& _styleIndex >= 0
|
||||||
|
&& _styleIndex < int(_tones.size())) {
|
||||||
|
setAuthorId(_tones[_styleIndex].authorId);
|
||||||
|
} else {
|
||||||
|
setAuthorId(UserId(0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateTitles();
|
updateTitles();
|
||||||
notifyReadyChanged();
|
notifyReadyChanged();
|
||||||
@@ -1242,6 +1422,7 @@ void ComposeAiContent::applyResult(Api::ComposeWithAi::Result &&result) {
|
|||||||
|
|
||||||
void ComposeAiContent::showError(const QString &error) {
|
void ComposeAiContent::showError(const QString &error) {
|
||||||
_state = CardState::Failed;
|
_state = CardState::Failed;
|
||||||
|
setAuthorId(UserId(0));
|
||||||
_preview->setState(CardState::Failed);
|
_preview->setState(CardState::Failed);
|
||||||
notifyLoadingChanged();
|
notifyLoadingChanged();
|
||||||
updateTitles();
|
updateTitles();
|
||||||
@@ -1264,6 +1445,9 @@ void ComposeAiContent::showError(const QString &error) {
|
|||||||
_premiumFlood();
|
_premiumFlood();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
} else if (error == u"INPUT_TEXT_TOO_LONG"_q) {
|
||||||
|
_box->showToast(tr::lng_ai_compose_error_too_long(tr::now));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
_box->showToast(error.isEmpty()
|
_box->showToast(error.isEmpty()
|
||||||
? tr::lng_ai_compose_error(tr::now)
|
? tr::lng_ai_compose_error(tr::now)
|
||||||
@@ -1320,7 +1504,12 @@ bool ComposeAiContent::hasStyleSelection() const {
|
|||||||
return _styleIndex >= 0;
|
return _styleIndex >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] Fn<void(bool)> SetupStyleTooltip(
|
struct StyleTooltipHandle {
|
||||||
|
QPointer<Ui::ImportantTooltip> tooltip;
|
||||||
|
Fn<void(bool)> updateVisibility;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] StyleTooltipHandle SetupStyleTooltip(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
not_null<Ui::RpWidget*> pinnedToTop,
|
not_null<Ui::RpWidget*> pinnedToTop,
|
||||||
not_null<Ui::RpWidget*> stylesWrap,
|
not_null<Ui::RpWidget*> stylesWrap,
|
||||||
@@ -1393,7 +1582,7 @@ bool ComposeAiContent::hasStyleSelection() const {
|
|||||||
}
|
}
|
||||||
}, tooltip->lifetime());
|
}, tooltip->lifetime());
|
||||||
|
|
||||||
return updateVisibility;
|
return { tooltip, updateVisibility };
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -1418,10 +1607,10 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
|
|||||||
const auto session = args.session;
|
const auto session = args.session;
|
||||||
box->addTopButton(st::aiComposeBoxClose, [=] {
|
box->addTopButton(st::aiComposeBoxClose, [=] {
|
||||||
box->closeBox();
|
box->closeBox();
|
||||||
});
|
})->setAccessibleName(tr::lng_close(tr::now));
|
||||||
box->addTopButton(st::aiComposeBoxInfoButton, [=] {
|
box->addTopButton(st::aiComposeBoxInfoButton, [=] {
|
||||||
box->uiShow()->show(Box(Ui::AboutCocoonBox));
|
box->uiShow()->show(Box(Ui::AboutCocoonBox));
|
||||||
});
|
})->setAccessibleName(tr::lng_sr_ai_compose_info(tr::now));
|
||||||
|
|
||||||
const auto body = box->verticalLayout();
|
const auto body = box->verticalLayout();
|
||||||
const auto tabsSkip = QMargins(0, 0, 0, st::aiComposeBoxStyleTabsSkip);
|
const auto tabsSkip = QMargins(0, 0, 0, st::aiComposeBoxStyleTabsSkip);
|
||||||
@@ -1433,26 +1622,112 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
|
|||||||
const auto content = body->add(
|
const auto content = body->add(
|
||||||
object_ptr<ComposeAiContent>(box, box, args),
|
object_ptr<ComposeAiContent>(box, box, args),
|
||||||
st::aiComposeContentMargin);
|
st::aiComposeContentMargin);
|
||||||
auto emojiFactory = session->data().customEmojiManager().factory(
|
const auto contextMenu = box->lifetime().make_state<
|
||||||
Data::CustomEmojiSizeTag::Large);
|
base::unique_qptr<Ui::PopupMenu>>();
|
||||||
const auto stylesWrap = pinnedToTop->add(
|
const auto stylesWrapHolder = box->lifetime().make_state<
|
||||||
object_ptr<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>>(
|
QPointer<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>>>();
|
||||||
|
const auto styleTooltipHolder = box->lifetime().make_state<
|
||||||
|
QPointer<Ui::ImportantTooltip>>();
|
||||||
|
const auto styleTooltipUpdater = box->lifetime().make_state<
|
||||||
|
Fn<void(bool)>>();
|
||||||
|
|
||||||
|
content->setModeTabs(tabs);
|
||||||
|
|
||||||
|
const auto rebuildStylesWrap = [=] {
|
||||||
|
auto savedScroll = -1;
|
||||||
|
if (const auto old = stylesWrapHolder->data()) {
|
||||||
|
savedScroll = old->entity()->scrollLeft();
|
||||||
|
delete old;
|
||||||
|
}
|
||||||
|
if (const auto old = styleTooltipHolder->data()) {
|
||||||
|
delete old;
|
||||||
|
}
|
||||||
|
auto emojiFactory = session->data().customEmojiManager().factory(
|
||||||
|
Data::CustomEmojiSizeTag::Large);
|
||||||
|
auto wrap = object_ptr<Ui::SlideWrap<Ui::LabeledEmojiScrollTabs>>(
|
||||||
pinnedToTop,
|
pinnedToTop,
|
||||||
object_ptr<Ui::LabeledEmojiScrollTabs>(
|
object_ptr<Ui::LabeledEmojiScrollTabs>(
|
||||||
pinnedToTop,
|
pinnedToTop,
|
||||||
content->stylesData(),
|
WithAddStyleTab(content->stylesData()),
|
||||||
std::move(emojiFactory)),
|
std::move(emojiFactory)),
|
||||||
tabsSkip),
|
tabsSkip);
|
||||||
st::aiComposeContentMargin);
|
const auto ptr = wrap.data();
|
||||||
stylesWrap->hide(anim::type::instant);
|
pinnedToTop->add(std::move(wrap), st::aiComposeContentMargin);
|
||||||
content->setModeTabs(tabs);
|
*stylesWrapHolder = ptr;
|
||||||
content->setStyleTabs(stylesWrap);
|
ptr->entity()->setContextMenuCallback([=](int index, QPoint globalPos) {
|
||||||
|
const auto &tones = content->tones();
|
||||||
|
if (index < 0 || index >= int(tones.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &tone = tones[index];
|
||||||
|
if (tone.isDefault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*contextMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||||
|
ptr->entity(),
|
||||||
|
st::popupMenuWithIcons);
|
||||||
|
const auto toneCopy = tone;
|
||||||
|
if (toneCopy.creator) {
|
||||||
|
(*contextMenu)->addAction(
|
||||||
|
tr::lng_ai_compose_tone_edit(tr::now),
|
||||||
|
[=] {
|
||||||
|
box->uiShow()->show(Box(
|
||||||
|
EditAiToneBox,
|
||||||
|
session,
|
||||||
|
toneCopy,
|
||||||
|
crl::guard(content, [=](Data::AiComposeTone tone) {
|
||||||
|
content->selectToneById(tone.id);
|
||||||
|
})));
|
||||||
|
},
|
||||||
|
&st::menuIconEdit);
|
||||||
|
}
|
||||||
|
(*contextMenu)->addAction(
|
||||||
|
tr::lng_ai_compose_tone_share(tr::now),
|
||||||
|
[=] {
|
||||||
|
const auto url = session->createInternalLinkFull(
|
||||||
|
"addstyle/" + toneCopy.slug);
|
||||||
|
FastShareLink(
|
||||||
|
Main::MakeSessionShow(box->uiShow(), session),
|
||||||
|
url);
|
||||||
|
},
|
||||||
|
&st::menuIconShare);
|
||||||
|
(*contextMenu)->addAction(base::make_unique_q<Ui::Menu::Action>(
|
||||||
|
(*contextMenu)->menu(),
|
||||||
|
st::menuWithIconsAttention,
|
||||||
|
Ui::Menu::CreateAction(
|
||||||
|
(*contextMenu)->menu().get(),
|
||||||
|
toneCopy.creator
|
||||||
|
? tr::lng_ai_compose_tone_delete(tr::now)
|
||||||
|
: tr::lng_ai_compose_tone_remove(tr::now),
|
||||||
|
[=] {
|
||||||
|
ConfirmDeleteAiTone(
|
||||||
|
box->uiShow(),
|
||||||
|
session,
|
||||||
|
toneCopy);
|
||||||
|
}),
|
||||||
|
&st::menuIconDeleteAttention,
|
||||||
|
&st::menuIconDeleteAttention));
|
||||||
|
(*contextMenu)->popup(globalPos);
|
||||||
|
});
|
||||||
|
content->setStyleTabs(ptr);
|
||||||
|
if (savedScroll >= 0) {
|
||||||
|
ptr->entity()->setScrollLeft(savedScroll);
|
||||||
|
}
|
||||||
|
auto handle = SetupStyleTooltip(
|
||||||
|
box,
|
||||||
|
pinnedToTop,
|
||||||
|
ptr,
|
||||||
|
[=] { return content->mode(); });
|
||||||
|
*styleTooltipHolder = handle.tooltip;
|
||||||
|
*styleTooltipUpdater = std::move(handle.updateVisibility);
|
||||||
|
};
|
||||||
|
rebuildStylesWrap();
|
||||||
|
|
||||||
const auto updateStyleTooltipVisibility = SetupStyleTooltip(
|
session->data().aiComposeTones().updated(
|
||||||
box,
|
) | rpl::on_next([=] {
|
||||||
pinnedToTop,
|
content->refreshTones();
|
||||||
stylesWrap,
|
rebuildStylesWrap();
|
||||||
[=] { return content->mode(); });
|
}, box->lifetime());
|
||||||
|
|
||||||
const auto sparkle = LoadingTitleSparkle(session);
|
const auto sparkle = LoadingTitleSparkle(session);
|
||||||
const auto loading = box->lifetime().make_state<
|
const auto loading = box->lifetime().make_state<
|
||||||
@@ -1517,10 +1792,10 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
|
|||||||
box->clearButtons();
|
box->clearButtons();
|
||||||
box->addTopButton(st::aiComposeBoxClose, [=] {
|
box->addTopButton(st::aiComposeBoxClose, [=] {
|
||||||
box->closeBox();
|
box->closeBox();
|
||||||
});
|
})->setAccessibleName(tr::lng_close(tr::now));
|
||||||
box->addTopButton(st::aiComposeBoxInfoButton, [=] {
|
box->addTopButton(st::aiComposeBoxInfoButton, [=] {
|
||||||
box->uiShow()->show(Box(Ui::AboutCocoonBox));
|
box->uiShow()->show(Box(Ui::AboutCocoonBox));
|
||||||
});
|
})->setAccessibleName(tr::lng_sr_ai_compose_info(tr::now));
|
||||||
|
|
||||||
if (*premiumFlooded) {
|
if (*premiumFlooded) {
|
||||||
auto helper = Ui::Text::CustomEmojiHelper();
|
auto helper = Ui::Text::CustomEmojiHelper();
|
||||||
@@ -1570,6 +1845,7 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
|
|||||||
btn->parentWidget(),
|
btn->parentWidget(),
|
||||||
st::aiComposeSendButton);
|
st::aiComposeSendButton);
|
||||||
send->setState({ .type = Ui::SendButton::Type::Send });
|
send->setState({ .type = Ui::SendButton::Type::Send });
|
||||||
|
send->setAccessibleName(tr::lng_send_button(tr::now));
|
||||||
send->show();
|
send->show();
|
||||||
btn->geometryValue(
|
btn->geometryValue(
|
||||||
) | rpl::on_next([=](QRect geometry) {
|
) | rpl::on_next([=](QRect geometry) {
|
||||||
@@ -1614,14 +1890,14 @@ void ComposeAiBox(not_null<Ui::GenericBox*> box, ComposeAiBoxArgs &&args) {
|
|||||||
});
|
});
|
||||||
content->setModeChangedCallback([=](ComposeAiMode mode) {
|
content->setModeChangedCallback([=](ComposeAiMode mode) {
|
||||||
rebuildButtons();
|
rebuildButtons();
|
||||||
updateStyleTooltipVisibility(mode == ComposeAiMode::Style);
|
(*styleTooltipUpdater)(mode == ComposeAiMode::Style);
|
||||||
});
|
});
|
||||||
content->setStyleSelectedCallback([=] {
|
content->setStyleSelectedCallback([=] {
|
||||||
rebuildButtons();
|
rebuildButtons();
|
||||||
if (!Core::App().settings().readPref<bool>(kAiComposeStyleTooltipHiddenPref)) {
|
if (!Core::App().settings().readPref<bool>(kAiComposeStyleTooltipHiddenPref)) {
|
||||||
Core::App().settings().writePref<bool>(kAiComposeStyleTooltipHiddenPref, true);
|
Core::App().settings().writePref<bool>(kAiComposeStyleTooltipHiddenPref, true);
|
||||||
}
|
}
|
||||||
updateStyleTooltipVisibility(false);
|
(*styleTooltipUpdater)(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
rebuildButtons();
|
rebuildButtons();
|
||||||
|
|||||||
@@ -1828,10 +1828,11 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
|||||||
} else {
|
} else {
|
||||||
box->uiShow()->showBox(Ui::MakeConfirmBox({
|
box->uiShow()->showBox(Ui::MakeConfirmBox({
|
||||||
.text = tr::lng_proxy_check_ip_warning(),
|
.text = tr::lng_proxy_check_ip_warning(),
|
||||||
.confirmed = [=] {
|
.confirmed = [=](Fn<void()> close) {
|
||||||
auto &proxy = Core::App().settings().proxy();
|
auto &proxy = Core::App().settings().proxy();
|
||||||
proxy.setCheckIpWarningShown(true);
|
proxy.setCheckIpWarningShown(true);
|
||||||
Local::writeSettings();
|
Local::writeSettings();
|
||||||
|
close();
|
||||||
runCheck();
|
runCheck();
|
||||||
},
|
},
|
||||||
.confirmText = tr::lng_proxy_check_ip_proceed(),
|
.confirmText = tr::lng_proxy_check_ip_proceed(),
|
||||||
|
|||||||
@@ -0,0 +1,718 @@
|
|||||||
|
/*
|
||||||
|
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/create_ai_tone_box.h"
|
||||||
|
|
||||||
|
#include "chat_helpers/compose/compose_show.h"
|
||||||
|
#include "chat_helpers/emoji_list_widget.h"
|
||||||
|
#include "chat_helpers/stickers_lottie.h"
|
||||||
|
#include "data/data_ai_compose_tones.h"
|
||||||
|
#include "data/data_document.h"
|
||||||
|
#include "data/data_document_media.h"
|
||||||
|
#include "data/data_forum_icons.h"
|
||||||
|
#include "data/data_premium_limits.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "data/stickers/data_custom_emoji.h"
|
||||||
|
#include "history/view/media/history_view_sticker_player.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/session/session_show.h"
|
||||||
|
#include "main/main_app_config.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "settings/sections/settings_premium.h"
|
||||||
|
#include "ui/abstract_button.h"
|
||||||
|
#include "ui/boxes/confirm_box.h"
|
||||||
|
#include "ui/controls/custom_emoji_toast_icon.h"
|
||||||
|
#include "ui/controls/warning_tooltip.h"
|
||||||
|
#include "ui/effects/animations.h"
|
||||||
|
#include "ui/layers/generic_box.h"
|
||||||
|
#include "ui/layers/show.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/text/text_utilities.h"
|
||||||
|
#include "ui/toast/toast.h"
|
||||||
|
#include "ui/vertical_list.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "ui/widgets/checkbox.h"
|
||||||
|
#include "ui/widgets/fields/input_field.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/widgets/shadow.h"
|
||||||
|
#include "ui/wrap/padding_wrap.h"
|
||||||
|
#include "ui/wrap/vertical_layout.h"
|
||||||
|
#include "window/window_session_controller.h"
|
||||||
|
|
||||||
|
#include "styles/style_boxes.h"
|
||||||
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_dialogs.h"
|
||||||
|
#include "styles/style_layers.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kAiComposeToneToastDuration = crl::time(4000);
|
||||||
|
|
||||||
|
void ShowToneToast(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone,
|
||||||
|
bool created) {
|
||||||
|
const auto size = QSize(
|
||||||
|
st::aiComposeToneToastIconSize.width(),
|
||||||
|
st::aiComposeToneToastIconSize.height());
|
||||||
|
show->showToast(Ui::Toast::Config{
|
||||||
|
.title = (created
|
||||||
|
? tr::lng_ai_compose_tone_created
|
||||||
|
: tr::lng_ai_compose_tone_updated)(
|
||||||
|
tr::now,
|
||||||
|
lt_title,
|
||||||
|
tone.title),
|
||||||
|
.text = tr::lng_ai_compose_tone_created_description(
|
||||||
|
tr::now,
|
||||||
|
Ui::Text::WithEntities),
|
||||||
|
.iconContent = Ui::MakeCustomEmojiToastIcon(
|
||||||
|
session,
|
||||||
|
tone.emojiId,
|
||||||
|
size),
|
||||||
|
.iconPadding = st::aiComposeToneToastIconPadding,
|
||||||
|
.duration = kAiComposeToneToastDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChooseToneIconBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Window::SessionController*> controller,
|
||||||
|
Fn<void(DocumentId)> chosen) {
|
||||||
|
using namespace ChatHelpers;
|
||||||
|
|
||||||
|
box->setTitle(tr::lng_ai_compose_tone_icon_title());
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
box->setMaxHeight(st::editTopicMaxHeight);
|
||||||
|
box->setScrollStyle(st::reactPanelScroll);
|
||||||
|
|
||||||
|
const auto manager = &controller->session().data().customEmojiManager();
|
||||||
|
const auto icons = &controller->session().data().forumIcons();
|
||||||
|
|
||||||
|
auto factory = [=](DocumentId id, Fn<void()> repaint)
|
||||||
|
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
||||||
|
return manager->create(
|
||||||
|
id,
|
||||||
|
std::move(repaint),
|
||||||
|
Data::CustomEmojiManager::SizeTag::Large);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto top = box->setPinnedToTopContent(
|
||||||
|
object_ptr<Ui::VerticalLayout>(box));
|
||||||
|
|
||||||
|
const auto body = box->verticalLayout();
|
||||||
|
const auto selector = body->add(
|
||||||
|
object_ptr<EmojiListWidget>(body, EmojiListDescriptor{
|
||||||
|
.show = controller->uiShow(),
|
||||||
|
.mode = EmojiListWidget::Mode::TopicIcon,
|
||||||
|
.paused = Window::PausedIn(
|
||||||
|
controller,
|
||||||
|
Window::GifPauseReason::Layer),
|
||||||
|
.customRecentList = DocumentListToRecent(icons->list()),
|
||||||
|
.customRecentFactory = std::move(factory),
|
||||||
|
.st = &st::reactPanelEmojiPan,
|
||||||
|
}),
|
||||||
|
st::reactPanelEmojiPan.padding);
|
||||||
|
|
||||||
|
icons->requestDefaultIfUnknown();
|
||||||
|
icons->defaultUpdates(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
selector->provideRecent(DocumentListToRecent(icons->list()));
|
||||||
|
}, selector->lifetime());
|
||||||
|
|
||||||
|
top->add(selector->createFooter());
|
||||||
|
|
||||||
|
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(box.get());
|
||||||
|
shadow->show();
|
||||||
|
|
||||||
|
rpl::combine(
|
||||||
|
top->heightValue(),
|
||||||
|
selector->widthValue()
|
||||||
|
) | rpl::on_next([=](int topHeight, int width) {
|
||||||
|
shadow->setGeometry(0, topHeight, width, st::lineWidth);
|
||||||
|
}, shadow->lifetime());
|
||||||
|
|
||||||
|
selector->refreshEmoji();
|
||||||
|
|
||||||
|
selector->scrollToRequests(
|
||||||
|
) | rpl::on_next([=](int y) {
|
||||||
|
box->scrollToY(y);
|
||||||
|
shadow->update();
|
||||||
|
}, selector->lifetime());
|
||||||
|
|
||||||
|
rpl::combine(
|
||||||
|
box->heightValue(),
|
||||||
|
top->heightValue(),
|
||||||
|
rpl::mappers::_1 - rpl::mappers::_2
|
||||||
|
) | rpl::on_next([=](int height) {
|
||||||
|
selector->setMinimalHeight(selector->width(), height);
|
||||||
|
}, body->lifetime());
|
||||||
|
|
||||||
|
selector->customChosen(
|
||||||
|
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
|
||||||
|
chosen(data.document->id);
|
||||||
|
box->closeBox();
|
||||||
|
}, selector->lifetime());
|
||||||
|
|
||||||
|
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
not_null<Ui::AbstractButton*> AddAiToneIconPreview(
|
||||||
|
not_null<Ui::VerticalLayout*> container,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
rpl::producer<DocumentId> emojiIdValue,
|
||||||
|
Fn<void(DocumentId)> emojiIdChosen) {
|
||||||
|
using StickerPlayer = HistoryView::StickerPlayer;
|
||||||
|
struct State {
|
||||||
|
DocumentId emojiId = 0;
|
||||||
|
std::shared_ptr<StickerPlayer> player;
|
||||||
|
bool playerUsesTextColor = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto outer = st::aiToneIconPreviewSize;
|
||||||
|
const auto inner = st::aiToneIconPreviewInnerSize;
|
||||||
|
const auto top = st::aiToneIconPreviewTopSkip;
|
||||||
|
const auto bottom = st::aiToneIconPreviewBottomSkip;
|
||||||
|
const auto holder = container->add(
|
||||||
|
object_ptr<Ui::FixedHeightWidget>(
|
||||||
|
container,
|
||||||
|
outer + top + bottom));
|
||||||
|
const auto button = Ui::CreateChild<Ui::AbstractButton>(holder);
|
||||||
|
button->resize(outer, outer);
|
||||||
|
button->show();
|
||||||
|
|
||||||
|
holder->widthValue(
|
||||||
|
) | rpl::on_next([=](int width) {
|
||||||
|
button->move((width - outer) / 2, top);
|
||||||
|
}, button->lifetime());
|
||||||
|
|
||||||
|
const auto state = button->lifetime().make_state<State>();
|
||||||
|
const auto emojiIdVar = button->lifetime().make_state<
|
||||||
|
rpl::variable<DocumentId>>(std::move(emojiIdValue));
|
||||||
|
|
||||||
|
emojiIdVar->value(
|
||||||
|
) | rpl::on_next([=](DocumentId id) {
|
||||||
|
state->emojiId = id;
|
||||||
|
}, button->lifetime());
|
||||||
|
|
||||||
|
emojiIdVar->value(
|
||||||
|
) | rpl::map([=](DocumentId id) -> rpl::producer<DocumentData*> {
|
||||||
|
if (!id) {
|
||||||
|
return rpl::single((DocumentData*)nullptr);
|
||||||
|
}
|
||||||
|
return session->data().customEmojiManager().resolve(
|
||||||
|
id
|
||||||
|
) | rpl::map([=](not_null<DocumentData*> document) {
|
||||||
|
return document.get();
|
||||||
|
}) | rpl::map_error_to_done();
|
||||||
|
}) | rpl::flatten_latest(
|
||||||
|
) | rpl::map([=](DocumentData *document)
|
||||||
|
-> rpl::producer<std::shared_ptr<StickerPlayer>> {
|
||||||
|
if (!document) {
|
||||||
|
return rpl::single(std::shared_ptr<StickerPlayer>());
|
||||||
|
}
|
||||||
|
const auto media = document->createMediaView();
|
||||||
|
media->checkStickerLarge();
|
||||||
|
media->goodThumbnailWanted();
|
||||||
|
|
||||||
|
return rpl::single() | rpl::then(
|
||||||
|
document->session().downloaderTaskFinished()
|
||||||
|
) | rpl::filter([=] {
|
||||||
|
return media->loaded();
|
||||||
|
}) | rpl::take(1) | rpl::map([=] {
|
||||||
|
auto result = std::shared_ptr<StickerPlayer>();
|
||||||
|
const auto sticker = document->sticker();
|
||||||
|
const auto size = QSize(inner, inner);
|
||||||
|
if (sticker && sticker->isLottie()) {
|
||||||
|
result = std::make_shared<HistoryView::LottiePlayer>(
|
||||||
|
ChatHelpers::LottiePlayerFromDocument(
|
||||||
|
media.get(),
|
||||||
|
ChatHelpers::StickerLottieSize::StickerSet,
|
||||||
|
size,
|
||||||
|
Lottie::Quality::High));
|
||||||
|
} else if (sticker && sticker->isWebm()) {
|
||||||
|
result = std::make_shared<HistoryView::WebmPlayer>(
|
||||||
|
media->owner()->location(),
|
||||||
|
media->bytes(),
|
||||||
|
size);
|
||||||
|
} else {
|
||||||
|
result = std::make_shared<HistoryView::StaticStickerPlayer>(
|
||||||
|
media->owner()->location(),
|
||||||
|
media->bytes(),
|
||||||
|
size);
|
||||||
|
}
|
||||||
|
result->setRepaintCallback([=] { button->update(); });
|
||||||
|
state->playerUsesTextColor
|
||||||
|
= media->owner()->emojiUsesTextColor();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}) | rpl::flatten_latest(
|
||||||
|
) | rpl::on_next([=](std::shared_ptr<StickerPlayer> player) {
|
||||||
|
state->player = std::move(player);
|
||||||
|
button->update();
|
||||||
|
}, button->lifetime());
|
||||||
|
|
||||||
|
button->paintRequest(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
auto p = QPainter(button);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::aiToneIconPreviewBg);
|
||||||
|
p.drawEllipse(button->rect());
|
||||||
|
if (state->player && state->player->ready()) {
|
||||||
|
const auto color = state->playerUsesTextColor
|
||||||
|
? st::windowFg->c
|
||||||
|
: QColor(0, 0, 0, 0);
|
||||||
|
const auto frame = state->player->frame(
|
||||||
|
QSize(inner, inner),
|
||||||
|
color,
|
||||||
|
false,
|
||||||
|
crl::now(),
|
||||||
|
false).image;
|
||||||
|
const auto sz = frame.size() / style::DevicePixelRatio();
|
||||||
|
p.drawImage(
|
||||||
|
QRect(
|
||||||
|
(outer - sz.width()) / 2,
|
||||||
|
(outer - sz.height()) / 2,
|
||||||
|
sz.width(),
|
||||||
|
sz.height()),
|
||||||
|
frame);
|
||||||
|
state->player->markFrameShown();
|
||||||
|
} else if (!state->emojiId) {
|
||||||
|
st::aiToneIconPreviewPlaceholder.paintInCenter(
|
||||||
|
p,
|
||||||
|
button->rect());
|
||||||
|
}
|
||||||
|
}, button->lifetime());
|
||||||
|
|
||||||
|
if (emojiIdChosen) {
|
||||||
|
button->setClickedCallback([=] {
|
||||||
|
const auto controller = ChatHelpers::ResolveWindowDefault()(
|
||||||
|
session);
|
||||||
|
if (!controller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller->uiShow()->showBox(Box(
|
||||||
|
ChooseToneIconBox,
|
||||||
|
controller,
|
||||||
|
crl::guard(button, [=](DocumentId id) {
|
||||||
|
emojiIdChosen(id);
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
button->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void SetupToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
DocumentId initialEmojiId,
|
||||||
|
const QString &initialName,
|
||||||
|
const QString &initialPrompt,
|
||||||
|
bool initialDisplayAuthor,
|
||||||
|
rpl::producer<QString> title,
|
||||||
|
rpl::producer<QString> submitLabel,
|
||||||
|
Fn<void(DocumentId, QString, QString, bool)> submit,
|
||||||
|
Fn<void()> requestDelete = nullptr) {
|
||||||
|
box->setStyle(st::aiComposeBox);
|
||||||
|
box->setNoContentMargin(true);
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); });
|
||||||
|
box->setTitle(std::move(title));
|
||||||
|
|
||||||
|
const auto container = box->verticalLayout();
|
||||||
|
const auto emojiId = container->lifetime().make_state<
|
||||||
|
rpl::variable<DocumentId>>(initialEmojiId);
|
||||||
|
|
||||||
|
const auto iconButton = AddAiToneIconPreview(
|
||||||
|
container,
|
||||||
|
session,
|
||||||
|
emojiId->value(),
|
||||||
|
[=](DocumentId id) { *emojiId = id; });
|
||||||
|
|
||||||
|
const auto name = box->addRow(
|
||||||
|
object_ptr<Ui::InputField>(
|
||||||
|
box,
|
||||||
|
st::aiToneNameField,
|
||||||
|
Ui::InputField::Mode::SingleLine,
|
||||||
|
rpl::producer<QString>(),
|
||||||
|
initialName),
|
||||||
|
st::aiToneFieldsMargin);
|
||||||
|
name->setMaxLength(session->appConfig().get<int>(
|
||||||
|
u"aicompose_tone_title_length_max"_q,
|
||||||
|
12));
|
||||||
|
|
||||||
|
Ui::AddSkip(container, st::aiToneFieldsSkip);
|
||||||
|
|
||||||
|
const auto promptSt = box->lifetime().make_state<style::InputField>(
|
||||||
|
st::aiTonePromptField);
|
||||||
|
{
|
||||||
|
const auto &placeholderStyle = st::aiTonePlaceholderLabel.style;
|
||||||
|
const auto fieldsMargin = st::aiToneFieldsMargin;
|
||||||
|
const auto contentWidth = st::boxWideWidth
|
||||||
|
- fieldsMargin.left() - fieldsMargin.right()
|
||||||
|
- promptSt->textMargins.left() - promptSt->textMargins.right();
|
||||||
|
auto measure = Ui::Text::String{ contentWidth / 2 };
|
||||||
|
measure.setText(
|
||||||
|
placeholderStyle,
|
||||||
|
tr::lng_ai_compose_tone_prompt_placeholder(tr::now));
|
||||||
|
const auto desiredMin = measure.countHeight(contentWidth)
|
||||||
|
+ promptSt->textMargins.top()
|
||||||
|
+ promptSt->textMargins.bottom();
|
||||||
|
if (promptSt->heightMin < desiredMin) {
|
||||||
|
promptSt->heightMin = desiredMin;
|
||||||
|
}
|
||||||
|
if (promptSt->heightMax < promptSt->heightMin) {
|
||||||
|
promptSt->heightMax = promptSt->heightMin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto prompt = box->addRow(
|
||||||
|
object_ptr<Ui::InputField>(
|
||||||
|
box,
|
||||||
|
*promptSt,
|
||||||
|
Ui::InputField::Mode::MultiLine,
|
||||||
|
rpl::producer<QString>(),
|
||||||
|
initialPrompt),
|
||||||
|
st::aiToneFieldsMargin);
|
||||||
|
prompt->setSubmitSettings(Ui::InputField::SubmitSettings::None);
|
||||||
|
prompt->setMaxLength(session->appConfig().get<int>(
|
||||||
|
u"aicompose_tone_prompt_length_max"_q,
|
||||||
|
1024));
|
||||||
|
|
||||||
|
struct FieldDecor {
|
||||||
|
not_null<Ui::RpWidget*> bg;
|
||||||
|
not_null<Ui::FlatLabel*> placeholder;
|
||||||
|
Ui::Animations::Simple anim;
|
||||||
|
bool hidden = false;
|
||||||
|
};
|
||||||
|
const auto makeDecor = [=](
|
||||||
|
not_null<Ui::InputField*> field,
|
||||||
|
rpl::producer<QString> placeholderText) {
|
||||||
|
const auto parent = field->parentWidget();
|
||||||
|
const auto decor = field->lifetime().make_state<FieldDecor>(FieldDecor{
|
||||||
|
.bg = Ui::CreateChild<Ui::RpWidget>(parent),
|
||||||
|
.placeholder = Ui::CreateChild<Ui::FlatLabel>(
|
||||||
|
parent,
|
||||||
|
std::move(placeholderText),
|
||||||
|
st::aiTonePlaceholderLabel),
|
||||||
|
});
|
||||||
|
decor->bg->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
decor->placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
decor->bg->paintRequest(
|
||||||
|
) | rpl::on_next([bg = decor->bg] {
|
||||||
|
auto p = QPainter(bg);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::aiToneFieldBg);
|
||||||
|
const auto r = st::aiToneFieldRadius;
|
||||||
|
p.drawRoundedRect(bg->rect(), r, r);
|
||||||
|
}, decor->bg->lifetime());
|
||||||
|
decor->bg->lower();
|
||||||
|
decor->placeholder->raise();
|
||||||
|
|
||||||
|
const auto applyPosition = [=] {
|
||||||
|
const auto pad = st::aiToneFieldPadding;
|
||||||
|
const auto progress = decor->anim.value(decor->hidden ? 1. : 0.);
|
||||||
|
const auto shift = int(base::SafeRound(
|
||||||
|
progress * (-st::defaultInputField.placeholderShift)));
|
||||||
|
decor->placeholder->moveToLeft(
|
||||||
|
field->x() + pad.left() + shift,
|
||||||
|
field->y() + pad.top());
|
||||||
|
decor->placeholder->setOpacity(1. - progress);
|
||||||
|
};
|
||||||
|
field->geometryValue(
|
||||||
|
) | rpl::on_next([=](QRect g) {
|
||||||
|
if (g.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto pad = st::aiToneFieldPadding;
|
||||||
|
decor->bg->setGeometry(g);
|
||||||
|
decor->placeholder->resizeToWidth(
|
||||||
|
g.width() - pad.left() - pad.right());
|
||||||
|
applyPosition();
|
||||||
|
}, field->lifetime());
|
||||||
|
|
||||||
|
const auto animate = [=](bool hidden) {
|
||||||
|
if (decor->hidden == hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
decor->hidden = hidden;
|
||||||
|
decor->anim.start(
|
||||||
|
applyPosition,
|
||||||
|
hidden ? 0. : 1.,
|
||||||
|
hidden ? 1. : 0.,
|
||||||
|
st::defaultInputField.duration);
|
||||||
|
};
|
||||||
|
field->changes(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
animate(!field->getLastText().isEmpty());
|
||||||
|
}, field->lifetime());
|
||||||
|
decor->hidden = !field->getLastText().isEmpty();
|
||||||
|
applyPosition();
|
||||||
|
return decor;
|
||||||
|
};
|
||||||
|
makeDecor(name, tr::lng_ai_compose_tone_name_placeholder());
|
||||||
|
const auto promptDecor = makeDecor(
|
||||||
|
prompt,
|
||||||
|
tr::lng_ai_compose_tone_prompt_placeholder());
|
||||||
|
|
||||||
|
const auto authorCheckbox = box->addRow(
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
box,
|
||||||
|
tr::lng_ai_compose_tone_author(tr::now),
|
||||||
|
st::aiComposeEmojifyCheckbox,
|
||||||
|
std::make_unique<Ui::RoundCheckView>(
|
||||||
|
st::defaultCheck,
|
||||||
|
initialDisplayAuthor)),
|
||||||
|
st::aiToneAuthorCheckboxMargin,
|
||||||
|
style::al_top);
|
||||||
|
|
||||||
|
const auto deleteButton = requestDelete
|
||||||
|
? box->addRow(
|
||||||
|
object_ptr<Ui::RoundButton>(
|
||||||
|
box,
|
||||||
|
tr::lng_ai_compose_tone_delete(),
|
||||||
|
st::aiToneDeleteButton),
|
||||||
|
st::aiToneDeleteButtonMargin)
|
||||||
|
: nullptr;
|
||||||
|
if (deleteButton) {
|
||||||
|
deleteButton->setFullRadius(true);
|
||||||
|
deleteButton->setClickedCallback(std::move(requestDelete));
|
||||||
|
box->widthValue(
|
||||||
|
) | rpl::on_next([=](int width) {
|
||||||
|
const auto &margin = st::aiToneDeleteButtonMargin;
|
||||||
|
deleteButton->setFullWidth(
|
||||||
|
width - margin.left() - margin.right());
|
||||||
|
}, deleteButton->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::combine(
|
||||||
|
prompt->topValue(),
|
||||||
|
promptDecor->placeholder->heightValue(),
|
||||||
|
box->getDelegate()->contentHeightMaxValue()
|
||||||
|
) | rpl::on_next([=](int top, int phHeight, int contentHeight) {
|
||||||
|
const auto pad = st::aiToneFieldPadding;
|
||||||
|
const auto deleteBlock = deleteButton
|
||||||
|
? (deleteButton->heightNoMargins()
|
||||||
|
+ st::aiToneDeleteButtonMargin.top()
|
||||||
|
+ st::aiToneDeleteButtonMargin.bottom())
|
||||||
|
: 0;
|
||||||
|
prompt->setMaxHeight(contentHeight
|
||||||
|
- top
|
||||||
|
- st::aiToneFieldsMargin.bottom()
|
||||||
|
- authorCheckbox->heightNoMargins()
|
||||||
|
- st::aiToneAuthorCheckboxMargin.top()
|
||||||
|
- st::aiToneAuthorCheckboxMargin.bottom()
|
||||||
|
- deleteBlock);
|
||||||
|
prompt->setMinHeight(phHeight + pad.top() + pad.bottom());
|
||||||
|
}, prompt->lifetime());
|
||||||
|
|
||||||
|
box->setFocusCallback([=] {
|
||||||
|
name->setFocusFast();
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto warning = box->lifetime().make_state<Ui::WarningTooltip>();
|
||||||
|
const auto save = [=] {
|
||||||
|
const auto nameText = name->getLastText().trimmed();
|
||||||
|
const auto promptText = prompt->getLastText().trimmed();
|
||||||
|
const auto showWarning = [=](
|
||||||
|
not_null<QWidget*> target,
|
||||||
|
rpl::producer<TextWithEntities> text) {
|
||||||
|
warning->show({
|
||||||
|
.parent = box,
|
||||||
|
.target = target,
|
||||||
|
.text = std::move(text),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (!emojiId->current()) {
|
||||||
|
showWarning(
|
||||||
|
iconButton,
|
||||||
|
tr::lng_ai_compose_tone_warn_icon(tr::marked));
|
||||||
|
return;
|
||||||
|
} else if (nameText.isEmpty()) {
|
||||||
|
name->showError();
|
||||||
|
showWarning(
|
||||||
|
name,
|
||||||
|
tr::lng_ai_compose_tone_warn_name(tr::marked));
|
||||||
|
return;
|
||||||
|
} else if (promptText.isEmpty()) {
|
||||||
|
prompt->showError();
|
||||||
|
showWarning(
|
||||||
|
prompt,
|
||||||
|
tr::lng_ai_compose_tone_warn_prompt(tr::marked));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warning->hide(anim::type::normal);
|
||||||
|
submit(
|
||||||
|
emojiId->current(),
|
||||||
|
nameText,
|
||||||
|
promptText,
|
||||||
|
authorCheckbox->checked());
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto submitBtn = box->addButton(std::move(submitLabel), save);
|
||||||
|
submitBtn->setFullRadius(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CreateAiToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
Fn<void(Data::AiComposeTone)> saved) {
|
||||||
|
SetupToneBox(
|
||||||
|
box,
|
||||||
|
session,
|
||||||
|
DocumentId(0),
|
||||||
|
QString(),
|
||||||
|
QString(),
|
||||||
|
false,
|
||||||
|
tr::lng_ai_compose_create_tone_title(),
|
||||||
|
tr::lng_ai_compose_tone_create(),
|
||||||
|
[=](DocumentId emojiId,
|
||||||
|
const QString &name,
|
||||||
|
const QString &prompt,
|
||||||
|
bool displayAuthor) {
|
||||||
|
session->data().aiComposeTones().create(
|
||||||
|
name,
|
||||||
|
prompt,
|
||||||
|
emojiId,
|
||||||
|
displayAuthor,
|
||||||
|
crl::guard(box, [=](Data::AiComposeTone tone) {
|
||||||
|
const auto show = box->uiShow();
|
||||||
|
box->closeBox();
|
||||||
|
ShowToneToast(show, session, tone, true);
|
||||||
|
if (saved) {
|
||||||
|
saved(tone);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
crl::guard(box, [=](const MTP::Error &error) {
|
||||||
|
if (error.type() == u"TONES_SAVED_TOO_MANY"_q) {
|
||||||
|
ShowAiComposeToneLimitError(box->uiShow(), session);
|
||||||
|
} else if (!MTP::IgnoreError(error)) {
|
||||||
|
box->showToast(error.type());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditAiToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone,
|
||||||
|
Fn<void(Data::AiComposeTone)> saved) {
|
||||||
|
const auto toneCopy = tone;
|
||||||
|
SetupToneBox(
|
||||||
|
box,
|
||||||
|
session,
|
||||||
|
tone.emojiId,
|
||||||
|
tone.title,
|
||||||
|
tone.prompt,
|
||||||
|
tone.authorId != 0,
|
||||||
|
tr::lng_ai_compose_edit_tone_title(),
|
||||||
|
tr::lng_ai_compose_tone_save(),
|
||||||
|
[=](DocumentId emojiId,
|
||||||
|
const QString &name,
|
||||||
|
const QString &prompt,
|
||||||
|
bool displayAuthor) {
|
||||||
|
session->data().aiComposeTones().update(
|
||||||
|
toneCopy,
|
||||||
|
name,
|
||||||
|
prompt,
|
||||||
|
std::make_optional(emojiId),
|
||||||
|
std::make_optional(displayAuthor),
|
||||||
|
crl::guard(box, [=](Data::AiComposeTone updated) {
|
||||||
|
const auto show = box->uiShow();
|
||||||
|
box->closeBox();
|
||||||
|
ShowToneToast(show, session, updated, false);
|
||||||
|
if (saved) {
|
||||||
|
saved(updated);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[=] {
|
||||||
|
ConfirmDeleteAiTone(
|
||||||
|
box->uiShow(),
|
||||||
|
session,
|
||||||
|
toneCopy,
|
||||||
|
[=] { box->closeBox(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfirmDeleteAiTone(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone,
|
||||||
|
Fn<void()> done) {
|
||||||
|
if (!tone.creator) {
|
||||||
|
show->show(Ui::MakeConfirmBox({
|
||||||
|
.text = tr::lng_ai_compose_tone_remove_sure(),
|
||||||
|
.confirmed = [=](Fn<void()> &&close) {
|
||||||
|
close();
|
||||||
|
session->data().aiComposeTones().save(
|
||||||
|
tone,
|
||||||
|
true,
|
||||||
|
done);
|
||||||
|
},
|
||||||
|
.confirmText = tr::lng_box_remove(),
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
show->show(Ui::MakeConfirmBox({
|
||||||
|
.text = tr::lng_ai_compose_tone_delete_sure(),
|
||||||
|
.confirmed = [=](Fn<void()> &&close) {
|
||||||
|
close();
|
||||||
|
session->data().aiComposeTones().remove(tone, done);
|
||||||
|
},
|
||||||
|
.confirmText = tr::lng_box_delete(),
|
||||||
|
.confirmStyle = &st::attentionBoxButton,
|
||||||
|
.title = tr::lng_ai_compose_tone_delete(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowAiComposeToneLimitError(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<Main::Session*> session) {
|
||||||
|
const auto limits = Data::PremiumLimits(session);
|
||||||
|
const auto premium = session->premium();
|
||||||
|
const auto premiumPossible = session->premiumPossible();
|
||||||
|
const auto defaultLimit = limits.aiComposeSavedTonesDefault();
|
||||||
|
const auto premiumLimit = limits.aiComposeSavedTonesPremium();
|
||||||
|
const auto current = premium ? premiumLimit : defaultLimit;
|
||||||
|
if (premium || !premiumPossible) {
|
||||||
|
show->showToast(tr::lng_ai_compose_tone_saved_limit_final(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
current,
|
||||||
|
tr::rich));
|
||||||
|
} else {
|
||||||
|
Settings::ShowPremiumPromoToast(
|
||||||
|
Main::MakeSessionShow(show, session),
|
||||||
|
ChatHelpers::ResolveWindowDefault(),
|
||||||
|
tr::lng_ai_compose_tone_saved_limit(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
defaultLimit,
|
||||||
|
lt_link,
|
||||||
|
tr::bold(tr::lng_ai_compose_tone_saved_limit_link(
|
||||||
|
tr::now,
|
||||||
|
tr::link)),
|
||||||
|
lt_premium_count,
|
||||||
|
tr::bold(QString::number(premiumLimit)),
|
||||||
|
tr::rich),
|
||||||
|
u"ai_compose_tones"_q);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Telegram Desktop,
|
||||||
|
the official desktop application for the Telegram messaging service.
|
||||||
|
|
||||||
|
For license and copyright information please follow this link:
|
||||||
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
struct AiComposeTone;
|
||||||
|
} // namespace Data
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class AbstractButton;
|
||||||
|
class GenericBox;
|
||||||
|
class Show;
|
||||||
|
class VerticalLayout;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
not_null<Ui::AbstractButton*> AddAiToneIconPreview(
|
||||||
|
not_null<Ui::VerticalLayout*> container,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
rpl::producer<DocumentId> emojiIdValue,
|
||||||
|
Fn<void(DocumentId)> emojiIdChosen = nullptr);
|
||||||
|
|
||||||
|
void CreateAiToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
Fn<void(Data::AiComposeTone)> saved = nullptr);
|
||||||
|
|
||||||
|
void EditAiToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone,
|
||||||
|
Fn<void(Data::AiComposeTone)> saved = nullptr);
|
||||||
|
|
||||||
|
void ConfirmDeleteAiTone(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone,
|
||||||
|
Fn<void()> done = nullptr);
|
||||||
|
|
||||||
|
void ShowAiComposeToneLimitError(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<Main::Session*> session);
|
||||||
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "base/event_filter.h"
|
#include "base/event_filter.h"
|
||||||
#include "base/random.h"
|
#include "base/random.h"
|
||||||
#include "base/unique_qptr.h"
|
#include "base/unique_qptr.h"
|
||||||
|
#include "countries/countries_instance.h"
|
||||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||||
#include "chat_helpers/message_field.h"
|
#include "chat_helpers/message_field.h"
|
||||||
#include "chat_helpers/tabbed_panel.h"
|
#include "chat_helpers/tabbed_panel.h"
|
||||||
@@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/stickers/data_custom_emoji.h"
|
#include "data/stickers/data_custom_emoji.h"
|
||||||
#include "history/view/media/menu/history_view_poll_menu.h"
|
#include "history/view/media/menu/history_view_poll_menu.h"
|
||||||
#include "history/view/history_view_schedule_box.h"
|
#include "history/view/history_view_schedule_box.h"
|
||||||
|
#include "info/channel_statistics/boosts/giveaway/select_countries_box.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "layout/layout_document_generic_preview.h"
|
#include "layout/layout_document_generic_preview.h"
|
||||||
#include "main/main_app_config.h"
|
#include "main/main_app_config.h"
|
||||||
@@ -159,6 +161,10 @@ public:
|
|||||||
[[nodiscard]] rpl::producer<> backspaceInFront() const;
|
[[nodiscard]] rpl::producer<> backspaceInFront() const;
|
||||||
[[nodiscard]] rpl::producer<> tabbed() const;
|
[[nodiscard]] rpl::producer<> tabbed() const;
|
||||||
|
|
||||||
|
void handlePaste(
|
||||||
|
not_null<Ui::InputField*> field,
|
||||||
|
const QStringList &list);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class Option {
|
class Option {
|
||||||
public:
|
public:
|
||||||
@@ -201,6 +207,7 @@ private:
|
|||||||
void showAddIcon(bool show);
|
void showAddIcon(bool show);
|
||||||
|
|
||||||
[[nodiscard]] not_null<Ui::InputField*> field() const;
|
[[nodiscard]] not_null<Ui::InputField*> field() const;
|
||||||
|
[[nodiscard]] not_null<Ui::RpWidget*> wrapWidget() const;
|
||||||
|
|
||||||
[[nodiscard]] PollAnswer toPollAnswer(int index) const;
|
[[nodiscard]] PollAnswer toPollAnswer(int index) const;
|
||||||
|
|
||||||
@@ -233,12 +240,18 @@ private:
|
|||||||
void fixShadows();
|
void fixShadows();
|
||||||
void removeEmptyTail();
|
void removeEmptyTail();
|
||||||
void addEmptyOption();
|
void addEmptyOption();
|
||||||
|
void insertOption(
|
||||||
|
int beforeIndex,
|
||||||
|
const QString &text,
|
||||||
|
anim::type animated);
|
||||||
|
void initOptionField(not_null<Ui::InputField*> field);
|
||||||
void checkLastOption();
|
void checkLastOption();
|
||||||
void validateState();
|
void validateState();
|
||||||
void fixAfterErase();
|
void fixAfterErase();
|
||||||
void destroy(std::unique_ptr<Option> option);
|
void destroy(std::unique_ptr<Option> option);
|
||||||
void removeDestroyed(not_null<Option*> field);
|
void removeDestroyed(not_null<Option*> field);
|
||||||
int findField(not_null<Ui::InputField*> field) const;
|
int findField(not_null<Ui::InputField*> field) const;
|
||||||
|
int findLayoutPosition(not_null<Option*> option) const;
|
||||||
[[nodiscard]] auto createChooseCorrectGroup()
|
[[nodiscard]] auto createChooseCorrectGroup()
|
||||||
-> std::shared_ptr<Ui::RadiobuttonGroup>;
|
-> std::shared_ptr<Ui::RadiobuttonGroup>;
|
||||||
void setupReorder();
|
void setupReorder();
|
||||||
@@ -327,6 +340,27 @@ void FocusAtEnd(not_null<Ui::InputField*> field) {
|
|||||||
field->ensureCursorVisible();
|
field->ensureCursorVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QStringList ParsePastedList(const QString &text) {
|
||||||
|
auto list = QStringView(text).split('\n');
|
||||||
|
for (auto i = list.begin(); i != list.end();) {
|
||||||
|
auto trimmed = i->trimmed();
|
||||||
|
if (trimmed.isEmpty() && (i + 1 != list.end())) {
|
||||||
|
i = list.erase(i);
|
||||||
|
} else {
|
||||||
|
*i++ = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (list.size() < 2) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto result = QStringList();
|
||||||
|
result.reserve(list.size());
|
||||||
|
for (const auto &view : list) {
|
||||||
|
result.push_back(view.toString());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
not_null<DetailedSettingsButton*> AddPollToggleButton(
|
not_null<DetailedSettingsButton*> AddPollToggleButton(
|
||||||
not_null<Ui::VerticalLayout*> container,
|
not_null<Ui::VerticalLayout*> container,
|
||||||
rpl::producer<QString> title,
|
rpl::producer<QString> title,
|
||||||
@@ -362,7 +396,7 @@ Options::Option::Option(
|
|||||||
Ui::CreateChild<Ui::InputField>(
|
Ui::CreateChild<Ui::InputField>(
|
||||||
_content.get(),
|
_content.get(),
|
||||||
st::createPollOptionFieldPremium,
|
st::createPollOptionFieldPremium,
|
||||||
Ui::InputField::Mode::NoNewlines,
|
Ui::InputField::Mode::MultiLine,
|
||||||
tr::lng_polls_create_option_add()))
|
tr::lng_polls_create_option_add()))
|
||||||
, _attachCallback(std::move(attachCallback))
|
, _attachCallback(std::move(attachCallback))
|
||||||
, _fieldDropCallback(std::move(fieldDropCallback))
|
, _fieldDropCallback(std::move(fieldDropCallback))
|
||||||
@@ -645,6 +679,10 @@ not_null<Ui::InputField*> Options::Option::field() const {
|
|||||||
return _field;
|
return _field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
not_null<Ui::RpWidget*> Options::Option::wrapWidget() const {
|
||||||
|
return _wrap.get();
|
||||||
|
}
|
||||||
|
|
||||||
void Options::Option::removePlaceholder() const {
|
void Options::Option::removePlaceholder() const {
|
||||||
field()->setPlaceholder(rpl::single(QString()));
|
field()->setPlaceholder(rpl::single(QString()));
|
||||||
}
|
}
|
||||||
@@ -930,28 +968,67 @@ void Options::addEmptyOption() {
|
|||||||
} else if (!_list.empty() && _list.back()->isEmpty()) {
|
} else if (!_list.empty() && _list.back()->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_list.empty()) {
|
const auto animated = _list.empty()
|
||||||
_list.back()->showAddIcon(false);
|
? anim::type::instant
|
||||||
|
: anim::type::normal;
|
||||||
|
insertOption(int(_list.size()), QString(), animated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Options::insertOption(
|
||||||
|
int beforeIndex,
|
||||||
|
const QString &text,
|
||||||
|
anim::type animated) {
|
||||||
|
if (full()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (_list.size() > 1) {
|
Assert(beforeIndex >= 0 && beforeIndex <= int(_list.size()));
|
||||||
(*(_list.end() - 2))->removePlaceholder();
|
|
||||||
|
const auto isAppend = (beforeIndex == int(_list.size()));
|
||||||
|
if (isAppend) {
|
||||||
|
if (!_list.empty()) {
|
||||||
|
_list.back()->showAddIcon(false);
|
||||||
|
}
|
||||||
|
if (_list.size() > 1) {
|
||||||
|
(*(_list.end() - 2))->removePlaceholder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_list.push_back(std::make_unique<Option>(
|
|
||||||
|
const auto layoutPosition = isAppend
|
||||||
|
? _optionsLayout->count()
|
||||||
|
: findLayoutPosition(_list[beforeIndex].get());
|
||||||
|
|
||||||
|
auto option = std::make_unique<Option>(
|
||||||
_box,
|
_box,
|
||||||
_optionsLayout,
|
_optionsLayout,
|
||||||
&_controller->session(),
|
&_controller->session(),
|
||||||
_optionsLayout->count(),
|
layoutPosition,
|
||||||
_chooseCorrectGroup,
|
_chooseCorrectGroup,
|
||||||
_attachCallback,
|
_attachCallback,
|
||||||
_fieldDropCallback,
|
_fieldDropCallback,
|
||||||
_widgetDropCallback));
|
_widgetDropCallback);
|
||||||
|
const auto raw = option.get();
|
||||||
|
_list.insert(begin(_list) + beforeIndex, std::move(option));
|
||||||
|
|
||||||
if (_multiCorrect) {
|
if (_multiCorrect) {
|
||||||
_list.back()->enableChooseCorrect(
|
raw->enableChooseCorrect(
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
_multiCorrectChanged);
|
_multiCorrectChanged);
|
||||||
}
|
}
|
||||||
const auto field = _list.back()->field();
|
if (!text.isEmpty()) {
|
||||||
|
raw->field()->setText(text);
|
||||||
|
}
|
||||||
|
initOptionField(raw->field());
|
||||||
|
|
||||||
|
if (isAppend) {
|
||||||
|
raw->showAddIcon(true);
|
||||||
|
}
|
||||||
|
raw->show(animated);
|
||||||
|
fixShadows();
|
||||||
|
restartReorder();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Options::initOptionField(not_null<Ui::InputField*> field) {
|
||||||
if (const auto emojiPanel = _emojiPanel) {
|
if (const auto emojiPanel = _emojiPanel) {
|
||||||
const auto isPremium = _controller->session().user()->isPremium();
|
const auto isPremium = _controller->session().user()->isPremium();
|
||||||
const auto emojiToggle = Ui::AddEmojiToggleToField(
|
const auto emojiToggle = Ui::AddEmojiToggleToField(
|
||||||
@@ -992,6 +1069,13 @@ void Options::addEmptyOption() {
|
|||||||
}, field->lifetime());
|
}, field->lifetime());
|
||||||
field->changes(
|
field->changes(
|
||||||
) | rpl::on_next([=] {
|
) | rpl::on_next([=] {
|
||||||
|
auto list = ParsePastedList(field->getLastText());
|
||||||
|
if (!list.empty()) {
|
||||||
|
field->setText(list.front());
|
||||||
|
field->forceProcessContentsChanges();
|
||||||
|
list.pop_front();
|
||||||
|
handlePaste(field, list);
|
||||||
|
}
|
||||||
Ui::PostponeCall(crl::guard(field, [=] {
|
Ui::PostponeCall(crl::guard(field, [=] {
|
||||||
validateState();
|
validateState();
|
||||||
}));
|
}));
|
||||||
@@ -1028,13 +1112,26 @@ void Options::addEmptyOption() {
|
|||||||
}
|
}
|
||||||
return base::EventFilterResult::Cancel;
|
return base::EventFilterResult::Cancel;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_list.back()->showAddIcon(true);
|
void Options::handlePaste(
|
||||||
_list.back()->show((_list.size() == 1)
|
not_null<Ui::InputField*> field,
|
||||||
? anim::type::instant
|
const QStringList &list) {
|
||||||
: anim::type::normal);
|
const auto index = findField(field);
|
||||||
fixShadows();
|
for (auto i = 0, count = int(list.size()); i != count; ++i) {
|
||||||
restartReorder();
|
insertOption(
|
||||||
|
index + 1 + i,
|
||||||
|
list[i],
|
||||||
|
anim::type::instant);
|
||||||
|
}
|
||||||
|
const auto last = std::min(
|
||||||
|
int(index + list.size()),
|
||||||
|
int(_list.size()) - 1);
|
||||||
|
const auto focus = _list[last]->field();
|
||||||
|
crl::on_main(focus, [=] {
|
||||||
|
focus->setCursorPosition(focus->getLastText().size());
|
||||||
|
focus->setFocus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Options::removeDestroyed(not_null<Option*> option) {
|
void Options::removeDestroyed(not_null<Option*> option) {
|
||||||
@@ -1067,6 +1164,16 @@ int Options::findField(not_null<Ui::InputField*> field) const {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Options::findLayoutPosition(not_null<Option*> option) const {
|
||||||
|
const auto widget = option->wrapWidget();
|
||||||
|
for (auto i = 0, count = _optionsLayout->count(); i != count; ++i) {
|
||||||
|
if (_optionsLayout->widgetAt(i).get() == widget.get()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unexpected("Poll option widget missing in layout.");
|
||||||
|
}
|
||||||
|
|
||||||
void Options::checkLastOption() {
|
void Options::checkLastOption() {
|
||||||
removeEmptyTail();
|
removeEmptyTail();
|
||||||
addEmptyOption();
|
addEmptyOption();
|
||||||
@@ -1458,6 +1565,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
rpl::event_stream<bool> showWhoVotedForceOn;
|
rpl::event_stream<bool> showWhoVotedForceOn;
|
||||||
rpl::variable<int> closePeriod = 0;
|
rpl::variable<int> closePeriod = 0;
|
||||||
rpl::variable<TimeId> closeDate = TimeId(0);
|
rpl::variable<TimeId> closeDate = TimeId(0);
|
||||||
|
rpl::variable<std::vector<QString>> countriesValue;
|
||||||
std::shared_ptr<PollMediaState> descriptionMedia
|
std::shared_ptr<PollMediaState> descriptionMedia
|
||||||
= std::make_shared<PollMediaState>();
|
= std::make_shared<PollMediaState>();
|
||||||
std::shared_ptr<PollMediaState> solutionMedia
|
std::shared_ptr<PollMediaState> solutionMedia
|
||||||
@@ -2218,11 +2326,36 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
const auto installPhotoDropToField = [=](
|
const auto installPhotoDropToField = [=](
|
||||||
not_null<Ui::InputField*> field,
|
not_null<Ui::InputField*> field,
|
||||||
std::shared_ptr<PollMediaState> media) {
|
std::shared_ptr<PollMediaState> media) {
|
||||||
installDropToField(
|
field->setMimeDataHook([=](
|
||||||
field,
|
not_null<const QMimeData*> data,
|
||||||
media,
|
Ui::InputField::MimeAction action) {
|
||||||
validatePhotoOrVideo,
|
using MimeAction = Ui::InputField::MimeAction;
|
||||||
applyPhotoOrVideoDrop);
|
const auto text = data->hasText()
|
||||||
|
? data->text()
|
||||||
|
: QString();
|
||||||
|
if (text.contains('\n')) {
|
||||||
|
if (action == MimeAction::Check) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
auto list = ParsePastedList(text);
|
||||||
|
if (list.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
field->setText(list.front());
|
||||||
|
field->forceProcessContentsChanges();
|
||||||
|
list.pop_front();
|
||||||
|
if (state->options) {
|
||||||
|
state->options->handlePaste(field, list);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (action == MimeAction::Check) {
|
||||||
|
return validatePhotoOrVideo(data);
|
||||||
|
} else if (action == MimeAction::Insert) {
|
||||||
|
return applyPhotoOrVideoDrop(media, data);
|
||||||
|
}
|
||||||
|
Unexpected("Polls: action in MimeData hook.");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const auto applyFileDrop = ApplyDropFn([=](
|
const auto applyFileDrop = ApplyDropFn([=](
|
||||||
std::shared_ptr<PollMediaState> media,
|
std::shared_ptr<PollMediaState> media,
|
||||||
@@ -2537,6 +2670,8 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
|
|
||||||
Ui::AddSkip(container);
|
Ui::AddSkip(container);
|
||||||
Ui::AddSubsectionTitle(container, tr::lng_polls_create_settings());
|
Ui::AddSubsectionTitle(container, tr::lng_polls_create_settings());
|
||||||
|
const auto isBroadcastChannel = _peer->isChannel()
|
||||||
|
&& !_peer->isMegagroup();
|
||||||
|
|
||||||
const auto showWhoVoted = (!(_disabled & PollData::Flag::PublicVotes))
|
const auto showWhoVoted = (!(_disabled & PollData::Flag::PublicVotes))
|
||||||
? AddPollToggleButton(
|
? AddPollToggleButton(
|
||||||
@@ -2615,6 +2750,94 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
| rpl::then(state->quizForceOff.events()),
|
| rpl::then(state->quizForceOff.events()),
|
||||||
st::detailedSettingsButtonStyle);
|
st::detailedSettingsButtonStyle);
|
||||||
|
|
||||||
|
const auto show = uiShow();
|
||||||
|
|
||||||
|
const auto restrictToSubscribers = isBroadcastChannel
|
||||||
|
? AddPollToggleButton(
|
||||||
|
container,
|
||||||
|
tr::lng_polls_create_restrict_to_subscribers(),
|
||||||
|
tr::lng_polls_create_restrict_to_subscribers_about(),
|
||||||
|
{
|
||||||
|
.icon = &st::pollBoxFilledPollSubscribersIcon,
|
||||||
|
.background = &st::settingsIconBg5,
|
||||||
|
},
|
||||||
|
rpl::single(!!(_chosen & PollData::Flag::SubscribersOnly)),
|
||||||
|
st::detailedSettingsButtonStyle).get()
|
||||||
|
: nullptr;
|
||||||
|
const auto limitByCountry = isBroadcastChannel
|
||||||
|
? AddPollToggleButton(
|
||||||
|
container,
|
||||||
|
tr::lng_polls_create_limit_by_country(),
|
||||||
|
tr::lng_polls_create_limit_by_country_about(),
|
||||||
|
{
|
||||||
|
.icon = &st::pollBoxFilledPollCountryIcon,
|
||||||
|
.background = &st::settingsIconBg4,
|
||||||
|
},
|
||||||
|
rpl::single(false),
|
||||||
|
st::detailedSettingsButtonStyle).get()
|
||||||
|
: nullptr;
|
||||||
|
const auto countriesWrap = limitByCountry
|
||||||
|
? container->add(
|
||||||
|
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||||
|
container,
|
||||||
|
object_ptr<Ui::VerticalLayout>(container)))
|
||||||
|
: nullptr;
|
||||||
|
const auto countriesButton = [=] {
|
||||||
|
if (!countriesWrap) {
|
||||||
|
return (Ui::SettingsButton*)(nullptr);
|
||||||
|
}
|
||||||
|
const auto inner = countriesWrap->entity();
|
||||||
|
return AddButtonWithLabel(
|
||||||
|
inner,
|
||||||
|
tr::lng_polls_create_allowed_countries(),
|
||||||
|
state->countriesValue.value(
|
||||||
|
) | rpl::map([=](const std::vector<QString> &countries) {
|
||||||
|
if (countries.empty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
if (countries.size() == 1) {
|
||||||
|
return Countries::Instance().countryNameByISO2(
|
||||||
|
countries.front(),
|
||||||
|
Countries::Naming::Polls);
|
||||||
|
}
|
||||||
|
return tr::lng_polls_create_countries_count(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
countries.size());
|
||||||
|
}),
|
||||||
|
st::settingsButtonNoIcon).get();
|
||||||
|
}();
|
||||||
|
if (countriesWrap) {
|
||||||
|
countriesWrap->toggleOn(
|
||||||
|
rpl::single(limitByCountry->toggled())
|
||||||
|
| rpl::then(limitByCountry->toggledChanges()));
|
||||||
|
}
|
||||||
|
if (countriesButton) {
|
||||||
|
countriesButton->setClickedCallback([=] {
|
||||||
|
const auto done = [=](std::vector<QString> countries) {
|
||||||
|
state->countriesValue = std::move(countries);
|
||||||
|
};
|
||||||
|
const auto limit
|
||||||
|
= _controller->session().appConfig().pollCountriesMax();
|
||||||
|
const auto checkError = [=](int count) {
|
||||||
|
if (count >= limit) {
|
||||||
|
show->showToast(tr::lng_polls_create_countries_limit(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
limit));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
show->show(Box(
|
||||||
|
Ui::SelectCountriesBox,
|
||||||
|
state->countriesValue.current(),
|
||||||
|
done,
|
||||||
|
checkError,
|
||||||
|
Countries::Naming::Polls));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const auto duration = AddPollToggleButton(
|
const auto duration = AddPollToggleButton(
|
||||||
container,
|
container,
|
||||||
tr::lng_polls_create_limit_duration(),
|
tr::lng_polls_create_limit_duration(),
|
||||||
@@ -2659,7 +2882,6 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
std::move(pollEndsLabelText),
|
std::move(pollEndsLabelText),
|
||||||
st::settingsButtonNoIcon);
|
st::settingsButtonNoIcon);
|
||||||
|
|
||||||
const auto show = uiShow();
|
|
||||||
pollEndsLabel->setClickedCallback([=] {
|
pollEndsLabel->setClickedCallback([=] {
|
||||||
state->durationMenu = base::make_unique_q<Ui::PopupMenu>(
|
state->durationMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||||
pollEndsLabel,
|
pollEndsLabel,
|
||||||
@@ -2732,11 +2954,6 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
st::settingsButtonNoIcon)
|
st::settingsButtonNoIcon)
|
||||||
)->toggleOn(rpl::single(false));
|
)->toggleOn(rpl::single(false));
|
||||||
|
|
||||||
Ui::AddSkip(durationInner);
|
|
||||||
Ui::AddDividerText(
|
|
||||||
durationInner,
|
|
||||||
tr::lng_polls_create_hide_results_about());
|
|
||||||
|
|
||||||
const auto solution = setupSolution(
|
const auto solution = setupSolution(
|
||||||
container,
|
container,
|
||||||
rpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges()));
|
rpl::single(quiz->toggled()) | rpl::then(quiz->toggledChanges()));
|
||||||
@@ -2775,6 +2992,10 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
};
|
};
|
||||||
quiz->setToggleLocked(_disabled & PollData::Flag::Quiz);
|
quiz->setToggleLocked(_disabled & PollData::Flag::Quiz);
|
||||||
shuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers);
|
shuffle->setToggleLocked(_disabled & PollData::Flag::ShuffleAnswers);
|
||||||
|
if (restrictToSubscribers) {
|
||||||
|
restrictToSubscribers->setToggleLocked(
|
||||||
|
_disabled & PollData::Flag::SubscribersOnly);
|
||||||
|
}
|
||||||
updateQuizDependentLocks(quiz->toggled());
|
updateQuizDependentLocks(quiz->toggled());
|
||||||
|
|
||||||
using namespace rpl::mappers;
|
using namespace rpl::mappers;
|
||||||
@@ -2862,8 +3083,14 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
}
|
}
|
||||||
const auto publicVotes = (showWhoVoted && showWhoVoted->toggled());
|
const auto publicVotes = (showWhoVoted && showWhoVoted->toggled());
|
||||||
const auto multiChoice = multiple->toggled();
|
const auto multiChoice = multiple->toggled();
|
||||||
|
const auto subscribersOnly = (restrictToSubscribers
|
||||||
|
&& restrictToSubscribers->toggled());
|
||||||
const auto hideResultsEnabled = duration->toggled()
|
const auto hideResultsEnabled = duration->toggled()
|
||||||
&& hideResults->toggled();
|
&& hideResults->toggled();
|
||||||
|
result.countries = (limitByCountry
|
||||||
|
&& limitByCountry->toggled())
|
||||||
|
? state->countriesValue.current()
|
||||||
|
: std::vector<QString>();
|
||||||
result.setFlags(Flag(0)
|
result.setFlags(Flag(0)
|
||||||
| (publicVotes ? Flag::PublicVotes : Flag(0))
|
| (publicVotes ? Flag::PublicVotes : Flag(0))
|
||||||
| (multiChoice ? Flag::MultiChoice : Flag(0))
|
| (multiChoice ? Flag::MultiChoice : Flag(0))
|
||||||
@@ -2871,6 +3098,7 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
| (!revoting->toggled() ? Flag::RevotingDisabled : Flag(0))
|
| (!revoting->toggled() ? Flag::RevotingDisabled : Flag(0))
|
||||||
| (shuffle->toggled() ? Flag::ShuffleAnswers : Flag(0))
|
| (shuffle->toggled() ? Flag::ShuffleAnswers : Flag(0))
|
||||||
| (quiz->toggled() ? Flag::Quiz : Flag(0))
|
| (quiz->toggled() ? Flag::Quiz : Flag(0))
|
||||||
|
| (subscribersOnly ? Flag::SubscribersOnly : Flag(0))
|
||||||
| (hideResultsEnabled
|
| (hideResultsEnabled
|
||||||
? Flag::HideResultsUntilClose
|
? Flag::HideResultsUntilClose
|
||||||
: Flag(0)));
|
: Flag(0)));
|
||||||
@@ -2934,6 +3162,13 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
} else {
|
} else {
|
||||||
state->error &= ~Error::Deadline;
|
state->error &= ~Error::Deadline;
|
||||||
}
|
}
|
||||||
|
if (limitByCountry
|
||||||
|
&& limitByCountry->toggled()
|
||||||
|
&& state->countriesValue.current().empty()) {
|
||||||
|
state->error |= Error::Country;
|
||||||
|
} else {
|
||||||
|
state->error &= ~Error::Country;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const auto showError = [show = uiShow()](
|
const auto showError = [show = uiShow()](
|
||||||
tr::phrase<> text) {
|
tr::phrase<> text) {
|
||||||
@@ -2993,6 +3228,11 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
ShowMediaUploadingToast();
|
ShowMediaUploadingToast();
|
||||||
} else if (state->error & Error::Deadline) {
|
} else if (state->error & Error::Deadline) {
|
||||||
showError(tr::lng_polls_create_deadline_expired);
|
showError(tr::lng_polls_create_deadline_expired);
|
||||||
|
} else if (state->error & Error::Country) {
|
||||||
|
showError(tr::lng_polls_create_choose_country);
|
||||||
|
if (countriesButton) {
|
||||||
|
scrollToWidget(countriesButton);
|
||||||
|
}
|
||||||
} else if (!state->error) {
|
} else if (!state->error) {
|
||||||
auto result = collectResult();
|
auto result = collectResult();
|
||||||
result.options = sendOptions;
|
result.options = sendOptions;
|
||||||
@@ -3049,6 +3289,15 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
|||||||
duration->finishAnimating();
|
duration->finishAnimating();
|
||||||
durationWrap->finishAnimating();
|
durationWrap->finishAnimating();
|
||||||
hideResults->finishAnimating();
|
hideResults->finishAnimating();
|
||||||
|
if (restrictToSubscribers) {
|
||||||
|
restrictToSubscribers->finishAnimating();
|
||||||
|
}
|
||||||
|
if (limitByCountry) {
|
||||||
|
limitByCountry->finishAnimating();
|
||||||
|
}
|
||||||
|
if (countriesWrap) {
|
||||||
|
countriesWrap->finishAnimating();
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ private:
|
|||||||
Solution = 0x10,
|
Solution = 0x10,
|
||||||
Media = 0x20,
|
Media = 0x20,
|
||||||
Deadline = 0x40,
|
Deadline = 0x40,
|
||||||
|
Country = 0x80,
|
||||||
};
|
};
|
||||||
friend constexpr inline bool is_flag_type(Error) { return true; }
|
friend constexpr inline bool is_flag_type(Error) { return true; }
|
||||||
using Errors = base::flags<Error>;
|
using Errors = base::flags<Error>;
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "boxes/delete_messages_box.h"
|
#include "boxes/delete_messages_box.h"
|
||||||
|
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "api/api_chat_participants.h"
|
|
||||||
#include "api/api_messages_search.h"
|
|
||||||
#include "api/api_report.h"
|
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
#include "core/core_settings.h"
|
#include "core/core_settings.h"
|
||||||
@@ -44,18 +41,9 @@ constexpr auto kDeleteMessagesBoxAnimationDuration = crl::time(80);
|
|||||||
|
|
||||||
DeleteMessagesBox::DeleteMessagesBox(
|
DeleteMessagesBox::DeleteMessagesBox(
|
||||||
QWidget*,
|
QWidget*,
|
||||||
not_null<HistoryItem*> item,
|
not_null<HistoryItem*> item)
|
||||||
bool suggestModerateActions)
|
|
||||||
: _session(&item->history()->session())
|
: _session(&item->history()->session())
|
||||||
, _ids(1, item->fullId()) {
|
, _ids(1, item->fullId()) {
|
||||||
if (suggestModerateActions) {
|
|
||||||
_moderateBan = item->suggestBanReport();
|
|
||||||
_moderateDeleteAll = item->suggestDeleteAllReport();
|
|
||||||
if (_moderateBan || _moderateDeleteAll) {
|
|
||||||
_moderateFrom = item->from();
|
|
||||||
_moderateInChannel = item->history()->peer->asChannel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteMessagesBox::DeleteMessagesBox(
|
DeleteMessagesBox::DeleteMessagesBox(
|
||||||
@@ -188,53 +176,6 @@ void DeleteMessagesBox::prepare() {
|
|||||||
tr::lng_delete_clear_for_me(tr::now)
|
tr::lng_delete_clear_for_me(tr::now)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (_moderateFrom) {
|
|
||||||
Assert(_moderateInChannel != nullptr);
|
|
||||||
|
|
||||||
details.text = tr::lng_selected_delete_sure_this(tr::now);
|
|
||||||
if (_moderateBan) {
|
|
||||||
_banUser.create(
|
|
||||||
this,
|
|
||||||
tr::lng_ban_user(tr::now),
|
|
||||||
false,
|
|
||||||
st::defaultBoxCheckbox);
|
|
||||||
}
|
|
||||||
_reportSpam.create(
|
|
||||||
this,
|
|
||||||
tr::lng_report_spam(tr::now),
|
|
||||||
false,
|
|
||||||
st::defaultBoxCheckbox);
|
|
||||||
if (_moderateDeleteAll) {
|
|
||||||
const auto search = lifetime().make_state<Api::MessagesSearch>(
|
|
||||||
_session->data().message(_ids.front())->history());
|
|
||||||
_deleteAll.create(
|
|
||||||
this,
|
|
||||||
tr::lng_delete_all_from_user(
|
|
||||||
tr::now,
|
|
||||||
lt_user,
|
|
||||||
tr::bold(_moderateFrom->name()),
|
|
||||||
tr::marked),
|
|
||||||
false,
|
|
||||||
st::defaultBoxCheckbox);
|
|
||||||
|
|
||||||
*deleteText = rpl::combine(
|
|
||||||
rpl::single(
|
|
||||||
0
|
|
||||||
) | rpl::then(
|
|
||||||
search->messagesFounds(
|
|
||||||
) | rpl::map([](const Api::FoundMessages &found) {
|
|
||||||
return found.total;
|
|
||||||
})
|
|
||||||
),
|
|
||||||
_deleteAll->checkedValue()
|
|
||||||
) | rpl::map([](int total, bool checked) {
|
|
||||||
return tr::lng_box_delete(tr::now)
|
|
||||||
+ ((total <= 0 || !checked)
|
|
||||||
? QString()
|
|
||||||
: QString(" (%1)").arg(total));
|
|
||||||
});
|
|
||||||
search->searchMessages({ .from = _moderateFrom });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
details.text = hasSavedMusicMessages()
|
details.text = hasSavedMusicMessages()
|
||||||
? tr::lng_selected_remove_saved_music(tr::now)
|
? tr::lng_selected_remove_saved_music(tr::now)
|
||||||
@@ -337,16 +278,7 @@ void DeleteMessagesBox::prepare() {
|
|||||||
auto fullHeight = st::boxPadding.top()
|
auto fullHeight = st::boxPadding.top()
|
||||||
+ _text->height()
|
+ _text->height()
|
||||||
+ st::boxPadding.bottom();
|
+ st::boxPadding.bottom();
|
||||||
if (_moderateFrom) {
|
if (_revoke) {
|
||||||
fullHeight += st::boxMediumSkip;
|
|
||||||
if (_banUser) {
|
|
||||||
fullHeight += _banUser->heightNoMargins() + st::boxLittleSkip;
|
|
||||||
}
|
|
||||||
fullHeight += _reportSpam->heightNoMargins();
|
|
||||||
if (_deleteAll) {
|
|
||||||
fullHeight += st::boxLittleSkip + _deleteAll->heightNoMargins();
|
|
||||||
}
|
|
||||||
} else if (_revoke) {
|
|
||||||
fullHeight += st::boxMediumSkip + _revoke->heightNoMargins();
|
fullHeight += st::boxMediumSkip + _revoke->heightNoMargins();
|
||||||
}
|
}
|
||||||
if (_autoDeleteSettings) {
|
if (_autoDeleteSettings) {
|
||||||
@@ -485,20 +417,7 @@ void DeleteMessagesBox::resizeEvent(QResizeEvent *e) {
|
|||||||
const auto &padding = st::boxPadding;
|
const auto &padding = st::boxPadding;
|
||||||
_text->moveToLeft(padding.left(), padding.top());
|
_text->moveToLeft(padding.left(), padding.top());
|
||||||
auto top = _text->bottomNoMargins() + st::boxMediumSkip;
|
auto top = _text->bottomNoMargins() + st::boxMediumSkip;
|
||||||
if (_moderateFrom) {
|
if (_revoke) {
|
||||||
if (_banUser) {
|
|
||||||
_banUser->moveToLeft(padding.left(), top);
|
|
||||||
top += _banUser->heightNoMargins() + st::boxLittleSkip;
|
|
||||||
}
|
|
||||||
_reportSpam->moveToLeft(padding.left(), top);
|
|
||||||
top += _reportSpam->heightNoMargins() + st::boxLittleSkip;
|
|
||||||
if (_deleteAll) {
|
|
||||||
const auto availableWidth = width() - 2 * padding.left();
|
|
||||||
_deleteAll->resizeToNaturalWidth(availableWidth);
|
|
||||||
_deleteAll->moveToLeft(padding.left(), top);
|
|
||||||
top += _deleteAll->heightNoMargins() + st::boxLittleSkip;
|
|
||||||
}
|
|
||||||
} else if (_revoke) {
|
|
||||||
const auto availableWidth = width() - 2 * padding.left();
|
const auto availableWidth = width() - 2 * padding.left();
|
||||||
_revoke->resizeToNaturalWidth(availableWidth);
|
_revoke->resizeToNaturalWidth(availableWidth);
|
||||||
_revoke->moveToLeft(padding.left(), top);
|
_revoke->moveToLeft(padding.left(), top);
|
||||||
@@ -635,23 +554,6 @@ void DeleteMessagesBox::deleteAndClear() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_moderateFrom) {
|
|
||||||
if (_banUser && _banUser->checked()) {
|
|
||||||
_moderateInChannel->session().api().chatParticipants().kick(
|
|
||||||
_moderateInChannel,
|
|
||||||
_moderateFrom,
|
|
||||||
ChatRestrictionsInfo());
|
|
||||||
}
|
|
||||||
if (_reportSpam->checked()) {
|
|
||||||
Api::ReportSpam(_moderateFrom, { _ids[0] });
|
|
||||||
}
|
|
||||||
if (_deleteAll && _deleteAll->checked()) {
|
|
||||||
_moderateInChannel->session().api().deleteAllFromParticipant(
|
|
||||||
_moderateInChannel,
|
|
||||||
_moderateFrom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto ids = _ids;
|
const auto ids = _ids;
|
||||||
invokeCallbackAndClose();
|
invokeCallbackAndClose();
|
||||||
session->data().histories().deleteMessages(ids, revoke);
|
session->data().histories().deleteMessages(ids, revoke);
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ class DeleteMessagesBox final : public Ui::BoxContent {
|
|||||||
public:
|
public:
|
||||||
DeleteMessagesBox(
|
DeleteMessagesBox(
|
||||||
QWidget*,
|
QWidget*,
|
||||||
not_null<HistoryItem*> item,
|
not_null<HistoryItem*> item);
|
||||||
bool suggestModerateActions);
|
|
||||||
DeleteMessagesBox(
|
DeleteMessagesBox(
|
||||||
QWidget*,
|
QWidget*,
|
||||||
not_null<Main::Session*> session,
|
not_null<Main::Session*> session,
|
||||||
@@ -71,10 +70,6 @@ private:
|
|||||||
const QDate _wipeHistoryFirstToDelete;
|
const QDate _wipeHistoryFirstToDelete;
|
||||||
const QDate _wipeHistoryLastToDelete;
|
const QDate _wipeHistoryLastToDelete;
|
||||||
const MessageIdsList _ids;
|
const MessageIdsList _ids;
|
||||||
PeerData *_moderateFrom = nullptr;
|
|
||||||
ChannelData *_moderateInChannel = nullptr;
|
|
||||||
bool _moderateBan = false;
|
|
||||||
bool _moderateDeleteAll = false;
|
|
||||||
|
|
||||||
bool _revokeForBot = false;
|
bool _revokeForBot = false;
|
||||||
bool _revokeJustClearForChannel = false;
|
bool _revokeJustClearForChannel = false;
|
||||||
@@ -82,9 +77,6 @@ private:
|
|||||||
object_ptr<Ui::FlatLabel> _text = { nullptr };
|
object_ptr<Ui::FlatLabel> _text = { nullptr };
|
||||||
object_ptr<Ui::Checkbox> _revoke = { nullptr };
|
object_ptr<Ui::Checkbox> _revoke = { nullptr };
|
||||||
object_ptr<Ui::SlideWrap<Ui::Checkbox>> _revokeRemember = { nullptr };
|
object_ptr<Ui::SlideWrap<Ui::Checkbox>> _revokeRemember = { nullptr };
|
||||||
object_ptr<Ui::Checkbox> _banUser = { nullptr };
|
|
||||||
object_ptr<Ui::Checkbox> _reportSpam = { nullptr };
|
|
||||||
object_ptr<Ui::Checkbox> _deleteAll = { nullptr };
|
|
||||||
object_ptr<Ui::LinkButton> _autoDeleteSettings = { nullptr };
|
object_ptr<Ui::LinkButton> _autoDeleteSettings = { nullptr };
|
||||||
|
|
||||||
int _fullHeight = 0;
|
int _fullHeight = 0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "base/event_filter.h"
|
#include "base/event_filter.h"
|
||||||
#include "boxes/premium_limits_box.h"
|
#include "boxes/premium_limits_box.h"
|
||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
|
#include "boxes/send_files_box.h"
|
||||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||||
#include "chat_helpers/field_autocomplete.h"
|
#include "chat_helpers/field_autocomplete.h"
|
||||||
#include "chat_helpers/message_field.h"
|
#include "chat_helpers/message_field.h"
|
||||||
@@ -545,6 +546,24 @@ void EditCaptionBox::rebuildPreview() {
|
|||||||
_content->heightValue(
|
_content->heightValue(
|
||||||
) | rpl::start_to_stream(_contentHeight, _content->lifetime());
|
) | rpl::start_to_stream(_contentHeight, _content->lifetime());
|
||||||
|
|
||||||
|
if (const auto file = dynamic_cast<Ui::AbstractSingleFilePreview*>(
|
||||||
|
_content.get())) {
|
||||||
|
file->setRenameEnabled(!_preparedList.files.empty());
|
||||||
|
file->renameRequests(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
renameCurrentFile();
|
||||||
|
}, _content->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
base::install_event_filter(_content.get(), [=](not_null<QEvent*> e) {
|
||||||
|
if (e->type() == QEvent::ContextMenu) {
|
||||||
|
const auto mouse = static_cast<QContextMenuEvent*>(e.get());
|
||||||
|
showMenu(mouse->globalPos(), false);
|
||||||
|
return base::EventFilterResult::Cancel;
|
||||||
|
}
|
||||||
|
return base::EventFilterResult::Continue;
|
||||||
|
}, _content->lifetime());
|
||||||
|
|
||||||
_scroll->setOwnedWidget(
|
_scroll->setOwnedWidget(
|
||||||
object_ptr<Ui::RpWidget>::fromRaw(_content.get()));
|
object_ptr<Ui::RpWidget>::fromRaw(_content.get()));
|
||||||
|
|
||||||
@@ -728,81 +747,109 @@ void EditCaptionBox::setupControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EditCaptionBox::setupEditEventHandler() {
|
void EditCaptionBox::setupEditEventHandler() {
|
||||||
const auto menu
|
|
||||||
= lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();
|
|
||||||
_editMediaClicks.events(
|
_editMediaClicks.events(
|
||||||
) | rpl::on_next([=] {
|
) | rpl::on_next([=] {
|
||||||
*menu = base::make_unique_q<Ui::PopupMenu>(
|
showMenu(QCursor::pos(), true);
|
||||||
this,
|
|
||||||
st::popupMenuWithIcons);
|
|
||||||
(*menu)->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
|
||||||
if (_isAllowedEditMedia) {
|
|
||||||
(*menu)->addAction(tr::lng_attach_replace(tr::now), [=] {
|
|
||||||
ChooseReplacement(
|
|
||||||
_controller,
|
|
||||||
_albumType,
|
|
||||||
crl::guard(this, [=](Ui::PreparedList &&list) {
|
|
||||||
setPreparedList(std::move(list));
|
|
||||||
}));
|
|
||||||
}, &st::menuIconReplace);
|
|
||||||
}
|
|
||||||
using Type = Ui::PreparedFile::Type;
|
|
||||||
const auto canDraw = !_preparedList.files.empty()
|
|
||||||
? (_preparedList.files.front().type == Type::Photo)
|
|
||||||
: (_isPhoto && !_asFile);
|
|
||||||
if (canDraw) {
|
|
||||||
(*menu)->addAction(tr::lng_context_draw(tr::now), [=] {
|
|
||||||
_photoEditorOpens.fire({});
|
|
||||||
}, &st::menuIconDraw);
|
|
||||||
}
|
|
||||||
if (!_asFile && (_isPhoto || _isVideo)) {
|
|
||||||
if (hasSendLargePhotosOption()) {
|
|
||||||
const auto enabled = _sendLargePhotos;
|
|
||||||
Menu::AddCheckedAction(
|
|
||||||
menu->get(),
|
|
||||||
tr::lng_send_high_quality(tr::now),
|
|
||||||
[=] {
|
|
||||||
_sendLargePhotos = !enabled;
|
|
||||||
rebuildPreview();
|
|
||||||
},
|
|
||||||
&st::menuIconQualityHigh,
|
|
||||||
enabled);
|
|
||||||
}
|
|
||||||
if (_preparedList.hasSpoilerMenu(!_asFile)) {
|
|
||||||
const auto spoilered = hasSpoiler();
|
|
||||||
Menu::AddCheckedAction(
|
|
||||||
menu->get(),
|
|
||||||
tr::lng_context_spoiler_effect(tr::now),
|
|
||||||
[=] {
|
|
||||||
_mediaEditManager.apply({ .type = spoilered
|
|
||||||
? SendMenu::ActionType::SpoilerOff
|
|
||||||
: SendMenu::ActionType::SpoilerOn
|
|
||||||
});
|
|
||||||
rebuildPreview();
|
|
||||||
},
|
|
||||||
&st::menuIconSpoiler,
|
|
||||||
spoilered);
|
|
||||||
}
|
|
||||||
if (_isVideo && !_preparedList.files.empty()) {
|
|
||||||
(*menu)->addAction(tr::lng_context_edit_cover(tr::now), [=] {
|
|
||||||
setupEditCoverHandler();
|
|
||||||
}, &st::menuIconEdit);
|
|
||||||
if (_preparedList.files.front().videoCover != nullptr) {
|
|
||||||
(*menu)->addAction(
|
|
||||||
tr::lng_context_clear_cover(tr::now),
|
|
||||||
[=] { setupClearCoverHandler(); },
|
|
||||||
&st::menuIconCancel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((*menu)->empty()) {
|
|
||||||
*menu = nullptr;
|
|
||||||
} else {
|
|
||||||
(*menu)->popup(QCursor::pos());
|
|
||||||
}
|
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EditCaptionBox::showMenu(QPoint globalPos, bool forceTopRight) {
|
||||||
|
_previewMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||||
|
this,
|
||||||
|
st::popupMenuWithIcons);
|
||||||
|
if (forceTopRight) {
|
||||||
|
_previewMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
||||||
|
}
|
||||||
|
if (_isAllowedEditMedia) {
|
||||||
|
_previewMenu->addAction(tr::lng_attach_replace(tr::now), [=] {
|
||||||
|
ChooseReplacement(
|
||||||
|
_controller,
|
||||||
|
_albumType,
|
||||||
|
crl::guard(this, [=](Ui::PreparedList &&list) {
|
||||||
|
setPreparedList(std::move(list));
|
||||||
|
}));
|
||||||
|
}, &st::menuIconReplace);
|
||||||
|
}
|
||||||
|
if (dynamic_cast<Ui::AbstractSingleFilePreview*>(_content.get())
|
||||||
|
&& !_preparedList.files.empty()) {
|
||||||
|
_previewMenu->addAction(tr::lng_rename_file(tr::now), [=] {
|
||||||
|
renameCurrentFile();
|
||||||
|
}, &st::menuIconEdit);
|
||||||
|
}
|
||||||
|
using Type = Ui::PreparedFile::Type;
|
||||||
|
const auto canDraw = !_preparedList.files.empty()
|
||||||
|
? (_preparedList.files.front().type == Type::Photo)
|
||||||
|
: (_isPhoto && !_asFile);
|
||||||
|
if (canDraw) {
|
||||||
|
_previewMenu->addAction(tr::lng_context_draw(tr::now), [=] {
|
||||||
|
_photoEditorOpens.fire({});
|
||||||
|
}, &st::menuIconDraw);
|
||||||
|
}
|
||||||
|
if (!_asFile && (_isPhoto || _isVideo)) {
|
||||||
|
if (hasSendLargePhotosOption()) {
|
||||||
|
const auto enabled = _sendLargePhotos;
|
||||||
|
Menu::AddCheckedAction(
|
||||||
|
_previewMenu.get(),
|
||||||
|
tr::lng_send_high_quality(tr::now),
|
||||||
|
[=] {
|
||||||
|
_sendLargePhotos = !enabled;
|
||||||
|
rebuildPreview();
|
||||||
|
},
|
||||||
|
&st::menuIconQualityHigh,
|
||||||
|
enabled);
|
||||||
|
}
|
||||||
|
if (_preparedList.hasSpoilerMenu(!_asFile)) {
|
||||||
|
const auto spoilered = hasSpoiler();
|
||||||
|
Menu::AddCheckedAction(
|
||||||
|
_previewMenu.get(),
|
||||||
|
tr::lng_context_spoiler_effect(tr::now),
|
||||||
|
[=] {
|
||||||
|
_mediaEditManager.apply({ .type = spoilered
|
||||||
|
? SendMenu::ActionType::SpoilerOff
|
||||||
|
: SendMenu::ActionType::SpoilerOn
|
||||||
|
});
|
||||||
|
rebuildPreview();
|
||||||
|
},
|
||||||
|
&st::menuIconSpoiler,
|
||||||
|
spoilered);
|
||||||
|
}
|
||||||
|
if (_isVideo && !_preparedList.files.empty()) {
|
||||||
|
_previewMenu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
|
||||||
|
setupEditCoverHandler();
|
||||||
|
}, &st::menuIconEdit);
|
||||||
|
if (_preparedList.files.front().videoCover != nullptr) {
|
||||||
|
_previewMenu->addAction(
|
||||||
|
tr::lng_context_clear_cover(tr::now),
|
||||||
|
[=] { setupClearCoverHandler(); },
|
||||||
|
&st::menuIconCancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_previewMenu->empty()) {
|
||||||
|
_previewMenu = nullptr;
|
||||||
|
} else {
|
||||||
|
_previewMenu->popup(globalPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditCaptionBox::renameCurrentFile() {
|
||||||
|
if (_preparedList.files.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &file = _preparedList.files.front();
|
||||||
|
const auto allowExtensionEdit = file.path.isEmpty();
|
||||||
|
_controller->show(Box(RenameFileBox, file.displayName, allowExtensionEdit, [=](
|
||||||
|
QString displayName) {
|
||||||
|
_preparedList.files.front().displayName = displayName;
|
||||||
|
if (const auto filePreview = dynamic_cast<Ui::AbstractSingleFilePreview*>(
|
||||||
|
_content.get())) {
|
||||||
|
filePreview->setDisplayName(displayName);
|
||||||
|
} else {
|
||||||
|
rebuildPreview();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
void EditCaptionBox::setupPhotoEditorEventHandler() {
|
void EditCaptionBox::setupPhotoEditorEventHandler() {
|
||||||
const auto openedOnce = lifetime().make_state<bool>(false);
|
const auto openedOnce = lifetime().make_state<bool>(false);
|
||||||
_photoEditorOpens.events(
|
_photoEditorOpens.events(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace Ui {
|
|||||||
class AbstractSinglePreview;
|
class AbstractSinglePreview;
|
||||||
class InputField;
|
class InputField;
|
||||||
class EmojiButton;
|
class EmojiButton;
|
||||||
|
class PopupMenu;
|
||||||
class VerticalLayout;
|
class VerticalLayout;
|
||||||
enum class AlbumType;
|
enum class AlbumType;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
@@ -90,6 +91,8 @@ protected:
|
|||||||
private:
|
private:
|
||||||
void rebuildPreview();
|
void rebuildPreview();
|
||||||
void setupEditEventHandler();
|
void setupEditEventHandler();
|
||||||
|
void showMenu(QPoint globalPos, bool forceTopRight);
|
||||||
|
void renameCurrentFile();
|
||||||
void setupPhotoEditorEventHandler();
|
void setupPhotoEditorEventHandler();
|
||||||
void setupEditCoverHandler();
|
void setupEditCoverHandler();
|
||||||
void setupClearCoverHandler();
|
void setupClearCoverHandler();
|
||||||
@@ -137,6 +140,7 @@ private:
|
|||||||
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
|
std::unique_ptr<ChatHelpers::FieldAutocomplete> _autocomplete;
|
||||||
|
|
||||||
base::unique_qptr<Ui::AbstractSinglePreview> _content;
|
base::unique_qptr<Ui::AbstractSinglePreview> _content;
|
||||||
|
base::unique_qptr<Ui::PopupMenu> _previewMenu;
|
||||||
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
|
||||||
base::unique_qptr<QObject> _emojiFilter;
|
base::unique_qptr<QObject> _emojiFilter;
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
#include "ui/controls/userpic_button.h"
|
#include "ui/controls/userpic_button.h"
|
||||||
#include "ui/effects/ripple_animation.h"
|
#include "ui/effects/ripple_animation.h"
|
||||||
|
#include "ui/effects/toggle_arrow.h"
|
||||||
#include "ui/layers/generic_box.h"
|
#include "ui/layers/generic_box.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
#include "ui/rect.h"
|
#include "ui/rect.h"
|
||||||
@@ -44,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/text/text_lottie_custom_emoji.h"
|
#include "ui/text/text_lottie_custom_emoji.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/vertical_list.h"
|
#include "ui/vertical_list.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
#include "ui/widgets/expandable_peer_list.h"
|
#include "ui/widgets/expandable_peer_list.h"
|
||||||
#include "ui/widgets/participants_check_view.h"
|
#include "ui/widgets/participants_check_view.h"
|
||||||
@@ -84,22 +86,43 @@ namespace {
|
|||||||
constexpr auto kModerateMessagesBoxAnimationDuration = crl::time(80);
|
constexpr auto kModerateMessagesBoxAnimationDuration = crl::time(80);
|
||||||
|
|
||||||
struct ModerateOptions final {
|
struct ModerateOptions final {
|
||||||
bool allCanBan = false;
|
bool reportSpam = false;
|
||||||
bool allCanDelete = false;
|
bool deleteAllMessages = false;
|
||||||
|
bool deleteAllReactions = false;
|
||||||
|
bool banOrRestrict = false;
|
||||||
Participants participants;
|
Participants participants;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool PeerCanDeleteMessages(not_null<PeerData*> peer) {
|
||||||
|
if (const auto chat = peer->asChat()) {
|
||||||
|
return chat->canDeleteMessages();
|
||||||
|
}
|
||||||
|
const auto channel = peer->asChannel();
|
||||||
|
return channel && channel->canDeleteMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsExcludedModerateParticipant(
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
not_null<PeerData*> participant) {
|
||||||
|
if ((participant == peer) || participant->isSelf()) {
|
||||||
|
return true;
|
||||||
|
} else if (const auto channel = participant->asChannel()) {
|
||||||
|
return (channel->discussionLink() == peer);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
|
ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
|
||||||
Expects(!items.empty());
|
Expects(!items.empty());
|
||||||
|
|
||||||
auto result = ModerateOptions{
|
auto result = ModerateOptions{
|
||||||
.allCanBan = true,
|
.deleteAllMessages = true,
|
||||||
.allCanDelete = true,
|
.banOrRestrict = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto peer = items.front()->history()->peer;
|
const auto peer = items.front()->history()->peer;
|
||||||
for (const auto &item : items) {
|
for (const auto &item : items) {
|
||||||
if (!result.allCanBan && !result.allCanDelete) {
|
if (!result.deleteAllMessages && !result.banOrRestrict) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (peer != item->history()->peer) {
|
if (peer != item->history()->peer) {
|
||||||
@@ -116,10 +139,10 @@ ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!item->suggestBanReport()) {
|
if (!item->suggestBanReport()) {
|
||||||
result.allCanBan = false;
|
result.banOrRestrict = false;
|
||||||
}
|
}
|
||||||
if (!item->suggestDeleteAllReport()) {
|
if (!item->suggestDeleteAllReport()) {
|
||||||
result.allCanDelete = false;
|
result.deleteAllMessages = false;
|
||||||
}
|
}
|
||||||
if (const auto p = item->from()) {
|
if (const auto p = item->from()) {
|
||||||
if (!ranges::contains(result.participants, not_null{ p })) {
|
if (!ranges::contains(result.participants, not_null{ p })) {
|
||||||
@@ -127,9 +150,50 @@ ModerateOptions CalculateModerateOptions(const HistoryItemsList &items) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result.deleteAllReactions = result.deleteAllMessages;
|
||||||
|
result.reportSpam = result.deleteAllMessages || result.banOrRestrict;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModerateOptions CalculateModerateOptions(const ModerateReactionEntry &reaction) {
|
||||||
|
auto result = ModerateOptions{
|
||||||
|
.participants = { reaction.participant },
|
||||||
|
};
|
||||||
|
if (IsExcludedModerateParticipant(reaction.peer, reaction.participant)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.reportSpam = Api::GetReactionReportCapabilities(
|
||||||
|
reaction.peer,
|
||||||
|
reaction.participant
|
||||||
|
).canReport || (reaction.peer->asChannel() != nullptr);
|
||||||
|
result.deleteAllReactions = PeerCanDeleteMessages(reaction.peer);
|
||||||
|
if (const auto channel = reaction.peer->asChannel()) {
|
||||||
|
result.deleteAllMessages = channel->canDeleteMessages();
|
||||||
|
result.banOrRestrict = channel->canRestrictParticipant(
|
||||||
|
reaction.participant);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool HasModerateActions(const ModerateOptions &options) {
|
||||||
|
return options.reportSpam
|
||||||
|
|| options.deleteAllMessages
|
||||||
|
|| options.deleteAllReactions
|
||||||
|
|| options.banOrRestrict;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] TextWithEntities ParticipantsExpanderText(int count) {
|
||||||
|
return tr::marked()
|
||||||
|
.append(st::moderateBoxExpand)
|
||||||
|
.append(QString::number(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] TextWithEntities DeleteOptionsExpanderText(
|
||||||
|
int checkedCount,
|
||||||
|
int totalCount) {
|
||||||
|
return tr::marked(u"%1 / %2"_q.arg(checkedCount).arg(totalCount));
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] rpl::producer<base::flat_map<PeerId, int>> MessagesCountValue(
|
[[nodiscard]] rpl::producer<base::flat_map<PeerId, int>> MessagesCountValue(
|
||||||
not_null<History*> history,
|
not_null<History*> history,
|
||||||
std::vector<not_null<PeerData*>> from) {
|
std::vector<not_null<PeerData*>> from) {
|
||||||
@@ -312,7 +376,7 @@ void ProccessCommonGroups(
|
|||||||
Fn<void(CommonGroups, not_null<UserData*>)> processHas) {
|
Fn<void(CommonGroups, not_null<UserData*>)> processHas) {
|
||||||
const auto moderateOptions = CalculateModerateOptions(items);
|
const auto moderateOptions = CalculateModerateOptions(items);
|
||||||
if (moderateOptions.participants.size() != 1
|
if (moderateOptions.participants.size() != 1
|
||||||
|| !moderateOptions.allCanBan) {
|
|| !moderateOptions.banOrRestrict) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto participant = moderateOptions.participants.front();
|
const auto participant = moderateOptions.participants.front();
|
||||||
@@ -351,16 +415,28 @@ void ProccessCommonGroups(
|
|||||||
|
|
||||||
void CreateModerateMessagesBox(
|
void CreateModerateMessagesBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
const HistoryItemsList &items,
|
ModerateMessagesBoxEntry entry,
|
||||||
Fn<void()> confirmed,
|
Fn<void()> confirmed,
|
||||||
ModerateMessagesBoxOptions options) {
|
ModerateMessagesBoxOptions options) {
|
||||||
Expects(!items.empty());
|
const auto &items = entry.items;
|
||||||
|
const auto reaction = entry.reaction;
|
||||||
|
Expects(!items.empty() || reaction.has_value());
|
||||||
box->setLayerAnimationDuration(kModerateMessagesBoxAnimationDuration);
|
box->setLayerAnimationDuration(kModerateMessagesBoxAnimationDuration);
|
||||||
|
|
||||||
|
const auto hasItems = !items.empty();
|
||||||
|
const auto hasReaction = reaction.has_value();
|
||||||
|
const auto itemsCount = hasItems ? int(items.size()) : 0;
|
||||||
|
|
||||||
using Controller = Ui::ExpandablePeerListController;
|
using Controller = Ui::ExpandablePeerListController;
|
||||||
|
|
||||||
const auto [allCanBan, allCanDelete, participants]
|
const auto moderateOptions = hasItems
|
||||||
= CalculateModerateOptions(items);
|
? CalculateModerateOptions(items)
|
||||||
|
: CalculateModerateOptions(*reaction);
|
||||||
|
const auto reportSpam = moderateOptions.reportSpam;
|
||||||
|
const auto deleteAllMessages = moderateOptions.deleteAllMessages;
|
||||||
|
const auto deleteAllReactions = moderateOptions.deleteAllReactions;
|
||||||
|
const auto banOrRestrict = moderateOptions.banOrRestrict;
|
||||||
|
const auto &participants = moderateOptions.participants;
|
||||||
const auto inner = box->verticalLayout();
|
const auto inner = box->verticalLayout();
|
||||||
|
|
||||||
Assert(!participants.empty());
|
Assert(!participants.empty());
|
||||||
@@ -374,18 +450,54 @@ void CreateModerateMessagesBox(
|
|||||||
: QMargins(
|
: QMargins(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
Ui::ParticipantsCheckView::ComputeSize(
|
Ui::ExpanderButton::ComputeSize(
|
||||||
participants.size()).width(),
|
ParticipantsExpanderText(int(participants.size()))).width(),
|
||||||
0);
|
0);
|
||||||
|
|
||||||
const auto itemsCount = int(items.size());
|
const auto firstItem = hasItems ? items.front().get() : nullptr;
|
||||||
const auto firstItem = items.front();
|
const auto session = hasItems
|
||||||
const auto history = firstItem->history();
|
? &firstItem->history()->session()
|
||||||
const auto session = &history->session();
|
: &reaction->peer->session();
|
||||||
const auto historyPeerId = history->peer->id;
|
const auto peer = hasItems
|
||||||
const auto ids = session->data().itemsToIds(items);
|
? firstItem->history()->peer
|
||||||
|
: reaction->peer;
|
||||||
|
const auto history = hasItems
|
||||||
|
? firstItem->history().get()
|
||||||
|
: session->data().historyLoaded(peer);
|
||||||
|
const auto historyPeerId = peer->id;
|
||||||
|
const auto ids = hasItems
|
||||||
|
? session->data().itemsToIds(items)
|
||||||
|
: MessageIdsList{ FullMsgId(reaction->peer->id, reaction->msgId) };
|
||||||
|
const auto selectedMessagesByParticipant = [&] {
|
||||||
|
auto result = base::flat_map<PeerId, int>();
|
||||||
|
if (!hasItems && !hasReaction) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (hasItems) {
|
||||||
|
for (const auto &item : items) {
|
||||||
|
const auto from = item->from();
|
||||||
|
if (!from) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto i = result.find(from->id);
|
||||||
|
if (i == result.end()) {
|
||||||
|
result.emplace(from->id, 1);
|
||||||
|
} else {
|
||||||
|
++i->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.emplace(reaction->participant->id, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}();
|
||||||
|
const auto participantIds = ranges::views::all(
|
||||||
|
participants
|
||||||
|
) | ranges::views::transform([](not_null<PeerData*> peer) {
|
||||||
|
return peer->id;
|
||||||
|
}) | ranges::to_vector;
|
||||||
|
|
||||||
{
|
if (hasItems) {
|
||||||
const auto remainingIds
|
const auto remainingIds
|
||||||
= box->lifetime().make_state<base::flat_set<FullMsgId>>(
|
= box->lifetime().make_state<base::flat_set<FullMsgId>>(
|
||||||
ids.begin(),
|
ids.begin(),
|
||||||
@@ -399,7 +511,8 @@ void CreateModerateMessagesBox(
|
|||||||
}, box->lifetime());
|
}, box->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModerateCommonGroups.value() || session->supportMode()) {
|
if (hasItems
|
||||||
|
&& (ModerateCommonGroups.value() || session->supportMode())) {
|
||||||
ProccessCommonGroups(
|
ProccessCommonGroups(
|
||||||
items,
|
items,
|
||||||
crl::guard(box, [=](CommonGroups groups, not_null<UserData*> user) {
|
crl::guard(box, [=](CommonGroups groups, not_null<UserData*> user) {
|
||||||
@@ -550,9 +663,10 @@ void CreateModerateMessagesBox(
|
|||||||
return base::EventFilterResult::Continue;
|
return base::EventFilterResult::Continue;
|
||||||
});
|
});
|
||||||
|
|
||||||
const auto handleSubmition = [=](not_null<Ui::Checkbox*> checkbox) {
|
const auto handleSubmitionIf = [=](Fn<bool()> enabled) {
|
||||||
base::install_event_filter(box, [=](not_null<QEvent*> event) {
|
base::install_event_filter(box, [=, enabled = std::move(enabled)](
|
||||||
if (!isEnter(event) || !checkbox->checked()) {
|
not_null<QEvent*> event) {
|
||||||
|
if (!isEnter(event) || !enabled()) {
|
||||||
return base::EventFilterResult::Continue;
|
return base::EventFilterResult::Continue;
|
||||||
}
|
}
|
||||||
box->uiShow()->show(Ui::MakeConfirmBox({
|
box->uiShow()->show(Ui::MakeConfirmBox({
|
||||||
@@ -567,21 +681,57 @@ void CreateModerateMessagesBox(
|
|||||||
return base::EventFilterResult::Cancel;
|
return base::EventFilterResult::Cancel;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const auto handleSubmition = [=](not_null<Ui::Checkbox*> checkbox) {
|
||||||
|
handleSubmitionIf([=] {
|
||||||
|
return checkbox->checked();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Ui::Checkbox *deleteOptions = nullptr;
|
||||||
|
Ui::Checkbox *deleteMessages = nullptr;
|
||||||
|
Controller *deleteMessagesController = nullptr;
|
||||||
|
rpl::variable<base::flat_map<PeerId, int>> *deleteMessagesCounts = nullptr;
|
||||||
|
Ui::Checkbox *deleteReactions = nullptr;
|
||||||
|
Controller *deleteReactionsController = nullptr;
|
||||||
|
const auto effectiveCheckedParticipants = [](
|
||||||
|
Ui::Checkbox *checkbox,
|
||||||
|
Controller *controller) {
|
||||||
|
if (!checkbox || !controller || !controller->collectRequests) {
|
||||||
|
return Participants();
|
||||||
|
} else if (!checkbox->checked()
|
||||||
|
&& (controller->data.participants.size() == 1)) {
|
||||||
|
return Participants();
|
||||||
|
}
|
||||||
|
return controller->collectRequests();
|
||||||
|
};
|
||||||
|
const auto checkedParticipantsValue = [=](
|
||||||
|
not_null<Ui::Checkbox*> checkbox,
|
||||||
|
not_null<Controller*> controller)
|
||||||
|
-> rpl::producer<Participants> {
|
||||||
|
if (controller->data.participants.size() == 1) {
|
||||||
|
return checkbox->checkedValue() | rpl::map([=](bool) {
|
||||||
|
return effectiveCheckedParticipants(checkbox, controller);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rpl::merge(
|
||||||
|
rpl::single(false),
|
||||||
|
controller->checkAllRequests.events(),
|
||||||
|
controller->toggleRequestsFromInner.events()
|
||||||
|
) | rpl::map([=](bool) {
|
||||||
|
return effectiveCheckedParticipants(checkbox, controller);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Ui::AddSkip(inner);
|
const auto subtitle = box->addRow(
|
||||||
const auto title = box->addRow(
|
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||||
object_ptr<Ui::FlatLabel>(
|
|
||||||
box,
|
box,
|
||||||
(itemsCount == 1)
|
object_ptr<Ui::FlatLabel>(
|
||||||
? tr::lng_selected_delete_sure_this()
|
box,
|
||||||
: tr::lng_selected_delete_sure(
|
QString(),
|
||||||
lt_count,
|
st::boxLabel)));
|
||||||
rpl::single(itemsCount) | tr::to_count()),
|
subtitle->entity()->setTextColorOverride(st::windowSubTextFg->c);
|
||||||
st::boxLabel));
|
subtitle->hide(anim::type::instant);
|
||||||
Ui::AddSkip(inner);
|
Ui::AddSkip(inner);
|
||||||
Ui::AddSkip(inner);
|
if (reportSpam) {
|
||||||
Ui::AddSkip(inner);
|
|
||||||
{
|
|
||||||
const auto report = box->addRow(
|
const auto report = box->addRow(
|
||||||
object_ptr<Ui::Checkbox>(
|
object_ptr<Ui::Checkbox>(
|
||||||
box,
|
box,
|
||||||
@@ -597,118 +747,504 @@ void CreateModerateMessagesBox(
|
|||||||
handleConfirmation(report, controller, [=](
|
handleConfirmation(report, controller, [=](
|
||||||
not_null<PeerData*> p,
|
not_null<PeerData*> p,
|
||||||
not_null<ChannelData*> c) {
|
not_null<ChannelData*> c) {
|
||||||
Api::ReportSpam(p, ids);
|
if (reaction.has_value()
|
||||||
|
&& Api::GetReactionReportCapabilities(
|
||||||
|
reaction->peer,
|
||||||
|
p
|
||||||
|
).canReport) {
|
||||||
|
Api::ReportReaction(
|
||||||
|
box->uiShow(),
|
||||||
|
reaction->peer,
|
||||||
|
reaction->msgId,
|
||||||
|
p);
|
||||||
|
} else {
|
||||||
|
Api::ReportSpam(p, ids);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allCanDelete) {
|
const auto showMessagesCheckbox = deleteAllMessages;
|
||||||
|
const auto showReactionsCheckbox = deleteAllReactions;
|
||||||
|
const auto useSingleDeleteOptions = isSingle
|
||||||
|
&& showMessagesCheckbox
|
||||||
|
&& showReactionsCheckbox;
|
||||||
|
if (showMessagesCheckbox || showReactionsCheckbox) {
|
||||||
Ui::AddSkip(inner);
|
Ui::AddSkip(inner);
|
||||||
Ui::AddSkip(inner);
|
Ui::AddSkip(inner);
|
||||||
|
const auto checkedParticipants = options.deleteAll
|
||||||
|
? participantIds
|
||||||
|
: std::vector<PeerId>();
|
||||||
|
|
||||||
const auto deleteAll = inner->add(
|
if (useSingleDeleteOptions) {
|
||||||
object_ptr<Ui::Checkbox>(
|
const auto participant = participants.front();
|
||||||
inner,
|
Assert(history != nullptr);
|
||||||
!(isSingle)
|
deleteMessagesCounts = box->lifetime().make_state<
|
||||||
? tr::lng_delete_all_from_users(
|
rpl::variable<base::flat_map<PeerId, int>>>(
|
||||||
tr::now,
|
base::flat_map<PeerId, int>());
|
||||||
tr::marked)
|
MessagesCountValue(
|
||||||
: tr::lng_delete_all_from_user(
|
history,
|
||||||
tr::now,
|
participants
|
||||||
|
) | rpl::on_next([=](base::flat_map<PeerId, int> counts) {
|
||||||
|
deleteMessagesCounts->force_assign(std::move(counts));
|
||||||
|
}, box->lifetime());
|
||||||
|
deleteMessagesController = box->lifetime().make_state<Controller>(
|
||||||
|
Controller::Data{
|
||||||
|
.messagesCounts = deleteMessagesCounts->value(),
|
||||||
|
.participants = Participants{ participant },
|
||||||
|
.checked = checkedParticipants,
|
||||||
|
});
|
||||||
|
deleteReactionsController = box->lifetime().make_state<Controller>(
|
||||||
|
Controller::Data{
|
||||||
|
.participants = Participants{ participant },
|
||||||
|
.checked = checkedParticipants,
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto deleteOptionsSize = Ui::ExpanderButton::ComputeSize(
|
||||||
|
DeleteOptionsExpanderText(2, 2));
|
||||||
|
const auto deleteOptionsPadding = QMargins(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
deleteOptionsSize.width(),
|
||||||
|
0);
|
||||||
|
deleteOptions = inner->add(
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
inner,
|
||||||
|
tr::lng_delete_all_from_user(
|
||||||
lt_user,
|
lt_user,
|
||||||
tr::bold(firstItem->from()->name()),
|
rpl::single(participant->shortName())),
|
||||||
tr::marked),
|
options.deleteAll,
|
||||||
options.deleteAll,
|
st::defaultBoxCheckbox),
|
||||||
st::defaultBoxCheckbox),
|
st::boxRowPadding + deleteOptionsPadding);
|
||||||
st::boxRowPadding + buttonPadding);
|
const auto button = Ui::CreateChild<Ui::ExpanderButton>(
|
||||||
auto messagesCounts = MessagesCountValue(history, participants);
|
inner,
|
||||||
|
DeleteOptionsExpanderText(2, 2));
|
||||||
|
button->resize(deleteOptionsSize);
|
||||||
|
deleteOptions->geometryValue(
|
||||||
|
) | rpl::on_next([=](const QRect &rect) {
|
||||||
|
button->moveToRight(
|
||||||
|
st::moderateBoxExpandRight,
|
||||||
|
rect.top() + (rect.height() - button->height()) / 2,
|
||||||
|
inner->width());
|
||||||
|
button->raise();
|
||||||
|
}, button->lifetime());
|
||||||
|
|
||||||
const auto controller = box->lifetime().make_state<Controller>(
|
const auto wrap = inner->add(
|
||||||
Controller::Data{
|
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||||
.messagesCounts = rpl::duplicate(messagesCounts),
|
inner,
|
||||||
.participants = participants,
|
object_ptr<Ui::VerticalLayout>(inner)));
|
||||||
|
wrap->toggle(false, anim::type::instant);
|
||||||
|
button->setClickedCallback([=] {
|
||||||
|
button->checkView()->setChecked(
|
||||||
|
!button->checkView()->checked(),
|
||||||
|
anim::type::normal);
|
||||||
|
wrap->toggle(
|
||||||
|
button->checkView()->checked(),
|
||||||
|
anim::type::normal);
|
||||||
});
|
});
|
||||||
Ui::AddExpandablePeerList(deleteAll, controller, inner);
|
|
||||||
{
|
|
||||||
auto itemFromIds = items | ranges::views::transform([](
|
|
||||||
const auto &item) {
|
|
||||||
return item->from()->id;
|
|
||||||
}) | ranges::to_vector;
|
|
||||||
|
|
||||||
rpl::combine(
|
const auto container = wrap->entity();
|
||||||
std::move(messagesCounts),
|
const auto optionCheckRect = deleteOptions->checkRect();
|
||||||
isSingle
|
const auto childOptionPadding = st::boxRowPadding
|
||||||
? deleteAll->checkedValue()
|
+ QMargins(
|
||||||
: rpl::merge(
|
optionCheckRect.width()
|
||||||
controller->toggleRequestsFromInner.events(),
|
+ st::defaultBoxCheckbox.textPosition.x()
|
||||||
controller->checkAllRequests.events())
|
- optionCheckRect.x(),
|
||||||
) | rpl::map([=](const auto &map, bool c) {
|
0,
|
||||||
const auto checked = (isSingle && !c)
|
0,
|
||||||
? Participants()
|
0);
|
||||||
: controller->collectRequests
|
Ui::AddSkip(container);
|
||||||
? controller->collectRequests()
|
Ui::AddSkip(container);
|
||||||
|
deleteMessages = container->add(
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
container,
|
||||||
|
tr::lng_delete_sub_messages(tr::now),
|
||||||
|
options.deleteAll,
|
||||||
|
st::defaultBoxCheckbox),
|
||||||
|
childOptionPadding);
|
||||||
|
Ui::AddSkip(container);
|
||||||
|
Ui::AddSkip(container);
|
||||||
|
deleteReactions = container->add(
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
container,
|
||||||
|
tr::lng_delete_sub_reactions(tr::now),
|
||||||
|
options.deleteAll,
|
||||||
|
st::defaultBoxCheckbox),
|
||||||
|
childOptionPadding);
|
||||||
|
deleteMessagesController->collectRequests = [=] {
|
||||||
|
return deleteMessages->checked()
|
||||||
|
? Participants{ participant }
|
||||||
: Participants();
|
: Participants();
|
||||||
auto result = 0;
|
};
|
||||||
for (const auto &[peerId, count] : map) {
|
deleteReactionsController->collectRequests = [=] {
|
||||||
for (const auto &peer : checked) {
|
return deleteReactions->checked()
|
||||||
if (peer->id == peerId) {
|
? Participants{ participant }
|
||||||
result += count;
|
: Participants();
|
||||||
break;
|
};
|
||||||
}
|
const auto updateDeleteOptions = [=] {
|
||||||
|
const auto count = (deleteMessages->checked() ? 1 : 0)
|
||||||
|
+ (deleteReactions->checked() ? 1 : 0);
|
||||||
|
deleteOptions->setChecked(
|
||||||
|
count == 2,
|
||||||
|
Ui::Checkbox::NotifyAboutChange::DontNotify);
|
||||||
|
button->setText(DeleteOptionsExpanderText(count, 2));
|
||||||
|
};
|
||||||
|
deleteOptions->checkedChanges(
|
||||||
|
) | rpl::on_next([=](bool checked) {
|
||||||
|
deleteMessages->setChecked(checked);
|
||||||
|
deleteReactions->setChecked(checked);
|
||||||
|
updateDeleteOptions();
|
||||||
|
}, deleteOptions->lifetime());
|
||||||
|
deleteMessages->checkedChanges(
|
||||||
|
) | rpl::on_next(updateDeleteOptions, deleteMessages->lifetime());
|
||||||
|
deleteReactions->checkedChanges(
|
||||||
|
) | rpl::on_next(updateDeleteOptions, deleteReactions->lifetime());
|
||||||
|
updateDeleteOptions();
|
||||||
|
handleSubmitionIf([=] {
|
||||||
|
return deleteMessages->checked()
|
||||||
|
|| deleteReactions->checked();
|
||||||
|
});
|
||||||
|
handleConfirmation(
|
||||||
|
not_null{ deleteMessages },
|
||||||
|
not_null{ deleteMessagesController },
|
||||||
|
[=](
|
||||||
|
not_null<PeerData*> p,
|
||||||
|
not_null<ChannelData*> c) {
|
||||||
|
p->session().api().deleteAllFromParticipant(c, p);
|
||||||
|
});
|
||||||
|
confirms->events() | rpl::on_next([=] {
|
||||||
|
if (deleteReactions->checked()
|
||||||
|
&& deleteReactionsController->collectRequests
|
||||||
|
&& !effectiveCheckedParticipants(
|
||||||
|
deleteReactions,
|
||||||
|
deleteReactionsController).empty()) {
|
||||||
|
for (const auto &participant
|
||||||
|
: deleteReactionsController->collectRequests()) {
|
||||||
|
const auto useOriginReaction = reaction
|
||||||
|
&& (participant == reaction->participant);
|
||||||
|
const auto originMsgId = useOriginReaction
|
||||||
|
? reaction->msgId
|
||||||
|
: MsgId();
|
||||||
|
const auto originReaction = useOriginReaction
|
||||||
|
? reaction->reaction
|
||||||
|
: Data::ReactionId();
|
||||||
|
peer->session().api().deleteAllReactionsFromParticipant(
|
||||||
|
peer,
|
||||||
|
participant,
|
||||||
|
originMsgId,
|
||||||
|
originReaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const auto &fromId : itemFromIds) {
|
}, deleteReactions->lifetime());
|
||||||
for (const auto &peer : checked) {
|
} else {
|
||||||
if (peer->id == fromId) {
|
if (showMessagesCheckbox) {
|
||||||
result--;
|
Assert(history != nullptr);
|
||||||
break;
|
deleteMessagesCounts = box->lifetime().make_state<
|
||||||
}
|
rpl::variable<base::flat_map<PeerId, int>>>(
|
||||||
}
|
base::flat_map<PeerId, int>());
|
||||||
result++;
|
MessagesCountValue(
|
||||||
}
|
history,
|
||||||
return float64(result);
|
participants
|
||||||
}) | rpl::on_next([=](int amount) {
|
) | rpl::on_next([=](base::flat_map<PeerId, int> counts) {
|
||||||
auto text = tr::lng_selected_delete_sure(
|
deleteMessagesCounts->force_assign(std::move(counts));
|
||||||
tr::now,
|
}, box->lifetime());
|
||||||
lt_count,
|
deleteMessagesController = box->lifetime().make_state<Controller>(
|
||||||
float64(amount));
|
Controller::Data{
|
||||||
if (amount > 0) {
|
.messagesCounts = deleteMessagesCounts->value(),
|
||||||
title->setText(std::move(text));
|
.participants = participants,
|
||||||
} else {
|
.checked = checkedParticipants,
|
||||||
const auto zeroIndex = text.indexOf('0');
|
});
|
||||||
if (zeroIndex != -1) {
|
deleteMessages = inner->add(
|
||||||
auto descriptor = Lottie::IconDescriptor{
|
object_ptr<Ui::Checkbox>(
|
||||||
.name = u"transcribe_loading"_q,
|
inner,
|
||||||
.color = &st::attentionButtonFg, // Any contrast.
|
tr::lng_delete_sub_messages(tr::now),
|
||||||
.sizeOverride = Size(
|
options.deleteAll,
|
||||||
st::historyTranscribeLoadingSize),
|
st::defaultBoxCheckbox),
|
||||||
.colorizeUsingAlpha = true,
|
st::boxRowPadding + buttonPadding);
|
||||||
};
|
Ui::AddExpandablePeerList(
|
||||||
auto result = TextWithEntities()
|
not_null{ deleteMessages },
|
||||||
.append(text.mid(0, zeroIndex))
|
not_null{ deleteMessagesController },
|
||||||
.append(Ui::Text::LottieEmoji(descriptor))
|
inner);
|
||||||
.append(text.mid(zeroIndex + 1));
|
handleSubmition(not_null{ deleteMessages });
|
||||||
using namespace Ui::Text;
|
handleConfirmation(
|
||||||
title->setMarkedText(
|
not_null{ deleteMessages },
|
||||||
std::move(result),
|
not_null{ deleteMessagesController },
|
||||||
LottieEmojiContext(std::move(descriptor)));
|
[=](
|
||||||
} else {
|
not_null<PeerData*> p,
|
||||||
title->setText(std::move(text));
|
not_null<ChannelData*> c) {
|
||||||
}
|
p->session().api().deleteAllFromParticipant(c, p);
|
||||||
}
|
});
|
||||||
title->resizeToWidth(inner->width()
|
}
|
||||||
- rect::m::sum::h(st::boxRowPadding));
|
|
||||||
}, title->lifetime());
|
|
||||||
}
|
|
||||||
handleSubmition(deleteAll);
|
|
||||||
|
|
||||||
handleConfirmation(deleteAll, controller, [=](
|
if (deleteMessages && showReactionsCheckbox) {
|
||||||
not_null<PeerData*> p,
|
Ui::AddSkip(inner);
|
||||||
not_null<ChannelData*> c) {
|
Ui::AddSkip(inner);
|
||||||
p->session().api().deleteAllFromParticipant(c, p);
|
}
|
||||||
});
|
|
||||||
|
if (showReactionsCheckbox) {
|
||||||
|
deleteReactionsController = box->lifetime().make_state<Controller>(
|
||||||
|
Controller::Data{
|
||||||
|
.participants = participants,
|
||||||
|
.checked = checkedParticipants,
|
||||||
|
});
|
||||||
|
deleteReactions = inner->add(
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
inner,
|
||||||
|
tr::lng_delete_sub_reactions(tr::now),
|
||||||
|
options.deleteAll,
|
||||||
|
st::defaultBoxCheckbox),
|
||||||
|
st::boxRowPadding + buttonPadding);
|
||||||
|
Ui::AddExpandablePeerList(
|
||||||
|
not_null{ deleteReactions },
|
||||||
|
not_null{ deleteReactionsController },
|
||||||
|
inner);
|
||||||
|
handleSubmition(not_null{ deleteReactions });
|
||||||
|
confirms->events() | rpl::on_next([=] {
|
||||||
|
if (deleteReactions->checked()
|
||||||
|
&& deleteReactionsController->collectRequests
|
||||||
|
&& !effectiveCheckedParticipants(
|
||||||
|
deleteReactions,
|
||||||
|
deleteReactionsController).empty()) {
|
||||||
|
for (const auto &participant
|
||||||
|
: deleteReactionsController->collectRequests()) {
|
||||||
|
const auto useOriginReaction = reaction
|
||||||
|
&& (participant == reaction->participant);
|
||||||
|
peer->session().api()
|
||||||
|
.deleteAllReactionsFromParticipant(
|
||||||
|
peer,
|
||||||
|
participant,
|
||||||
|
useOriginReaction ? reaction->msgId : MsgId(),
|
||||||
|
useOriginReaction
|
||||||
|
? reaction->reaction
|
||||||
|
: Data::ReactionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deleteReactions->lifetime());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (allCanBan) {
|
const auto makeTitleLoadingDescriptor = [] {
|
||||||
const auto peer = items.front()->history()->peer;
|
return Lottie::IconDescriptor{
|
||||||
|
.name = u"transcribe_loading"_q,
|
||||||
|
.color = &st::attentionButtonFg,
|
||||||
|
.sizeOverride = Size(st::historyTranscribeLoadingSize),
|
||||||
|
.colorizeUsingAlpha = true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const auto titleLoadingEmojiData = Ui::Text::LottieEmojiData(
|
||||||
|
makeTitleLoadingDescriptor());
|
||||||
|
struct MessageTitleData final {
|
||||||
|
int count = 0;
|
||||||
|
bool resolved = false;
|
||||||
|
};
|
||||||
|
const auto baseMessagesCount = int(ids.size());
|
||||||
|
const auto langUpdated = rpl::single(
|
||||||
|
0
|
||||||
|
) | rpl::then(Lang::Updated() | rpl::map([] {
|
||||||
|
return 0;
|
||||||
|
}));
|
||||||
|
const auto makeMessageTitleData = [=](
|
||||||
|
const base::flat_map<PeerId, int> &messagesCounts,
|
||||||
|
const Participants &checked) {
|
||||||
|
auto result = MessageTitleData{
|
||||||
|
.count = baseMessagesCount,
|
||||||
|
.resolved = true,
|
||||||
|
};
|
||||||
|
for (const auto &peer : checked) {
|
||||||
|
const auto i = messagesCounts.find(peer->id);
|
||||||
|
if (i == end(messagesCounts)) {
|
||||||
|
result.resolved = false;
|
||||||
|
} else {
|
||||||
|
result.count += i->second;
|
||||||
|
}
|
||||||
|
if (const auto j = selectedMessagesByParticipant.find(peer->id);
|
||||||
|
j != end(selectedMessagesByParticipant)) {
|
||||||
|
result.count -= j->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
auto title = [&]() -> rpl::producer<TextWithEntities> {
|
||||||
|
if (showMessagesCheckbox && !(hasReaction && !hasItems)) {
|
||||||
|
auto messageTitleData = rpl::combine(
|
||||||
|
deleteMessagesCounts->value(),
|
||||||
|
checkedParticipantsValue(
|
||||||
|
not_null{ deleteMessages },
|
||||||
|
not_null{ deleteMessagesController })
|
||||||
|
) | rpl::map(makeMessageTitleData);
|
||||||
|
return rpl::combine(
|
||||||
|
std::move(messageTitleData),
|
||||||
|
rpl::duplicate(langUpdated)
|
||||||
|
) | rpl::map([=](const MessageTitleData &data, int) {
|
||||||
|
const auto count = data.count;
|
||||||
|
const auto resolved = data.resolved;
|
||||||
|
const auto text = (count == 1)
|
||||||
|
? tr::lng_delete_title_message_one(tr::now)
|
||||||
|
: tr::lng_delete_title_message_many(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
count);
|
||||||
|
if (resolved || count != 0) {
|
||||||
|
return TextWithEntities{ text };
|
||||||
|
}
|
||||||
|
const auto zeroIndex = text.indexOf('0');
|
||||||
|
return (zeroIndex == -1)
|
||||||
|
? TextWithEntities{ text }
|
||||||
|
: TextWithEntities()
|
||||||
|
.append(text.mid(0, zeroIndex))
|
||||||
|
.append(Ui::Text::LottieEmoji(
|
||||||
|
makeTitleLoadingDescriptor()))
|
||||||
|
.append(text.mid(zeroIndex + 1));
|
||||||
|
});
|
||||||
|
} else if (hasReaction && showMessagesCheckbox) {
|
||||||
|
auto messageTitleData = rpl::combine(
|
||||||
|
deleteMessagesCounts->value(),
|
||||||
|
checkedParticipantsValue(
|
||||||
|
not_null{ deleteMessages },
|
||||||
|
not_null{ deleteMessagesController })
|
||||||
|
) | rpl::map(makeMessageTitleData);
|
||||||
|
auto deleteReactionsChecked = deleteReactions
|
||||||
|
? deleteReactions->checkedValue()
|
||||||
|
: rpl::single(false);
|
||||||
|
return rpl::combine(
|
||||||
|
deleteMessages->checkedValue(),
|
||||||
|
std::move(messageTitleData),
|
||||||
|
std::move(deleteReactionsChecked),
|
||||||
|
rpl::duplicate(langUpdated)
|
||||||
|
) | rpl::map([=](
|
||||||
|
bool deleteMessagesChecked,
|
||||||
|
const MessageTitleData &data,
|
||||||
|
bool deleteReactionsChecked,
|
||||||
|
int) {
|
||||||
|
if (!deleteMessagesChecked) {
|
||||||
|
return TextWithEntities{ deleteReactionsChecked
|
||||||
|
? tr::lng_delete_title_reaction_all(tr::now)
|
||||||
|
: tr::lng_delete_title_reaction_this(tr::now) };
|
||||||
|
}
|
||||||
|
const auto count = data.count;
|
||||||
|
const auto resolved = data.resolved;
|
||||||
|
const auto text = (count == 1)
|
||||||
|
? tr::lng_delete_title_message_one(tr::now)
|
||||||
|
: tr::lng_delete_title_message_many(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
count);
|
||||||
|
if (resolved || count != 0) {
|
||||||
|
return TextWithEntities{ text };
|
||||||
|
}
|
||||||
|
const auto zeroIndex = text.indexOf('0');
|
||||||
|
return (zeroIndex == -1)
|
||||||
|
? TextWithEntities{ text }
|
||||||
|
: TextWithEntities()
|
||||||
|
.append(text.mid(0, zeroIndex))
|
||||||
|
.append(Ui::Text::LottieEmoji(
|
||||||
|
makeTitleLoadingDescriptor()))
|
||||||
|
.append(text.mid(zeroIndex + 1));
|
||||||
|
});
|
||||||
|
} else if (hasItems) {
|
||||||
|
return rpl::duplicate(langUpdated) | rpl::map([=](int) {
|
||||||
|
return (itemsCount == 1)
|
||||||
|
? TextWithEntities{
|
||||||
|
tr::lng_delete_title_message_one(tr::now)
|
||||||
|
}
|
||||||
|
: TextWithEntities{
|
||||||
|
tr::lng_delete_title_message_many(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
itemsCount)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (deleteReactions) {
|
||||||
|
return rpl::combine(
|
||||||
|
deleteReactions->checkedValue(),
|
||||||
|
rpl::duplicate(langUpdated)
|
||||||
|
) | rpl::map([=](bool checked, int) {
|
||||||
|
return TextWithEntities{ checked
|
||||||
|
? tr::lng_delete_title_reaction_all(tr::now)
|
||||||
|
: tr::lng_delete_title_reaction_this(tr::now) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rpl::duplicate(langUpdated) | rpl::map([](int) {
|
||||||
|
return TextWithEntities{
|
||||||
|
tr::lng_delete_title_reaction_this(tr::now)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}();
|
||||||
|
auto titleContext = Core::TextContext({ .session = session });
|
||||||
|
auto titleFactory = std::move(titleContext.customEmojiFactory);
|
||||||
|
titleContext.customEmojiFactory = [
|
||||||
|
titleFactory = std::move(titleFactory),
|
||||||
|
titleLoadingEmojiData,
|
||||||
|
makeTitleLoadingDescriptor
|
||||||
|
](QStringView data, const Ui::Text::MarkedContext &context)
|
||||||
|
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
||||||
|
if (data == titleLoadingEmojiData) {
|
||||||
|
return std::make_unique<Ui::Text::LottieCustomEmoji>(
|
||||||
|
makeTitleLoadingDescriptor(),
|
||||||
|
context.repaint);
|
||||||
|
}
|
||||||
|
return titleFactory(data, context);
|
||||||
|
};
|
||||||
|
box->getDelegate()->setTitle(std::move(title), std::move(titleContext));
|
||||||
|
enum class SubtitleKind {
|
||||||
|
None,
|
||||||
|
ThisReaction,
|
||||||
|
SomeReactions,
|
||||||
|
AllReactions,
|
||||||
|
};
|
||||||
|
if (hasItems || (hasReaction && showMessagesCheckbox)) {
|
||||||
|
const auto subtitleKind = box->lifetime().make_state<
|
||||||
|
rpl::variable<SubtitleKind>>(SubtitleKind::None);
|
||||||
|
auto reactionsCheckedValue = showReactionsCheckbox
|
||||||
|
? checkedParticipantsValue(
|
||||||
|
not_null{ deleteReactions },
|
||||||
|
not_null{ deleteReactionsController })
|
||||||
|
: rpl::single(Participants());
|
||||||
|
auto messageTitleShownValue = [&] {
|
||||||
|
return hasItems
|
||||||
|
? rpl::single(true)
|
||||||
|
: (hasReaction && showMessagesCheckbox)
|
||||||
|
? deleteMessages->checkedValue()
|
||||||
|
: rpl::single(false);
|
||||||
|
}();
|
||||||
|
rpl::combine(
|
||||||
|
subtitleKind->value(),
|
||||||
|
rpl::duplicate(langUpdated)
|
||||||
|
) | rpl::map([](SubtitleKind kind, int) {
|
||||||
|
switch (kind) {
|
||||||
|
case SubtitleKind::ThisReaction:
|
||||||
|
return tr::lng_delete_label_also_this_reaction(tr::now);
|
||||||
|
case SubtitleKind::SomeReactions:
|
||||||
|
return tr::lng_delete_label_also_some_reactions(tr::now);
|
||||||
|
case SubtitleKind::AllReactions:
|
||||||
|
return tr::lng_delete_label_also_all_reactions(tr::now);
|
||||||
|
case SubtitleKind::None:
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
Unexpected("Bad subtitle kind.");
|
||||||
|
}) | rpl::on_next([=](const QString &text) {
|
||||||
|
subtitle->entity()->setText(text);
|
||||||
|
}, subtitle->lifetime());
|
||||||
|
rpl::combine(
|
||||||
|
std::move(reactionsCheckedValue),
|
||||||
|
std::move(messageTitleShownValue)
|
||||||
|
) | rpl::on_next([=](
|
||||||
|
const Participants &checked,
|
||||||
|
bool messageTitleShown) {
|
||||||
|
auto kind = SubtitleKind::None;
|
||||||
|
if (messageTitleShown) {
|
||||||
|
if (!checked.empty()) {
|
||||||
|
kind = (checked.size() == participants.size())
|
||||||
|
? SubtitleKind::AllReactions
|
||||||
|
: SubtitleKind::SomeReactions;
|
||||||
|
} else if (hasReaction) {
|
||||||
|
kind = SubtitleKind::ThisReaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subtitleKind->force_assign(kind);
|
||||||
|
subtitle->toggle(kind != SubtitleKind::None, anim::type::normal);
|
||||||
|
}, subtitle->lifetime());
|
||||||
|
}
|
||||||
|
if (banOrRestrict) {
|
||||||
auto ownedWrap = peer->isMonoforum()
|
auto ownedWrap = peer->isMonoforum()
|
||||||
? nullptr
|
? nullptr
|
||||||
: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
: object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||||
@@ -731,7 +1267,9 @@ void CreateModerateMessagesBox(
|
|||||||
rpl::single(participants.size()) | tr::to_count()),
|
rpl::single(participants.size()) | tr::to_count()),
|
||||||
rpl::conditional(
|
rpl::conditional(
|
||||||
rpl::single(isSingle),
|
rpl::single(isSingle),
|
||||||
tr::lng_ban_user(),
|
tr::lng_ban_specific_user(
|
||||||
|
lt_user,
|
||||||
|
rpl::single(participants.front()->shortName())),
|
||||||
tr::lng_ban_users())),
|
tr::lng_ban_users())),
|
||||||
options.banUser,
|
options.banUser,
|
||||||
st::defaultBoxCheckbox),
|
st::defaultBoxCheckbox),
|
||||||
@@ -787,12 +1325,12 @@ void CreateModerateMessagesBox(
|
|||||||
wrap->toggledValue(
|
wrap->toggledValue(
|
||||||
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
|
) | rpl::map([isSingle, emojiUp, emojiDown](bool toggled) {
|
||||||
return ((toggled && isSingle)
|
return ((toggled && isSingle)
|
||||||
? tr::lng_restrict_user_part
|
|
||||||
: (toggled && !isSingle)
|
|
||||||
? tr::lng_restrict_users_part
|
|
||||||
: isSingle
|
|
||||||
? tr::lng_restrict_user_full
|
? tr::lng_restrict_user_full
|
||||||
: tr::lng_restrict_users_full)(
|
: (toggled && !isSingle)
|
||||||
|
? tr::lng_restrict_users_full
|
||||||
|
: isSingle
|
||||||
|
? tr::lng_restrict_user_part
|
||||||
|
: tr::lng_restrict_users_part)(
|
||||||
lt_emoji,
|
lt_emoji,
|
||||||
rpl::single(toggled ? emojiUp : emojiDown),
|
rpl::single(toggled ? emojiUp : emojiDown),
|
||||||
tr::marked);
|
tr::marked);
|
||||||
@@ -905,25 +1443,36 @@ void CreateModerateMessagesBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
const auto close = crl::guard(box, [=] { box->closeBox(); });
|
||||||
{
|
box->addButton(tr::lng_box_delete(), [=] {
|
||||||
const auto data = &participants.front()->session().data();
|
confirms->fire({});
|
||||||
const auto ids = data->itemsToIds(items);
|
if (confirmed) {
|
||||||
box->addButton(tr::lng_box_delete(), [=] {
|
confirmed();
|
||||||
confirms->fire({});
|
}
|
||||||
if (confirmed) {
|
if (hasItems) {
|
||||||
confirmed();
|
session->data().histories().deleteMessages(ids, true);
|
||||||
}
|
session->data().sendHistoryChangeNotifications();
|
||||||
data->histories().deleteMessages(ids, true);
|
}
|
||||||
data->sendHistoryChangeNotifications();
|
const auto deleteThisReaction = reaction
|
||||||
close();
|
&& !ranges::contains(
|
||||||
});
|
effectiveCheckedParticipants(
|
||||||
}
|
deleteReactions,
|
||||||
|
deleteReactionsController),
|
||||||
|
reaction->participant);
|
||||||
|
if (deleteThisReaction) {
|
||||||
|
session->api().deleteParticipantReaction(
|
||||||
|
reaction->peer,
|
||||||
|
reaction->msgId,
|
||||||
|
reaction->participant,
|
||||||
|
reaction->reaction);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
});
|
||||||
box->addButton(tr::lng_cancel(), close);
|
box->addButton(tr::lng_cancel(), close);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CanCreateModerateMessagesBox(const HistoryItemsList &items) {
|
bool CanCreateModerateMessagesBox(const HistoryItemsList &items) {
|
||||||
const auto options = CalculateModerateOptions(items);
|
const auto options = CalculateModerateOptions(items);
|
||||||
return (options.allCanBan || options.allCanDelete)
|
return HasModerateActions(options)
|
||||||
&& !options.participants.empty();
|
&& !options.participants.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "data/data_message_reaction_id.h"
|
||||||
|
|
||||||
class PeerData;
|
class PeerData;
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
@@ -25,11 +27,23 @@ struct ModerateMessagesBoxOptions final {
|
|||||||
bool banUser = false;
|
bool banUser = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ModerateReactionEntry {
|
||||||
|
not_null<PeerData*> peer;
|
||||||
|
MsgId msgId;
|
||||||
|
not_null<PeerData*> participant;
|
||||||
|
Data::ReactionId reaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModerateMessagesBoxEntry {
|
||||||
|
HistoryItemsList items;
|
||||||
|
std::optional<ModerateReactionEntry> reaction;
|
||||||
|
};
|
||||||
|
|
||||||
[[nodiscard]] ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions();
|
[[nodiscard]] ModerateMessagesBoxOptions DefaultModerateMessagesBoxOptions();
|
||||||
|
|
||||||
void CreateModerateMessagesBox(
|
void CreateModerateMessagesBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
const HistoryItemsList &items,
|
ModerateMessagesBoxEntry entry,
|
||||||
Fn<void()> confirmed,
|
Fn<void()> confirmed,
|
||||||
ModerateMessagesBoxOptions options);
|
ModerateMessagesBoxOptions options);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/data_chat.h"
|
#include "data/data_chat.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
|
#include "data/stickers/data_custom_emoji.h"
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
#include "styles/style_boxes.h"
|
#include "styles/style_boxes.h"
|
||||||
@@ -282,20 +283,21 @@ void PeerListBox::setInnerFocus() {
|
|||||||
void PeerListBox::peerListSetRowChecked(
|
void PeerListBox::peerListSetRowChecked(
|
||||||
not_null<PeerListRow*> row,
|
not_null<PeerListRow*> row,
|
||||||
bool checked) {
|
bool checked) {
|
||||||
|
const auto trackSelected = _controller->trackSelectedList();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (_controller->trackSelectedList()) {
|
if (trackSelected) {
|
||||||
addSelectItem(row, anim::type::normal);
|
addSelectItem(row, anim::type::normal);
|
||||||
}
|
}
|
||||||
PeerListContentDelegate::peerListSetRowChecked(row, checked);
|
PeerListContentDelegate::peerListSetRowChecked(row, checked);
|
||||||
peerListUpdateRow(row);
|
peerListUpdateRow(row);
|
||||||
|
|
||||||
// This call deletes row from _searchRows.
|
// This call deletes row from _searchRows.
|
||||||
if (_select) {
|
if (_select && trackSelected) {
|
||||||
_select->entity()->clearQuery();
|
_select->entity()->clearQuery();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The itemRemovedCallback will call changeCheckState() here.
|
// The itemRemovedCallback will call changeCheckState() here.
|
||||||
if (_select) {
|
if (_select && trackSelected) {
|
||||||
_select->entity()->removeItem(row->id());
|
_select->entity()->removeItem(row->id());
|
||||||
} else {
|
} else {
|
||||||
PeerListContentDelegate::peerListSetRowChecked(row, checked);
|
PeerListContentDelegate::peerListSetRowChecked(row, checked);
|
||||||
@@ -841,6 +843,42 @@ int PeerListRow::paintNameIconGetWidth(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PeerListRow::paintNameIconGetLeadingWidth(
|
||||||
|
Painter &p,
|
||||||
|
Fn<void()> repaint,
|
||||||
|
crl::time now,
|
||||||
|
int nameLeft,
|
||||||
|
int nameTop,
|
||||||
|
int outerWidth,
|
||||||
|
bool selected) {
|
||||||
|
if (_skipPeerBadge
|
||||||
|
|| special()
|
||||||
|
|| !_savedMessagesStatus.isEmpty()
|
||||||
|
|| _isRepliesMessagesChat
|
||||||
|
|| _isVerifyCodesChat) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const auto info = peer()->botVerifyDetails();
|
||||||
|
if (!info) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!_badge.ready(info)) {
|
||||||
|
_badge.set(
|
||||||
|
info,
|
||||||
|
peer()->owner().customEmojiManager().factory(
|
||||||
|
Data::CustomEmojiSizeTag::Isolated),
|
||||||
|
std::move(repaint));
|
||||||
|
}
|
||||||
|
const auto &st = selected
|
||||||
|
? st::dialogsVerifiedColorsOver
|
||||||
|
: st::dialogsVerifiedColors;
|
||||||
|
const auto skip = _badge.drawVerified(
|
||||||
|
p,
|
||||||
|
QPoint(nameLeft, nameTop),
|
||||||
|
st);
|
||||||
|
return skip;// ? skip + st::dialogsChatTypeSkip) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
void PeerListRow::paintStatusText(
|
void PeerListRow::paintStatusText(
|
||||||
Painter &p,
|
Painter &p,
|
||||||
const style::PeerListItem &st,
|
const style::PeerListItem &st,
|
||||||
@@ -1892,19 +1930,28 @@ crl::time PeerListContent::paintRow(
|
|||||||
+ rightActionMargins.right()
|
+ rightActionMargins.right()
|
||||||
- skipRight;
|
- skipRight;
|
||||||
}
|
}
|
||||||
namew -= row->paintNameIconGetWidth(
|
const auto leading = row->paintNameIconGetLeadingWidth(
|
||||||
p,
|
p,
|
||||||
[=] { updateRow(row); },
|
[=] { updateRow(row); },
|
||||||
now,
|
now,
|
||||||
namex,
|
namex,
|
||||||
namey,
|
namey,
|
||||||
|
width(),
|
||||||
|
selected);
|
||||||
|
namew -= leading;
|
||||||
|
namew -= row->paintNameIconGetWidth(
|
||||||
|
p,
|
||||||
|
[=] { updateRow(row); },
|
||||||
|
now,
|
||||||
|
namex + leading,
|
||||||
|
namey,
|
||||||
name.maxWidth(),
|
name.maxWidth(),
|
||||||
namew,
|
namew,
|
||||||
width(),
|
width(),
|
||||||
selected);
|
selected);
|
||||||
auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();
|
auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();
|
||||||
p.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));
|
p.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));
|
||||||
name.drawLeftElided(p, namex, namey, namew, width());
|
name.drawLeftElided(p, namex + leading, namey, namew, width());
|
||||||
|
|
||||||
p.setFont(st::contactsStatusFont);
|
p.setFont(st::contactsStatusFont);
|
||||||
if (row->isSearchResult()
|
if (row->isSearchResult()
|
||||||
|
|||||||
@@ -127,6 +127,15 @@ public:
|
|||||||
int outerWidth,
|
int outerWidth,
|
||||||
bool selected);
|
bool selected);
|
||||||
|
|
||||||
|
virtual int paintNameIconGetLeadingWidth(
|
||||||
|
Painter &p,
|
||||||
|
Fn<void()> repaint,
|
||||||
|
crl::time now,
|
||||||
|
int nameLeft,
|
||||||
|
int nameTop,
|
||||||
|
int outerWidth,
|
||||||
|
bool selected);
|
||||||
|
|
||||||
virtual QSize rightActionSize() const {
|
virtual QSize rightActionSize() const {
|
||||||
return QSize();
|
return QSize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,19 +91,28 @@ crl::time PeerListWidgets::paintRow(
|
|||||||
+ rightActionMargins.right()
|
+ rightActionMargins.right()
|
||||||
- skipRight;
|
- skipRight;
|
||||||
}
|
}
|
||||||
namew -= row->paintNameIconGetWidth(
|
const auto leading = row->paintNameIconGetLeadingWidth(
|
||||||
p,
|
p,
|
||||||
[=] { updateRow(row); },
|
[=] { updateRow(row); },
|
||||||
now,
|
now,
|
||||||
namex,
|
namex,
|
||||||
namey,
|
namey,
|
||||||
|
w,
|
||||||
|
selected);
|
||||||
|
namew -= leading;
|
||||||
|
namew -= row->paintNameIconGetWidth(
|
||||||
|
p,
|
||||||
|
[=] { updateRow(row); },
|
||||||
|
now,
|
||||||
|
namex + leading,
|
||||||
|
namey,
|
||||||
name.maxWidth(),
|
name.maxWidth(),
|
||||||
namew,
|
namew,
|
||||||
w,
|
w,
|
||||||
selected);
|
selected);
|
||||||
auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();
|
auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio();
|
||||||
p.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));
|
p.setPen(anim::pen(st.nameFg, st.nameFgChecked, nameCheckedRatio));
|
||||||
name.drawLeftElided(p, namex, namey, namew, w);
|
name.drawLeftElided(p, namex + leading, namey, namew, w);
|
||||||
|
|
||||||
p.setFont(st::contactsStatusFont);
|
p.setFont(st::contactsStatusFont);
|
||||||
row->paintStatusText(p, st, statusx, statusy, statusw, w, selected);
|
row->paintStatusText(p, st, statusx, statusy, statusw, w, selected);
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/effects/path_shift_gradient.h"
|
#include "ui/effects/path_shift_gradient.h"
|
||||||
#include "ui/effects/premium_graphics.h"
|
#include "ui/effects/premium_graphics.h"
|
||||||
#include "ui/layers/generic_box.h"
|
#include "ui/layers/generic_box.h"
|
||||||
#include "ui/new_badges.h"
|
|
||||||
#include "ui/peer/color_sample.h"
|
#include "ui/peer/color_sample.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
@@ -2745,32 +2744,6 @@ not_null<Ui::SettingsButton*> AddPeerColorButton(
|
|||||||
SetupPeerColorSample(button, peer, rpl::duplicate(label), style);
|
SetupPeerColorSample(button, peer, rpl::duplicate(label), style);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
const auto badge = Ui::NewBadge::CreateNewBadge(
|
|
||||||
button,
|
|
||||||
tr::lng_premium_summary_new_badge()).get();
|
|
||||||
rpl::combine(
|
|
||||||
rpl::duplicate(label),
|
|
||||||
button->widthValue()
|
|
||||||
) | rpl::on_next([=](
|
|
||||||
const QString &text,
|
|
||||||
int width) {
|
|
||||||
const auto space = st.style.font->spacew;
|
|
||||||
const auto left = st.padding.left()
|
|
||||||
+ st.style.font->width(text)
|
|
||||||
+ space;
|
|
||||||
const auto available = width - left - st.padding.right();
|
|
||||||
badge->setVisible(available >= badge->width());
|
|
||||||
if (!badge->isHidden()) {
|
|
||||||
const auto top = st.padding.top()
|
|
||||||
+ st.style.font->ascent
|
|
||||||
- st::settingsPremiumNewBadge.style.font->ascent
|
|
||||||
- st::settingsPremiumNewBadgePadding.top();
|
|
||||||
badge->moveToLeft(left, top, width);
|
|
||||||
}
|
|
||||||
}, badge->lifetime());
|
|
||||||
}
|
|
||||||
|
|
||||||
button->setClickedCallback([=] {
|
button->setClickedCallback([=] {
|
||||||
if (const auto controller = show->resolveWindow()) {
|
if (const auto controller = show->resolveWindow()) {
|
||||||
controller->show(Box(
|
controller->show(Box(
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ constexpr auto kDefaultChargeStars = 10;
|
|||||||
| Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) },
|
| Flag::SendInline, tr::lng_rights_chat_stickers(tr::now) },
|
||||||
{ Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) },
|
{ Flag::EmbedLinks, tr::lng_rights_chat_send_links(tr::now) },
|
||||||
{ Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) },
|
{ Flag::SendPolls, tr::lng_rights_chat_send_polls(tr::now) },
|
||||||
|
{ Flag::SendReactions, tr::lng_rights_chat_send_reactions(tr::now) },
|
||||||
};
|
};
|
||||||
auto second = std::vector<RestrictionLabel>{
|
auto second = std::vector<RestrictionLabel>{
|
||||||
{ Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) },
|
{ Flag::AddParticipants, tr::lng_rights_chat_add_members(tr::now) },
|
||||||
@@ -305,6 +306,7 @@ ChatRestrictions NegateRestrictions(ChatRestrictions value) {
|
|||||||
//| Flag::ViewMessages
|
//| Flag::ViewMessages
|
||||||
| Flag::ChangeInfo
|
| Flag::ChangeInfo
|
||||||
| Flag::EmbedLinks
|
| Flag::EmbedLinks
|
||||||
|
| Flag::SendReactions
|
||||||
| Flag::AddParticipants
|
| Flag::AddParticipants
|
||||||
| Flag::CreateTopics
|
| Flag::CreateTopics
|
||||||
| Flag::PinMessages
|
| Flag::PinMessages
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ historyPollRadio: Radio(defaultRadio) {
|
|||||||
rippleAreaPadding: 8px;
|
rippleAreaPadding: 8px;
|
||||||
}
|
}
|
||||||
historyPollCheckboxRadius: 3px;
|
historyPollCheckboxRadius: 3px;
|
||||||
historyPollReplyIcon: icon {{ "dialogs/dialogs_chatlist_poll-17x17", windowFg }};
|
historyPollReplyIcon: IconEmoji {
|
||||||
historyPollReplyIconSkip: 3px;
|
icon: icon {{ "dialogs/dialogs_chatlist_poll-17x17", windowFg }};
|
||||||
|
padding: margins(0px, 0px, 3px, 0px);
|
||||||
|
}
|
||||||
historyPollRadioOpacity: 0.7;
|
historyPollRadioOpacity: 0.7;
|
||||||
historyPollRadioOpacityOver: 1.;
|
historyPollRadioOpacityOver: 1.;
|
||||||
historyPollDuration: 300;
|
historyPollDuration: 300;
|
||||||
@@ -75,7 +77,6 @@ historyPollExplanationTitleSkip: 4px;
|
|||||||
historyPollExplanationCloseSize: 20px;
|
historyPollExplanationCloseSize: 20px;
|
||||||
historyPollExplanationCloseIconSize: 8px;
|
historyPollExplanationCloseIconSize: 8px;
|
||||||
historyPollExplanationCloseStroke: 2px;
|
historyPollExplanationCloseStroke: 2px;
|
||||||
historyPollExplanationMediaMaxHeight: 300px;
|
|
||||||
historyPollExplanationMediaSkip: 6px;
|
historyPollExplanationMediaSkip: 6px;
|
||||||
historyPollChoiceRight: icon {{ "poll_choice_right", activeButtonFg }};
|
historyPollChoiceRight: icon {{ "poll_choice_right", activeButtonFg }};
|
||||||
historyPollChoiceWrong: icon {{ "poll_choice_wrong", activeButtonFg }};
|
historyPollChoiceWrong: icon {{ "poll_choice_wrong", activeButtonFg }};
|
||||||
@@ -89,7 +90,6 @@ historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }};
|
|||||||
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }};
|
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }};
|
||||||
|
|
||||||
historyPollAddOptionTop: 3px;
|
historyPollAddOptionTop: 3px;
|
||||||
historyPollAddOptionHeight: 32px;
|
|
||||||
historyPollAddOptionButtonSize: 26px;
|
historyPollAddOptionButtonSize: 26px;
|
||||||
historyPollAddOptionEmojiLeft: -4px;
|
historyPollAddOptionEmojiLeft: -4px;
|
||||||
historyPollAddOptionField: InputField(defaultInputField) {
|
historyPollAddOptionField: InputField(defaultInputField) {
|
||||||
@@ -127,9 +127,7 @@ historyPollAddOptionAttach: IconButton(defaultIconButton) {
|
|||||||
ripple: defaultRippleAnimationBgOver;
|
ripple: defaultRippleAnimationBgOver;
|
||||||
}
|
}
|
||||||
|
|
||||||
pollBoxOutlinePollEmojiIcon: icon{{ "poll/general/outline_poll_emoji", activeButtonFg }};
|
|
||||||
pollBoxOutlinePollAddIcon: icon{{ "poll/general/outline_poll_add-18x18", activeButtonFg }};
|
pollBoxOutlinePollAddIcon: icon{{ "poll/general/outline_poll_add-18x18", activeButtonFg }};
|
||||||
pollBoxOutlinePollAttachIcon: icon{{ "poll/general/outline_poll_attach", activeButtonFg }};
|
|
||||||
pollBoxMenuPollOrderIcon: icon{{ "poll/general/menu_poll_order-24x24", historyComposeIconFg }};
|
pollBoxMenuPollOrderIcon: icon{{ "poll/general/menu_poll_order-24x24", historyComposeIconFg }};
|
||||||
pollBoxFilledPollDeadlineIcon: icon{{ "poll/filled/filled_poll_deadline", activeButtonFg }};
|
pollBoxFilledPollDeadlineIcon: icon{{ "poll/filled/filled_poll_deadline", activeButtonFg }};
|
||||||
pollBoxFilledPollViewIcon: icon{{ "poll/filled/filled_poll_view", activeButtonFg }};
|
pollBoxFilledPollViewIcon: icon{{ "poll/filled/filled_poll_view", activeButtonFg }};
|
||||||
@@ -138,12 +136,13 @@ pollBoxFilledPollCorrectIcon: icon{{ "poll/filled/filled_poll_correct", activeBu
|
|||||||
pollBoxFilledPollRevoteIcon: icon{{ "poll/filled/filled_poll_revote", activeButtonFg }};
|
pollBoxFilledPollRevoteIcon: icon{{ "poll/filled/filled_poll_revote", activeButtonFg }};
|
||||||
pollBoxFilledPollShuffleIcon: icon{{ "poll/filled/filled_poll_shuffle", activeButtonFg }};
|
pollBoxFilledPollShuffleIcon: icon{{ "poll/filled/filled_poll_shuffle", activeButtonFg }};
|
||||||
pollBoxFilledPollMultipleIcon: icon{{ "poll/filled/filled_poll_multiple", activeButtonFg }};
|
pollBoxFilledPollMultipleIcon: icon{{ "poll/filled/filled_poll_multiple", activeButtonFg }};
|
||||||
|
pollBoxFilledPollSubscribersIcon: icon{{ "poll/filled/filled_poll_subscribers-20x20", activeButtonFg }};
|
||||||
|
pollBoxFilledPollCountryIcon: icon{{ "poll/filled/filled_poll_country-20x20", activeButtonFg }};
|
||||||
|
|
||||||
pollAttachTextSkip: 28px;
|
pollAttachTextSkip: 28px;
|
||||||
pollAttachProgressMargin: 4px;
|
pollAttachProgressMargin: 4px;
|
||||||
pollAttachCancel: icon {{ "history_audio_cancel", historyFileThumbIconFg }};
|
pollAttachCancel: icon {{ "history_audio_cancel", historyFileThumbIconFg }};
|
||||||
pollAttachView: icon {{ "mediaview/views", historyFileThumbIconFg }};
|
pollAttachView: icon {{ "mediaview/views", historyFileThumbIconFg }};
|
||||||
pollAttachShift: point(-11px, -2px);
|
|
||||||
|
|
||||||
createPollField: InputField(defaultInputField) {
|
createPollField: InputField(defaultInputField) {
|
||||||
textMargins: margins(0px, 4px, 0px, 4px);
|
textMargins: margins(0px, 4px, 0px, 4px);
|
||||||
|
|||||||
@@ -0,0 +1,581 @@
|
|||||||
|
/*
|
||||||
|
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/preview_ai_tone_box.h"
|
||||||
|
|
||||||
|
#include "boxes/create_ai_tone_box.h"
|
||||||
|
#include "core/ui_integration.h"
|
||||||
|
#include "data/data_ai_compose_tones.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "data/data_user.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/main_app_config.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "ui/controls/custom_emoji_toast_icon.h"
|
||||||
|
#include "ui/effects/animation_value.h"
|
||||||
|
#include "ui/effects/animations.h"
|
||||||
|
#include "ui/effects/skeleton_animation.h"
|
||||||
|
#include "ui/layers/generic_box.h"
|
||||||
|
#include "ui/layers/show.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/text/text_custom_emoji.h"
|
||||||
|
#include "ui/text/text_entity.h"
|
||||||
|
#include "ui/text/text_utilities.h"
|
||||||
|
#include "ui/toast/toast.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/widgets/shadow.h"
|
||||||
|
#include "ui/wrap/vertical_layout.h"
|
||||||
|
#include "ui/vertical_list.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "styles/style_boxes.h"
|
||||||
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_layers.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kToastDuration = crl::time(4000);
|
||||||
|
constexpr auto kSpinDuration = crl::time(600);
|
||||||
|
constexpr auto kWaitDuration = crl::time(1000);
|
||||||
|
|
||||||
|
class RefreshSpinEmoji final : public Ui::Text::CustomEmoji {
|
||||||
|
public:
|
||||||
|
RefreshSpinEmoji(
|
||||||
|
std::shared_ptr<rpl::variable<bool>> loading,
|
||||||
|
Fn<void()> repaint);
|
||||||
|
|
||||||
|
int width() override;
|
||||||
|
QString entityData() override;
|
||||||
|
void paint(QPainter &p, const Context &context) override;
|
||||||
|
void unload() override;
|
||||||
|
bool ready() override;
|
||||||
|
bool readyInDefaultState() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class Phase : uchar { Idle, Spinning, Waiting };
|
||||||
|
|
||||||
|
void handleLoading(bool now);
|
||||||
|
void tick(crl::time now);
|
||||||
|
[[nodiscard]] float64 angleDegrees(crl::time now) const;
|
||||||
|
|
||||||
|
rpl::variable<bool> _loading;
|
||||||
|
const Fn<void()> _repaint;
|
||||||
|
Ui::Animations::Basic _animation;
|
||||||
|
crl::time _phaseStarted = 0;
|
||||||
|
Phase _phase = Phase::Idle;
|
||||||
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
RefreshSpinEmoji::RefreshSpinEmoji(
|
||||||
|
std::shared_ptr<rpl::variable<bool>> loading,
|
||||||
|
Fn<void()> repaint)
|
||||||
|
: _loading(loading->value())
|
||||||
|
, _repaint(std::move(repaint))
|
||||||
|
, _animation([=](crl::time now) { tick(now); }) {
|
||||||
|
_loading.value(
|
||||||
|
) | rpl::on_next([=](bool value) {
|
||||||
|
handleLoading(value);
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
int RefreshSpinEmoji::width() {
|
||||||
|
const auto &e = st::aiTonePreviewAnotherExampleIcon;
|
||||||
|
return e.padding.left() + e.icon.width() + e.padding.right();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RefreshSpinEmoji::entityData() {
|
||||||
|
return u"ai-tone-refresh"_q;
|
||||||
|
}
|
||||||
|
|
||||||
|
float64 RefreshSpinEmoji::angleDegrees(crl::time now) const {
|
||||||
|
if (_phase != Phase::Spinning) {
|
||||||
|
return 0.;
|
||||||
|
}
|
||||||
|
const auto elapsed = now - _phaseStarted;
|
||||||
|
const auto dt = std::clamp(
|
||||||
|
elapsed / float64(kSpinDuration),
|
||||||
|
0.,
|
||||||
|
1.);
|
||||||
|
return anim::easeOutBack(360., dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshSpinEmoji::paint(QPainter &p, const Context &context) {
|
||||||
|
const auto &e = st::aiTonePreviewAnotherExampleIcon;
|
||||||
|
const auto size = e.icon.size();
|
||||||
|
const auto pos = context.position
|
||||||
|
+ QPoint(e.padding.left(), e.padding.top());
|
||||||
|
const auto angle = angleDegrees(context.now);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
if (angle != 0.) {
|
||||||
|
const auto center = QPointF(pos)
|
||||||
|
+ QPointF(size.width() / 2.0, size.height() / 2.0);
|
||||||
|
p.save();
|
||||||
|
p.translate(center);
|
||||||
|
p.rotate(angle);
|
||||||
|
p.translate(-center);
|
||||||
|
e.icon.paint(p, pos, 0, context.textColor);
|
||||||
|
p.restore();
|
||||||
|
} else {
|
||||||
|
e.icon.paint(p, pos, 0, context.textColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshSpinEmoji::unload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RefreshSpinEmoji::ready() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RefreshSpinEmoji::readyInDefaultState() {
|
||||||
|
return _phase == Phase::Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshSpinEmoji::handleLoading(bool now) {
|
||||||
|
if (now) {
|
||||||
|
if (_phase == Phase::Idle) {
|
||||||
|
_phase = Phase::Spinning;
|
||||||
|
_phaseStarted = crl::now();
|
||||||
|
_animation.start();
|
||||||
|
if (_repaint) {
|
||||||
|
_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (_phase == Phase::Waiting) {
|
||||||
|
_phase = Phase::Idle;
|
||||||
|
_animation.stop();
|
||||||
|
if (_repaint) {
|
||||||
|
_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshSpinEmoji::tick(crl::time now) {
|
||||||
|
const auto elapsed = now - _phaseStarted;
|
||||||
|
if (_phase == Phase::Spinning && elapsed >= kSpinDuration) {
|
||||||
|
if (_loading.current()) {
|
||||||
|
_phase = Phase::Waiting;
|
||||||
|
_phaseStarted = now;
|
||||||
|
} else {
|
||||||
|
_phase = Phase::Idle;
|
||||||
|
_animation.stop();
|
||||||
|
}
|
||||||
|
} else if (_phase == Phase::Waiting && elapsed >= kWaitDuration) {
|
||||||
|
if (_loading.current()) {
|
||||||
|
_phase = Phase::Spinning;
|
||||||
|
_phaseStarted = now;
|
||||||
|
} else {
|
||||||
|
_phase = Phase::Idle;
|
||||||
|
_animation.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_repaint) {
|
||||||
|
_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreviewAiToneExampleCard final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
PreviewAiToneExampleCard(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
std::shared_ptr<rpl::variable<bool>> loading);
|
||||||
|
|
||||||
|
void showExample(Data::AiComposeToneExample example);
|
||||||
|
void showSkeleton(bool shown);
|
||||||
|
void setAnotherVisible(bool visible);
|
||||||
|
[[nodiscard]] rpl::producer<> anotherExampleRequested() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int resizeGetHeight(int newWidth) override;
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const not_null<Main::Session*> _session;
|
||||||
|
const not_null<Ui::VerticalLayout*> _layout;
|
||||||
|
const not_null<Ui::FlatLabel*> _beforeTitle;
|
||||||
|
const not_null<Ui::FlatLabel*> _beforeBody;
|
||||||
|
const style::complex_color _shadowColor;
|
||||||
|
const not_null<Ui::PlainShadow*> _shadow;
|
||||||
|
const not_null<Ui::FlatLabel*> _afterTitle;
|
||||||
|
const not_null<Ui::FlatLabel*> _afterBody;
|
||||||
|
const not_null<Ui::RoundButton*> _another;
|
||||||
|
Ui::SkeletonAnimation _beforeSkeleton;
|
||||||
|
Ui::SkeletonAnimation _afterSkeleton;
|
||||||
|
rpl::event_stream<> _anotherExampleRequested;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
PreviewAiToneExampleCard::PreviewAiToneExampleCard(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
std::shared_ptr<rpl::variable<bool>> loading)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _session(session)
|
||||||
|
, _layout(Ui::CreateChild<Ui::VerticalLayout>(this))
|
||||||
|
, _beforeTitle(_layout->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
_layout,
|
||||||
|
tr::lng_ai_compose_before(tr::now),
|
||||||
|
st::aiComposeCardTitle),
|
||||||
|
QMargins(
|
||||||
|
st::aiTonePreviewExampleCardPadding.left(),
|
||||||
|
st::aiTonePreviewExampleCardPadding.top(),
|
||||||
|
st::aiTonePreviewExampleCardPadding.right(),
|
||||||
|
0)))
|
||||||
|
, _beforeBody(_layout->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(_layout, st::aiComposeBodyLabel),
|
||||||
|
QMargins(
|
||||||
|
st::aiTonePreviewExampleCardPadding.left(),
|
||||||
|
st::aiTonePreviewExampleCardTitleSkip,
|
||||||
|
st::aiTonePreviewExampleCardPadding.right(),
|
||||||
|
0)))
|
||||||
|
, _shadowColor([] {
|
||||||
|
auto color = st::windowSubTextFg->c;
|
||||||
|
color.setAlphaF(st::aiComposeShadowOpacity);
|
||||||
|
return color;
|
||||||
|
})
|
||||||
|
, _shadow(_layout->add(
|
||||||
|
object_ptr<Ui::PlainShadow>(_layout, _shadowColor.color()),
|
||||||
|
QMargins(
|
||||||
|
st::aiTonePreviewExampleCardPadding.left(),
|
||||||
|
st::aiTonePreviewExampleCardSectionSkip / 2,
|
||||||
|
st::aiTonePreviewExampleCardPadding.right(),
|
||||||
|
0)))
|
||||||
|
, _afterTitle(_layout->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
_layout,
|
||||||
|
tr::lng_ai_compose_after(tr::now),
|
||||||
|
st::aiComposeCardTitle),
|
||||||
|
QMargins(
|
||||||
|
st::aiTonePreviewExampleCardPadding.left(),
|
||||||
|
st::aiTonePreviewExampleCardSectionSkip / 2,
|
||||||
|
st::aiTonePreviewExampleCardPadding.right(),
|
||||||
|
0)))
|
||||||
|
, _afterBody(_layout->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(_layout, st::aiComposeBodyLabel),
|
||||||
|
QMargins(
|
||||||
|
st::aiTonePreviewExampleCardPadding.left(),
|
||||||
|
st::aiTonePreviewExampleCardTitleSkip,
|
||||||
|
st::aiTonePreviewExampleCardPadding.right(),
|
||||||
|
st::aiTonePreviewExampleCardPadding.bottom())))
|
||||||
|
, _another(Ui::CreateChild<Ui::RoundButton>(
|
||||||
|
this,
|
||||||
|
rpl::single(QString()),
|
||||||
|
st::aiTonePreviewAnotherExampleButton))
|
||||||
|
, _beforeSkeleton(_beforeBody)
|
||||||
|
, _afterSkeleton(_afterBody) {
|
||||||
|
_beforeBody->setSelectable(true);
|
||||||
|
_afterBody->setSelectable(true);
|
||||||
|
_another->raise();
|
||||||
|
rpl::combine(
|
||||||
|
widthValue(),
|
||||||
|
_beforeTitle->geometryValue(),
|
||||||
|
_another->widthValue()
|
||||||
|
) | rpl::on_next([=](int width, QRect titleGeometry, int) {
|
||||||
|
const auto right = st::aiTonePreviewExampleCardPadding.left();
|
||||||
|
const auto &button = st::aiTonePreviewAnotherExampleButton;
|
||||||
|
const auto &title = st::aiComposeCardTitle;
|
||||||
|
const auto shift = title.style.font->ascent
|
||||||
|
- button.style.font->ascent
|
||||||
|
- button.textTop
|
||||||
|
- button.padding.top();
|
||||||
|
_another->moveToRight(
|
||||||
|
right,
|
||||||
|
titleGeometry.top() + shift,
|
||||||
|
width);
|
||||||
|
}, lifetime());
|
||||||
|
auto context = Ui::Text::MarkedContext();
|
||||||
|
context.repaint = [raw = _another.get()] { raw->update(); };
|
||||||
|
context.customEmojiFactory = [loading = std::move(loading)](
|
||||||
|
QStringView data,
|
||||||
|
const Ui::Text::MarkedContext &context
|
||||||
|
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
|
||||||
|
if (data != u"ai-tone-refresh"_q) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::make_unique<RefreshSpinEmoji>(loading, context.repaint);
|
||||||
|
};
|
||||||
|
_another->setContext(context);
|
||||||
|
_another->setText(rpl::single(
|
||||||
|
Ui::Text::SingleCustomEmoji(u"ai-tone-refresh"_q)
|
||||||
|
.append(tr::lng_ai_compose_tone_preview_add_example(tr::now))));
|
||||||
|
_another->setClickedCallback([=] {
|
||||||
|
_anotherExampleRequested.fire({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewAiToneExampleCard::showExample(
|
||||||
|
Data::AiComposeToneExample example) {
|
||||||
|
const auto context = Core::TextContext({ .session = _session });
|
||||||
|
_beforeBody->setMarkedText(example.from, context);
|
||||||
|
_afterBody->setMarkedText(example.to, context);
|
||||||
|
_beforeSkeleton.stop();
|
||||||
|
_afterSkeleton.stop();
|
||||||
|
if (width() > 0) {
|
||||||
|
resizeToWidth(width());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewAiToneExampleCard::showSkeleton(bool shown) {
|
||||||
|
if (shown) {
|
||||||
|
_beforeSkeleton.start();
|
||||||
|
_afterSkeleton.start();
|
||||||
|
} else {
|
||||||
|
_beforeSkeleton.stop();
|
||||||
|
_afterSkeleton.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewAiToneExampleCard::setAnotherVisible(bool visible) {
|
||||||
|
_another->setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> PreviewAiToneExampleCard::anotherExampleRequested() const {
|
||||||
|
return _anotherExampleRequested.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
int PreviewAiToneExampleCard::resizeGetHeight(int newWidth) {
|
||||||
|
_layout->resizeToWidth(newWidth);
|
||||||
|
_layout->moveToLeft(0, 0, newWidth);
|
||||||
|
return _layout->heightNoMargins();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewAiToneExampleCard::paintEvent(QPaintEvent *e) {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::aiTonePreviewExampleCardBg);
|
||||||
|
p.drawRoundedRect(
|
||||||
|
rect(),
|
||||||
|
st::aiTonePreviewExampleCardRadius,
|
||||||
|
st::aiTonePreviewExampleCardRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowToneAddedToast(
|
||||||
|
std::shared_ptr<Ui::Show> show,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone) {
|
||||||
|
const auto size = QSize(
|
||||||
|
st::aiComposeToneToastIconSize.width(),
|
||||||
|
st::aiComposeToneToastIconSize.height());
|
||||||
|
show->showToast(Ui::Toast::Config{
|
||||||
|
.title = tr::lng_ai_compose_tone_added(tr::now),
|
||||||
|
.text = tr::lng_ai_compose_tone_added_description(
|
||||||
|
tr::now,
|
||||||
|
lt_name,
|
||||||
|
tr::marked(tone.title),
|
||||||
|
tr::marked),
|
||||||
|
.iconContent = Ui::MakeCustomEmojiToastIcon(
|
||||||
|
session,
|
||||||
|
tone.emojiId,
|
||||||
|
size),
|
||||||
|
.iconPadding = st::aiComposeToneToastIconPadding,
|
||||||
|
.duration = kToastDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowToneRemovedToast(std::shared_ptr<Ui::Show> show, bool deleted) {
|
||||||
|
show->showToast(Ui::Toast::Config{
|
||||||
|
.text = { (deleted
|
||||||
|
? tr::lng_ai_compose_tone_deleted
|
||||||
|
: tr::lng_ai_compose_tone_removed)(tr::now) },
|
||||||
|
.icon = &st::aiComposeToneRemovedToastIcon,
|
||||||
|
.duration = kToastDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto FindInstalledCustomTone(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const Data::AiComposeTone &tone)
|
||||||
|
-> std::optional<Data::AiComposeTone> {
|
||||||
|
if (tone.isDefault) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
for (const auto &installedTone : session->data().aiComposeTones().list()) {
|
||||||
|
if (!installedTone.isDefault && (installedTone.id == tone.id)) {
|
||||||
|
return installedTone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void PreviewAiToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
Data::AiComposeTone tone) {
|
||||||
|
box->setStyle(st::aiComposeBox);
|
||||||
|
box->setNoContentMargin(true);
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); });
|
||||||
|
|
||||||
|
const auto top = box->setPinnedToTopContent(
|
||||||
|
object_ptr<Ui::VerticalLayout>(box));
|
||||||
|
Ui::AddSkip(top, st::defaultVerticalListSkip * 4);
|
||||||
|
AddAiToneIconPreview(top, session, rpl::single(tone.emojiId), nullptr);
|
||||||
|
top->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
top,
|
||||||
|
rpl::single(tone.title),
|
||||||
|
st::aiTonePreviewTitleLabel),
|
||||||
|
st::aiTonePreviewTitleMargin,
|
||||||
|
style::al_top);
|
||||||
|
top->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
top,
|
||||||
|
tr::lng_ai_compose_tone_preview_about(),
|
||||||
|
st::aiTonePreviewAboutLabel),
|
||||||
|
st::aiTonePreviewAboutMargin,
|
||||||
|
style::al_top
|
||||||
|
)->setTryMakeSimilarLines(true);
|
||||||
|
|
||||||
|
const auto body = box->verticalLayout();
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
int examplesCount = 0;
|
||||||
|
std::shared_ptr<rpl::variable<bool>> requesting
|
||||||
|
= std::make_shared<rpl::variable<bool>>(false);
|
||||||
|
};
|
||||||
|
const auto state = box->lifetime().make_state<State>();
|
||||||
|
state->examplesCount = tone.firstExample ? 1 : 0;
|
||||||
|
|
||||||
|
const auto card = body->add(
|
||||||
|
object_ptr<PreviewAiToneExampleCard>(
|
||||||
|
body,
|
||||||
|
session,
|
||||||
|
state->requesting),
|
||||||
|
st::aiTonePreviewExampleCardMargin);
|
||||||
|
const auto maxExamples = session->appConfig().get<int>(
|
||||||
|
u"aicompose_tone_examples_num"_q,
|
||||||
|
3);
|
||||||
|
const auto updateAnother = [=] {
|
||||||
|
card->setAnotherVisible(state->examplesCount < maxExamples);
|
||||||
|
};
|
||||||
|
updateAnother();
|
||||||
|
const auto loadAnother = [=] {
|
||||||
|
if (state->requesting->current()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*state->requesting = true;
|
||||||
|
card->showSkeleton(true);
|
||||||
|
const auto num = state->examplesCount;
|
||||||
|
session->data().aiComposeTones().getToneExample(
|
||||||
|
tone,
|
||||||
|
num,
|
||||||
|
crl::guard(box, [=](Data::AiComposeToneExample example) {
|
||||||
|
*state->requesting = false;
|
||||||
|
++state->examplesCount;
|
||||||
|
card->showExample(std::move(example));
|
||||||
|
updateAnother();
|
||||||
|
}),
|
||||||
|
crl::guard(box, [=](const MTP::Error &error) {
|
||||||
|
*state->requesting = false;
|
||||||
|
card->showSkeleton(false);
|
||||||
|
if (!MTP::IgnoreError(error)) {
|
||||||
|
box->showToast(error.type());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
card->anotherExampleRequested(
|
||||||
|
) | rpl::on_next(loadAnother, card->lifetime());
|
||||||
|
|
||||||
|
if (tone.firstExample) {
|
||||||
|
card->showExample(*tone.firstExample);
|
||||||
|
} else {
|
||||||
|
loadAnother();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto text = tr::marked();
|
||||||
|
if (tone.installsCount > 0) {
|
||||||
|
text = tr::lng_ai_compose_tone_preview_used_by(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
tone.installsCount,
|
||||||
|
tr::marked);
|
||||||
|
}
|
||||||
|
if (const auto user = session->data().userLoaded(tone.authorId)) {
|
||||||
|
const auto name = user->shortName();
|
||||||
|
auto mention = tr::marked(name);
|
||||||
|
mention.entities.push_back(EntityInText(
|
||||||
|
EntityType::MentionName,
|
||||||
|
0,
|
||||||
|
name.size(),
|
||||||
|
TextUtilities::MentionNameDataFromFields({
|
||||||
|
.selfId = session->userId().bare,
|
||||||
|
.userId = tone.authorId.bare,
|
||||||
|
.accessHash = user->accessHash(),
|
||||||
|
})));
|
||||||
|
auto createdBy = tr::lng_ai_compose_tone_preview_created_by(
|
||||||
|
tr::now,
|
||||||
|
lt_user,
|
||||||
|
std::move(mention),
|
||||||
|
tr::marked);
|
||||||
|
if (!text.empty()) {
|
||||||
|
text.append(' ').append(std::move(createdBy));
|
||||||
|
} else {
|
||||||
|
text = std::move(createdBy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!text.empty()) {
|
||||||
|
const auto attribution = body->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(body, st::aiTonePreviewAttributionLabel),
|
||||||
|
st::aiTonePreviewAttributionMargin,
|
||||||
|
style::al_top);
|
||||||
|
attribution->setMarkedText(
|
||||||
|
std::move(text),
|
||||||
|
Core::TextContext({ .session = session }));
|
||||||
|
}
|
||||||
|
Ui::AddSkip(body, st::aiTonePreviewBottomSkip);
|
||||||
|
|
||||||
|
const auto installedTone = FindInstalledCustomTone(session, tone);
|
||||||
|
|
||||||
|
if (installedTone) {
|
||||||
|
const auto remove = box->addButton(
|
||||||
|
installedTone->creator
|
||||||
|
? tr::lng_ai_compose_tone_delete()
|
||||||
|
: tr::lng_ai_compose_tone_remove(),
|
||||||
|
[=] {
|
||||||
|
const auto show = box->uiShow();
|
||||||
|
ConfirmDeleteAiTone(
|
||||||
|
show,
|
||||||
|
session,
|
||||||
|
*installedTone,
|
||||||
|
crl::guard(box, [=] {
|
||||||
|
box->closeBox();
|
||||||
|
ShowToneRemovedToast(show, installedTone->creator);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
st::aiToneDeleteButton);
|
||||||
|
remove->setFullRadius(true);
|
||||||
|
} else {
|
||||||
|
const auto add = box->addButton(
|
||||||
|
tr::lng_ai_compose_tone_preview_add(),
|
||||||
|
[=] {
|
||||||
|
const auto show = box->uiShow();
|
||||||
|
session->data().aiComposeTones().save(
|
||||||
|
tone,
|
||||||
|
false,
|
||||||
|
crl::guard(box, [=] {
|
||||||
|
box->closeBox();
|
||||||
|
ShowToneAddedToast(show, session, tone);
|
||||||
|
}),
|
||||||
|
crl::guard(box, [=](const MTP::Error &error) {
|
||||||
|
if (error.type() == u"TONES_SAVED_TOO_MANY"_q) {
|
||||||
|
ShowAiComposeToneLimitError(show, session);
|
||||||
|
} else if (!MTP::IgnoreError(error)) {
|
||||||
|
box->showToast(error.type());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
add->setFullRadius(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Telegram Desktop,
|
||||||
|
the official desktop application for the Telegram messaging service.
|
||||||
|
|
||||||
|
For license and copyright information please follow this link:
|
||||||
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
struct AiComposeTone;
|
||||||
|
} // namespace Data
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class GenericBox;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
void PreviewAiToneBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
Data::AiComposeTone tone);
|
||||||
@@ -113,9 +113,12 @@ void FileDialogCallback(
|
|||||||
callback(std::move(*list));
|
callback(std::move(*list));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void RenameFileBox(
|
void RenameFileBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
const QString ¤tName,
|
const QString ¤tName,
|
||||||
|
bool allowExtensionEdit,
|
||||||
Fn<void(QString)> apply) {
|
Fn<void(QString)> apply) {
|
||||||
box->setTitle(tr::lng_rename_file());
|
box->setTitle(tr::lng_rename_file());
|
||||||
const auto field = box->addRow(object_ptr<Ui::InputField>(
|
const auto field = box->addRow(object_ptr<Ui::InputField>(
|
||||||
@@ -123,19 +126,25 @@ void RenameFileBox(
|
|||||||
st::settingsDeviceName,
|
st::settingsDeviceName,
|
||||||
rpl::single(QString()),
|
rpl::single(QString()),
|
||||||
currentName));
|
currentName));
|
||||||
const auto extension = [&] {
|
QString extension;
|
||||||
if (currentName.isEmpty()) {
|
if (allowExtensionEdit) {
|
||||||
return u".png"_q;
|
field->setMaxLength(kMaxDisplayNameLength);
|
||||||
}
|
field->setText(currentName);
|
||||||
const auto dot = currentName.lastIndexOf('.');
|
} else {
|
||||||
return (dot >= 0) ? currentName.mid(dot) : QString();
|
extension = [&] {
|
||||||
}();
|
if (currentName.isEmpty()) {
|
||||||
const auto nameWithoutExt = extension.isEmpty()
|
return u".png"_q;
|
||||||
? currentName
|
}
|
||||||
: currentName.left(currentName.size() - extension.size());
|
const auto dot = currentName.lastIndexOf('.');
|
||||||
const auto maxNameLength = kMaxDisplayNameLength - extension.size();
|
return (dot >= 0) ? currentName.mid(dot) : QString();
|
||||||
field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0);
|
}();
|
||||||
field->setText(nameWithoutExt);
|
const auto nameWithoutExt = extension.isEmpty()
|
||||||
|
? currentName
|
||||||
|
: currentName.left(currentName.size() - extension.size());
|
||||||
|
const auto maxNameLength = kMaxDisplayNameLength - extension.size();
|
||||||
|
field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0);
|
||||||
|
field->setText(nameWithoutExt);
|
||||||
|
}
|
||||||
field->selectAll();
|
field->selectAll();
|
||||||
box->setFocusCallback([=] {
|
box->setFocusCallback([=] {
|
||||||
field->setFocusFast();
|
field->setFocusFast();
|
||||||
@@ -146,12 +155,18 @@ void RenameFileBox(
|
|||||||
field->showError();
|
field->showError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((newName.size() + extension.size()) > kMaxDisplayNameLength) {
|
if (allowExtensionEdit) {
|
||||||
|
if (newName.size() > kMaxDisplayNameLength) {
|
||||||
|
field->showError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if ((newName.size() + extension.size())
|
||||||
|
> kMaxDisplayNameLength) {
|
||||||
field->showError();
|
field->showError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto weak = base::make_weak(box);
|
const auto weak = base::make_weak(box);
|
||||||
apply(newName + extension);
|
apply(allowExtensionEdit ? newName : (newName + extension));
|
||||||
if (const auto strong = weak.get()) {
|
if (const auto strong = weak.get()) {
|
||||||
strong->closeBox();
|
strong->closeBox();
|
||||||
}
|
}
|
||||||
@@ -165,6 +180,8 @@ void RenameFileBox(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
void EditFileCaptionBox(
|
void EditFileCaptionBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
const style::ComposeControls &st,
|
const style::ComposeControls &st,
|
||||||
@@ -416,11 +433,13 @@ SendFilesBox::Block::Block(
|
|||||||
media->setCanShowHighQualityBadge(first.canUseHighQualityPhoto());
|
media->setCanShowHighQualityBadge(first.canUseHighQualityPhoto());
|
||||||
_preview.reset(media);
|
_preview.reset(media);
|
||||||
} else {
|
} else {
|
||||||
_preview.reset(Ui::CreateChild<Ui::SingleFilePreview>(
|
const auto single = Ui::CreateChild<Ui::SingleFilePreview>(
|
||||||
parent.get(),
|
parent.get(),
|
||||||
st,
|
st,
|
||||||
first,
|
first,
|
||||||
captionContext));
|
captionContext);
|
||||||
|
single->setRenameEnabled(!SkipCaption(first, way));
|
||||||
|
_preview.reset(single);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_preview->show();
|
_preview->show();
|
||||||
@@ -492,6 +511,19 @@ rpl::producer<int> SendFilesBox::Block::itemModifyRequest() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpl::producer<int> SendFilesBox::Block::itemRenameRequest() const {
|
||||||
|
using namespace rpl::mappers;
|
||||||
|
|
||||||
|
const auto preview = _preview.get();
|
||||||
|
const auto from = _from;
|
||||||
|
if (_isAlbum || _isSingleMedia) {
|
||||||
|
return rpl::never<int>();
|
||||||
|
} else {
|
||||||
|
const auto single = static_cast<Ui::SingleFilePreview*>(preview);
|
||||||
|
return single->renameRequests() | rpl::map_to(from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rpl::producer<> SendFilesBox::Block::orderUpdated() const {
|
rpl::producer<> SendFilesBox::Block::orderUpdated() const {
|
||||||
if (_isAlbum) {
|
if (_isAlbum) {
|
||||||
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
|
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
|
||||||
@@ -506,6 +538,10 @@ void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {
|
|||||||
const auto media = static_cast<Ui::SingleMediaPreview*>(
|
const auto media = static_cast<Ui::SingleMediaPreview*>(
|
||||||
_preview.get());
|
_preview.get());
|
||||||
media->setSendWay(way);
|
media->setSendWay(way);
|
||||||
|
} else {
|
||||||
|
const auto single = static_cast<Ui::SingleFilePreview*>(
|
||||||
|
_preview.get());
|
||||||
|
single->setRenameEnabled(!SkipCaption((*_items)[_from], way));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -714,10 +750,8 @@ Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
|
|||||||
? SendMenu::SpoilerState::Enabled
|
? SendMenu::SpoilerState::Enabled
|
||||||
: SendMenu::SpoilerState::Possible;
|
: SendMenu::SpoilerState::Possible;
|
||||||
const auto way = _sendWay.current();
|
const auto way = _sendWay.current();
|
||||||
const auto canMoveCaption = _list.canMoveCaption(
|
const auto canMoveCaption = canMoveCaptionInCurrentSendWay()
|
||||||
way.groupFiles() && way.sendImagesAsPhotos(),
|
&& HasSendText(_caption);
|
||||||
way.sendImagesAsPhotos()
|
|
||||||
) && HasSendText(_caption);
|
|
||||||
result.caption = !canMoveCaption
|
result.caption = !canMoveCaption
|
||||||
? SendMenu::CaptionState::None
|
? SendMenu::CaptionState::None
|
||||||
: _invertCaption
|
: _invertCaption
|
||||||
@@ -849,7 +883,7 @@ void SendFilesBox::setupDragArea() {
|
|||||||
|
|
||||||
const auto droppedCallback = [=](bool compress) {
|
const auto droppedCallback = [=](bool compress) {
|
||||||
return [=](const QMimeData *data) {
|
return [=](const QMimeData *data) {
|
||||||
addFiles(data);
|
addFiles(data, compress);
|
||||||
_show->activate();
|
_show->activate();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -997,6 +1031,23 @@ bool SendFilesBox::hasSendLargePhotosOption() const {
|
|||||||
_sendWay.current().sendImagesAsPhotos());
|
_sendWay.current().sendImagesAsPhotos());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SendFilesBox::canMoveCaptionInCurrentSendWay() const {
|
||||||
|
const auto way = _sendWay.current();
|
||||||
|
if (!way.sendImagesAsPhotos() || !_list.canAddCaption(true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto count = int(_list.files.size());
|
||||||
|
if (count < 1 || count > Ui::MaxAlbumItems()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto isPhotoOrVideo = [](const Ui::PreparedFile &file) {
|
||||||
|
return file.type == Ui::PreparedFile::Type::Photo
|
||||||
|
|| file.type == Ui::PreparedFile::Type::Video;
|
||||||
|
};
|
||||||
|
return (count == 1 || way.groupFiles())
|
||||||
|
&& ranges::all_of(_list.files, isPhotoOrVideo);
|
||||||
|
}
|
||||||
|
|
||||||
bool SendFilesBox::canChangePrice() const {
|
bool SendFilesBox::canChangePrice() const {
|
||||||
const auto way = _sendWay.current();
|
const auto way = _sendWay.current();
|
||||||
const auto broadcast = _toPeer->asBroadcast();
|
const auto broadcast = _toPeer->asBroadcast();
|
||||||
@@ -1467,6 +1518,32 @@ void SendFilesBox::pushBlock(int from, int till) {
|
|||||||
entry.videoCover = nullptr;
|
entry.videoCover = nullptr;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const auto renameFile = [=](int fileIndex) {
|
||||||
|
if (fileIndex < 0 || fileIndex >= _list.files.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &file = _list.files[fileIndex];
|
||||||
|
const auto canEditFileData = !SkipCaption(
|
||||||
|
file,
|
||||||
|
_sendWay.current());
|
||||||
|
if (!canEditFileData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto allowExtensionEdit = file.path.isEmpty();
|
||||||
|
_show->show(Box(
|
||||||
|
RenameFileBox,
|
||||||
|
file.displayName,
|
||||||
|
allowExtensionEdit,
|
||||||
|
[=](QString newName) {
|
||||||
|
const auto displayName = std::move(newName);
|
||||||
|
_list.files[fileIndex].displayName = displayName;
|
||||||
|
if (!setDisplayNameInSingleFilePreview(
|
||||||
|
fileIndex,
|
||||||
|
displayName)) {
|
||||||
|
refreshAllAfterChanges(from);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
const auto showContextMenu = [=](
|
const auto showContextMenu = [=](
|
||||||
int fileIndex,
|
int fileIndex,
|
||||||
QPoint globalPosition,
|
QPoint globalPosition,
|
||||||
@@ -1501,17 +1578,7 @@ void SendFilesBox::pushBlock(int from, int till) {
|
|||||||
_sendWay.current());
|
_sendWay.current());
|
||||||
if (canEditFileData) {
|
if (canEditFileData) {
|
||||||
state->menu->addAction(tr::lng_rename_file(tr::now), [=] {
|
state->menu->addAction(tr::lng_rename_file(tr::now), [=] {
|
||||||
auto &file = _list.files[fileIndex];
|
renameFile(fileIndex);
|
||||||
_show->show(Box(RenameFileBox, file.displayName, [=](
|
|
||||||
QString newName) {
|
|
||||||
const auto displayName = std::move(newName);
|
|
||||||
_list.files[fileIndex].displayName = displayName;
|
|
||||||
if (!setDisplayNameInSingleFilePreview(
|
|
||||||
fileIndex,
|
|
||||||
displayName)) {
|
|
||||||
refreshAllAfterChanges(from);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}, &st::menuIconEdit);
|
}, &st::menuIconEdit);
|
||||||
state->menu->addAction(
|
state->menu->addAction(
|
||||||
tr::lng_context_upload_edit_caption(tr::now),
|
tr::lng_context_upload_edit_caption(tr::now),
|
||||||
@@ -1626,6 +1693,11 @@ void SendFilesBox::pushBlock(int from, int till) {
|
|||||||
openInPhotoEditor(index);
|
openInPhotoEditor(index);
|
||||||
}, widget->lifetime());
|
}, widget->lifetime());
|
||||||
|
|
||||||
|
block.itemRenameRequest(
|
||||||
|
) | rpl::on_next([=](int index) {
|
||||||
|
renameFile(index);
|
||||||
|
}, widget->lifetime());
|
||||||
|
|
||||||
block.orderUpdated() | rpl::on_next([=]{
|
block.orderUpdated() | rpl::on_next([=]{
|
||||||
if (_priceTag) {
|
if (_priceTag) {
|
||||||
_priceTagBg = QImage();
|
_priceTagBg = QImage();
|
||||||
@@ -2041,7 +2113,9 @@ void SendFilesBox::captionResized() {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
|
bool SendFilesBox::addFiles(
|
||||||
|
not_null<const QMimeData*> data,
|
||||||
|
std::optional<bool> overrideSendImagesAsPhotos) {
|
||||||
const auto premium = _show->session().premium();
|
const auto premium = _show->session().premium();
|
||||||
auto list = [&] {
|
auto list = [&] {
|
||||||
const auto urls = Core::ReadMimeUrls(data);
|
const auto urls = Core::ReadMimeUrls(data);
|
||||||
@@ -2063,13 +2137,30 @@ bool SendFilesBox::addFiles(not_null<const QMimeData*> data) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}();
|
}();
|
||||||
|
if (overrideSendImagesAsPhotos.has_value()) {
|
||||||
|
list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
|
||||||
|
}
|
||||||
return addFiles(std::move(list));
|
return addFiles(std::move(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SendFilesBox::applySendImagesAsPhotosOverride(
|
||||||
|
const Ui::PreparedList &list) {
|
||||||
|
if (!list.overrideSendImagesAsPhotos.has_value()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_list.overrideSendImagesAsPhotos = list.overrideSendImagesAsPhotos;
|
||||||
|
auto candidate = _sendWay.current();
|
||||||
|
candidate.setSendImagesAsPhotos(*list.overrideSendImagesAsPhotos);
|
||||||
|
if (checkWith(list, candidate, true)) {
|
||||||
|
_sendWay = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool SendFilesBox::addFiles(Ui::PreparedList list) {
|
bool SendFilesBox::addFiles(Ui::PreparedList list) {
|
||||||
if (list.error != Ui::PreparedList::Error::None) {
|
if (list.error != Ui::PreparedList::Error::None) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
applySendImagesAsPhotosOverride(list);
|
||||||
const auto count = int(_list.files.size());
|
const auto count = int(_list.files.size());
|
||||||
_list.filesToProcess.insert(
|
_list.filesToProcess.insert(
|
||||||
_list.filesToProcess.end(),
|
_list.filesToProcess.end(),
|
||||||
@@ -2270,7 +2361,7 @@ void SendFilesBox::requestToTakeTextWithTags() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto text = _caption->getTextWithTags();
|
const auto text = _caption->getTextWithTags();
|
||||||
if (!_prefilledCaptionText.text.isEmpty() && text.text.isEmpty()) {
|
if (text.text.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_textTaken = true;
|
_textTaken = true;
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ using SendFilesCheck = Fn<bool(
|
|||||||
[[nodiscard]] SendFilesCheck DefaultCheckForPeer(
|
[[nodiscard]] SendFilesCheck DefaultCheckForPeer(
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
not_null<PeerData*> peer);
|
not_null<PeerData*> peer);
|
||||||
|
void RenameFileBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
const QString ¤tName,
|
||||||
|
bool allowExtensionEdit,
|
||||||
|
Fn<void(QString)> apply);
|
||||||
|
|
||||||
using SendFilesConfirmed = Fn<void(
|
using SendFilesConfirmed = Fn<void(
|
||||||
std::shared_ptr<Ui::PreparedBundle>,
|
std::shared_ptr<Ui::PreparedBundle>,
|
||||||
@@ -178,6 +183,7 @@ private:
|
|||||||
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
|
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
|
||||||
[[nodiscard]] rpl::producer<int> itemReplaceRequest() const;
|
[[nodiscard]] rpl::producer<int> itemReplaceRequest() const;
|
||||||
[[nodiscard]] rpl::producer<int> itemModifyRequest() const;
|
[[nodiscard]] rpl::producer<int> itemModifyRequest() const;
|
||||||
|
[[nodiscard]] rpl::producer<int> itemRenameRequest() const;
|
||||||
[[nodiscard]] rpl::producer<> orderUpdated() const;
|
[[nodiscard]] rpl::producer<> orderUpdated() const;
|
||||||
|
|
||||||
void setSendWay(Ui::SendFilesWay way);
|
void setSendWay(Ui::SendFilesWay way);
|
||||||
@@ -219,9 +225,10 @@ private:
|
|||||||
void setSendLargePhotos(bool enabled);
|
void setSendLargePhotos(bool enabled);
|
||||||
void changePrice();
|
void changePrice();
|
||||||
|
|
||||||
[[nodiscard]] bool canChangePrice() const;
|
|
||||||
[[nodiscard]] bool hasPrice() const;
|
[[nodiscard]] bool hasPrice() const;
|
||||||
[[nodiscard]] bool hasSendLargePhotosOption() const;
|
[[nodiscard]] bool hasSendLargePhotosOption() const;
|
||||||
|
[[nodiscard]] bool canMoveCaptionInCurrentSendWay() const;
|
||||||
|
[[nodiscard]] bool canChangePrice() const;
|
||||||
void refreshPriceTag();
|
void refreshPriceTag();
|
||||||
[[nodiscard]] QImage preparePriceTagBg(QSize size) const;
|
[[nodiscard]] QImage preparePriceTagBg(QSize size) const;
|
||||||
|
|
||||||
@@ -250,7 +257,10 @@ private:
|
|||||||
void updateControlsGeometry();
|
void updateControlsGeometry();
|
||||||
void updateCaptionVisibility();
|
void updateCaptionVisibility();
|
||||||
|
|
||||||
bool addFiles(not_null<const QMimeData*> data);
|
bool addFiles(
|
||||||
|
not_null<const QMimeData*> data,
|
||||||
|
std::optional<bool> overrideSendImagesAsPhotos = std::nullopt);
|
||||||
|
void applySendImagesAsPhotosOverride(const Ui::PreparedList &list);
|
||||||
bool addFiles(Ui::PreparedList list);
|
bool addFiles(Ui::PreparedList list);
|
||||||
void addFile(Ui::PreparedFile &&file);
|
void addFile(Ui::PreparedFile &&file);
|
||||||
void pushBlock(int from, int till);
|
void pushBlock(int from, int till);
|
||||||
|
|||||||
@@ -138,11 +138,9 @@ namespace {
|
|||||||
constexpr auto kPriceTabAll = 0;
|
constexpr auto kPriceTabAll = 0;
|
||||||
constexpr auto kPriceTabMy = -1;
|
constexpr auto kPriceTabMy = -1;
|
||||||
constexpr auto kPriceTabCollectibles = -2;
|
constexpr auto kPriceTabCollectibles = -2;
|
||||||
constexpr auto kGiftMessageLimit = 255;
|
|
||||||
constexpr auto kSentToastDuration = 3 * crl::time(1000);
|
constexpr auto kSentToastDuration = 3 * crl::time(1000);
|
||||||
constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
|
constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
|
||||||
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
|
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
|
||||||
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
|
|
||||||
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
|
constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000);
|
||||||
|
|
||||||
using namespace HistoryView;
|
using namespace HistoryView;
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ void OpenPhotoEditorForSticker(
|
|||||||
Editor::EditorData{
|
Editor::EditorData{
|
||||||
.exactSize = QSize(kStickerSide, kStickerSide),
|
.exactSize = QSize(kStickerSide, kStickerSide),
|
||||||
.cropType = Editor::EditorData::CropType::RoundedRect,
|
.cropType = Editor::EditorData::CropType::RoundedRect,
|
||||||
|
.cropMode = Editor::EditorData::CropMode::Mask,
|
||||||
.keepAspectRatio = true,
|
.keepAspectRatio = true,
|
||||||
.fixedCrop = true,
|
.fixedCrop = true,
|
||||||
});
|
});
|
||||||
@@ -163,6 +164,7 @@ void OpenPhotoEditorForSticker(
|
|||||||
Qt::IgnoreAspectRatio,
|
Qt::IgnoreAspectRatio,
|
||||||
Qt::SmoothTransformation);
|
Qt::SmoothTransformation);
|
||||||
}
|
}
|
||||||
|
Editor::ApplyShapeMask(result, mods);
|
||||||
done(std::move(result));
|
done(std::move(result));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -943,10 +943,12 @@ void StickerSetBox::updateButtons() {
|
|||||||
raw->setForcedOrigin(
|
raw->setForcedOrigin(
|
||||||
Ui::PanelAnimation::Origin::TopRight);
|
Ui::PanelAnimation::Origin::TopRight);
|
||||||
top->setForceRippled(true);
|
top->setForceRippled(true);
|
||||||
raw->setDestroyedCallback([=] {
|
raw->setDestroyedCallback([top] {
|
||||||
if (const auto strong = top.data()) {
|
crl::on_main(top, [top] {
|
||||||
strong->setForceRippled(false);
|
if (const auto strong = top.data()) {
|
||||||
}
|
strong->setForceRippled(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
raw->popup(top->mapToGlobal(QPoint(
|
raw->popup(top->mapToGlobal(QPoint(
|
||||||
top->width(),
|
top->width(),
|
||||||
@@ -1016,10 +1018,12 @@ void StickerSetBox::updateButtons() {
|
|||||||
raw->setForcedOrigin(
|
raw->setForcedOrigin(
|
||||||
Ui::PanelAnimation::Origin::TopRight);
|
Ui::PanelAnimation::Origin::TopRight);
|
||||||
top->setForceRippled(true);
|
top->setForceRippled(true);
|
||||||
raw->setDestroyedCallback([=] {
|
raw->setDestroyedCallback([top] {
|
||||||
if (const auto strong = top.data()) {
|
crl::on_main(top, [top] {
|
||||||
strong->setForceRippled(false);
|
if (const auto strong = top.data()) {
|
||||||
}
|
strong->setForceRippled(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
raw->popup(top->mapToGlobal(QPoint(
|
raw->popup(top->mapToGlobal(QPoint(
|
||||||
top->width(),
|
top->width(),
|
||||||
@@ -2465,8 +2469,9 @@ void StickerSetBox::Inner::paintAddCell(QPainter &p) const {
|
|||||||
ltrRect.width(),
|
ltrRect.width(),
|
||||||
ltrRect.height())
|
ltrRect.height())
|
||||||
: ltrRect;
|
: ltrRect;
|
||||||
|
const auto center = rect::center(rect);
|
||||||
const auto inner = QRect(
|
const auto inner = QRect(
|
||||||
rect::center(rect) - QPoint(
|
center - QPoint(
|
||||||
st::stickersAddCellBgRadius,
|
st::stickersAddCellBgRadius,
|
||||||
st::stickersAddCellBgRadius),
|
st::stickersAddCellBgRadius),
|
||||||
Size(st::stickersAddCellBgRadius * 2));
|
Size(st::stickersAddCellBgRadius * 2));
|
||||||
@@ -2482,7 +2487,6 @@ void StickerSetBox::Inner::paintAddCell(QPainter &p) const {
|
|||||||
|
|
||||||
const auto plusHalf = st::stickersAddCellPlusSize / 2;
|
const auto plusHalf = st::stickersAddCellPlusSize / 2;
|
||||||
const auto thickness = st::stickersAddCellPlusThickness;
|
const auto thickness = st::stickersAddCellPlusThickness;
|
||||||
const auto center = rect.center();
|
|
||||||
const auto plusH = QRectF(
|
const auto plusH = QRectF(
|
||||||
center.x() - plusHalf,
|
center.x() - plusHalf,
|
||||||
center.y() - thickness / 2.,
|
center.y() - thickness / 2.,
|
||||||
|
|||||||
@@ -548,10 +548,6 @@ callTitle: WindowTitle(defaultWindowTitle) {
|
|||||||
callTitleShadowRight: icon {{ "calls/calls_shadow_controls", windowShadowFg }};
|
callTitleShadowRight: icon {{ "calls/calls_shadow_controls", windowShadowFg }};
|
||||||
callTitleShadowLeft: icon {{ "calls/calls_shadow_controls-flip_horizontal", windowShadowFg }};
|
callTitleShadowLeft: icon {{ "calls/calls_shadow_controls-flip_horizontal", windowShadowFg }};
|
||||||
|
|
||||||
callErrorToast: Toast(defaultToast) {
|
|
||||||
minWidth: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
groupCallWidth: 380px;
|
groupCallWidth: 380px;
|
||||||
groupCallHeight: 520px;
|
groupCallHeight: 520px;
|
||||||
groupCallWidthRtmp: 720px;
|
groupCallWidthRtmp: 720px;
|
||||||
@@ -1508,8 +1504,6 @@ groupCallAttentionBoxButton: RoundButton(groupCallBoxButton) {
|
|||||||
groupCallRtmpUrlSkip: 1px;
|
groupCallRtmpUrlSkip: 1px;
|
||||||
groupCallRtmpKeySubsectionTitleSkip: 8px;
|
groupCallRtmpKeySubsectionTitleSkip: 8px;
|
||||||
groupCallRtmpSubsectionTitleAddPadding: margins(0px, -1px, 0px, -4px);
|
groupCallRtmpSubsectionTitleAddPadding: margins(0px, -1px, 0px, -4px);
|
||||||
groupCallRtmpShowButtonPosition: point(21px, -5px);
|
|
||||||
groupCallDividerBg: groupCallMembersBgRipple;
|
|
||||||
|
|
||||||
groupCallScheduleDateField: InputField(groupCallField) {
|
groupCallScheduleDateField: InputField(groupCallField) {
|
||||||
textMargins: margins(2px, 0px, 2px, 0px);
|
textMargins: margins(2px, 0px, 2px, 0px);
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ public:
|
|||||||
bool selected) override {
|
bool selected) override {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
int paintNameIconGetLeadingWidth(
|
||||||
|
Painter &p,
|
||||||
|
Fn<void()> repaint,
|
||||||
|
crl::time now,
|
||||||
|
int nameLeft,
|
||||||
|
int nameTop,
|
||||||
|
int outerWidth,
|
||||||
|
bool selected) override {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
QSize rightActionSize() const override {
|
QSize rightActionSize() const override {
|
||||||
return peer()->isChannel() ? QSize(_st.width, _st.height) : QSize();
|
return peer()->isChannel() ? QSize(_st.width, _st.height) : QSize();
|
||||||
}
|
}
|
||||||
@@ -330,6 +340,16 @@ public:
|
|||||||
bool selected) override {
|
bool selected) override {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
int paintNameIconGetLeadingWidth(
|
||||||
|
Painter &p,
|
||||||
|
Fn<void()> repaint,
|
||||||
|
crl::time now,
|
||||||
|
int nameLeft,
|
||||||
|
int nameTop,
|
||||||
|
int outerWidth,
|
||||||
|
bool selected) override {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
QSize rightActionSize() const override {
|
QSize rightActionSize() const override {
|
||||||
return peer()->isUser() ? QSize(_st->width, _st->height) : QSize();
|
return peer()->isUser() ? QSize(_st->width, _st->height) : QSize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,23 @@ EmojiPan {
|
|||||||
tabs: SettingsSlider;
|
tabs: SettingsSlider;
|
||||||
search: TabbedSearch;
|
search: TabbedSearch;
|
||||||
searchMargin: margins;
|
searchMargin: margins;
|
||||||
|
searchPacksTop: pixels;
|
||||||
|
searchPackWidth: pixels;
|
||||||
|
searchPackHeight: pixels;
|
||||||
|
searchPackSkip: pixels;
|
||||||
|
searchPackIconSize: pixels;
|
||||||
|
searchPackIconTop: pixels;
|
||||||
|
searchPackTextTop: pixels;
|
||||||
|
searchPackTextPadding: pixels;
|
||||||
|
searchPacksBottom: pixels;
|
||||||
|
searchResultsHeight: pixels;
|
||||||
|
searchResultsTextTop: pixels;
|
||||||
|
searchSwapDuration: int;
|
||||||
|
searchBackHeight: pixels;
|
||||||
|
searchBackIconLeft: pixels;
|
||||||
|
searchBackIconTop: pixels;
|
||||||
|
searchBackTextLeft: pixels;
|
||||||
|
searchBackTextTop: pixels;
|
||||||
colorAll: IconButton;
|
colorAll: IconButton;
|
||||||
colorAllLabel: FlatLabel;
|
colorAllLabel: FlatLabel;
|
||||||
removeSet: IconButton;
|
removeSet: IconButton;
|
||||||
@@ -430,7 +447,6 @@ stickersAddCellBgRadius: 28px;
|
|||||||
|
|
||||||
stickersEmojiPickerExpandedRadius: 20px;
|
stickersEmojiPickerExpandedRadius: 20px;
|
||||||
stickersEmojiPickerBg: emojiPanBg;
|
stickersEmojiPickerBg: emojiPanBg;
|
||||||
stickersEmojiPickerShadow: windowShadowFg;
|
|
||||||
stickersEmojiPickerPadding: margins(12px, 8px, 12px, 0px);
|
stickersEmojiPickerPadding: margins(12px, 8px, 12px, 0px);
|
||||||
stickersEmojiPickerItemSize: 30px;
|
stickersEmojiPickerItemSize: 30px;
|
||||||
stickersEmojiPickerItemSkip: 4px;
|
stickersEmojiPickerItemSkip: 4px;
|
||||||
@@ -442,8 +458,6 @@ stickersEmojiPickerStripBubble: icon{
|
|||||||
};
|
};
|
||||||
stickersEmojiPickerStripBubbleRight: 20px;
|
stickersEmojiPickerStripBubbleRight: 20px;
|
||||||
stickersEmojiPickerSelectedBg: windowBgActive;
|
stickersEmojiPickerSelectedBg: windowBgActive;
|
||||||
stickersEmojiPickerSelectedFg: windowBgActive;
|
|
||||||
stickersEmojiPickerHeaderFg: windowSubTextFg;
|
|
||||||
stickersEmojiPickerScroll: ScrollArea(boxScroll) {
|
stickersEmojiPickerScroll: ScrollArea(boxScroll) {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
deltax: 5px;
|
deltax: 5px;
|
||||||
@@ -458,14 +472,6 @@ stickersEmojiPickerAbout: FlatLabel(defaultFlatLabel) {
|
|||||||
font: font(12px);
|
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 }};
|
stickersEmojiPickerExpandIcon: icon {{ "intro_country_dropdown", windowSubTextFg }};
|
||||||
stickersEmojiPickerCollapseIcon: icon {{ "intro_country_dropdown-flip_vertical", windowSubTextFg }};
|
stickersEmojiPickerCollapseIcon: icon {{ "intro_country_dropdown-flip_vertical", windowSubTextFg }};
|
||||||
stickersEmojiPickerExpandSize: 24px;
|
stickersEmojiPickerExpandSize: 24px;
|
||||||
@@ -603,9 +609,18 @@ stickerPanRemoveSet: IconButton(hashtagClose) {
|
|||||||
iconPosition: point(-1px, -1px);
|
iconPosition: point(-1px, -1px);
|
||||||
rippleAreaPosition: point(0px, 0px);
|
rippleAreaPosition: point(0px, 0px);
|
||||||
}
|
}
|
||||||
|
whoReadClose: IconButton(stickerPanRemoveSet) {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
icon: smallCloseIconOver;
|
||||||
|
rippleAreaSize: 20px;
|
||||||
|
}
|
||||||
|
whoReadCloseVisibleRadius: 7px;
|
||||||
|
whoReadCloseBlurPadding: 5px;
|
||||||
stickerIconMove: 400;
|
stickerIconMove: 400;
|
||||||
stickerPreviewDuration: 150;
|
stickerPreviewDuration: 150;
|
||||||
stickerPreviewMin: 0.1;
|
stickerPreviewMin: 0.1;
|
||||||
|
stickerPanFirstAfterShortcutsSkip: 4px;
|
||||||
mediaPreviewPhotoSkip: 48px;
|
mediaPreviewPhotoSkip: 48px;
|
||||||
|
|
||||||
emojiPanColorAll: IconButton(stickerPanRemoveSet) {
|
emojiPanColorAll: IconButton(stickerPanRemoveSet) {
|
||||||
@@ -769,6 +784,23 @@ defaultEmojiPan: EmojiPan {
|
|||||||
tabs: emojiTabs;
|
tabs: emojiTabs;
|
||||||
search: defaultTabbedSearch;
|
search: defaultTabbedSearch;
|
||||||
searchMargin: margins(1px, 11px, 2px, 5px);
|
searchMargin: margins(1px, 11px, 2px, 5px);
|
||||||
|
searchPacksTop: 0px;
|
||||||
|
searchPackWidth: 70px;
|
||||||
|
searchPackHeight: 78px;
|
||||||
|
searchPackSkip: 6px;
|
||||||
|
searchPackIconSize: 42px;
|
||||||
|
searchPackIconTop: 8px;
|
||||||
|
searchPackTextTop: 55px;
|
||||||
|
searchPackTextPadding: 5px;
|
||||||
|
searchPacksBottom: 0px;
|
||||||
|
searchResultsHeight: 20px;
|
||||||
|
searchResultsTextTop: 3px;
|
||||||
|
searchSwapDuration: 100;
|
||||||
|
searchBackHeight: 36px;
|
||||||
|
searchBackIconLeft: 5px;
|
||||||
|
searchBackIconTop: 6px;
|
||||||
|
searchBackTextLeft: 40px;
|
||||||
|
searchBackTextTop: 10px;
|
||||||
colorAll: emojiPanColorAll;
|
colorAll: emojiPanColorAll;
|
||||||
colorAllLabel: emojiPanColorAllLabel;
|
colorAllLabel: emojiPanColorAllLabel;
|
||||||
removeSet: stickerPanRemoveSet;
|
removeSet: stickerPanRemoveSet;
|
||||||
@@ -976,10 +1008,6 @@ historyComposeButton: FlatButton {
|
|||||||
color: historyComposeButtonBgRipple;
|
color: historyComposeButtonBgRipple;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
historyComposeButtonText: FlatLabel(defaultFlatLabel) {
|
|
||||||
style: semiboldTextStyle;
|
|
||||||
textFg: windowActiveTextFg;
|
|
||||||
}
|
|
||||||
historyGiftToChannel: IconButton(defaultIconButton) {
|
historyGiftToChannel: IconButton(defaultIconButton) {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
@@ -1258,7 +1286,6 @@ historyAddMedia: IconButton(historyAttach) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }};
|
historyAttachEmojiActive: icon {{ "chat/input_smile_face", windowBgActive }};
|
||||||
historyEmojiCircle: size(20px, 20px);
|
|
||||||
historyEmojiCircleLine: 1.5;
|
historyEmojiCircleLine: 1.5;
|
||||||
historyEmojiCircleFg: historyComposeIconFg;
|
historyEmojiCircleFg: historyComposeIconFg;
|
||||||
historyEmojiCircleFgOver: historyComposeIconFgOver;
|
historyEmojiCircleFgOver: historyComposeIconFgOver;
|
||||||
@@ -1295,25 +1322,6 @@ historySuggestPostToggle: IconButton(historyAttach) {
|
|||||||
historySuggestIconPosition: point(4px, 4px);
|
historySuggestIconPosition: point(4px, 4px);
|
||||||
historySuggestIconActive: icon{{ "chat/input_paid", windowActiveTextFg }};
|
historySuggestIconActive: icon{{ "chat/input_paid", windowActiveTextFg }};
|
||||||
|
|
||||||
suggestOptionsPrice: InputField(defaultInputField) {
|
|
||||||
textBg: transparent;
|
|
||||||
textMargins: margins(2px, 20px, 2px, 0px);
|
|
||||||
|
|
||||||
placeholderFg: placeholderFg;
|
|
||||||
placeholderFgActive: placeholderFgActive;
|
|
||||||
placeholderFgError: placeholderFgActive;
|
|
||||||
placeholderMargins: margins(2px, 0px, 2px, 0px);
|
|
||||||
placeholderScale: 0.;
|
|
||||||
placeholderFont: normalFont;
|
|
||||||
|
|
||||||
border: 0px;
|
|
||||||
borderActive: 0px;
|
|
||||||
|
|
||||||
heightMin: 32px;
|
|
||||||
|
|
||||||
style: defaultTextStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
historyAttachEmojiInner: IconButton(historyAttach) {
|
historyAttachEmojiInner: IconButton(historyAttach) {
|
||||||
icon: icon {{ "chat/input_smile_face", historyComposeIconFg }};
|
icon: icon {{ "chat/input_smile_face", historyComposeIconFg }};
|
||||||
iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }};
|
iconOver: icon {{ "chat/input_smile_face", historyComposeIconFgOver }};
|
||||||
@@ -1365,9 +1373,7 @@ historyRecordVoiceFgInactive: attentionButtonFg;
|
|||||||
historyRecordVoiceFgActive: windowBgActive;
|
historyRecordVoiceFgActive: windowBgActive;
|
||||||
historyRecordVoiceFgActiveIcon: windowFgActive;
|
historyRecordVoiceFgActiveIcon: windowFgActive;
|
||||||
historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }};
|
historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }};
|
||||||
historyRecordVoiceOnceBgOver: icon {{ "voice_lock/audio_once_bg", historySendIconFgOver }};
|
|
||||||
historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }};
|
historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }};
|
||||||
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
|
|
||||||
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
|
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
|
||||||
historyRecordSendIconPosition: point(2px, 0px);
|
historyRecordSendIconPosition: point(2px, 0px);
|
||||||
historyRecordVoiceRippleBgActive: lightButtonBgOver;
|
historyRecordVoiceRippleBgActive: lightButtonBgOver;
|
||||||
@@ -1377,7 +1383,6 @@ historyRecordCancelActive: historySendIconFg;
|
|||||||
historyRecordFont: font(13px);
|
historyRecordFont: font(13px);
|
||||||
historyRecordDurationSkip: 12px;
|
historyRecordDurationSkip: 12px;
|
||||||
historyRecordDurationFg: historyComposeAreaFg;
|
historyRecordDurationFg: historyComposeAreaFg;
|
||||||
historyRecordTTLLineWidth: 2px;
|
|
||||||
|
|
||||||
historyRecordMainBlobMinRadius: 23px;
|
historyRecordMainBlobMinRadius: 23px;
|
||||||
historyRecordMainBlobMaxRadius: 37px;
|
historyRecordMainBlobMaxRadius: 37px;
|
||||||
@@ -1422,7 +1427,6 @@ historyRecordDelete: IconButton(historyAttach) {
|
|||||||
iconOver: icon {{ "voice_lock/recorded_delete", historyComposeIconFgOver }};
|
iconOver: icon {{ "voice_lock/recorded_delete", historyComposeIconFgOver }};
|
||||||
iconPosition: point(10px, 11px);
|
iconPosition: point(10px, 11px);
|
||||||
}
|
}
|
||||||
historyRecordWaveformRightSkip: 10px;
|
|
||||||
historyRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px);
|
historyRecordWaveformBgMargins: margins(5px, 8px, 5px, 9px);
|
||||||
historyRecordWaveformBgRadius: 7px;
|
historyRecordWaveformBgRadius: 7px;
|
||||||
historyRecordWaveformOutsideAlpha: 0.6;
|
historyRecordWaveformOutsideAlpha: 0.6;
|
||||||
@@ -1434,16 +1438,12 @@ historyRecordCenterControlTextSkip: 2px;
|
|||||||
historyRecordCenterControlMinimumProgressPadding: 5px;
|
historyRecordCenterControlMinimumProgressPadding: 5px;
|
||||||
|
|
||||||
historyRecordWaveformBar: 3px;
|
historyRecordWaveformBar: 3px;
|
||||||
historyRecordTrimFrameRadius: 5px;
|
|
||||||
historyRecordTrimFrameBorder: 1px;
|
|
||||||
historyRecordTrimHandleWidth: 10px;
|
historyRecordTrimHandleWidth: 10px;
|
||||||
historyRecordTrimHandleInnerSize: size(2px, 8px);
|
historyRecordTrimHandleInnerSize: size(2px, 8px);
|
||||||
historyRecordTrimHandleInnerSkip: 2px;
|
|
||||||
|
|
||||||
historyRecordLockPosition: point(1px, 22px);
|
historyRecordLockPosition: point(1px, 22px);
|
||||||
|
|
||||||
historyRecordCancelButtonWidth: 100px;
|
historyRecordCancelButtonWidth: 100px;
|
||||||
historyRecordCancelButtonFg: lightButtonFg;
|
|
||||||
|
|
||||||
historyRecordTooltipSkip: 8px;
|
historyRecordTooltipSkip: 8px;
|
||||||
historyRecordTooltip: ImportantTooltip(defaultImportantTooltip) {
|
historyRecordTooltip: ImportantTooltip(defaultImportantTooltip) {
|
||||||
@@ -1522,7 +1522,6 @@ importantTooltipHide: IconButton(defaultIconButton) {
|
|||||||
ripple: emptyRippleAnimation;
|
ripple: emptyRippleAnimation;
|
||||||
}
|
}
|
||||||
boxAiComposeButtonPosition: point(0px, -4px);
|
boxAiComposeButtonPosition: point(0px, -4px);
|
||||||
historyRecordFrameIndex: 30;
|
|
||||||
|
|
||||||
defaultComposeFilesMenu: IconButton(defaultIconButton) {
|
defaultComposeFilesMenu: IconButton(defaultIconButton) {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
#include "ui/controls/tabbed_search.h"
|
#include "ui/controls/tabbed_search.h"
|
||||||
#include "ui/text/format_values.h"
|
#include "ui/text/format_values.h"
|
||||||
|
#include "ui/text/text_entity.h"
|
||||||
#include "ui/effects/animations.h"
|
#include "ui/effects/animations.h"
|
||||||
#include "ui/widgets/menu/menu_add_action_callback.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_add_action_callback_factory.h"
|
||||||
@@ -652,6 +653,11 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
_searchResults.clear();
|
_searchResults.clear();
|
||||||
_searchCustomIds.clear();
|
_searchCustomIds.clear();
|
||||||
_searchSets.clear();
|
_searchSets.clear();
|
||||||
|
_searchShortcutSets.clear();
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsScrollMax = 0;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
}
|
}
|
||||||
resizeToWidth(width());
|
resizeToWidth(width());
|
||||||
_recentShownCount = searching
|
_recentShownCount = searching
|
||||||
@@ -668,6 +674,9 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
finish(false);
|
finish(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
const auto guard = gsl::finally([&] { finish(); });
|
const auto guard = gsl::finally([&] { finish(); });
|
||||||
auto plain = collectPlainSearchResults();
|
auto plain = collectPlainSearchResults();
|
||||||
_searchEmoticon = QString();
|
_searchEmoticon = QString();
|
||||||
@@ -681,6 +690,7 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
_searchResults.clear();
|
_searchResults.clear();
|
||||||
_searchCustomIds.clear();
|
_searchCustomIds.clear();
|
||||||
_searchSets.clear();
|
_searchSets.clear();
|
||||||
|
_searchShortcutSets.clear();
|
||||||
if (_mode == Mode::Full) {
|
if (_mode == Mode::Full) {
|
||||||
for (const auto emoji : plain) {
|
for (const auto emoji : plain) {
|
||||||
_searchResults.push_back({
|
_searchResults.push_back({
|
||||||
@@ -708,6 +718,7 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
}
|
}
|
||||||
_searchNextRequestQuery = _searchQueryText;
|
_searchNextRequestQuery = _searchQueryText;
|
||||||
_searchRequestQuery = _searchQueryText;
|
_searchRequestQuery = _searchQueryText;
|
||||||
|
refreshSearchShortcuts();
|
||||||
const auto cloudCached = _searchCloudCache.find(_searchRequestQuery)
|
const auto cloudCached = _searchCloudCache.find(_searchRequestQuery)
|
||||||
!= _searchCloudCache.cend();
|
!= _searchCloudCache.cend();
|
||||||
const auto setsCached = _searchSetsCache.find(_searchRequestQuery)
|
const auto setsCached = _searchSetsCache.find(_searchRequestQuery)
|
||||||
@@ -715,7 +726,6 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
if (cloudCached || setsCached) {
|
if (cloudCached || setsCached) {
|
||||||
_searchRequestTimer.cancel();
|
_searchRequestTimer.cancel();
|
||||||
fillCloudSearchResults();
|
fillCloudSearchResults();
|
||||||
fillCloudSearchSets();
|
|
||||||
if (!cloudCached || !setsCached) {
|
if (!cloudCached || !setsCached) {
|
||||||
sendSearchRequest();
|
sendSearchRequest();
|
||||||
}
|
}
|
||||||
@@ -916,6 +926,11 @@ void EmojiListWidget::cancelSearchRequest() {
|
|||||||
_searchCloudNextOffset.clear();
|
_searchCloudNextOffset.clear();
|
||||||
_searchSetsCache.clear();
|
_searchSetsCache.clear();
|
||||||
_searchSets.clear();
|
_searchSets.clear();
|
||||||
|
_searchShortcutSets.clear();
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsScrollMax = 0;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmojiListWidget::searchCloudResultsDone(
|
void EmojiListWidget::searchCloudResultsDone(
|
||||||
@@ -1059,21 +1074,36 @@ void EmojiListWidget::showSearchResults() {
|
|||||||
_searchResults.clear();
|
_searchResults.clear();
|
||||||
_searchCustomIds.clear();
|
_searchCustomIds.clear();
|
||||||
_searchSets.clear();
|
_searchSets.clear();
|
||||||
|
auto wasShortcuts = base::take(_searchShortcutSets);
|
||||||
_searchEmoji.clear();
|
_searchEmoji.clear();
|
||||||
|
|
||||||
auto plain = collectPlainSearchResults();
|
refreshSearchShortcuts();
|
||||||
if (_mode == Mode::Full) {
|
for (auto &set : _searchShortcutSets) {
|
||||||
for (const auto emoji : plain) {
|
const auto i = ranges::find(
|
||||||
_searchResults.push_back({
|
wasShortcuts,
|
||||||
.id = { emoji },
|
set.id,
|
||||||
});
|
&CustomSet::id);
|
||||||
|
if (i != wasShortcuts.end() && i->ripple) {
|
||||||
|
set.ripple = std::move(i->ripple);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_mode != Mode::Full || session().premium()) {
|
if (searchShortcutSelected()) {
|
||||||
appendPremiumSearchResults();
|
fillSelectedSearchShortcut();
|
||||||
|
}
|
||||||
|
if (!searchShortcutSelected()) {
|
||||||
|
auto plain = collectPlainSearchResults();
|
||||||
|
if (_mode == Mode::Full) {
|
||||||
|
for (const auto emoji : plain) {
|
||||||
|
_searchResults.push_back({
|
||||||
|
.id = { emoji },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_mode != Mode::Full || session().premium()) {
|
||||||
|
appendPremiumSearchResults();
|
||||||
|
}
|
||||||
|
fillCloudSearchResults();
|
||||||
}
|
}
|
||||||
fillCloudSearchResults();
|
|
||||||
fillCloudSearchSets();
|
|
||||||
|
|
||||||
resizeToWidth(width());
|
resizeToWidth(width());
|
||||||
_recentShownCount = _searchResults.size();
|
_recentShownCount = _searchResults.size();
|
||||||
@@ -1107,55 +1137,268 @@ void EmojiListWidget::fillCloudSearchResults() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmojiListWidget::fillCloudSearchSets() {
|
void EmojiListWidget::refreshSearchShortcuts() {
|
||||||
|
fillLocalSearchShortcuts(_searchQueryText);
|
||||||
const auto it = _searchSetsCache.find(_searchRequestQuery);
|
const auto it = _searchSetsCache.find(_searchRequestQuery);
|
||||||
if (it == _searchSetsCache.cend() || it->second.empty()) {
|
if (it != _searchSetsCache.cend()) {
|
||||||
return;
|
const auto &sets = session().data().stickers().sets();
|
||||||
}
|
for (const auto setId : it->second) {
|
||||||
const auto &sets = session().data().stickers().sets();
|
if (const auto setIt = sets.find(setId); setIt != sets.end()) {
|
||||||
for (const auto setId : it->second) {
|
addSearchShortcut(setIt->second.get());
|
||||||
const auto setIt = sets.find(setId);
|
|
||||||
if (setIt == sets.end()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto set = setIt->second.get();
|
|
||||||
const auto &list = set->stickers.empty()
|
|
||||||
? set->covers
|
|
||||||
: set->stickers;
|
|
||||||
if (list.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
auto customs = std::vector<CustomOne>();
|
|
||||||
customs.reserve(list.size());
|
|
||||||
for (const auto document : list) {
|
|
||||||
if (const auto sticker = document->sticker()) {
|
|
||||||
const auto statusId = EmojiStatusId{ document->id };
|
|
||||||
customs.push_back({
|
|
||||||
.custom = resolveCustomEmoji(
|
|
||||||
statusId,
|
|
||||||
document,
|
|
||||||
setId),
|
|
||||||
.document = document,
|
|
||||||
.emoji = Ui::Emoji::Find(sticker->alt),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (customs.empty()) {
|
}
|
||||||
|
if (_searchSelectedSetId
|
||||||
|
&& !ranges::contains(
|
||||||
|
_searchShortcutSets,
|
||||||
|
_searchSelectedSetId,
|
||||||
|
&CustomSet::id)) {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
}
|
||||||
|
refreshSearchShortcutsScroll(width());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::fillLocalSearchShortcuts(const QString &query) {
|
||||||
|
const auto searchWordsList = TextUtilities::PrepareSearchWords(query);
|
||||||
|
if (searchWordsList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto &set : _custom) {
|
||||||
|
if (!set.canRemove) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto installed = !!(set->flags
|
const auto words = TextUtilities::PrepareSearchWords(
|
||||||
& Data::StickersSetFlag::Installed);
|
set.title + ' ' + set.set->shortName);
|
||||||
_searchSets.push_back({
|
if (MatchAllPreparedSearchWords(words, searchWordsList)) {
|
||||||
.id = setId,
|
addSearchShortcut(set.set);
|
||||||
.set = set,
|
}
|
||||||
.thumbnailDocument = set->lookupThumbnailDocument(),
|
|
||||||
.title = set->title,
|
|
||||||
.list = std::move(customs),
|
|
||||||
.canRemove = installed,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EmojiListWidget::addSearchShortcut(not_null<Data::StickersSet*> set) {
|
||||||
|
if (ranges::contains(_searchShortcutSets, set->id, &CustomSet::id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto &documents = set->stickers.empty()
|
||||||
|
? set->covers
|
||||||
|
: set->stickers;
|
||||||
|
auto list = std::vector<CustomOne>();
|
||||||
|
for (const auto document : documents) {
|
||||||
|
if (const auto sticker = document->sticker()) {
|
||||||
|
list.push_back({
|
||||||
|
.custom = resolveCustomEmoji(
|
||||||
|
EmojiStatusId{ document->id },
|
||||||
|
document,
|
||||||
|
set->id),
|
||||||
|
.document = document,
|
||||||
|
.emoji = Ui::Emoji::Find(sticker->alt),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (list.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto installed = !!(set->flags & Data::StickersSetFlag::Installed);
|
||||||
|
_searchShortcutSets.push_back({
|
||||||
|
.id = set->id,
|
||||||
|
.set = set,
|
||||||
|
.thumbnailDocument = set->lookupThumbnailDocument(),
|
||||||
|
.title = set->title,
|
||||||
|
.list = std::move(list),
|
||||||
|
.canRemove = installed,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<EmojiListWidget::CustomOne> EmojiListWidget::collectSearchSet(
|
||||||
|
not_null<Data::StickersSet*> set) {
|
||||||
|
const auto &documents = set->stickers.empty()
|
||||||
|
? set->covers
|
||||||
|
: set->stickers;
|
||||||
|
auto result = std::vector<CustomOne>();
|
||||||
|
result.reserve(documents.size());
|
||||||
|
for (const auto document : documents) {
|
||||||
|
if (const auto sticker = document->sticker()) {
|
||||||
|
const auto statusId = EmojiStatusId{ document->id };
|
||||||
|
result.push_back({
|
||||||
|
.custom = resolveCustomEmoji(
|
||||||
|
statusId,
|
||||||
|
document,
|
||||||
|
set->id),
|
||||||
|
.document = document,
|
||||||
|
.emoji = Ui::Emoji::Find(sticker->alt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::fillSelectedSearchShortcut() {
|
||||||
|
const auto &sets = session().data().stickers().sets();
|
||||||
|
const auto it = sets.find(_searchSelectedSetId);
|
||||||
|
if (it == sets.end()) {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto set = it->second.get();
|
||||||
|
auto list = collectSearchSet(set);
|
||||||
|
if (list.empty()) {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto installed = !!(set->flags & Data::StickersSetFlag::Installed);
|
||||||
|
_searchSets.push_back({
|
||||||
|
.id = set->id,
|
||||||
|
.set = set,
|
||||||
|
.thumbnailDocument = set->lookupThumbnailDocument(),
|
||||||
|
.title = tr::lng_custom_emoji_count(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
set->count),
|
||||||
|
.list = std::move(list),
|
||||||
|
.canRemove = installed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmojiListWidget::searchShortcutsShown() const {
|
||||||
|
return _searchMode && !_searchShortcutSets.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmojiListWidget::searchShortcutSelected() const {
|
||||||
|
return _searchSelectedSetId != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::startSearchSwapAnimation(
|
||||||
|
Fn<void()> change,
|
||||||
|
bool packToPack) {
|
||||||
|
if (!isVisible() || size().isEmpty()) {
|
||||||
|
change();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto top = searchShortcutsTop()
|
||||||
|
+ (packToPack ? searchShortcutsHeight() : 0);
|
||||||
|
const auto computeRect = [&] {
|
||||||
|
const auto bottom = std::max(top + 1, getVisibleBottom());
|
||||||
|
return QRect(0, top, width(), bottom - top);
|
||||||
|
};
|
||||||
|
_searchSwapAnimation.stop();
|
||||||
|
const auto wasSelected = searchShortcutSelected();
|
||||||
|
_searchSwapBefore = Ui::GrabWidget(this, computeRect());
|
||||||
|
_searchSwapTop = top;
|
||||||
|
_searchSwapPartial = packToPack;
|
||||||
|
change();
|
||||||
|
_searchSwapReverse = wasSelected && !searchShortcutSelected();
|
||||||
|
_searchSwapAfter = Ui::GrabWidget(this, computeRect());
|
||||||
|
_searchSwapAnimation.start(
|
||||||
|
[=, this] {
|
||||||
|
update();
|
||||||
|
if (!_searchSwapAnimation.animating()) {
|
||||||
|
_searchSwapBefore = QPixmap();
|
||||||
|
_searchSwapAfter = QPixmap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
st().searchSwapDuration,
|
||||||
|
anim::sineInOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiListWidget::searchShortcutsTop() const {
|
||||||
|
return _search ? _search->height() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiListWidget::searchShortcutsHeight() const {
|
||||||
|
if (!searchShortcutsShown()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto result = st().searchPacksTop
|
||||||
|
+ st().searchPackHeight
|
||||||
|
+ st().searchPacksBottom;
|
||||||
|
result += searchShortcutSelected()
|
||||||
|
? st().searchBackHeight
|
||||||
|
: st().searchResultsHeight;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect EmojiListWidget::searchBackRect() const {
|
||||||
|
return QRect(
|
||||||
|
0,
|
||||||
|
searchShortcutsTop(),
|
||||||
|
width(),
|
||||||
|
searchShortcutSelected() ? st().searchBackHeight : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect EmojiListWidget::searchShortcutRect(int index) const {
|
||||||
|
Expects(index >= 0 && index < int(_searchShortcutSets.size()));
|
||||||
|
|
||||||
|
const auto left = st().headerLeft
|
||||||
|
- st().margin.left()
|
||||||
|
- _searchShortcutsScroll
|
||||||
|
+ index * (st().searchPackWidth + st().searchPackSkip);
|
||||||
|
const auto top = searchShortcutsTop()
|
||||||
|
+ (searchShortcutSelected() ? st().searchBackHeight : 0)
|
||||||
|
+ st().searchPacksTop;
|
||||||
|
return QRect(
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
st().searchPackWidth,
|
||||||
|
st().searchPackHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::refreshSearchShortcutsScroll(int newWidth) {
|
||||||
|
if (_searchShortcutSets.empty()) {
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsScrollMax = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto count = int(_searchShortcutSets.size());
|
||||||
|
const auto full = st().headerLeft
|
||||||
|
- st().margin.left()
|
||||||
|
+ count * st().searchPackWidth
|
||||||
|
+ std::max(count - 1, 0) * st().searchPackSkip
|
||||||
|
+ st().margin.right();
|
||||||
|
_searchShortcutsScrollMax = std::max(full - newWidth, 0);
|
||||||
|
scrollSearchShortcutsTo(_searchShortcutsScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::scrollSearchShortcutsTo(int value) {
|
||||||
|
const auto scroll = std::clamp(
|
||||||
|
value,
|
||||||
|
0,
|
||||||
|
_searchShortcutsScrollMax);
|
||||||
|
if (_searchShortcutsScroll == scroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_searchShortcutsScroll = scroll;
|
||||||
|
update(0, searchShortcutsTop(), width(), searchShortcutsHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::toggleSearchShortcut(int index) {
|
||||||
|
if (index < 0 || index >= int(_searchShortcutSets.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto setId = _searchShortcutSets[index].id;
|
||||||
|
const auto target = (_searchSelectedSetId == setId) ? 0 : setId;
|
||||||
|
const auto packToPack = _searchSelectedSetId
|
||||||
|
&& target
|
||||||
|
&& _searchSelectedSetId != target;
|
||||||
|
startSearchSwapAnimation([=, this] {
|
||||||
|
_searchSelectedSetId = target;
|
||||||
|
showSearchResults();
|
||||||
|
}, packToPack);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::backToSearchResults() {
|
||||||
|
if (!_searchSelectedSetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startSearchSwapAnimation([=, this] {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
showSearchResults();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
EmojiListWidget::CustomSet &EmojiListWidget::searchSetBySection(
|
EmojiListWidget::CustomSet &EmojiListWidget::searchSetBySection(
|
||||||
int section) {
|
int section) {
|
||||||
Expects(section > 0 && section <= int(_searchSets.size()));
|
Expects(section > 0 && section <= int(_searchSets.size()));
|
||||||
@@ -1186,6 +1429,12 @@ void EmojiListWidget::repaintCustom(uint64 setId) {
|
|||||||
if (repaintSearch) {
|
if (repaintSearch) {
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
|
for (auto i = 0, count = int(_searchShortcutSets.size());
|
||||||
|
i != count; ++i) {
|
||||||
|
if (_searchShortcutSets[i].id == setId) {
|
||||||
|
rtlupdate(searchShortcutRect(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
enumerateSections([&](const SectionInfo &info) {
|
enumerateSections([&](const SectionInfo &info) {
|
||||||
if (info.section > 0
|
if (info.section > 0
|
||||||
&& searchSetBySection(info.section).id == setId) {
|
&& searchSetBySection(info.section).id == setId) {
|
||||||
@@ -1389,12 +1638,19 @@ bool EmojiListWidget::enumerateSections(Callback callback) const {
|
|||||||
|
|
||||||
auto i = 0;
|
auto i = 0;
|
||||||
auto info = SectionInfo();
|
auto info = SectionInfo();
|
||||||
|
info.top = searchShortcutsHeight();
|
||||||
const auto next = [&] {
|
const auto next = [&] {
|
||||||
info.rowsCount = info.collapsed
|
info.rowsCount = info.collapsed
|
||||||
? kCollapsedRows
|
? kCollapsedRows
|
||||||
: (info.count + _columnCount - 1) / _columnCount;
|
: (info.count + _columnCount - 1) / _columnCount;
|
||||||
|
const auto firstAfterShortcuts = !i
|
||||||
|
&& searchShortcutsShown()
|
||||||
|
&& !searchShortcutSelected();
|
||||||
info.rowsTop = info.top
|
info.rowsTop = info.top
|
||||||
+ (i == 0 ? _rowsTop : st().header);
|
+ (i == 0 ? _rowsTop : st().header)
|
||||||
|
+ (firstAfterShortcuts
|
||||||
|
? st::stickerPanFirstAfterShortcutsSkip
|
||||||
|
: 0);
|
||||||
info.rowsBottom = info.rowsTop
|
info.rowsBottom = info.rowsTop
|
||||||
+ (info.rowsCount * _singleSize.height());
|
+ (info.rowsCount * _singleSize.height());
|
||||||
if (!callback(info)) {
|
if (!callback(info)) {
|
||||||
@@ -1531,6 +1787,7 @@ int EmojiListWidget::countDesiredHeight(int newWidth) {
|
|||||||
+ (innerWidth - _columnCount * singleWidth) / 2
|
+ (innerWidth - _columnCount * singleWidth) / 2
|
||||||
- st().margin.left();
|
- st().margin.left();
|
||||||
setSingleSize({ singleWidth, singleWidth - 2 * st().verticalSizeSub });
|
setSingleSize({ singleWidth, singleWidth - 2 * st().verticalSizeSub });
|
||||||
|
refreshSearchShortcutsScroll(newWidth);
|
||||||
|
|
||||||
const auto countResult = [this](int minimalLastHeight) {
|
const auto countResult = [this](int minimalLastHeight) {
|
||||||
const auto info = sectionInfo(sectionsCount() - 1);
|
const auto info = sectionInfo(sectionsCount() - 1);
|
||||||
@@ -1783,6 +2040,27 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) {
|
|||||||
_searchExpandCache = QImage();
|
_searchExpandCache = QImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_searchSwapAnimation.animating()) {
|
||||||
|
if (_searchSwapPartial) {
|
||||||
|
paint(p, {}, clip);
|
||||||
|
}
|
||||||
|
const auto progress = _searchSwapAnimation.value(1.);
|
||||||
|
const auto direction = _searchSwapReverse ? -1 : 1;
|
||||||
|
const auto slide = st().searchBackHeight;
|
||||||
|
p.setOpacity(1. - progress);
|
||||||
|
p.drawPixmap(
|
||||||
|
0,
|
||||||
|
_searchSwapTop + direction * int(base::SafeRound(slide * progress)),
|
||||||
|
_searchSwapBefore);
|
||||||
|
p.setOpacity(progress);
|
||||||
|
p.drawPixmap(
|
||||||
|
0,
|
||||||
|
_searchSwapTop - direction * int(base::SafeRound(slide * (1. - progress))),
|
||||||
|
_searchSwapAfter);
|
||||||
|
p.setOpacity(1.);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
paint(p, {}, clip);
|
paint(p, {}, clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1813,11 +2091,157 @@ void EmojiListWidget::validateEmojiPaintContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::paintSearchShortcuts(Painter &p, QRect clip) {
|
||||||
|
if (!searchShortcutsShown()
|
||||||
|
|| clip.bottom() < searchShortcutsTop()
|
||||||
|
|| clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto back = searchBackRect();
|
||||||
|
if (back.height() > 0) {
|
||||||
|
const auto selected = std::get_if<OverSearchBack>(
|
||||||
|
!v::is_null(_pressed) ? &_pressed : &_selected);
|
||||||
|
const auto &icon = selected
|
||||||
|
? st().search.back.iconOver
|
||||||
|
: st().search.back.icon;
|
||||||
|
icon.paint(
|
||||||
|
p,
|
||||||
|
st().searchBackIconLeft,
|
||||||
|
back.y() + st().searchBackIconTop,
|
||||||
|
width());
|
||||||
|
const auto text = tr::lng_search_back_to_results(tr::now);
|
||||||
|
const auto &font = st::emojiPanHeaderFont;
|
||||||
|
const auto available = width()
|
||||||
|
- st().searchBackTextLeft
|
||||||
|
- st().margin.right();
|
||||||
|
auto shown = text;
|
||||||
|
auto textWidth = font->width(shown);
|
||||||
|
if (textWidth > available) {
|
||||||
|
shown = font->elided(shown, available);
|
||||||
|
textWidth = font->width(shown);
|
||||||
|
}
|
||||||
|
p.setFont(font);
|
||||||
|
p.setPen(st().headerFg);
|
||||||
|
p.drawTextLeft(
|
||||||
|
st().searchBackTextLeft,
|
||||||
|
back.y() + st().searchBackTextTop,
|
||||||
|
width(),
|
||||||
|
shown,
|
||||||
|
textWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto selectedShortcut = std::get_if<OverSearchShortcut>(
|
||||||
|
!v::is_null(_pressed) ? &_pressed : &_selected);
|
||||||
|
p.save();
|
||||||
|
p.setClipRect(
|
||||||
|
QRect(
|
||||||
|
0,
|
||||||
|
searchShortcutsTop() + back.height(),
|
||||||
|
width(),
|
||||||
|
st().searchPacksTop
|
||||||
|
+ st().searchPackHeight
|
||||||
|
+ st().searchPacksBottom),
|
||||||
|
Qt::IntersectClip);
|
||||||
|
for (auto i = 0, count = int(_searchShortcutSets.size()); i != count; ++i) {
|
||||||
|
auto &set = _searchShortcutSets[i];
|
||||||
|
const auto rect = searchShortcutRect(i);
|
||||||
|
if (!rect.intersects(clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto selected = (set.id == _searchSelectedSetId)
|
||||||
|
|| (selectedShortcut && selectedShortcut->index == i);
|
||||||
|
if (selected) {
|
||||||
|
_overBg.paint(p, myrtlrect(rect));
|
||||||
|
}
|
||||||
|
if (set.ripple) {
|
||||||
|
set.ripple->paint(
|
||||||
|
p,
|
||||||
|
myrtlrect(rect).x(),
|
||||||
|
rect.y(),
|
||||||
|
width());
|
||||||
|
if (set.ripple->empty()) {
|
||||||
|
set.ripple.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto icon = QRect(
|
||||||
|
rect.x() + (rect.width() - st().searchPackIconSize) / 2,
|
||||||
|
rect.y() + st().searchPackIconTop,
|
||||||
|
st().searchPackIconSize,
|
||||||
|
st().searchPackIconSize);
|
||||||
|
paintSearchShortcutIcon(p, set, icon);
|
||||||
|
|
||||||
|
const auto available = rect.width()
|
||||||
|
- 2 * st().searchPackTextPadding;
|
||||||
|
auto title = set.title;
|
||||||
|
auto titleWidth = st::normalFont->width(title);
|
||||||
|
if (titleWidth > available) {
|
||||||
|
title = st::normalFont->elided(title, available);
|
||||||
|
titleWidth = st::normalFont->width(title);
|
||||||
|
}
|
||||||
|
const auto titleLeft = (titleWidth < available)
|
||||||
|
? (rect.x() + (rect.width() - titleWidth) / 2)
|
||||||
|
: (rect.x() + st().searchPackTextPadding);
|
||||||
|
p.setFont(st::normalFont);
|
||||||
|
p.setPen(st().textFg);
|
||||||
|
p.drawTextLeft(
|
||||||
|
titleLeft,
|
||||||
|
rect.y() + st().searchPackTextTop,
|
||||||
|
width(),
|
||||||
|
title,
|
||||||
|
titleWidth);
|
||||||
|
}
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
if (!searchShortcutSelected()) {
|
||||||
|
const auto top = searchShortcutsTop()
|
||||||
|
+ st().searchPacksTop
|
||||||
|
+ st().searchPackHeight
|
||||||
|
+ st().searchPacksBottom;
|
||||||
|
p.setFont(st::emojiPanHeaderFont);
|
||||||
|
p.setPen(st().headerFg);
|
||||||
|
p.drawTextLeft(
|
||||||
|
st().headerLeft - st().margin.left(),
|
||||||
|
top + st().searchResultsTextTop,
|
||||||
|
width(),
|
||||||
|
tr::lng_search_results_header(tr::now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::paintSearchShortcutIcon(
|
||||||
|
Painter &p,
|
||||||
|
const CustomSet &set,
|
||||||
|
QRect rect) {
|
||||||
|
if (set.list.empty() || _customSingleSize <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto native = _customSingleSize;
|
||||||
|
const auto scale = double(rect.width()) / double(native);
|
||||||
|
auto context = Ui::Text::CustomEmojiPaintContext{
|
||||||
|
.textColor = (_customTextColor
|
||||||
|
? _customTextColor()
|
||||||
|
: st().textFg->c),
|
||||||
|
.size = QSize(native, native),
|
||||||
|
.now = crl::now(),
|
||||||
|
.scale = 1.,
|
||||||
|
.position = QPoint(),
|
||||||
|
.paused = On(powerSavingFlag()) || paused(),
|
||||||
|
.scaled = false,
|
||||||
|
.internal = { .forceFirstFrame = true },
|
||||||
|
};
|
||||||
|
p.save();
|
||||||
|
p.translate(rect.center());
|
||||||
|
p.scale(scale, scale);
|
||||||
|
p.translate(-native / 2, -native / 2);
|
||||||
|
set.list.front().custom->paint(p, context);
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
|
||||||
void EmojiListWidget::paint(
|
void EmojiListWidget::paint(
|
||||||
Painter &p,
|
Painter &p,
|
||||||
ExpandingContext context,
|
ExpandingContext context,
|
||||||
QRect clip) {
|
QRect clip) {
|
||||||
validateEmojiPaintContext(context);
|
validateEmojiPaintContext(context);
|
||||||
|
paintSearchShortcuts(p, clip);
|
||||||
|
|
||||||
_paintAsPremium = session().premium();
|
_paintAsPremium = session().premium();
|
||||||
|
|
||||||
@@ -1842,6 +2266,7 @@ void EmojiListWidget::paint(
|
|||||||
: &_selected);
|
: &_selected);
|
||||||
if (_searchResults.empty()
|
if (_searchResults.empty()
|
||||||
&& _searchSets.empty()
|
&& _searchSets.empty()
|
||||||
|
&& _searchShortcutSets.empty()
|
||||||
&& _searchMode
|
&& _searchMode
|
||||||
&& !_searchLoading
|
&& !_searchLoading
|
||||||
&& !_searchRequestTimer.isActive()) {
|
&& !_searchRequestTimer.isActive()) {
|
||||||
@@ -2287,6 +2712,11 @@ void EmojiListWidget::mousePressEvent(QMouseEvent *e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPressed(_selected);
|
setPressed(_selected);
|
||||||
|
if (std::get_if<OverSearchShortcut>(&_selected)) {
|
||||||
|
_searchShortcutsMouseDown = _lastMousePos;
|
||||||
|
_searchShortcutsDragStart = _searchShortcutsScroll;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
|
}
|
||||||
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
|
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
|
||||||
const auto emoji = lookupOverEmoji(over);
|
const auto emoji = lookupOverEmoji(over);
|
||||||
if (emoji && emoji->hasVariants()) {
|
if (emoji && emoji->hasVariants()) {
|
||||||
@@ -2325,6 +2755,10 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateSelected();
|
updateSelected();
|
||||||
|
if (_searchShortcutsDragging) {
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_showPickerTimer.isActive()) {
|
if (_showPickerTimer.isActive()) {
|
||||||
_showPickerTimer.cancel();
|
_showPickerTimer.cancel();
|
||||||
@@ -2342,7 +2776,14 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto over = std::get_if<OverEmoji>(&_selected)) {
|
if (std::get_if<OverSearchBack>(&_selected)) {
|
||||||
|
backToSearchResults();
|
||||||
|
return;
|
||||||
|
} else if (const auto shortcut = std::get_if<OverSearchShortcut>(
|
||||||
|
&_selected)) {
|
||||||
|
toggleSearchShortcut(shortcut->index);
|
||||||
|
return;
|
||||||
|
} else if (const auto over = std::get_if<OverEmoji>(&_selected)) {
|
||||||
const auto section = over->section;
|
const auto section = over->section;
|
||||||
const auto index = over->index;
|
const auto index = over->index;
|
||||||
if (sectionInfo(section).collapsed
|
if (sectionInfo(section).collapsed
|
||||||
@@ -2747,8 +3188,45 @@ void EmojiListWidget::colorChosen(EmojiChosen data) {
|
|||||||
_picker->hideAnimated();
|
_picker->hideAnimated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EmojiListWidget::wheelEvent(QWheelEvent *e) {
|
||||||
|
if (searchShortcutsShown() && _searchShortcutsScrollMax > 0) {
|
||||||
|
const auto pos = mapFromGlobal(e->globalPosition().toPoint());
|
||||||
|
if (pos.y() >= searchShortcutsTop()
|
||||||
|
&& pos.y() < searchShortcutsTop() + searchShortcutsHeight()) {
|
||||||
|
const auto angle = e->angleDelta();
|
||||||
|
const auto pixel = e->pixelDelta();
|
||||||
|
const auto horizontal = (angle.x() != 0);
|
||||||
|
const auto vertical = (angle.y() != 0);
|
||||||
|
if (horizontal || vertical) {
|
||||||
|
const auto delta = horizontal
|
||||||
|
? ((rtl() ? -1 : 1)
|
||||||
|
* (pixel.x() ? pixel.x() : angle.x()))
|
||||||
|
: (pixel.y() ? pixel.y() : angle.y());
|
||||||
|
scrollSearchShortcutsTo(_searchShortcutsScroll - delta);
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Inner::wheelEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
void EmojiListWidget::mouseMoveEvent(QMouseEvent *e) {
|
void EmojiListWidget::mouseMoveEvent(QMouseEvent *e) {
|
||||||
_lastMousePos = e->globalPos();
|
_lastMousePos = e->globalPos();
|
||||||
|
if (std::get_if<OverSearchShortcut>(&_pressed)
|
||||||
|
&& _searchShortcutsScrollMax > 0) {
|
||||||
|
const auto delta = _lastMousePos - _searchShortcutsMouseDown;
|
||||||
|
if (!_searchShortcutsDragging
|
||||||
|
&& delta.manhattanLength() >= QApplication::startDragDistance()) {
|
||||||
|
_searchShortcutsDragging = true;
|
||||||
|
}
|
||||||
|
if (_searchShortcutsDragging) {
|
||||||
|
scrollSearchShortcutsTo(
|
||||||
|
_searchShortcutsDragStart
|
||||||
|
+ (rtl() ? -1 : 1) * -delta.x());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!_picker->isHidden()) {
|
if (!_picker->isHidden()) {
|
||||||
if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) {
|
if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) {
|
||||||
return _picker->handleMouseMove(QCursor::pos());
|
return _picker->handleMouseMove(QCursor::pos());
|
||||||
@@ -2837,12 +3315,17 @@ void EmojiListWidget::processHideFinished() {
|
|||||||
_picker->hideFast();
|
_picker->hideFast();
|
||||||
_pickerSelected = v::null;
|
_pickerSelected = v::null;
|
||||||
}
|
}
|
||||||
cancelSearchRequest();
|
|
||||||
unloadAllCustom();
|
unloadAllCustom();
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmojiListWidget::processPanelHideFinished() {
|
void EmojiListWidget::processPanelHideFinished() {
|
||||||
|
if (_search) {
|
||||||
|
_search->cancel();
|
||||||
|
}
|
||||||
|
_nextSearchQuery.clear();
|
||||||
|
applyNextSearchQuery();
|
||||||
|
cancelSearchRequest();
|
||||||
unloadAllCustom();
|
unloadAllCustom();
|
||||||
if (_localSetsManager->clearInstalledLocally()) {
|
if (_localSetsManager->clearInstalledLocally()) {
|
||||||
refreshCustom();
|
refreshCustom();
|
||||||
@@ -3303,6 +3786,23 @@ void EmojiListWidget::updateSelected() {
|
|||||||
|
|
||||||
auto newSelected = OverState{ v::null };
|
auto newSelected = OverState{ v::null };
|
||||||
auto p = mapFromGlobal(_lastMousePos);
|
auto p = mapFromGlobal(_lastMousePos);
|
||||||
|
if (searchShortcutsShown()
|
||||||
|
&& p.y() >= searchShortcutsTop()
|
||||||
|
&& p.y() < searchShortcutsTop() + searchShortcutsHeight()) {
|
||||||
|
if (searchShortcutSelected() && searchBackRect().contains(p)) {
|
||||||
|
newSelected = OverSearchBack{};
|
||||||
|
} else {
|
||||||
|
for (auto i = 0, count = int(_searchShortcutSets.size());
|
||||||
|
i != count; ++i) {
|
||||||
|
if (myrtlrect(searchShortcutRect(i)).contains(p)) {
|
||||||
|
newSelected = OverSearchShortcut{ i };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelected(newSelected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto info = sectionInfoByOffset(p.y());
|
auto info = sectionInfoByOffset(p.y());
|
||||||
auto section = info.section;
|
auto section = info.section;
|
||||||
if (p.y() >= info.top && p.y() < info.rowsTop) {
|
if (p.y() >= info.top && p.y() < info.rowsTop) {
|
||||||
@@ -3339,6 +3839,14 @@ void EmojiListWidget::setSelected(OverState newSelected) {
|
|||||||
rtlupdate(emojiRect(sticker->section, sticker->index));
|
rtlupdate(emojiRect(sticker->section, sticker->index));
|
||||||
} else if (const auto button = std::get_if<OverButton>(&_selected)) {
|
} else if (const auto button = std::get_if<OverButton>(&_selected)) {
|
||||||
rtlupdate(buttonRect(button->section));
|
rtlupdate(buttonRect(button->section));
|
||||||
|
} else if (const auto shortcut
|
||||||
|
= std::get_if<OverSearchShortcut>(&_selected)) {
|
||||||
|
if (shortcut->index >= 0
|
||||||
|
&& shortcut->index < _searchShortcutSets.size()) {
|
||||||
|
rtlupdate(searchShortcutRect(shortcut->index));
|
||||||
|
}
|
||||||
|
} else if (std::get_if<OverSearchBack>(&_selected)) {
|
||||||
|
rtlupdate(searchBackRect());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
updateSelected();
|
updateSelected();
|
||||||
@@ -3382,6 +3890,14 @@ void EmojiListWidget::setPressed(OverState newPressed) {
|
|||||||
if (ripple) {
|
if (ripple) {
|
||||||
ripple->lastStop();
|
ripple->lastStop();
|
||||||
}
|
}
|
||||||
|
} else if (auto shortcut = std::get_if<OverSearchShortcut>(&_pressed)) {
|
||||||
|
if (shortcut->index >= 0
|
||||||
|
&& shortcut->index < _searchShortcutSets.size()) {
|
||||||
|
auto &ripple = _searchShortcutSets[shortcut->index].ripple;
|
||||||
|
if (ripple) {
|
||||||
|
ripple->lastStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_pressed = newPressed;
|
_pressed = newPressed;
|
||||||
if (auto button = std::get_if<OverButton>(&_pressed)) {
|
if (auto button = std::get_if<OverButton>(&_pressed)) {
|
||||||
@@ -3399,6 +3915,16 @@ void EmojiListWidget::setPressed(OverState newPressed) {
|
|||||||
ripple = createButtonRipple(button->section);
|
ripple = createButtonRipple(button->section);
|
||||||
}
|
}
|
||||||
ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section));
|
ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section));
|
||||||
|
} else if (auto shortcut = std::get_if<OverSearchShortcut>(&_pressed)) {
|
||||||
|
if (shortcut->index >= 0
|
||||||
|
&& shortcut->index < _searchShortcutSets.size()) {
|
||||||
|
auto &ripple = _searchShortcutSets[shortcut->index].ripple;
|
||||||
|
if (!ripple) {
|
||||||
|
ripple = createSearchShortcutRipple(shortcut->index);
|
||||||
|
}
|
||||||
|
ripple->add(mapFromGlobal(QCursor::pos())
|
||||||
|
- myrtlrect(searchShortcutRect(shortcut->index)).topLeft());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3472,6 +3998,29 @@ QPoint EmojiListWidget::buttonRippleTopLeft(int section) const {
|
|||||||
: QPoint());
|
: QPoint());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::RippleAnimation>
|
||||||
|
EmojiListWidget::createSearchShortcutRipple(int index) {
|
||||||
|
Expects(index >= 0 && index < _searchShortcutSets.size());
|
||||||
|
|
||||||
|
const auto setId = _searchShortcutSets[index].id;
|
||||||
|
auto mask = Ui::RippleAnimation::RoundRectMask(
|
||||||
|
searchShortcutRect(index).size(),
|
||||||
|
st::roundRadiusLarge);
|
||||||
|
return std::make_unique<Ui::RippleAnimation>(
|
||||||
|
st::defaultRippleAnimation,
|
||||||
|
std::move(mask),
|
||||||
|
[this, setId] {
|
||||||
|
const auto i = ranges::find(
|
||||||
|
_searchShortcutSets,
|
||||||
|
setId,
|
||||||
|
&CustomSet::id);
|
||||||
|
if (i != _searchShortcutSets.end()) {
|
||||||
|
rtlupdate(searchShortcutRect(
|
||||||
|
int(i - _searchShortcutSets.begin())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
PowerSaving::Flag EmojiListWidget::powerSavingFlag() const {
|
PowerSaving::Flag EmojiListWidget::powerSavingFlag() const {
|
||||||
const auto reactions = (_mode == Mode::FullReactions)
|
const auto reactions = (_mode == Mode::FullReactions)
|
||||||
|| (_mode == Mode::RecentReactions);
|
|| (_mode == Mode::RecentReactions);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
#include "chat_helpers/compose/compose_features.h"
|
#include "chat_helpers/compose/compose_features.h"
|
||||||
#include "chat_helpers/tabbed_selector.h"
|
#include "chat_helpers/tabbed_selector.h"
|
||||||
|
#include "ui/effects/animations.h"
|
||||||
#include "ui/widgets/tooltip.h"
|
#include "ui/widgets/tooltip.h"
|
||||||
#include "ui/round_rect.h"
|
#include "ui/round_rect.h"
|
||||||
#include "base/timer.h"
|
#include "base/timer.h"
|
||||||
@@ -170,6 +171,7 @@ protected:
|
|||||||
void mousePressEvent(QMouseEvent *e) override;
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||||
void mouseMoveEvent(QMouseEvent *e) override;
|
void mouseMoveEvent(QMouseEvent *e) override;
|
||||||
|
void wheelEvent(QWheelEvent *e) override;
|
||||||
void paintEvent(QPaintEvent *e) override;
|
void paintEvent(QPaintEvent *e) override;
|
||||||
void leaveEventHook(QEvent *e) override;
|
void leaveEventHook(QEvent *e) override;
|
||||||
void leaveToChildEvent(QEvent *e, QWidget *child) override;
|
void leaveToChildEvent(QEvent *e, QWidget *child) override;
|
||||||
@@ -252,11 +254,31 @@ private:
|
|||||||
return !(*this == other);
|
return !(*this == other);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
struct OverSearchShortcut {
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
inline bool operator==(OverSearchShortcut other) const {
|
||||||
|
return (index == other.index);
|
||||||
|
}
|
||||||
|
inline bool operator!=(OverSearchShortcut other) const {
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct OverSearchBack {
|
||||||
|
inline bool operator==(OverSearchBack other) const {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
inline bool operator!=(OverSearchBack other) const {
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
};
|
||||||
using OverState = std::variant<
|
using OverState = std::variant<
|
||||||
v::null_t,
|
v::null_t,
|
||||||
OverEmoji,
|
OverEmoji,
|
||||||
OverSet,
|
OverSet,
|
||||||
OverButton>;
|
OverButton,
|
||||||
|
OverSearchShortcut,
|
||||||
|
OverSearchBack>;
|
||||||
struct ExpandingContext {
|
struct ExpandingContext {
|
||||||
float64 progress = 0.;
|
float64 progress = 0.;
|
||||||
int finalHeight = 0;
|
int finalHeight = 0;
|
||||||
@@ -318,7 +340,25 @@ private:
|
|||||||
const MTPmessages_FoundStickerSets &result);
|
const MTPmessages_FoundStickerSets &result);
|
||||||
void showSearchResults();
|
void showSearchResults();
|
||||||
void fillCloudSearchResults();
|
void fillCloudSearchResults();
|
||||||
void fillCloudSearchSets();
|
void refreshSearchShortcuts();
|
||||||
|
void fillLocalSearchShortcuts(const QString &query);
|
||||||
|
bool addSearchShortcut(not_null<Data::StickersSet*> set);
|
||||||
|
[[nodiscard]] std::vector<CustomOne> collectSearchSet(
|
||||||
|
not_null<Data::StickersSet*> set);
|
||||||
|
void fillSelectedSearchShortcut();
|
||||||
|
[[nodiscard]] bool searchShortcutsShown() const;
|
||||||
|
[[nodiscard]] bool searchShortcutSelected() const;
|
||||||
|
void startSearchSwapAnimation(Fn<void()> change, bool packToPack = false);
|
||||||
|
[[nodiscard]] int searchShortcutsHeight() const;
|
||||||
|
[[nodiscard]] int searchShortcutsTop() const;
|
||||||
|
[[nodiscard]] QRect searchBackRect() const;
|
||||||
|
[[nodiscard]] QRect searchShortcutRect(int index) const;
|
||||||
|
void refreshSearchShortcutsScroll(int newWidth);
|
||||||
|
void scrollSearchShortcutsTo(int value);
|
||||||
|
void paintSearchShortcuts(Painter &p, QRect clip);
|
||||||
|
void paintSearchShortcutIcon(Painter &p, const CustomSet &set, QRect rect);
|
||||||
|
void toggleSearchShortcut(int index);
|
||||||
|
void backToSearchResults();
|
||||||
[[nodiscard]] CustomSet &searchSetBySection(int section);
|
[[nodiscard]] CustomSet &searchSetBySection(int section);
|
||||||
[[nodiscard]] const CustomSet &searchSetBySection(int section) const;
|
[[nodiscard]] const CustomSet &searchSetBySection(int section) const;
|
||||||
void ensureLoaded(int section);
|
void ensureLoaded(int section);
|
||||||
@@ -411,6 +451,8 @@ private:
|
|||||||
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
|
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
|
||||||
int section);
|
int section);
|
||||||
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
|
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
|
||||||
|
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation>
|
||||||
|
createSearchShortcutRipple(int index);
|
||||||
[[nodiscard]] PowerSaving::Flag powerSavingFlag() const;
|
[[nodiscard]] PowerSaving::Flag powerSavingFlag() const;
|
||||||
|
|
||||||
void repaintCustom(uint64 setId);
|
void repaintCustom(uint64 setId);
|
||||||
@@ -497,9 +539,22 @@ private:
|
|||||||
std::map<QString, int> _searchCloudNextOffset;
|
std::map<QString, int> _searchCloudNextOffset;
|
||||||
std::map<QString, std::vector<uint64>> _searchSetsCache;
|
std::map<QString, std::vector<uint64>> _searchSetsCache;
|
||||||
std::vector<CustomSet> _searchSets;
|
std::vector<CustomSet> _searchSets;
|
||||||
|
std::vector<CustomSet> _searchShortcutSets;
|
||||||
QString _searchRequestQuery;
|
QString _searchRequestQuery;
|
||||||
QString _searchNextRequestQuery;
|
QString _searchNextRequestQuery;
|
||||||
QString _searchEmoticon;
|
QString _searchEmoticon;
|
||||||
|
uint64 _searchSelectedSetId = 0;
|
||||||
|
int _searchShortcutsScroll = 0;
|
||||||
|
int _searchShortcutsScrollMax = 0;
|
||||||
|
int _searchShortcutsDragStart = 0;
|
||||||
|
QPoint _searchShortcutsMouseDown;
|
||||||
|
bool _searchShortcutsDragging = false;
|
||||||
|
Ui::Animations::Simple _searchSwapAnimation;
|
||||||
|
QPixmap _searchSwapBefore;
|
||||||
|
QPixmap _searchSwapAfter;
|
||||||
|
int _searchSwapTop = 0;
|
||||||
|
bool _searchSwapReverse = false;
|
||||||
|
bool _searchSwapPartial = false;
|
||||||
mtpRequestId _searchCloudRequestId = 0;
|
mtpRequestId _searchCloudRequestId = 0;
|
||||||
mtpRequestId _searchSetsRequestId = 0;
|
mtpRequestId _searchSetsRequestId = 0;
|
||||||
bool _searchLoading = false;
|
bool _searchLoading = false;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
#include "base/qt/qt_key_modifiers.h"
|
#include "base/qt/qt_key_modifiers.h"
|
||||||
#include "data/business/data_shortcut_messages.h"
|
#include "data/business/data_shortcut_messages.h"
|
||||||
|
#include "data/components/recent_inline_bots.h"
|
||||||
|
#include "data/components/top_peers.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_document_media.h"
|
#include "data/data_document_media.h"
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
@@ -65,7 +67,7 @@ namespace {
|
|||||||
|
|
||||||
template <typename T, typename U>
|
template <typename T, typename U>
|
||||||
inline int indexOfInFirstN(const T &v, const U &elem, int last) {
|
inline int indexOfInFirstN(const T &v, const U &elem, int last) {
|
||||||
for (auto b = v.cbegin(), i = b, e = b + std::max(int(v.size()), last)
|
for (auto b = v.cbegin(), i = b, e = b + std::min(int(v.size()), last)
|
||||||
; i != e
|
; i != e
|
||||||
; ++i) {
|
; ++i) {
|
||||||
if (i->user == elem) {
|
if (i->user == elem) {
|
||||||
@@ -101,7 +103,6 @@ public:
|
|||||||
int index,
|
int index,
|
||||||
Api::SendOptions options = {}) const;
|
Api::SendOptions options = {}) const;
|
||||||
|
|
||||||
void setRecentInlineBotsInRows(int32 bots);
|
|
||||||
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
|
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
|
||||||
void rowsUpdated();
|
void rowsUpdated();
|
||||||
|
|
||||||
@@ -127,6 +128,7 @@ private:
|
|||||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||||
|
|
||||||
QRect selectedRect(int index) const;
|
QRect selectedRect(int index) const;
|
||||||
|
[[nodiscard]] bool isRemovableMentionRow(int index) const;
|
||||||
void updateSelectedRow();
|
void updateSelectedRow();
|
||||||
void setSel(int sel, bool scroll = false);
|
void setSel(int sel, bool scroll = false);
|
||||||
void showPreview();
|
void showPreview();
|
||||||
@@ -155,7 +157,6 @@ private:
|
|||||||
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
|
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
|
||||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||||
int _stickersPerRow = 1;
|
int _stickersPerRow = 1;
|
||||||
int _recentInlineBotsInRows = 0;
|
|
||||||
int _sel = -1;
|
int _sel = -1;
|
||||||
int _down = -1;
|
int _down = -1;
|
||||||
std::optional<QPoint> _lastMousePosition;
|
std::optional<QPoint> _lastMousePosition;
|
||||||
@@ -191,9 +192,21 @@ struct FieldAutocomplete::StickerSuggestion {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct FieldAutocomplete::MentionRow {
|
struct FieldAutocomplete::MentionRow {
|
||||||
|
enum class Source {
|
||||||
|
InlineRecent,
|
||||||
|
GuestChatTopPeer,
|
||||||
|
MentionCandidate,
|
||||||
|
};
|
||||||
|
|
||||||
not_null<UserData*> user;
|
not_null<UserData*> user;
|
||||||
|
Source source = Source::MentionCandidate;
|
||||||
Ui::Text::String name;
|
Ui::Text::String name;
|
||||||
Ui::PeerUserpicView userpic;
|
Ui::PeerUserpicView userpic;
|
||||||
|
|
||||||
|
[[nodiscard]] bool removable() const {
|
||||||
|
return (source == Source::InlineRecent)
|
||||||
|
|| (source == Source::GuestChatTopPeer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FieldAutocomplete::BotCommandRow {
|
struct FieldAutocomplete::BotCommandRow {
|
||||||
@@ -242,6 +255,17 @@ FieldAutocomplete::FieldAutocomplete(
|
|||||||
) | rpl::on_next(crl::guard(_inner, [=] {
|
) | rpl::on_next(crl::guard(_inner, [=] {
|
||||||
_inner->onParentGeometryChanged();
|
_inner->onParentGeometryChanged();
|
||||||
}), lifetime());
|
}), lifetime());
|
||||||
|
|
||||||
|
_session->topGuestChatBots().updates(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
if (_hiding
|
||||||
|
|| isHidden()
|
||||||
|
|| (_type != Type::Mentions)
|
||||||
|
|| !_addInlineBots) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateFiltered();
|
||||||
|
}, lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<Show> FieldAutocomplete::uiShow() const {
|
std::shared_ptr<Show> FieldAutocomplete::uiShow() const {
|
||||||
@@ -430,7 +454,7 @@ FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
||||||
int32 now = base::unixtime::now(), recentInlineBots = 0;
|
int32 now = base::unixtime::now();
|
||||||
MentionRows mrows;
|
MentionRows mrows;
|
||||||
HashtagRows hrows;
|
HashtagRows hrows;
|
||||||
BotCommandRows brows;
|
BotCommandRows brows;
|
||||||
@@ -438,7 +462,12 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
|||||||
if (_emoji) {
|
if (_emoji) {
|
||||||
srows = getStickerSuggestions();
|
srows = getStickerSuggestions();
|
||||||
} else if (_type == Type::Mentions) {
|
} else if (_type == Type::Mentions) {
|
||||||
int maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0;
|
const auto guestChatBots = _addInlineBots
|
||||||
|
? _session->topGuestChatBots().list()
|
||||||
|
: std::vector<not_null<PeerData*>>();
|
||||||
|
int maxListSize = _addInlineBots
|
||||||
|
? (_session->recentInlineBots().list().size() + int(guestChatBots.size()))
|
||||||
|
: 0;
|
||||||
if (_chat) {
|
if (_chat) {
|
||||||
maxListSize += (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size());
|
maxListSize += (_chat->participants.empty() ? _chat->lastAuthors.size() : _chat->participants.size());
|
||||||
} else if (_channel && _channel->isMegagroup()) {
|
} else if (_channel && _channel->isMegagroup()) {
|
||||||
@@ -471,17 +500,51 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
|||||||
}
|
}
|
||||||
return filterNotPassedByUsername(user);
|
return filterNotPassedByUsername(user);
|
||||||
};
|
};
|
||||||
|
const auto mentionUserIndex = [&](not_null<UserData*> user) {
|
||||||
|
return indexOfInFirstN(mrows, user, int(mrows.size()));
|
||||||
|
};
|
||||||
|
const auto containsMentionUser = [&](not_null<UserData*> user) {
|
||||||
|
return mentionUserIndex(user) >= 0;
|
||||||
|
};
|
||||||
|
const auto pushMentionRow = [&](
|
||||||
|
not_null<UserData*> user,
|
||||||
|
MentionRow::Source source) {
|
||||||
|
if (containsMentionUser(user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mrows.push_back({ user, source });
|
||||||
|
};
|
||||||
|
const auto markMentionCandidateIfExists = [&](
|
||||||
|
not_null<UserData*> user) {
|
||||||
|
const auto index = mentionUserIndex(user);
|
||||||
|
if (index < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mrows[index].source = MentionRow::Source::MentionCandidate;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
bool listAllSuggestions = _filter.isEmpty();
|
bool listAllSuggestions = _filter.isEmpty();
|
||||||
if (_addInlineBots) {
|
if (_addInlineBots) {
|
||||||
for (const auto user : cRecentInlineBots()) {
|
for (const auto &user : _session->recentInlineBots().list()) {
|
||||||
if (user->isInaccessible()
|
if (user->isInaccessible()
|
||||||
|| (!listAllSuggestions
|
|| (!listAllSuggestions
|
||||||
&& filterNotPassedByUsername(user))) {
|
&& filterNotPassedByUsername(user))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mrows.push_back({ user });
|
pushMentionRow(user, MentionRow::Source::InlineRecent);
|
||||||
++recentInlineBots;
|
}
|
||||||
|
for (const auto &peer : guestChatBots) {
|
||||||
|
const auto user = peer->asUser();
|
||||||
|
if (!user
|
||||||
|
|| user->isInaccessible()
|
||||||
|
|| !user->isBot()
|
||||||
|
|| (!listAllSuggestions
|
||||||
|
&& filterNotPassedByUsername(user))
|
||||||
|
|| containsMentionUser(user)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pushMentionRow(user, MentionRow::Source::GuestChatTopPeer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_chat) {
|
if (_chat) {
|
||||||
@@ -496,20 +559,23 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
|||||||
for (const auto &user : _chat->participants) {
|
for (const auto &user : _chat->participants) {
|
||||||
if (user->isInaccessible()) continue;
|
if (user->isInaccessible()) continue;
|
||||||
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
||||||
if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
|
if (markMentionCandidateIfExists(user)) continue;
|
||||||
sorted.emplace(byOnline(user), user);
|
sorted.emplace(byOnline(user), user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const auto &user : _chat->lastAuthors) {
|
for (const auto &user : _chat->lastAuthors) {
|
||||||
if (user->isInaccessible()) continue;
|
if (user->isInaccessible()) continue;
|
||||||
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
||||||
if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
|
if (markMentionCandidateIfExists(user)) {
|
||||||
mrows.push_back({ user });
|
sorted.remove(byOnline(user), user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pushMentionRow(user, MentionRow::Source::MentionCandidate);
|
||||||
sorted.remove(byOnline(user), user);
|
sorted.remove(byOnline(user), user);
|
||||||
}
|
}
|
||||||
for (auto i = sorted.cend(), b = sorted.cbegin(); i != b;) {
|
for (auto i = sorted.cend(), b = sorted.cbegin(); i != b;) {
|
||||||
--i;
|
--i;
|
||||||
mrows.push_back({ i->second });
|
pushMentionRow(i->second, MentionRow::Source::MentionCandidate);
|
||||||
}
|
}
|
||||||
} else if (_channel && _channel->isMegagroup()) {
|
} else if (_channel && _channel->isMegagroup()) {
|
||||||
if (!_channel->canViewMembers()) {
|
if (!_channel->canViewMembers()) {
|
||||||
@@ -521,8 +587,8 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
|||||||
if (const auto user = _channel->owner().userLoaded(userId)) {
|
if (const auto user = _channel->owner().userLoaded(userId)) {
|
||||||
if (user->isInaccessible()) continue;
|
if (user->isInaccessible()) continue;
|
||||||
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
||||||
if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
|
if (markMentionCandidateIfExists(user)) continue;
|
||||||
mrows.push_back({ user });
|
pushMentionRow(user, MentionRow::Source::MentionCandidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,8 +600,8 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
|||||||
for (const auto &user : _channel->mgInfo->lastParticipants) {
|
for (const auto &user : _channel->mgInfo->lastParticipants) {
|
||||||
if (user->isInaccessible()) continue;
|
if (user->isInaccessible()) continue;
|
||||||
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
if (!listAllSuggestions && filterNotPassedByName(user)) continue;
|
||||||
if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue;
|
if (markMentionCandidateIfExists(user)) continue;
|
||||||
mrows.push_back({ user });
|
pushMentionRow(user, MentionRow::Source::MentionCandidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -686,7 +752,6 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
|||||||
std::move(brows),
|
std::move(brows),
|
||||||
std::move(srows),
|
std::move(srows),
|
||||||
resetScroll);
|
resetScroll);
|
||||||
_inner->setRecentInlineBotsInRows(recentInlineBots);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FieldAutocomplete::rowsUpdated(
|
void FieldAutocomplete::rowsUpdated(
|
||||||
@@ -1041,7 +1106,7 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::mentionBgOver);
|
p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::mentionBgOver);
|
||||||
int skip = (st::mentionHeight - st::smallCloseIconOver.height()) / 2;
|
int skip = (st::mentionHeight - st::smallCloseIconOver.height()) / 2;
|
||||||
if (!_hrows->empty() || (!_mrows->empty() && i < _recentInlineBotsInRows)) {
|
if (!_hrows->empty() || isRemovableMentionRow(i)) {
|
||||||
st::smallCloseIconOver.paint(p, QPoint(width() - st::smallCloseIconOver.width() - skip, i * st::mentionHeight + skip), width());
|
st::smallCloseIconOver.paint(p, QPoint(width() - st::smallCloseIconOver.width() - skip, i * st::mentionHeight + skip), width());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1313,36 +1378,45 @@ bool FieldAutocomplete::Inner::chooseAtIndex(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FieldAutocomplete::Inner::setRecentInlineBotsInRows(int32 bots) {
|
bool FieldAutocomplete::Inner::isRemovableMentionRow(int index) const {
|
||||||
_recentInlineBotsInRows = bots;
|
return (index >= 0)
|
||||||
|
&& (index < _mrows->size())
|
||||||
|
&& _mrows->at(index).removable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {
|
void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {
|
||||||
selectByMouse(e->globalPos());
|
selectByMouse(e->globalPos());
|
||||||
if (e->button() == Qt::LeftButton) {
|
if (e->button() == Qt::LeftButton) {
|
||||||
if (_overDelete && _sel >= 0 && _sel < (_mrows->empty() ? _hrows->size() : _recentInlineBotsInRows)) {
|
if (_overDelete
|
||||||
bool removed = false;
|
&& (_mrows->empty()
|
||||||
|
? (_sel >= 0 && _sel < _hrows->size())
|
||||||
|
: isRemovableMentionRow(_sel))) {
|
||||||
|
auto writeRecent = false;
|
||||||
if (_mrows->empty()) {
|
if (_mrows->empty()) {
|
||||||
QString toRemove = _hrows->at(_sel);
|
QString toRemove = _hrows->at(_sel);
|
||||||
RecentHashtagPack &recent(cRefRecentWriteHashtags());
|
RecentHashtagPack &recent(cRefRecentWriteHashtags());
|
||||||
for (RecentHashtagPack::iterator i = recent.begin(); i != recent.cend();) {
|
for (RecentHashtagPack::iterator i = recent.begin(); i != recent.cend();) {
|
||||||
if (i->first == toRemove) {
|
if (i->first == toRemove) {
|
||||||
i = recent.erase(i);
|
i = recent.erase(i);
|
||||||
removed = true;
|
writeRecent = true;
|
||||||
} else {
|
} else {
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
UserData *toRemove = _mrows->at(_sel).user;
|
const auto &row = _mrows->at(_sel);
|
||||||
RecentInlineBots &recent(cRefRecentInlineBots());
|
switch (row.source) {
|
||||||
int32 index = recent.indexOf(toRemove);
|
case MentionRow::Source::InlineRecent:
|
||||||
if (index >= 0) {
|
_session->recentInlineBots().remove(row.user);
|
||||||
recent.remove(index);
|
break;
|
||||||
removed = true;
|
case MentionRow::Source::GuestChatTopPeer:
|
||||||
|
_session->topGuestChatBots().remove(row.user);
|
||||||
|
break;
|
||||||
|
case MentionRow::Source::MentionCandidate:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (removed) {
|
if (writeRecent) {
|
||||||
_show->session().local().writeRecentHashtagsAndBots();
|
_show->session().local().writeRecentHashtagsAndBots();
|
||||||
}
|
}
|
||||||
_parent->updateFiltered();
|
_parent->updateFiltered();
|
||||||
@@ -1586,7 +1660,9 @@ void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) {
|
|||||||
: !_hrows->empty()
|
: !_hrows->empty()
|
||||||
? _hrows->size()
|
? _hrows->size()
|
||||||
: _brows->size();
|
: _brows->size();
|
||||||
_overDelete = (!_hrows->empty() || (!_mrows->empty() && sel < _recentInlineBotsInRows)) ? (mouse.x() >= width() - st::mentionHeight) : false;
|
_overDelete = (!_hrows->empty() || isRemovableMentionRow(sel))
|
||||||
|
? (mouse.x() >= width() - st::mentionHeight)
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
if (sel < 0 || sel >= maxSel) {
|
if (sel < 0 || sel >= maxSel) {
|
||||||
sel = -1;
|
sel = -1;
|
||||||
@@ -1746,7 +1822,7 @@ void InitFieldAutocomplete(
|
|||||||
&& cRecentSearchHashtags().isEmpty()) {
|
&& cRecentSearchHashtags().isEmpty()) {
|
||||||
peer->session().local().readRecentHashtagsAndBots();
|
peer->session().local().readRecentHashtagsAndBots();
|
||||||
} else if (parsed.query[0] == '@'
|
} else if (parsed.query[0] == '@'
|
||||||
&& cRecentInlineBots().isEmpty()) {
|
&& peer->session().recentInlineBots().list().empty()) {
|
||||||
peer->session().local().readRecentHashtagsAndBots();
|
peer->session().local().readRecentHashtagsAndBots();
|
||||||
} else if (parsed.query[0] == '/'
|
} else if (parsed.query[0] == '/'
|
||||||
&& peer->isUser()
|
&& peer->isUser()
|
||||||
@@ -1756,6 +1832,11 @@ void InitFieldAutocomplete(
|
|||||||
|| peer->starsPerMessageChecked() != 0)) {
|
|| peer->starsPerMessageChecked() != 0)) {
|
||||||
parsed = {};
|
parsed = {};
|
||||||
}
|
}
|
||||||
|
if (!parsed.query.isEmpty()
|
||||||
|
&& parsed.query[0] == '@'
|
||||||
|
&& parsed.fromStart) {
|
||||||
|
peer->session().topGuestChatBots().reload();
|
||||||
|
}
|
||||||
raw->showFiltered(peer, parsed.query, parsed.fromStart);
|
raw->showFiltered(peer, parsed.query, parsed.fromStart);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ EmojiPack::EmojiPack(not_null<Main::Session*> session)
|
|||||||
EmojiPack::~EmojiPack() = default;
|
EmojiPack::~EmojiPack() = default;
|
||||||
|
|
||||||
bool EmojiPack::add(not_null<ViewElement*> view) {
|
bool EmojiPack::add(not_null<ViewElement*> view) {
|
||||||
if (view->data()->textAppearing()) {
|
if (view->data()->textAppearing()
|
||||||
|
|| view->Get<HistoryView::FakeBotAboutTop>()) {
|
||||||
return false;
|
return false;
|
||||||
} else if (const auto custom = view->onlyCustomEmoji()) {
|
} else if (const auto custom = view->onlyCustomEmoji()) {
|
||||||
_onlyCustomItems.emplace(view);
|
_onlyCustomItems.emplace(view);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ DocumentData *GiftBoxPack::lookup(
|
|||||||
if (it == begin(pack.dividers)) {
|
if (it == begin(pack.dividers)) {
|
||||||
return fallback;
|
return fallback;
|
||||||
} else if (it == end(pack.dividers)) {
|
} else if (it == end(pack.dividers)) {
|
||||||
return pack.documents.back();
|
return pack.documents.empty() ? nullptr : pack.documents.back();
|
||||||
}
|
}
|
||||||
const auto shift = exact
|
const auto shift = exact
|
||||||
? ((*it > divider) ? 1 : 0)
|
? ((*it > divider) ? 1 : 0)
|
||||||
|
|||||||
@@ -63,6 +63,24 @@ void UpdateAnimated(
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool MatchAllPreparedSearchWords(
|
||||||
|
const QStringList &titleWords,
|
||||||
|
const QStringList &searchWords) {
|
||||||
|
for (const auto &searchWord : searchWords) {
|
||||||
|
auto found = false;
|
||||||
|
for (const auto &titleWord : titleWords) {
|
||||||
|
if (titleWord.startsWith(searchWord)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
uint64 EmojiSectionSetId(EmojiSection section) {
|
uint64 EmojiSectionSetId(EmojiSection section) {
|
||||||
Expects(section >= EmojiSection::Recent
|
Expects(section >= EmojiSection::Recent
|
||||||
&& section <= EmojiSection::Symbols);
|
&& section <= EmojiSection::Symbols);
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ private:
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool MatchAllPreparedSearchWords(
|
||||||
|
const QStringList &titleWords,
|
||||||
|
const QStringList &searchWords);
|
||||||
|
|
||||||
class StickersListFooter final : public TabbedSelector::InnerFooter {
|
class StickersListFooter final : public TabbedSelector::InnerFooter {
|
||||||
public:
|
public:
|
||||||
struct Descriptor {
|
struct Descriptor {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/image/image.h"
|
#include "ui/image/image.h"
|
||||||
#include "ui/cached_round_corners.h"
|
#include "ui/cached_round_corners.h"
|
||||||
#include "ui/power_saving.h"
|
#include "ui/power_saving.h"
|
||||||
|
#include "ui/ui_utility.h"
|
||||||
#include "lottie/lottie_multi_player.h"
|
#include "lottie/lottie_multi_player.h"
|
||||||
#include "lottie/lottie_single_player.h"
|
#include "lottie/lottie_single_player.h"
|
||||||
#include "lottie/lottie_animation.h"
|
#include "lottie/lottie_animation.h"
|
||||||
@@ -500,15 +501,21 @@ template <typename Callback>
|
|||||||
bool StickersListWidget::enumerateSections(Callback callback) const {
|
bool StickersListWidget::enumerateSections(Callback callback) const {
|
||||||
auto info = SectionInfo();
|
auto info = SectionInfo();
|
||||||
info.top = _search ? _search->height() : 0;
|
info.top = _search ? _search->height() : 0;
|
||||||
|
info.top += searchShortcutsHeight();
|
||||||
const auto &sets = shownSets();
|
const auto &sets = shownSets();
|
||||||
for (auto i = 0; i != sets.size(); ++i) {
|
for (auto i = 0; i != sets.size(); ++i) {
|
||||||
auto &set = sets[i];
|
auto &set = sets[i];
|
||||||
info.section = i;
|
info.section = i;
|
||||||
info.count = set.stickers.size();
|
info.count = set.stickers.size();
|
||||||
|
const auto firstAfterShortcuts = !i
|
||||||
|
&& searchShortcutsShown()
|
||||||
|
&& !searchShortcutSelected();
|
||||||
const auto titleSkip = set.externalLayout
|
const auto titleSkip = set.externalLayout
|
||||||
? st::stickersTrendingHeader
|
? st::stickersTrendingHeader
|
||||||
: setHasTitle(set)
|
: setHasTitle(set)
|
||||||
? st().header
|
? st().header
|
||||||
|
: firstAfterShortcuts
|
||||||
|
? st::stickerPanFirstAfterShortcutsSkip
|
||||||
: st::stickerPanPadding;
|
: st::stickerPanPadding;
|
||||||
info.rowsTop = info.top + titleSkip;
|
info.rowsTop = info.top + titleSkip;
|
||||||
if (set.externalLayout) {
|
if (set.externalLayout) {
|
||||||
@@ -584,6 +591,7 @@ int StickersListWidget::countDesiredHeight(int newWidth) {
|
|||||||
- st().margin.left();
|
- st().margin.left();
|
||||||
_singleSize = QSize(singleWidth, singleWidth);
|
_singleSize = QSize(singleWidth, singleWidth);
|
||||||
setColumnCount(columnCount);
|
setColumnCount(columnCount);
|
||||||
|
refreshSearchShortcutsScroll(newWidth);
|
||||||
|
|
||||||
auto visibleHeight = minimalHeight();
|
auto visibleHeight = minimalHeight();
|
||||||
auto minimalHeight = (visibleHeight - st::stickerPanPadding);
|
auto minimalHeight = (visibleHeight - st::stickerPanPadding);
|
||||||
@@ -604,32 +612,35 @@ int StickersListWidget::countDesiredHeight(int newWidth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::sendSearchRequest() {
|
void StickersListWidget::sendSearchRequest() {
|
||||||
if (_searchSetsRequestId
|
if (_searchNextQuery.isEmpty() || _isEffects) {
|
||||||
|| _searchStickersRequestId
|
|
||||||
|| _searchNextQuery.isEmpty()
|
|
||||||
|| _isEffects) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_searchRequestTimer.cancel();
|
_searchRequestTimer.cancel();
|
||||||
_searchQuery = _searchNextQuery;
|
_searchQuery = _searchNextQuery;
|
||||||
|
|
||||||
auto it = _searchStickersCache.find(_searchQuery);
|
|
||||||
if (it != _searchStickersCache.cend()) {
|
|
||||||
toggleSearchLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toggleSearchLoading(true);
|
|
||||||
if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) {
|
if (_searchQuery == Ui::PremiumGroupFakeEmoticon()) {
|
||||||
toggleSearchLoading(false);
|
toggleSearchLoading(false);
|
||||||
_searchSetsRequestId = 0;
|
|
||||||
_searchSetsCache.emplace(_searchQuery, std::vector<uint64>());
|
_searchSetsCache.emplace(_searchQuery, std::vector<uint64>());
|
||||||
_searchStickersCache.emplace(_searchQuery, std::vector<DocumentId>());
|
_searchStickersCache.emplace(_searchQuery, std::vector<DocumentId>());
|
||||||
showSearchResults();
|
showSearchResults();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
requestSearchStickers(_searchQuery, 0, true);
|
const auto stickersCached = (_searchStickersCache.find(_searchQuery)
|
||||||
|
!= _searchStickersCache.cend());
|
||||||
|
const auto setsCached = (_searchSetsCache.find(_searchQuery)
|
||||||
|
!= _searchSetsCache.cend());
|
||||||
|
if (stickersCached && setsCached) {
|
||||||
|
toggleSearchLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleSearchLoading(true);
|
||||||
|
if (!stickersCached && !_searchStickersRequestId) {
|
||||||
|
requestSearchStickers(_searchQuery, 0, true);
|
||||||
|
}
|
||||||
|
if (!setsCached && !_searchSetsRequestId) {
|
||||||
|
sendSearchSetsRequest(_searchQuery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::sendSearchSetsRequest(const QString &query) {
|
void StickersListWidget::sendSearchSetsRequest(const QString &query) {
|
||||||
@@ -642,7 +653,8 @@ void StickersListWidget::sendSearchSetsRequest(const QString &query) {
|
|||||||
searchResultsDone(query, result);
|
searchResultsDone(query, result);
|
||||||
}).fail([=] {
|
}).fail([=] {
|
||||||
_searchSetsRequestId = 0;
|
_searchSetsRequestId = 0;
|
||||||
if (_searchNextQuery == query) {
|
_searchSetsCache.emplace(query, std::vector<uint64>());
|
||||||
|
if (_searchNextQuery == query && !_searchStickersRequestId) {
|
||||||
toggleSearchLoading(false);
|
toggleSearchLoading(false);
|
||||||
}
|
}
|
||||||
}).handleAllErrors().send();
|
}).handleAllErrors().send();
|
||||||
@@ -651,7 +663,7 @@ void StickersListWidget::sendSearchSetsRequest(const QString &query) {
|
|||||||
void StickersListWidget::requestSearchStickers(
|
void StickersListWidget::requestSearchStickers(
|
||||||
const QString &query,
|
const QString &query,
|
||||||
int offset,
|
int offset,
|
||||||
bool requestSetsOnEmpty) {
|
bool isInitial) {
|
||||||
const auto hash = uint64(0);
|
const auto hash = uint64(0);
|
||||||
_searchStickersRequestId = _api.request(MTPmessages_SearchStickers(
|
_searchStickersRequestId = _api.request(MTPmessages_SearchStickers(
|
||||||
MTP_flags(0),
|
MTP_flags(0),
|
||||||
@@ -662,18 +674,12 @@ void StickersListWidget::requestSearchStickers(
|
|||||||
MTP_int(50),
|
MTP_int(50),
|
||||||
MTP_long(hash)
|
MTP_long(hash)
|
||||||
)).done([=](const MTPmessages_FoundStickers &result) {
|
)).done([=](const MTPmessages_FoundStickers &result) {
|
||||||
searchStickersResultsDone(
|
searchStickersResultsDone(query, offset, isInitial, result);
|
||||||
query,
|
|
||||||
offset,
|
|
||||||
requestSetsOnEmpty,
|
|
||||||
result);
|
|
||||||
}).fail([=] {
|
}).fail([=] {
|
||||||
_searchStickersRequestId = 0;
|
_searchStickersRequestId = 0;
|
||||||
if (requestSetsOnEmpty) {
|
_searchStickersCache.emplace(query, std::vector<DocumentId>());
|
||||||
_searchStickersCache.emplace(query, std::vector<DocumentId>());
|
if (_searchNextQuery == query && !_searchSetsRequestId) {
|
||||||
if (_searchNextQuery == query) {
|
toggleSearchLoading(false);
|
||||||
sendSearchSetsRequest(query);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).handleAllErrors().send();
|
}).handleAllErrors().send();
|
||||||
}
|
}
|
||||||
@@ -707,13 +713,16 @@ void StickersListWidget::searchForSets(
|
|||||||
_api.request(requestId).cancel();
|
_api.request(requestId).cancel();
|
||||||
}
|
}
|
||||||
if (_searchStickersCache.find(cleaned) != _searchStickersCache.cend()
|
if (_searchStickersCache.find(cleaned) != _searchStickersCache.cend()
|
||||||
|| _searchSetsCache.find(cleaned) != _searchSetsCache.cend()) {
|
&& _searchSetsCache.find(cleaned) != _searchSetsCache.cend()) {
|
||||||
_searchRequestTimer.cancel();
|
_searchRequestTimer.cancel();
|
||||||
_searchQuery = _searchNextQuery = cleaned;
|
_searchQuery = _searchNextQuery = cleaned;
|
||||||
} else {
|
} else {
|
||||||
_searchNextQuery = cleaned;
|
_searchNextQuery = cleaned;
|
||||||
_searchRequestTimer.callOnce(kSearchRequestDelay);
|
_searchRequestTimer.callOnce(kSearchRequestDelay);
|
||||||
}
|
}
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
showSearchResults();
|
showSearchResults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,6 +742,11 @@ void StickersListWidget::cancelSetsSearch() {
|
|||||||
_searchSetsCache.clear();
|
_searchSetsCache.clear();
|
||||||
_searchStickersCache.clear();
|
_searchStickersCache.clear();
|
||||||
_searchStickersNextOffset.clear();
|
_searchStickersNextOffset.clear();
|
||||||
|
_searchShortcutSets.clear();
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsScrollMax = 0;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
refreshSearchRows(nullptr);
|
refreshSearchRows(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,9 +769,23 @@ void StickersListWidget::refreshSearchRows(
|
|||||||
|
|
||||||
const auto wasSection = _section;
|
const auto wasSection = _section;
|
||||||
auto wasSets = base::take(_searchSets);
|
auto wasSets = base::take(_searchSets);
|
||||||
|
auto wasShortcuts = base::take(_searchShortcutSets);
|
||||||
const auto guard = gsl::finally([&] {
|
const auto guard = gsl::finally([&] {
|
||||||
if (_section == wasSection && _section == Section::Search) {
|
if (_section == wasSection && _section == Section::Search) {
|
||||||
takeHeavyData(_searchSets, wasSets);
|
takeHeavyData(_searchSets, wasSets);
|
||||||
|
takeHeavyData(_searchShortcutSets, wasShortcuts);
|
||||||
|
auto indices = base::flat_map<uint64, int>();
|
||||||
|
indices.reserve(wasShortcuts.size());
|
||||||
|
auto index = 0;
|
||||||
|
for (const auto &set : wasShortcuts) {
|
||||||
|
indices.emplace(set.id, index++);
|
||||||
|
}
|
||||||
|
for (auto &set : _searchShortcutSets) {
|
||||||
|
const auto i = indices.find(set.id);
|
||||||
|
if (i != end(indices)) {
|
||||||
|
set.ripple = std::move(wasShortcuts[i->second].ripple);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -766,14 +794,17 @@ void StickersListWidget::refreshSearchRows(
|
|||||||
&& (foundStickersIt != _searchStickersCache.end())
|
&& (foundStickersIt != _searchStickersCache.end())
|
||||||
&& !foundStickersIt->second.empty();
|
&& !foundStickersIt->second.empty();
|
||||||
|
|
||||||
fillFilteredStickersRow();
|
|
||||||
|
|
||||||
if (!_isEffects) {
|
if (!_isEffects) {
|
||||||
fillLocalSearchRows(_searchNextQuery);
|
refreshSearchShortcuts(_searchNextQuery, cloudSets);
|
||||||
}
|
}
|
||||||
|
if (searchShortcutSelected()) {
|
||||||
if (hasCloudFoundStickers) {
|
fillSelectedSearchShortcut();
|
||||||
fillFoundStickersRow(foundStickersIt->second);
|
}
|
||||||
|
if (!searchShortcutSelected()) {
|
||||||
|
fillFilteredStickersRow();
|
||||||
|
if (hasCloudFoundStickers) {
|
||||||
|
fillFoundStickersRow(foundStickersIt->second);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!cloudSets && _searchNextQuery.isEmpty()) {
|
if (!cloudSets && _searchNextQuery.isEmpty()) {
|
||||||
showStickerSet(!_mySets.empty()
|
showStickerSet(!_mySets.empty()
|
||||||
@@ -783,9 +814,6 @@ void StickersListWidget::refreshSearchRows(
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSection(Section::Search);
|
setSection(Section::Search);
|
||||||
if (!_isEffects && cloudSets) {
|
|
||||||
fillCloudSearchRows(*cloudSets);
|
|
||||||
}
|
|
||||||
refreshIcons(ValidateIconAnimations::Scroll);
|
refreshIcons(ValidateIconAnimations::Scroll);
|
||||||
_lastMousePosition = QCursor::pos();
|
_lastMousePosition = QCursor::pos();
|
||||||
|
|
||||||
@@ -798,49 +826,231 @@ rpl::producer<int> StickersListWidget::recentShownCount() const {
|
|||||||
return _recentShownCount.value();
|
return _recentShownCount.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::fillLocalSearchRows(const QString &query) {
|
void StickersListWidget::refreshSearchShortcuts(
|
||||||
|
const QString &query,
|
||||||
|
const std::vector<uint64> *cloudSets) {
|
||||||
|
fillLocalSearchShortcuts(query);
|
||||||
|
if (cloudSets) {
|
||||||
|
const auto &sets = session().data().stickers().sets();
|
||||||
|
for (const auto setId : *cloudSets) {
|
||||||
|
if (const auto it = sets.find(setId); it != sets.end()) {
|
||||||
|
addSearchShortcut(it->second.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_searchSelectedSetId
|
||||||
|
&& !ranges::contains(
|
||||||
|
_searchShortcutSets,
|
||||||
|
_searchSelectedSetId,
|
||||||
|
&Set::id)) {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
}
|
||||||
|
refreshSearchShortcutsScroll(width());
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::fillLocalSearchShortcuts(const QString &query) {
|
||||||
const auto searchWordsList = TextUtilities::PrepareSearchWords(query);
|
const auto searchWordsList = TextUtilities::PrepareSearchWords(query);
|
||||||
if (searchWordsList.isEmpty()) {
|
if (searchWordsList.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto searchWordInTitle = [](
|
|
||||||
const QStringList &titleWords,
|
|
||||||
const QString &searchWord) {
|
|
||||||
for (const auto &titleWord : titleWords) {
|
|
||||||
if (titleWord.startsWith(searchWord)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
auto allSearchWordsInTitle = [&](
|
|
||||||
const QStringList &titleWords) {
|
|
||||||
for (const auto &searchWord : searchWordsList) {
|
|
||||||
if (!searchWordInTitle(titleWords, searchWord)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto &sets = session().data().stickers().sets();
|
const auto &sets = session().data().stickers().sets();
|
||||||
for (const auto &[setId, titleWords] : _searchIndex) {
|
for (const auto &[setId, titleWords] : _searchIndex) {
|
||||||
if (allSearchWordsInTitle(titleWords)) {
|
if (!MatchAllPreparedSearchWords(titleWords, searchWordsList)) {
|
||||||
if (const auto it = sets.find(setId); it != sets.end()) {
|
continue;
|
||||||
addSearchRow(it->second.get());
|
} else if (const auto it = sets.find(setId); it != sets.end()) {
|
||||||
}
|
addSearchShortcut(it->second.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::fillCloudSearchRows(
|
bool StickersListWidget::addSearchShortcut(not_null<StickersSet*> set) {
|
||||||
const std::vector<uint64> &cloudSets) {
|
if (ranges::contains(_searchShortcutSets, set->id, &Set::id)) {
|
||||||
const auto &sets = session().data().stickers().sets();
|
return false;
|
||||||
for (const auto setId : cloudSets) {
|
|
||||||
if (const auto it = sets.find(setId); it != sets.end()) {
|
|
||||||
addSearchRow(it->second.get());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const auto skipPremium = !session().premiumPossible();
|
||||||
|
auto elements = PrepareStickers(
|
||||||
|
set->stickers.empty() ? set->covers : set->stickers,
|
||||||
|
skipPremium);
|
||||||
|
if (elements.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_searchShortcutSets.emplace_back(
|
||||||
|
set->id,
|
||||||
|
set,
|
||||||
|
set->flags,
|
||||||
|
set->title,
|
||||||
|
set->shortName,
|
||||||
|
set->count,
|
||||||
|
false,
|
||||||
|
std::move(elements));
|
||||||
|
_searchShortcutSets.back().thumbnailDocument
|
||||||
|
= set->lookupThumbnailDocument();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::fillSelectedSearchShortcut() {
|
||||||
|
const auto &sets = session().data().stickers().sets();
|
||||||
|
const auto it = sets.find(_searchSelectedSetId);
|
||||||
|
if (it == sets.end()) {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto set = it->second.get();
|
||||||
|
const auto skipPremium = !session().premiumPossible();
|
||||||
|
auto elements = PrepareStickers(
|
||||||
|
set->stickers.empty() ? set->covers : set->stickers,
|
||||||
|
skipPremium);
|
||||||
|
if (elements.empty()) {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_searchSets.emplace_back(
|
||||||
|
set->id,
|
||||||
|
set,
|
||||||
|
set->flags | SetFlag::Special,
|
||||||
|
tr::lng_stickers_count(tr::now, lt_count, set->count),
|
||||||
|
set->shortName,
|
||||||
|
set->count,
|
||||||
|
false,
|
||||||
|
std::move(elements));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StickersListWidget::searchShortcutsShown() const {
|
||||||
|
return (_section == Section::Search) && !_searchShortcutSets.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StickersListWidget::searchShortcutSelected() const {
|
||||||
|
return _searchSelectedSetId != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::startSearchSwapAnimation(
|
||||||
|
Fn<void()> change,
|
||||||
|
bool packToPack) {
|
||||||
|
if (!isVisible() || size().isEmpty()) {
|
||||||
|
change();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto top = searchShortcutsTop()
|
||||||
|
+ (packToPack ? searchShortcutsHeight() : 0);
|
||||||
|
const auto computeRect = [&] {
|
||||||
|
const auto bottom = std::max(top + 1, getVisibleBottom());
|
||||||
|
return QRect(0, top, width(), bottom - top);
|
||||||
|
};
|
||||||
|
_searchSwapAnimation.stop();
|
||||||
|
const auto wasSelected = searchShortcutSelected();
|
||||||
|
_searchSwapBefore = Ui::GrabWidget(this, computeRect());
|
||||||
|
_searchSwapTop = top;
|
||||||
|
_searchSwapPartial = packToPack;
|
||||||
|
change();
|
||||||
|
_searchSwapReverse = wasSelected && !searchShortcutSelected();
|
||||||
|
_searchSwapAfter = Ui::GrabWidget(this, computeRect());
|
||||||
|
_searchSwapAnimation.start(
|
||||||
|
[=, this] {
|
||||||
|
update();
|
||||||
|
if (!_searchSwapAnimation.animating()) {
|
||||||
|
_searchSwapBefore = QPixmap();
|
||||||
|
_searchSwapAfter = QPixmap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
st().searchSwapDuration,
|
||||||
|
anim::sineInOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
int StickersListWidget::searchShortcutsTop() const {
|
||||||
|
return _search ? _search->height() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int StickersListWidget::searchShortcutsHeight() const {
|
||||||
|
if (!searchShortcutsShown()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto result = st().searchPacksTop
|
||||||
|
+ st().searchPackHeight
|
||||||
|
+ st().searchPacksBottom;
|
||||||
|
result += searchShortcutSelected()
|
||||||
|
? st().searchBackHeight
|
||||||
|
: st().searchResultsHeight;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect StickersListWidget::searchBackRect() const {
|
||||||
|
return QRect(
|
||||||
|
0,
|
||||||
|
searchShortcutsTop(),
|
||||||
|
width(),
|
||||||
|
searchShortcutSelected() ? st().searchBackHeight : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect StickersListWidget::searchShortcutRect(int index) const {
|
||||||
|
Expects(index >= 0 && index < int(_searchShortcutSets.size()));
|
||||||
|
|
||||||
|
const auto left = st().headerLeft
|
||||||
|
- st().margin.left()
|
||||||
|
- _searchShortcutsScroll
|
||||||
|
+ index * (st().searchPackWidth + st().searchPackSkip);
|
||||||
|
const auto top = searchShortcutsTop()
|
||||||
|
+ (searchShortcutSelected() ? st().searchBackHeight : 0)
|
||||||
|
+ st().searchPacksTop;
|
||||||
|
return QRect(
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
st().searchPackWidth,
|
||||||
|
st().searchPackHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::refreshSearchShortcutsScroll(int newWidth) {
|
||||||
|
if (_searchShortcutSets.empty()) {
|
||||||
|
_searchShortcutsScroll = 0;
|
||||||
|
_searchShortcutsScrollMax = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto count = int(_searchShortcutSets.size());
|
||||||
|
const auto full = st().headerLeft
|
||||||
|
- st().margin.left()
|
||||||
|
+ count * st().searchPackWidth
|
||||||
|
+ std::max(count - 1, 0) * st().searchPackSkip
|
||||||
|
+ st().margin.right();
|
||||||
|
_searchShortcutsScrollMax = std::max(full - newWidth, 0);
|
||||||
|
scrollSearchShortcutsTo(_searchShortcutsScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::scrollSearchShortcutsTo(int value) {
|
||||||
|
const auto scroll = std::clamp(
|
||||||
|
value,
|
||||||
|
0,
|
||||||
|
_searchShortcutsScrollMax);
|
||||||
|
if (_searchShortcutsScroll == scroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_searchShortcutsScroll = scroll;
|
||||||
|
update(0, searchShortcutsTop(), width(), searchShortcutsHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::toggleSearchShortcut(int index) {
|
||||||
|
if (index < 0 || index >= int(_searchShortcutSets.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto setId = _searchShortcutSets[index].id;
|
||||||
|
const auto target = (_searchSelectedSetId == setId) ? 0 : setId;
|
||||||
|
const auto packToPack = _searchSelectedSetId
|
||||||
|
&& target
|
||||||
|
&& _searchSelectedSetId != target;
|
||||||
|
startSearchSwapAnimation([=, this] {
|
||||||
|
_searchSelectedSetId = target;
|
||||||
|
showSearchResults();
|
||||||
|
}, packToPack);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::backToSearchResults() {
|
||||||
|
if (!_searchSelectedSetId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startSearchSwapAnimation([=, this] {
|
||||||
|
_searchSelectedSetId = 0;
|
||||||
|
showSearchResults();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::fillFoundStickersRow(
|
void StickersListWidget::fillFoundStickersRow(
|
||||||
@@ -891,22 +1101,6 @@ void StickersListWidget::fillFilteredStickersRow() {
|
|||||||
std::move(elements));
|
std::move(elements));
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::addSearchRow(not_null<StickersSet*> set) {
|
|
||||||
const auto skipPremium = !session().premiumPossible();
|
|
||||||
auto elements = PrepareStickers(
|
|
||||||
set->stickers.empty() ? set->covers : set->stickers,
|
|
||||||
skipPremium);
|
|
||||||
_searchSets.emplace_back(
|
|
||||||
set->id,
|
|
||||||
set,
|
|
||||||
set->flags,
|
|
||||||
set->title,
|
|
||||||
set->shortName,
|
|
||||||
set->count,
|
|
||||||
!SetInMyList(set->flags),
|
|
||||||
std::move(elements));
|
|
||||||
}
|
|
||||||
|
|
||||||
void StickersListWidget::toggleSearchLoading(bool loading) {
|
void StickersListWidget::toggleSearchLoading(bool loading) {
|
||||||
if (_search) {
|
if (_search) {
|
||||||
_search->setLoading(loading);
|
_search->setLoading(loading);
|
||||||
@@ -999,10 +1193,15 @@ auto StickersListWidget::shownSets() -> std::vector<Set> & {
|
|||||||
void StickersListWidget::searchStickersResultsDone(
|
void StickersListWidget::searchStickersResultsDone(
|
||||||
const QString &query,
|
const QString &query,
|
||||||
int requestedOffset,
|
int requestedOffset,
|
||||||
bool requestSetsOnEmpty,
|
bool isInitial,
|
||||||
const MTPmessages_FoundStickers &result) {
|
const MTPmessages_FoundStickers &result) {
|
||||||
_searchStickersRequestId = 0;
|
_searchStickersRequestId = 0;
|
||||||
const auto active = (_searchNextQuery == query);
|
const auto active = (_searchNextQuery == query);
|
||||||
|
const auto finishLoading = [&] {
|
||||||
|
if (active && !_searchSetsRequestId) {
|
||||||
|
toggleSearchLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
result.match([&](const MTPDmessages_foundStickersNotModified &data) {
|
result.match([&](const MTPDmessages_foundStickersNotModified &data) {
|
||||||
LOG(("API: messages.foundStickersNotModified."));
|
LOG(("API: messages.foundStickersNotModified."));
|
||||||
@@ -1019,11 +1218,12 @@ void StickersListWidget::searchStickersResultsDone(
|
|||||||
if (!active) {
|
if (!active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (requestSetsOnEmpty) {
|
finishLoading();
|
||||||
sendSearchSetsRequest(query);
|
if (isInitial) {
|
||||||
return;
|
showSearchResults();
|
||||||
|
} else {
|
||||||
|
refreshSearchRows();
|
||||||
}
|
}
|
||||||
refreshSearchRows();
|
|
||||||
checkPaginateSearchStickers(
|
checkPaginateSearchStickers(
|
||||||
getVisibleTop(),
|
getVisibleTop(),
|
||||||
getVisibleBottom());
|
getVisibleBottom());
|
||||||
@@ -1054,12 +1254,8 @@ void StickersListWidget::searchStickersResultsDone(
|
|||||||
if (!active) {
|
if (!active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (requestSetsOnEmpty && it->second.empty()) {
|
finishLoading();
|
||||||
sendSearchSetsRequest(query);
|
if (isInitial) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
toggleSearchLoading(false);
|
|
||||||
if (requestSetsOnEmpty) {
|
|
||||||
showSearchResults();
|
showSearchResults();
|
||||||
} else {
|
} else {
|
||||||
refreshSearchRows();
|
refreshSearchRows();
|
||||||
@@ -1106,10 +1302,10 @@ void StickersListWidget::checkPaginateSearchStickers(
|
|||||||
void StickersListWidget::searchResultsDone(
|
void StickersListWidget::searchResultsDone(
|
||||||
const QString &query,
|
const QString &query,
|
||||||
const MTPmessages_FoundStickerSets &result) {
|
const MTPmessages_FoundStickerSets &result) {
|
||||||
if (_searchNextQuery == query) {
|
_searchSetsRequestId = 0;
|
||||||
|
if (_searchNextQuery == query && !_searchStickersRequestId) {
|
||||||
toggleSearchLoading(false);
|
toggleSearchLoading(false);
|
||||||
}
|
}
|
||||||
_searchSetsRequestId = 0;
|
|
||||||
|
|
||||||
result.match([&](const MTPDmessages_foundStickerSetsNotModified &data) {
|
result.match([&](const MTPDmessages_foundStickerSetsNotModified &data) {
|
||||||
LOG(("API Error: "
|
LOG(("API Error: "
|
||||||
@@ -1159,9 +1355,178 @@ void StickersListWidget::paintEvent(QPaintEvent *e) {
|
|||||||
p.fillRect(clip, st().bg);
|
p.fillRect(clip, st().bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_searchSwapAnimation.animating()) {
|
||||||
|
if (_searchSwapPartial) {
|
||||||
|
paintStickers(p, clip);
|
||||||
|
}
|
||||||
|
const auto progress = _searchSwapAnimation.value(1.);
|
||||||
|
const auto direction = _searchSwapReverse ? -1 : 1;
|
||||||
|
const auto slide = st().searchBackHeight;
|
||||||
|
p.setOpacity(1. - progress);
|
||||||
|
p.drawPixmap(
|
||||||
|
0,
|
||||||
|
_searchSwapTop + direction * int(base::SafeRound(slide * progress)),
|
||||||
|
_searchSwapBefore);
|
||||||
|
p.setOpacity(progress);
|
||||||
|
p.drawPixmap(
|
||||||
|
0,
|
||||||
|
_searchSwapTop - direction * int(base::SafeRound(slide * (1. - progress))),
|
||||||
|
_searchSwapAfter);
|
||||||
|
p.setOpacity(1.);
|
||||||
|
return;
|
||||||
|
}
|
||||||
paintStickers(p, clip);
|
paintStickers(p, clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::paintSearchShortcuts(Painter &p, QRect clip) {
|
||||||
|
if (!searchShortcutsShown()
|
||||||
|
|| clip.bottom() < searchShortcutsTop()
|
||||||
|
|| clip.top() >= searchShortcutsTop() + searchShortcutsHeight()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto back = searchBackRect();
|
||||||
|
if (back.height() > 0) {
|
||||||
|
const auto selected = std::get_if<OverSearchBack>(
|
||||||
|
!v::is_null(_pressed) ? &_pressed : &_selected);
|
||||||
|
const auto &icon = selected
|
||||||
|
? st().search.back.iconOver
|
||||||
|
: st().search.back.icon;
|
||||||
|
icon.paint(
|
||||||
|
p,
|
||||||
|
st().searchBackIconLeft,
|
||||||
|
back.y() + st().searchBackIconTop,
|
||||||
|
width());
|
||||||
|
const auto text = tr::lng_search_back_to_results(tr::now);
|
||||||
|
const auto &font = st::emojiPanHeaderFont;
|
||||||
|
const auto available = width()
|
||||||
|
- st().searchBackTextLeft
|
||||||
|
- st().margin.right();
|
||||||
|
auto shown = text;
|
||||||
|
auto textWidth = font->width(shown);
|
||||||
|
if (textWidth > available) {
|
||||||
|
shown = font->elided(shown, available);
|
||||||
|
textWidth = font->width(shown);
|
||||||
|
}
|
||||||
|
p.setFont(font);
|
||||||
|
p.setPen(st().headerFg);
|
||||||
|
p.drawTextLeft(
|
||||||
|
st().searchBackTextLeft,
|
||||||
|
back.y() + st().searchBackTextTop,
|
||||||
|
width(),
|
||||||
|
shown,
|
||||||
|
textWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto selectedShortcut = std::get_if<OverSearchShortcut>(
|
||||||
|
!v::is_null(_pressed) ? &_pressed : &_selected);
|
||||||
|
p.save();
|
||||||
|
p.setClipRect(
|
||||||
|
QRect(
|
||||||
|
0,
|
||||||
|
searchShortcutsTop() + back.height(),
|
||||||
|
width(),
|
||||||
|
st().searchPacksTop
|
||||||
|
+ st().searchPackHeight
|
||||||
|
+ st().searchPacksBottom),
|
||||||
|
Qt::IntersectClip);
|
||||||
|
for (auto i = 0, count = int(_searchShortcutSets.size()); i != count; ++i) {
|
||||||
|
auto &set = _searchShortcutSets[i];
|
||||||
|
const auto rect = searchShortcutRect(i);
|
||||||
|
if (!rect.intersects(clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto selected = (set.id == _searchSelectedSetId)
|
||||||
|
|| (selectedShortcut && selectedShortcut->index == i);
|
||||||
|
if (selected) {
|
||||||
|
_overBg.paint(p, myrtlrect(rect));
|
||||||
|
}
|
||||||
|
if (set.ripple) {
|
||||||
|
set.ripple->paint(
|
||||||
|
p,
|
||||||
|
myrtlrect(rect).x(),
|
||||||
|
rect.y(),
|
||||||
|
width());
|
||||||
|
if (set.ripple->empty()) {
|
||||||
|
set.ripple.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto icon = QRect(
|
||||||
|
rect.x() + (rect.width() - st().searchPackIconSize) / 2,
|
||||||
|
rect.y() + st().searchPackIconTop,
|
||||||
|
st().searchPackIconSize,
|
||||||
|
st().searchPackIconSize);
|
||||||
|
paintSearchShortcutIcon(p, set, icon);
|
||||||
|
|
||||||
|
const auto available = rect.width()
|
||||||
|
- 2 * st().searchPackTextPadding;
|
||||||
|
auto title = set.title;
|
||||||
|
auto titleWidth = st::normalFont->width(title);
|
||||||
|
if (titleWidth > available) {
|
||||||
|
title = st::normalFont->elided(title, available);
|
||||||
|
titleWidth = st::normalFont->width(title);
|
||||||
|
}
|
||||||
|
const auto titleLeft = (titleWidth < available)
|
||||||
|
? (rect.x() + (rect.width() - titleWidth) / 2)
|
||||||
|
: (rect.x() + st().searchPackTextPadding);
|
||||||
|
p.setFont(st::normalFont);
|
||||||
|
p.setPen(st().textFg);
|
||||||
|
p.drawTextLeft(
|
||||||
|
titleLeft,
|
||||||
|
rect.y() + st().searchPackTextTop,
|
||||||
|
width(),
|
||||||
|
title,
|
||||||
|
titleWidth);
|
||||||
|
}
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
if (!searchShortcutSelected()) {
|
||||||
|
const auto top = searchShortcutsTop()
|
||||||
|
+ st().searchPacksTop
|
||||||
|
+ st().searchPackHeight
|
||||||
|
+ st().searchPacksBottom;
|
||||||
|
p.setFont(st::emojiPanHeaderFont);
|
||||||
|
p.setPen(st().headerFg);
|
||||||
|
p.drawTextLeft(
|
||||||
|
st().headerLeft - st().margin.left(),
|
||||||
|
top + st().searchResultsTextTop,
|
||||||
|
width(),
|
||||||
|
tr::lng_search_results_header(tr::now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::paintSearchShortcutIcon(
|
||||||
|
Painter &p,
|
||||||
|
Set &set,
|
||||||
|
QRect rect) {
|
||||||
|
if (set.stickers.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto &sticker = set.stickers.front();
|
||||||
|
sticker.ensureMediaCreated();
|
||||||
|
const auto document = sticker.document;
|
||||||
|
const auto media = sticker.documentMedia.get();
|
||||||
|
media->thumbnailWanted(document->stickerSetOrigin());
|
||||||
|
media->checkStickerSmall();
|
||||||
|
|
||||||
|
const auto size = ComputeStickerSize(document, rect.size());
|
||||||
|
if (size.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto point = rect.topLeft() + QPoint(
|
||||||
|
(rect.width() - size.width()) / 2,
|
||||||
|
(rect.height() - size.height()) / 2);
|
||||||
|
if (const auto image = media->getStickerSmall()) {
|
||||||
|
const auto pixmap = image->pixSingle(size, { .outer = size });
|
||||||
|
p.drawPixmapLeft(point, width(), pixmap);
|
||||||
|
} else {
|
||||||
|
PaintStickerThumbnailPath(
|
||||||
|
p,
|
||||||
|
media,
|
||||||
|
QRect(point, size),
|
||||||
|
_pathGradient.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
||||||
auto fromColumn = floorclamp(
|
auto fromColumn = floorclamp(
|
||||||
clip.x() - stickersLeft(),
|
clip.x() - stickersLeft(),
|
||||||
@@ -1181,6 +1546,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
|||||||
|
|
||||||
_paintAsPremium = session().premium();
|
_paintAsPremium = session().premium();
|
||||||
_pathGradient->startFrame(0, width(), width() / 2);
|
_pathGradient->startFrame(0, width(), width() / 2);
|
||||||
|
paintSearchShortcuts(p, clip);
|
||||||
|
|
||||||
auto &sets = shownSets();
|
auto &sets = shownSets();
|
||||||
const auto selectedSticker = std::get_if<OverSticker>(&_selected);
|
const auto selectedSticker = std::get_if<OverSticker>(&_selected);
|
||||||
@@ -1191,7 +1557,9 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
|||||||
const auto now = crl::now();
|
const auto now = crl::now();
|
||||||
const auto paused = On(PowerSaving::kStickersPanel)
|
const auto paused = On(PowerSaving::kStickersPanel)
|
||||||
|| this->paused();
|
|| this->paused();
|
||||||
if (sets.empty() && _section == Section::Search) {
|
if (sets.empty()
|
||||||
|
&& _searchShortcutSets.empty()
|
||||||
|
&& _section == Section::Search) {
|
||||||
const auto loading = _searchLoading || _searchRequestTimer.isActive();
|
const auto loading = _searchLoading || _searchRequestTimer.isActive();
|
||||||
Inner::paintEmptySearchResults(
|
Inner::paintEmptySearchResults(
|
||||||
p,
|
p,
|
||||||
@@ -2002,8 +2370,15 @@ void StickersListWidget::mousePressEvent(QMouseEvent *e) {
|
|||||||
updateSelected();
|
updateSelected();
|
||||||
|
|
||||||
setPressed(_selected);
|
setPressed(_selected);
|
||||||
|
if (std::get_if<OverSearchShortcut>(&_selected)) {
|
||||||
|
_searchShortcutsMouseDown = _lastMousePosition;
|
||||||
|
_searchShortcutsDragStart = _searchShortcutsScroll;
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
|
}
|
||||||
ClickHandler::pressed();
|
ClickHandler::pressed();
|
||||||
_previewTimer.callOnce(QApplication::startDragTime());
|
if (std::get_if<OverSticker>(&_selected)) {
|
||||||
|
_previewTimer.callOnce(QApplication::startDragTime());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::setPressed(OverState newPressed) {
|
void StickersListWidget::setPressed(OverState newPressed) {
|
||||||
@@ -2014,6 +2389,14 @@ void StickersListWidget::setPressed(OverState newPressed) {
|
|||||||
if (set.ripple) {
|
if (set.ripple) {
|
||||||
set.ripple->lastStop();
|
set.ripple->lastStop();
|
||||||
}
|
}
|
||||||
|
} else if (auto shortcut = std::get_if<OverSearchShortcut>(&_pressed)) {
|
||||||
|
if (shortcut->index >= 0
|
||||||
|
&& shortcut->index < _searchShortcutSets.size()) {
|
||||||
|
auto &set = _searchShortcutSets[shortcut->index];
|
||||||
|
if (set.ripple) {
|
||||||
|
set.ripple->lastStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (std::get_if<OverGroupAdd>(&_pressed)) {
|
} else if (std::get_if<OverGroupAdd>(&_pressed)) {
|
||||||
if (_megagroupSetButtonRipple) {
|
if (_megagroupSetButtonRipple) {
|
||||||
_megagroupSetButtonRipple->lastStop();
|
_megagroupSetButtonRipple->lastStop();
|
||||||
@@ -2029,6 +2412,16 @@ void StickersListWidget::setPressed(OverState newPressed) {
|
|||||||
}
|
}
|
||||||
set.ripple->add(mapFromGlobal(QCursor::pos())
|
set.ripple->add(mapFromGlobal(QCursor::pos())
|
||||||
- buttonRippleTopLeft(button->section));
|
- buttonRippleTopLeft(button->section));
|
||||||
|
} else if (auto shortcut = std::get_if<OverSearchShortcut>(&_pressed)) {
|
||||||
|
if (shortcut->index >= 0
|
||||||
|
&& shortcut->index < _searchShortcutSets.size()) {
|
||||||
|
auto &set = _searchShortcutSets[shortcut->index];
|
||||||
|
if (!set.ripple) {
|
||||||
|
set.ripple = createSearchShortcutRipple(shortcut->index);
|
||||||
|
}
|
||||||
|
set.ripple->add(mapFromGlobal(QCursor::pos())
|
||||||
|
- myrtlrect(searchShortcutRect(shortcut->index)).topLeft());
|
||||||
|
}
|
||||||
} else if (std::get_if<OverGroupAdd>(&_pressed)) {
|
} else if (std::get_if<OverGroupAdd>(&_pressed)) {
|
||||||
if (!_megagroupSetButtonRipple) {
|
if (!_megagroupSetButtonRipple) {
|
||||||
auto mask = Ui::RippleAnimation::RoundRectMask(
|
auto mask = Ui::RippleAnimation::RoundRectMask(
|
||||||
@@ -2044,6 +2437,26 @@ void StickersListWidget::setPressed(OverState newPressed) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::RippleAnimation>
|
||||||
|
StickersListWidget::createSearchShortcutRipple(int index) {
|
||||||
|
Expects(index >= 0 && index < _searchShortcutSets.size());
|
||||||
|
|
||||||
|
const auto setId = _searchShortcutSets[index].id;
|
||||||
|
auto mask = Ui::RippleAnimation::RoundRectMask(
|
||||||
|
searchShortcutRect(index).size(),
|
||||||
|
st::roundRadiusLarge);
|
||||||
|
return std::make_unique<Ui::RippleAnimation>(
|
||||||
|
st::defaultRippleAnimation,
|
||||||
|
std::move(mask),
|
||||||
|
[this, setId] {
|
||||||
|
const auto i = ranges::find(_searchShortcutSets, setId, &Set::id);
|
||||||
|
if (i != _searchShortcutSets.end()) {
|
||||||
|
rtlupdate(searchShortcutRect(
|
||||||
|
int(i - _searchShortcutSets.begin())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QRect StickersListWidget::megagroupSetButtonRectFinal() const {
|
QRect StickersListWidget::megagroupSetButtonRectFinal() const {
|
||||||
auto result = QRect();
|
auto result = QRect();
|
||||||
if (_section == Section::Stickers) {
|
if (_section == Section::Stickers) {
|
||||||
@@ -2238,10 +2651,20 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {
|
|||||||
|
|
||||||
_lastMousePosition = e->globalPos();
|
_lastMousePosition = e->globalPos();
|
||||||
updateSelected();
|
updateSelected();
|
||||||
|
if (_searchShortcutsDragging) {
|
||||||
|
_searchShortcutsDragging = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto &sets = shownSets();
|
auto &sets = shownSets();
|
||||||
if (!v::is_null(pressed) && pressed == _selected) {
|
if (!v::is_null(pressed) && pressed == _selected) {
|
||||||
if (auto sticker = std::get_if<OverSticker>(&pressed)) {
|
if (std::get_if<OverSearchBack>(&pressed)) {
|
||||||
|
backToSearchResults();
|
||||||
|
return;
|
||||||
|
} else if (auto shortcut = std::get_if<OverSearchShortcut>(&pressed)) {
|
||||||
|
toggleSearchShortcut(shortcut->index);
|
||||||
|
return;
|
||||||
|
} else if (auto sticker = std::get_if<OverSticker>(&pressed)) {
|
||||||
Assert(sticker->section >= 0 && sticker->section < sets.size());
|
Assert(sticker->section >= 0 && sticker->section < sets.size());
|
||||||
auto &set = sets[sticker->section];
|
auto &set = sets[sticker->section];
|
||||||
Assert(sticker->index >= 0 && sticker->index < set.stickers.size());
|
Assert(sticker->index >= 0 && sticker->index < set.stickers.size());
|
||||||
@@ -2356,8 +2779,45 @@ void StickersListWidget::setColumnCount(int count) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StickersListWidget::wheelEvent(QWheelEvent *e) {
|
||||||
|
if (searchShortcutsShown() && _searchShortcutsScrollMax > 0) {
|
||||||
|
const auto pos = mapFromGlobal(e->globalPosition().toPoint());
|
||||||
|
if (pos.y() >= searchShortcutsTop()
|
||||||
|
&& pos.y() < searchShortcutsTop() + searchShortcutsHeight()) {
|
||||||
|
const auto angle = e->angleDelta();
|
||||||
|
const auto pixel = e->pixelDelta();
|
||||||
|
const auto horizontal = (angle.x() != 0);
|
||||||
|
const auto vertical = (angle.y() != 0);
|
||||||
|
if (horizontal || vertical) {
|
||||||
|
const auto delta = horizontal
|
||||||
|
? ((rtl() ? -1 : 1)
|
||||||
|
* (pixel.x() ? pixel.x() : angle.x()))
|
||||||
|
: (pixel.y() ? pixel.y() : angle.y());
|
||||||
|
scrollSearchShortcutsTo(_searchShortcutsScroll - delta);
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Inner::wheelEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
void StickersListWidget::mouseMoveEvent(QMouseEvent *e) {
|
void StickersListWidget::mouseMoveEvent(QMouseEvent *e) {
|
||||||
_lastMousePosition = e->globalPos();
|
_lastMousePosition = e->globalPos();
|
||||||
|
if (std::get_if<OverSearchShortcut>(&_pressed)
|
||||||
|
&& _searchShortcutsScrollMax > 0) {
|
||||||
|
const auto delta = _lastMousePosition - _searchShortcutsMouseDown;
|
||||||
|
if (!_searchShortcutsDragging
|
||||||
|
&& delta.manhattanLength() >= QApplication::startDragDistance()) {
|
||||||
|
_searchShortcutsDragging = true;
|
||||||
|
}
|
||||||
|
if (_searchShortcutsDragging) {
|
||||||
|
scrollSearchShortcutsTo(
|
||||||
|
_searchShortcutsDragStart
|
||||||
|
+ (rtl() ? -1 : 1) * -delta.x());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
updateSelected();
|
updateSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2429,6 +2889,9 @@ void StickersListWidget::clearHeavyData() {
|
|||||||
for (auto &set : shownSets()) {
|
for (auto &set : shownSets()) {
|
||||||
clearHeavyIn(set, false);
|
clearHeavyIn(set, false);
|
||||||
}
|
}
|
||||||
|
for (auto &set : _searchShortcutSets) {
|
||||||
|
clearHeavyIn(set, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickersListWidget::refreshStickers() {
|
void StickersListWidget::refreshStickers() {
|
||||||
@@ -2520,20 +2983,45 @@ void StickersListWidget::refreshSearchSets() {
|
|||||||
|
|
||||||
const auto &sets = session().data().stickers().sets();
|
const auto &sets = session().data().stickers().sets();
|
||||||
const auto skipPremium = !session().premiumPossible();
|
const auto skipPremium = !session().premiumPossible();
|
||||||
for (auto &entry : _searchSets) {
|
const auto refreshElements = [&](Set &entry, not_null<StickersSet*> set) {
|
||||||
if (const auto it = sets.find(entry.id); it != sets.end()) {
|
auto elements = PrepareStickers(
|
||||||
const auto set = it->second.get();
|
set->stickers.empty() ? set->covers : set->stickers,
|
||||||
entry.flags = set->flags;
|
skipPremium);
|
||||||
auto elements = PrepareStickers(set->stickers, skipPremium);
|
if (!elements.empty()) {
|
||||||
if (!elements.empty()) {
|
entry.lottiePlayer = nullptr;
|
||||||
entry.lottiePlayer = nullptr;
|
entry.stickers = std::move(elements);
|
||||||
entry.stickers = std::move(elements);
|
|
||||||
}
|
|
||||||
if (!SetInMyList(entry.flags)) {
|
|
||||||
_localSetsManager->removeInstalledLocally(entry.id);
|
|
||||||
entry.externalLayout = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
entry.thumbnailDocument = set->lookupThumbnailDocument();
|
||||||
|
};
|
||||||
|
for (auto &entry : _searchSets) {
|
||||||
|
const auto it = sets.find(entry.id);
|
||||||
|
if (it == sets.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto set = it->second.get();
|
||||||
|
const auto selected = (_searchSelectedSetId == entry.id);
|
||||||
|
entry.flags = selected
|
||||||
|
? (set->flags | SetFlag::Special)
|
||||||
|
: set->flags;
|
||||||
|
refreshElements(entry, set);
|
||||||
|
entry.title = selected
|
||||||
|
? tr::lng_stickers_count(tr::now, lt_count, set->count)
|
||||||
|
: set->title;
|
||||||
|
if (selected) {
|
||||||
|
entry.externalLayout = false;
|
||||||
|
} else if (!SetInMyList(entry.flags)) {
|
||||||
|
_localSetsManager->removeInstalledLocally(entry.id);
|
||||||
|
entry.externalLayout = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto &entry : _searchShortcutSets) {
|
||||||
|
const auto it = sets.find(entry.id);
|
||||||
|
if (it == sets.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto set = it->second.get();
|
||||||
|
entry.title = set->title;
|
||||||
|
refreshElements(entry, set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2934,6 +3422,23 @@ void StickersListWidget::updateSelected() {
|
|||||||
clearSelection();
|
clearSelection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (searchShortcutsShown()
|
||||||
|
&& p.y() >= searchShortcutsTop()
|
||||||
|
&& p.y() < searchShortcutsTop() + searchShortcutsHeight()) {
|
||||||
|
if (searchShortcutSelected() && searchBackRect().contains(p)) {
|
||||||
|
newSelected = OverSearchBack{};
|
||||||
|
} else {
|
||||||
|
for (auto i = 0, count = int(_searchShortcutSets.size());
|
||||||
|
i != count; ++i) {
|
||||||
|
if (myrtlrect(searchShortcutRect(i)).contains(p)) {
|
||||||
|
newSelected = OverSearchShortcut{ i };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelected(newSelected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto &sets = shownSets();
|
auto &sets = shownSets();
|
||||||
auto sx = (rtl() ? width() - p.x() : p.x()) - stickersLeft();
|
auto sx = (rtl() ? width() - p.x() : p.x()) - stickersLeft();
|
||||||
if (!shownSets().empty()) {
|
if (!shownSets().empty()) {
|
||||||
@@ -3031,6 +3536,14 @@ void StickersListWidget::setSelected(OverState newSelected) {
|
|||||||
} else {
|
} else {
|
||||||
rtlupdate(removeButtonRect(button->section));
|
rtlupdate(removeButtonRect(button->section));
|
||||||
}
|
}
|
||||||
|
} else if (auto shortcut
|
||||||
|
= std::get_if<OverSearchShortcut>(&_selected)) {
|
||||||
|
if (shortcut->index >= 0
|
||||||
|
&& shortcut->index < _searchShortcutSets.size()) {
|
||||||
|
rtlupdate(searchShortcutRect(shortcut->index));
|
||||||
|
}
|
||||||
|
} else if (std::get_if<OverSearchBack>(&_selected)) {
|
||||||
|
rtlupdate(searchBackRect());
|
||||||
} else if (std::get_if<OverGroupAdd>(&_selected)) {
|
} else if (std::get_if<OverGroupAdd>(&_selected)) {
|
||||||
rtlupdate(megagroupSetButtonRectFinal());
|
rtlupdate(megagroupSetButtonRectFinal());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "chat_helpers/compose/compose_features.h"
|
#include "chat_helpers/compose/compose_features.h"
|
||||||
#include "chat_helpers/tabbed_selector.h"
|
#include "chat_helpers/tabbed_selector.h"
|
||||||
#include "data/stickers/data_stickers.h"
|
#include "data/stickers/data_stickers.h"
|
||||||
|
#include "ui/effects/animations.h"
|
||||||
#include "ui/round_rect.h"
|
#include "ui/round_rect.h"
|
||||||
#include "base/variant.h"
|
#include "base/variant.h"
|
||||||
#include "base/timer.h"
|
#include "base/timer.h"
|
||||||
@@ -144,6 +145,7 @@ protected:
|
|||||||
void mousePressEvent(QMouseEvent *e) override;
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||||
void mouseMoveEvent(QMouseEvent *e) override;
|
void mouseMoveEvent(QMouseEvent *e) override;
|
||||||
|
void wheelEvent(QWheelEvent *e) override;
|
||||||
void resizeEvent(QResizeEvent *e) override;
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
void paintEvent(QPaintEvent *e) override;
|
void paintEvent(QPaintEvent *e) override;
|
||||||
void leaveEventHook(QEvent *e) override;
|
void leaveEventHook(QEvent *e) override;
|
||||||
@@ -199,6 +201,24 @@ private:
|
|||||||
return !(*this == other);
|
return !(*this == other);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
struct OverSearchShortcut {
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
inline bool operator==(OverSearchShortcut other) const {
|
||||||
|
return (index == other.index);
|
||||||
|
}
|
||||||
|
inline bool operator!=(OverSearchShortcut other) const {
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct OverSearchBack {
|
||||||
|
inline bool operator==(OverSearchBack other) const {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
inline bool operator!=(OverSearchBack other) const {
|
||||||
|
return !(*this == other);
|
||||||
|
}
|
||||||
|
};
|
||||||
struct OverGroupAdd {
|
struct OverGroupAdd {
|
||||||
inline bool operator==(OverGroupAdd other) const {
|
inline bool operator==(OverGroupAdd other) const {
|
||||||
return true;
|
return true;
|
||||||
@@ -212,6 +232,8 @@ private:
|
|||||||
OverSticker,
|
OverSticker,
|
||||||
OverSet,
|
OverSet,
|
||||||
OverButton,
|
OverButton,
|
||||||
|
OverSearchShortcut,
|
||||||
|
OverSearchBack,
|
||||||
OverGroupAdd>;
|
OverGroupAdd>;
|
||||||
|
|
||||||
struct SectionInfo {
|
struct SectionInfo {
|
||||||
@@ -271,6 +293,8 @@ private:
|
|||||||
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
|
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
|
||||||
int section);
|
int section);
|
||||||
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
|
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
|
||||||
|
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation>
|
||||||
|
createSearchShortcutRipple(int index);
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Set> &shownSets();
|
[[nodiscard]] std::vector<Set> &shownSets();
|
||||||
[[nodiscard]] const std::vector<Set> &shownSets() const;
|
[[nodiscard]] const std::vector<Set> &shownSets() const;
|
||||||
@@ -366,21 +390,37 @@ private:
|
|||||||
void requestSearchStickers(
|
void requestSearchStickers(
|
||||||
const QString &query,
|
const QString &query,
|
||||||
int offset,
|
int offset,
|
||||||
bool requestSetsOnEmpty);
|
bool isInitial);
|
||||||
void searchStickersResultsDone(
|
void searchStickersResultsDone(
|
||||||
const QString &query,
|
const QString &query,
|
||||||
int requestedOffset,
|
int requestedOffset,
|
||||||
bool requestSetsOnEmpty,
|
bool isInitial,
|
||||||
const MTPmessages_FoundStickers &result);
|
const MTPmessages_FoundStickers &result);
|
||||||
void loadMoreSearchStickers();
|
void loadMoreSearchStickers();
|
||||||
void checkPaginateSearchStickers(int visibleTop, int visibleBottom);
|
void checkPaginateSearchStickers(int visibleTop, int visibleBottom);
|
||||||
void refreshSearchRows();
|
void refreshSearchRows();
|
||||||
void refreshSearchRows(const std::vector<uint64> *cloudSets);
|
void refreshSearchRows(const std::vector<uint64> *cloudSets);
|
||||||
|
void refreshSearchShortcuts(
|
||||||
|
const QString &query,
|
||||||
|
const std::vector<uint64> *cloudSets);
|
||||||
|
void fillLocalSearchShortcuts(const QString &query);
|
||||||
|
bool addSearchShortcut(not_null<Data::StickersSet*> set);
|
||||||
|
void fillSelectedSearchShortcut();
|
||||||
|
[[nodiscard]] bool searchShortcutsShown() const;
|
||||||
|
[[nodiscard]] bool searchShortcutSelected() const;
|
||||||
|
void startSearchSwapAnimation(Fn<void()> change, bool packToPack = false);
|
||||||
|
[[nodiscard]] int searchShortcutsHeight() const;
|
||||||
|
[[nodiscard]] int searchShortcutsTop() const;
|
||||||
|
[[nodiscard]] QRect searchBackRect() const;
|
||||||
|
[[nodiscard]] QRect searchShortcutRect(int index) const;
|
||||||
|
void refreshSearchShortcutsScroll(int newWidth);
|
||||||
|
void scrollSearchShortcutsTo(int value);
|
||||||
|
void paintSearchShortcuts(Painter &p, QRect clip);
|
||||||
|
void paintSearchShortcutIcon(Painter &p, Set &set, QRect rect);
|
||||||
|
void toggleSearchShortcut(int index);
|
||||||
|
void backToSearchResults();
|
||||||
void fillFilteredStickersRow();
|
void fillFilteredStickersRow();
|
||||||
void fillLocalSearchRows(const QString &query);
|
|
||||||
void fillCloudSearchRows(const std::vector<uint64> &cloudSets);
|
|
||||||
void fillFoundStickersRow(const std::vector<DocumentId> &stickerIds);
|
void fillFoundStickersRow(const std::vector<DocumentId> &stickerIds);
|
||||||
void addSearchRow(not_null<Data::StickersSet*> set);
|
|
||||||
void toggleSearchLoading(bool loading);
|
void toggleSearchLoading(bool loading);
|
||||||
|
|
||||||
void showPreview();
|
void showPreview();
|
||||||
@@ -403,6 +443,7 @@ private:
|
|||||||
std::vector<Set> _mySets;
|
std::vector<Set> _mySets;
|
||||||
std::vector<Set> _officialSets;
|
std::vector<Set> _officialSets;
|
||||||
std::vector<Set> _searchSets;
|
std::vector<Set> _searchSets;
|
||||||
|
std::vector<Set> _searchShortcutSets;
|
||||||
int _featuredSetsCount = 0;
|
int _featuredSetsCount = 0;
|
||||||
std::vector<bool> _custom;
|
std::vector<bool> _custom;
|
||||||
std::vector<EmojiPtr> _cornerEmoji;
|
std::vector<EmojiPtr> _cornerEmoji;
|
||||||
@@ -467,6 +508,18 @@ private:
|
|||||||
std::vector<std::pair<uint64, QStringList>> _searchIndex;
|
std::vector<std::pair<uint64, QStringList>> _searchIndex;
|
||||||
base::Timer _searchRequestTimer;
|
base::Timer _searchRequestTimer;
|
||||||
QString _searchQuery, _searchNextQuery;
|
QString _searchQuery, _searchNextQuery;
|
||||||
|
uint64 _searchSelectedSetId = 0;
|
||||||
|
int _searchShortcutsScroll = 0;
|
||||||
|
int _searchShortcutsScrollMax = 0;
|
||||||
|
int _searchShortcutsDragStart = 0;
|
||||||
|
QPoint _searchShortcutsMouseDown;
|
||||||
|
bool _searchShortcutsDragging = false;
|
||||||
|
Ui::Animations::Simple _searchSwapAnimation;
|
||||||
|
QPixmap _searchSwapBefore;
|
||||||
|
QPixmap _searchSwapAfter;
|
||||||
|
int _searchSwapTop = 0;
|
||||||
|
bool _searchSwapReverse = false;
|
||||||
|
bool _searchSwapPartial = false;
|
||||||
mtpRequestId _searchSetsRequestId = 0;
|
mtpRequestId _searchSetsRequestId = 0;
|
||||||
mtpRequestId _searchStickersRequestId = 0;
|
mtpRequestId _searchStickersRequestId = 0;
|
||||||
bool _searchLoading = false;
|
bool _searchLoading = false;
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ enum {
|
|||||||
LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway)
|
LocalEncryptNoPwdIterCount = 4, // key derivation iteration count without pwd (not secure anyway)
|
||||||
LocalEncryptSaltSize = 32, // 256 bit
|
LocalEncryptSaltSize = 32, // 256 bit
|
||||||
|
|
||||||
RecentInlineBotsLimit = 10,
|
|
||||||
|
|
||||||
AutoSearchTimeout = 900, // 0.9 secs
|
AutoSearchTimeout = 900, // 0.9 secs
|
||||||
|
|
||||||
PreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request
|
PreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request
|
||||||
|
|||||||
@@ -1153,7 +1153,25 @@ void Application::checkStartUrls() {
|
|||||||
if (!cRefStartUrls().isEmpty()
|
if (!cRefStartUrls().isEmpty()
|
||||||
&& _lastActivePrimaryWindow
|
&& _lastActivePrimaryWindow
|
||||||
&& !_lastActivePrimaryWindow->locked()) {
|
&& !_lastActivePrimaryWindow->locked()) {
|
||||||
_lastActivePrimaryWindow->widget()->sendPaths();
|
auto interprets = QStringList();
|
||||||
|
auto paths = QStringList();
|
||||||
|
cRefStartUrls() = ranges::views::all(
|
||||||
|
cRefStartUrls()
|
||||||
|
) | ranges::views::filter([&](const QUrl &url) {
|
||||||
|
if (url.scheme() == u"interpret"_q) {
|
||||||
|
interprets.append(url.path());
|
||||||
|
return false;
|
||||||
|
} else if (url.isLocalFile()) {
|
||||||
|
paths.append(url.toLocalFile());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}) | ranges::to<QList<QUrl>>;
|
||||||
|
if (!interprets.isEmpty() || !paths.isEmpty()) {
|
||||||
|
_lastActivePrimaryWindow->widget()->handleStartFiles(
|
||||||
|
std::move(interprets),
|
||||||
|
std::move(paths));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ constexpr auto kReminderSetToastDuration = 4 * crl::time(1000);
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsTelegramShortLinkHost(const QUrl &url) {
|
||||||
|
using namespace qthelp;
|
||||||
|
|
||||||
|
return regex_match(
|
||||||
|
"(^|\\.)(telegram\\.(me|dog)|t\\.me)$",
|
||||||
|
url.host(),
|
||||||
|
RegExOption::CaseInsensitive).valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool HiddenUrlRequiresConfirmation(const QUrl &url) {
|
||||||
|
return UrlRequiresConfirmation(url) || IsTelegramShortLinkHost(url);
|
||||||
|
}
|
||||||
|
|
||||||
// Possible context owners: media viewer, profile, history widget.
|
// Possible context owners: media viewer, profile, history widget.
|
||||||
|
|
||||||
void SearchByHashtag(ClickContext context, const QString &tag) {
|
void SearchByHashtag(ClickContext context, const QString &tag) {
|
||||||
@@ -254,7 +267,8 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
|
|||||||
const auto parsedUrl = url.startsWith(u"tonsite://"_q)
|
const auto parsedUrl = url.startsWith(u"tonsite://"_q)
|
||||||
? QUrl(url)
|
? QUrl(url)
|
||||||
: QUrl::fromUserInput(url);
|
: QUrl::fromUserInput(url);
|
||||||
if (UrlRequiresConfirmation(parsedUrl) && !base::IsCtrlPressed()) {
|
if (HiddenUrlRequiresConfirmation(parsedUrl)
|
||||||
|
&& !base::IsCtrlPressed()) {
|
||||||
const auto my = context.value<ClickHandlerContext>();
|
const auto my = context.value<ClickHandlerContext>();
|
||||||
if (!my.show) {
|
if (!my.show) {
|
||||||
Core::App().hideMediaView();
|
Core::App().hideMediaView();
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "base/zlib_help.h"
|
#include "base/zlib_help.h"
|
||||||
|
|
||||||
#include <QtWidgets/QFileDialog>
|
#include <QtWidgets/QFileDialog>
|
||||||
|
#include <QtWidgets/QMenu>
|
||||||
|
#include <QtGui/QClipboard>
|
||||||
|
#include <QtGui/QContextMenuEvent>
|
||||||
#include <QtGui/QFontInfo>
|
#include <QtGui/QFontInfo>
|
||||||
|
#include <QtGui/QGuiApplication>
|
||||||
#include <QtGui/QScreen>
|
#include <QtGui/QScreen>
|
||||||
#include <QtGui/QDesktopServices>
|
#include <QtGui/QDesktopServices>
|
||||||
#include <QtCore/QStandardPaths>
|
#include <QtCore/QStandardPaths>
|
||||||
@@ -114,6 +118,46 @@ void PreLaunchLabel::setText(const QString &text) {
|
|||||||
resize(sizeHint());
|
resize(sizeHint());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PreLaunchLabel::contextMenuEvent(QContextMenuEvent *e) {
|
||||||
|
const auto flags = textInteractionFlags();
|
||||||
|
const auto selectable = flags
|
||||||
|
& (Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
|
||||||
|
if (!selectable) {
|
||||||
|
e->ignore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto accel = [](QKeySequence::StandardKey key) {
|
||||||
|
return QCoreApplication::testAttribute(
|
||||||
|
Qt::AA_DontShowShortcutsInContextMenus)
|
||||||
|
? QString()
|
||||||
|
: QChar('\t')
|
||||||
|
+ QKeySequence(key).toString(QKeySequence::NativeText);
|
||||||
|
};
|
||||||
|
const auto menu = new QMenu(this);
|
||||||
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
|
||||||
|
const auto copy = menu->addAction(
|
||||||
|
u"&Copy"_q + accel(QKeySequence::Copy));
|
||||||
|
copy->setEnabled(hasSelectedText());
|
||||||
|
connect(copy, &QAction::triggered, this, [=] {
|
||||||
|
if (hasSelectedText()) {
|
||||||
|
QGuiApplication::clipboard()->setText(selectedText());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
menu->addSeparator();
|
||||||
|
|
||||||
|
const auto selectAll = menu->addAction(
|
||||||
|
u"Select All"_q + accel(QKeySequence::SelectAll));
|
||||||
|
selectAll->setEnabled(!text().isEmpty());
|
||||||
|
connect(selectAll, &QAction::triggered, this, [=] {
|
||||||
|
setSelection(0, text().size());
|
||||||
|
});
|
||||||
|
|
||||||
|
e->accept();
|
||||||
|
menu->popup(e->globalPos());
|
||||||
|
}
|
||||||
|
|
||||||
PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) {
|
PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) {
|
||||||
QFont logFont(font());
|
QFont logFont(font());
|
||||||
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ public:
|
|||||||
PreLaunchLabel(QWidget *parent);
|
PreLaunchLabel(QWidget *parent);
|
||||||
void setText(const QString &text);
|
void setText(const QString &text);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class PreLaunchInput : public QLineEdit {
|
class PreLaunchInput : public QLineEdit {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "boxes/gift_premium_box.h"
|
#include "boxes/gift_premium_box.h"
|
||||||
#include "boxes/edit_privacy_box.h"
|
#include "boxes/edit_privacy_box.h"
|
||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
|
#include "boxes/preview_ai_tone_box.h"
|
||||||
#include "boxes/sticker_set_box.h"
|
#include "boxes/sticker_set_box.h"
|
||||||
#include "boxes/star_gift_box.h"
|
#include "boxes/star_gift_box.h"
|
||||||
#include "boxes/language_box.h"
|
#include "boxes/language_box.h"
|
||||||
@@ -40,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/toast/toast.h"
|
#include "ui/toast/toast.h"
|
||||||
#include "ui/vertical_list.h"
|
#include "ui/vertical_list.h"
|
||||||
#include "data/components/credits.h"
|
#include "data/components/credits.h"
|
||||||
|
#include "data/data_ai_compose_tones.h"
|
||||||
#include "data/data_birthday.h"
|
#include "data/data_birthday.h"
|
||||||
#include "data/data_channel.h"
|
#include "data/data_channel.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
@@ -294,6 +296,41 @@ bool ShowTheme(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ShowAiStyle(
|
||||||
|
Window::SessionController *controller,
|
||||||
|
const Match &match,
|
||||||
|
const QVariant &context) {
|
||||||
|
if (!controller) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto slug = match->captured(1);
|
||||||
|
Core::App().hideMediaView();
|
||||||
|
const auto weak = base::make_weak(controller);
|
||||||
|
auto &tones = controller->session().data().aiComposeTones();
|
||||||
|
tones.resolve(slug, [=](Data::AiComposeTone tone) {
|
||||||
|
const auto strong = weak.get();
|
||||||
|
if (!strong) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
strong->window().show(Box(
|
||||||
|
PreviewAiToneBox,
|
||||||
|
&strong->session(),
|
||||||
|
std::move(tone)));
|
||||||
|
}, [=](const MTP::Error &error) {
|
||||||
|
const auto strong = weak.get();
|
||||||
|
if (!strong) {
|
||||||
|
return;
|
||||||
|
} else if (error.type() == u"AICOMPOSE_TONE_SLUG_INVALID"_q) {
|
||||||
|
strong->window().showToast(
|
||||||
|
tr::lng_ai_compose_tone_invalid(tr::now));
|
||||||
|
} else if (!MTP::IgnoreError(error)) {
|
||||||
|
strong->window().showToast(error.type());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
controller->window().activate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ShowLanguagesBox(Window::SessionController *controller) {
|
void ShowLanguagesBox(Window::SessionController *controller) {
|
||||||
static auto Guard = base::binary_guard();
|
static auto Guard = base::binary_guard();
|
||||||
Guard = LanguageBox::Show(controller);
|
Guard = LanguageBox::Show(controller);
|
||||||
@@ -550,7 +587,8 @@ bool ResolveUsernameOrPhone(
|
|||||||
UrlAuthBox::ActivateUrl(
|
UrlAuthBox::ActivateUrl(
|
||||||
controller->uiShow(),
|
controller->uiShow(),
|
||||||
&controller->session(),
|
&controller->session(),
|
||||||
u"tg://resolve?domain=oauth&startapp="_q + token,
|
u"tg://resolve?domain=oauth&startapp="_q
|
||||||
|
+ qthelp::url_encode(token),
|
||||||
context);
|
context);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1629,7 +1667,7 @@ bool ResolveOAuth(
|
|||||||
UrlAuthBox::ActivateUrl(
|
UrlAuthBox::ActivateUrl(
|
||||||
controller->uiShow(),
|
controller->uiShow(),
|
||||||
&controller->session(),
|
&controller->session(),
|
||||||
u"tg://oauth?token="_q + token,
|
u"tg://oauth?token="_q + qthelp::url_encode(token),
|
||||||
context);
|
context);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1660,6 +1698,10 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
|
|||||||
u"^addtheme/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q,
|
u"^addtheme/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q,
|
||||||
ShowTheme
|
ShowTheme
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
u"^addstyle/?\\?slug=([a-zA-Z0-9\\.\\_]+)(&|$)"_q,
|
||||||
|
ShowAiStyle
|
||||||
|
},
|
||||||
{
|
{
|
||||||
u"^setlanguage/?(\\?lang=([a-zA-Z0-9\\.\\_\\-]+))?(&|$)"_q,
|
u"^setlanguage/?(\\?lang=([a-zA-Z0-9\\.\\_\\-]+))?(&|$)"_q,
|
||||||
SetLanguage
|
SetLanguage
|
||||||
@@ -1877,6 +1919,8 @@ QString TryConvertUrlToLocal(QString url) {
|
|||||||
return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2));
|
return u"tg://"_q + stickerSetMatch->captured(1) + "?set=" + url_encode(stickerSetMatch->captured(2));
|
||||||
} else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) {
|
} else if (const auto themeMatch = regex_match(u"^addtheme/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) {
|
||||||
return u"tg://addtheme?slug="_q + url_encode(themeMatch->captured(1));
|
return u"tg://addtheme?slug="_q + url_encode(themeMatch->captured(1));
|
||||||
|
} else if (const auto addStyleMatch = regex_match(u"^addstyle/([a-zA-Z0-9\\.\\_]+)(\\?|$)"_q, query, matchOptions)) {
|
||||||
|
return u"tg://addstyle?slug="_q + url_encode(addStyleMatch->captured(1));
|
||||||
} else if (const auto languageMatch = regex_match(u"^setlanguage/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
|
} else if (const auto languageMatch = regex_match(u"^setlanguage/([a-zA-Z0-9\\.\\_\\-]+)(\\?|$)"_q, query, matchOptions)) {
|
||||||
return u"tg://setlanguage?lang="_q + url_encode(languageMatch->captured(1));
|
return u"tg://setlanguage?lang="_q + url_encode(languageMatch->captured(1));
|
||||||
} else if (const auto shareUrlMatch = regex_match(u"^share/url/?\\?(.+)$"_q, query, matchOptions)) {
|
} else if (const auto shareUrlMatch = regex_match(u"^share/url/?\\?(.+)$"_q, query, matchOptions)) {
|
||||||
@@ -2001,46 +2045,64 @@ QString TryConvertUrlToLocal(QString url) {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InternalPassportOrOAuthLink(const QString &url) {
|
struct InternalLinkCheckResult {
|
||||||
|
QString command;
|
||||||
|
QString username;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] InternalLinkCheckResult InternalLinkCheck(const QString &url) {
|
||||||
const auto urlTrimmed = url.trimmed();
|
const auto urlTrimmed = url.trimmed();
|
||||||
if (!urlTrimmed.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
|
if (!urlTrimmed.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
const auto command = base::StringViewMid(urlTrimmed, u"tg://"_q.size());
|
const auto command = base::StringViewMid(urlTrimmed, u"tg://"_q.size());
|
||||||
|
|
||||||
using namespace qthelp;
|
using namespace qthelp;
|
||||||
const auto matchOptions = RegExOption::CaseInsensitive;
|
const auto matchOptions = RegExOption::CaseInsensitive;
|
||||||
const auto authMatch = regex_match(
|
|
||||||
u"^passport/?\\?(.+)(#|$)"_q,
|
|
||||||
command,
|
|
||||||
matchOptions);
|
|
||||||
const auto oauthMatch = regex_match(
|
|
||||||
u"^oauth/?\\?(.+)(#|$)"_q,
|
|
||||||
command,
|
|
||||||
matchOptions);
|
|
||||||
const auto usernameMatch = regex_match(
|
const auto usernameMatch = regex_match(
|
||||||
u"^resolve/?\\?(.+)(#|$)"_q,
|
u"^resolve/?\\?(.+)(#|$)"_q,
|
||||||
command,
|
command,
|
||||||
matchOptions);
|
matchOptions);
|
||||||
auto usernameValue = QString();
|
auto username = QString();
|
||||||
if (usernameMatch->hasMatch()) {
|
if (usernameMatch->hasMatch()) {
|
||||||
const auto params = url_parse_params(
|
const auto params = url_parse_params(
|
||||||
usernameMatch->captured(1),
|
usernameMatch->captured(1),
|
||||||
UrlParamNameTransform::ToLower);
|
UrlParamNameTransform::ToLower);
|
||||||
usernameValue = params.value(u"domain"_q);
|
username = params.value(u"domain"_q);
|
||||||
}
|
}
|
||||||
const auto authLegacy = (usernameValue == u"telegrampassport"_q);
|
return { .command = command.toString(), .username = username };
|
||||||
const auto oauthLegacy = (usernameValue == u"oauth"_q);
|
}
|
||||||
return authMatch->hasMatch()
|
|
||||||
|
bool InternalPassportLink(const QString &url) {
|
||||||
|
const auto result = InternalLinkCheck(url);
|
||||||
|
|
||||||
|
using namespace qthelp;
|
||||||
|
const auto matchOptions = RegExOption::CaseInsensitive;
|
||||||
|
const auto authMatch = regex_match(
|
||||||
|
u"^passport/?\\?(.+)(#|$)"_q,
|
||||||
|
result.command,
|
||||||
|
matchOptions);
|
||||||
|
const auto authLegacy = (result.username == u"telegrampassport"_q);
|
||||||
|
return authMatch->hasMatch() || authLegacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InternalPassportOrOAuthLink(const QString &url) {
|
||||||
|
const auto result = InternalLinkCheck(url);
|
||||||
|
|
||||||
|
using namespace qthelp;
|
||||||
|
const auto matchOptions = RegExOption::CaseInsensitive;
|
||||||
|
const auto oauthMatch = regex_match(
|
||||||
|
u"^oauth/?\\?(.+)(#|$)"_q,
|
||||||
|
result.command,
|
||||||
|
matchOptions);
|
||||||
|
const auto oauthLegacy = (result.username == u"oauth"_q);
|
||||||
|
return InternalPassportLink(url)
|
||||||
|| oauthMatch->hasMatch()
|
|| oauthMatch->hasMatch()
|
||||||
|| authLegacy
|
|
||||||
|| oauthLegacy;
|
|| oauthLegacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StartUrlRequiresActivate(const QString &url) {
|
bool StartUrlRequiresActivate(const QString &url) {
|
||||||
return Core::App().passcodeLocked()
|
return Core::App().passcodeLocked() || !InternalPassportLink(url);
|
||||||
? true
|
|
||||||
: !InternalPassportOrOAuthLink(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResolveAndShowUniqueGift(
|
void ResolveAndShowUniqueGift(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
|||||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||||
constexpr auto AppFile = "Telegram"_cs;
|
constexpr auto AppFile = "Telegram"_cs;
|
||||||
constexpr auto AppVersion = 6007009;
|
constexpr auto AppVersion = 6008001;
|
||||||
constexpr auto AppVersionStr = "6.7.9";
|
constexpr auto AppVersionStr = "6.8.1";
|
||||||
constexpr auto AppBetaVersion = false;
|
constexpr auto AppBetaVersion = false;
|
||||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||||
|
|||||||
@@ -302,7 +302,13 @@ QString CountriesInstance::validPhoneCode(QString fullCode) const {
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CountriesInstance::countryNameByISO2(const QString &iso) const {
|
QString CountriesInstance::countryNameByISO2(
|
||||||
|
const QString &iso,
|
||||||
|
Naming naming) const {
|
||||||
|
if (naming == Naming::Polls
|
||||||
|
&& !iso.compare(u"FT"_q, Qt::CaseInsensitive)) {
|
||||||
|
return u"Fragment"_q;
|
||||||
|
}
|
||||||
const auto &listByISO2 = byISO2();
|
const auto &listByISO2 = byISO2();
|
||||||
const auto i = listByISO2.constFind(iso);
|
const auto i = listByISO2.constFind(iso);
|
||||||
return (i != listByISO2.cend()) ? (*i)->name : QString();
|
return (i != listByISO2.cend()) ? (*i)->name : QString();
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ struct Info {
|
|||||||
bool isHidden = false;
|
bool isHidden = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class Naming : uchar {
|
||||||
|
Default,
|
||||||
|
Polls,
|
||||||
|
};
|
||||||
|
|
||||||
struct FormatResult {
|
struct FormatResult {
|
||||||
QString formatted;
|
QString formatted;
|
||||||
QVector<int> groups;
|
QVector<int> groups;
|
||||||
@@ -50,7 +55,9 @@ public:
|
|||||||
[[nodiscard]] const Map &byISO2() const;
|
[[nodiscard]] const Map &byISO2() const;
|
||||||
|
|
||||||
[[nodiscard]] QString validPhoneCode(QString fullCode) const;
|
[[nodiscard]] QString validPhoneCode(QString fullCode) const;
|
||||||
[[nodiscard]] QString countryNameByISO2(const QString &iso) const;
|
[[nodiscard]] QString countryNameByISO2(
|
||||||
|
const QString &iso,
|
||||||
|
Naming naming = Naming::Default) const;
|
||||||
[[nodiscard]] QString countryISO2ByPhone(const QString &phone) const;
|
[[nodiscard]] QString countryISO2ByPhone(const QString &phone) const;
|
||||||
[[nodiscard]] QString flagEmojiByISO2(const QString &iso) const;
|
[[nodiscard]] QString flagEmojiByISO2(const QString &iso) const;
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
|
|||||||
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
|
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
|
||||||
MTP_long(data.vvia_bot_id().value_or_empty()),
|
MTP_long(data.vvia_bot_id().value_or_empty()),
|
||||||
MTP_long(data.vvia_business_bot_id().value_or_empty()),
|
MTP_long(data.vvia_business_bot_id().value_or_empty()),
|
||||||
|
data.vguestchat_via_from() ? *data.vguestchat_via_from() : MTPPeer(),
|
||||||
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
|
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
|
||||||
data.vdate(),
|
data.vdate(),
|
||||||
data.vmessage(),
|
data.vmessage(),
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
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 "data/components/recent_inline_bots.h"
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "data/data_user.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "storage/storage_account.h"
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kLimit = 10;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RecentInlineBots::RecentInlineBots(not_null<Main::Session*> session)
|
||||||
|
: _session(session) {
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<not_null<UserData*>> &RecentInlineBots::list() const {
|
||||||
|
_session->local().readRecentHashtagsAndBots();
|
||||||
|
return _list;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> RecentInlineBots::updates() const {
|
||||||
|
return _updates.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecentInlineBots::bump(not_null<UserData*> user) {
|
||||||
|
_session->local().readRecentHashtagsAndBots();
|
||||||
|
|
||||||
|
if (!_list.empty() && _list.front() == user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto i = ranges::find(_list, user);
|
||||||
|
if (i == end(_list)) {
|
||||||
|
if (int(_list.size()) >= kLimit) {
|
||||||
|
_list.pop_back();
|
||||||
|
}
|
||||||
|
_list.insert(begin(_list), user);
|
||||||
|
} else {
|
||||||
|
ranges::rotate(begin(_list), i, i + 1);
|
||||||
|
}
|
||||||
|
_updates.fire({});
|
||||||
|
|
||||||
|
_session->local().writeRecentHashtagsAndBots();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecentInlineBots::remove(not_null<UserData*> user) {
|
||||||
|
const auto i = ranges::find(_list, user);
|
||||||
|
if (i != end(_list)) {
|
||||||
|
_list.erase(i);
|
||||||
|
_updates.fire({});
|
||||||
|
_session->local().writeRecentHashtagsAndBots();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecentInlineBots::applyLocal(
|
||||||
|
std::vector<not_null<UserData*>> list) {
|
||||||
|
_list = std::move(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Data
|
||||||
@@ -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
|
||||||
|
|
||||||
|
class UserData;
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
|
||||||
|
class RecentInlineBots final {
|
||||||
|
public:
|
||||||
|
explicit RecentInlineBots(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
[[nodiscard]] const std::vector<not_null<UserData*>> &list() const;
|
||||||
|
[[nodiscard]] rpl::producer<> updates() const;
|
||||||
|
|
||||||
|
void bump(not_null<UserData*> user);
|
||||||
|
void remove(not_null<UserData*> user);
|
||||||
|
void applyLocal(std::vector<not_null<UserData*>> list);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const not_null<Main::Session*> _session;
|
||||||
|
std::vector<not_null<UserData*>> _list;
|
||||||
|
rpl::event_stream<> _updates;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Data
|
||||||
@@ -77,6 +77,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
|
|||||||
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
|
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
|
||||||
MTP_long(data.vvia_bot_id().value_or_empty()),
|
MTP_long(data.vvia_bot_id().value_or_empty()),
|
||||||
MTP_long(data.vvia_business_bot_id().value_or_empty()),
|
MTP_long(data.vvia_business_bot_id().value_or_empty()),
|
||||||
|
data.vguestchat_via_from() ? *data.vguestchat_via_from() : MTPPeer(),
|
||||||
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
|
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
|
||||||
data.vdate(),
|
data.vdate(),
|
||||||
data.vmessage(),
|
data.vmessage(),
|
||||||
@@ -258,6 +259,7 @@ void ScheduledMessages::sendNowSimpleMessage(
|
|||||||
MTPMessageFwdHeader(),
|
MTPMessageFwdHeader(),
|
||||||
MTPlong(), // via_bot_id
|
MTPlong(), // via_bot_id
|
||||||
MTPlong(), // via_business_bot_id
|
MTPlong(), // via_business_bot_id
|
||||||
|
MTPPeer(), // guestchat_via_from
|
||||||
replyHeader,
|
replyHeader,
|
||||||
update.vdate(),
|
update.vdate(),
|
||||||
MTP_string(local->originalText().text),
|
MTP_string(local->originalText().text),
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ constexpr auto kRequestTimeLimit = 10 * crl::time(1000);
|
|||||||
[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) {
|
[[nodiscard]] MTPTopPeerCategory TypeToCategory(TopPeerType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents();
|
case TopPeerType::Chat: return MTP_topPeerCategoryCorrespondents();
|
||||||
|
case TopPeerType::BotGuestChat:
|
||||||
|
return MTP_topPeerCategoryBotsGuestChat();
|
||||||
case TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp();
|
case TopPeerType::BotApp: return MTP_topPeerCategoryBotsApp();
|
||||||
}
|
}
|
||||||
Unexpected("Type in TypeToCategory.");
|
Unexpected("Type in TypeToCategory.");
|
||||||
@@ -53,11 +55,44 @@ constexpr auto kRequestTimeLimit = 10 * crl::time(1000);
|
|||||||
using Flag = MTPcontacts_GetTopPeers::Flag;
|
using Flag = MTPcontacts_GetTopPeers::Flag;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TopPeerType::Chat: return Flag::f_correspondents;
|
case TopPeerType::Chat: return Flag::f_correspondents;
|
||||||
|
case TopPeerType::BotGuestChat: return Flag::f_bots_guestchat;
|
||||||
case TopPeerType::BotApp: return Flag::f_bots_app;
|
case TopPeerType::BotApp: return Flag::f_bots_app;
|
||||||
}
|
}
|
||||||
Unexpected("Type in TypeToGetFlags.");
|
Unexpected("Type in TypeToGetFlags.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] mtpTypeId TypeToCategoryConstructor(TopPeerType type) {
|
||||||
|
switch (type) {
|
||||||
|
case TopPeerType::Chat:
|
||||||
|
return mtpc_topPeerCategoryCorrespondents;
|
||||||
|
case TopPeerType::BotGuestChat:
|
||||||
|
return mtpc_topPeerCategoryBotsGuestChat;
|
||||||
|
case TopPeerType::BotApp:
|
||||||
|
return mtpc_topPeerCategoryBotsApp;
|
||||||
|
}
|
||||||
|
Unexpected("Type in TypeToCategoryConstructor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool CanIncrementPeer(
|
||||||
|
TopPeerType type,
|
||||||
|
not_null<PeerData*> peer) {
|
||||||
|
const auto user = peer->asUser();
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case TopPeerType::Chat:
|
||||||
|
return !user->isBot();
|
||||||
|
case TopPeerType::BotGuestChat:
|
||||||
|
return user->isBot()
|
||||||
|
&& user->botInfo
|
||||||
|
&& user->botInfo->supportsGuestChat;
|
||||||
|
case TopPeerType::BotApp:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Unexpected("Type in CanIncrementPeer.");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TopPeers::TopPeers(not_null<Main::Session*> session, TopPeerType type)
|
TopPeers::TopPeers(not_null<Main::Session*> session, TopPeerType type)
|
||||||
@@ -119,31 +154,32 @@ void TopPeers::increment(not_null<PeerData*> peer, TimeId date) {
|
|||||||
if (_disabled || date <= _lastReceivedDate) {
|
if (_disabled || date <= _lastReceivedDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (const auto user = peer->asUser(); user && !user->isBot()) {
|
if (!CanIncrementPeer(_type, peer)) {
|
||||||
auto changed = false;
|
return;
|
||||||
auto i = ranges::find(_list, peer, &TopPeer::peer);
|
}
|
||||||
if (i == end(_list)) {
|
auto changed = false;
|
||||||
_list.push_back({ .peer = peer });
|
auto i = ranges::find(_list, peer, &TopPeer::peer);
|
||||||
i = end(_list) - 1;
|
if (i == end(_list)) {
|
||||||
|
_list.push_back({ .peer = peer });
|
||||||
|
i = end(_list) - 1;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
const auto &config = peer->session().mtp().config();
|
||||||
|
const auto decay = config.values().ratingDecay;
|
||||||
|
i->rating += RatingDelta(date, _lastReceivedDate, decay);
|
||||||
|
for (; i != begin(_list); --i) {
|
||||||
|
if (i->rating >= (i - 1)->rating) {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
std::swap(*i, *(i - 1));
|
||||||
const auto &config = peer->session().mtp().config();
|
|
||||||
const auto decay = config.values().ratingDecay;
|
|
||||||
i->rating += RatingDelta(date, _lastReceivedDate, decay);
|
|
||||||
for (; i != begin(_list); --i) {
|
|
||||||
if (i->rating >= (i - 1)->rating) {
|
|
||||||
changed = true;
|
|
||||||
std::swap(*i, *(i - 1));
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
updated();
|
|
||||||
} else {
|
} else {
|
||||||
_session->local().writeSearchSuggestionsDelayed();
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changed) {
|
||||||
|
updated();
|
||||||
|
} else {
|
||||||
|
_session->local().writeSearchSuggestionsDelayed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TopPeers::reload() {
|
void TopPeers::reload() {
|
||||||
@@ -202,9 +238,7 @@ void TopPeers::request() {
|
|||||||
owner->processChats(data.vchats());
|
owner->processChats(data.vchats());
|
||||||
for (const auto &category : data.vcategories().v) {
|
for (const auto &category : data.vcategories().v) {
|
||||||
const auto &data = category.data();
|
const auto &data = category.data();
|
||||||
const auto cons = (_type == TopPeerType::Chat)
|
const auto cons = TypeToCategoryConstructor(_type);
|
||||||
? mtpc_topPeerCategoryCorrespondents
|
|
||||||
: mtpc_topPeerCategoryBotsApp;
|
|
||||||
if (data.vcategory().type() != cons) {
|
if (data.vcategory().type() != cons) {
|
||||||
LOG(("API Error: Unexpected top peer category."));
|
LOG(("API Error: Unexpected top peer category."));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace Data {
|
|||||||
|
|
||||||
enum class TopPeerType {
|
enum class TopPeerType {
|
||||||
Chat,
|
Chat,
|
||||||
|
BotGuestChat,
|
||||||
BotApp,
|
BotApp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
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 "data/data_ai_compose_tones.h"
|
||||||
|
|
||||||
|
#include "apiwrap.h"
|
||||||
|
#include "api/api_text_entities.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kRefreshInterval = 3600 * crl::time(1000);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AiComposeTones::AiComposeTones(not_null<Main::Session*> session)
|
||||||
|
: _session(session)
|
||||||
|
, _refreshTimer([=] { refresh(); }) {
|
||||||
|
refresh();
|
||||||
|
_refreshTimer.callEach(kRefreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::refresh() {
|
||||||
|
refreshWithHash(_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::refreshWithHash(uint64 hash) {
|
||||||
|
if (_refreshRequestId) {
|
||||||
|
if (hash == 0) {
|
||||||
|
_pendingRefresh = PendingRefresh::Full;
|
||||||
|
} else if (_pendingRefresh == PendingRefresh::None) {
|
||||||
|
_pendingRefresh = PendingRefresh::Incremental;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_refreshRequestId = _session->api().request(MTPaicompose_GetTones(
|
||||||
|
MTP_long(hash)
|
||||||
|
)).done([=](const MTPaicompose_Tones &result) {
|
||||||
|
_refreshRequestId = 0;
|
||||||
|
result.match([&](const MTPDaicompose_tones &data) {
|
||||||
|
_session->data().processUsers(data.vusers());
|
||||||
|
_hash = data.vhash().v;
|
||||||
|
parseTones(data.vtones().v);
|
||||||
|
_updates.fire({});
|
||||||
|
}, [](const MTPDaicompose_tonesNotModified &) {
|
||||||
|
});
|
||||||
|
finishRefresh();
|
||||||
|
}).fail([=] {
|
||||||
|
_refreshRequestId = 0;
|
||||||
|
finishRefresh();
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::parseTones(const QVector<MTPAiComposeTone> &list) {
|
||||||
|
_list.clear();
|
||||||
|
_list.reserve(list.size());
|
||||||
|
for (const auto &tone : list) {
|
||||||
|
_list.push_back(parseTone(tone));
|
||||||
|
}
|
||||||
|
reapplyRecentCustomToneOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
AiComposeTone AiComposeTones::parseTone(
|
||||||
|
const MTPAiComposeTone &tone) const {
|
||||||
|
return tone.match([&](const MTPDaiComposeTone &data) {
|
||||||
|
auto result = AiComposeTone{
|
||||||
|
.id = data.vid().v,
|
||||||
|
.accessHash = data.vaccess_hash().v,
|
||||||
|
.slug = qs(data.vslug()),
|
||||||
|
.title = qs(data.vtitle()),
|
||||||
|
.emojiId = data.vemoji_id().value_or_empty(),
|
||||||
|
.prompt = qs(data.vprompt().value_or_empty()),
|
||||||
|
.installsCount = data.vinstalls_count().value_or_empty(),
|
||||||
|
.authorId = data.vauthor_id()
|
||||||
|
? UserId(data.vauthor_id()->v)
|
||||||
|
: UserId(0),
|
||||||
|
.creator = data.is_creator(),
|
||||||
|
};
|
||||||
|
if (const auto example = data.vexample_english()) {
|
||||||
|
example->match([&](const MTPDaiComposeToneExample &d) {
|
||||||
|
result.firstExample = AiComposeToneExample{
|
||||||
|
.from = Api::ParseTextWithEntities(_session, d.vfrom()),
|
||||||
|
.to = Api::ParseTextWithEntities(_session, d.vto()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [&](const MTPDaiComposeToneDefault &data) {
|
||||||
|
return AiComposeTone{
|
||||||
|
.title = qs(data.vtitle()),
|
||||||
|
.emojiId = data.vemoji_id().v,
|
||||||
|
.isDefault = true,
|
||||||
|
.defaultType = qs(data.vtone()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::create(
|
||||||
|
const QString &title,
|
||||||
|
const QString &prompt,
|
||||||
|
DocumentId emojiId,
|
||||||
|
bool displayAuthor,
|
||||||
|
Fn<void(AiComposeTone)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail) {
|
||||||
|
using Flag = MTPaicompose_CreateTone::Flag;
|
||||||
|
auto flags = MTPaicompose_CreateTone::Flags(0);
|
||||||
|
if (displayAuthor) {
|
||||||
|
flags |= Flag::f_display_author;
|
||||||
|
}
|
||||||
|
_session->api().request(MTPaicompose_CreateTone(
|
||||||
|
MTP_flags(flags),
|
||||||
|
MTP_long(emojiId),
|
||||||
|
MTP_string(title),
|
||||||
|
MTP_string(prompt)
|
||||||
|
)).done([=](const MTPAiComposeTone &result) {
|
||||||
|
auto parsed = parseTone(result);
|
||||||
|
promoteCustomTone(parsed);
|
||||||
|
_hash = 0;
|
||||||
|
_updates.fire({});
|
||||||
|
if (done) {
|
||||||
|
done(parsed);
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error);
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::update(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
std::optional<QString> title,
|
||||||
|
std::optional<QString> prompt,
|
||||||
|
std::optional<DocumentId> emojiId,
|
||||||
|
std::optional<bool> displayAuthor,
|
||||||
|
Fn<void(AiComposeTone)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail) {
|
||||||
|
using Flag = MTPaicompose_UpdateTone::Flag;
|
||||||
|
auto flags = MTPaicompose_UpdateTone::Flags(0);
|
||||||
|
if (displayAuthor) {
|
||||||
|
flags |= Flag::f_display_author;
|
||||||
|
}
|
||||||
|
if (emojiId) {
|
||||||
|
flags |= Flag::f_emoji_id;
|
||||||
|
}
|
||||||
|
if (title) {
|
||||||
|
flags |= Flag::f_title;
|
||||||
|
}
|
||||||
|
if (prompt) {
|
||||||
|
flags |= Flag::f_prompt;
|
||||||
|
}
|
||||||
|
_session->api().request(MTPaicompose_UpdateTone(
|
||||||
|
MTP_flags(flags),
|
||||||
|
toneToMTP(tone),
|
||||||
|
displayAuthor
|
||||||
|
? (*displayAuthor ? MTP_boolTrue() : MTP_boolFalse())
|
||||||
|
: MTPBool(),
|
||||||
|
MTP_long(emojiId.value_or(0)),
|
||||||
|
MTP_string(title.value_or(QString())),
|
||||||
|
MTP_string(prompt.value_or(QString()))
|
||||||
|
)).done([=](const MTPAiComposeTone &result) {
|
||||||
|
auto parsed = parseTone(result);
|
||||||
|
promoteCustomTone(parsed);
|
||||||
|
_hash = 0;
|
||||||
|
_updates.fire({});
|
||||||
|
if (done) {
|
||||||
|
done(parsed);
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error);
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::save(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
bool unsave,
|
||||||
|
Fn<void()> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail) {
|
||||||
|
_session->api().request(MTPaicompose_SaveTone(
|
||||||
|
toneToMTP(tone),
|
||||||
|
unsave ? MTP_boolTrue() : MTP_boolFalse()
|
||||||
|
)).done([=] {
|
||||||
|
if (unsave) {
|
||||||
|
removeCustomTone(tone.id);
|
||||||
|
forgetRecentCustomTone(tone.id);
|
||||||
|
} else {
|
||||||
|
promoteCustomTone(tone);
|
||||||
|
}
|
||||||
|
_hash = 0;
|
||||||
|
_updates.fire({});
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error);
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::remove(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
Fn<void()> done) {
|
||||||
|
const auto toneCopy = tone;
|
||||||
|
_session->api().request(MTPaicompose_DeleteTone(
|
||||||
|
toneToMTP(tone)
|
||||||
|
)).done([=] {
|
||||||
|
if (!toneCopy.isDefault) {
|
||||||
|
removeCustomTone(toneCopy.id);
|
||||||
|
forgetRecentCustomTone(toneCopy.id);
|
||||||
|
}
|
||||||
|
_hash = 0;
|
||||||
|
_updates.fire({});
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}).fail([=] {
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::resolve(
|
||||||
|
const QString &slug,
|
||||||
|
Fn<void(AiComposeTone)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail) {
|
||||||
|
_session->api().request(MTPaicompose_GetTone(
|
||||||
|
MTP_inputAiComposeToneSlug(MTP_string(slug))
|
||||||
|
)).done([=](const MTPaicompose_Tones &result) {
|
||||||
|
result.match([&](const MTPDaicompose_tones &data) {
|
||||||
|
_session->data().processUsers(data.vusers());
|
||||||
|
const auto &tones = data.vtones().v;
|
||||||
|
if (!tones.isEmpty()) {
|
||||||
|
if (done) {
|
||||||
|
done(parseTone(tones.front()));
|
||||||
|
}
|
||||||
|
} else if (fail) {
|
||||||
|
fail(MTP::Error::Local(
|
||||||
|
"TONE_NOT_FOUND",
|
||||||
|
"Tone not found."));
|
||||||
|
}
|
||||||
|
}, [&](const MTPDaicompose_tonesNotModified &) {
|
||||||
|
if (fail) {
|
||||||
|
fail(MTP::Error::Local(
|
||||||
|
"TONE_NOT_MODIFIED",
|
||||||
|
"Tone not modified."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error);
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::getToneExample(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
int num,
|
||||||
|
Fn<void(AiComposeToneExample)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail) {
|
||||||
|
_session->api().request(MTPaicompose_GetToneExample(
|
||||||
|
toneToMTP(tone),
|
||||||
|
MTP_int(num)
|
||||||
|
)).done([=](const MTPAiComposeToneExample &result) {
|
||||||
|
result.match([&](const MTPDaiComposeToneExample &data) {
|
||||||
|
if (done) {
|
||||||
|
done(AiComposeToneExample{
|
||||||
|
.from = Api::ParseTextWithEntities(_session, data.vfrom()),
|
||||||
|
.to = Api::ParseTextWithEntities(_session, data.vto()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error);
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::applyUpdate() {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::finishRefresh() {
|
||||||
|
const auto pending = _pendingRefresh;
|
||||||
|
_pendingRefresh = PendingRefresh::None;
|
||||||
|
if (pending == PendingRefresh::None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshWithHash((pending == PendingRefresh::Full) ? 0 : _hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::promoteCustomTone(AiComposeTone tone) {
|
||||||
|
if (tone.isDefault) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeCustomTone(tone.id);
|
||||||
|
rememberRecentCustomTone(tone.id);
|
||||||
|
_list.insert(begin(_list), std::move(tone));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::removeCustomTone(uint64 id) {
|
||||||
|
const auto i = ranges::find(_list, id, &AiComposeTone::id);
|
||||||
|
if (i != end(_list)) {
|
||||||
|
_list.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::rememberRecentCustomTone(uint64 id) {
|
||||||
|
const auto i = ranges::find(_recentCustomToneIds, id);
|
||||||
|
if (i != end(_recentCustomToneIds)) {
|
||||||
|
_recentCustomToneIds.erase(i);
|
||||||
|
}
|
||||||
|
_recentCustomToneIds.insert(begin(_recentCustomToneIds), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::forgetRecentCustomTone(uint64 id) {
|
||||||
|
const auto i = ranges::find(_recentCustomToneIds, id);
|
||||||
|
if (i != end(_recentCustomToneIds)) {
|
||||||
|
_recentCustomToneIds.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiComposeTones::reapplyRecentCustomToneOrder() {
|
||||||
|
auto reordered = std::vector<AiComposeTone>();
|
||||||
|
reordered.reserve(_list.size());
|
||||||
|
|
||||||
|
auto recent = std::vector<uint64>();
|
||||||
|
recent.reserve(_recentCustomToneIds.size());
|
||||||
|
for (const auto id : _recentCustomToneIds) {
|
||||||
|
const auto i = ranges::find(_list, id, &AiComposeTone::id);
|
||||||
|
if (i != end(_list) && !i->isDefault) {
|
||||||
|
reordered.push_back(*i);
|
||||||
|
recent.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &tone : _list) {
|
||||||
|
const auto promoted = !tone.isDefault
|
||||||
|
&& (ranges::find(recent, tone.id) != end(recent));
|
||||||
|
if (!promoted) {
|
||||||
|
reordered.push_back(tone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_recentCustomToneIds = std::move(recent);
|
||||||
|
_list = std::move(reordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
MTPInputAiComposeTone AiComposeTones::toneToMTP(
|
||||||
|
const AiComposeTone &tone) const {
|
||||||
|
return tone.isDefault
|
||||||
|
? MTP_inputAiComposeToneDefault(MTP_string(tone.defaultType))
|
||||||
|
: MTP_inputAiComposeToneID(
|
||||||
|
MTP_long(tone.id),
|
||||||
|
MTP_long(tone.accessHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<AiComposeTone> &AiComposeTones::list() const {
|
||||||
|
return _list;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> AiComposeTones::updated() const {
|
||||||
|
return _updates.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Data
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
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 "ui/text/text_entity.h"
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace MTP {
|
||||||
|
class Error;
|
||||||
|
} // namespace MTP
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
|
||||||
|
struct AiComposeToneExample {
|
||||||
|
TextWithEntities from;
|
||||||
|
TextWithEntities to;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AiComposeTone {
|
||||||
|
uint64 id = 0;
|
||||||
|
uint64 accessHash = 0;
|
||||||
|
QString slug;
|
||||||
|
QString title;
|
||||||
|
DocumentId emojiId = 0;
|
||||||
|
QString prompt;
|
||||||
|
int installsCount = 0;
|
||||||
|
UserId authorId = 0;
|
||||||
|
bool creator = false;
|
||||||
|
bool isDefault = false;
|
||||||
|
QString defaultType;
|
||||||
|
std::optional<AiComposeToneExample> firstExample;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AiComposeTones final {
|
||||||
|
public:
|
||||||
|
explicit AiComposeTones(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
void refresh();
|
||||||
|
[[nodiscard]] const std::vector<AiComposeTone> &list() const;
|
||||||
|
[[nodiscard]] rpl::producer<> updated() const;
|
||||||
|
|
||||||
|
void create(
|
||||||
|
const QString &title,
|
||||||
|
const QString &prompt,
|
||||||
|
DocumentId emojiId,
|
||||||
|
bool displayAuthor,
|
||||||
|
Fn<void(AiComposeTone)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail = nullptr);
|
||||||
|
void update(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
std::optional<QString> title,
|
||||||
|
std::optional<QString> prompt,
|
||||||
|
std::optional<DocumentId> emojiId,
|
||||||
|
std::optional<bool> displayAuthor,
|
||||||
|
Fn<void(AiComposeTone)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail = nullptr);
|
||||||
|
void save(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
bool unsave,
|
||||||
|
Fn<void()> done = nullptr,
|
||||||
|
Fn<void(const MTP::Error &)> fail = nullptr);
|
||||||
|
void remove(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
Fn<void()> done = nullptr);
|
||||||
|
void resolve(
|
||||||
|
const QString &slug,
|
||||||
|
Fn<void(AiComposeTone)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail = nullptr);
|
||||||
|
void getToneExample(
|
||||||
|
const AiComposeTone &tone,
|
||||||
|
int num,
|
||||||
|
Fn<void(AiComposeToneExample)> done,
|
||||||
|
Fn<void(const MTP::Error &)> fail = nullptr);
|
||||||
|
|
||||||
|
void applyUpdate();
|
||||||
|
|
||||||
|
[[nodiscard]] MTPInputAiComposeTone toneToMTP(
|
||||||
|
const AiComposeTone &tone) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void parseTones(const QVector<MTPAiComposeTone> &list);
|
||||||
|
[[nodiscard]] AiComposeTone parseTone(
|
||||||
|
const MTPAiComposeTone &tone) const;
|
||||||
|
void finishRefresh();
|
||||||
|
void promoteCustomTone(AiComposeTone tone);
|
||||||
|
void removeCustomTone(uint64 id);
|
||||||
|
void rememberRecentCustomTone(uint64 id);
|
||||||
|
void forgetRecentCustomTone(uint64 id);
|
||||||
|
void reapplyRecentCustomToneOrder();
|
||||||
|
void refreshWithHash(uint64 hash);
|
||||||
|
|
||||||
|
enum class PendingRefresh {
|
||||||
|
None,
|
||||||
|
Incremental,
|
||||||
|
Full,
|
||||||
|
};
|
||||||
|
|
||||||
|
const not_null<Main::Session*> _session;
|
||||||
|
uint64 _hash = 0;
|
||||||
|
mtpRequestId _refreshRequestId = 0;
|
||||||
|
std::vector<AiComposeTone> _list;
|
||||||
|
std::vector<uint64> _recentCustomToneIds;
|
||||||
|
rpl::event_stream<> _updates;
|
||||||
|
base::Timer _refreshTimer;
|
||||||
|
PendingRefresh _pendingRefresh = PendingRefresh::None;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Data
|
||||||
@@ -75,6 +75,7 @@ namespace {
|
|||||||
| (data.is_send_docs() ? Flag::SendFiles : Flag())
|
| (data.is_send_docs() ? Flag::SendFiles : Flag())
|
||||||
| (data.is_send_plain() ? Flag::SendOther : Flag())
|
| (data.is_send_plain() ? Flag::SendOther : Flag())
|
||||||
| (data.is_embed_links() ? Flag::EmbedLinks : Flag())
|
| (data.is_embed_links() ? Flag::EmbedLinks : Flag())
|
||||||
|
| (data.is_send_reactions() ? Flag::SendReactions : Flag())
|
||||||
| (data.is_change_info() ? Flag::ChangeInfo : Flag())
|
| (data.is_change_info() ? Flag::ChangeInfo : Flag())
|
||||||
| (data.is_invite_users() ? Flag::AddParticipants : Flag())
|
| (data.is_invite_users() ? Flag::AddParticipants : Flag())
|
||||||
| (data.is_pin_messages() ? Flag::PinMessages : Flag())
|
| (data.is_pin_messages() ? Flag::PinMessages : Flag())
|
||||||
@@ -149,6 +150,7 @@ MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) {
|
|||||||
| ((flags & R::SendFiles) ? Flag::f_send_docs : Flag())
|
| ((flags & R::SendFiles) ? Flag::f_send_docs : Flag())
|
||||||
| ((flags & R::SendOther) ? Flag::f_send_plain : Flag())
|
| ((flags & R::SendOther) ? Flag::f_send_plain : Flag())
|
||||||
| ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag())
|
| ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag())
|
||||||
|
| ((flags & R::SendReactions) ? Flag::f_send_reactions : Flag())
|
||||||
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
|
| ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag())
|
||||||
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())
|
| ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag())
|
||||||
| ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag())
|
| ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag())
|
||||||
@@ -247,12 +249,9 @@ bool CanSendAnyOf(
|
|||||||
if (!chat->amIn()) {
|
if (!chat->amIn()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const auto right : AllSendRestrictionsList()) {
|
return chat->amCreator()
|
||||||
if ((rights & right) && !chat->amRestricted(right)) {
|
|| chat->hasAdminRights()
|
||||||
return true;
|
|| (rights & ~chat->defaultRestrictions());
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else if (const auto channel = peer->asChannel()) {
|
} else if (const auto channel = peer->asChannel()) {
|
||||||
if (channel->monoforumDisabled()) {
|
if (channel->monoforumDisabled()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -264,17 +263,15 @@ bool CanSendAnyOf(
|
|||||||
|| channel->isMonoforum();
|
|| channel->isMonoforum();
|
||||||
if (!allowed || (forbidInForums && channel->isForum())) {
|
if (!allowed || (forbidInForums && channel->isForum())) {
|
||||||
return false;
|
return false;
|
||||||
} else if (channel->canPostMessages()) {
|
|
||||||
return true;
|
|
||||||
} else if (channel->isBroadcast()) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
for (const auto right : AllSendRestrictionsList()) {
|
const auto restricted = channel->restrictions()
|
||||||
if ((rights & right) && !channel->amRestricted(right)) {
|
| (channel->unrestrictedByBoosts()
|
||||||
return true;
|
? ChatRestrictions()
|
||||||
}
|
: channel->defaultRestrictions());
|
||||||
}
|
return channel->canPostMessages()
|
||||||
return false;
|
|| (!channel->isBroadcast()
|
||||||
|
&& (channel->hasAdminRights()
|
||||||
|
|| (rights & ~restricted)));
|
||||||
}
|
}
|
||||||
Unexpected("Peer type in CanSendAnyOf.");
|
Unexpected("Peer type in CanSendAnyOf.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ enum class ChatRestriction {
|
|||||||
PinMessages = (1 << 17),
|
PinMessages = (1 << 17),
|
||||||
CreateTopics = (1 << 18),
|
CreateTopics = (1 << 18),
|
||||||
EditRank = (1 << 26),
|
EditRank = (1 << 26),
|
||||||
|
SendReactions = (1 << 27),
|
||||||
};
|
};
|
||||||
inline constexpr bool is_flag_type(ChatRestriction) { return true; }
|
inline constexpr bool is_flag_type(ChatRestriction) { return true; }
|
||||||
using ChatRestrictions = base::flags<ChatRestriction>;
|
using ChatRestrictions = base::flags<ChatRestriction>;
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ struct CreditsHistoryEntry final {
|
|||||||
bool giftUpgradeSeparate : 1 = false;
|
bool giftUpgradeSeparate : 1 = false;
|
||||||
bool giftUpgradeGifted : 1 = false;
|
bool giftUpgradeGifted : 1 = false;
|
||||||
bool giftResale : 1 = false;
|
bool giftResale : 1 = false;
|
||||||
|
bool giftOffer : 1 = false;
|
||||||
bool giftResaleForceTon : 1 = false;
|
bool giftResaleForceTon : 1 = false;
|
||||||
bool giftPinned : 1 = false;
|
bool giftPinned : 1 = false;
|
||||||
bool giftCrafted : 1 = false;
|
bool giftCrafted : 1 = false;
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ struct FileReferenceAccumulator {
|
|||||||
push(data.vicons());
|
push(data.vicons());
|
||||||
}, [&](const MTPDwebPageAttributeStarGiftAuction &data) {
|
}, [&](const MTPDwebPageAttributeStarGiftAuction &data) {
|
||||||
push(data.vgift());
|
push(data.vgift());
|
||||||
|
}, [](const MTPDwebPageAttributeAiComposeTone &) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void push(const MTPStarGift &data) {
|
void push(const MTPStarGift &data) {
|
||||||
|
|||||||
@@ -58,6 +58,17 @@ Forum::Forum(not_null<History*> history)
|
|||||||
if (peer()->canCreateTopics()) {
|
if (peer()->canCreateTopics()) {
|
||||||
owner().forumIcons().requestDefaultIfUnknown();
|
owner().forumIcons().requestDefaultIfUnknown();
|
||||||
}
|
}
|
||||||
|
_topicsList.fullSize().value(
|
||||||
|
) | rpl::map([](int size) {
|
||||||
|
return size > 0;
|
||||||
|
}) | rpl::distinct_until_changed(
|
||||||
|
) | rpl::skip(
|
||||||
|
1
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
if (IsBotCreatesTopics(_history->peer)) {
|
||||||
|
_history->updateChatListEntryHeight();
|
||||||
|
}
|
||||||
|
}, _lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
Forum::~Forum() {
|
Forum::~Forum() {
|
||||||
|
|||||||
@@ -1360,7 +1360,7 @@ bool MediaFile::forwardedBecomesUnread() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MediaFile::dropForwardedInfo() const {
|
bool MediaFile::dropForwardedInfo() const {
|
||||||
return _document->isSong();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MediaFile::hasSpoiler() const {
|
bool MediaFile::hasSpoiler() const {
|
||||||
|
|||||||
@@ -7,8 +7,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "base/basic_types.h"
|
||||||
#include "base/qt/qt_compare.h"
|
#include "base/qt/qt_compare.h"
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace tl {
|
||||||
|
template <typename bare>
|
||||||
|
class boxed;
|
||||||
|
} // namespace tl
|
||||||
|
|
||||||
|
class MTPreaction;
|
||||||
|
using MTPReaction = tl::boxed<MTPreaction>;
|
||||||
|
using DocumentId = uint64;
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
|
|
||||||
struct ReactionId {
|
struct ReactionId {
|
||||||
|
|||||||
@@ -2044,6 +2044,71 @@ void MessageReactions::remove(const ReactionId &id) {
|
|||||||
owner.notifyItemDataChange(_item);
|
owner.notifyItemDataChange(_item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MessageReactions::removeFromParticipant(
|
||||||
|
not_null<PeerData*> participant,
|
||||||
|
const ReactionId &knownReaction) {
|
||||||
|
auto changed = false;
|
||||||
|
auto participantFound = false;
|
||||||
|
const auto decrementReactionCount = [&](const ReactionId &id, int count) {
|
||||||
|
const auto i = ranges::find(_list, id, &MessageReaction::id);
|
||||||
|
if (i == end(_list)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (i->count <= count) {
|
||||||
|
_list.erase(i);
|
||||||
|
} else {
|
||||||
|
i->count -= count;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
for (auto i = begin(_recent); i != end(_recent);) {
|
||||||
|
auto &list = i->second;
|
||||||
|
const auto was = int(list.size());
|
||||||
|
list.erase(
|
||||||
|
ranges::remove(list, participant, &RecentReaction::peer),
|
||||||
|
end(list));
|
||||||
|
if (const auto removed = was - int(list.size())) {
|
||||||
|
changed = true;
|
||||||
|
participantFound = true;
|
||||||
|
decrementReactionCount(i->first, removed);
|
||||||
|
}
|
||||||
|
if (list.empty()) {
|
||||||
|
i = _recent.erase(i);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_paid) {
|
||||||
|
auto removedCount = 0;
|
||||||
|
auto removedEntries = 0;
|
||||||
|
_paid->top.erase(
|
||||||
|
ranges::remove_if(_paid->top, [&](const TopPaid &entry) {
|
||||||
|
if (entry.peer != participant.get()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
removedCount += int(entry.count);
|
||||||
|
++removedEntries;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
end(_paid->top));
|
||||||
|
if (removedEntries) {
|
||||||
|
changed = true;
|
||||||
|
const auto paid = ReactionId::Paid();
|
||||||
|
participantFound = true;
|
||||||
|
decrementReactionCount(paid, removedCount);
|
||||||
|
if (_paid->top.empty() && !localPaidData()) {
|
||||||
|
_paid = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!knownReaction.empty()
|
||||||
|
&& !participantFound
|
||||||
|
&& decrementReactionCount(knownReaction, 1)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
bool MessageReactions::checkIfChanged(
|
bool MessageReactions::checkIfChanged(
|
||||||
const QVector<MTPReactionCount> &list,
|
const QVector<MTPReactionCount> &list,
|
||||||
const QVector<MTPMessagePeerReaction> &recent,
|
const QVector<MTPMessagePeerReaction> &recent,
|
||||||
|
|||||||
@@ -405,6 +405,9 @@ public:
|
|||||||
|
|
||||||
void add(const ReactionId &id, bool addToRecent);
|
void add(const ReactionId &id, bool addToRecent);
|
||||||
void remove(const ReactionId &id);
|
void remove(const ReactionId &id);
|
||||||
|
bool removeFromParticipant(
|
||||||
|
not_null<PeerData*> participant,
|
||||||
|
const ReactionId &knownReaction);
|
||||||
bool change(
|
bool change(
|
||||||
const QVector<MTPReactionCount> &list,
|
const QVector<MTPReactionCount> &list,
|
||||||
const QVector<MTPMessagePeerReaction> &recent,
|
const QVector<MTPMessagePeerReaction> &recent,
|
||||||
|
|||||||
@@ -1700,6 +1700,23 @@ bool PeerData::useSubsectionTabs() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PeerData::displayAsForum() const {
|
||||||
|
if (!isForum()) {
|
||||||
|
return false;
|
||||||
|
} else if (Data::IsBotCreatesTopics(this)) {
|
||||||
|
const auto forum = asBot()->botInfo->forum();
|
||||||
|
return forum && !forum->topicsList()->empty();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeerData::displaySubsectionTabs() const {
|
||||||
|
if (asBot()) {
|
||||||
|
return displayAsForum();
|
||||||
|
}
|
||||||
|
return useSubsectionTabs();
|
||||||
|
}
|
||||||
|
|
||||||
bool PeerData::viewForumAsMessages() const {
|
bool PeerData::viewForumAsMessages() const {
|
||||||
if (const auto channel = asChannel()) {
|
if (const auto channel = asChannel()) {
|
||||||
return channel->viewForumAsMessages();
|
return channel->viewForumAsMessages();
|
||||||
@@ -2240,4 +2257,11 @@ bool IsBotUserCreatesTopics(not_null<PeerData*> peer) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsBotCreatesTopics(not_null<const PeerData*> peer) {
|
||||||
|
if (const auto user = peer->asUser()) {
|
||||||
|
return user->botInfo && !user->botInfo->userCreatesTopics;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Data
|
} // namespace Data
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ public:
|
|||||||
PeerId sublistPeerId) const;
|
PeerId sublistPeerId) const;
|
||||||
|
|
||||||
[[nodiscard]] bool useSubsectionTabs() const;
|
[[nodiscard]] bool useSubsectionTabs() const;
|
||||||
|
[[nodiscard]] bool displaySubsectionTabs() const;
|
||||||
|
[[nodiscard]] bool displayAsForum() const;
|
||||||
[[nodiscard]] bool viewForumAsMessages() const;
|
[[nodiscard]] bool viewForumAsMessages() const;
|
||||||
void processTopics(const MTPVector<MTPForumTopic> &topics);
|
void processTopics(const MTPVector<MTPForumTopic> &topics);
|
||||||
|
|
||||||
@@ -679,5 +681,6 @@ void SetTopPinnedMessageId(
|
|||||||
[[nodiscard]] std::optional<uint8> ColorIndexFromColor(const MTPPeerColor *);
|
[[nodiscard]] std::optional<uint8> ColorIndexFromColor(const MTPPeerColor *);
|
||||||
|
|
||||||
[[nodiscard]] bool IsBotUserCreatesTopics(not_null<PeerData*>);
|
[[nodiscard]] bool IsBotUserCreatesTopics(not_null<PeerData*>);
|
||||||
|
[[nodiscard]] bool IsBotCreatesTopics(not_null<const PeerData*>);
|
||||||
|
|
||||||
} // namespace Data
|
} // namespace Data
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/data_poll.h"
|
#include "data/data_poll.h"
|
||||||
|
|
||||||
#include "api/api_text_entities.h"
|
#include "api/api_text_entities.h"
|
||||||
|
#include "countries/countries_instance.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_photo.h"
|
#include "data/data_photo.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "base/call_delayed.h"
|
#include "base/call_delayed.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "ui/text/text_options.h"
|
#include "ui/text/text_options.h"
|
||||||
|
|
||||||
@@ -105,9 +107,17 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
|||||||
| (poll.is_hide_results_until_close()
|
| (poll.is_hide_results_until_close()
|
||||||
? Flag::HideResultsUntilClose
|
? Flag::HideResultsUntilClose
|
||||||
: Flag(0))
|
: Flag(0))
|
||||||
| (poll.is_creator() ? Flag::Creator : Flag(0));
|
| (poll.is_creator() ? Flag::Creator : Flag(0))
|
||||||
|
| (poll.is_subscribers_only() ? Flag::SubscribersOnly : Flag(0));
|
||||||
const auto newCloseDate = poll.vclose_date().value_or_empty();
|
const auto newCloseDate = poll.vclose_date().value_or_empty();
|
||||||
const auto newClosePeriod = poll.vclose_period().value_or_empty();
|
const auto newClosePeriod = poll.vclose_period().value_or_empty();
|
||||||
|
auto newCountries = std::vector<QString>();
|
||||||
|
if (const auto countries = poll.vcountries_iso2()) {
|
||||||
|
newCountries.reserve(countries->v.size());
|
||||||
|
for (const auto &country : countries->v) {
|
||||||
|
newCountries.push_back(qs(country));
|
||||||
|
}
|
||||||
|
}
|
||||||
auto newAnswers = ranges::views::all(
|
auto newAnswers = ranges::views::all(
|
||||||
poll.vanswers().v
|
poll.vanswers().v
|
||||||
) | ranges::views::transform([&](const MTPPollAnswer &data) {
|
) | ranges::views::transform([&](const MTPPollAnswer &data) {
|
||||||
@@ -145,6 +155,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
|||||||
const auto changed1 = (question != newQuestion)
|
const auto changed1 = (question != newQuestion)
|
||||||
|| (closeDate != newCloseDate)
|
|| (closeDate != newCloseDate)
|
||||||
|| (closePeriod != newClosePeriod)
|
|| (closePeriod != newClosePeriod)
|
||||||
|
|| (countries != newCountries)
|
||||||
|| (_flags != newFlags);
|
|| (_flags != newFlags);
|
||||||
const auto changed2 = (answers != newAnswers);
|
const auto changed2 = (answers != newAnswers);
|
||||||
if (!changed1 && !changed2) {
|
if (!changed1 && !changed2) {
|
||||||
@@ -154,6 +165,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
|||||||
question = newQuestion;
|
question = newQuestion;
|
||||||
closeDate = newCloseDate;
|
closeDate = newCloseDate;
|
||||||
closePeriod = newClosePeriod;
|
closePeriod = newClosePeriod;
|
||||||
|
countries = std::move(newCountries);
|
||||||
_flags = newFlags;
|
_flags = newFlags;
|
||||||
}
|
}
|
||||||
if (changed2) {
|
if (changed2) {
|
||||||
@@ -178,6 +190,21 @@ bool PollData::applyResults(const MTPPollResults &results) {
|
|||||||
const auto newTotalVoters
|
const auto newTotalVoters
|
||||||
= results.vtotal_voters().value_or(totalVoters);
|
= results.vtotal_voters().value_or(totalVoters);
|
||||||
auto changed = (newTotalVoters != totalVoters);
|
auto changed = (newTotalVoters != totalVoters);
|
||||||
|
const auto setCanViewStats = [&](bool value) {
|
||||||
|
const auto previous = (_flags & Flag::CanViewStats);
|
||||||
|
if (bool(previous) == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
_flags |= Flag::CanViewStats;
|
||||||
|
} else {
|
||||||
|
_flags &= ~Flag::CanViewStats;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
};
|
||||||
|
if (!results.is_min() || results.is_can_view_stats()) {
|
||||||
|
setCanViewStats(results.is_can_view_stats());
|
||||||
|
}
|
||||||
if (const auto list = results.vresults()) {
|
if (const auto list = results.vresults()) {
|
||||||
for (const auto &result : list->v) {
|
for (const auto &result : list->v) {
|
||||||
if (applyResultToAnswers(result, results.is_min())) {
|
if (applyResultToAnswers(result, results.is_min())) {
|
||||||
@@ -368,6 +395,32 @@ bool PollData::creator() const {
|
|||||||
return (_flags & Flag::Creator);
|
return (_flags & Flag::Creator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PollData::subscribersOnly() const {
|
||||||
|
return (_flags & Flag::SubscribersOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PollData::canViewStats() const {
|
||||||
|
return (_flags & Flag::CanViewStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollData::setVoteRestriction(VoteRestriction restriction) {
|
||||||
|
_voteRestrictionUpdated = (restriction == VoteRestriction::None)
|
||||||
|
? 0
|
||||||
|
: crl::now();
|
||||||
|
if (_voteRestriction != restriction) {
|
||||||
|
_voteRestriction = restriction;
|
||||||
|
++version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PollData::VoteRestriction PollData::voteRestriction() const {
|
||||||
|
return _voteRestriction;
|
||||||
|
}
|
||||||
|
|
||||||
|
crl::time PollData::voteRestrictionUpdated() const {
|
||||||
|
return _voteRestrictionUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
QString PollData::debugString() const {
|
QString PollData::debugString() const {
|
||||||
auto result = QString();
|
auto result = QString();
|
||||||
result += u"Poll #"_q + QString::number(id) + u'\n';
|
result += u"Poll #"_q + QString::number(id) + u'\n';
|
||||||
@@ -384,6 +437,12 @@ QString PollData::debugString() const {
|
|||||||
if (publicVotes()) {
|
if (publicVotes()) {
|
||||||
result += u"[PublicVotes]"_q;
|
result += u"[PublicVotes]"_q;
|
||||||
}
|
}
|
||||||
|
if (subscribersOnly()) {
|
||||||
|
result += u"[SubscribersOnly]"_q;
|
||||||
|
}
|
||||||
|
if (canViewStats()) {
|
||||||
|
result += u"[CanViewStats]"_q;
|
||||||
|
}
|
||||||
if (!result.endsWith(u'\n')) {
|
if (!result.endsWith(u'\n')) {
|
||||||
result += u'\n';
|
result += u'\n';
|
||||||
}
|
}
|
||||||
@@ -402,6 +461,13 @@ QString PollData::debugString() const {
|
|||||||
if (!solution.text.isEmpty()) {
|
if (!solution.text.isEmpty()) {
|
||||||
result += u"Solution: "_q + solution.text + u'\n';
|
result += u"Solution: "_q + solution.text + u'\n';
|
||||||
}
|
}
|
||||||
|
if (!countries.empty()) {
|
||||||
|
result += u"Countries: "_q + countries.front();
|
||||||
|
for (auto i = 1, count = int(countries.size()); i != count; ++i) {
|
||||||
|
result += u", "_q + countries[i];
|
||||||
|
}
|
||||||
|
result += u'\n';
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,6 +586,11 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
|||||||
poll->answers,
|
poll->answers,
|
||||||
ranges::back_inserter(answers),
|
ranges::back_inserter(answers),
|
||||||
convert);
|
convert);
|
||||||
|
auto countries = QVector<MTPstring>();
|
||||||
|
countries.reserve(poll->countries.size());
|
||||||
|
for (const auto &country : poll->countries) {
|
||||||
|
countries.push_back(MTP_string(country));
|
||||||
|
}
|
||||||
using Flag = MTPDpoll::Flag;
|
using Flag = MTPDpoll::Flag;
|
||||||
const auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0))
|
const auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0))
|
||||||
| (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0))
|
| (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0))
|
||||||
@@ -531,8 +602,10 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
|||||||
| (poll->hideResultsUntilClose()
|
| (poll->hideResultsUntilClose()
|
||||||
? Flag::f_hide_results_until_close
|
? Flag::f_hide_results_until_close
|
||||||
: Flag(0))
|
: Flag(0))
|
||||||
|
| (poll->subscribersOnly() ? Flag::f_subscribers_only : Flag(0))
|
||||||
| (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0))
|
| (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0))
|
||||||
| (poll->closeDate > 0 ? Flag::f_close_date : Flag(0));
|
| (poll->closeDate > 0 ? Flag::f_close_date : Flag(0))
|
||||||
|
| (countries.isEmpty() ? Flag(0) : Flag::f_countries_iso2);
|
||||||
return MTP_poll(
|
return MTP_poll(
|
||||||
MTP_long(poll->id),
|
MTP_long(poll->id),
|
||||||
MTP_flags(flags),
|
MTP_flags(flags),
|
||||||
@@ -542,6 +615,7 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
|||||||
MTP_vector<MTPPollAnswer>(answers),
|
MTP_vector<MTPPollAnswer>(answers),
|
||||||
MTP_int(poll->closePeriod),
|
MTP_int(poll->closePeriod),
|
||||||
MTP_int(poll->closeDate),
|
MTP_int(poll->closeDate),
|
||||||
|
MTP_vector<MTPstring>(std::move(countries)), // countries_iso2
|
||||||
MTP_long(0));
|
MTP_long(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,3 +666,69 @@ MTPInputMedia PollDataToInputMedia(
|
|||||||
? PollMediaToMTP(poll->solutionMedia)
|
? PollMediaToMTP(poll->solutionMedia)
|
||||||
: MTPInputMedia());
|
: MTPInputMedia());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString JoinPollCountries(const std::vector<QString> &countriesIso2) {
|
||||||
|
auto countries = QStringList();
|
||||||
|
countries.reserve(int(countriesIso2.size()));
|
||||||
|
const auto &instance = Countries::Instance();
|
||||||
|
for (const auto &iso2 : countriesIso2) {
|
||||||
|
const auto name = instance.countryNameByISO2(
|
||||||
|
iso2,
|
||||||
|
Countries::Naming::Polls);
|
||||||
|
countries.push_back(name.isEmpty() ? iso2 : name);
|
||||||
|
}
|
||||||
|
if (countries.empty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
auto result = countries.front();
|
||||||
|
for (auto i = 1, count = int(countries.size()); i != count; ++i) {
|
||||||
|
result = ((i + 1 == count)
|
||||||
|
? tr::lng_prizes_countries_and_last
|
||||||
|
: tr::lng_prizes_countries_and_one)(
|
||||||
|
tr::now,
|
||||||
|
lt_countries,
|
||||||
|
result,
|
||||||
|
lt_country,
|
||||||
|
countries[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextWithEntities PollCountriesRestrictionText(
|
||||||
|
const std::vector<QString> &countries) {
|
||||||
|
const auto joined = JoinPollCountries(countries);
|
||||||
|
return joined.isEmpty()
|
||||||
|
? tr::lng_polls_vote_restricted_countries(tr::now, tr::rich)
|
||||||
|
: tr::lng_polls_vote_restricted_countries_list(
|
||||||
|
tr::now,
|
||||||
|
lt_countries,
|
||||||
|
tr::bold(joined),
|
||||||
|
tr::rich);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextWithEntities PollVoteRestrictionText(
|
||||||
|
PollData::VoteRestriction restriction,
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
not_null<const PollData*> poll) {
|
||||||
|
switch (restriction) {
|
||||||
|
case PollData::VoteRestriction::SubscribersOnly: {
|
||||||
|
const auto channel = peer->name();
|
||||||
|
return channel.isEmpty()
|
||||||
|
? tr::lng_polls_vote_restricted_subscribers(tr::now, tr::rich)
|
||||||
|
: tr::lng_polls_vote_restricted_subscribers_channel(
|
||||||
|
tr::now,
|
||||||
|
lt_channel,
|
||||||
|
tr::bold(channel),
|
||||||
|
tr::rich);
|
||||||
|
}
|
||||||
|
case PollData::VoteRestriction::SubscribersJoinedTooRecently:
|
||||||
|
return tr::lng_polls_vote_restricted_subscribers_recent(
|
||||||
|
tr::now,
|
||||||
|
tr::rich);
|
||||||
|
case PollData::VoteRestriction::Countries:
|
||||||
|
return PollCountriesRestrictionText(poll->countries);
|
||||||
|
case PollData::VoteRestriction::None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,9 +76,17 @@ struct PollData {
|
|||||||
OpenAnswers = 0x040,
|
OpenAnswers = 0x040,
|
||||||
HideResultsUntilClose = 0x080,
|
HideResultsUntilClose = 0x080,
|
||||||
Creator = 0x100,
|
Creator = 0x100,
|
||||||
|
SubscribersOnly = 0x200,
|
||||||
|
CanViewStats = 0x400,
|
||||||
};
|
};
|
||||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||||
using Flags = base::flags<Flag>;
|
using Flags = base::flags<Flag>;
|
||||||
|
enum class VoteRestriction {
|
||||||
|
None,
|
||||||
|
SubscribersOnly,
|
||||||
|
SubscribersJoinedTooRecently,
|
||||||
|
Countries,
|
||||||
|
};
|
||||||
|
|
||||||
bool closeByTimer();
|
bool closeByTimer();
|
||||||
bool applyChanges(const MTPDpoll &poll);
|
bool applyChanges(const MTPDpoll &poll);
|
||||||
@@ -101,6 +109,11 @@ struct PollData {
|
|||||||
[[nodiscard]] bool openAnswers() const;
|
[[nodiscard]] bool openAnswers() const;
|
||||||
[[nodiscard]] bool hideResultsUntilClose() const;
|
[[nodiscard]] bool hideResultsUntilClose() const;
|
||||||
[[nodiscard]] bool creator() const;
|
[[nodiscard]] bool creator() const;
|
||||||
|
[[nodiscard]] bool subscribersOnly() const;
|
||||||
|
[[nodiscard]] bool canViewStats() const;
|
||||||
|
void setVoteRestriction(VoteRestriction restriction);
|
||||||
|
[[nodiscard]] VoteRestriction voteRestriction() const;
|
||||||
|
[[nodiscard]] crl::time voteRestrictionUpdated() const;
|
||||||
|
|
||||||
[[nodiscard]] QString debugString() const;
|
[[nodiscard]] QString debugString() const;
|
||||||
|
|
||||||
@@ -112,6 +125,7 @@ struct PollData {
|
|||||||
TextWithEntities solution;
|
TextWithEntities solution;
|
||||||
PollMedia attachedMedia;
|
PollMedia attachedMedia;
|
||||||
PollMedia solutionMedia;
|
PollMedia solutionMedia;
|
||||||
|
std::vector<QString> countries;
|
||||||
TimeId closePeriod = 0;
|
TimeId closePeriod = 0;
|
||||||
TimeId closeDate = 0;
|
TimeId closeDate = 0;
|
||||||
int totalVoters = 0;
|
int totalVoters = 0;
|
||||||
@@ -127,6 +141,8 @@ private:
|
|||||||
|
|
||||||
const not_null<Data::Session*> _owner;
|
const not_null<Data::Session*> _owner;
|
||||||
Flags _flags = Flags();
|
Flags _flags = Flags();
|
||||||
|
VoteRestriction _voteRestriction = VoteRestriction::None;
|
||||||
|
crl::time _voteRestrictionUpdated = 0;
|
||||||
crl::time _lastResultsUpdate = 0; // < 0 means force reload.
|
crl::time _lastResultsUpdate = 0; // < 0 means force reload.
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -136,6 +152,15 @@ inline constexpr auto kDefaultPollCreateFlags = PollData::Flag::PublicVotes
|
|||||||
| PollData::Flag::OpenAnswers
|
| PollData::Flag::OpenAnswers
|
||||||
| PollData::Flag::ShuffleAnswers;
|
| PollData::Flag::ShuffleAnswers;
|
||||||
|
|
||||||
|
[[nodiscard]] QString JoinPollCountries(
|
||||||
|
const std::vector<QString> &countriesIso2);
|
||||||
|
[[nodiscard]] TextWithEntities PollCountriesRestrictionText(
|
||||||
|
const std::vector<QString> &countries);
|
||||||
|
[[nodiscard]] TextWithEntities PollVoteRestrictionText(
|
||||||
|
PollData::VoteRestriction restriction,
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
not_null<const PollData*> poll);
|
||||||
|
|
||||||
[[nodiscard]] QByteArray PollOptionFromLink(const QString &value);
|
[[nodiscard]] QByteArray PollOptionFromLink(const QString &value);
|
||||||
[[nodiscard]] QString PollOptionToLink(const QByteArray &option);
|
[[nodiscard]] QString PollOptionToLink(const QByteArray &option);
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,13 @@ int PremiumLimits::botsCreatePremium() const {
|
|||||||
return appConfigLimit("bots_create_limit_premium", 40);
|
return appConfigLimit("bots_create_limit_premium", 40);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PremiumLimits::aiComposeSavedTonesDefault() const {
|
||||||
|
return appConfigLimit("aicompose_tone_saved_limit_default", 5);
|
||||||
|
}
|
||||||
|
int PremiumLimits::aiComposeSavedTonesPremium() const {
|
||||||
|
return appConfigLimit("aicompose_tone_saved_limit_premium", 20);
|
||||||
|
}
|
||||||
|
|
||||||
int PremiumLimits::appConfigLimit(
|
int PremiumLimits::appConfigLimit(
|
||||||
const QString &key,
|
const QString &key,
|
||||||
int fallback) const {
|
int fallback) const {
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ public:
|
|||||||
[[nodiscard]] int botsCreateDefault() const;
|
[[nodiscard]] int botsCreateDefault() const;
|
||||||
[[nodiscard]] int botsCreatePremium() const;
|
[[nodiscard]] int botsCreatePremium() const;
|
||||||
|
|
||||||
|
[[nodiscard]] int aiComposeSavedTonesDefault() const;
|
||||||
|
[[nodiscard]] int aiComposeSavedTonesPremium() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] int appConfigLimit(
|
[[nodiscard]] int appConfigLimit(
|
||||||
const QString &key,
|
const QString &key,
|
||||||
|
|||||||
Alguns arquivos não foram exibidos porque demasiados arquivos foram alterados neste diff Mostrar Mais
Referência em uma Nova Issue
Bloquear um usuário