diff --git a/.claude/commands/icon.md b/.claude/commands/icon.md
index 7e7bde46c3..479e23eceb 100644
--- a/.claude/commands/icon.md
+++ b/.claude/commands/icon.md
@@ -127,7 +127,7 @@ Locate the render tool (`codegen_style` with `--render-svg` mode):
```bash
if [[ "$OSTYPE" == darwin* ]]; then
- ls out/Debug/codegen_style
+ ls out/Telegram/codegen/codegen/style/Debug/codegen_style
else
ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe
fi
@@ -137,7 +137,7 @@ If missing, build it: `cmake --build out --config Debug --target codegen_style`
Test on a known good SVG (use the appropriate binary path for the OS):
```bash
-CODEGEN=$(if [[ "$OSTYPE" == darwin* ]]; then echo out/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)
+CODEGEN=$(if [[ "$OSTYPE" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)
$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512
```
diff --git a/.claude/commands/release.md b/.claude/commands/release.md
index 150d3e917e..baaa7c261d 100644
--- a/.claude/commands/release.md
+++ b/.claude/commands/release.md
@@ -27,9 +27,11 @@ Read `changelog.txt` from the repository root. Note the **latest version number*
### 3. Determine the new version number
-- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given).
+**Important:** version numbers are shared across the beta and stable tracks — the sequence advances through both. The same `major.minor.patch` cannot be released as both beta and stable. A beta release "uses up" that number; the next stable must bump to a new patch.
+
+- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given). If the latest changelog entry already used this exact number (regardless of beta/stable), warn the user — they likely want a bumped patch.
- **If no version was provided**, auto-increment from the latest changelog version:
- - If it was a beta, and the new release is **not** beta, reuse the same version number but drop "beta".
+ - If it was a beta, and the new release is **not** beta, bump the patch component by 1 (do **not** reuse the beta's number for stable).
- If the new release is beta and the latest was also beta with the same major.minor, bump patch.
- Otherwise bump the patch component by 1.
- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.
@@ -65,6 +67,10 @@ Use this exact format (date is today in DD.MM.YY):
Prepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.
+**Never delete or edit existing entries**, even if the new stable entry merges bullets from prior beta(s). Prior beta blocks remain as historical record for beta-track users.
+
+**For stable releases spanning prior beta(s):** the entry should cover everything since the last **stable** release (not just since the last beta), including noteworthy beta-shipped features, trimmed as needed to stay within 4–12 bullets.
+
### 8. Wait for approval
After writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
index a94c13542b..294f1ad3ef 100644
--- a/.github/workflows/changelog.yml
+++ b/.github/workflows/changelog.yml
@@ -44,4 +44,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deploy
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/unused_styles_updater.yml b/.github/workflows/unused_styles_updater.yml
new file mode 100644
index 0000000000..2c021d1cd0
--- /dev/null
+++ b/.github/workflows/unused_styles_updater.yml
@@ -0,0 +1,16 @@
+name: Unused styles updater.
+
+on:
+ repository_dispatch:
+ types: ["Restart unused_styles_updater workflow."]
+ schedule:
+ - cron: "0 3 1 * *"
+ workflow_dispatch:
+
+jobs:
+ Unused-styles:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: desktop-app/action_code_updater@master
+ with:
+ type: "unused-styles"
diff --git a/AGENTS.md b/AGENTS.md
index 9fffd0a059..ec1650302b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -212,6 +212,7 @@ api().request(MTPnamespace_MethodName(
```
- For single constructors, use `.data()` shortcut
- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling
+- Silently ignore HTTP 406 errors in UI: the server uses 406 to mean "show nothing to the user". Guard toasts with `MTP::IgnoreError(error)` or use `MTP::ShowErrorFallback(show, error)` (both in `mtproto/mtproto_response.h`) which shows `error.type()` as a toast unless the error should be ignored.
## UI Styling
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 66847c81b5..9b459eab68 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -350,6 +350,8 @@ PRIVATE
api/api_statistics_data_deserialize.h
api/api_statistics_sender.cpp
api/api_statistics_sender.h
+ api/api_stickers_creator.cpp
+ api/api_stickers_creator.h
api/api_suggest_post.cpp
api/api_suggest_post.h
api/api_text_entities.cpp
@@ -515,6 +517,8 @@ PRIVATE
boxes/send_gif_with_caption_box.h
boxes/send_files_box.cpp
boxes/send_files_box.h
+ boxes/send_files_box_reply_header.cpp
+ boxes/send_files_box_reply_header.h
boxes/share_box.cpp
boxes/share_box.h
boxes/star_gift_auction_box.cpp
@@ -531,6 +535,8 @@ PRIVATE
boxes/star_gift_preview_box.h
boxes/star_gift_resale_box.cpp
boxes/star_gift_resale_box.h
+ boxes/sticker_creator_box.cpp
+ boxes/sticker_creator_box.h
boxes/sticker_set_box.cpp
boxes/sticker_set_box.h
boxes/stickers_box.cpp
@@ -626,6 +632,8 @@ PRIVATE
chat_helpers/emoji_keywords.h
chat_helpers/emoji_list_widget.cpp
chat_helpers/emoji_list_widget.h
+ chat_helpers/emoji_picker_overlay.cpp
+ chat_helpers/emoji_picker_overlay.h
chat_helpers/emoji_sets_manager.cpp
chat_helpers/emoji_sets_manager.h
chat_helpers/emoji_suggestions_widget.cpp
@@ -662,6 +670,8 @@ PRIVATE
chat_helpers/ttl_media_layer_widget.h
core/application.cpp
core/application.h
+ core/proxy_rotation_manager.cpp
+ core/proxy_rotation_manager.h
core/cached_webview_availability.h
core/bank_card_click_handler.cpp
core/bank_card_click_handler.h
@@ -1593,6 +1603,8 @@ PRIVATE
mtproto/facade.h
mtproto/mtp_instance.cpp
mtproto/mtp_instance.h
+ mtproto/proxy_check.cpp
+ mtproto/proxy_check.h
mtproto/sender.h
mtproto/session.cpp
mtproto/session.h
@@ -1656,6 +1668,8 @@ PRIVATE
platform/linux/webauthn_linux.cpp
platform/mac/file_utilities_mac.mm
platform/mac/file_utilities_mac.h
+ platform/mac/global_menu_mac.h
+ platform/mac/global_menu_mac.mm
platform/mac/launcher_mac.mm
platform/mac/launcher_mac.h
platform/mac/integration_mac.mm
@@ -1813,6 +1827,8 @@ PRIVATE
settings/sections/settings_local_passcode.h
settings/sections/settings_main.cpp
settings/sections/settings_main.h
+ settings/settings_recent_searches.cpp
+ settings/settings_recent_searches.h
settings/settings_search.cpp
settings/settings_search.h
settings/settings_faq_suggestions.cpp
@@ -2004,6 +2020,7 @@ PRIVATE
window/window_section_common.h
window/window_separate_id.cpp
window/window_separate_id.h
+ window/session/window_session_media.cpp
window/window_session_controller.cpp
window/window_session_controller.h
window/window_session_controller_link_info.h
diff --git a/Telegram/Resources/icons/chat/text_to_file.svg b/Telegram/Resources/icons/chat/text_to_file.svg
new file mode 100644
index 0000000000..d3d7ce7039
--- /dev/null
+++ b/Telegram/Resources/icons/chat/text_to_file.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/dialogs/dialogs_unread_media_dot.svg b/Telegram/Resources/icons/dialogs/dialogs_unread_media_dot.svg
new file mode 100644
index 0000000000..d0da6ff5fc
--- /dev/null
+++ b/Telegram/Resources/icons/dialogs/dialogs_unread_media_dot.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/Telegram/Resources/icons/menu/sticker_add.png b/Telegram/Resources/icons/menu/sticker_add.png
new file mode 100644
index 0000000000..0a4ee1dbc7
Binary files /dev/null and b/Telegram/Resources/icons/menu/sticker_add.png differ
diff --git a/Telegram/Resources/icons/menu/sticker_add@2x.png b/Telegram/Resources/icons/menu/sticker_add@2x.png
new file mode 100644
index 0000000000..72c830ca74
Binary files /dev/null and b/Telegram/Resources/icons/menu/sticker_add@2x.png differ
diff --git a/Telegram/Resources/icons/menu/sticker_add@3x.png b/Telegram/Resources/icons/menu/sticker_add@3x.png
new file mode 100644
index 0000000000..2bfe690b77
Binary files /dev/null and b/Telegram/Resources/icons/menu/sticker_add@3x.png differ
diff --git a/Telegram/Resources/icons/menu/sticker_select.png b/Telegram/Resources/icons/menu/sticker_select.png
new file mode 100644
index 0000000000..788a9a317c
Binary files /dev/null and b/Telegram/Resources/icons/menu/sticker_select.png differ
diff --git a/Telegram/Resources/icons/menu/sticker_select@2x.png b/Telegram/Resources/icons/menu/sticker_select@2x.png
new file mode 100644
index 0000000000..19cec30c9b
Binary files /dev/null and b/Telegram/Resources/icons/menu/sticker_select@2x.png differ
diff --git a/Telegram/Resources/icons/menu/sticker_select@3x.png b/Telegram/Resources/icons/menu/sticker_select@3x.png
new file mode 100644
index 0000000000..04238df988
Binary files /dev/null and b/Telegram/Resources/icons/menu/sticker_select@3x.png differ
diff --git a/Telegram/Resources/icons/menu/text_style_framed.svg b/Telegram/Resources/icons/menu/text_style_framed.svg
new file mode 100644
index 0000000000..de5e3ff728
--- /dev/null
+++ b/Telegram/Resources/icons/menu/text_style_framed.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/menu/text_style_plain.svg b/Telegram/Resources/icons/menu/text_style_plain.svg
new file mode 100644
index 0000000000..72acd48596
--- /dev/null
+++ b/Telegram/Resources/icons/menu/text_style_plain.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/menu/text_style_semi.svg b/Telegram/Resources/icons/menu/text_style_semi.svg
new file mode 100644
index 0000000000..95cca3188f
--- /dev/null
+++ b/Telegram/Resources/icons/menu/text_style_semi.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/photo_editor/corners.svg b/Telegram/Resources/icons/photo_editor/corners.svg
new file mode 100644
index 0000000000..09b0428433
--- /dev/null
+++ b/Telegram/Resources/icons/photo_editor/corners.svg
@@ -0,0 +1,8 @@
+
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 7d9b0b0e7f..831b3cb3ad 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -339,6 +339,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_invalid" = "The proxy link is invalid.";
"lng_proxy_unsupported" = "Your Telegram Desktop version doesn't support this proxy type or the proxy link is invalid. Please update Telegram Desktop to the latest version.";
"lng_proxy_incorrect_secret" = "This proxy link uses invalid **secret** parameter. Please contact the proxy provider and ask him to update MTProxy source code and configure it with a correct **secret** value. Then let him provide a new link.";
+"lng_proxy_check_ip_warning_title" = "Warning";
+"lng_proxy_check_ip_warning" = "This will expose your IP address to the admin of the proxy server.";
+"lng_proxy_check_ip_proceed" = "Proceed";
"lng_edit_deleted" = "This message was deleted";
"lng_edit_limit_reached#one" = "You've reached the message text limit. Please make the text shorter by {count} character.";
@@ -1236,6 +1239,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_use_system_settings" = "Use system proxy settings";
"lng_proxy_use_custom" = "Use custom proxy";
"lng_proxy_use_for_calls" = "Use proxy for calls";
+"lng_proxy_auto_switch" = "Auto-switch proxies";
+"lng_proxy_auto_switch_about" = "You can choose how quickly the app should auto-connect to the nearest active proxy if the current one stops working.";
+"lng_proxy_auto_switch_timeout#one" = "{count} s";
+"lng_proxy_auto_switch_timeout#other" = "{count} s";
"lng_proxy_about" = "Proxy servers may be helpful in accessing Telegram if there is no connection in a specific region.";
"lng_proxy_add" = "Add proxy";
"lng_proxy_share" = "Share";
@@ -1388,6 +1395,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_blocked_list_confirm_title" = "Block {name}";
"lng_blocked_list_confirm_text" = "Do you want to block {name} from messaging and calling you on Telegram?";
"lng_blocked_list_confirm_clear" = "Delete this chat";
+"lng_blocked_list_confirm_reply" = "Delete this reply";
+"lng_blocked_list_confirm_reply_all" = "Delete all replies from {user}";
"lng_blocked_list_confirm_ok" = "Block";
"lng_blocked_list_empty_title" = "No blocked users";
"lng_blocked_list_empty_description" = "You haven't blocked anyone yet.";
@@ -4496,16 +4505,51 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_stickers_context_edit_name" = "Edit name";
"lng_stickers_context_delete" = "Delete sticker";
"lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?";
+"lng_emoji_context_delete" = "Delete emoji";
+"lng_emoji_context_delete_sure" = "Are you sure you want to delete this emoji from your set?";
+"lng_stickers_context_delete_pack" = "Delete Pack";
+"lng_stickers_context_delete_pack_everyone" = "Delete for Everyone";
+"lng_stickers_context_delete_pack_self" = "Delete for Myself";
+"lng_stickers_delete_pack_sure" = "Are you sure you want to delete this sticker set for everyone? This cannot be undone.";
+"lng_stickers_bot_more_options" = "Check the {bot} bot for more options";
"lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
"lng_stickers_box_edit_name_about" = "Choose a name for your set.";
"lng_stickers_creator_badge" = "edit";
+"lng_stickers_create_new" = "Create a New Sticker";
+"lng_stickers_add_existing" = "Add an Existing Sticker";
+"lng_stickers_add_to_set" = "Add to Sticker Set";
+"lng_stickers_already_in_set" = "This Sticker is already in the Set.";
+"lng_stickers_set_is_full" = "This Sticker Set is full.";
+"lng_emoji_add_to_set" = "Add to Emoji Set";
+"lng_emoji_already_in_set" = "This Emoji is already in the Set.";
+"lng_emoji_set_is_full" = "This Emoji Set is full.";
+"lng_emoji_added" = "Emoji added.";
+"lng_stickers_pack_choose_emoji_title" = "Choose Emoji";
+"lng_stickers_pack_choose_emoji_about" = "Pick an emoji that corresponds to this sticker.";
+"lng_stickers_pick_existing_title" = "Choose Sticker";
+"lng_stickers_pick_existing_about" = "Pick a sticker from your library to add it to this set.";
+"lng_stickers_pick_existing_empty" = "You don't have any stickers yet.";
+"lng_stickers_create_image_title" = "New Sticker";
+"lng_stickers_create_image_about" = "Choose an image to add as a sticker.";
+"lng_stickers_create_choose_image" = "Choose Image";
+"lng_stickers_create_image_filter" = "Images";
+"lng_stickers_create_open_failed" = "Could not load image.";
+"lng_stickers_create_too_small" = "The image must be at least {size} pixels on each side.";
+"lng_stickers_create_choose_emoji" = "Choose an emoji that corresponds to your sticker:";
+"lng_stickers_create_emoji_about" = "Choose emojis that match your sticker";
+"lng_stickers_create_emoji_required" = "Please choose an emoji.";
+"lng_stickers_create_uploading" = "Uploading sticker…";
+"lng_stickers_create_upload_failed" = "Sticker upload failed. Please try again.";
+"lng_stickers_create_added" = "Sticker added.";
+
"lng_in_dlg_photo" = "Photo";
"lng_in_dlg_album" = "Album";
"lng_in_dlg_video" = "Video";
"lng_in_dlg_audio_file" = "Audio file";
"lng_in_dlg_contact" = "Contact";
"lng_in_dlg_audio" = "Voice message";
+"lng_in_dlg_audio_unread" = "{emoji} Voice message";
"lng_in_dlg_video_message" = "Video message";
"lng_in_dlg_voice_message_ttl" = "One-time Voice Message";
"lng_in_dlg_video_message_ttl" = "One-time Video Message";
@@ -5120,6 +5164,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_reactions" = "Reactions contain emoji from **{name} pack**.";
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
+"lng_context_animated_poll_option" = "This option contains emoji from **{name} pack**.";
+"lng_context_animated_poll_option_many#one" = "This option contains emoji from **{count} pack**.";
+"lng_context_animated_poll_option_many#other" = "This option contains emoji from **{count} packs**.";
"lng_context_noforwards_info_channel" = "Copying and forwarding is not allowed in this channel.";
"lng_context_noforwards_info_group" = "Copying and forwarding is not allowed in this group.";
@@ -5597,6 +5644,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mediaview_playback_speed" = "Playback speed: {speed}";
"lng_mediaview_rotate_video" = "Rotate video";
"lng_mediaview_quality_auto" = "Auto";
+"lng_mediaview_quality_original" = "Original ({quality}p)";
"lng_theme_preview_title" = "Theme Preview";
"lng_theme_preview_generating" = "Generating color theme preview...";
@@ -7170,10 +7218,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_photo_editor_menu_flip" = "Flip";
"lng_photo_editor_menu_duplicate" = "Duplicate";
+"lng_photo_editor_text_style_plain" = "Plain";
+"lng_photo_editor_text_style_framed" = "Framed";
+"lng_photo_editor_text_style_semi_transparent" = "Semi-Transparent";
+
"lng_photo_editor_crop_original" = "Original";
"lng_photo_editor_crop_square" = "Square";
"lng_photo_editor_crop_free" = "Free";
+"lng_photo_editor_corners_about" = "Choose how rounded the sticker corners should look.";
+"lng_photo_editor_corners_large" = "Large";
+"lng_photo_editor_corners_medium" = "Medium";
+"lng_photo_editor_corners_small" = "Small";
+"lng_photo_editor_corners_none" = "None";
+
"lng_voice_speed_slow" = "Slow";
"lng_voice_speed_normal" = "Normal";
"lng_voice_speed_medium" = "Medium";
@@ -7838,6 +7896,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_ai_compose_apply_style" = "Apply Style";
"lng_ai_compose_style_tooltip" = "Choose Style";
+"lng_send_as_file_tooltip" = "Send text as a file.";
+
// Wnd specific
"lng_wnd_choose_program_menu" = "Choose Default Program...";
@@ -7904,6 +7964,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mac_hold_to_quit" = "Hold {text} to Quit";
+"lng_sr_country_column_name" = "Country name";
+"lng_sr_languages_column_native" = "Native name";
+"lng_sr_languages_column_name" = "Language name";
+
// Keys finished
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index f602bb2280..b789e77d2f 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="6.7.8.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index fd9ae9da39..624e3839ac 100644
--- a/Telegram/Resources/winrc/Telegram.rc
+++ b/Telegram/Resources/winrc/Telegram.rc
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 6,7,5,0
- PRODUCTVERSION 6,7,5,0
+ FILEVERSION 6,7,8,0
+ PRODUCTVERSION 6,7,8,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
- VALUE "FileVersion", "6.7.5.0"
+ VALUE "FileVersion", "6.7.8.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "6.7.5.0"
+ VALUE "ProductVersion", "6.7.8.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index c7ddd59011..68a32b8cc5 100644
--- a/Telegram/Resources/winrc/Updater.rc
+++ b/Telegram/Resources/winrc/Updater.rc
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
- FILEVERSION 6,7,5,0
- PRODUCTVERSION 6,7,5,0
+ FILEVERSION 6,7,8,0
+ PRODUCTVERSION 6,7,8,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
- VALUE "FileVersion", "6.7.5.0"
+ VALUE "FileVersion", "6.7.8.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "6.7.5.0"
+ VALUE "ProductVersion", "6.7.8.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp
index f1a3f81d1c..059cbfc850 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.cpp
+++ b/Telegram/SourceFiles/api/api_chat_invite.cpp
@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
+#include "styles/style_color_indices.h"
#include "styles/style_credits.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
@@ -48,6 +49,69 @@ namespace Api {
namespace {
+struct InviteParticipant {
+ not_null user;
+ Ui::PeerUserpicView userpic;
+};
+
+struct ChatInvite {
+ QString title;
+ QString about;
+ PhotoData *photo = nullptr;
+ int participantsCount = 0;
+ std::vector participants;
+ bool isPublic = false;
+ bool isChannel = false;
+ bool isMegagroup = false;
+ bool isBroadcast = false;
+ bool isRequestNeeded = false;
+ bool isFake = false;
+ bool isScam = false;
+ bool isVerified = false;
+};
+
+[[nodiscard]] ChatInvite ParseInvite(
+ not_null session,
+ const MTPDchatInvite &data) {
+ auto participants = std::vector();
+ if (const auto list = data.vparticipants()) {
+ participants.reserve(list->v.size());
+ for (const auto &participant : list->v) {
+ if (const auto user = session->data().processUser(participant)) {
+ participants.push_back(InviteParticipant{ user });
+ }
+ }
+ }
+ const auto photo = session->data().processPhoto(data.vphoto());
+ return {
+ .title = qs(data.vtitle()),
+ .about = data.vabout().value_or_empty(),
+ .photo = (photo->isNull() ? nullptr : photo.get()),
+ .participantsCount = data.vparticipants_count().v,
+ .participants = std::move(participants),
+ .isPublic = data.is_public(),
+ .isChannel = data.is_channel(),
+ .isMegagroup = data.is_megagroup(),
+ .isBroadcast = data.is_broadcast(),
+ .isRequestNeeded = data.is_request_needed(),
+ .isFake = data.is_fake(),
+ .isScam = data.is_scam(),
+ .isVerified = data.is_verified(),
+ };
+}
+
+[[nodiscard]] Info::Profile::BadgeType BadgeForInvite(
+ const ChatInvite &invite) {
+ using Type = Info::Profile::BadgeType;
+ return invite.isVerified
+ ? Type::Verified
+ : invite.isScam
+ ? Type::Scam
+ : invite.isFake
+ ? Type::Fake
+ : Type::None;
+}
+
void SubmitChatInvite(
base::weak_ptr weak,
not_null session,
@@ -200,7 +264,7 @@ void ConfirmSubscriptionBox(
}
} else {
state->photoEmpty = std::make_unique(
- Ui::EmptyUserpic::UserpicColor(0),
+ Ui::EmptyUserpic::UserpicColor(st::colorIndexRed),
name);
}
Ui::AddSkip(content);
@@ -356,6 +420,207 @@ void ConfirmSubscriptionBox(
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
+void ConfirmInviteBox(
+ not_null box,
+ not_null session,
+ const MTPDchatInvite *invitePtr,
+ ChannelData *invitePeekChannel,
+ Fn submit) {
+ auto invite = ParseInvite(session, *invitePtr);
+ const auto isChannel = invite.isChannel && !invite.isMegagroup;
+ const auto requestApprove = invite.isRequestNeeded;
+ const auto count = invite.participantsCount;
+
+ struct State {
+ std::shared_ptr photoMedia;
+ std::unique_ptr photoEmpty;
+ std::vector participants;
+ };
+ const auto state = box->lifetime().make_state();
+ state->participants = std::move(invite.participants);
+
+ const auto status = [&] {
+ return invitePeekChannel
+ ? tr::lng_channel_invite_private(tr::now)
+ : (!state->participants.empty()
+ && int(state->participants.size()) < count)
+ ? tr::lng_group_invite_members(tr::now, lt_count, count)
+ : (count > 0 && isChannel)
+ ? tr::lng_chat_status_subscribers(
+ tr::now,
+ lt_count_decimal,
+ count)
+ : (count > 0)
+ ? tr::lng_chat_status_members(tr::now, lt_count_decimal, count)
+ : isChannel
+ ? tr::lng_channel_status(tr::now)
+ : tr::lng_group_status(tr::now);
+ }();
+
+ box->setNoContentMargin(true);
+ box->setWidth(st::boxWideWidth);
+ const auto content = box->verticalLayout();
+
+ Ui::AddSkip(content, st::confirmInvitePhotoTop);
+ const auto userpic = content->add(
+ object_ptr(content),
+ style::al_top);
+ const auto photoSize = st::confirmInvitePhotoSize;
+ userpic->resize(Size(photoSize));
+ userpic->setNaturalWidth(photoSize);
+ userpic->paintRequest(
+ ) | rpl::on_next([=, small = Data::PhotoSize::Small] {
+ auto p = QPainter(userpic);
+ if (state->photoMedia) {
+ if (const auto image = state->photoMedia->image(small)) {
+ p.drawPixmap(
+ 0,
+ 0,
+ image->pix(
+ Size(photoSize),
+ { .options = Images::Option::RoundCircle }));
+ }
+ } else if (state->photoEmpty) {
+ state->photoEmpty->paintCircle(
+ p,
+ 0,
+ 0,
+ userpic->width(),
+ photoSize);
+ }
+ }, userpic->lifetime());
+ userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
+ if (const auto photo = invite.photo) {
+ state->photoMedia = photo->createMediaView();
+ state->photoMedia->wanted(
+ Data::PhotoSize::Small,
+ Data::FileOrigin());
+ if (!state->photoMedia->image(Data::PhotoSize::Small)) {
+ session->downloaderTaskFinished(
+ ) | rpl::on_next([=] {
+ userpic->update();
+ }, userpic->lifetime());
+ }
+ } else {
+ state->photoEmpty = std::make_unique(
+ Ui::EmptyUserpic::UserpicColor(st::colorIndexRed),
+ invite.title);
+ }
+
+ Ui::AddSkip(content);
+ const auto title = box->addRow(
+ object_ptr(
+ box,
+ invite.title,
+ st::confirmInviteTitle),
+ style::al_top);
+
+ const auto badgeType = BadgeForInvite(invite);
+ if (badgeType != Info::Profile::BadgeType::None) {
+ const auto badgeParent = title->parentWidget();
+ const auto badge = box->lifetime().make_state(
+ badgeParent,
+ st::infoPeerBadge,
+ session,
+ rpl::single(Info::Profile::Badge::Content{ badgeType }),
+ nullptr,
+ [] { return false; });
+ title->geometryValue(
+ ) | rpl::on_next([=](const QRect &r) {
+ badge->move(r.x() + r.width(), r.y(), r.y() + r.height());
+ }, title->lifetime());
+ }
+
+ box->addRow(
+ object_ptr(
+ box,
+ status,
+ st::confirmInviteStatus),
+ style::al_top);
+
+ if (!invite.about.isEmpty()) {
+ box->addRow(
+ object_ptr(
+ box,
+ invite.about,
+ st::confirmInviteAbout),
+ st::confirmInviteAboutPadding,
+ style::al_top);
+ }
+
+ if (requestApprove) {
+ box->addRow(
+ object_ptr(
+ box,
+ (isChannel
+ ? tr::lng_group_request_about_channel(tr::now)
+ : tr::lng_group_request_about(tr::now)),
+ st::confirmInviteStatus),
+ st::confirmInviteAboutRequestsPadding,
+ style::al_top);
+ }
+
+ if (!state->participants.empty()) {
+ while (state->participants.size() > 4) {
+ state->participants.pop_back();
+ }
+ const auto padding = (st::confirmInviteUsersWidth
+ - 4 * st::confirmInviteUserPhotoSize) / 10;
+ const auto userWidth = st::confirmInviteUserPhotoSize + 2 * padding;
+
+ auto strip = object_ptr(content);
+ const auto rawStrip = strip.data();
+ rawStrip->resize(st::boxWideWidth, st::confirmInviteUserHeight);
+ rawStrip->setNaturalWidth(st::boxWideWidth);
+
+ const auto shown = int(state->participants.size());
+ const auto sumWidth = shown * userWidth;
+ const auto baseLeft = (st::boxWideWidth - sumWidth) / 2;
+ for (auto i = 0; i != shown; ++i) {
+ const auto &participant = state->participants[i];
+ const auto name = Ui::CreateChild(
+ rawStrip,
+ st::confirmInviteUserName);
+ name->resizeToWidth(
+ st::confirmInviteUserPhotoSize + padding);
+ name->setText(participant.user->firstName.isEmpty()
+ ? participant.user->name()
+ : participant.user->firstName);
+ name->moveToLeft(
+ baseLeft + i * userWidth + (padding / 2),
+ st::confirmInviteUserNameTop - st::confirmInviteUserPhotoTop);
+ }
+
+ rawStrip->paintRequest(
+ ) | rpl::on_next([=] {
+ auto p = Painter(rawStrip);
+ const auto total = int(state->participants.size());
+ const auto totalWidth = total * userWidth;
+ auto left = (rawStrip->width() - totalWidth) / 2;
+ for (auto &participant : state->participants) {
+ participant.user->paintUserpicLeft(
+ p,
+ participant.userpic,
+ left + (userWidth - st::confirmInviteUserPhotoSize) / 2,
+ 0,
+ rawStrip->width(),
+ st::confirmInviteUserPhotoSize);
+ left += userWidth;
+ }
+ }, rawStrip->lifetime());
+
+ Ui::AddSkip(content, st::boxPadding.bottom());
+ content->add(std::move(strip), style::margins());
+ }
+
+ box->addButton((requestApprove
+ ? tr::lng_group_request_to_join()
+ : isChannel
+ ? tr::lng_profile_join_channel()
+ : tr::lng_profile_join_group()), submit);
+ box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+}
+
} // namespace
void CheckChatInvite(
@@ -399,9 +664,10 @@ void CheckChatInvite(
session,
hash,
&data))
- : strong->show(Box(
+ : strong->show(Box(
+ ConfirmInviteBox,
session,
- data,
+ &data,
invitePeekChannel,
[=] { SubmitChatInvite(weak, session, hash, isGroup); }));
if (invitePeekChannel) {
@@ -450,255 +716,3 @@ void CheckChatInvite(
}
} // namespace Api
-
-struct ConfirmInviteBox::Participant {
- not_null user;
- Ui::PeerUserpicView userpic;
-};
-
-ConfirmInviteBox::ConfirmInviteBox(
- QWidget*,
- not_null session,
- const MTPDchatInvite &data,
- ChannelData *invitePeekChannel,
- Fn submit)
-: ConfirmInviteBox(
- session,
- Parse(session, data),
- invitePeekChannel,
- std::move(submit)) {
-}
-
-ConfirmInviteBox::ConfirmInviteBox(
- not_null session,
- ChatInvite &&invite,
- ChannelData *invitePeekChannel,
- Fn submit)
-: _session(session)
-, _submit(std::move(submit))
-, _title(this, st::confirmInviteTitle)
-, _badge(std::make_unique(
- this,
- st::infoPeerBadge,
- _session,
- rpl::single(Info::Profile::Badge::Content{ BadgeForInvite(invite) }),
- nullptr,
- [=] { return false; }))
-, _status(this, st::confirmInviteStatus)
-, _about(this, st::confirmInviteAbout)
-, _aboutRequests(this, st::confirmInviteStatus)
-, _participants(std::move(invite.participants))
-, _isChannel(invite.isChannel && !invite.isMegagroup)
-, _requestApprove(invite.isRequestNeeded) {
- const auto count = invite.participantsCount;
- const auto status = [&] {
- return invitePeekChannel
- ? tr::lng_channel_invite_private(tr::now)
- : (!_participants.empty() && _participants.size() < count)
- ? tr::lng_group_invite_members(tr::now, lt_count, count)
- : (count > 0 && _isChannel)
- ? tr::lng_chat_status_subscribers(
- tr::now,
- lt_count_decimal,
- count)
- : (count > 0)
- ? tr::lng_chat_status_members(tr::now, lt_count_decimal, count)
- : _isChannel
- ? tr::lng_channel_status(tr::now)
- : tr::lng_group_status(tr::now);
- }();
- _title->setText(invite.title);
- _status->setText(status);
- if (!invite.about.isEmpty()) {
- _about->setText(invite.about);
- } else {
- _about.destroy();
- }
- if (_requestApprove) {
- _aboutRequests->setText(_isChannel
- ? tr::lng_group_request_about_channel(tr::now)
- : tr::lng_group_request_about(tr::now));
- } else {
- _aboutRequests.destroy();
- }
-
- if (invite.photo) {
- _photo = invite.photo->createMediaView();
- _photo->wanted(Data::PhotoSize::Small, Data::FileOrigin());
- if (!_photo->image(Data::PhotoSize::Small)) {
- _session->downloaderTaskFinished(
- ) | rpl::on_next([=] {
- update();
- }, lifetime());
- }
- } else {
- _photoEmpty = std::make_unique(
- Ui::EmptyUserpic::UserpicColor(0),
- invite.title);
- }
-}
-
-ConfirmInviteBox::~ConfirmInviteBox() = default;
-
-ConfirmInviteBox::ChatInvite ConfirmInviteBox::Parse(
- not_null session,
- const MTPDchatInvite &data) {
- auto participants = std::vector();
- if (const auto list = data.vparticipants()) {
- participants.reserve(list->v.size());
- for (const auto &participant : list->v) {
- if (const auto user = session->data().processUser(participant)) {
- participants.push_back(Participant{ user });
- }
- }
- }
- const auto photo = session->data().processPhoto(data.vphoto());
- return {
- .title = qs(data.vtitle()),
- .about = data.vabout().value_or_empty(),
- .photo = (photo->isNull() ? nullptr : photo.get()),
- .participantsCount = data.vparticipants_count().v,
- .participants = std::move(participants),
- .isPublic = data.is_public(),
- .isChannel = data.is_channel(),
- .isMegagroup = data.is_megagroup(),
- .isBroadcast = data.is_broadcast(),
- .isRequestNeeded = data.is_request_needed(),
- .isFake = data.is_fake(),
- .isScam = data.is_scam(),
- .isVerified = data.is_verified(),
- };
-}
-
-[[nodiscard]] Info::Profile::BadgeType ConfirmInviteBox::BadgeForInvite(
- const ChatInvite &invite) {
- using Type = Info::Profile::BadgeType;
- return invite.isVerified
- ? Type::Verified
- : invite.isScam
- ? Type::Scam
- : invite.isFake
- ? Type::Fake
- : Type::None;
-}
-
-void ConfirmInviteBox::prepare() {
- addButton(
- (_requestApprove
- ? tr::lng_group_request_to_join()
- : _isChannel
- ? tr::lng_profile_join_channel()
- : tr::lng_profile_join_group()),
- _submit);
- addButton(tr::lng_cancel(), [=] { closeBox(); });
-
- while (_participants.size() > 4) {
- _participants.pop_back();
- }
-
- auto newHeight = st::confirmInviteStatusTop + _status->height() + st::boxPadding.bottom();
- if (!_participants.empty()) {
- int skip = (st::confirmInviteUsersWidth - 4 * st::confirmInviteUserPhotoSize) / 5;
- int padding = skip / 2;
- _userWidth = (st::confirmInviteUserPhotoSize + 2 * padding);
- int sumWidth = _participants.size() * _userWidth;
- int left = (st::boxWideWidth - sumWidth) / 2;
- for (const auto &participant : _participants) {
- auto name = new Ui::FlatLabel(this, st::confirmInviteUserName);
- name->resizeToWidth(st::confirmInviteUserPhotoSize + padding);
- name->setText(participant.user->firstName.isEmpty()
- ? participant.user->name()
- : participant.user->firstName);
- name->moveToLeft(left + (padding / 2), st::confirmInviteUserNameTop);
- left += _userWidth;
- }
-
- newHeight += st::confirmInviteUserHeight;
- }
- if (_about) {
- const auto padding = st::confirmInviteAboutPadding;
- _about->resizeToWidth(st::boxWideWidth - padding.left() - padding.right());
- newHeight += padding.top() + _about->height() + padding.bottom();
- }
- if (_aboutRequests) {
- const auto padding = st::confirmInviteAboutRequestsPadding;
- _aboutRequests->resizeToWidth(st::boxWideWidth - padding.left() - padding.right());
- newHeight += padding.top() + _aboutRequests->height() + padding.bottom();
- }
- setDimensions(st::boxWideWidth, newHeight);
-}
-
-void ConfirmInviteBox::resizeEvent(QResizeEvent *e) {
- BoxContent::resizeEvent(e);
-
- const auto padding = st::boxRowPadding;
- auto nameWidth = width() - padding.left() - padding.right();
- auto badgeWidth = 0;
- if (const auto widget = _badge->widget()) {
- badgeWidth = st::infoVerifiedCheckPosition.x() + widget->width();
- nameWidth -= badgeWidth;
- }
- _title->resizeToWidth(std::min(nameWidth, _title->textMaxWidth()));
- _title->moveToLeft(
- (width() - _title->width() - badgeWidth) / 2,
- st::confirmInviteTitleTop);
- const auto badgeLeft = _title->x() + _title->width();
- const auto badgeTop = _title->y();
- const auto badgeBottom = _title->y() + _title->height();
- _badge->move(badgeLeft, badgeTop, badgeBottom);
-
- _status->move(
- (width() - _status->width()) / 2,
- st::confirmInviteStatusTop);
- auto bottom = _status->y()
- + _status->height()
- + st::boxPadding.bottom()
- + (_participants.empty() ? 0 : st::confirmInviteUserHeight);
- if (_about) {
- const auto padding = st::confirmInviteAboutPadding;
- _about->move((width() - _about->width()) / 2, bottom + padding.top());
- bottom += padding.top() + _about->height() + padding.bottom();
- }
- if (_aboutRequests) {
- const auto padding = st::confirmInviteAboutRequestsPadding;
- _aboutRequests->move((width() - _aboutRequests->width()) / 2, bottom + padding.top());
- }
-}
-
-void ConfirmInviteBox::paintEvent(QPaintEvent *e) {
- BoxContent::paintEvent(e);
-
- Painter p(this);
-
- if (_photo) {
- if (const auto image = _photo->image(Data::PhotoSize::Small)) {
- const auto size = st::confirmInvitePhotoSize;
- p.drawPixmap(
- (width() - size) / 2,
- st::confirmInvitePhotoTop,
- image->pix(
- { size, size },
- { .options = Images::Option::RoundCircle }));
- }
- } else if (_photoEmpty) {
- _photoEmpty->paintCircle(
- p,
- (width() - st::confirmInvitePhotoSize) / 2,
- st::confirmInvitePhotoTop,
- width(),
- st::confirmInvitePhotoSize);
- }
-
- int sumWidth = _participants.size() * _userWidth;
- int left = (width() - sumWidth) / 2;
- for (auto &participant : _participants) {
- participant.user->paintUserpicLeft(
- p,
- participant.userpic,
- left + (_userWidth - st::confirmInviteUserPhotoSize) / 2,
- st::confirmInviteUserPhotoTop,
- width(),
- st::confirmInviteUserPhotoSize);
- left += _userWidth;
- }
-}
diff --git a/Telegram/SourceFiles/api/api_chat_invite.h b/Telegram/SourceFiles/api/api_chat_invite.h
index 7ae7114243..dbd5a43321 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.h
+++ b/Telegram/SourceFiles/api/api_chat_invite.h
@@ -7,16 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
-#include "ui/layers/box_content.h"
-
-class UserData;
class ChannelData;
-namespace Info::Profile {
-class Badge;
-enum class BadgeType : ushort;
-} // namespace Info::Profile
-
namespace Main {
class Session;
} // namespace Main
@@ -25,14 +17,6 @@ namespace Window {
class SessionController;
} // namespace Window
-namespace Data {
-class PhotoMedia;
-} // namespace Data
-
-namespace Ui {
-class EmptyUserpic;
-} // namespace Ui
-
namespace Api {
void CheckChatInvite(
@@ -42,67 +26,3 @@ void CheckChatInvite(
Fn loaded = nullptr);
} // namespace Api
-
-class ConfirmInviteBox final : public Ui::BoxContent {
-public:
- ConfirmInviteBox(
- QWidget*,
- not_null session,
- const MTPDchatInvite &data,
- ChannelData *invitePeekChannel,
- Fn submit);
- ~ConfirmInviteBox();
-
-protected:
- void prepare() override;
-
- void resizeEvent(QResizeEvent *e) override;
- void paintEvent(QPaintEvent *e) override;
-
-private:
- struct Participant;
- struct ChatInvite {
- QString title;
- QString about;
- PhotoData *photo = nullptr;
- int participantsCount = 0;
- std::vector participants;
- bool isPublic = false;
- bool isChannel = false;
- bool isMegagroup = false;
- bool isBroadcast = false;
- bool isRequestNeeded = false;
- bool isFake = false;
- bool isScam = false;
- bool isVerified = false;
- };
- [[nodiscard]] static ChatInvite Parse(
- not_null session,
- const MTPDchatInvite &data);
- [[nodiscard]] Info::Profile::BadgeType BadgeForInvite(
- const ChatInvite &invite);
-
- ConfirmInviteBox(
- not_null session,
- ChatInvite &&invite,
- ChannelData *invitePeekChannel,
- Fn submit);
-
- const not_null _session;
-
- Fn _submit;
- object_ptr _title;
- std::unique_ptr _badge;
- object_ptr _status;
- object_ptr _about;
- object_ptr _aboutRequests;
- std::shared_ptr _photo;
- std::unique_ptr _photoEmpty;
- std::vector _participants;
-
- bool _isChannel = false;
- bool _requestApprove = false;
-
- int _userWidth = 0;
-
-};
diff --git a/Telegram/SourceFiles/api/api_credits.h b/Telegram/SourceFiles/api/api_credits.h
index 1b5745e42b..0ebe857a78 100644
--- a/Telegram/SourceFiles/api/api_credits.h
+++ b/Telegram/SourceFiles/api/api_credits.h
@@ -111,8 +111,6 @@ private:
const bool _isUser = false;
Data::CreditsEarnStatistics _data;
- mtpRequestId _requestId = 0;
-
};
[[nodiscard]] rpl::producer> PremiumPeerBot(
diff --git a/Telegram/SourceFiles/api/api_messages_search.cpp b/Telegram/SourceFiles/api/api_messages_search.cpp
index 13a0e96bf7..ac348199bc 100644
--- a/Telegram/SourceFiles/api/api_messages_search.cpp
+++ b/Telegram/SourceFiles/api/api_messages_search.cpp
@@ -58,9 +58,23 @@ constexpr auto kSearchPerPage = 50;
result += u"emoji"_q + tag.emoji();
}
}
+ switch (request.filter) {
+ case SearchFilter::NoFilter: break;
+ case SearchFilter::Pinned: result += u"\npinned"_q; break;
+ }
return result;
}
+[[nodiscard]] MTPMessagesFilter PrepareFilter(SearchFilter filter) {
+ switch (filter) {
+ case SearchFilter::Pinned:
+ return MTP_inputMessagesFilterPinned();
+ case SearchFilter::NoFilter:
+ return MTP_inputMessagesFilterEmpty();
+ }
+ return MTP_inputMessagesFilterEmpty();
+}
+
} // namespace
MessagesSearch::MessagesSearch(not_null history)
@@ -113,7 +127,7 @@ void MessagesSearch::searchRequest() {
Data::ReactionToMTP
)),
MTP_int(_request.topMsgId), // top_msg_id
- MTP_inputMessagesFilterEmpty(),
+ PrepareFilter(_request.filter),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_offsetId), // offset_id
diff --git a/Telegram/SourceFiles/api/api_messages_search.h b/Telegram/SourceFiles/api/api_messages_search.h
index b650b87994..99b222b703 100644
--- a/Telegram/SourceFiles/api/api_messages_search.h
+++ b/Telegram/SourceFiles/api/api_messages_search.h
@@ -20,6 +20,11 @@ struct ReactionId;
namespace Api {
+enum class SearchFilter {
+ NoFilter,
+ Pinned,
+};
+
struct FoundMessages {
int total = -1;
MessageIdsList messages;
@@ -33,6 +38,7 @@ public:
PeerData *from = nullptr;
std::vector tags;
MsgId topMsgId;
+ SearchFilter filter = SearchFilter::NoFilter;
friend inline bool operator==(
const Request &,
diff --git a/Telegram/SourceFiles/api/api_statistics.h b/Telegram/SourceFiles/api/api_statistics.h
index b419b8841c..b60dd3d28f 100644
--- a/Telegram/SourceFiles/api/api_statistics.h
+++ b/Telegram/SourceFiles/api/api_statistics.h
@@ -75,8 +75,6 @@ private:
Data::PublicForwardsSlice _firstSlice;
- mtpRequestId _requestId = 0;
-
};
class EarnStatistics final : public StatisticsRequestSender {
diff --git a/Telegram/SourceFiles/api/api_stickers_creator.cpp b/Telegram/SourceFiles/api/api_stickers_creator.cpp
new file mode 100644
index 0000000000..5b1b85d371
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_stickers_creator.cpp
@@ -0,0 +1,543 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "api/api_stickers_creator.h"
+
+#include "apiwrap.h"
+#include "base/random.h"
+#include "base/unixtime.h"
+#include "chat_helpers/compose/compose_show.h"
+#include "data/data_document.h"
+#include "data/data_file_origin.h"
+#include "data/data_session.h"
+#include "data/stickers/data_stickers.h"
+#include "data/stickers/data_stickers_set.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "menu/menu_action_with_thumbnail.h"
+#include "storage/file_upload.h"
+#include "storage/localimageloader.h"
+#include "styles/style_menu_icons.h"
+#include "styles/style_widgets.h"
+#include "ui/dynamic_thumbnails.h"
+#include "ui/widgets/menu/menu_add_action_callback.h"
+#include "ui/widgets/menu/menu_common.h"
+#include "ui/widgets/popup_menu.h"
+
+namespace Api {
+namespace {
+
+constexpr auto kStickerSide = 512;
+
+[[nodiscard]] MTPInputStickerSetItem InputItem(
+ const MTPInputDocument &document,
+ const QString &emoji) {
+ return MTP_inputStickerSetItem(
+ MTP_flags(0),
+ document,
+ MTP_string(emoji),
+ MTPMaskCoords(),
+ MTPstring());
+}
+
+[[nodiscard]] std::shared_ptr PrepareStickerWebp(
+ MTP::DcId dcId,
+ DocumentId id,
+ const QByteArray &bytes) {
+ const auto filename = u"sticker.webp"_q;
+ auto attributes = QVector(
+ 1,
+ MTP_documentAttributeFilename(MTP_string(filename)));
+ attributes.push_back(MTP_documentAttributeImageSize(
+ MTP_int(kStickerSide),
+ MTP_int(kStickerSide)));
+
+ auto result = MakePreparedFile({
+ .id = id,
+ .type = SendMediaType::File,
+ });
+ result->filename = filename;
+ result->filemime = u"image/webp"_q;
+ result->content = bytes;
+ result->filesize = bytes.size();
+ result->setFileData(bytes);
+ result->document = MTP_document(
+ MTP_flags(0),
+ MTP_long(id),
+ MTP_long(0),
+ MTP_bytes(),
+ MTP_int(base::unixtime::now()),
+ MTP_string("image/webp"),
+ MTP_long(bytes.size()),
+ MTP_vector(),
+ MTPVector(),
+ MTP_int(dcId),
+ MTP_vector(std::move(attributes)));
+ return result;
+}
+
+void FeedSetIfFull(
+ not_null session,
+ const MTPmessages_StickerSet &result) {
+ result.match([&](const MTPDmessages_stickerSet &data) {
+ session->data().stickers().feedSetFull(data);
+ session->data().stickers().notifyUpdated(
+ Data::StickersType::Stickers);
+ }, [](const auto &) {
+ });
+}
+
+template
+void EnumerateOwnedSets(
+ not_null session,
+ Data::StickersType type,
+ Callback &&callback) {
+ const auto &stickers = session->data().stickers();
+ const auto &sets = stickers.sets();
+ const auto &order = (type == Data::StickersType::Emoji)
+ ? stickers.emojiSetsOrder()
+ : stickers.setsOrder();
+ for (const auto setId : order) {
+ const auto it = sets.find(setId);
+ if (it == sets.end()) {
+ continue;
+ }
+ const auto set = it->second.get();
+ if (!(set->flags & Data::StickersSetFlag::AmCreator)
+ || (set->type() != type)) {
+ continue;
+ }
+ using namespace Data;
+ if constexpr (std::is_same_v<
+ bool,
+ std::invoke_result_t>>) {
+ if (!callback(set)) {
+ return;
+ }
+ } else {
+ callback(set);
+ }
+ }
+}
+
+void FillChooseOwnedSetMenu(
+ not_null menu,
+ std::shared_ptr show,
+ not_null document,
+ Data::StickersType type) {
+ const auto session = &show->session();
+ const auto emoji = StickerEmojiOrDefault(document);
+ const auto isEmoji = (type == Data::StickersType::Emoji);
+ const auto maxCount = isEmoji
+ ? kEmojiInOwnedSetMax
+ : kStickersInOwnedSetMax;
+ const auto fullMessage = isEmoji
+ ? tr::lng_emoji_set_is_full
+ : tr::lng_stickers_set_is_full;
+ const auto addedMessage = isEmoji
+ ? tr::lng_emoji_added
+ : tr::lng_stickers_create_added;
+ const auto alreadyMessage = isEmoji
+ ? tr::lng_emoji_already_in_set
+ : tr::lng_stickers_already_in_set;
+ const auto failToast = [=](QString err) {
+ show->showToast(err.isEmpty()
+ ? tr::lng_attach_failed(tr::now)
+ : err);
+ };
+ EnumerateOwnedSets(session, type, [&](not_null set) {
+ const auto identifier = set->identifier();
+ const auto coverDocument = set->lookupThumbnailDocument();
+ auto thumbnail = coverDocument
+ ? Ui::MakeDocumentThumbnail(
+ coverDocument,
+ Data::FileOriginStickerSet(set->id, set->accessHash))
+ : nullptr;
+ const auto targetSetId = set->id;
+ const auto handler = crl::guard(session, [=] {
+ const auto &map = session->data().stickers().sets();
+ const auto i = map.find(targetSetId);
+ if (i != map.end() && i->second->count >= maxCount) {
+ show->showToast(fullMessage(tr::now));
+ return;
+ }
+ const auto oldCount = (i != map.end())
+ ? i->second->count
+ : 0;
+ AddExistingStickerToSet(
+ session,
+ identifier,
+ document,
+ emoji,
+ crl::guard(session, [=](MTPmessages_StickerSet) {
+ const auto &map = session->data().stickers().sets();
+ const auto i = map.find(targetSetId);
+ const auto newCount = (i != map.end())
+ ? i->second->count
+ : oldCount;
+ show->showToast(newCount > oldCount
+ ? addedMessage(tr::now)
+ : alreadyMessage(tr::now));
+ }),
+ crl::guard(session, failToast));
+ });
+ const auto rawAction = Ui::Menu::CreateAction(
+ menu.get(),
+ set->title,
+ handler);
+ auto item = base::make_unique_q(
+ menu->menu(),
+ menu->menu()->st(),
+ rawAction,
+ std::move(thumbnail),
+ st::menuIconStickerAdd.width());
+ menu->addAction(std::move(item));
+ });
+}
+
+} // namespace
+
+void AddExistingStickerToSet(
+ not_null session,
+ const StickerSetIdentifier &set,
+ not_null document,
+ const QString &emoji,
+ Fn done,
+ Fn fail) {
+ session->api().request(MTPstickers_AddStickerToSet(
+ Data::InputStickerSet(set),
+ InputItem(document->mtpInput(), emoji))
+ ).done([=](const MTPmessages_StickerSet &result) {
+ FeedSetIfFull(session, result);
+ if (done) {
+ done(result);
+ }
+ }).fail([=](const MTP::Error &error) {
+ if (fail) {
+ fail(error.type());
+ }
+ }).handleFloodErrors().send();
+}
+
+QString StickerEmojiOrDefault(not_null document) {
+ if (const auto sticker = document->sticker()) {
+ if (!sticker->alt.isEmpty()) {
+ return sticker->alt;
+ }
+ }
+ return QString::fromUtf8("\xF0\x9F\x99\x82");
+}
+
+bool HasOwnedStickerSets(not_null session) {
+ auto found = false;
+ EnumerateOwnedSets(
+ session,
+ Data::StickersType::Stickers,
+ [&](not_null) {
+ found = true;
+ return false;
+ });
+ return found;
+}
+
+bool HasOwnedEmojiSets(not_null session) {
+ auto found = false;
+ EnumerateOwnedSets(
+ session,
+ Data::StickersType::Emoji,
+ [&](not_null) {
+ found = true;
+ return false;
+ });
+ return found;
+}
+
+void FillChooseStickerSetMenu(
+ not_null menu,
+ std::shared_ptr show,
+ not_null document) {
+ using namespace Data;
+ FillChooseOwnedSetMenu(menu, show, document, StickersType::Stickers);
+}
+
+void FillChooseEmojiSetMenu(
+ not_null menu,
+ std::shared_ptr show,
+ not_null document) {
+ FillChooseOwnedSetMenu(menu, show, document, Data::StickersType::Emoji);
+}
+
+void AddAddToStickerSetAction(
+ const Ui::Menu::MenuCallback &addAction,
+ std::shared_ptr show,
+ not_null document) {
+ const auto session = &show->session();
+ if (!HasOwnedStickerSets(session)) {
+ return;
+ }
+ addAction({
+ .text = tr::lng_stickers_add_to_set(tr::now),
+ .icon = &st::menuIconStickerAdd,
+ .fillSubmenu = [show, document](not_null submenu) {
+ FillChooseStickerSetMenu(submenu, show, document);
+ },
+ .submenuSt = &st::popupMenuWithIcons,
+ });
+}
+
+void AddAddToEmojiSetAction(
+ const Ui::Menu::MenuCallback &addAction,
+ std::shared_ptr show,
+ not_null document) {
+ const auto session = &show->session();
+ if (!HasOwnedEmojiSets(session)) {
+ return;
+ }
+ addAction({
+ .text = tr::lng_emoji_add_to_set(tr::now),
+ .icon = &st::menuIconEmoji,
+ .fillSubmenu = [show, document](not_null submenu) {
+ FillChooseEmojiSetMenu(submenu, show, document);
+ },
+ .submenuSt = &st::popupMenuWithIcons,
+ });
+}
+
+void AddAddToOwnedSetAction(
+ const Ui::Menu::MenuCallback &addAction,
+ std::shared_ptr show,
+ not_null document) {
+ const auto sticker = document->sticker();
+ const auto isLottie = sticker && sticker->isLottie();
+ const auto size = document->dimensions;
+ const auto fitsEmoji = !size.isEmpty()
+ && (size.width() <= kEmojiStickerSideMax)
+ && (size.height() <= kEmojiStickerSideMax);
+ if (isLottie || !fitsEmoji) {
+ AddAddToStickerSetAction(addAction, show, document);
+ }
+ if (isLottie || fitsEmoji) {
+ AddAddToEmojiSetAction(addAction, std::move(show), document);
+ }
+}
+
+void DeleteStickerSet(
+ not_null session,
+ const StickerSetIdentifier &set,
+ Fn done,
+ Fn fail) {
+ session->api().request(MTPstickers_DeleteStickerSet(
+ Data::InputStickerSet(set))
+ ).done([=] {
+ session->data().stickers().notifyUpdated(
+ Data::StickersType::Stickers);
+ if (done) {
+ done();
+ }
+ }).fail([=](const MTP::Error &error) {
+ if (fail) {
+ fail(error.type());
+ }
+ }).send();
+}
+
+StickerUpload::StickerUpload(
+ not_null session,
+ StickerSetIdentifier set,
+ QByteArray webpBytes,
+ QString emoji)
+: _session(session)
+, _set(std::move(set))
+, _bytes(std::move(webpBytes))
+, _emoji(std::move(emoji))
+, _api(&session->mtp()) {
+}
+
+StickerUpload::~StickerUpload() {
+ cancel();
+}
+
+void StickerUpload::start(
+ Fn done,
+ Fn fail,
+ Fn progress) {
+ Expects(!_uploadId);
+
+ _done = std::move(done);
+ _fail = std::move(fail);
+ _progress = std::move(progress);
+
+ _documentId = base::RandomValue();
+ auto ready = PrepareStickerWebp(
+ _session->mtp().mainDcId(),
+ _documentId,
+ _bytes);
+ _uploadId = FullMsgId(
+ _session->userPeerId(),
+ _session->data().nextLocalMessageId());
+
+ const auto document = _session->data().document(_documentId);
+ document->uploadingData = std::make_unique(
+ document->size > 0 ? document->size : int64(_bytes.size()));
+
+ _session->uploader().documentReady(
+ ) | rpl::filter([=](const Storage::UploadedMedia &data) {
+ return data.fullId == _uploadId;
+ }) | rpl::on_next([=](const Storage::UploadedMedia &data) {
+ uploadReady(data.info.file);
+ }, _uploadLifetime);
+
+ _session->uploader().documentFailed(
+ ) | rpl::filter([=](const FullMsgId &id) {
+ return id == _uploadId;
+ }) | rpl::on_next([=] {
+ uploadFailed();
+ }, _uploadLifetime);
+
+ if (_progress) {
+ _session->uploader().documentProgress(
+ ) | rpl::filter([=](const FullMsgId &id) {
+ return id == _uploadId;
+ }) | rpl::on_next([=] {
+ uploadProgressed();
+ }, _uploadLifetime);
+ }
+
+ _session->uploader().upload(_uploadId, ready);
+}
+
+void StickerUpload::cancel() {
+ if (_uploadId) {
+ _session->uploader().cancel(_uploadId);
+ _uploadId = FullMsgId();
+ }
+ if (_addRequestId) {
+ _api.request(_addRequestId).cancel();
+ _addRequestId = 0;
+ }
+ _uploadLifetime.destroy();
+ _done = nullptr;
+ _fail = nullptr;
+ _progress = nullptr;
+}
+
+void StickerUpload::uploadProgressed() {
+ if (!_progress) {
+ return;
+ }
+ const auto document = _session->data().document(_documentId);
+ if (!document->uploading() || !document->uploadingData) {
+ return;
+ }
+ const auto size = document->uploadingData->size;
+ if (size <= 0) {
+ return;
+ }
+ const auto percent = int(
+ (document->uploadingData->offset * 100) / size);
+ if (percent != _lastReportedPercent) {
+ _lastReportedPercent = percent;
+ _progress(percent);
+ }
+}
+
+void StickerUpload::uploadFailed() {
+ const auto fail = std::move(_fail);
+ cancel();
+ if (fail) {
+ fail(QString());
+ }
+}
+
+void StickerUpload::uploadReady(const MTPInputFile &file) {
+ _uploadLifetime.destroy();
+ _uploadId = FullMsgId();
+
+ auto attributes = QVector();
+ attributes.push_back(MTP_documentAttributeSticker(
+ MTP_flags(0),
+ MTP_string(_emoji),
+ MTP_inputStickerSetEmpty(),
+ MTPMaskCoords()));
+ attributes.push_back(MTP_documentAttributeImageSize(
+ MTP_int(kStickerSide),
+ MTP_int(kStickerSide)));
+
+ const auto media = MTP_inputMediaUploadedDocument(
+ MTP_flags(0),
+ file,
+ MTPInputFile(),
+ MTP_string("image/webp"),
+ MTP_vector(std::move(attributes)),
+ MTP_vector(),
+ MTPInputPhoto(),
+ MTP_int(0),
+ MTP_int(0));
+
+ _addRequestId = _api.request(MTPmessages_UploadMedia(
+ MTP_flags(0),
+ MTPstring(),
+ MTP_inputPeerSelf(),
+ media
+ )).done(crl::guard(this, [=](const MTPMessageMedia &result) {
+ _addRequestId = 0;
+ auto inputDoc = (MTPInputDocument*)(nullptr);
+ auto storage = MTPInputDocument();
+ result.match([&](const MTPDmessageMediaDocument &data) {
+ if (const auto doc = data.vdocument()) {
+ doc->match([&](const MTPDdocument &d) {
+ storage = MTP_inputDocument(
+ d.vid(),
+ d.vaccess_hash(),
+ d.vfile_reference());
+ inputDoc = &storage;
+ }, [](const auto &) {
+ });
+ }
+ }, [](const auto &) {
+ });
+ if (inputDoc) {
+ requestAddSticker(*inputDoc);
+ } else if (const auto fail = std::move(_fail)) {
+ cancel();
+ fail(QString());
+ }
+ })).fail(crl::guard(this, [=](const MTP::Error &error) {
+ _addRequestId = 0;
+ const auto fail = std::move(_fail);
+ const auto type = error.type();
+ cancel();
+ if (fail) {
+ fail(type);
+ }
+ })).handleFloodErrors().send();
+}
+
+void StickerUpload::requestAddSticker(const MTPInputDocument &document) {
+ _addRequestId = _api.request(MTPstickers_AddStickerToSet(
+ Data::InputStickerSet(_set),
+ InputItem(document, _emoji))
+ ).done(crl::guard(this, [=](const MTPmessages_StickerSet &result) {
+ _addRequestId = 0;
+ FeedSetIfFull(_session, result);
+ const auto done = std::move(_done);
+ cancel();
+ if (done) {
+ done(result);
+ }
+ })).fail(crl::guard(this, [=](const MTP::Error &error) {
+ _addRequestId = 0;
+ const auto fail = std::move(_fail);
+ const auto type = error.type();
+ cancel();
+ if (fail) {
+ fail(type);
+ }
+ })).handleFloodErrors().send();
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_stickers_creator.h b/Telegram/SourceFiles/api/api_stickers_creator.h
new file mode 100644
index 0000000000..069766854d
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_stickers_creator.h
@@ -0,0 +1,121 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "base/weak_ptr.h"
+#include "data/stickers/data_stickers.h"
+#include "mtproto/sender.h"
+
+class DocumentData;
+
+namespace ChatHelpers {
+class Show;
+} // namespace ChatHelpers
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Ui {
+class PopupMenu;
+namespace Menu {
+struct MenuCallback;
+} // namespace Menu
+} // namespace Ui
+
+namespace Api {
+
+inline constexpr auto kStickersInOwnedSetMax = 120;
+inline constexpr auto kEmojiInOwnedSetMax = 200;
+inline constexpr auto kEmojiStickerSideMax = 100;
+
+void AddExistingStickerToSet(
+ not_null session,
+ const StickerSetIdentifier &set,
+ not_null document,
+ const QString &emoji,
+ Fn done,
+ Fn fail);
+
+void DeleteStickerSet(
+ not_null session,
+ const StickerSetIdentifier &set,
+ Fn done,
+ Fn fail);
+
+[[nodiscard]] bool HasOwnedStickerSets(not_null session);
+[[nodiscard]] bool HasOwnedEmojiSets(not_null session);
+
+[[nodiscard]] QString StickerEmojiOrDefault(
+ not_null document);
+
+void FillChooseStickerSetMenu(
+ not_null menu,
+ std::shared_ptr show,
+ not_null document);
+
+void FillChooseEmojiSetMenu(
+ not_null menu,
+ std::shared_ptr show,
+ not_null document);
+
+void AddAddToStickerSetAction(
+ const Ui::Menu::MenuCallback &addAction,
+ std::shared_ptr show,
+ not_null document);
+
+void AddAddToEmojiSetAction(
+ const Ui::Menu::MenuCallback &addAction,
+ std::shared_ptr show,
+ not_null document);
+
+void AddAddToOwnedSetAction(
+ const Ui::Menu::MenuCallback &addAction,
+ std::shared_ptr show,
+ not_null document);
+
+class StickerUpload final : public base::has_weak_ptr {
+public:
+ StickerUpload(
+ not_null session,
+ StickerSetIdentifier set,
+ QByteArray webpBytes,
+ QString emoji);
+ ~StickerUpload();
+
+ void start(
+ Fn done,
+ Fn fail,
+ Fn progress = nullptr);
+
+ void cancel();
+
+private:
+ void uploadReady(const MTPInputFile &file);
+ void uploadFailed();
+ void uploadProgressed();
+ void requestAddSticker(const MTPInputDocument &document);
+
+ const not_null _session;
+ StickerSetIdentifier _set;
+ QByteArray _bytes;
+ QString _emoji;
+ MTP::Sender _api;
+ rpl::lifetime _uploadLifetime;
+ FullMsgId _uploadId;
+ DocumentId _documentId = 0;
+ mtpRequestId _addRequestId = 0;
+
+ Fn _done;
+ Fn _fail;
+ Fn _progress;
+ int _lastReportedPercent = -1;
+
+};
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 56d0f48529..ba6f79f17d 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -59,6 +59,7 @@ boxPhotoTitlePosition: point(28px, 20px);
boxPhotoPadding: margins(28px, 28px, 28px, 18px);
boxPhotoCompressedSkip: 20px;
boxPhotoCaptionSkip: 8px;
+boxPhotoCaptionReplyOverlap: 5px;
defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }};
defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }};
@@ -92,7 +93,7 @@ restoreUserpicIcon: UserpicButton(defaultUserpicButton) {
}
confirmInviteTitle: FlatLabel(defaultFlatLabel) {
- align: align(center);
+ align: align(top);
minWidth: 320px;
maxHeight: 24px;
textFg: windowBoldFg;
@@ -101,9 +102,8 @@ confirmInviteTitle: FlatLabel(defaultFlatLabel) {
}
}
confirmInviteAbout: FlatLabel(boxLabel) {
- align: align(center);
+ align: align(top);
minWidth: 320px;
- maxHeight: 60px;
style: TextStyle(boxLabelStyle) {
lineHeight: 19px;
}
diff --git a/Telegram/SourceFiles/boxes/compose_ai_box.cpp b/Telegram/SourceFiles/boxes/compose_ai_box.cpp
index 8b20695ee0..0d51c9d322 100644
--- a/Telegram/SourceFiles/boxes/compose_ai_box.cpp
+++ b/Telegram/SourceFiles/boxes/compose_ai_box.cpp
@@ -1203,6 +1203,10 @@ void ComposeAiContent::request() {
return;
}
weak->_requestId = 0;
+ if (MTP::IgnoreError(error)) {
+ weak->resetState(CardState::Waiting);
+ return;
+ }
weak->showError(error.type());
});
}
diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp
index 1bdb77d824..fd0be2317a 100644
--- a/Telegram/SourceFiles/boxes/connection_box.cpp
+++ b/Telegram/SourceFiles/boxes/connection_box.cpp
@@ -18,6 +18,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_account.h"
#include "main/main_session.h"
#include "mtproto/facade.h"
+#include "mtproto/mtproto_config.h"
+#include "mtproto/proxy_check.h"
+#include "qr/qr_generate.h"
#include "settings/settings_common.h"
#include "storage/localstorage.h"
#include "ui/basic_click_handlers.h"
@@ -32,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
+#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/password_input.h"
@@ -46,10 +50,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "boxes/abstract_box.h" // Ui::show().
#include "window/window_session_controller.h"
-#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
+#include "styles/style_intro.h"
+#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
@@ -62,22 +67,55 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
using ProxyData = MTP::ProxyData;
-[[nodiscard]] std::vector ExtractUrlsSimple(const QString &input) {
+[[nodiscard]] int ClosestProxyRotationTimeoutSection(int value) {
+ auto result = 0;
+ auto bestDistance = 0;
+ for (auto i = 0; i != int(Core::SettingsProxy::kProxyRotationTimeouts.size()); ++i) {
+ const auto current = Core::SettingsProxy::kProxyRotationTimeouts[i];
+ const auto distance = (current > value) ? (current - value) : (value - current);
+ if ((i == 0) || (distance < bestDistance)) {
+ result = i;
+ bestDistance = distance;
+ }
+ }
+ return result;
+}
+
+[[nodiscard]] std::vector ExtractLinkCandidates(const QString &input) {
auto urls = std::vector();
- static auto urlRegex = QRegularExpression(R"((https?:\/\/[^\s]+))");
+ static const auto urlRegex = QRegularExpression(
+ R"((?:https?:\/\/[^\s]+|tg:\/\/[^\s]+|(?:www\.)?(?:t\.me|telegram\.me|telegram\.dog)\/[^\s]+))",
+ QRegularExpression::CaseInsensitiveOption);
auto it = urlRegex.globalMatch(input);
while (it.hasNext()) {
- urls.push_back(it.next().captured(1));
+ urls.push_back(it.next().captured(0));
}
return urls;
}
-[[nodiscard]] QString ProxyDataToString(const ProxyData &proxy) {
+[[nodiscard]] bool ProxyDataIsShareable(const ProxyData &proxy) {
using Type = ProxyData::Type;
- return u"tg://"_q
- + (proxy.type == Type::Socks5 ? "socks" : "proxy")
+ return (proxy.type == Type::Socks5)
+ || (proxy.type == Type::Mtproto);
+}
+
+[[nodiscard]] QString ProxyDataToQueryPath(const ProxyData &proxy) {
+ using Type = ProxyData::Type;
+ const auto path = [&] {
+ switch (proxy.type) {
+ case Type::Socks5: return u"socks"_q;
+ case Type::Mtproto: return u"proxy"_q;
+ case Type::None:
+ case Type::Http: return QString();
+ }
+ Unexpected("Proxy type in ProxyDataToQueryPath.");
+ }();
+ if (path.isEmpty()) {
+ return QString();
+ }
+ return path
+ "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
+ ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
? "&user=" + qthelp::url_encode(proxy.user) : "")
@@ -87,6 +125,148 @@ using ProxyData = MTP::ProxyData;
? "&secret=" + proxy.password : "");
}
+[[nodiscard]] QString ProxyDataToLocalLink(const ProxyData &proxy) {
+ const auto queryPath = ProxyDataToQueryPath(proxy);
+ return queryPath.isEmpty() ? QString() : (u"tg://"_q + queryPath);
+}
+
+[[nodiscard]] QString ProxyDataToPublicLink(
+ not_null account,
+ const ProxyData &proxy) {
+ const auto queryPath = ProxyDataToQueryPath(proxy);
+ if (queryPath.isEmpty()) {
+ return QString();
+ }
+ if (const auto session = account->maybeSession()) {
+ return session->createInternalLinkFull(queryPath);
+ }
+ auto domain = MTP::ConfigFields(
+ account->mtp().environment()
+ ).internalLinksDomain;
+ if (domain.endsWith('/') && queryPath.startsWith('/')) {
+ domain.chop(1);
+ } else if (!domain.endsWith('/') && !queryPath.startsWith('/')) {
+ domain += '/';
+ }
+ return domain + queryPath;
+}
+
+[[nodiscard]] QColor ProxyQrActiveColor() {
+ return QColor(0x40, 0xA7, 0xE3);
+}
+
+[[nodiscard]] QImage ProxyQr(const Qr::Data &data, int pixel, int max = 0) {
+ Expects(data.size > 0);
+
+ const auto available = max
+ ? std::max(max - 2 * st::introQrBackgroundSkip, 1)
+ : 0;
+ if (available > 0 && data.size * pixel > available) {
+ pixel = std::max(available / data.size, 1);
+ }
+ return Qr::Generate(
+ data,
+ pixel * style::DevicePixelRatio(),
+ Qt::black,
+ Qt::white);
+}
+
+[[nodiscard]] QImage ProxyQrLogo() {
+ const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize);
+ auto result = QImage(
+ size * style::DevicePixelRatio(),
+ QImage::Format_ARGB32_Premultiplied);
+ result.fill(Qt::transparent);
+ result.setDevicePixelRatio(style::DevicePixelRatio());
+ {
+ auto p = QPainter(&result);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setBrush(ProxyQrActiveColor());
+ p.setPen(Qt::NoPen);
+ p.drawEllipse(QRect(QPoint(), size));
+ st::introQrPlane.paintInCenter(p, QRect(QPoint(), size));
+ }
+ return result;
+}
+
+[[nodiscard]] QImage ProxyQrTile(const QString &link, int max = 0) {
+ const auto data = Qr::Encode(link, Qr::Redundancy::Quartile);
+ const auto qr = ProxyQr(
+ data,
+ st::introQrPixel,
+ max);
+ const auto qrSize = qr.width() / style::DevicePixelRatio();
+ const auto skip = st::introQrBackgroundSkip;
+ const auto size = QSize(qrSize + 2 * skip, qrSize + 2 * skip);
+ auto result = QImage(
+ size * style::DevicePixelRatio(),
+ QImage::Format_ARGB32_Premultiplied);
+ result.fill(Qt::transparent);
+ result.setDevicePixelRatio(style::DevicePixelRatio());
+ {
+ auto p = QPainter(&result);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::white);
+ p.drawRoundedRect(
+ QRect(QPoint(), size),
+ st::introQrBackgroundRadius,
+ st::introQrBackgroundRadius);
+ p.drawImage(QRect(skip, skip, qrSize, qrSize), qr);
+ const auto logo = ProxyQrLogo();
+ p.drawImage(
+ QRect(
+ (size.width() - st::introQrCenterSize) / 2,
+ (size.height() - st::introQrCenterSize) / 2,
+ st::introQrCenterSize,
+ st::introQrCenterSize),
+ logo);
+ }
+ return result;
+}
+
+[[nodiscard]] QImage ProxyQrForShare(const QString &link) {
+ return ProxyQrTile(
+ link,
+ st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right());
+}
+
+void ShowProxyQrBox(std::shared_ptr show, const QString &link) {
+ show->showBox(Box([=](not_null box) {
+ box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
+ box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
+
+ const auto copyCallback = [=, image = ProxyQrForShare(link)] {
+ QGuiApplication::clipboard()->setImage(image);
+ show->showToast(tr::lng_group_invite_qr_copied(tr::now));
+ };
+
+ const auto qr = ProxyQrTile(
+ link,
+ st::boxWidth
+ - st::boxRowPadding.left()
+ - st::boxRowPadding.right());
+ const auto size = qr.width() / style::DevicePixelRatio();
+ const auto height = st::inviteLinkQrSkip * 2 + size;
+ const auto container = box->addRow(
+ object_ptr(box, height),
+ st::inviteLinkQrMargin);
+ const auto button = Ui::CreateChild(container);
+ button->resize(size, size);
+ button->paintRequest(
+ ) | rpl::on_next([=] {
+ QPainter(button).drawImage(QRect(0, 0, size, size), qr);
+ }, button->lifetime());
+ container->widthValue(
+ ) | rpl::on_next([=](int width) {
+ button->move((width - size) / 2, st::inviteLinkQrSkip);
+ }, button->lifetime());
+ button->setClickedCallback(copyCallback);
+
+ box->addLeftButton(tr::lng_group_invite_context_copy(), copyCallback);
+ }));
+}
+
[[nodiscard]] ProxyData ProxyDataFromFields(
ProxyData::Type type,
const QMap &fields) {
@@ -110,7 +290,7 @@ void AddProxyFromClipboard(
const auto socksString = u"socks"_q;
const auto protocol = u"tg://"_q;
- const auto maybeUrls = ExtractUrlsSimple(
+ const auto maybeUrls = ExtractLinkCandidates(
QGuiApplication::clipboard()->text());
const auto isSingle = maybeUrls.size() == 1;
@@ -128,8 +308,8 @@ void AddProxyFromClipboard(
protocol.size(),
8192);
- if (local.startsWith(protocol + proxyString)
- || local.startsWith(protocol + socksString)) {
+ if (local.startsWith(protocol + proxyString, Qt::CaseInsensitive)
+ || local.startsWith(protocol + socksString, Qt::CaseInsensitive)) {
using namespace qthelp;
const auto options = RegExOption::CaseInsensitive;
@@ -188,7 +368,10 @@ void AddProxyFromClipboard(
auto success = Result::Failed;
for (const auto &maybeUrl : maybeUrls) {
- const auto result = proceedUrl(Core::TryConvertUrlToLocal(maybeUrl));
+ const auto trimmed = maybeUrl.trimmed();
+ const auto local = Core::TryConvertUrlToLocal(trimmed);
+ const auto check = local.isEmpty() ? trimmed : local;
+ const auto result = proceedUrl(check);
if (success != Result::Success) {
success = result;
}
@@ -376,12 +559,16 @@ private:
void setupButtons(int id, not_null button);
int rowHeight() const;
void refreshProxyForCalls();
+ void refreshProxyRotation();
not_null _controller;
Core::SettingsProxy &_settings;
QPointer _tryIPv6;
std::shared_ptr> _proxySettings;
QPointer> _proxyForCalls;
+ QPointer> _proxyRotation;
+ QPointer> _proxyRotationOptions;
+ QPointer _proxyRotationTimeout;
QPointer _about;
base::unique_qptr _noRows;
object_ptr _initialWrap;
@@ -426,6 +613,7 @@ private:
not_null parent,
const QString &text) const;
+ const bool _allowShare = false;
Fn _callback;
Fn _shareCallback;
@@ -900,6 +1088,47 @@ void ProxiesBox::setupContent() {
0,
st::proxyTryIPv6Padding.right(),
st::proxyTryIPv6Padding.top()));
+ _proxyRotation = inner->add(
+ object_ptr>(
+ inner,
+ object_ptr(
+ inner,
+ tr::lng_proxy_auto_switch(tr::now),
+ _settings.proxyRotationEnabled()),
+ style::margins(
+ 0,
+ st::proxyUsePadding.top(),
+ 0,
+ st::proxyUsePadding.bottom())),
+ style::margins(
+ st::proxyTryIPv6Padding.left(),
+ 0,
+ st::proxyTryIPv6Padding.right(),
+ st::proxyTryIPv6Padding.top()));
+ _proxyRotationOptions = inner->add(
+ object_ptr>(
+ inner,
+ object_ptr(inner)));
+ _proxyRotationTimeout = _proxyRotationOptions->entity()->add(
+ object_ptr(
+ _proxyRotationOptions->entity(),
+ st::settingsSlider),
+ st::settingsBigScalePadding);
+ for (const auto seconds : Core::SettingsProxy::kProxyRotationTimeouts) {
+ _proxyRotationTimeout->addSection(
+ tr::lng_proxy_auto_switch_timeout(
+ tr::now,
+ lt_count,
+ seconds));
+ }
+ _proxyRotationTimeout->setActiveSectionFast(
+ ClosestProxyRotationTimeoutSection(_settings.proxyRotationTimeout()));
+ _proxyRotationOptions->entity()->add(
+ object_ptr(
+ _proxyRotationOptions->entity(),
+ tr::lng_proxy_auto_switch_about(tr::now),
+ st::boxDividerLabel),
+ st::proxyAboutPadding);
_about = inner->add(
object_ptr(
@@ -922,6 +1151,7 @@ void ProxiesBox::setupContent() {
addNewProxy();
}
refreshProxyForCalls();
+ refreshProxyRotation();
});
_tryIPv6->checkedChanges(
) | rpl::on_next([=](bool checked) {
@@ -931,18 +1161,33 @@ void ProxiesBox::setupContent() {
_controller->proxySettingsValue(
) | rpl::on_next([=](ProxyData::Settings value) {
_proxySettings->setValue(value);
+ refreshProxyForCalls();
+ refreshProxyRotation();
}, inner->lifetime());
_proxyForCalls->entity()->checkedChanges(
) | rpl::on_next([=](bool checked) {
_controller->setProxyForCalls(checked);
}, _proxyForCalls->lifetime());
+ _proxyRotation->entity()->checkedChanges(
+ ) | rpl::on_next([=](bool checked) {
+ _controller->setProxyRotationEnabled(checked);
+ refreshProxyRotation();
+ }, _proxyRotation->lifetime());
+ _proxyRotationTimeout->sectionActivated(
+ ) | rpl::on_next([=](int section) {
+ _controller->setProxyRotationTimeout(
+ Core::SettingsProxy::kProxyRotationTimeouts[section]);
+ }, _proxyRotationTimeout->lifetime());
if (_rows.empty()) {
createNoRowsLabel();
}
refreshProxyForCalls();
+ refreshProxyRotation();
_proxyForCalls->finishAnimating();
+ _proxyRotation->finishAnimating();
+ _proxyRotationOptions->finishAnimating();
{
const auto wrap = inner->add(
@@ -987,6 +1232,20 @@ void ProxiesBox::refreshProxyForCalls() {
anim::type::normal);
}
+void ProxiesBox::refreshProxyRotation() {
+ if (!_proxyRotation || !_proxyRotationOptions) {
+ return;
+ }
+ const auto visible = (_proxySettings->current()
+ == ProxyData::Settings::Enabled)
+ && _settings.selected()
+ && (_settings.list().size() > 1);
+ _proxyRotation->toggle(visible, anim::type::normal);
+ _proxyRotationOptions->toggle(
+ visible && _proxyRotation->entity()->checked(),
+ anim::type::normal);
+}
+
int ProxiesBox::rowHeight() const {
return st::proxyRowPadding.top()
+ st::semiboldFont->height
@@ -1029,6 +1288,7 @@ void ProxiesBox::applyView(View &&view) {
} else {
i->second->updateFields(std::move(view));
}
+ refreshProxyRotation();
}
void ProxiesBox::createNoRowsLabel() {
@@ -1087,7 +1347,8 @@ ProxyBox::ProxyBox(
const ProxyData &data,
Fn callback,
Fn shareCallback)
-: _callback(std::move(callback))
+: _allowShare(data.type != Type::None)
+, _callback(std::move(callback))
, _shareCallback(std::move(shareCallback))
, _content(this) {
setupControls(data);
@@ -1156,7 +1417,8 @@ void ProxyBox::refreshButtons() {
addButton(tr::lng_cancel(), [=] { closeBox(); });
const auto type = _type->current();
- if (type == Type::Socks5 || type == Type::Mtproto) {
+ if (_allowShare
+ && (type == Type::Socks5 || type == Type::Mtproto)) {
addLeftButton(tr::lng_proxy_share(), [=] { share(); });
}
}
@@ -1203,10 +1465,10 @@ ProxyData ProxyBox::collectData() {
}
void ProxyBox::setupTypes() {
- const auto types = std::map{
- { Type::Http, "HTTP" },
- { Type::Socks5, "SOCKS5" },
- { Type::Mtproto, "MTPROTO" },
+ const auto types = std::vector>{
+ { Type::Mtproto, u"MTPROTO"_q },
+ { Type::Socks5, u"SOCKS5"_q },
+ { Type::Http, u"HTTP"_q },
};
for (const auto &[type, label] : types) {
_content->add(
@@ -1318,7 +1580,7 @@ void ProxyBox::setupMtprotoCredentials(const ProxyData &data) {
void ProxyBox::setupControls(const ProxyData &data) {
_type = std::make_shared>(
(data.type == Type::None
- ? Type::Socks5
+ ? Type::Mtproto
: data.type));
_content.create(this);
_content->resizeToWidth(st::boxWideWidth);
@@ -1359,95 +1621,7 @@ void ProxyBox::addLabel(
}
using Connection = MTP::details::AbstractConnection;
-using Checker = MTP::details::ConnectionPointer;
-
-void ResetProxyCheckers(Checker &v4, Checker &v6) {
- v4 = nullptr;
- v6 = nullptr;
-}
-
-void DropProxyChecker(Checker &v4, Checker &v6, not_null raw) {
- if (v4.get() == raw) {
- v4 = nullptr;
- } else if (v6.get() == raw) {
- v6 = nullptr;
- }
-}
-
-[[nodiscard]] bool HasProxyCheckers(const Checker &v4, const Checker &v6) {
- return v4 || v6;
-}
-
-void StartProxyCheck(
- not_null mtproto,
- const ProxyData &proxy,
- Checker &v4,
- Checker &v6,
- Fn done,
- Fn fail) {
- using Variants = MTP::DcOptions::Variants;
-
- ResetProxyCheckers(v4, v6);
- const auto connType = (proxy.type == ProxyData::Type::Http)
- ? Variants::Http
- : Variants::Tcp;
- const auto dcId = mtproto->mainDcId();
- const auto setup = [&](Checker &checker, const bytes::vector &secret) {
- checker = Connection::Create(
- mtproto,
- connType,
- QThread::currentThread(),
- secret,
- proxy);
- const auto raw = checker.get();
- raw->connect(raw, &Connection::connected, [=] {
- if (done) {
- done(raw, raw->pingTime());
- }
- });
- const auto failed = [=] {
- if (fail) {
- fail(raw);
- }
- };
- raw->connect(raw, &Connection::disconnected, failed);
- raw->connect(raw, &Connection::error, failed);
- };
- if (proxy.type == ProxyData::Type::Mtproto) {
- const auto secret = proxy.secretFromMtprotoPassword();
- setup(v4, secret);
- v4->connectToServer(
- proxy.host,
- proxy.port,
- secret,
- dcId,
- false);
- return;
- }
- const auto options = mtproto->dcOptions().lookup(
- dcId,
- MTP::DcType::Regular,
- true);
- const auto tryConnect = [&](Checker &checker, Variants::Address address) {
- const auto &list = options.data[address][connType];
- if (list.empty()
- || ((address == Variants::IPv6)
- && !Core::App().settings().proxy().tryIPv6())) {
- checker = nullptr;
- return;
- }
- const auto &endpoint = list.front();
- setup(checker, endpoint.secret);
- checker->connectToServer(
- QString::fromStdString(endpoint.ip),
- endpoint.port,
- endpoint.secret,
- dcId,
- false);
- };
- tryConnect(v4, Variants::IPv4);
- tryConnect(v6, Variants::IPv6);
-}
+using Checker = MTP::ProxyCheckConnection;
} // namespace
@@ -1609,18 +1783,19 @@ void ProxiesBoxController::ShowApplyConfirmation(
};
statusLabel->setTextColorOverride(st::proxyRowStatusFg->c);
relayout();
- StartProxyCheck(
+ MTP::StartProxyCheck(
&account->mtp(),
proxy,
+ Core::App().settings().proxy().tryIPv6(),
state->v4,
state->v6,
[=](Connection *raw, int ping) {
if (!weak || state->finished) {
return;
}
- DropProxyChecker(state->v4, state->v6, raw);
+ MTP::DropProxyChecker(state->v4, state->v6, raw);
state->finished = true;
- ResetProxyCheckers(state->v4, state->v6);
+ MTP::ResetProxyCheckers(state->v4, state->v6);
state->statusValue = TextWithEntities{
tr::lng_proxy_box_table_available(
tr::now,
@@ -1635,19 +1810,34 @@ void ProxiesBoxController::ShowApplyConfirmation(
if (!weak || state->finished) {
return;
}
- DropProxyChecker(state->v4, state->v6, raw);
- if (!HasProxyCheckers(state->v4, state->v6)) {
+ MTP::DropProxyChecker(state->v4, state->v6, raw);
+ if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
state->finished = true;
setUnavailable();
}
});
- if (!HasProxyCheckers(state->v4, state->v6)) {
+ if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
state->finished = true;
setUnavailable();
}
};
statusLabel->setClickHandlerFilter([=](const auto &...) {
- runCheck();
+ auto &proxy = Core::App().settings().proxy();
+ if (proxy.checkIpWarningShown()) {
+ runCheck();
+ } else {
+ box->uiShow()->showBox(Ui::MakeConfirmBox({
+ .text = tr::lng_proxy_check_ip_warning(),
+ .confirmed = [=] {
+ auto &proxy = Core::App().settings().proxy();
+ proxy.setCheckIpWarningShown(true);
+ Local::writeSettings();
+ runCheck();
+ },
+ .confirmText = tr::lng_proxy_check_ip_proceed(),
+ .title = tr::lng_proxy_check_ip_warning_title(),
+ }));
+ }
return false;
});
}
@@ -1666,9 +1856,9 @@ void ProxiesBoxController::ShowApplyConfirmation(
const auto enableButton = box->addButton(
tr::lng_proxy_box_table_button(),
[=] {
- auto &proxies = Core::App().settings().proxy().list();
- if (!ranges::contains(proxies, proxy)) {
- proxies.push_back(proxy);
+ auto &settings = Core::App().settings().proxy();
+ if (settings.indexInList(proxy) < 0) {
+ settings.addToList(proxy);
}
Core::App().setCurrentProxy(
proxy,
@@ -1704,9 +1894,10 @@ auto ProxiesBoxController::proxySettingsValue() const
void ProxiesBoxController::refreshChecker(Item &item) {
item.state = ItemState::Checking;
const auto id = item.id;
- StartProxyCheck(
+ MTP::StartProxyCheck(
&_account->mtp(),
item.data,
+ Core::App().settings().proxy().tryIPv6(),
item.checker,
item.checkerv6,
[=](Connection *raw, int pingTime) {
@@ -1717,8 +1908,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
if (item == end(_list)) {
return;
}
- DropProxyChecker(item->checker, item->checkerv6, raw);
- ResetProxyCheckers(item->checker, item->checkerv6);
+ MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
+ MTP::ResetProxyCheckers(item->checker, item->checkerv6);
if (item->state == ItemState::Checking) {
item->state = ItemState::Available;
item->ping = pingTime;
@@ -1733,14 +1924,14 @@ void ProxiesBoxController::refreshChecker(Item &item) {
if (item == end(_list)) {
return;
}
- DropProxyChecker(item->checker, item->checkerv6, raw);
- if (!HasProxyCheckers(item->checker, item->checkerv6)
+ MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
+ if (!MTP::HasProxyCheckers(item->checker, item->checkerv6)
&& item->state == ItemState::Checking) {
item->state = ItemState::Unavailable;
updateView(*item);
}
});
- if (!HasProxyCheckers(item.checker, item.checkerv6)) {
+ if (!MTP::HasProxyCheckers(item.checker, item.checkerv6)) {
item.state = ItemState::Unavailable;
}
}
@@ -1802,8 +1993,11 @@ void ProxiesBoxController::shareItem(int id, bool qr) {
void ProxiesBoxController::shareItems() {
auto result = QString();
for (const auto &item : _list) {
- if (!item.deleted) {
- result += ProxyDataToString(item.data) + '\n' + '\n';
+ if (!item.deleted && ProxyDataIsShareable(item.data)) {
+ const auto link = ProxyDataToPublicLink(_account, item.data);
+ if (!link.isEmpty()) {
+ result += (result.isEmpty() ? QString() : u"\n\n"_q) + link;
+ }
}
}
if (result.isEmpty()) {
@@ -1839,8 +2033,8 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
item->deleted = deleted;
if (deleted) {
- auto &proxies = _settings.list();
- proxies.erase(ranges::remove(proxies, item->data), end(proxies));
+ const auto removed = _settings.removeFromList(item->data);
+ Assert(removed);
if (item->data == _settings.selected()) {
_lastSelectedProxy = _settings.selected();
@@ -1856,16 +2050,19 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
}
}
} else {
- auto &proxies = _settings.list();
- if (ranges::find(proxies, item->data) == end(proxies)) {
+ if (_settings.indexInList(item->data) < 0) {
+ const auto &proxies = _settings.list();
auto insertBefore = item + 1;
while (insertBefore != end(_list) && insertBefore->deleted) {
++insertBefore;
}
- auto insertBeforeIt = (insertBefore == end(_list))
- ? end(proxies)
- : ranges::find(proxies, insertBefore->data);
- proxies.insert(insertBeforeIt, item->data);
+ const auto foundIndex = (insertBefore == end(_list))
+ ? int(proxies.size())
+ : _settings.indexInList(insertBefore->data);
+ const auto insertIndex = (foundIndex >= 0)
+ ? foundIndex
+ : int(proxies.size());
+ _settings.insertToList(insertIndex, item->data);
}
if (!_settings.selected() && _lastSelectedProxy == item->data) {
@@ -1904,8 +2101,8 @@ object_ptr ProxiesBoxController::editItemBox(int id) {
void ProxiesBoxController::replaceItemWith(
std::vector- ::iterator which,
std::vector
- ::iterator with) {
- auto &proxies = _settings.list();
- proxies.erase(ranges::remove(proxies, which->data), end(proxies));
+ const auto removed = _settings.removeFromList(which->data);
+ Assert(removed);
_views.fire({ which->id });
_list.erase(which);
@@ -1924,10 +2121,8 @@ void ProxiesBoxController::replaceItemValue(
restoreItem(which->id);
}
- auto &proxies = _settings.list();
- const auto i = ranges::find(proxies, which->data);
- Assert(i != end(proxies));
- *i = proxy;
+ const auto replaced = _settings.replaceInList(which->data, proxy);
+ Assert(replaced);
which->data = proxy;
refreshChecker(*which);
@@ -1963,8 +2158,7 @@ bool ProxiesBoxController::contains(const ProxyData &proxy) const {
}
void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
- auto &proxies = _settings.list();
- proxies.push_back(proxy);
+ _settings.addToList(proxy);
_list.push_back({ ++_idCounter, proxy });
refreshChecker(_list.back());
@@ -2001,6 +2195,22 @@ void ProxiesBoxController::setProxyForCalls(bool enabled) {
saveDelayed();
}
+void ProxiesBoxController::setProxyRotationEnabled(bool enabled) {
+ if (_settings.proxyRotationEnabled() == enabled) {
+ return;
+ }
+ _settings.setProxyRotationEnabled(enabled);
+ saveDelayed();
+}
+
+void ProxiesBoxController::setProxyRotationTimeout(int value) {
+ if (_settings.proxyRotationTimeout() == value) {
+ return;
+ }
+ _settings.setProxyRotationTimeout(value);
+ saveDelayed();
+}
+
void ProxiesBoxController::setTryIPv6(bool enabled) {
if (Core::App().settings().proxy().tryIPv6() == enabled) {
return;
@@ -2012,6 +2222,7 @@ void ProxiesBoxController::setTryIPv6(bool enabled) {
}
void ProxiesBoxController::saveDelayed() {
+ Core::App().proxyRotationSettingsChanged();
_saveTimer.callOnce(kSaveSettingsDelayedTimeout);
}
@@ -2022,7 +2233,7 @@ auto ProxiesBoxController::views() const -> rpl::producer {
rpl::producer ProxiesBoxController::listShareableChanges() const {
return _views.events_starting_with(ItemView()) | rpl::map([=] {
for (const auto &item : _list) {
- if (!item.deleted) {
+ if (!item.deleted && ProxyDataIsShareable(item.data)) {
return true;
}
}
@@ -2049,8 +2260,7 @@ void ProxiesBoxController::updateView(const Item &item) {
}
return ItemState::Connecting;
}();
- const auto supportsShare = (item.data.type == Type::Socks5)
- || (item.data.type == Type::Mtproto);
+ const auto supportsShare = ProxyDataIsShareable(item.data);
const auto supportsCalls = item.data.supportsCalls();
_views.fire({
item.id,
@@ -2067,18 +2277,29 @@ void ProxiesBoxController::updateView(const Item &item) {
}
void ProxiesBoxController::share(const ProxyData &proxy, bool qr) {
- if (proxy.type == Type::Http) {
+ if (!ProxyDataIsShareable(proxy)) {
+ return;
+ }
+ const auto qrLink = ProxyDataToLocalLink(proxy);
+ if (qrLink.isEmpty()) {
return;
}
- const auto link = ProxyDataToString(proxy);
if (qr) {
- _show->showBox(Box([=](not_null box) {
- Ui::FillPeerQrBox(box, nullptr, link, rpl::single(QString()));
- box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
- }));
+ if (_account->sessionExists()) {
+ _show->showBox(Box([=](not_null box) {
+ Ui::FillPeerQrBox(box, nullptr, qrLink, rpl::single(QString()));
+ box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
+ }));
+ } else {
+ ShowProxyQrBox(_show, qrLink);
+ }
return;
}
- QGuiApplication::clipboard()->setText(link);
+ const auto shareLink = ProxyDataToPublicLink(_account, proxy);
+ if (shareLink.isEmpty()) {
+ return;
+ }
+ QGuiApplication::clipboard()->setText(shareLink);
_show->showToast(tr::lng_username_copied(tr::now));
}
diff --git a/Telegram/SourceFiles/boxes/connection_box.h b/Telegram/SourceFiles/boxes/connection_box.h
index c33f8ea0f4..5de98bc663 100644
--- a/Telegram/SourceFiles/boxes/connection_box.h
+++ b/Telegram/SourceFiles/boxes/connection_box.h
@@ -85,6 +85,8 @@ public:
object_ptr addNewItemBox();
bool setProxySettings(ProxyData::Settings value);
void setProxyForCalls(bool enabled);
+ void setProxyRotationEnabled(bool enabled);
+ void setProxyRotationTimeout(int value);
void setTryIPv6(bool enabled);
rpl::producer proxySettingsValue() const;
diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp
index de621d2be1..bbd253a4d2 100644
--- a/Telegram/SourceFiles/boxes/create_poll_box.cpp
+++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp
@@ -1049,7 +1049,7 @@ void Options::removeDestroyed(not_null