diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000000..8c04e71228
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,42 @@
+name: Docker.
+
+on:
+ push:
+ paths:
+ - '.github/workflows/docker.yml'
+ - 'Telegram/build/docker/centos_env/**'
+
+jobs:
+ docker:
+ name: Ubuntu
+ runs-on: ubuntu-latest
+ if: github.ref_name == github.event.repository.default_branch
+
+ env:
+ IMAGE_TAG: ghcr.io/${{ github.repository }}/centos_env:latest
+
+ steps:
+ - name: Clone.
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: First set up.
+ run: |
+ sudo apt update
+ curl -sSL https://install.python-poetry.org | python3 -
+ echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
+
+ - name: Free up some disk space.
+ uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
+ with:
+ tool-cache: true
+
+ - name: Docker image build.
+ run: |
+ cd Telegram/build/docker/centos_env
+ poetry install
+ DEBUG= LTO= poetry run gen_dockerfile | DOCKER_BUILDKIT=1 docker build -t $IMAGE_TAG -
+
+ - name: Push the Docker image.
+ run: docker push $IMAGE_TAG
diff --git a/.gitmodules b/.gitmodules
index f6fa03612c..f9d202ecbe 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
-[submodule "Telegram/ThirdParty/libtgvoip"]
- path = Telegram/ThirdParty/libtgvoip
- url = https://github.com/telegramdesktop/libtgvoip
[submodule "Telegram/ThirdParty/GSL"]
path = Telegram/ThirdParty/GSL
url = https://github.com/Microsoft/GSL.git
@@ -76,9 +73,6 @@
[submodule "Telegram/lib_webview"]
path = Telegram/lib_webview
url = https://github.com/desktop-app/lib_webview.git
-[submodule "Telegram/ThirdParty/jemalloc"]
- path = Telegram/ThirdParty/jemalloc
- url = https://github.com/jemalloc/jemalloc
[submodule "Telegram/ThirdParty/dispatch"]
path = Telegram/ThirdParty/dispatch
url = https://github.com/apple/swift-corelibs-libdispatch
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 830c44c5f6..bc55b3ca93 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,19 +12,17 @@ include(cmake/validate_special_target.cmake)
include(cmake/version.cmake)
desktop_app_parse_version(Telegram/build/version)
-set(project_langs C CXX)
-if (APPLE)
- list(APPEND project_langs OBJC OBJCXX)
-elseif (LINUX)
- list(APPEND project_langs ASM)
-endif()
-
project(Telegram
- LANGUAGES ${project_langs}
+ LANGUAGES C CXX
VERSION ${desktop_app_version_cmake}
DESCRIPTION "AyuGram Desktop"
HOMEPAGE_URL "https://ayugram.one"
)
+
+if (APPLE)
+ enable_language(OBJC OBJCXX)
+endif()
+
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Telegram)
get_filename_component(third_party_loc "Telegram/ThirdParty" REALPATH)
@@ -39,9 +37,7 @@ include(cmake/variables.cmake)
include(cmake/nice_target_sources.cmake)
include(cmake/target_compile_options_if_exists.cmake)
include(cmake/target_link_frameworks.cmake)
-include(cmake/target_link_optional_libraries.cmake)
include(cmake/target_link_options_if_exists.cmake)
-include(cmake/target_link_static_libraries.cmake)
include(cmake/init_target.cmake)
include(cmake/generate_target.cmake)
include(cmake/nuget.cmake)
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 81b1ccba6b..fd47cfb6ba 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -53,9 +53,7 @@ set_target_properties(Telegram PROPERTIES AUTOMOC ON)
target_link_libraries(Telegram
PRIVATE
- # tdesktop::lib_tgcalls_legacy
tdesktop::lib_tgcalls
- # tdesktop::lib_tgvoip
# Order in this list defines the order of include paths in command line.
# We need to place desktop-app::external_minizip this early to have its
@@ -211,6 +209,8 @@ PRIVATE
api/api_confirm_phone.h
api/api_credits.cpp
api/api_credits.h
+ api/api_credits_history_entry.cpp
+ api/api_credits_history_entry.h
api/api_earn.cpp
api/api_earn.h
api/api_editing.cpp
@@ -260,8 +260,12 @@ PRIVATE
api/api_statistics_data_deserialize.h
api/api_statistics_sender.cpp
api/api_statistics_sender.h
+ api/api_suggest_post.cpp
+ api/api_suggest_post.h
api/api_text_entities.cpp
api/api_text_entities.h
+ api/api_todo_lists.cpp
+ api/api_todo_lists.h
api/api_toggling_media.cpp
api/api_toggling_media.h
api/api_transcribes.cpp
@@ -365,6 +369,8 @@ PRIVATE
boxes/edit_caption_box.h
boxes/edit_privacy_box.cpp
boxes/edit_privacy_box.h
+ boxes/edit_todo_list_box.cpp
+ boxes/edit_todo_list_box.h
boxes/gift_credits_box.cpp
boxes/gift_credits_box.h
boxes/gift_premium_box.cpp
@@ -548,6 +554,7 @@ PRIVATE
core/crash_report_window.h
core/crash_reports.cpp
core/crash_reports.h
+ core/credits_amount.h
core/deadlock_detector.h
core/file_utilities.cpp
core/file_utilities.h
@@ -561,7 +568,6 @@ PRIVATE
core/sandbox.h
core/shortcuts.cpp
core/shortcuts.h
- core/stars_amount.h
core/ui_integration.cpp
core/ui_integration.h
core/update_checker.cpp
@@ -589,6 +595,8 @@ PRIVATE
data/components/promo_suggestions.h
data/components/recent_peers.cpp
data/components/recent_peers.h
+ data/components/recent_shared_media_gifts.cpp
+ data/components/recent_shared_media_gifts.h
data/components/scheduled_messages.cpp
data/components/scheduled_messages.h
data/components/sponsored_messages.cpp
@@ -733,6 +741,8 @@ PRIVATE
data/data_streaming.h
data/data_thread.cpp
data/data_thread.h
+ data/data_todo_list.cpp
+ data/data_todo_list.h
data/data_types.cpp
data/data_types.h
data/data_unread_value.cpp
@@ -833,6 +843,8 @@ PRIVATE
history/view/controls/history_view_draft_options.h
history/view/controls/history_view_forward_panel.cpp
history/view/controls/history_view_forward_panel.h
+ history/view/controls/history_view_suggest_options.cpp
+ history/view/controls/history_view_suggest_options.h
history/view/controls/history_view_ttl_button.cpp
history/view/controls/history_view_ttl_button.h
history/view/controls/history_view_voice_record_bar.cpp
@@ -894,8 +906,12 @@ PRIVATE
history/view/media/history_view_sticker_player_abstract.h
history/view/media/history_view_story_mention.cpp
history/view/media/history_view_story_mention.h
+ history/view/media/history_view_suggest_decision.cpp
+ history/view/media/history_view_suggest_decision.h
history/view/media/history_view_theme_document.cpp
history/view/media/history_view_theme_document.h
+ history/view/media/history_view_todo_list.cpp
+ history/view/media/history_view_todo_list.h
history/view/media/history_view_unique_gift.cpp
history/view/media/history_view_unique_gift.h
history/view/media/history_view_userpic_suggestion.cpp
@@ -920,6 +936,8 @@ PRIVATE
history/view/history_view_bottom_info.h
history/view/history_view_chat_preview.cpp
history/view/history_view_chat_preview.h
+ history/view/history_view_chat_section.cpp
+ history/view/history_view_chat_section.h
history/view/history_view_contact_status.cpp
history/view/history_view_contact_status.h
history/view/history_view_context_menu.cpp
@@ -954,8 +972,6 @@ PRIVATE
history/view/history_view_pinned_tracker.h
history/view/history_view_quick_action.cpp
history/view/history_view_quick_action.h
- history/view/history_view_replies_section.cpp
- history/view/history_view_replies_section.h
history/view/history_view_reply.cpp
history/view/history_view_reply.h
history/view/history_view_requests_bar.cpp
@@ -972,8 +988,8 @@ PRIVATE
history/view/history_view_sponsored_click_handler.h
history/view/history_view_sticker_toast.cpp
history/view/history_view_sticker_toast.h
- history/view/history_view_sublist_section.cpp
- history/view/history_view_sublist_section.h
+ history/view/history_view_subsection_tabs.cpp
+ history/view/history_view_subsection_tabs.h
history/view/history_view_text_helper.cpp
history/view/history_view_text_helper.h
history/view/history_view_transcribe_button.cpp
@@ -1528,6 +1544,8 @@ PRIVATE
settings/cloud_password/settings_cloud_password_start.h
settings/cloud_password/settings_cloud_password_step.cpp
settings/cloud_password/settings_cloud_password_step.h
+ settings/cloud_password/settings_cloud_password_validate_icon.cpp
+ settings/cloud_password/settings_cloud_password_validate_icon.h
settings/settings_active_sessions.cpp
settings/settings_active_sessions.h
settings/settings_advanced.cpp
@@ -1948,11 +1966,7 @@ else()
set(bundle_identifier "one.ayugram.AyuGramDesktop")
endif()
set(bundle_entitlements "Telegram.entitlements")
- if (LINUX AND DESKTOP_APP_USE_PACKAGED)
- set(output_name "ayugram-desktop")
- else()
- set(output_name "AyuGram")
- endif()
+ set(output_name "AyuGram")
endif()
if (CMAKE_GENERATOR STREQUAL Xcode)
@@ -1994,8 +2008,9 @@ PRIVATE
G_LOG_DOMAIN="Telegram"
)
+get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if (APPLE
- OR "${CMAKE_GENERATOR}" STREQUAL "Ninja Multi-Config"
+ OR is_multi_config
OR NOT CMAKE_EXECUTABLE_SUFFIX STREQUAL ""
OR NOT "${output_name}" STREQUAL "AyuGram")
set(output_folder ${CMAKE_BINARY_DIR})
@@ -2012,8 +2027,67 @@ if (MSVC)
)
target_link_options(Telegram
PRIVATE
+ /DELAYLOAD:secur32.dll
+ /DELAYLOAD:winmm.dll
+ /DELAYLOAD:ws2_32.dll
+ /DELAYLOAD:user32.dll
+ /DELAYLOAD:gdi32.dll
/DELAYLOAD:advapi32.dll
+ /DELAYLOAD:avrt.dll
+ /DELAYLOAD:shell32.dll
+ /DELAYLOAD:ole32.dll
+ /DELAYLOAD:oleaut32.dll
+ /DELAYLOAD:shlwapi.dll
+ /DELAYLOAD:iphlpapi.dll
+ /DELAYLOAD:gdiplus.dll
+ /DELAYLOAD:version.dll
+ /DELAYLOAD:dwmapi.dll
+ /DELAYLOAD:uxtheme.dll
+ /DELAYLOAD:crypt32.dll
+ /DELAYLOAD:bcrypt.dll
+ /DELAYLOAD:netapi32.dll
+ /DELAYLOAD:imm32.dll
+ /DELAYLOAD:userenv.dll
+ /DELAYLOAD:wtsapi32.dll
+ /DELAYLOAD:propsys.dll
)
+ if (QT_VERSION GREATER 6)
+ if (NOT build_winarm)
+ target_link_options(Telegram PRIVATE
+ /DELAYLOAD:API-MS-Win-EventLog-Legacy-l1-1-0.dll
+ )
+ endif()
+
+ target_link_options(Telegram
+ PRIVATE
+ /DELAYLOAD:API-MS-Win-Core-Console-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Fibers-l2-1-1.dll
+ /DELAYLOAD:API-MS-Win-Core-File-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-LibraryLoader-l1-2-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Localization-l1-2-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Memory-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Memory-l1-1-1.dll
+ /DELAYLOAD:API-MS-Win-Core-ProcessThreads-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Synch-l1-2-0.dll # Synchronization.lib
+ /DELAYLOAD:API-MS-Win-Core-SysInfo-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-Timezone-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-WinRT-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-WinRT-Error-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Core-WinRT-String-l1-1-0.dll
+ /DELAYLOAD:API-MS-Win-Security-CryptoAPI-l1-1-0.dll
+ # /DELAYLOAD:API-MS-Win-Shcore-Scaling-l1-1-1.dll # We shadowed GetDpiForMonitor
+ /DELAYLOAD:authz.dll # Authz.lib
+ /DELAYLOAD:comdlg32.dll
+ /DELAYLOAD:dwrite.dll # DWrite.lib
+ /DELAYLOAD:dxgi.dll # DXGI.lib
+ /DELAYLOAD:d3d9.dll # D3D9.lib
+ /DELAYLOAD:d3d11.dll # D3D11.lib
+ /DELAYLOAD:d3d12.dll # D3D12.lib
+ /DELAYLOAD:setupapi.dll # SetupAPI.lib
+ /DELAYLOAD:winhttp.dll
+ )
+ endif()
endif()
target_prepare_qrc(Telegram)
@@ -2044,6 +2118,22 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
base/platform/win/base_windows_safe_library.h
)
target_include_directories(Updater PRIVATE ${lib_base_loc})
+ if (MSVC)
+ target_link_libraries(Updater
+ PRIVATE
+ delayimp
+ )
+ target_link_options(Updater
+ PRIVATE
+ /DELAYLOAD:user32.dll
+ /DELAYLOAD:advapi32.dll
+ /DELAYLOAD:shell32.dll
+ /DELAYLOAD:ole32.dll
+ /DELAYLOAD:shlwapi.dll
+ )
+ else()
+ target_link_options(Updater PRIVATE -municode)
+ endif()
elseif (APPLE)
add_custom_command(TARGET Updater
PRE_LINK
@@ -2072,6 +2162,10 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins
desktop-app::external_openssl
)
+ if (DESKTOP_APP_USE_PACKAGED)
+ target_compile_definitions(Packer PRIVATE PACKER_USE_PACKAGED)
+ endif()
+
set_target_properties(Packer PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
endif()
elseif (build_winstore)
diff --git a/Telegram/Resources/animations/cloud_password/validate.tgs b/Telegram/Resources/animations/cloud_password/validate.tgs
new file mode 100644
index 0000000000..fd5161d885
Binary files /dev/null and b/Telegram/Resources/animations/cloud_password/validate.tgs differ
diff --git a/Telegram/Resources/animations/diamond.tgs b/Telegram/Resources/animations/diamond.tgs
new file mode 100644
index 0000000000..63d1896d9a
Binary files /dev/null and b/Telegram/Resources/animations/diamond.tgs differ
diff --git a/Telegram/Resources/animations/edit_peers/direct_messages.tgs b/Telegram/Resources/animations/edit_peers/direct_messages.tgs
new file mode 100644
index 0000000000..3a0806e1ae
Binary files /dev/null and b/Telegram/Resources/animations/edit_peers/direct_messages.tgs differ
diff --git a/Telegram/Resources/animations/edit_peers/topics.tgs b/Telegram/Resources/animations/edit_peers/topics.tgs
new file mode 100644
index 0000000000..a5552a4acb
Binary files /dev/null and b/Telegram/Resources/animations/edit_peers/topics.tgs differ
diff --git a/Telegram/Resources/animations/edit_peers/topics_list.tgs b/Telegram/Resources/animations/edit_peers/topics_list.tgs
new file mode 100644
index 0000000000..d85bf7ff85
Binary files /dev/null and b/Telegram/Resources/animations/edit_peers/topics_list.tgs differ
diff --git a/Telegram/Resources/animations/edit_peers/topics_tabs.tgs b/Telegram/Resources/animations/edit_peers/topics_tabs.tgs
new file mode 100644
index 0000000000..3a240b4c62
Binary files /dev/null and b/Telegram/Resources/animations/edit_peers/topics_tabs.tgs differ
diff --git a/Telegram/Resources/animations/media_forbidden.tgs b/Telegram/Resources/animations/media_forbidden.tgs
index b1846cd5db..34e9808eea 100644
Binary files a/Telegram/Resources/animations/media_forbidden.tgs and b/Telegram/Resources/animations/media_forbidden.tgs differ
diff --git a/Telegram/Resources/icons/chat/input_paid.svg b/Telegram/Resources/icons/chat/input_paid.svg
new file mode 100644
index 0000000000..1179751c9a
--- /dev/null
+++ b/Telegram/Resources/icons/chat/input_paid.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/chat/large_messages.png b/Telegram/Resources/icons/chat/large_messages.png
new file mode 100644
index 0000000000..f2b2fda9d0
Binary files /dev/null and b/Telegram/Resources/icons/chat/large_messages.png differ
diff --git a/Telegram/Resources/icons/chat/large_messages@2x.png b/Telegram/Resources/icons/chat/large_messages@2x.png
new file mode 100644
index 0000000000..c4a0821ec6
Binary files /dev/null and b/Telegram/Resources/icons/chat/large_messages@2x.png differ
diff --git a/Telegram/Resources/icons/chat/large_messages@3x.png b/Telegram/Resources/icons/chat/large_messages@3x.png
new file mode 100644
index 0000000000..1d5b682e32
Binary files /dev/null and b/Telegram/Resources/icons/chat/large_messages@3x.png differ
diff --git a/Telegram/Resources/icons/chat/paid_approve.svg b/Telegram/Resources/icons/chat/paid_approve.svg
new file mode 100644
index 0000000000..c122dca22d
--- /dev/null
+++ b/Telegram/Resources/icons/chat/paid_approve.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/chat/paid_decline.svg b/Telegram/Resources/icons/chat/paid_decline.svg
new file mode 100644
index 0000000000..66f52af7ea
--- /dev/null
+++ b/Telegram/Resources/icons/chat/paid_decline.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/chat/paid_edit.svg b/Telegram/Resources/icons/chat/paid_edit.svg
new file mode 100644
index 0000000000..5b4ec4a489
--- /dev/null
+++ b/Telegram/Resources/icons/chat/paid_edit.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/settings/earn.png b/Telegram/Resources/icons/settings/earn.png
new file mode 100644
index 0000000000..c2e73499e1
Binary files /dev/null and b/Telegram/Resources/icons/settings/earn.png differ
diff --git a/Telegram/Resources/icons/settings/earn@2x.png b/Telegram/Resources/icons/settings/earn@2x.png
new file mode 100644
index 0000000000..53b7eec28c
Binary files /dev/null and b/Telegram/Resources/icons/settings/earn@2x.png differ
diff --git a/Telegram/Resources/icons/settings/earn@3x.png b/Telegram/Resources/icons/settings/earn@3x.png
new file mode 100644
index 0000000000..33c7144690
Binary files /dev/null and b/Telegram/Resources/icons/settings/earn@3x.png differ
diff --git a/Telegram/Resources/icons/settings/gift.png b/Telegram/Resources/icons/settings/gift.png
new file mode 100644
index 0000000000..8dacb6c7eb
Binary files /dev/null and b/Telegram/Resources/icons/settings/gift.png differ
diff --git a/Telegram/Resources/icons/settings/gift@2x.png b/Telegram/Resources/icons/settings/gift@2x.png
new file mode 100644
index 0000000000..996bd85d0b
Binary files /dev/null and b/Telegram/Resources/icons/settings/gift@2x.png differ
diff --git a/Telegram/Resources/icons/settings/gift@3x.png b/Telegram/Resources/icons/settings/gift@3x.png
new file mode 100644
index 0000000000..240680269e
Binary files /dev/null and b/Telegram/Resources/icons/settings/gift@3x.png differ
diff --git a/Telegram/Resources/icons/settings/premium/checklist.svg b/Telegram/Resources/icons/settings/premium/checklist.svg
new file mode 100644
index 0000000000..6cb5fac3f9
--- /dev/null
+++ b/Telegram/Resources/icons/settings/premium/checklist.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 9adfa372ef..eda9942456 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -164,11 +164,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_chat_status_members_online" = "{members_count}, {online_count}";
"lng_chat_status_subscribers#one" = "{count} subscriber";
"lng_chat_status_subscribers#other" = "{count} subscribers";
+"lng_chat_status_direct" = "Direct messages";
"lng_channel_status" = "channel";
"lng_group_status" = "group";
"lng_scam_badge" = "SCAM";
"lng_fake_badge" = "FAKE";
+"lng_direct_badge" = "DIRECT";
"lng_remember" = "Remember this choice";
@@ -840,6 +842,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_suggestion_phone_number_about" = "Keep your number up to date to ensure you can always log into Telegram. {link}";
"lng_settings_suggestion_phone_number_about_link" = "https://telegram.org/faq#q-i-have-a-new-phone-number-what-do-i-do";
"lng_settings_suggestion_phone_number_change" = "Please change your phone number in the official Telegram app on your phone as soon as possible. {emoji}";
+"lng_settings_suggestion_password_title" = "Your password";
+"lng_settings_suggestion_password_about" = "Your account is protected by 2-Step Veritifaction. Do you still remember your password?";
+"lng_settings_suggestion_password_yes" = "Yes, definitely";
+"lng_settings_suggestion_password_no" = "Not sure";
+"lng_settings_suggestion_password_step_input_title" = "Enter your password";
+"lng_settings_suggestion_password_step_input_about" = "Do you still remember your password?";
+"lng_settings_suggestion_password_step_finish_title" = "Perfect!";
+"lng_settings_suggestion_password_step_finish_about" = "You still remember your password.";
"lng_settings_power_menu" = "Battery and Animations";
"lng_settings_power_title" = "Power Usage";
@@ -1152,6 +1162,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_proxy_menu_delete" = "Delete";
"lng_proxy_menu_restore" = "Restore";
"lng_proxy_edit_share" = "Share";
+"lng_proxy_edit_share_qr_box_title" = "Share proxy with QR code";
+"lng_proxy_edit_share_list_button" = "Share Proxy List";
+"lng_proxy_edit_share_list_toast" = "Proxy List copied to clipboard.";
"lng_proxy_address_label" = "Socket address";
"lng_proxy_credentials_optional" = "Credentials (optional)";
"lng_proxy_credentials" = "Credentials";
@@ -1190,6 +1203,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_faq_link" = "https://telegram.org/faq#general-questions";
"lng_settings_features" = "Telegram Features";
"lng_settings_credits" = "My Stars";
+"lng_settings_currency" = "My TON";
"lng_settings_logout" = "Log out";
"lng_sure_logout" = "Are you sure you want to log out?";
@@ -1481,6 +1495,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_profile_hide_participants_about" = "Switch this on to hide the list of members in this group. Admins will remain visible.";
"lng_profile_view_channel" = "View Channel";
"lng_profile_view_discussion" = "View discussion";
+"lng_profile_direct_messages" = "Direct messages";
"lng_profile_join_channel" = "Join Channel";
"lng_profile_join_group" = "Join Group";
"lng_profile_apply_to_join_group" = "Apply to Join Group";
@@ -1878,6 +1893,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_manage_linked_channel_posted" = "All new posts from this channel are forwarded to the group.";
"lng_manage_discussion_group_warning" = "\"Chat history for new members\" will be switched to **Visible**.";
+"lng_manage_monoforum" = "Direct Messages";
+"lng_manage_monoforum_off" = "Off";
+"lng_manage_monoforum_free" = "Free";
+"lng_manage_monoforum_allow" = "Allow Channel Messages";
+"lng_manage_monoforum_price" = "Price for each message";
+"lng_manage_monoforum_about" = "Allow users to send messages to your channel, with the option to charge a fee for each message.";
+"lng_manage_monoforum_price_about" = "Your channel will receive {percent} of the selected fee ({amount}) for each incoming message.";
+
"lng_manage_history_visibility_title" = "Chat history for new members";
"lng_manage_history_visibility_shown" = "Visible";
"lng_manage_history_visibility_shown_about" = "New members will see messages that were sent before they joined.";
@@ -2061,6 +2084,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_changed_title" = "{from} changed group name to «{title}»";
"lng_action_changed_title_channel" = "Channel name was changed to «{title}»";
"lng_action_created_chat" = "{from} created the group «{title}»";
+"lng_action_created_monoforum" = "Direct messages were enabled in this channel.";
"lng_action_ttl_changed" = "{from} set messages to auto-delete in {duration}";
"lng_action_ttl_changed_you" = "You set messages to auto-delete in {duration}";
"lng_action_ttl_changed_channel" = "Messages in this channel will be automatically deleted after {duration}";
@@ -2168,6 +2192,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_gift_premium_months#other" = "{count} Months Premium";
"lng_action_gift_premium_about" = "Subscription for exclusive Telegram features.";
"lng_action_gift_refunded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.";
+"lng_action_gift_got_ton" = "Use TON to suggest posts to channels.";
"lng_action_suggested_photo_me" = "You suggested this photo for {user}'s Telegram profile.";
"lng_action_suggested_photo" = "{user} suggests this photo for your Telegram profile.";
"lng_action_suggested_photo_button" = "View Photo";
@@ -2235,6 +2260,28 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_message_price_free" = "Messages are now free in this group.";
"lng_action_message_price_paid#one" = "Messages now cost {count} Star each in this group.";
"lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group.";
+"lng_action_direct_messages_enabled" = "Channel enabled Direct Messages.";
+"lng_action_direct_messages_paid#one" = "Channel allows Direct Messages for {count} Star each.";
+"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each.";
+"lng_action_direct_messages_disabled" = "Channel disabled Direct Messages.";
+"lng_action_todo_marked_done" = "{from} marked {tasks} as done.";
+"lng_action_todo_marked_done_self" = "You marked {tasks} as done.";
+"lng_action_todo_marked_not_done" = "{from} marked {tasks} as not done.";
+"lng_action_todo_marked_not_done_self" = "You marked {tasks} as not done.";
+"lng_action_todo_added" = "{from} added {tasks} to the list.";
+"lng_action_todo_added_self" = "You added {tasks} to the list.";
+"lng_action_todo_tasks_fallback#one" = "task";
+"lng_action_todo_tasks_fallback#other" = "{count} tasks";
+"lng_action_todo_tasks_and_one" = "{tasks}, {task}";
+"lng_action_todo_tasks_and_last" = "{tasks} and {task}";
+"lng_action_suggest_success_stars#one" = "{from} has received {count} Star for publishing post.";
+"lng_action_suggest_success_stars#other" = "{from} has received {count} Stars for publishing post.";
+"lng_action_suggest_success_ton#one" = "{from} has received {count} TON for publishing post.";
+"lng_action_suggest_success_ton#other" = "{from} has received {count} TON for publishing post.";
+"lng_action_suggest_refund_user" = "User refunded the Stars so that post was deleted.";
+"lng_action_suggest_refund_admin" = "Admin deleted the post early so that the price was refunded to the user.";
+"lng_action_post_rejected" = "The post was rejected.";
+"lng_action_not_enough_funds" = "Transaction failed.";
"lng_you_paid_stars#one" = "You paid {count} Star.";
"lng_you_paid_stars#other" = "You paid {count} Stars.";
@@ -2602,6 +2649,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_effects" = "Add over 500 animated effects to private messages.";
"lng_premium_summary_subtitle_filter_tags" = "Tag Your Chats";
"lng_premium_summary_about_filter_tags" = "Display folder names for each chat in the chat list.";
+"lng_premium_summary_subtitle_todo_lists" = "Checklists";
+"lng_premium_summary_about_todo_lists" = "Plan, assign, and complete tasks - seamlessly and efficiently.";
"lng_premium_summary_bottom_subtitle" = "About Telegram Premium";
"lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
"lng_premium_summary_button" = "Subscribe for {cost} per month";
@@ -2732,6 +2781,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_summary_title" = "Telegram Stars";
"lng_credits_summary_about" = "Buy Stars to unlock content and services in miniapps on Telegram.";
+"lng_credits_currency_summary_title" = "TON Balance";
+"lng_credits_currency_summary_about" = "Offer TON to submit post suggestions to channels on Telegram.";
+"lng_credits_currency_summary_subtitle" = "You can withdraw your TON using Fragment.";
+"lng_credits_currency_summary_in_button" = "Top-up via Fragment";
+"lng_credits_currency_summary_in_subtitle" = "You can top-up your TON balance via Fragment.";
"lng_credits_summary_options_subtitle" = "Choose package";
"lng_credits_summary_options_credits#one" = "{count} Star";
"lng_credits_summary_options_credits#other" = "{count} Stars";
@@ -2755,8 +2809,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_premium_gift_duration" = "Duration";
"lng_credits_more_options" = "More Options";
"lng_credits_balance_me" = "your balance";
-"lng_credits_buy_button" = "Buy More Stars";
+"lng_credits_balance_me_count" = "Your balance: {emoji} {amount}";
+"lng_credits_buy_button" = "Top Up Balance";
+"lng_credits_topup_button" = "{emoji} Top Up Balance";
+"lng_credits_buy_button_short" = "Top Up";
+"lng_credits_stats_button_short" = "Stats";
+"lng_credits_stats_button" = "View Statistics";
"lng_credits_gift_button" = "Gift Stars to Friends";
+"lng_credits_earn_button" = "Earn Stars from Mini Apps";
"lng_credits_box_out_title" = "Confirm Your Purchase";
"lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?";
"lng_credits_box_out_sure#other" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Stars**?";
@@ -2810,6 +2870,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_box_history_entry_gift_examples" = "Examples";
"lng_credits_box_history_entry_ads" = "Ads Platform";
"lng_credits_box_history_entry_premium_bot" = "Stars Top-Up";
+"lng_credits_box_history_entry_currency_in" = "TON Top-Up";
"lng_credits_box_history_entry_api" = "Paid Broadcast";
"lng_credits_box_history_entry_floodskip_about#one" = "{count} Message";
"lng_credits_box_history_entry_floodskip_about#other" = "{count} Messages";
@@ -2873,6 +2934,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
"lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}.";
"lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages.";
+"lng_credits_small_balance_for_suggest" = "Buy **Stars** to suggest post to {channel}.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}";
@@ -3196,6 +3258,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_feature_transcribe" = "Voice-to-Text Conversion";
"lng_feature_autotranslate" = "Autotranslation of Messages";
+"lng_edit_topics_enable" = "Enable Topics";
+"lng_edit_topics_about" = "The group chat will be divided into topics created by admins or users.";
+"lng_edit_topics_layout" = "Topics layout";
+"lng_edit_topics_layout_about" = "Choose how topics appear for all members.";
+"lng_edit_topics_tabs" = "Tabs";
+"lng_edit_topics_list" = "List";
+
"lng_giveaway_new_title" = "Boosts via Gifts";
"lng_giveaway_new_about" = "Get more boosts for your channel by gifting Premium to your subscribers.";
"lng_giveaway_new_about_group" = "Get more boosts for your group by gifting Premium to your members.";
@@ -3432,6 +3501,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_stars_incoming" = "Use Stars to unlock content and services on Telegram.";
"lng_gift_until" = "Until";
+"lng_gift_ton_amount#one" = "{count} TON";
+"lng_gift_ton_amount#other" = "{count} TON";
+
"lng_gift_premium_or_stars" = "Gift Premium or Stars";
"lng_gift_premium_subtitle" = "Gift Premium";
"lng_gift_premium_about" = "Give {name} access to exclusive features with Telegram Premium. {features}";
@@ -3758,6 +3830,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_in_dlg_sticker_emoji" = "{emoji} Sticker";
"lng_in_dlg_poll" = "Poll";
"lng_in_dlg_story" = "Story";
+"lng_in_dlg_todo_list" = "Checklist";
"lng_in_dlg_story_expired" = "Expired story";
"lng_in_dlg_media_count#one" = "{count} media";
"lng_in_dlg_media_count#other" = "{count} media";
@@ -3847,7 +3920,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_anonymous_ph" = "Send anonymously...";
"lng_story_reply_ph" = "Reply privately...";
"lng_story_comment_ph" = "Comment story...";
-"lng_message_paid_ph" = "Message for {amount}";
+"lng_message_stars_ph#one" = "Message for {count} Star";
+"lng_message_stars_ph#other" = "Message for {count} Stars";
"lng_send_text_no" = "Text not allowed.";
"lng_send_text_no_about" = "The admins of this group only allow sending {types}.";
"lng_send_text_type_and_last" = "{types} and {last}";
@@ -4186,6 +4260,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_edit_msg" = "Edit";
"lng_context_add_factcheck" = "Add Fact Check";
"lng_context_edit_factcheck" = "Edit Fact Check";
+"lng_context_add_offer" = "Add Offer";
"lng_context_forward_msg" = "Forward";
"lng_context_send_now_msg" = "Send Now";
"lng_context_reschedule" = "Reschedule";
@@ -4197,6 +4272,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_pin_msg" = "Pin";
"lng_context_unpin_msg" = "Unpin";
"lng_context_cancel_upload" = "Cancel Upload";
+"lng_context_upload_edit_caption" = "Edit Caption";
"lng_context_copy_selected" = "Copy Selected Text";
"lng_context_copy_selected_items" = "Copy Selected as Text";
"lng_context_forward_selected" = "Forward Selected";
@@ -4220,6 +4296,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_seen_reacted#other" = "{count} Reacted";
"lng_context_seen_reacted_none" = "Nobody Reacted";
"lng_context_seen_reacted_all" = "Show All Reactions";
+"lng_context_sent_by" = "Sent by {user}";
"lng_context_set_as_quick" = "Set As Quick";
"lng_context_filter_by_tag" = "Filter by Tag";
"lng_context_tag_add_name" = "Add Name";
@@ -4367,6 +4444,113 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_preview_reply_to" = "Reply to {name}";
"lng_preview_reply_to_quote" = "Reply to quote from {name}";
+"lng_suggest_bar_title" = "Suggest a Post Below";
+"lng_suggest_bar_text" = "Click to offer a price for publishing.";
+"lng_suggest_bar_priced" = "{amount} for publishing anytime.";
+"lng_suggest_bar_dated" = "Publish on {date}";
+"lng_suggest_options_title" = "Suggest a Message";
+"lng_suggest_options_change" = "Suggest Changes";
+"lng_suggest_options_stars_offer" = "Offer Stars";
+"lng_suggest_options_stars_request" = "Request Stars";
+"lng_suggest_options_stars_price" = "Enter Price in Stars";
+"lng_suggest_options_stars_price_about" = "Choose how many Stars to pay to publish this message.";
+"lng_suggest_options_ton_offer" = "Offer TON";
+"lng_suggest_options_ton_request" = "Request TON";
+"lng_suggest_options_ton_price" = "Enter Price in TON";
+"lng_suggest_options_ton_price_about" = "Choose how many TON to pay to publish this message.";
+"lng_suggest_options_date" = "Time";
+"lng_suggest_options_date_any" = "Anytime";
+"lng_suggest_options_date_publish" = "Publish";
+"lng_suggest_options_date_now" = "Publish Now";
+"lng_suggest_options_date_about" = "Select the date and time you want the message to be published. The post will remain available for at least 24 hours from this date.";
+"lng_suggest_options_you_get_stars#one" = "You will receive {count} Star ({percent}) for publishing this post.";
+"lng_suggest_options_you_get_stars#other" = "You will receive {count} Stars ({percent}) for publishing this post.";
+"lng_suggest_options_you_get_ton#one" = "You will receive {count} TON ({percent}) for publishing this post.";
+"lng_suggest_options_you_get_ton#other" = "You will receive {count} TON ({percent}) for publishing this post.";
+"lng_suggest_options_stars_warning" = "Transactions in **Stars** may be reversed by the payment provider within **21** days. Only accept Stars from people you trust.";
+"lng_suggest_options_offer" = "Offer {amount}";
+"lng_suggest_options_offer_free" = "Offer for Free";
+"lng_suggest_options_update" = "Update Terms";
+"lng_suggest_options_update_date" = "Update Time";
+
+"lng_suggest_action_decline" = "Decline";
+"lng_suggest_action_accept" = "Accept";
+"lng_suggest_action_change" = "Suggest Changes";
+"lng_suggest_action_your" = "You suggest to post this message.";
+"lng_suggest_action_his" = "{from} suggests to post this message.";
+"lng_suggest_action_price_label" = "Price";
+"lng_suggest_action_price_free" = "Free";
+"lng_suggest_action_time_label" = "Time";
+"lng_suggest_action_time_any" = "Anytime";
+"lng_suggest_action_agreement" = "Agreement reached!";
+"lng_suggest_action_agree_date" = "The post will be automatically published on {channel} {date}.";
+"lng_suggest_action_your_charged_stars#one" = "You have been charged **{count} Star**.";
+"lng_suggest_action_your_charged_stars#other" = "You have been charged **{count} Stars**.";
+"lng_suggest_action_your_charged_ton#one" = "You have been charged **{count} TON**.";
+"lng_suggest_action_your_charged_ton#other" = "You have been charged **{count} TON**.";
+"lng_suggest_action_his_charged_stars#one" = "{from} has been charged **{count} Star**.";
+"lng_suggest_action_his_charged_stars#other" = "{from} has been charged **{count} Stars**.";
+"lng_suggest_action_his_charged_ton#one" = "{from} has been charged **{count} TON**.";
+"lng_suggest_action_his_charged_ton#other" = "{from} has been charged **{count} TON**.";
+"lng_suggest_action_agree_receive_stars" = "{channel} will receive the Stars once the post has been live for 24 hours.";
+"lng_suggest_action_agree_receive_ton" = "{channel} will receive TON once the post has been live for 24 hours.";
+"lng_suggest_action_agree_removed_stars" = "If {channel} removes the post before it has been live for 24 hours, the Stars will be refunded.";
+"lng_suggest_action_agree_removed_ton" = "If {channel} removes the post before it has been live for 24 hours, TON will be refunded.";
+"lng_suggest_action_your_not_enough_stars" = "**Transaction failed** because you didn't have enough Stars.";
+"lng_suggest_action_your_not_enough_ton" = "**Transaction failed** because you didn't have enough TON.";
+"lng_suggest_action_his_not_enough_stars" = "**Transaction failed** because the user didn't have enough Stars.";
+"lng_suggest_action_his_not_enough_ton" = "**Transaction failed** because the user didn't have enough TON.";
+"lng_suggest_action_declined" = "{from} rejected the message.";
+"lng_suggest_action_declined_reason" = "{from} rejected the message with the comment.";
+"lng_suggest_change_price" = "{from} suggests a new price for the message.";
+"lng_suggest_change_time" = "{from} suggests a new time for the message.";
+"lng_suggest_change_price_time" = "{from} suggests a new price and time for the message.";
+"lng_suggest_change_content" = "{from} suggests changes for the message.";
+"lng_suggest_change_price_label" = "New Price";
+"lng_suggest_change_time_label" = "New Time";
+"lng_suggest_change_text_label" = "Check the suggested message below";
+"lng_suggest_menu_edit_message" = "Edit Message";
+"lng_suggest_menu_edit_price" = "Edit Price";
+"lng_suggest_menu_edit_time" = "Edit Time";
+"lng_suggest_decline_title" = "Decline";
+"lng_suggest_decline_text" = "Do you want to decline publishing this post from {from}?";
+"lng_suggest_decline_text_to" = "Do you want to decline publishing this post to {channel}?";
+"lng_suggest_decline_reason" = "Add a reason (optional)";
+"lng_suggest_accept_title" = "Accept Terms";
+"lng_suggest_accept_text" = "Do you want to publish this post from {from}?";
+"lng_suggest_accept_text_to" = "Do you want to publish this post to {channel}?";
+"lng_suggest_accept_receive_stars#one" = "{channel} will receive **{count} Star** ({percent}) for publishing {date}.";
+"lng_suggest_accept_receive_stars#other" = "{channel} will receive **{count} Stars** ({percent}) for publishing {date}.";
+"lng_suggest_accept_receive_ton#one" = "{channel} will receive **{count} TON** ({percent}) for publishing {date}.";
+"lng_suggest_accept_receive_ton#other" = "{channel} will receive **{count} TON** ({percent}) for publishing {date}.";
+"lng_suggest_accept_receive_now_stars#one" = "{channel} will receive **{count} Star** ({percent}) for publishing right now.";
+"lng_suggest_accept_receive_now_stars#other" = "{channel} will receive **{count} Stars** ({percent}) for publishing right now.";
+"lng_suggest_accept_receive_now_ton#one" = "{channel} will receive **{count} TON** ({percent}) for publishing right now.";
+"lng_suggest_accept_receive_now_ton#other" = "{channel} will receive **{count} TON** ({percent}) for publishing right now.";
+"lng_suggest_accept_receive_if" = "It must remain visible for at least **24** hours after publication.";
+"lng_suggest_accept_pay_stars#one" = "You will pay **{count} Star** for publishing {date}.";
+"lng_suggest_accept_pay_stars#other" = "You will pay **{count} Stars** for publishing {date}.";
+"lng_suggest_accept_pay_ton#one" = "You will pay **{count} TON** for publishing {date}.";
+"lng_suggest_accept_pay_ton#other" = "You will pay **{count} TON** for publishing {date}.";
+"lng_suggest_accept_pay_now_stars#one" = "You will pay **{count} Star** for publishing right now.";
+"lng_suggest_accept_pay_now_stars#other" = "You will pay **{count} Stars** for publishing right now.";
+"lng_suggest_accept_pay_now_ton#one" = "You will pay **{count} TON** for publishing right now.";
+"lng_suggest_accept_pay_now_ton#other" = "You will pay **{count} TON** for publishing right now.";
+"lng_suggest_accept_send" = "Publish";
+"lng_suggest_stars_amount#one" = "{count} Star";
+"lng_suggest_stars_amount#other" = "{count} Stars";
+"lng_suggest_ton_amount#one" = "{count} TON";
+"lng_suggest_ton_amount#other" = "{count} TON";
+"lng_suggest_warn_title_stars" = "Stars will be lost";
+"lng_suggest_warn_title_ton" = "TON will be lost";
+"lng_suggest_warn_text_stars" = "You won't receive **Stars** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published.";
+"lng_suggest_warn_text_ton" = "You won't receive **TON** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published.";
+"lng_suggest_warn_delete_anyway" = "Delete Anyway";
+"lng_suggest_low_ton_title" = "{amount} TON Needed";
+"lng_suggest_low_ton_text" = "You can add funds to your balance via the third-party platform Fragment.";
+"lng_suggest_low_ton_fragment" = "Add Funds via Fragment";
+"lng_suggest_low_ton_fragment_url" = "https://fragment.com/ads/topup";
+
"lng_reply_in_another_title" = "Reply in...";
"lng_reply_in_another_chat" = "Reply in Another Chat";
"lng_reply_in_author" = "Message author";
@@ -5159,6 +5343,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_channel_edit_stories" = "Edit stories of others";
"lng_rights_channel_delete_stories" = "Delete stories of others";
"lng_rights_channel_manage_calls" = "Manage live streams";
+"lng_rights_channel_manage_direct" = "Manage direct messages";
"lng_rights_group_info" = "Change group info";
"lng_rights_group_ban" = "Ban users";
"lng_rights_group_invite_link" = "Invite users via link";
@@ -5251,6 +5436,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_restricted_send_polls_all" = "Sorry, sending polls is not allowed in this group.";
"lng_restricted_send_public_polls" = "Sorry, polls with visible votes can't be forwarded to channels.";
+"lng_restricted_send_todo_lists" = "Sorry, Checklists can't be forwarded to channels.";
"lng_restricted_send_paid_media" = "Sorry, paid media can't be sent to this channel.";
"lng_restricted_send_voice_messages" = "{user} doesn't accept voice messages.";
@@ -5265,6 +5451,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers.";
"lng_send_non_premium_message_toast_link" = "Telegram Premium";
+"lng_send_charges_stars_channel" = "{channel} charges {amount} per message to its admin.";
+"lng_send_free_channel" = "Send a direct message to the administrator of {channel}.";
"lng_send_charges_stars_text" = "{user} charges {amount} for each message.";
"lng_send_charges_stars_go" = "Buy Stars";
@@ -5480,6 +5668,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_admin_log_admin_create_topics" = "Create topics";
"lng_admin_log_admin_manage_calls" = "Manage video chats";
"lng_admin_log_admin_manage_calls_channel" = "Manage live streams";
+"lng_admin_log_admin_manage_direct" = "Manage direct messages";
"lng_admin_log_admin_add_admins" = "Add new admins";
"lng_admin_log_subscription_extend" = "{name} renewed subscription until {date}";
@@ -5779,6 +5968,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_stop" = "Stop poll";
"lng_polls_stop_warning" = "If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone.";
"lng_polls_stop_sure" = "Stop";
+"lng_polls_menu_item" = "Poll";
"lng_polls_create" = "Create poll";
"lng_polls_create_title" = "New poll";
"lng_polls_create_question" = "Question";
@@ -5807,6 +5997,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_polls_show_more#other" = "Show more ({count})";
"lng_polls_votes_collapse" = "Collapse";
+"lng_todo_title" = "Checklist";
+"lng_todo_title_group" = "Group Checklist";
+"lng_todo_completed#one" = "{count} of {total} completed";
+"lng_todo_completed#other" = "{count} of {total} completed";
+"lng_todo_completed_none" = "None of {total} completed";
+"lng_todo_menu_item" = "Checklist";
+"lng_todo_create" = "Create Checklist";
+"lng_todo_create_title" = "New Checklist";
+"lng_todo_create_title_placeholder" = "Title";
+"lng_todo_create_list" = "Tasks List";
+"lng_todo_create_list_add" = "Add a task...";
+"lng_todo_create_limit#one" = "You can add {count} more task.";
+"lng_todo_create_limit#other" = "You can add {count} more tasks.";
+"lng_todo_create_maximum" = "You have added the maximum number of tasks.";
+"lng_todo_create_settings" = "Settings";
+"lng_todo_create_allow_add" = "Allow Others to Add Tasks";
+"lng_todo_create_allow_mark" = "Allow Others to Mark As Done";
+"lng_todo_create_button" = "Create";
+"lng_todo_choose_title" = "Please enter a title.";
+"lng_todo_choose_tasks" = "Please enter at least one task.";
+
+"lng_todo_add_title" = "Add Tasks";
+"lng_todo_create_premium" = "Only subscribers of {link} can create Checklists.";
+"lng_todo_add_premium" = "Only subscribers of {link} can add tasks.";
+"lng_todo_mark_premium" = "Only subscribers of {link} can mark tasks as done.";
+"lng_todo_premium_link" = "Telegram Premium";
+"lng_todo_mark_restricted" = "{user} has restricted others from marking tasks as done.";
+"lng_todo_mark_forwarded" = "You can't change forwarded checklists.";
+
"lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM.";
"lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM.";
"lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}.";
@@ -6071,6 +6290,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_forum_messages#other" = "{count} messages";
"lng_forum_show_topics_list" = "Show Topics List";
+"lng_monoforum_choose_to_reply" = "Choose a message to reply.";
+
"lng_request_peer_requirements" = "Requirements";
"lng_request_peer_rights" = "You must have these admin rights: {rights}.";
"lng_request_peer_rights_and" = "{rights} and {last}";
@@ -6117,6 +6338,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_request_channel_delete_messages" = "delete messages";
"lng_request_channel_add_subscribers" = "add subscribers";
"lng_request_channel_manage_livestreams" = "manage live streams";
+"lng_request_channel_manage_direct" = "manage direct messages";
"lng_request_channel_add_admins" = "add new admins";
"lng_request_channel_create" = "Create a New Channel for This";
@@ -6394,7 +6616,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_bot_earn_balance_button_locked" = "Withdraw";
"lng_bot_earn_balance_button_buy_ads" = "Buy Ads";
"lng_bot_earn_learn_credits_out_about" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. {link}";
+"lng_self_earn_learn_credits_out_about" = "You can withdraw from 10 Stars using Fragment. {link}";
"lng_bot_earn_out_ph" = "Enter amount to withdraw";
+"lng_bot_earn_out_ph_max" = "Enter amount to withdraw (max. {amount})";
"lng_bot_earn_balance_password_title" = "Two-step verification";
"lng_bot_earn_balance_password_description" = "Please enter your password to collect.";
"lng_bot_earn_credits_out_minimal" = "You cannot withdraw less than {link}.";
@@ -6576,6 +6800,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_mac_menu_new_channel" = "New Channel";
"lng_mac_menu_show" = "Show Telegram";
"lng_mac_menu_emoji_and_symbols" = "Emoji & Symbols";
+"lng_mac_menu_fullscreen" = "Toggle Full Screen";
"lng_mac_menu_player_pause" = "Pause";
"lng_mac_menu_player_resume" = "Resume";
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index 50cf186e02..9beaa522d3 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -9,6 +9,7 @@
../../animations/cloud_password/password_input.tgs
../../animations/cloud_password/hint.tgs
../../animations/cloud_password/email.tgs
+ ../../animations/cloud_password/validate.tgs
../../animations/ttl.tgs
../../animations/discussion.tgs
../../animations/stats.tgs
@@ -25,6 +26,7 @@
../../animations/hours.tgs
../../animations/phone.tgs
../../animations/chat_link.tgs
+ ../../animations/diamond.tgs
../../animations/collectible_username.tgs
../../animations/collectible_phone.tgs
../../animations/search.tgs
@@ -32,6 +34,10 @@
../../animations/hello_status.tgs
../../animations/starref_link.tgs
../../animations/media_forbidden.tgs
+ ../../animations/edit_peers/topics.tgs
+ ../../animations/edit_peers/topics_tabs.tgs
+ ../../animations/edit_peers/topics_list.tgs
+ ../../animations/edit_peers/direct_messages.tgs
../../animations/dice/dice_idle.tgs
../../animations/dice/dart_idle.tgs
diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml
index 0510faaad4..135f939788 100644
--- a/Telegram/Resources/uwp/AppX/AppxManifest.xml
+++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml
@@ -10,7 +10,7 @@
+ Version="5.16.2.0" />
Telegram Desktop
Telegram Messenger LLP
diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc
index 9f31e5cc98..f573d05ff7 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 5,14,3,0
- PRODUCTVERSION 5,14,3,0
+ FILEVERSION 5,16,2,0
+ PRODUCTVERSION 5,16,2,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop"
- VALUE "FileVersion", "5.14.3.0"
+ VALUE "FileVersion", "5.16.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "5.14.3.0"
+ VALUE "ProductVersion", "5.16.2.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc
index 0812bee7bf..e59ae58110 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 5,14,3,0
- PRODUCTVERSION 5,14,3,0
+ FILEVERSION 5,16,2,0
+ PRODUCTVERSION 5,16,2,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater"
- VALUE "FileVersion", "5.14.3.0"
+ VALUE "FileVersion", "5.16.2.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2025"
VALUE "ProductName", "AyuGram Desktop"
- VALUE "ProductVersion", "5.14.3.0"
+ VALUE "ProductVersion", "5.16.2.0"
END
END
BLOCK "VarFileInfo"
diff --git a/Telegram/SourceFiles/_other/packer.cpp b/Telegram/SourceFiles/_other/packer.cpp
index 1399aa490b..da09b252b5 100644
--- a/Telegram/SourceFiles/_other/packer.cpp
+++ b/Telegram/SourceFiles/_other/packer.cpp
@@ -282,7 +282,7 @@ int main(int argc, char *argv[])
cout << "Compression start, size: " << resultSize << "\n";
QByteArray compressed, resultCheck;
-#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win
+#if defined Q_OS_WIN && !defined PACKER_USE_PACKAGED // use Lzma SDK for win
const int32 hSigLen = 128, hShaLen = 20, hPropsLen = LZMA_PROPS_SIZE, hOriginalSizeLen = sizeof(int32), hSize = hSigLen + hShaLen + hPropsLen + hOriginalSizeLen; // header
compressed.resize(hSize + resultSize + 1024 * 1024); // rsa signature + sha1 + lzma props + max compressed size
diff --git a/Telegram/SourceFiles/_other/packer.h b/Telegram/SourceFiles/_other/packer.h
index 4e5fbfc7ac..2c200eefd7 100644
--- a/Telegram/SourceFiles/_other/packer.h
+++ b/Telegram/SourceFiles/_other/packer.h
@@ -27,7 +27,7 @@ extern "C" {
#include
} // extern "C"
-#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win
+#if defined Q_OS_WIN && !defined PACKER_USE_PACKAGED // use Lzma SDK for win
#include
#else
#include
diff --git a/Telegram/SourceFiles/_other/updater_linux.cpp b/Telegram/SourceFiles/_other/updater_linux.cpp
index 58bc10efad..67b74453e1 100644
--- a/Telegram/SourceFiles/_other/updater_linux.cpp
+++ b/Telegram/SourceFiles/_other/updater_linux.cpp
@@ -5,6 +5,7 @@ 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
*/
+#define _GLIBCXX_USE_CXX11_ABI 0
#include
#include
#include
diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp
index 5342cbd2e0..b86ce1a70c 100644
--- a/Telegram/SourceFiles/api/api_bot.cpp
+++ b/Telegram/SourceFiles/api/api_bot.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_cloud_password.h"
#include "api/api_send_progress.h"
+#include "api/api_suggest_post.h"
#include "boxes/share_box.h"
#include "boxes/passcode_box.h"
#include "boxes/url_auth_box.h"
@@ -399,10 +400,12 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
}
}
const auto replyTo = FullReplyTo();
+ const auto suggest = SuggestPostOptions();
Window::PeerMenuCreatePoll(
controller,
item->history()->peer,
replyTo,
+ suggest,
chosen,
disabled);
} break;
@@ -519,6 +522,27 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
controller->showToast(tr::lng_text_copied(tr::now));
}
} break;
+
+ case ButtonType::SuggestAccept: {
+ Api::AcceptClickHandler(item)->onClick(ClickContext{
+ Qt::LeftButton,
+ QVariant::fromValue(context),
+ });
+ } break;
+
+ case ButtonType::SuggestDecline: {
+ Api::DeclineClickHandler(item)->onClick(ClickContext{
+ Qt::LeftButton,
+ QVariant::fromValue(context),
+ });
+ } break;
+
+ case ButtonType::SuggestChange: {
+ Api::SuggestChangesClickHandler(item)->onClick(ClickContext{
+ Qt::LeftButton,
+ QVariant::fromValue(context),
+ });
+ } break;
}
}
diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp
index 18cab335e6..b67e148677 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.cpp
+++ b/Telegram/SourceFiles/api/api_chat_invite.cpp
@@ -256,6 +256,7 @@ void ConfirmSubscriptionBox(
{
const auto balance = Settings::AddBalanceWidget(
content,
+ session,
session->credits().balanceValue(),
true);
session->credits().load(true);
@@ -436,6 +437,12 @@ void CheckChatInvite(
}
});
}, [=](const MTP::Error &error) {
+ if (MTP::IsFloodError(error)) {
+ if (const auto strong = weak.get()) {
+ strong->show(Ui::MakeInformBox(tr::lng_flood_error()));
+ }
+ return;
+ }
if (error.code() != 400) {
return;
}
diff --git a/Telegram/SourceFiles/api/api_chat_invite.h b/Telegram/SourceFiles/api/api_chat_invite.h
index 94eeab5e92..123ccb1f8d 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.h
+++ b/Telegram/SourceFiles/api/api_chat_invite.h
@@ -14,7 +14,7 @@ class ChannelData;
namespace Info::Profile {
class Badge;
-enum class BadgeType;
+enum class BadgeType : uchar;
} // namespace Info::Profile
namespace Main {
diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp
index 478390798f..8093168d3c 100644
--- a/Telegram/SourceFiles/api/api_chat_participants.cpp
+++ b/Telegram/SourceFiles/api/api_chat_participants.cpp
@@ -494,8 +494,15 @@ void ChatParticipants::requestBots(not_null channel) {
LOG(("API Error: "
"channels.channelParticipantsNotModified received!"));
});
- }).fail([=] {
+ }).fail([=](const MTP::Error &error) {
_botsRequests.remove(channel);
+ if (error.type() == u"CHANNEL_MONOFORUM_UNSUPPORTED"_q) {
+ channel->mgInfo->bots.clear();
+ channel->mgInfo->botStatus = -1;
+ channel->session().changes().peerUpdated(
+ channel,
+ Data::PeerUpdate::Flag::FullInfo);
+ }
}).send();
_botsRequests[channel] = requestId;
@@ -648,10 +655,7 @@ void ChatParticipants::Restrict(
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel,
participant->input,
- MTP_chatBannedRights(
- MTP_flags(MTPDchatBannedRights::Flags::from_raw(
- uint32(newRights.flags))),
- MTP_int(newRights.until))
+ RestrictionsToMTP(newRights)
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(participant, oldRights, newRights);
@@ -756,10 +760,7 @@ void ChatParticipants::kick(
const auto requestId = _api.request(MTPchannels_EditBanned(
channel->inputChannel,
participant->input,
- MTP_chatBannedRights(
- MTP_flags(
- MTPDchatBannedRights::Flags::from_raw(uint32(rights.flags))),
- MTP_int(rights.until))
+ RestrictionsToMTP(rights)
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp
index cfb1e72207..55017ade84 100644
--- a/Telegram/SourceFiles/api/api_common.cpp
+++ b/Telegram/SourceFiles/api/api_common.cpp
@@ -14,6 +14,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Api {
+MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) {
+ using Flag = MTPDsuggestedPost::Flag;
+ return suggest.exists
+ ? MTP_suggestedPost(
+ MTP_flags((suggest.date ? Flag::f_schedule_date : Flag())
+ | (suggest.price().empty() ? Flag() : Flag::f_price)),
+ StarsAmountToTL(suggest.price()),
+ MTP_int(suggest.date))
+ : MTPSuggestedPost();
+}
+
SendAction::SendAction(
not_null thread,
SendOptions options)
diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h
index c58f525c95..bbbff2b0cc 100644
--- a/Telegram/SourceFiles/api/api_common.h
+++ b/Telegram/SourceFiles/api/api_common.h
@@ -19,6 +19,8 @@ namespace Api {
inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
+[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest);
+
struct SendOptions {
uint64 price = 0;
PeerData *sendAs = nullptr;
@@ -31,6 +33,7 @@ struct SendOptions {
bool invertCaption = false;
bool hideViaBot = false;
crl::time ttlSeconds = 0;
+ SuggestPostOptions suggest;
friend inline bool operator==(
const SendOptions &,
diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp
index ec49947ebf..d274d424d2 100644
--- a/Telegram/SourceFiles/api/api_credits.cpp
+++ b/Telegram/SourceFiles/api/api_credits.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_credits.h"
+#include "api/api_credits_history_entry.h"
#include "api/api_premium.h"
#include "api/api_statistics_data_deserialize.h"
#include "api/api_updates.h"
@@ -27,146 +28,6 @@ namespace {
constexpr auto kTransactionsLimit = 100;
-[[nodiscard]] Data::CreditsHistoryEntry HistoryFromTL(
- const MTPStarsTransaction &tl,
- not_null peer) {
- using HistoryPeerTL = MTPDstarsTransactionPeer;
- using namespace Data;
- const auto owner = &peer->owner();
- const auto photo = tl.data().vphoto()
- ? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())
- : nullptr;
- auto extended = std::vector();
- if (const auto list = tl.data().vextended_media()) {
- extended.reserve(list->v.size());
- for (const auto &media : list->v) {
- media.match([&](const MTPDmessageMediaPhoto &data) {
- if (const auto inner = data.vphoto()) {
- const auto photo = owner->processPhoto(*inner);
- if (!photo->isNull()) {
- extended.push_back(CreditsHistoryMedia{
- .type = CreditsHistoryMediaType::Photo,
- .id = photo->id,
- });
- }
- }
- }, [&](const MTPDmessageMediaDocument &data) {
- if (const auto inner = data.vdocument()) {
- const auto document = owner->processDocument(
- *inner,
- data.valt_documents());
- if (document->isAnimation()
- || document->isVideoFile()
- || document->isGifv()) {
- extended.push_back(CreditsHistoryMedia{
- .type = CreditsHistoryMediaType::Video,
- .id = document->id,
- });
- }
- }
- }, [&](const auto &) {});
- }
- }
- const auto barePeerId = tl.data().vpeer().match([](
- const HistoryPeerTL &p) {
- return peerFromMTP(p.vpeer());
- }, [](const auto &) {
- return PeerId(0);
- }).value;
- const auto stargift = tl.data().vstargift();
- const auto nonUniqueGift = stargift
- ? stargift->match([&](const MTPDstarGift &data) {
- return &data;
- }, [](const auto &) { return (const MTPDstarGift*)nullptr; })
- : nullptr;
- const auto reaction = tl.data().is_reaction();
- const auto amount = Data::FromTL(tl.data().vstars());
- const auto starrefAmount = tl.data().vstarref_amount()
- ? Data::FromTL(*tl.data().vstarref_amount())
- : StarsAmount();
- const auto starrefCommission
- = tl.data().vstarref_commission_permille().value_or_empty();
- const auto starrefBarePeerId = tl.data().vstarref_peer()
- ? peerFromMTP(*tl.data().vstarref_peer()).value
- : 0;
- const auto incoming = (amount >= StarsAmount());
- const auto paidMessagesCount
- = tl.data().vpaid_messages().value_or_empty();
- const auto premiumMonthsForStars
- = tl.data().vpremium_gift_months().value_or_empty();
- const auto saveActorId = (reaction
- || !extended.empty()
- || paidMessagesCount) && incoming;
- const auto parsedGift = stargift
- ? FromTL(&peer->session(), *stargift)
- : std::optional();
- const auto giftStickerId = parsedGift ? parsedGift->document->id : 0;
- return Data::CreditsHistoryEntry{
- .id = qs(tl.data().vid()),
- .title = qs(tl.data().vtitle().value_or_empty()),
- .description = { qs(tl.data().vdescription().value_or_empty()) },
- .date = base::unixtime::parse(tl.data().vdate().v),
- .photoId = photo ? photo->id : 0,
- .extended = std::move(extended),
- .credits = Data::FromTL(tl.data().vstars()),
- .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),
- .barePeerId = saveActorId ? peer->id.value : barePeerId,
- .bareGiveawayMsgId = uint64(
- tl.data().vgiveaway_post_id().value_or_empty()),
- .bareGiftStickerId = giftStickerId,
- .bareActorId = saveActorId ? barePeerId : uint64(0),
- .uniqueGift = parsedGift ? parsedGift->unique : nullptr,
- .starrefAmount = paidMessagesCount ? StarsAmount() : starrefAmount,
- .starrefCommission = paidMessagesCount ? 0 : starrefCommission,
- .starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId,
- .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
- return Data::CreditsHistoryEntry::PeerType::Peer;
- }, [](const MTPDstarsTransactionPeerPlayMarket &) {
- return Data::CreditsHistoryEntry::PeerType::PlayMarket;
- }, [](const MTPDstarsTransactionPeerFragment &) {
- return Data::CreditsHistoryEntry::PeerType::Fragment;
- }, [](const MTPDstarsTransactionPeerAppStore &) {
- return Data::CreditsHistoryEntry::PeerType::AppStore;
- }, [](const MTPDstarsTransactionPeerUnsupported &) {
- return Data::CreditsHistoryEntry::PeerType::Unsupported;
- }, [](const MTPDstarsTransactionPeerPremiumBot &) {
- return Data::CreditsHistoryEntry::PeerType::PremiumBot;
- }, [](const MTPDstarsTransactionPeerAds &) {
- return Data::CreditsHistoryEntry::PeerType::Ads;
- }, [](const MTPDstarsTransactionPeerAPI &) {
- return Data::CreditsHistoryEntry::PeerType::API;
- }),
- .subscriptionUntil = tl.data().vsubscription_period()
- ? base::unixtime::parse(base::unixtime::now()
- + tl.data().vsubscription_period()->v)
- : QDateTime(),
- .successDate = tl.data().vtransaction_date()
- ? base::unixtime::parse(tl.data().vtransaction_date()->v)
- : QDateTime(),
- .successLink = qs(tl.data().vtransaction_url().value_or_empty()),
- .paidMessagesCount = paidMessagesCount,
- .paidMessagesAmount = (paidMessagesCount
- ? starrefAmount
- : StarsAmount()),
- .paidMessagesCommission = paidMessagesCount ? starrefCommission : 0,
- .starsConverted = int(nonUniqueGift
- ? nonUniqueGift->vconvert_stars().v
- : 0),
- .premiumMonthsForStars = premiumMonthsForStars,
- .floodSkip = int(tl.data().vfloodskip_number().value_or(0)),
- .converted = stargift && incoming,
- .stargift = stargift.has_value(),
- .giftUpgraded = tl.data().is_stargift_upgrade(),
- .giftResale = tl.data().is_stargift_resale(),
- .reaction = tl.data().is_reaction(),
- .refunded = tl.data().is_refund(),
- .pending = tl.data().is_pending(),
- .failed = tl.data().is_failed(),
- .in = incoming,
- .gift = tl.data().is_gift() || stargift.has_value(),
- };
-}
-
[[nodiscard]] Data::SubscriptionEntry SubscriptionFromTL(
const MTPStarsSubscription &tl,
not_null peer) {
@@ -203,7 +64,7 @@ constexpr auto kTransactionsLimit = 100;
if (const auto history = data.vhistory()) {
entries.reserve(history->v.size());
for (const auto &tl : history->v) {
- entries.push_back(HistoryFromTL(tl, peer));
+ entries.push_back(CreditsHistoryEntryFromTL(tl, peer));
}
}
auto subscriptions = std::vector();
@@ -216,7 +77,7 @@ constexpr auto kTransactionsLimit = 100;
return Data::CreditsStatusSlice{
.list = std::move(entries),
.subscriptions = std::move(subscriptions),
- .balance = Data::FromTL(status.data().vbalance()),
+ .balance = CreditsAmountFromTL(status.data().vbalance()),
.subscriptionsMissingBalance
= status.data().vsubscriptions_missing_balance().value_or_empty(),
.allLoaded = !status.data().vnext_offset().has_value()
@@ -300,11 +161,14 @@ void CreditsStatus::request(
using TLResult = MTPpayments_StarsStatus;
_requestId = _api.request(MTPpayments_GetStarsStatus(
+ MTP_flags(0),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input
)).done([=](const TLResult &result) {
_requestId = 0;
const auto &balance = result.data().vbalance();
- _peer->session().credits().apply(_peer->id, Data::FromTL(balance));
+ _peer->session().credits().apply(
+ _peer->id,
+ CreditsAmountFromTL(balance));
if (const auto onstack = done) {
onstack(StatusFromTL(result, _peer));
}
@@ -316,13 +180,18 @@ void CreditsStatus::request(
}).send();
}
-CreditsHistory::CreditsHistory(not_null peer, bool in, bool out)
+CreditsHistory::CreditsHistory(
+ not_null peer,
+ bool in,
+ bool out,
+ bool currency)
: _peer(peer)
-, _flags((in == out)
+, _flags(((in == out)
? HistoryTL::Flags(0)
: HistoryTL::Flags(0)
| (in ? HistoryTL::Flag::f_inbound : HistoryTL::Flags(0))
| (out ? HistoryTL::Flag::f_outbound : HistoryTL::Flags(0)))
+ | (currency ? HistoryTL::Flag::f_ton : HistoryTL::Flags(0)))
, _api(&peer->session().api().instance()) {
}
@@ -420,13 +289,15 @@ rpl::producer CreditsEarnStatistics::request() {
)).done([=](const MTPpayments_StarsRevenueStats &result) {
const auto &data = result.data();
const auto &status = data.vstatus().data();
- using Data::FromTL;
_data = Data::CreditsEarnStatistics{
.revenueGraph = StatisticalGraphFromTL(
data.vrevenue_graph()),
- .currentBalance = FromTL(status.vcurrent_balance()),
- .availableBalance = FromTL(status.vavailable_balance()),
- .overallRevenue = FromTL(status.voverall_revenue()),
+ .currentBalance = CreditsAmountFromTL(
+ status.vcurrent_balance()),
+ .availableBalance = CreditsAmountFromTL(
+ status.vavailable_balance()),
+ .overallRevenue = CreditsAmountFromTL(
+ status.voverall_revenue()),
.usdRate = data.vusd_rate().v,
.isWithdrawalEnabled = status.is_withdrawal_enabled(),
.nextWithdrawalAt = status.vnext_withdrawal_at()
diff --git a/Telegram/SourceFiles/api/api_credits.h b/Telegram/SourceFiles/api/api_credits.h
index 559e84711e..1b5745e42b 100644
--- a/Telegram/SourceFiles/api/api_credits.h
+++ b/Telegram/SourceFiles/api/api_credits.h
@@ -75,7 +75,11 @@ private:
class CreditsHistory final {
public:
- CreditsHistory(not_null peer, bool in, bool out);
+ CreditsHistory(
+ not_null peer,
+ bool in,
+ bool out,
+ bool currency = false);
void request(
const Data::CreditsStatusSlice::OffsetToken &token,
diff --git a/Telegram/SourceFiles/api/api_credits_history_entry.cpp b/Telegram/SourceFiles/api/api_credits_history_entry.cpp
new file mode 100644
index 0000000000..c6acdfeb6d
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_credits_history_entry.cpp
@@ -0,0 +1,167 @@
+/*
+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_credits_history_entry.h"
+
+#include "api/api_premium.h"
+#include "base/unixtime.h"
+#include "data/data_channel.h"
+#include "data/data_credits.h"
+#include "data/data_document.h"
+#include "data/data_peer.h"
+#include "data/data_photo.h"
+#include "data/data_session.h"
+#include "data/data_user.h"
+#include "main/main_session.h"
+
+namespace Api {
+
+Data::CreditsHistoryEntry CreditsHistoryEntryFromTL(
+ const MTPStarsTransaction &tl,
+ not_null peer) {
+ using HistoryPeerTL = MTPDstarsTransactionPeer;
+ using namespace Data;
+ const auto owner = &peer->owner();
+ const auto photo = tl.data().vphoto()
+ ? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())
+ : nullptr;
+ auto extended = std::vector();
+ if (const auto list = tl.data().vextended_media()) {
+ extended.reserve(list->v.size());
+ for (const auto &media : list->v) {
+ media.match([&](const MTPDmessageMediaPhoto &data) {
+ if (const auto inner = data.vphoto()) {
+ const auto photo = owner->processPhoto(*inner);
+ if (!photo->isNull()) {
+ extended.push_back(CreditsHistoryMedia{
+ .type = CreditsHistoryMediaType::Photo,
+ .id = photo->id,
+ });
+ }
+ }
+ }, [&](const MTPDmessageMediaDocument &data) {
+ if (const auto inner = data.vdocument()) {
+ const auto document = owner->processDocument(
+ *inner,
+ data.valt_documents());
+ if (document->isAnimation()
+ || document->isVideoFile()
+ || document->isGifv()) {
+ extended.push_back(CreditsHistoryMedia{
+ .type = CreditsHistoryMediaType::Video,
+ .id = document->id,
+ });
+ }
+ }
+ }, [&](const auto &) {});
+ }
+ }
+ const auto barePeerId = tl.data().vpeer().match([](
+ const HistoryPeerTL &p) {
+ return peerFromMTP(p.vpeer());
+ }, [](const auto &) {
+ return PeerId(0);
+ }).value;
+ const auto stargift = tl.data().vstargift();
+ const auto nonUniqueGift = stargift
+ ? stargift->match([&](const MTPDstarGift &data) {
+ return &data;
+ }, [](const auto &) { return (const MTPDstarGift*)nullptr; })
+ : nullptr;
+ const auto reaction = tl.data().is_reaction();
+ const auto amount = CreditsAmountFromTL(tl.data().vamount());
+ const auto starrefAmount = CreditsAmountFromTL(
+ tl.data().vstarref_amount());
+ const auto starrefCommission
+ = tl.data().vstarref_commission_permille().value_or_empty();
+ const auto starrefBarePeerId = tl.data().vstarref_peer()
+ ? peerFromMTP(*tl.data().vstarref_peer()).value
+ : 0;
+ const auto incoming = (amount >= CreditsAmount());
+ const auto paidMessagesCount
+ = tl.data().vpaid_messages().value_or_empty();
+ const auto premiumMonthsForStars
+ = tl.data().vpremium_gift_months().value_or_empty();
+ const auto saveActorId = (reaction
+ || !extended.empty()
+ || paidMessagesCount) && incoming;
+ const auto parsedGift = stargift
+ ? FromTL(&peer->session(), *stargift)
+ : std::optional();
+ const auto giftStickerId = parsedGift ? parsedGift->document->id : 0;
+ return Data::CreditsHistoryEntry{
+ .id = qs(tl.data().vid()),
+ .title = qs(tl.data().vtitle().value_or_empty()),
+ .description = { qs(tl.data().vdescription().value_or_empty()) },
+ .date = base::unixtime::parse(
+ tl.data().vads_proceeds_from_date().value_or(
+ tl.data().vdate().v)),
+ .photoId = photo ? photo->id : 0,
+ .extended = std::move(extended),
+ .credits = CreditsAmountFromTL(tl.data().vamount()),
+ .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),
+ .barePeerId = saveActorId ? peer->id.value : barePeerId,
+ .bareGiveawayMsgId = uint64(
+ tl.data().vgiveaway_post_id().value_or_empty()),
+ .bareGiftStickerId = giftStickerId,
+ .bareActorId = saveActorId ? barePeerId : uint64(0),
+ .uniqueGift = parsedGift ? parsedGift->unique : nullptr,
+ .starrefAmount = paidMessagesCount ? CreditsAmount() : starrefAmount,
+ .starrefCommission = paidMessagesCount ? 0 : starrefCommission,
+ .starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId,
+ .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
+ return Data::CreditsHistoryEntry::PeerType::Peer;
+ }, [](const MTPDstarsTransactionPeerPlayMarket &) {
+ return Data::CreditsHistoryEntry::PeerType::PlayMarket;
+ }, [](const MTPDstarsTransactionPeerFragment &) {
+ return Data::CreditsHistoryEntry::PeerType::Fragment;
+ }, [](const MTPDstarsTransactionPeerAppStore &) {
+ return Data::CreditsHistoryEntry::PeerType::AppStore;
+ }, [](const MTPDstarsTransactionPeerUnsupported &) {
+ return Data::CreditsHistoryEntry::PeerType::Unsupported;
+ }, [](const MTPDstarsTransactionPeerPremiumBot &) {
+ return Data::CreditsHistoryEntry::PeerType::PremiumBot;
+ }, [](const MTPDstarsTransactionPeerAds &) {
+ return Data::CreditsHistoryEntry::PeerType::Ads;
+ }, [](const MTPDstarsTransactionPeerAPI &) {
+ return Data::CreditsHistoryEntry::PeerType::API;
+ }),
+ .subscriptionUntil = tl.data().vsubscription_period()
+ ? base::unixtime::parse(base::unixtime::now()
+ + tl.data().vsubscription_period()->v)
+ : QDateTime(),
+ .adsProceedsToDate = tl.data().vads_proceeds_to_date()
+ ? base::unixtime::parse(tl.data().vads_proceeds_to_date()->v)
+ : QDateTime(),
+ .successDate = tl.data().vtransaction_date()
+ ? base::unixtime::parse(tl.data().vtransaction_date()->v)
+ : QDateTime(),
+ .successLink = qs(tl.data().vtransaction_url().value_or_empty()),
+ .paidMessagesCount = paidMessagesCount,
+ .paidMessagesAmount = (paidMessagesCount
+ ? starrefAmount
+ : CreditsAmount()),
+ .paidMessagesCommission = paidMessagesCount ? starrefCommission : 0,
+ .starsConverted = int(nonUniqueGift
+ ? nonUniqueGift->vconvert_stars().v
+ : 0),
+ .premiumMonthsForStars = premiumMonthsForStars,
+ .floodSkip = int(tl.data().vfloodskip_number().value_or(0)),
+ .converted = stargift && incoming,
+ .stargift = stargift.has_value(),
+ .giftUpgraded = tl.data().is_stargift_upgrade(),
+ .giftResale = tl.data().is_stargift_resale(),
+ .reaction = tl.data().is_reaction(),
+ .refunded = tl.data().is_refund(),
+ .pending = tl.data().is_pending(),
+ .failed = tl.data().is_failed(),
+ .in = incoming,
+ .gift = tl.data().is_gift() || stargift.has_value(),
+ };
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_credits_history_entry.h b/Telegram/SourceFiles/api/api_credits_history_entry.h
new file mode 100644
index 0000000000..5b3762a851
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_credits_history_entry.h
@@ -0,0 +1,22 @@
+/*
+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 PeerData;
+
+namespace Data {
+struct CreditsHistoryEntry;
+} // namespace Data
+
+namespace Api {
+
+[[nodiscard]] Data::CreditsHistoryEntry CreditsHistoryEntryFromTL(
+ const MTPStarsTransaction &tl,
+ not_null peer);
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_earn.cpp b/Telegram/SourceFiles/api/api_earn.cpp
index d0b58b40fc..9df407aad2 100644
--- a/Telegram/SourceFiles/api/api_earn.cpp
+++ b/Telegram/SourceFiles/api/api_earn.cpp
@@ -56,7 +56,6 @@ void HandleWithdrawalButton(
? ¤cyReceiver->session()
: &creditsReceiver->session());
- using ChannelOutUrl = MTPstats_BroadcastRevenueWithdrawalUrl;
using CreditsOutUrl = MTPpayments_StarsRevenueWithdrawalUrl;
session->api().cloudPassword().reload();
@@ -98,19 +97,19 @@ void HandleWithdrawalButton(
show->showToast(message);
}
};
- if (currencyReceiver) {
- session->api().request(
- MTPstats_GetBroadcastRevenueWithdrawalUrl(
- currencyReceiver->input,
- result.result
- )).done([=](const ChannelOutUrl &r) {
- done(qs(r.data().vurl()));
- }).fail(fail).send();
- } else if (creditsReceiver) {
+ if (currencyReceiver || creditsReceiver) {
+ using F = MTPpayments_getStarsRevenueWithdrawalUrl::Flag;
session->api().request(
MTPpayments_GetStarsRevenueWithdrawalUrl(
- creditsReceiver->input,
- MTP_long(receiver.creditsAmount()),
+ MTP_flags(currencyReceiver
+ ? F::f_ton
+ : F::f_amount),
+ currencyReceiver
+ ? currencyReceiver->input
+ : creditsReceiver->input,
+ MTP_long(creditsReceiver
+ ? receiver.creditsAmount()
+ : 0),
result.result
)).done([=](const CreditsOutUrl &r) {
done(qs(r.data().vurl()));
@@ -138,17 +137,19 @@ void HandleWithdrawalButton(
processOut();
}
};
- if (currencyReceiver) {
- session->api().request(
- MTPstats_GetBroadcastRevenueWithdrawalUrl(
- currencyReceiver->input,
- MTP_inputCheckPasswordEmpty()
- )).fail(fail).send();
- } else if (creditsReceiver) {
+ if (currencyReceiver || creditsReceiver) {
+ using F = MTPpayments_getStarsRevenueWithdrawalUrl::Flag;
session->api().request(
MTPpayments_GetStarsRevenueWithdrawalUrl(
- creditsReceiver->input,
- MTP_long(receiver.creditsAmount()),
+ MTP_flags(currencyReceiver
+ ? F::f_ton
+ : F::f_amount),
+ currencyReceiver
+ ? currencyReceiver->input
+ : creditsReceiver->input,
+ MTP_long(creditsReceiver
+ ? receiver.creditsAmount()
+ : 0),
MTP_inputCheckPasswordEmpty()
)).fail(fail).send();
}
diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp
index 4f5073a3b8..e3ec52e172 100644
--- a/Telegram/SourceFiles/api/api_editing.cpp
+++ b/Telegram/SourceFiles/api/api_editing.cpp
@@ -10,15 +10,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "api/api_media.h"
#include "api/api_text_entities.h"
+#include "base/random.h"
#include "ui/boxes/confirm_box.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/scheduled_messages.h"
#include "data/data_file_origin.h"
#include "data/data_histories.h"
+#include "data/data_saved_sublist.h"
#include "data/data_session.h"
+#include "data/data_todo_list.h"
#include "data/data_web_page.h"
#include "history/view/controls/history_view_compose_media_edit_manager.h"
#include "history/history.h"
+#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mtproto/mtproto_response.h"
@@ -45,6 +49,193 @@ template
constexpr auto ErrorWithoutId
= is_callable_plain_v;
+template
+mtpRequestId SuggestMessage(
+ not_null item,
+ const TextWithEntities &textWithEntities,
+ Data::WebPageDraft webpage,
+ SendOptions options,
+ DoneCallback &&done,
+ FailCallback &&fail) {
+ Expects(options.suggest.exists);
+ Expects(!options.scheduled);
+
+ const auto session = &item->history()->session();
+ const auto api = &session->api();
+
+ const auto thread = item->history()->amMonoforumAdmin()
+ ? item->savedSublist()
+ : (Data::Thread*)item->history();
+ auto action = SendAction(thread, options);
+ action.replyTo = FullReplyTo{
+ .messageId = item->fullId(),
+ .monoforumPeerId = (item->history()->amMonoforumAdmin()
+ ? item->sublistPeerId()
+ : PeerId()),
+ };
+
+ auto message = MessageToSend(std::move(action));
+ message.textWithTags = TextWithTags{
+ textWithEntities.text,
+ TextUtilities::ConvertEntitiesToTextTags(textWithEntities.entities)
+ };
+ message.webPage = webpage;
+ api->sendMessage(std::move(message));
+
+ const auto requestId = -1;
+ crl::on_main(session, [=] {
+ const auto type = u"MESSAGE_NOT_MODIFIED"_q;
+ if constexpr (ErrorWithId) {
+ fail(type, requestId);
+ } else if constexpr (ErrorWithoutId) {
+ fail(type);
+ } else if constexpr (WithoutCallback) {
+ fail();
+ } else {
+ t_bad_callback(fail);
+ }
+ });
+ return requestId;
+}
+
+template
+mtpRequestId SuggestMedia(
+ not_null item,
+ const TextWithEntities &textWithEntities,
+ Data::WebPageDraft webpage,
+ SendOptions options,
+ DoneCallback &&done,
+ FailCallback &&fail,
+ std::optional inputMedia) {
+ Expects(options.suggest.exists);
+ Expects(!options.scheduled);
+
+ const auto session = &item->history()->session();
+ const auto api = &session->api();
+
+ const auto text = textWithEntities.text;
+ const auto sentEntities = EntitiesToMTP(
+ session,
+ textWithEntities.entities,
+ ConvertOption::SkipLocal);
+
+ const auto updateRecentStickers = inputMedia
+ ? Api::HasAttachedStickers(*inputMedia)
+ : false;
+
+ const auto emptyFlag = MTPmessages_SendMedia::Flag(0);
+ auto replyTo = FullReplyTo{
+ .messageId = item->fullId(),
+ .monoforumPeerId = (item->history()->amMonoforumAdmin()
+ ? item->sublistPeerId()
+ : PeerId()),
+ };
+ const auto flags = emptyFlag
+ | MTPmessages_SendMedia::Flag::f_reply_to
+ | MTPmessages_SendMedia::Flag::f_suggested_post
+ | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
+ || options.invertCaption)
+ ? MTPmessages_SendMedia::Flag::f_invert_media
+ : emptyFlag)
+ | (!sentEntities.v.isEmpty()
+ ? MTPmessages_SendMedia::Flag::f_entities
+ : emptyFlag)
+ | (options.starsApproved
+ ? MTPmessages_SendMedia::Flag::f_allow_paid_stars
+ : emptyFlag);
+ const auto randomId = base::RandomValue();
+ return api->request(MTPmessages_SendMedia(
+ MTP_flags(flags),
+ item->history()->peer->input,
+ ReplyToForMTP(item->history(), replyTo),
+ inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())),
+ MTP_string(text),
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ sentEntities,
+ MTPint(), // schedule_date
+ MTPInputPeer(), // send_as
+ MTPInputQuickReplyShortcut(), // quick_reply_shortcut
+ MTPlong(), // effect
+ MTP_long(options.starsApproved),
+ Api::SuggestToMTP(options.suggest)
+ )).done([=](
+ const MTPUpdates &result,
+ [[maybe_unused]] mtpRequestId requestId) {
+ const auto apply = [=] { api->applyUpdates(result); };
+
+ if constexpr (WithId) {
+ done(apply, requestId);
+ } else if constexpr (WithoutId) {
+ done(apply);
+ } else if constexpr (WithoutCallback) {
+ done();
+ apply();
+ } else {
+ t_bad_callback(done);
+ }
+
+ if (updateRecentStickers) {
+ api->requestSpecialStickersForce(false, false, true);
+ }
+ }).fail([=](const MTP::Error &error, mtpRequestId requestId) {
+ if constexpr (ErrorWithId) {
+ fail(error.type(), requestId);
+ } else if constexpr (ErrorWithoutId) {
+ fail(error.type());
+ } else if constexpr (WithoutCallback) {
+ fail();
+ } else {
+ t_bad_callback(fail);
+ }
+ }).send();
+}
+
+template
+mtpRequestId SuggestMessageOrMedia(
+ not_null item,
+ const TextWithEntities &textWithEntities,
+ Data::WebPageDraft webpage,
+ SendOptions options,
+ DoneCallback &&done,
+ FailCallback &&fail,
+ std::optional inputMedia) {
+ const auto wasMedia = item->media();
+ if (!inputMedia && wasMedia && wasMedia->allowsEditCaption()) {
+ if (const auto photo = wasMedia->photo()) {
+ inputMedia = MTP_inputMediaPhoto(
+ MTP_flags(0),
+ photo->mtpInput(),
+ MTPint()); // ttl_seconds
+ } else if (const auto document = wasMedia->document()) {
+ inputMedia = MTP_inputMediaDocument(
+ MTP_flags(0),
+ document->mtpInput(),
+ MTPInputPhoto(), // video_cover
+ MTPint(), // video_timestamp
+ MTPint(), // ttl_seconds
+ MTPstring()); // query
+ }
+ }
+ if (inputMedia) {
+ return SuggestMedia(
+ item,
+ textWithEntities,
+ webpage,
+ options,
+ std::move(done),
+ std::move(fail),
+ inputMedia);
+ }
+ return SuggestMessage(
+ item,
+ textWithEntities,
+ webpage,
+ options,
+ std::move(done),
+ std::move(fail));
+}
+
template
mtpRequestId EditMessage(
not_null item,
@@ -54,6 +245,18 @@ mtpRequestId EditMessage(
DoneCallback &&done,
FailCallback &&fail,
std::optional inputMedia = std::nullopt) {
+ if (item->computeSuggestionActions()
+ == SuggestionActions::AcceptAndDecline) {
+ return SuggestMessageOrMedia(
+ item,
+ textWithEntities,
+ webpage,
+ options,
+ std::move(done),
+ std::move(fail),
+ inputMedia);
+ }
+
const auto session = &item->history()->session();
const auto api = &session->api();
@@ -70,31 +273,31 @@ mtpRequestId EditMessage(
const auto emptyFlag = MTPmessages_EditMessage::Flag(0);
const auto flags = emptyFlag
- | ((!text.isEmpty() || media)
- ? MTPmessages_EditMessage::Flag::f_message
- : emptyFlag)
- | ((media && inputMedia.has_value())
- ? MTPmessages_EditMessage::Flag::f_media
- : emptyFlag)
- | (webpage.removed
- ? MTPmessages_EditMessage::Flag::f_no_webpage
- : emptyFlag)
- | ((!webpage.removed && !webpage.url.isEmpty())
- ? MTPmessages_EditMessage::Flag::f_media
- : emptyFlag)
- | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
- || options.invertCaption)
- ? MTPmessages_EditMessage::Flag::f_invert_media
- : emptyFlag)
- | (!sentEntities.v.isEmpty()
- ? MTPmessages_EditMessage::Flag::f_entities
- : emptyFlag)
- | (options.scheduled
- ? MTPmessages_EditMessage::Flag::f_schedule_date
- : emptyFlag)
- | (item->isBusinessShortcut()
- ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id
- : emptyFlag);
+ | ((!text.isEmpty() || media)
+ ? MTPmessages_EditMessage::Flag::f_message
+ : emptyFlag)
+ | ((media && inputMedia.has_value())
+ ? MTPmessages_EditMessage::Flag::f_media
+ : emptyFlag)
+ | (webpage.removed
+ ? MTPmessages_EditMessage::Flag::f_no_webpage
+ : emptyFlag)
+ | ((!webpage.removed && !webpage.url.isEmpty())
+ ? MTPmessages_EditMessage::Flag::f_media
+ : emptyFlag)
+ | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
+ || options.invertCaption)
+ ? MTPmessages_EditMessage::Flag::f_invert_media
+ : emptyFlag)
+ | (!sentEntities.v.isEmpty()
+ ? MTPmessages_EditMessage::Flag::f_entities
+ : emptyFlag)
+ | (options.scheduled
+ ? MTPmessages_EditMessage::Flag::f_schedule_date
+ : emptyFlag)
+ | (item->isBusinessShortcut()
+ ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id
+ : emptyFlag);
const auto id = item->isScheduled()
? session->scheduledMessages().lookupId(item)
@@ -358,4 +561,22 @@ mtpRequestId EditTextMessage(
std::nullopt);
}
+void EditTodoList(
+ not_null item,
+ const TodoListData &data,
+ SendOptions options,
+ Fn done,
+ Fn fail) {
+ const auto callback = [=](Fn applyUpdates, mtpRequestId id) {
+ applyUpdates();
+ done(id);
+ };
+ EditMessage(
+ item,
+ options,
+ callback,
+ fail,
+ MTP_inputMediaTodo(TodoListDataToMTP(&data)));
+}
+
} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_editing.h b/Telegram/SourceFiles/api/api_editing.h
index 630e1cd8d5..ca3ff7c121 100644
--- a/Telegram/SourceFiles/api/api_editing.h
+++ b/Telegram/SourceFiles/api/api_editing.h
@@ -58,4 +58,11 @@ mtpRequestId EditTextMessage(
Fn fail,
bool spoilered);
+void EditTodoList(
+ not_null item,
+ const TodoListData &data,
+ SendOptions options,
+ Fn done,
+ Fn fail);
+
} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp
index 9a68a71e01..5306eb9ebe 100644
--- a/Telegram/SourceFiles/api/api_polls.cpp
+++ b/Telegram/SourceFiles/api/api_polls.cpp
@@ -52,6 +52,7 @@ void Polls::create(
const auto topicRootId = action.replyTo.messageId
? action.replyTo.topicRootId
: 0;
+ const auto monoforumPeerId = action.replyTo.monoforumPeerId;
auto sendFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
@@ -59,9 +60,9 @@ void Polls::create(
const auto clearCloudDraft = action.clearDraft;
if (clearCloudDraft) {
sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
- history->clearLocalDraft(topicRootId);
- history->clearCloudDraft(topicRootId);
- history->startSavingCloudDraft(topicRootId);
+ history->clearLocalDraft(topicRootId, monoforumPeerId);
+ history->clearCloudDraft(topicRootId, monoforumPeerId);
+ history->startSavingCloudDraft(topicRootId, monoforumPeerId);
}
const auto silentPost = ShouldSendSilent(peer, action.options);
const auto starsPaid = std::min(
@@ -79,6 +80,9 @@ void Polls::create(
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ if (action.options.suggest) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
+ }
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
@@ -106,11 +110,13 @@ void Polls::create(
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
+ monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
_session->changes().historyUpdated(
@@ -123,6 +129,7 @@ void Polls::create(
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
+ monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
fail();
diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp
index fc48c21c50..5119f3908c 100644
--- a/Telegram/SourceFiles/api/api_premium.cpp
+++ b/Telegram/SourceFiles/api/api_premium.cpp
@@ -424,7 +424,7 @@ void Premium::requestPremiumRequiredSlice() {
constexpr auto hasPrem = Flag::HasRequirePremiumToWrite;
constexpr auto hasStars = Flag::HasStarsPerMessage;
user->setStarsPerMessage(stars);
- user->setFlags((user->flags() & ~(me | hasPrem | hasStars))
+ user->setFlags((user->flags() & ~me)
| known
| (requirePremium ? (me | hasPrem) : Flag())
| (stars ? hasStars : Flag()));
diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp
index 814b0a9832..cbfa6a937d 100644
--- a/Telegram/SourceFiles/api/api_sending.cpp
+++ b/Telegram/SourceFiles/api/api_sending.cpp
@@ -109,6 +109,9 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ if (action.options.suggest) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
+ }
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
@@ -136,7 +139,8 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId);
@@ -211,6 +215,9 @@ void SendExistingMedia(
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ if (action.options.suggest) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
+ }
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
@@ -232,6 +239,7 @@ void SendExistingMedia(
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
+ .suggest = HistoryMessageSuggestInfo(action.options),
}, media, caption);
const auto performRequest = [=](const auto &repeatRequest) -> void {
@@ -255,7 +263,8 @@ void SendExistingMedia(
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (error.code() == 400
@@ -391,6 +400,9 @@ bool SendDice(MessageToSend &message) {
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ if (action.options.suggest) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
+ }
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
@@ -415,6 +427,7 @@ bool SendDice(MessageToSend &message) {
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
+ .suggest = HistoryMessageSuggestInfo(action.options),
}, TextWithEntities(), MTP_messageMediaDice(
MTP_int(0),
MTP_string(emoji)));
@@ -435,7 +448,8 @@ bool SendDice(MessageToSend &message) {
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId, newId);
@@ -624,6 +638,7 @@ void SendConfirmedFile(
edition.useSameMarkup = true;
edition.useSameReplies = true;
edition.useSameReactions = true;
+ edition.useSameSuggest = true;
edition.savePreviousMedia = true;
itemToEdit->applyEdition(std::move(edition));
} else {
@@ -640,6 +655,7 @@ void SendConfirmedFile(
.postAuthor = NewMessagePostAuthor(action),
.groupedId = groupId,
.effectId = file->to.options.effectId,
+ .suggest = HistoryMessageSuggestInfo(file->to.options),
}, caption, media);
}
diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp
index 400ad7fcae..430af6f158 100644
--- a/Telegram/SourceFiles/api/api_statistics.cpp
+++ b/Telegram/SourceFiles/api/api_statistics.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "api/api_statistics.h"
+#include "api/api_credits_history_entry.h"
#include "api/api_statistics_data_deserialize.h"
#include "apiwrap.h"
#include "base/unixtime.h"
@@ -695,19 +696,23 @@ rpl::producer EarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
- makeRequest(MTPstats_GetBroadcastRevenueStats(
- MTP_flags(0),
+ makeRequest(MTPpayments_GetStarsRevenueStats(
+ MTP_flags(MTPpayments_getStarsRevenueStats::Flag::f_ton),
(_isUser ? user()->input : channel()->input)
- )).done([=](const MTPstats_BroadcastRevenueStats &result) {
+ )).done([=](const MTPpayments_StarsRevenueStats &result) {
const auto &data = result.data();
- const auto &balances = data.vbalances().data();
+ const auto &balances = data.vstatus().data();
+ const auto amount = [](const auto &a) {
+ return CreditsAmountFromTL(a);
+ };
_data = Data::EarnStatistics{
- .topHoursGraph = StatisticalGraphFromTL(
- data.vtop_hours_graph()),
+ .topHoursGraph = data.vtop_hours_graph()
+ ? StatisticalGraphFromTL(*data.vtop_hours_graph())
+ : Data::StatisticalGraph(),
.revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()),
- .currentBalance = balances.vcurrent_balance().v,
- .availableBalance = balances.vavailable_balance().v,
- .overallRevenue = balances.voverall_revenue().v,
+ .currentBalance = amount(balances.vcurrent_balance()),
+ .availableBalance = amount(balances.vavailable_balance()),
+ .overallRevenue = amount(balances.voverall_revenue()),
.usdRate = data.vusd_rate().v,
};
@@ -745,62 +750,35 @@ void EarnStatistics::requestHistory(
if (_requestId) {
return;
}
+
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit);
- _requestId = api().request(MTPstats_GetBroadcastRevenueTransactions(
+
+ _requestId = api().request(MTPpayments_GetStarsTransactions(
+ MTP_flags(MTPpayments_getStarsTransactions::Flag::f_ton),
+ MTP_string(), // Subscription ID.
(_isUser ? user()->input : channel()->input),
- MTP_int(token),
- (!token) ? kTlFirstSlice : kTlLimit
- )).done([=](const MTPstats_BroadcastRevenueTransactions &result) {
+ MTP_string(token),
+ token.isEmpty() ? kTlFirstSlice : kTlLimit
+ )).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
- const auto &tlTransactions = result.data().vtransactions().v;
+ const auto nextToken = result.data().vnext_offset().value_or_empty();
- auto list = std::vector();
- list.reserve(tlTransactions.size());
- for (const auto &tlTransaction : tlTransactions) {
- list.push_back(tlTransaction.match([&](
- const MTPDbroadcastRevenueTransactionProceeds &d) {
- return Data::EarnHistoryEntry{
- .type = Data::EarnHistoryEntry::Type::In,
- .amount = d.vamount().v,
- .date = base::unixtime::parse(d.vfrom_date().v),
- .dateTo = base::unixtime::parse(d.vto_date().v),
- };
- }, [&](const MTPDbroadcastRevenueTransactionWithdrawal &d) {
- return Data::EarnHistoryEntry{
- .type = Data::EarnHistoryEntry::Type::Out,
- .status = d.is_pending()
- ? Data::EarnHistoryEntry::Status::Pending
- : d.is_failed()
- ? Data::EarnHistoryEntry::Status::Failed
- : Data::EarnHistoryEntry::Status::Success,
- .amount = (std::numeric_limits::max()
- - d.vamount().v
- + 1),
- .date = base::unixtime::parse(d.vdate().v),
- // .provider = qs(d.vprovider()),
- .successDate = d.vtransaction_date()
- ? base::unixtime::parse(d.vtransaction_date()->v)
- : QDateTime(),
- .successLink = d.vtransaction_url()
- ? qs(*d.vtransaction_url())
- : QString(),
- };
- }, [&](const MTPDbroadcastRevenueTransactionRefund &d) {
- return Data::EarnHistoryEntry{
- .type = Data::EarnHistoryEntry::Type::Return,
- .amount = d.vamount().v,
- .date = base::unixtime::parse(d.vdate().v),
- // .provider = qs(d.vprovider()),
- };
- }));
- }
- const auto nextToken = token + tlTransactions.size();
+ const auto tlTransactions
+ = result.data().vhistory().value_or_empty();
+
+ const auto peer = _isUser ? (PeerData*)user() : (PeerData*)channel();
+ auto list = ranges::views::all(
+ tlTransactions
+ ) | ranges::views::transform([=](const auto &d) {
+ return CreditsHistoryEntryFromTL(d, peer);
+ }) | ranges::to_vector;
done(Data::EarnHistorySlice{
.list = std::move(list),
- .total = result.data().vcount().v,
- .allLoaded = (result.data().vcount().v == nextToken),
+ .total = int(tlTransactions.size()),
+ // .total = result.data().vcount().v,
+ .allLoaded = nextToken.isEmpty(),
.token = Data::EarnHistorySlice::OffsetToken(nextToken),
});
}).fail([=] {
diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp
new file mode 100644
index 0000000000..7af4fbc84d
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_suggest_post.cpp
@@ -0,0 +1,638 @@
+/*
+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_suggest_post.h"
+
+#include "apiwrap.h"
+#include "base/unixtime.h"
+#include "chat_helpers/message_field.h"
+#include "core/click_handler_types.h"
+#include "data/components/credits.h"
+#include "data/data_changes.h"
+#include "data/data_channel.h"
+#include "data/data_session.h"
+#include "data/data_saved_sublist.h"
+#include "history/view/controls/history_view_suggest_options.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "history/history_item_components.h"
+#include "history/history_item_helpers.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "mainwindow.h"
+#include "settings/settings_credits_graphics.h"
+#include "ui/boxes/choose_date_time.h"
+#include "ui/layers/generic_box.h"
+#include "ui/boxes/confirm_box.h"
+#include "ui/text/text_utilities.h"
+#include "ui/widgets/fields/input_field.h"
+#include "ui/widgets/popup_menu.h"
+#include "window/window_session_controller.h"
+#include "styles/style_chat.h"
+#include "styles/style_layers.h"
+#include "styles/style_menu_icons.h"
+
+namespace Api {
+namespace {
+
+void SendApproval(
+ std::shared_ptr show,
+ not_null item,
+ TimeId scheduleDate = 0) {
+ using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;
+ const auto suggestion = item->Get();
+ if (!suggestion
+ || suggestion->accepted
+ || suggestion->rejected
+ || suggestion->requestId) {
+ return;
+ }
+
+ const auto id = item->fullId();
+ const auto session = &show->session();
+ const auto finish = [=] {
+ if (const auto item = session->data().message(id)) {
+ const auto suggestion = item->Get();
+ if (suggestion) {
+ suggestion->requestId = 0;
+ }
+ }
+ };
+ suggestion->requestId = session->api().request(
+ MTPmessages_ToggleSuggestedPostApproval(
+ MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag()),
+ item->history()->peer->input,
+ MTP_int(item->id.bare),
+ MTP_int(scheduleDate),
+ MTPstring()) // reject_comment
+ ).done([=](const MTPUpdates &result) {
+ session->api().applyUpdates(result);
+ finish();
+ }).fail([=](const MTP::Error &error) {
+ show->showToast(error.type());
+ finish();
+ }).send();
+}
+
+void ConfirmApproval(
+ std::shared_ptr show,
+ not_null item,
+ TimeId scheduleDate = 0,
+ Fn accepted = nullptr) {
+ const auto suggestion = item->Get();
+ if (!suggestion
+ || suggestion->accepted
+ || suggestion->rejected
+ || suggestion->requestId) {
+ return;
+ }
+ const auto id = item->fullId();
+ const auto price = suggestion->price;
+ const auto admin = item->history()->amMonoforumAdmin();
+ if (!admin && !price.empty()) {
+ const auto credits = &item->history()->session().credits();
+ if (price.ton()) {
+ if (!credits->tonLoaded()) {
+ credits->tonLoad();
+ return;
+ } else if (price > credits->tonBalance()) {
+ const auto peer = item->history()->peer;
+ show->show(
+ Box(HistoryView::InsufficientTonBox, peer, price));
+ return;
+ }
+ } else {
+ if (!credits->loaded()) {
+ credits->load();
+ return;
+ } else if (price > credits->balance()) {
+ using namespace Settings;
+ const auto peer = item->history()->peer;
+ const auto broadcast = peer->monoforumBroadcast();
+ const auto broadcastId = (broadcast ? broadcast : peer)->id;
+ const auto done = [=](SmallBalanceResult result) {
+ if (result == SmallBalanceResult::Success
+ || result == SmallBalanceResult::Already) {
+ const auto item = peer->owner().message(id);
+ if (item) {
+ ConfirmApproval(
+ show,
+ item,
+ scheduleDate,
+ accepted);
+ }
+ }
+ };
+ MaybeRequestBalanceIncrease(
+ show,
+ int(base::SafeRound(price.value())),
+ SmallBalanceForSuggest{ broadcastId },
+ done);
+ return;
+ }
+ }
+ }
+ const auto peer = item->history()->peer;
+ const auto session = &peer->session();
+ const auto broadcast = peer->monoforumBroadcast();
+ const auto channelName = (broadcast ? broadcast : peer)->name();
+ const auto amount = admin
+ ? HistoryView::PriceAfterCommission(session, price)
+ : price;
+ const auto commission = HistoryView::FormatAfterCommissionPercent(
+ session,
+ price);
+ const auto date = langDateTime(base::unixtime::parse(scheduleDate));
+ show->show(Box([=](not_null box) {
+ const auto callback = std::make_shared>();
+ auto text = admin
+ ? tr::lng_suggest_accept_text(
+ tr::now,
+ lt_from,
+ Ui::Text::Bold(item->from()->shortName()),
+ Ui::Text::WithEntities)
+ : tr::lng_suggest_accept_text_to(
+ tr::now,
+ lt_channel,
+ Ui::Text::Bold(channelName),
+ Ui::Text::WithEntities);
+ if (price) {
+ text.append("\n\n").append(admin
+ ? (scheduleDate
+ ? (amount.stars()
+ ? tr::lng_suggest_accept_receive_stars
+ : tr::lng_suggest_accept_receive_ton)(
+ tr::now,
+ lt_count_decimal,
+ amount.value(),
+ lt_channel,
+ Ui::Text::Bold(channelName),
+ lt_percent,
+ TextWithEntities{ commission },
+ lt_date,
+ Ui::Text::Bold(date),
+ Ui::Text::RichLangValue)
+ : (amount.stars()
+ ? tr::lng_suggest_accept_receive_now_stars
+ : tr::lng_suggest_accept_receive_now_ton)(
+ tr::now,
+ lt_count_decimal,
+ amount.value(),
+ lt_channel,
+ Ui::Text::Bold(channelName),
+ lt_percent,
+ TextWithEntities{ commission },
+ Ui::Text::RichLangValue))
+ : (scheduleDate
+ ? (amount.stars()
+ ? tr::lng_suggest_accept_pay_stars
+ : tr::lng_suggest_accept_pay_ton)(
+ tr::now,
+ lt_count_decimal,
+ amount.value(),
+ lt_date,
+ Ui::Text::Bold(date),
+ Ui::Text::RichLangValue)
+ : (amount.stars()
+ ? tr::lng_suggest_accept_pay_now_stars
+ : tr::lng_suggest_accept_pay_now_ton)(
+ tr::now,
+ lt_count_decimal,
+ amount.value(),
+ Ui::Text::RichLangValue)));
+ if (admin) {
+ text.append(' ').append(
+ tr::lng_suggest_accept_receive_if(
+ tr::now,
+ Ui::Text::RichLangValue));
+ if (price.stars()) {
+ text.append("\n\n").append(
+ tr::lng_suggest_options_stars_warning(
+ tr::now,
+ Ui::Text::RichLangValue));
+ }
+ }
+ }
+ Ui::ConfirmBox(box, {
+ .text = text,
+ .confirmed = [=](Fn close) { (*callback)(); close(); },
+ .confirmText = tr::lng_suggest_accept_send(),
+ .title = tr::lng_suggest_accept_title(),
+ });
+ *callback = [=, weak = Ui::MakeWeak(box)] {
+ if (const auto onstack = accepted) {
+ onstack();
+ }
+ const auto item = show->session().data().message(id);
+ if (!item) {
+ return;
+ }
+ SendApproval(show, item, scheduleDate);
+ if (const auto strong = weak.data()) {
+ strong->closeBox();
+ }
+ };
+ }));
+}
+
+void SendDecline(
+ std::shared_ptr show,
+ not_null item,
+ const QString &comment) {
+ using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;
+ const auto suggestion = item->Get();
+ if (!suggestion
+ || suggestion->accepted
+ || suggestion->rejected
+ || suggestion->requestId) {
+ return;
+ }
+
+ const auto id = item->fullId();
+ const auto session = &show->session();
+ const auto finish = [=] {
+ if (const auto item = session->data().message(id)) {
+ const auto suggestion = item->Get();
+ if (suggestion) {
+ suggestion->requestId = 0;
+ }
+ }
+ };
+ suggestion->requestId = session->api().request(
+ MTPmessages_ToggleSuggestedPostApproval(
+ MTP_flags(Flag::f_reject
+ | (comment.isEmpty() ? Flag() : Flag::f_reject_comment)),
+ item->history()->peer->input,
+ MTP_int(item->id.bare),
+ MTPint(), // schedule_date
+ MTP_string(comment))
+ ).done([=](const MTPUpdates &result) {
+ session->api().applyUpdates(result);
+ finish();
+ }).fail([=](const MTP::Error &error) {
+ show->showToast(error.type());
+ finish();
+ }).send();
+}
+
+void RequestApprovalDate(
+ std::shared_ptr show,
+ not_null item) {
+ const auto id = item->fullId();
+ const auto weak = std::make_shared>();
+ const auto close = [=] {
+ if (const auto strong = weak->data()) {
+ strong->closeBox();
+ }
+ };
+ const auto done = [=](TimeId result) {
+ if (const auto item = show->session().data().message(id)) {
+ ConfirmApproval(show, item, result, close);
+ } else {
+ close();
+ }
+ };
+ using namespace HistoryView;
+ auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
+ .session = &show->session(),
+ .done = done,
+ .mode = SuggestMode::Publish,
+ });
+ *weak = dateBox.data();
+ show->show(std::move(dateBox));
+}
+
+void RequestDeclineComment(
+ std::shared_ptr show,
+ not_null item) {
+ const auto id = item->fullId();
+ const auto admin = item->history()->amMonoforumAdmin();
+ const auto peer = item->history()->peer;
+ const auto broadcast = peer->monoforumBroadcast();
+ const auto channelName = (broadcast ? broadcast : peer)->name();
+ show->show(Box([=](not_null box) {
+ const auto callback = std::make_shared>();
+ Ui::ConfirmBox(box, {
+ .text = (admin
+ ? tr::lng_suggest_decline_text(
+ lt_from,
+ rpl::single(Ui::Text::Bold(item->from()->shortName())),
+ Ui::Text::WithEntities)
+ : tr::lng_suggest_decline_text_to(
+ lt_channel,
+ rpl::single(Ui::Text::Bold(channelName)),
+ Ui::Text::WithEntities)),
+ .confirmed = [=](Fn close) { (*callback)(); close(); },
+ .confirmText = tr::lng_suggest_action_decline(),
+ .confirmStyle = &st::attentionBoxButton,
+ .title = tr::lng_suggest_decline_title(),
+ });
+ const auto reason = box->addRow(object_ptr(
+ box,
+ st::factcheckField,
+ Ui::InputField::Mode::NoNewlines,
+ tr::lng_suggest_decline_reason()));
+ box->setFocusCallback([=] {
+ reason->setFocusFast();
+ });
+ *callback = [=, weak = Ui::MakeWeak(box)] {
+ const auto item = show->session().data().message(id);
+ if (!item) {
+ return;
+ }
+ SendDecline(show, item, reason->getLastText().trimmed());
+ if (const auto strong = weak.data()) {
+ strong->closeBox();
+ }
+ };
+ reason->submits(
+ ) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) {
+ if (!(modifiers & Qt::ShiftModifier)) {
+ (*callback)();
+ }
+ }, box->lifetime());
+ }));
+}
+
+struct SendSuggestState {
+ SendPaymentHelper sendPayment;
+};
+void SendSuggest(
+ std::shared_ptr show,
+ not_null item,
+ std::shared_ptr state,
+ Fn modify,
+ Fn done = nullptr,
+ int starsApproved = 0) {
+ const auto suggestion = item->Get();
+ const auto id = item->fullId();
+ const auto withPaymentApproved = [=](int stars) {
+ if (const auto item = show->session().data().message(id)) {
+ SendSuggest(show, item, state, modify, done, stars);
+ }
+ };
+ const auto isForward = item->Get();
+ auto action = SendAction(item->history());
+ action.options.suggest.exists = 1;
+ if (suggestion) {
+ action.options.suggest.date = suggestion->date;
+ action.options.suggest.priceWhole = suggestion->price.whole();
+ action.options.suggest.priceNano = suggestion->price.nano();
+ action.options.suggest.ton = suggestion->price.ton() ? 1 : 0;
+ }
+ modify(action.options.suggest);
+ action.options.starsApproved = starsApproved;
+ action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin()
+ ? item->sublistPeerId()
+ : PeerId();
+ action.replyTo.messageId = item->fullId();
+
+ const auto checked = state->sendPayment.check(
+ show,
+ item->history()->peer,
+ action.options,
+ 1,
+ withPaymentApproved);
+ if (!checked) {
+ return;
+ }
+
+ show->session().api().sendAction(action);
+ show->session().api().forwardMessages({
+ .items = { item },
+ .options = (isForward
+ ? Data::ForwardOptions::PreserveInfo
+ : Data::ForwardOptions::NoSenderNames),
+ }, action);
+ if (const auto onstack = done) {
+ onstack();
+ }
+}
+
+void SuggestApprovalDate(
+ std::shared_ptr show,
+ not_null item) {
+ const auto suggestion = item->Get();
+ if (!suggestion) {
+ return;
+ }
+ const auto id = item->fullId();
+ const auto state = std::make_shared();
+ const auto weak = std::make_shared>();
+ const auto done = [=](TimeId result) {
+ const auto item = show->session().data().message(id);
+ if (!item) {
+ return;
+ }
+ const auto close = [=] {
+ if (const auto strong = weak->data()) {
+ strong->closeBox();
+ }
+ };
+ SendSuggest(
+ show,
+ item,
+ state,
+ [=](SuggestPostOptions &options) { options.date = result; },
+ close);
+ };
+ using namespace HistoryView;
+ auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
+ .session = &show->session(),
+ .done = done,
+ .value = suggestion->date,
+ .mode = SuggestMode::Change,
+ });
+ *weak = dateBox.data();
+ show->show(std::move(dateBox));
+}
+
+void SuggestOfferForMessage(
+ std::shared_ptr show,
+ not_null item,
+ SuggestPostOptions values,
+ HistoryView::SuggestMode mode) {
+ const auto id = item->fullId();
+ const auto state = std::make_shared();
+ const auto weak = std::make_shared>();
+ const auto done = [=](SuggestPostOptions result) {
+ const auto item = show->session().data().message(id);
+ if (!item) {
+ return;
+ }
+ const auto close = [=] {
+ if (const auto strong = weak->data()) {
+ strong->closeBox();
+ }
+ };
+ SendSuggest(
+ show,
+ item,
+ state,
+ [=](SuggestPostOptions &options) { options = result; },
+ close);
+ };
+ using namespace HistoryView;
+ auto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{
+ .peer = item->history()->peer,
+ .done = done,
+ .value = values,
+ .mode = mode,
+ });
+ *weak = priceBox.data();
+ show->show(std::move(priceBox));
+}
+
+void SuggestApprovalPrice(
+ std::shared_ptr show,
+ not_null item) {
+ const auto suggestion = item->Get();
+ if (!suggestion) {
+ return;
+ }
+ using namespace HistoryView;
+ SuggestOfferForMessage(show, item, {
+ .exists = uint32(1),
+ .priceWhole = uint32(suggestion->price.whole()),
+ .priceNano = uint32(suggestion->price.nano()),
+ .ton = uint32(suggestion->price.ton() ? 1 : 0),
+ .date = suggestion->date,
+ }, SuggestMode::Change);
+}
+
+} // namespace
+
+std::shared_ptr AcceptClickHandler(
+ not_null item) {
+ const auto session = &item->history()->session();
+ const auto id = item->fullId();
+ return std::make_shared([=](ClickContext context) {
+ const auto my = context.other.value();
+ const auto controller = my.sessionWindow.get();
+ if (!controller || &controller->session() != session) {
+ return;
+ }
+ const auto item = session->data().message(id);
+ if (!item) {
+ return;
+ }
+ const auto show = controller->uiShow();
+ const auto suggestion = item->Get();
+ if (!suggestion) {
+ return;
+ } else if (!suggestion->date) {
+ RequestApprovalDate(show, item);
+ } else {
+ ConfirmApproval(show, item);
+ }
+ });
+}
+
+std::shared_ptr DeclineClickHandler(
+ not_null item) {
+ const auto session = &item->history()->session();
+ const auto id = item->fullId();
+ return std::make_shared([=](ClickContext context) {
+ const auto my = context.other.value();
+ const auto controller = my.sessionWindow.get();
+ if (!controller || &controller->session() != session) {
+ return;
+ }
+ const auto item = session->data().message(id);
+ if (!item) {
+ return;
+ }
+ RequestDeclineComment(controller->uiShow(), item);
+ });
+}
+
+std::shared_ptr SuggestChangesClickHandler(
+ not_null item) {
+ const auto session = &item->history()->session();
+ const auto id = item->fullId();
+ return std::make_shared([=](ClickContext context) {
+ const auto my = context.other.value();
+ const auto window = my.sessionWindow.get();
+ if (!window || &window->session() != session) {
+ return;
+ }
+ const auto item = session->data().message(id);
+ if (!item) {
+ return;
+ }
+ const auto menu = Ui::CreateChild(
+ window->widget(),
+ st::popupMenuWithIcons);
+ if (HistoryView::CanEditSuggestedMessage(item)) {
+ menu->addAction(tr::lng_suggest_menu_edit_message(tr::now), [=] {
+ const auto item = session->data().message(id);
+ if (!item) {
+ return;
+ }
+ const auto suggestion = item->Get();
+ if (!suggestion) {
+ return;
+ }
+ const auto history = item->history();
+ const auto editData = PrepareEditText(item);
+ const auto cursor = MessageCursor{
+ int(editData.text.size()),
+ int(editData.text.size()),
+ Ui::kQFixedMax
+ };
+ const auto monoforumPeerId = history->amMonoforumAdmin()
+ ? item->sublistPeerId()
+ : PeerId();
+ const auto previewDraft = Data::WebPageDraft::FromItem(item);
+ history->setLocalEditDraft(std::make_unique(
+ editData,
+ FullReplyTo{
+ .messageId = FullMsgId(history->peer->id, item->id),
+ .monoforumPeerId = monoforumPeerId,
+ },
+ SuggestPostOptions{
+ .exists = uint32(1),
+ .priceWhole = uint32(suggestion->price.whole()),
+ .priceNano = uint32(suggestion->price.nano()),
+ .ton = uint32(suggestion->price.ton() ? 1 : 0),
+ .date = suggestion->date,
+ },
+ cursor,
+ previewDraft));
+ history->session().changes().entryUpdated(
+ (monoforumPeerId
+ ? item->savedSublist()
+ : (Data::Thread*)history.get()),
+ Data::EntryUpdate::Flag::LocalDraftSet);
+ }, &st::menuIconEdit);
+ }
+ menu->addAction(tr::lng_suggest_menu_edit_price(tr::now), [=] {
+ if (const auto item = session->data().message(id)) {
+ SuggestApprovalPrice(window->uiShow(), item);
+ }
+ }, &st::menuIconTagSell);
+ menu->addAction(tr::lng_suggest_menu_edit_time(tr::now), [=] {
+ if (const auto item = session->data().message(id)) {
+ SuggestApprovalDate(window->uiShow(), item);
+ }
+ }, &st::menuIconSchedule);
+ menu->popup(QCursor::pos());
+ });
+}
+
+void AddOfferToMessage(
+ std::shared_ptr show,
+ FullMsgId itemId) {
+ const auto session = &show->session();
+ const auto item = session->data().message(itemId);
+ if (!item || !HistoryView::CanAddOfferToMessage(item)) {
+ return;
+ }
+ SuggestOfferForMessage(show, item, {}, HistoryView::SuggestMode::New);
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_suggest_post.h b/Telegram/SourceFiles/api/api_suggest_post.h
new file mode 100644
index 0000000000..582f0adca0
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_suggest_post.h
@@ -0,0 +1,29 @@
+/*
+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 ClickHandler;
+
+namespace Main {
+class SessionShow;
+} // namespace Main
+
+namespace Api {
+
+[[nodiscard]] std::shared_ptr AcceptClickHandler(
+ not_null item);
+[[nodiscard]] std::shared_ptr DeclineClickHandler(
+ not_null item);
+[[nodiscard]] std::shared_ptr SuggestChangesClickHandler(
+ not_null item);
+
+void AddOfferToMessage(
+ std::shared_ptr show,
+ FullMsgId itemId);
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp
new file mode 100644
index 0000000000..c65001f671
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_todo_lists.cpp
@@ -0,0 +1,257 @@
+/*
+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_todo_lists.h"
+
+#include "api/api_editing.h"
+#include "apiwrap.h"
+#include "base/random.h"
+#include "data/business/data_shortcut_messages.h" // ShortcutIdToMTP
+#include "data/data_changes.h"
+#include "data/data_histories.h"
+#include "data/data_todo_list.h"
+#include "data/data_session.h"
+#include "history/history.h"
+#include "history/history_item.h"
+#include "history/history_item_helpers.h" // ShouldSendSilent
+#include "main/main_session.h"
+
+namespace Api {
+namespace {
+
+constexpr auto kSendTogglesDelay = 3 * crl::time(1000);
+
+[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId) {
+ return TimeId(msgId >> 32);
+}
+
+} // namespace
+
+TodoLists::TodoLists(not_null api)
+: _session(&api->session())
+, _api(&api->instance())
+, _sendTimer([=] { sendAccumulatedToggles(false); }) {
+}
+
+void TodoLists::create(
+ const TodoListData &data,
+ SendAction action,
+ Fn done,
+ Fn fail) {
+ _session->api().sendAction(action);
+
+ const auto history = action.history;
+ const auto peer = history->peer;
+ const auto topicRootId = action.replyTo.messageId
+ ? action.replyTo.topicRootId
+ : 0;
+ const auto monoforumPeerId = action.replyTo.monoforumPeerId;
+ auto sendFlags = MTPmessages_SendMedia::Flags(0);
+ if (action.replyTo) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
+ }
+ const auto clearCloudDraft = action.clearDraft;
+ if (clearCloudDraft) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
+ history->clearLocalDraft(topicRootId, monoforumPeerId);
+ history->clearCloudDraft(topicRootId, monoforumPeerId);
+ history->startSavingCloudDraft(topicRootId, monoforumPeerId);
+ }
+ const auto silentPost = ShouldSendSilent(peer, action.options);
+ const auto starsPaid = std::min(
+ peer->starsPerMessageChecked(),
+ action.options.starsApproved);
+ if (silentPost) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
+ }
+ if (action.options.scheduled) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
+ }
+ if (action.options.shortcutId) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
+ }
+ if (action.options.effectId) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
+ }
+ if (action.options.suggest) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
+ }
+ if (starsPaid) {
+ action.options.starsApproved -= starsPaid;
+ sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
+ }
+ const auto sendAs = action.options.sendAs;
+ if (sendAs) {
+ sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
+ }
+ auto &histories = history->owner().histories();
+ const auto randomId = base::RandomValue();
+ histories.sendPreparedMessage(
+ history,
+ action.replyTo,
+ randomId,
+ Data::Histories::PrepareMessage(
+ MTP_flags(sendFlags),
+ peer->input,
+ Data::Histories::ReplyToPlaceholder(),
+ TodoListDataToInputMedia(&data),
+ MTP_string(),
+ MTP_long(randomId),
+ MTPReplyMarkup(),
+ MTPVector(),
+ MTP_int(action.options.scheduled),
+ (sendAs ? sendAs->input : MTP_inputPeerEmpty()),
+ Data::ShortcutIdToMTP(_session, action.options.shortcutId),
+ MTP_long(action.options.effectId),
+ MTP_long(starsPaid),
+ SuggestToMTP(action.options.suggest)
+ ), [=](const MTPUpdates &result, const MTP::Response &response) {
+ if (clearCloudDraft) {
+ history->finishSavingCloudDraft(
+ topicRootId,
+ monoforumPeerId,
+ UnixtimeFromMsgId(response.outerMsgId));
+ }
+ _session->changes().historyUpdated(
+ history,
+ (action.options.scheduled
+ ? Data::HistoryUpdate::Flag::ScheduledSent
+ : Data::HistoryUpdate::Flag::MessageSent));
+ if (const auto onstack = done) {
+ onstack();
+ }
+ }, [=](const MTP::Error &error, const MTP::Response &response) {
+ if (clearCloudDraft) {
+ history->finishSavingCloudDraft(
+ topicRootId,
+ monoforumPeerId,
+ UnixtimeFromMsgId(response.outerMsgId));
+ }
+ if (const auto onstack = fail) {
+ onstack(error.type());
+ }
+ });
+}
+
+void TodoLists::edit(
+ not_null item,
+ const TodoListData &data,
+ SendOptions options,
+ Fn done,
+ Fn fail) {
+ EditTodoList(item, data, options, [=](mtpRequestId) {
+ if (const auto onstack = done) {
+ onstack();
+ }
+ }, [=](const QString &error, mtpRequestId) {
+ if (const auto onstack = fail) {
+ onstack(error);
+ }
+ });
+}
+
+void TodoLists::add(
+ not_null item,
+ const std::vector &items,
+ Fn done,
+ Fn fail) {
+ if (items.empty()) {
+ return;
+ }
+ const auto session = _session;
+ _session->api().request(MTPmessages_AppendTodoList(
+ item->history()->peer->input,
+ MTP_int(item->id.bare),
+ TodoListItemsToMTP(&item->history()->session(), items)
+ )).done([=](const MTPUpdates &result) {
+ session->api().applyUpdates(result);
+ if (const auto onstack = done) {
+ onstack();
+ }
+ }).fail([=](const MTP::Error &error) {
+ if (const auto onstack = fail) {
+ onstack(error.type());
+ }
+ }).send();
+}
+
+void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) {
+ auto &entry = _toggles[itemId];
+ if (completed) {
+ const auto changed1 = entry.completed.emplace(id).second;
+ const auto changed2 = entry.incompleted.remove(id);
+ if (!changed1 && !changed2) {
+ return;
+ }
+ } else {
+ const auto changed1 = entry.incompleted.emplace(id).second;
+ const auto changed2 = entry.completed.remove(id);
+ if (!changed1 && !changed2) {
+ return;
+ }
+ }
+ entry.scheduled = crl::now();
+ if (!entry.requestId && !_sendTimer.isActive()) {
+ _sendTimer.callOnce(kSendTogglesDelay);
+ }
+}
+
+void TodoLists::sendAccumulatedToggles(bool force) {
+ const auto now = crl::now();
+ auto nearest = crl::time(0);
+ for (auto &[itemId, entry] : _toggles) {
+ if (entry.requestId) {
+ continue;
+ }
+ const auto wait = entry.scheduled + kSendTogglesDelay - now;
+ if (wait <= 0) {
+ entry.scheduled = 0;
+ send(itemId, entry);
+ } else if (!nearest || nearest > wait) {
+ nearest = wait;
+ }
+ }
+ if (nearest > 0) {
+ _sendTimer.callOnce(nearest);
+ }
+}
+
+void TodoLists::send(FullMsgId itemId, Accumulated &entry) {
+ const auto item = _session->data().message(itemId);
+ if (!item) {
+ return;
+ }
+ auto completed = entry.completed
+ | ranges::views::transform([](int id) { return MTP_int(id); });
+ auto incompleted = entry.incompleted
+ | ranges::views::transform([](int id) { return MTP_int(id); });
+ entry.requestId = _api.request(MTPmessages_ToggleTodoCompleted(
+ item->history()->peer->input,
+ MTP_int(item->id),
+ MTP_vector_from_range(completed),
+ MTP_vector_from_range(incompleted)
+ )).done([=](const MTPUpdates &result) {
+ _session->api().applyUpdates(result);
+ finishRequest(itemId);
+ }).fail([=](const MTP::Error &error) {
+ finishRequest(itemId);
+ }).send();
+ entry.completed.clear();
+ entry.incompleted.clear();
+}
+
+void TodoLists::finishRequest(FullMsgId itemId) {
+ auto &entry = _toggles[itemId];
+ entry.requestId = 0;
+ if (entry.completed.empty() && entry.incompleted.empty()) {
+ _toggles.remove(itemId);
+ } else {
+ sendAccumulatedToggles(false);
+ }
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_todo_lists.h b/Telegram/SourceFiles/api/api_todo_lists.h
new file mode 100644
index 0000000000..92d6a634b2
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_todo_lists.h
@@ -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
+*/
+#pragma once
+
+#include "base/timer.h"
+#include "mtproto/sender.h"
+
+class ApiWrap;
+class HistoryItem;
+struct TodoListItem;
+struct TodoListData;
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Api {
+
+struct SendAction;
+struct SendOptions;
+
+class TodoLists final {
+public:
+ explicit TodoLists(not_null api);
+
+ void create(
+ const TodoListData &data,
+ SendAction action,
+ Fn done,
+ Fn fail);
+ void edit(
+ not_null item,
+ const TodoListData &data,
+ SendOptions options,
+ Fn done,
+ Fn fail);
+ void add(
+ not_null item,
+ const std::vector &items,
+ Fn done,
+ Fn fail);
+ void toggleCompletion(FullMsgId itemId, int id, bool completed);
+
+private:
+ struct Accumulated {
+ base::flat_set completed;
+ base::flat_set incompleted;
+ crl::time scheduled = 0;
+ mtpRequestId requestId = 0;
+ };
+
+ void sendAccumulatedToggles(bool force);
+ void send(FullMsgId itemId, Accumulated &entry);
+ void finishRequest(FullMsgId itemId);
+
+ const not_null _session;
+ MTP::Sender _api;
+
+ base::flat_map _toggles;
+ base::Timer _sendTimer;
+
+};
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_unread_things.cpp b/Telegram/SourceFiles/api/api_unread_things.cpp
index ba1359caba..1945e52738 100644
--- a/Telegram/SourceFiles/api/api_unread_things.cpp
+++ b/Telegram/SourceFiles/api/api_unread_things.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_peer.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
+#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "history/history.h"
@@ -35,7 +36,9 @@ UnreadThings::UnreadThings(not_null api) : _api(api) {
bool UnreadThings::trackMentions(Data::Thread *thread) const {
const auto peer = thread ? thread->peer().get() : nullptr;
- return peer && (peer->isChat() || peer->isMegagroup());
+ return peer
+ && (peer->isChat() || peer->isMegagroup())
+ && !peer->isMonoforum();
}
bool UnreadThings::trackReactions(Data::Thread *thread) const {
@@ -107,7 +110,7 @@ void UnreadThings::cancelRequests(not_null thread) {
void UnreadThings::requestMentions(
not_null thread,
int loaded) {
- if (_mentionsRequests.contains(thread)) {
+ if (_mentionsRequests.contains(thread) || thread->asSublist()) {
return;
}
const auto offsetId = std::max(
@@ -152,12 +155,15 @@ void UnreadThings::requestReactions(
const auto maxId = 0;
const auto minId = 0;
const auto history = thread->owningHistory();
+ const auto sublist = thread->asSublist();
const auto topic = thread->asTopic();
using Flag = MTPmessages_GetUnreadReactions::Flag;
const auto requestId = _api->request(MTPmessages_GetUnreadReactions(
- MTP_flags(topic ? Flag::f_top_msg_id : Flag()),
+ MTP_flags((topic ? Flag::f_top_msg_id : Flag())
+ | (sublist ? Flag::f_saved_peer_id : Flag())),
history->peer->input,
MTP_int(topic ? topic->rootId() : 0),
+ (sublist ? sublist->sublistPeer()->input : MTPInputPeer()),
MTP_int(offsetId),
MTP_int(addOffset),
MTP_int(limit),
diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp
index 5bac3c0aab..1a5d0b9f6e 100644
--- a/Telegram/SourceFiles/api/api_updates.cpp
+++ b/Telegram/SourceFiles/api/api_updates.cpp
@@ -1236,7 +1236,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPlong(), // effect
MTPFactCheck(),
MTPint(), // report_delivery_until_date
- MTPlong()), // paid_message_stars
+ MTPlong(), // paid_message_stars
+ MTPSuggestedPost()),
MessageFlags(),
NewMessageType::Unread);
} break;
@@ -1275,7 +1276,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) {
MTPlong(), // effect
MTPFactCheck(),
MTPint(), // report_delivery_until_date
- MTPlong()), // paid_message_stars
+ MTPlong(), // paid_message_stars
+ MTPSuggestedPost()),
MessageFlags(),
NewMessageType::Unread);
} break;
@@ -1924,7 +1926,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
// Update web page anyway.
session().data().processWebpage(d.vwebpage());
- session().data().sendWebPageGamePollNotifications();
+ session().data().sendWebPageGamePollTodoListNotifications();
updateAndApply(d.vpts().v, d.vpts_count().v, update);
} break;
@@ -1934,7 +1936,7 @@ void Updates::feedUpdate(const MTPUpdate &update) {
// Update web page anyway.
session().data().processWebpage(d.vwebpage());
- session().data().sendWebPageGamePollNotifications();
+ session().data().sendWebPageGamePollTodoListNotifications();
auto channel = session().data().channelLoaded(d.vchannel_id());
if (channel && !_handlingChannelDifference) {
@@ -2450,6 +2452,32 @@ void Updates::feedUpdate(const MTPUpdate &update) {
session().data().updateRepliesReadTill({ id, readTillId, true });
} break;
+ case mtpc_updateReadMonoForumInbox: {
+ const auto &d = update.c_updateReadMonoForumInbox();
+ const auto parentChatId = ChannelId(d.vchannel_id());
+ const auto sublistPeerId = peerFromMTP(d.vsaved_peer_id());
+ const auto readTillId = d.vread_max_id().v;
+ session().data().updateSublistReadTill({
+ parentChatId,
+ sublistPeerId,
+ readTillId,
+ false,
+ });
+ } break;
+
+ case mtpc_updateReadMonoForumOutbox: {
+ const auto &d = update.c_updateReadMonoForumOutbox();
+ const auto parentChatId = ChannelId(d.vchannel_id());
+ const auto sublistPeerId = peerFromMTP(d.vsaved_peer_id());
+ const auto readTillId = d.vread_max_id().v;
+ session().data().updateSublistReadTill({
+ parentChatId,
+ sublistPeerId,
+ readTillId,
+ true,
+ });
+ } break;
+
case mtpc_updateChannelAvailableMessages: {
auto &d = update.c_updateChannelAvailableMessages();
if (const auto channel = session().data().channelLoaded(d.vchannel_id())) {
@@ -2669,13 +2697,22 @@ void Updates::feedUpdate(const MTPUpdate &update) {
const auto &data = update.c_updateDraftMessage();
const auto peerId = peerFromMTP(data.vpeer());
const auto topicRootId = data.vtop_msg_id().value_or_empty();
+ const auto monoforumPeerId = data.vsaved_peer_id()
+ ? peerFromMTP(*data.vsaved_peer_id())
+ : PeerId();
data.vdraft().match([&](const MTPDdraftMessage &data) {
- Data::ApplyPeerCloudDraft(&session(), peerId, topicRootId, data);
+ Data::ApplyPeerCloudDraft(
+ &session(),
+ peerId,
+ topicRootId,
+ monoforumPeerId,
+ data);
}, [&](const MTPDdraftMessageEmpty &data) {
Data::ClearPeerCloudDraft(
&session(),
peerId,
topicRootId,
+ monoforumPeerId,
data.vdate().value_or_empty());
});
} break;
diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp
index 6149da6828..bb8aa13441 100644
--- a/Telegram/SourceFiles/api/api_who_reacted.cpp
+++ b/Telegram/SourceFiles/api/api_who_reacted.cpp
@@ -712,7 +712,8 @@ bool WhoReadExists(not_null item) {
const auto megagroup = peer->asMegagroup();
if ((!chat && !megagroup)
|| (megagroup
- && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))) {
+ && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))
+ || (megagroup && megagroup->isMonoforum())) {
return false;
}
const auto &appConfig = peer->session().appConfig();
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index e47ddf991d..2536944edc 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_polls.h"
#include "api/api_sending.h"
#include "api/api_text_entities.h"
+#include "api/api_todo_lists.h"
#include "api/api_self_destruct.h"
#include "api/api_sensitive_content.h"
#include "api/api_global_privacy.h"
@@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_folder.h"
#include "data/data_forum_topic.h"
#include "data/data_forum.h"
+#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_search_controller.h"
#include "data/data_session.h"
@@ -184,6 +186,7 @@ ApiWrap::ApiWrap(not_null session)
, _confirmPhone(std::make_unique(this))
, _peerPhoto(std::make_unique(this))
, _polls(std::make_unique(this))
+, _todoLists(std::make_unique(this))
, _chatParticipants(std::make_unique(this))
, _unreadThings(std::make_unique(this))
, _ringtones(std::make_unique(this))
@@ -328,7 +331,7 @@ void ApiWrap::checkChatInvite(
request(base::take(_checkInviteRequestId)).cancel();
_checkInviteRequestId = request(MTPmessages_CheckChatInvite(
MTP_string(hash)
- )).done(std::move(done)).fail(std::move(fail)).send();
+ )).done(std::move(done)).fail(std::move(fail)).handleFloodErrors().send();
}
void ApiWrap::checkFilterInvite(
@@ -388,10 +391,13 @@ void ApiWrap::savePinnedOrder(not_null forum) {
}
void ApiWrap::savePinnedOrder(not_null saved) {
+ if (saved->parentChat()) {
+ return;
+ }
const auto &order = _session->data().pinnedChatsOrder(saved);
const auto input = [](Dialogs::Key key) {
if (const auto sublist = key.sublist()) {
- return MTP_inputDialogPeer(sublist->peer()->input);
+ return MTP_inputDialogPeer(sublist->sublistPeer()->input);
}
Unexpected("Key type in pinnedDialogsOrder().");
};
@@ -1404,6 +1410,32 @@ void ApiWrap::deleteAllFromParticipantSend(
}).send();
}
+void ApiWrap::deleteSublistHistory(
+ not_null channel,
+ not_null sublistPeer) {
+ deleteSublistHistorySend(channel, sublistPeer);
+}
+
+void ApiWrap::deleteSublistHistorySend(
+ not_null parentChat,
+ not_null sublistPeer) {
+ request(MTPmessages_DeleteSavedHistory(
+ MTP_flags(MTPmessages_DeleteSavedHistory::Flag::f_parent_peer),
+ parentChat->input,
+ sublistPeer->input,
+ MTP_int(0), // max_id
+ MTP_int(0), // min_date
+ MTP_int(0) // max_date
+ )).done([=](const MTPmessages_AffectedHistory &result) {
+ const auto offset = applyAffectedHistory(parentChat, result);
+ if (offset > 0) {
+ deleteSublistHistorySend(parentChat, sublistPeer);
+ } else if (const auto monoforum = parentChat->monoforum()) {
+ monoforum->applySublistDeleted(sublistPeer);
+ }
+ }).send();
+}
+
void ApiWrap::scheduleStickerSetRequest(uint64 setId, uint64 access) {
if (!_stickerSetRequests.contains(setId)) {
_stickerSetRequests.emplace(setId, StickerSetRequest{ access });
@@ -2099,8 +2131,13 @@ void ApiWrap::saveCurrentDraftToCloud() {
_session->local().writeDrafts(history);
const auto topicRootId = thread->topicRootId();
- const auto localDraft = history->localDraft(topicRootId);
- const auto cloudDraft = history->cloudDraft(topicRootId);
+ const auto monoforumPeerId = thread->monoforumPeerId();
+ const auto localDraft = history->localDraft(
+ topicRootId,
+ monoforumPeerId);
+ const auto cloudDraft = history->cloudDraft(
+ topicRootId,
+ monoforumPeerId);
if (!Data::DraftsAreEqual(localDraft, cloudDraft)
&& !_session->supportMode()) {
saveDraftToCloudDelayed(thread);
@@ -2123,15 +2160,22 @@ void ApiWrap::saveDraftsToCloud() {
const auto history = thread->owningHistory();
const auto topicRootId = thread->topicRootId();
- auto cloudDraft = history->cloudDraft(topicRootId);
- auto localDraft = history->localDraft(topicRootId);
+ const auto monoforumPeerId = thread->monoforumPeerId();
+ auto cloudDraft = history->cloudDraft(topicRootId, monoforumPeerId);
+ auto localDraft = history->localDraft(topicRootId, monoforumPeerId);
if (cloudDraft && cloudDraft->saveRequestId) {
request(base::take(cloudDraft->saveRequestId)).cancel();
}
if (!_session->supportMode()) {
- cloudDraft = history->createCloudDraft(topicRootId, localDraft);
+ cloudDraft = history->createCloudDraft(
+ topicRootId,
+ monoforumPeerId,
+ localDraft);
} else if (!cloudDraft) {
- cloudDraft = history->createCloudDraft(topicRootId, nullptr);
+ cloudDraft = history->createCloudDraft(
+ topicRootId,
+ monoforumPeerId,
+ nullptr);
}
auto flags = MTPmessages_SaveDraft::Flags(0);
@@ -2141,18 +2185,23 @@ void ApiWrap::saveDraftsToCloud() {
} else if (!cloudDraft->webpage.url.isEmpty()) {
flags |= MTPmessages_SaveDraft::Flag::f_media;
}
- if (cloudDraft->reply.messageId || cloudDraft->reply.topicRootId) {
+ if (cloudDraft->reply.messageId
+ || cloudDraft->reply.topicRootId
+ || cloudDraft->reply.monoforumPeerId) {
flags |= MTPmessages_SaveDraft::Flag::f_reply_to;
}
if (!textWithTags.tags.isEmpty()) {
flags |= MTPmessages_SaveDraft::Flag::f_entities;
}
+ if (cloudDraft->suggest) {
+ flags |= MTPmessages_SaveDraft::Flag::f_suggested_post;
+ }
auto entities = Api::EntitiesToMTP(
_session,
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags),
Api::ConvertOption::SkipLocal);
- history->startSavingCloudDraft(topicRootId);
+ history->startSavingCloudDraft(topicRootId, monoforumPeerId);
cloudDraft->saveRequestId = request(MTPmessages_SaveDraft(
MTP_flags(flags),
ReplyToForMTP(history, cloudDraft->reply),
@@ -2162,16 +2211,21 @@ void ApiWrap::saveDraftsToCloud() {
Data::WebPageForMTP(
cloudDraft->webpage,
textWithTags.text.isEmpty()),
- MTP_long(0) // effect
+ MTP_long(0), // effect
+ Api::SuggestToMTP(cloudDraft->suggest)
)).done([=](const MTPBool &result, const MTP::Response &response) {
const auto requestId = response.requestId;
history->finishSavingCloudDraft(
topicRootId,
+ monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
- if (const auto cloudDraft = history->cloudDraft(topicRootId)) {
+ const auto cloudDraft = history->cloudDraft(
+ topicRootId,
+ monoforumPeerId);
+ if (cloudDraft) {
if (cloudDraft->saveRequestId == requestId) {
cloudDraft->saveRequestId = 0;
- history->draftSavedToCloud(topicRootId);
+ history->draftSavedToCloud(topicRootId, monoforumPeerId);
}
}
const auto i = _draftsSaveRequestIds.find(weak);
@@ -2184,10 +2238,14 @@ void ApiWrap::saveDraftsToCloud() {
const auto requestId = response.requestId;
history->finishSavingCloudDraft(
topicRootId,
+ monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
- if (const auto cloudDraft = history->cloudDraft(topicRootId)) {
+ const auto cloudDraft = history->cloudDraft(
+ topicRootId,
+ monoforumPeerId);
+ if (cloudDraft) {
if (cloudDraft->saveRequestId == requestId) {
- history->clearCloudDraft(topicRootId);
+ history->clearCloudDraft(topicRootId, monoforumPeerId);
}
}
const auto i = _draftsSaveRequestIds.find(weak);
@@ -2561,7 +2619,10 @@ void ApiWrap::refreshFileReference(
});
}
-void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &result, mtpRequestId req) {
+void ApiWrap::gotWebPages(
+ ChannelData *channel,
+ const MTPmessages_Messages &result,
+ mtpRequestId req) {
WebPageData::ApplyChanges(_session, channel, result);
for (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) {
if (i->second == req) {
@@ -2575,7 +2636,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu
++i;
}
}
- _session->data().sendWebPageGamePollNotifications();
+ _session->data().sendWebPageGamePollTodoListNotifications();
}
void ApiWrap::updateStickers() {
@@ -2965,17 +3026,27 @@ void ApiWrap::resolveJumpToDate(
Fn, MsgId)> callback) {
if (const auto peer = chat.peer()) {
const auto topic = chat.topic();
- const auto rootId = topic ? topic->rootId() : 0;
- resolveJumpToHistoryDate(peer, rootId, date, std::move(callback));
+ const auto sublist = chat.sublist();
+ const auto rootId = topic ? topic->rootId() : MsgId();
+ const auto monoforumPeerId = sublist
+ ? sublist->sublistPeer()->id
+ : PeerId();
+ resolveJumpToHistoryDate(
+ peer,
+ rootId,
+ monoforumPeerId,
+ date,
+ std::move(callback));
}
}
template
void ApiWrap::requestMessageAfterDate(
- not_null peer,
- MsgId topicRootId,
- const QDate &date,
- Callback &&callback) {
+ not_null peer,
+ MsgId topicRootId,
+ PeerId monoforumPeerId,
+ const QDate &date,
+ Callback &&callback) {
// API returns a message with date <= offset_date.
// So we request a message with offset_date = desired_date - 1 and add_offset = -1.
// This should give us the first message with date >= desired_date.
@@ -2998,7 +3069,7 @@ void ApiWrap::requestMessageAfterDate(
return &messages.vmessages().v;
};
const auto list = result.match([&](
- const MTPDmessages_messages &data) {
+ const MTPDmessages_messages &data) {
return handleMessages(data);
}, [&](const MTPDmessages_messagesSlice &data) {
return handleMessages(data);
@@ -3041,6 +3112,18 @@ void ApiWrap::requestMessageAfterDate(
MTP_int(maxId),
MTP_int(minId),
MTP_long(historyHash)));
+ } else if (monoforumPeerId) {
+ send(MTPmessages_GetSavedHistory(
+ MTP_flags(MTPmessages_GetSavedHistory::Flag::f_parent_peer),
+ peer->input,
+ session().data().peer(monoforumPeerId)->input,
+ MTP_int(offsetId),
+ MTP_int(offsetDate),
+ MTP_int(addOffset),
+ MTP_int(limit),
+ MTP_int(maxId),
+ MTP_int(minId),
+ MTP_long(historyHash)));
} else {
send(MTPmessages_GetHistory(
peer->input,
@@ -3057,28 +3140,41 @@ void ApiWrap::requestMessageAfterDate(
void ApiWrap::resolveJumpToHistoryDate(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
const QDate &date,
Fn, MsgId)> callback) {
if (const auto channel = peer->migrateTo()) {
return resolveJumpToHistoryDate(
channel,
topicRootId,
+ monoforumPeerId,
date,
std::move(callback));
}
const auto jumpToDateInPeer = [=] {
- requestMessageAfterDate(peer, topicRootId, date, [=](MsgId itemId) {
- callback(peer, itemId);
- });
+ requestMessageAfterDate(
+ peer,
+ topicRootId,
+ monoforumPeerId,
+ date,
+ [=](MsgId itemId) { callback(peer, itemId); });
};
- if (const auto chat = topicRootId ? nullptr : peer->migrateFrom()) {
- requestMessageAfterDate(chat, 0, date, [=](MsgId itemId) {
- if (itemId) {
- callback(chat, itemId);
- } else {
- jumpToDateInPeer();
- }
- });
+ const auto migrated = (topicRootId || monoforumPeerId)
+ ? nullptr
+ : peer->migrateFrom();
+ if (migrated) {
+ requestMessageAfterDate(
+ migrated,
+ MsgId(),
+ PeerId(),
+ date,
+ [=](MsgId itemId) {
+ if (itemId) {
+ callback(migrated, itemId);
+ } else {
+ jumpToDateInPeer();
+ }
+ });
} else {
jumpToDateInPeer();
}
@@ -3127,12 +3223,14 @@ void ApiWrap::requestHistory(
void ApiWrap::requestSharedMedia(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
SharedMediaType type,
MsgId messageId,
SliceType slice) {
const auto key = SharedMediaRequest{
peer,
topicRootId,
+ monoforumPeerId,
type,
messageId,
slice,
@@ -3144,6 +3242,7 @@ void ApiWrap::requestSharedMedia(
const auto prepared = Api::PrepareSearchRequest(
peer,
topicRootId,
+ monoforumPeerId,
type,
QString(),
messageId,
@@ -3166,7 +3265,12 @@ void ApiWrap::requestSharedMedia(
messageId,
slice,
result);
- sharedMediaDone(peer, topicRootId, type, std::move(parsed));
+ sharedMediaDone(
+ peer,
+ topicRootId,
+ monoforumPeerId,
+ type,
+ std::move(parsed));
finish();
}).fail([=] {
_sharedMediaRequests.remove(key);
@@ -3179,16 +3283,19 @@ void ApiWrap::requestSharedMedia(
void ApiWrap::sharedMediaDone(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
SharedMediaType type,
Api::SearchResult &&parsed) {
const auto topic = peer->forumTopicFor(topicRootId);
- if (topicRootId && !topic) {
+ const auto sublist = peer->monoforumSublistFor(monoforumPeerId);
+ if ((topicRootId && !topic) || (monoforumPeerId && !sublist)) {
return;
}
const auto hasMessages = !parsed.messageIds.empty();
_session->storage().add(Storage::SharedMediaAddSlice(
peer->id,
topicRootId,
+ monoforumPeerId,
type,
std::move(parsed.messageIds),
parsed.noSkipRange,
@@ -3199,6 +3306,9 @@ void ApiWrap::sharedMediaDone(
if (topic) {
topic->setHasPinnedMessages(true);
}
+ if (sublist) {
+ sublist->setHasPinnedMessages(true);
+ }
}
}
@@ -3235,8 +3345,14 @@ void ApiWrap::sendAction(const SendAction &action) {
const auto topic = topicRootId
? action.history->peer->forumTopicFor(topicRootId)
: nullptr;
+ const auto monoforumPeerId = action.replyTo.monoforumPeerId;
+ const auto sublist = monoforumPeerId
+ ? action.history->peer->monoforumSublistFor(monoforumPeerId)
+ : nullptr;
if (topic) {
topic->readTillEnd();
+ } else if (sublist) {
+ sublist->readTillEnd();
} else {
_session->data().histories().readInbox(action.history);
}
@@ -3248,7 +3364,10 @@ void ApiWrap::sendAction(const SendAction &action) {
void ApiWrap::finishForwarding(const SendAction &action) {
const auto history = action.history;
const auto topicRootId = action.replyTo.topicRootId;
- auto toForward = history->resolveForwardDraft(topicRootId);
+ const auto monoforumPeerId = action.replyTo.monoforumPeerId;
+ auto toForward = history->resolveForwardDraft(
+ topicRootId,
+ monoforumPeerId);
if (!toForward.items.empty()) {
const auto error = GetErrorForSending(
history->peer,
@@ -3261,7 +3380,7 @@ void ApiWrap::finishForwarding(const SendAction &action) {
}
forwardMessages(std::move(toForward), action);
- history->setForwardDraft(topicRootId, {});
+ history->setForwardDraft(topicRootId, monoforumPeerId, {});
}
_session->data().sendHistoryChangeNotifications();
@@ -3346,6 +3465,9 @@ void ApiWrap::forwardMessages(
if (sendAs) {
sendFlags |= SendFlag::f_send_as;
}
+ if (action.options.suggest) {
+ sendFlags |= SendFlag::f_suggested_post;
+ }
const auto kGeneralId = Data::ForumTopic::kGeneralId;
const auto topicRootId = action.replyTo.topicRootId;
const auto topMsgId = (topicRootId == kGeneralId)
@@ -3354,6 +3476,13 @@ void ApiWrap::forwardMessages(
if (topMsgId) {
sendFlags |= SendFlag::f_top_msg_id;
}
+ const auto monoforumPeerId = action.replyTo.monoforumPeerId;
+ const auto monoforumPeer = monoforumPeerId
+ ? session().data().peer(monoforumPeerId).get()
+ : nullptr;
+ if (monoforumPeer || (action.options.suggest && action.replyTo)) {
+ sendFlags |= SendFlag::f_reply_to;
+ }
auto forwardFrom = draft.items.front()->history()->peer;
auto ids = QVector();
@@ -3383,11 +3512,17 @@ void ApiWrap::forwardMessages(
MTP_vector(randomIds),
peer->input,
MTP_int(topMsgId),
+ (action.options.suggest
+ ? ReplyToForMTP(history, action.replyTo)
+ : monoforumPeer
+ ? MTP_inputReplyToMonoForum(monoforumPeer->input)
+ : MTPInputReplyTo()),
MTP_int(action.options.scheduled),
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTPint(), // video_timestamp
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ Api::SuggestToMTP(action.options.suggest)
)).done([=](const MTPUpdates &result) {
if (!scheduled) {
this->updates().checkForSentToScheduled(result);
@@ -3436,12 +3571,15 @@ void ApiWrap::forwardMessages(
.id = newId.msg,
.flags = flags,
.from = NewMessageFromId(action),
- .replyTo = { .topicRootId = topMsgId },
+ .replyTo = {
+ .topicRootId = topMsgId,
+ .monoforumPeerId = monoforumPeerId,
+ },
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = action.options.starsApproved,
.postAuthor = NewMessagePostAuthor(action),
-
+ .suggest = HistoryMessageSuggestInfo(action.options),
// forwarded messages don't have effects
//.effectId = action.options.effectId,
}, item);
@@ -3536,6 +3674,7 @@ void ApiWrap::sendSharedContact(
.starsPaid = action.options.starsApproved,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
+ .suggest = HistoryMessageSuggestInfo(action.options),
}, TextWithEntities(), MTP_messageMediaContact(
MTP_string(phone),
MTP_string(firstName),
@@ -3584,7 +3723,19 @@ void ApiWrap::editMedia(
if (list.files.empty()) return;
auto &file = list.files.front();
- const auto to = FileLoadTaskOptions(action);
+ auto to = FileLoadTaskOptions(action);
+ const auto existing = to.replaceMediaOf
+ ? session().data().message(action.history->peer, to.replaceMediaOf)
+ : nullptr;
+ if (existing && existing->computeSuggestionActions()
+ == SuggestionActions::AcceptAndDecline) {
+ to.replyTo.messageId = {
+ action.history->peer->id,
+ to.replaceMediaOf
+ };
+ to.replyTo.monoforumPeerId = existing->sublistPeerId();
+ to.replaceMediaOf = MsgId();
+ }
_fileLoader->addTask(std::make_unique(
&session(),
file.path,
@@ -3774,6 +3925,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
const auto clearCloudDraft = action.clearDraft;
const auto draftTopicRootId = action.replyTo.topicRootId;
+ const auto draftMonoforumPeerId = action.replyTo.monoforumPeerId;
const auto replyTo = action.replyTo.messageId
? peer->owner().message(action.replyTo.messageId)
: nullptr;
@@ -3887,8 +4039,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
if (clearCloudDraft) {
sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft;
mediaFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
- history->clearCloudDraft(draftTopicRootId);
- history->startSavingCloudDraft(draftTopicRootId);
+ history->clearCloudDraft(draftTopicRootId, draftMonoforumPeerId);
+ history->startSavingCloudDraft(
+ draftTopicRootId,
+ draftMonoforumPeerId);
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
@@ -3909,6 +4063,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendFlags |= MTPmessages_SendMessage::Flag::f_effect;
mediaFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
+ if (action.options.suggest) {
+ sendFlags |= MTPmessages_SendMessage::Flag::f_suggested_post;
+ mediaFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
+ }
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
@@ -3927,6 +4085,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
+ .suggest = HistoryMessageSuggestInfo(action.options),
}, sending, media);
const auto done = [=](
const MTPUpdates &result,
@@ -3934,6 +4093,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
draftTopicRootId,
+ draftMonoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
@@ -3950,6 +4110,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
draftTopicRootId,
+ draftMonoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
};
@@ -3976,7 +4137,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
MTP_long(action.options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ Api::SuggestToMTP(action.options.suggest)
), done, fail);
} else {
histories.sendPreparedMessage(
@@ -3995,7 +4157,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
(sendAs ? sendAs->input : MTP_inputPeerEmpty()),
mtpShortcut,
MTP_long(action.options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ Api::SuggestToMTP(action.options.suggest)
), done, fail);
}
isFirst = false;
@@ -4076,6 +4239,7 @@ void ApiWrap::sendInlineResult(
const auto topicRootId = action.replyTo.messageId
? action.replyTo.topicRootId
: 0;
+ const auto monoforumPeerId = action.replyTo.monoforumPeerId;
using SendFlag = MTPmessages_SendInlineBotResult::Flag;
auto flags = NewMessageFlags(peer);
@@ -4128,8 +4292,8 @@ void ApiWrap::sendInlineResult(
.postAuthor = NewMessagePostAuthor(action),
});
- history->clearCloudDraft(topicRootId);
- history->startSavingCloudDraft(topicRootId);
+ history->clearCloudDraft(topicRootId, monoforumPeerId);
+ history->startSavingCloudDraft(topicRootId, monoforumPeerId);
auto &histories = history->owner().histories();
histories.sendPreparedMessage(
@@ -4150,6 +4314,7 @@ void ApiWrap::sendInlineResult(
), [=](const MTPUpdates &result, const MTP::Response &response) {
history->finishSavingCloudDraft(
topicRootId,
+ monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
if (done) {
done(true);
@@ -4158,6 +4323,7 @@ void ApiWrap::sendInlineResult(
sendMessageFail(error, peer, randomId, newId);
history->finishSavingCloudDraft(
topicRootId,
+ monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
if (done) {
done(false);
@@ -4311,6 +4477,7 @@ void ApiWrap::sendMediaWithRandomId(
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
+ | (options.suggest ? Flag::f_suggested_post : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
@@ -4339,7 +4506,8 @@ void ApiWrap::sendMediaWithRandomId(
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ Api::SuggestToMTP(options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (done) done(true);
if (updateRecentStickers) {
@@ -4396,6 +4564,7 @@ void ApiWrap::sendMultiPaidMedia(
| (options.sendAs ? Flag::f_send_as : Flag(0))
| (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0))
| (options.effectId ? Flag::f_effect : Flag(0))
+ | (options.suggest ? Flag::f_suggested_post : Flag(0))
| (options.invertCaption ? Flag::f_invert_media : Flag(0))
| (starsPaid ? Flag::f_allow_paid_stars : Flag(0));
@@ -4423,7 +4592,8 @@ void ApiWrap::sendMultiPaidMedia(
(options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, options.shortcutId),
MTP_long(options.effectId),
- MTP_long(starsPaid)
+ MTP_long(starsPaid),
+ Api::SuggestToMTP(options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (const auto album = _sendingAlbums.take(groupId)) {
const auto copy = (*album)->items;
@@ -4763,6 +4933,10 @@ Api::Polls &ApiWrap::polls() {
return *_polls;
}
+Api::TodoLists &ApiWrap::todoLists() {
+ return *_todoLists;
+}
+
Api::ChatParticipants &ApiWrap::chatParticipants() {
return *_chatParticipants;
}
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 529114b0a1..5eabc79096 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -77,6 +77,7 @@ class ConfirmPhone;
class PeerPhoto;
class PeerColors;
class Polls;
+class TodoLists;
class ChatParticipants;
class UnreadThings;
class Ringtones;
@@ -231,6 +232,9 @@ public:
void deleteAllFromParticipant(
not_null channel,
not_null from);
+ void deleteSublistHistory(
+ not_null parentChat,
+ not_null sublistPeer);
void requestWebPageDelayed(not_null page);
void clearWebPageRequest(not_null page);
@@ -286,6 +290,7 @@ public:
void requestSharedMedia(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
Storage::SharedMediaType type,
MsgId messageId,
SliceType slice);
@@ -409,6 +414,7 @@ public:
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
[[nodiscard]] Api::PeerPhoto &peerPhoto();
[[nodiscard]] Api::Polls &polls();
+ [[nodiscard]] Api::TodoLists &todoLists();
[[nodiscard]] Api::ChatParticipants &chatParticipants();
[[nodiscard]] Api::UnreadThings &unreadThings();
[[nodiscard]] Api::Ringtones &ringtones();
@@ -502,18 +508,21 @@ private:
void resolveJumpToHistoryDate(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
const QDate &date,
Fn, MsgId)> callback);
template
void requestMessageAfterDate(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
const QDate &date,
Callback &&callback);
void sharedMediaDone(
not_null peer,
MsgId topicRootId,
+ PeerId monoforumPeerId,
SharedMediaType type,
Api::SearchResult &&parsed);
void globalMediaDone(
@@ -539,6 +548,9 @@ private:
void deleteAllFromParticipantSend(
not_null channel,
not_null from);
+ void deleteSublistHistorySend(
+ not_null parentChat,
+ not_null sublistPeer);
void uploadAlbumMedia(
not_null item,
@@ -659,6 +671,7 @@ private:
struct SharedMediaRequest {
not_null peer;
MsgId topicRootId = 0;
+ PeerId monoforumPeerId = 0;
SharedMediaType mediaType = {};
MsgId aroundId = 0;
SliceType sliceType = {};
@@ -753,6 +766,7 @@ private:
const std::unique_ptr _confirmPhone;
const std::unique_ptr _peerPhoto;
const std::unique_ptr _polls;
+ const std::unique_ptr _todoLists;
const std::unique_ptr _chatParticipants;
const std::unique_ptr _unreadThings;
const std::unique_ptr _ringtones;
diff --git a/Telegram/SourceFiles/boxes/about_box.cpp b/Telegram/SourceFiles/boxes/about_box.cpp
index 71f94cdbaa..b15f1cabde 100644
--- a/Telegram/SourceFiles/boxes/about_box.cpp
+++ b/Telegram/SourceFiles/boxes/about_box.cpp
@@ -7,29 +7,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/about_box.h"
-#include "lang/lang_keys.h"
-#include "mainwidget.h"
-#include "mainwindow.h"
-#include "ui/boxes/confirm_box.h"
-#include "ui/widgets/buttons.h"
-#include "ui/widgets/labels.h"
-#include "ui/text/text_utilities.h"
#include "base/platform/base_platform_info.h"
-#include "core/file_utilities.h"
-#include "core/click_handler_types.h"
-#include "core/update_checker.h"
#include "core/application.h"
+#include "core/file_utilities.h"
+#include "core/update_checker.h"
+#include "lang/lang_keys.h"
+#include "ui/boxes/confirm_box.h"
+#include "ui/text/text_utilities.h"
+#include "ui/vertical_list.h"
+#include "ui/widgets/buttons.h"
+#include "ui/wrap/vertical_layout.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include
#include
-#include "window/window_controller.h"
-#include "window/window_session_controller.h"
-#include "window/window_session_controller_link_info.h"
-
-
namespace {
rpl::producer Text() {
@@ -47,54 +40,43 @@ rpl::producer Text() {
} // namespace
-AboutBox::AboutBox(QWidget *parent, Window::SessionController* controller)
-: _version(this, tr::lng_about_version(tr::now, lt_version, currentVersionText()), st::aboutVersionLink)
-, _text(this, Text(), st::aboutLabel)
-, _controller(controller) {
-}
+void AboutBox(not_null box) {
+ box->setTitle(rpl::single(u"AyuGram Desktop"_q));
-void AboutBox::prepare() {
- setTitle(rpl::single(u"AyuGram Desktop"_q));
+ auto layout = box->verticalLayout();
- addButton(tr::lng_close(), [this] { closeBox(); });
- addLeftButton(
- rpl::single(QString("@AyuGramReleases")),
- [this, controller = _controller]
- {
- closeBox();
- controller->showPeerByLink(Window::PeerByLinkInfo{
- .usernameOrId = QString("ayugramreleases"),
- });
- });
+ const auto version = layout->add(
+ object_ptr(
+ box,
+ tr::lng_about_version(
+ tr::now,
+ lt_version,
+ currentVersionText()),
+ st::aboutVersionLink),
+ QMargins(
+ st::boxRowPadding.left(),
+ -st::lineWidth * 3,
+ st::boxRowPadding.right(),
+ st::boxRowPadding.bottom()));
+ version->setClickedCallback([=] {
+ File::OpenUrl(Core::App().changelogLink());
+ });
- _text->setLinksTrusted();
+ Ui::AddSkip(layout, st::aboutTopSkip);
- _version->setClickedCallback([this] { showVersionHistory(); });
+ const auto addText = [&](rpl::producer text) {
+ const auto label = layout->add(
+ object_ptr(box, std::move(text), st::aboutLabel),
+ st::boxRowPadding);
+ label->setLinksTrusted();
+ Ui::AddSkip(layout, st::aboutSkip);
+ };
- setDimensions(st::aboutWidth, st::aboutTextTop + _text->height());
-}
+ addText(Text());
-void AboutBox::resizeEvent(QResizeEvent *e) {
- BoxContent::resizeEvent(e);
+ box->addButton(tr::lng_close(), [=] { box->closeBox(); });
- const auto available = width()
- - st::boxPadding.left()
- - st::boxPadding.right();
- _version->moveToLeft(st::boxPadding.left(), st::aboutVersionTop);
- _text->resizeToWidth(available);
- _text->moveToLeft(st::boxPadding.left(), st::aboutTextTop);
-}
-
-void AboutBox::showVersionHistory() {
- File::OpenUrl(Core::App().changelogLink());
-}
-
-void AboutBox::keyPressEvent(QKeyEvent *e) {
- if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
- closeBox();
- } else {
- BoxContent::keyPressEvent(e);
- }
+ box->setWidth(st::aboutWidth);
}
QString currentVersionText() {
@@ -109,5 +91,8 @@ QString currentVersionText() {
} else if (Platform::IsWindowsARM64()) {
result += " arm64";
}
+#ifdef _DEBUG
+ result += " DEBUG";
+#endif
return result;
}
diff --git a/Telegram/SourceFiles/boxes/about_box.h b/Telegram/SourceFiles/boxes/about_box.h
index 0748f3f9bb..9382e2d6ac 100644
--- a/Telegram/SourceFiles/boxes/about_box.h
+++ b/Telegram/SourceFiles/boxes/about_box.h
@@ -7,35 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
-#include "ui/layers/box_content.h"
+#include "ui/layers/generic_box.h"
-namespace Window {
-class SessionController;
-}
+void AboutBox(not_null box);
-namespace Ui {
-class LinkButton;
-class FlatLabel;
-} // namespace Ui
-
-class AboutBox : public Ui::BoxContent {
-public:
- AboutBox(QWidget*, Window::SessionController* controller);
-
-protected:
- void prepare() override;
-
- void resizeEvent(QResizeEvent *e) override;
- void keyPressEvent(QKeyEvent *e) override;
-
-private:
- void showVersionHistory();
-
- object_ptr _version;
- object_ptr _text;
- Window::SessionController* _controller;
-
-};
-
-QString telegramFaqLink();
QString currentVersionText();
diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp
index 020550774f..792eff79b4 100644
--- a/Telegram/SourceFiles/boxes/add_contact_box.cpp
+++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp
@@ -559,7 +559,7 @@ void GroupInfoBox::prepare() {
&_navigation->parentController()->window(),
Ui::UserpicButton::Role::ChoosePhoto,
st::defaultUserpicButton,
- (_type == Type::Forum));
+ (_type == Type::Forum) ? Ui::PeerUserpicShape::Forum : Ui::PeerUserpicShape::Auto);
_photo->showCustomOnChosen();
_title.create(
this,
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index d43e4c1f15..c159627e71 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -349,7 +349,7 @@ aboutVersionLink: LinkButton(defaultLinkButton) {
color: windowSubTextFg;
overColor: windowSubTextFg;
}
-aboutTextTop: 34px;
+aboutTopSkip: 19px;
aboutSkip: 14px;
aboutLabel: FlatLabel(defaultFlatLabel) {
minWidth: 300px;
diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp
index 621fd2c41e..e5833d5f85 100644
--- a/Telegram/SourceFiles/boxes/connection_box.cpp
+++ b/Telegram/SourceFiles/boxes/connection_box.cpp
@@ -16,9 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "mtproto/facade.h"
+#include "settings/settings_common.h"
#include "storage/localstorage.h"
#include "ui/basic_click_handlers.h"
#include "ui/boxes/confirm_box.h"
+#include "ui/boxes/peer_qr_box.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
@@ -32,6 +34,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/fields/password_input.h"
#include "ui/widgets/labels.h"
+#include "ui/widgets/menu/menu_add_action_callback.h"
+#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
@@ -44,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"
+#include "styles/style_settings.h"
#include
#include
@@ -54,6 +59,31 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
using ProxyData = MTP::ProxyData;
+[[nodiscard]] std::vector ExtractUrlsSimple(const QString &input) {
+ auto urls = std::vector();
+ static auto urlRegex = QRegularExpression(R"((https?:\/\/[^\s]+))");
+
+ auto it = urlRegex.globalMatch(input);
+ while (it.hasNext()) {
+ urls.push_back(it.next().captured(1));
+ }
+
+ return urls;
+}
+
+[[nodiscard]] QString ProxyDataToString(const ProxyData &proxy) {
+ using Type = ProxyData::Type;
+ return u"https://t.me/"_q
+ + (proxy.type == Type::Socks5 ? "socks" : "proxy")
+ + "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
+ + ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
+ ? "&user=" + qthelp::url_encode(proxy.user) : "")
+ + ((proxy.type == Type::Socks5 && !proxy.password.isEmpty())
+ ? "&pass=" + qthelp::url_encode(proxy.password) : "")
+ + ((proxy.type == Type::Mtproto && !proxy.password.isEmpty())
+ ? "&secret=" + proxy.password : "");
+}
+
[[nodiscard]] ProxyData ProxyDataFromFields(
ProxyData::Type type,
const QMap &fields) {
@@ -70,6 +100,80 @@ using ProxyData = MTP::ProxyData;
return proxy;
};
+void AddProxyFromClipboard(
+ not_null controller,
+ std::shared_ptr show) {
+ const auto proxyString = u"proxy"_q;
+ const auto socksString = u"socks"_q;
+ const auto protocol = u"tg://"_q;
+
+ const auto maybeUrls = ExtractUrlsSimple(
+ QGuiApplication::clipboard()->text());
+ const auto isSingle = maybeUrls.size() == 1;
+
+ const auto proceedUrl = [=](const auto &local) {
+ const auto command = base::StringViewMid(
+ local,
+ protocol.size(),
+ 8192);
+
+ if (local.startsWith(protocol + proxyString)
+ || local.startsWith(protocol + socksString)) {
+
+ using namespace qthelp;
+ const auto options = RegExOption::CaseInsensitive;
+ for (const auto &[expression, _] : Core::LocalUrlHandlers()) {
+ const auto midExpression = base::StringViewMid(
+ expression,
+ 1);
+ const auto isSocks = midExpression.startsWith(
+ socksString);
+ if (!midExpression.startsWith(proxyString)
+ && !isSocks) {
+ continue;
+ }
+ const auto match = regex_match(
+ expression,
+ command,
+ options);
+ if (!match) {
+ continue;
+ }
+ const auto type = isSocks
+ ? ProxyData::Type::Socks5
+ : ProxyData::Type::Mtproto;
+ const auto fields = url_parse_params(
+ match->captured(1),
+ qthelp::UrlParamNameTransform::ToLower);
+ const auto proxy = ProxyDataFromFields(type, fields);
+ const auto contains = controller->contains(proxy);
+ const auto toast = (contains
+ ? tr::lng_proxy_add_from_clipboard_existing_toast
+ : tr::lng_proxy_add_from_clipboard_good_toast)(tr::now);
+ if (isSingle) {
+ show->showToast(toast);
+ }
+ if (!contains) {
+ controller->addNewItem(proxy);
+ }
+ break;
+ }
+ return true;
+ }
+ return false;
+ };
+
+ auto success = false;
+ for (const auto &maybeUrl : maybeUrls) {
+ success |= proceedUrl(Core::TryConvertUrlToLocal(maybeUrl));
+ }
+
+ if (!success) {
+ show->showToast(
+ tr::lng_proxy_add_from_clipboard_failed_toast(tr::now));
+ }
+}
+
class HostInput : public Ui::MaskedInputField {
public:
HostInput(
@@ -177,6 +281,7 @@ public:
rpl::producer<> restoreClicks() const;
rpl::producer<> editClicks() const;
rpl::producer<> shareClicks() const;
+ rpl::producer<> showQrClicks() const;
protected:
int resizeGetHeight(int newWidth) override;
@@ -198,6 +303,7 @@ private:
rpl::event_stream<> _restoreClicks;
rpl::event_stream<> _editClicks;
rpl::event_stream<> _shareClicks;
+ rpl::event_stream<> _showQrClicks;
base::unique_qptr _menu;
bool _set = false;
@@ -222,6 +328,7 @@ public:
protected:
void prepare() override;
+ void keyPressEvent(QKeyEvent *e) override;
private:
void setupContent();
@@ -319,6 +426,10 @@ rpl::producer<> ProxyRow::shareClicks() const {
return _shareClicks.events();
}
+rpl::producer<> ProxyRow::showQrClicks() const {
+ return _showQrClicks.events();
+}
+
void ProxyRow::setupControls(View &&view) {
updateFields(std::move(view));
_toggled.stop();
@@ -563,6 +674,9 @@ void ProxyRow::showMenu() {
addAction(tr::lng_proxy_edit_share(tr::now), [=] {
_shareClicks.fire({});
}, &st::menuIconShare);
+ addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
+ _showQrClicks.fire({});
+ }, &st::menuIconQrCode);
}
if (_view.deleted) {
addAction(tr::lng_proxy_menu_restore(tr::now), [=] {
@@ -617,6 +731,18 @@ ProxiesBox::ProxiesBox(
}, lifetime());
}
+void ProxiesBox::keyPressEvent(QKeyEvent *e) {
+ if (e->key() == Qt::Key_Copy
+ || (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier)) {
+ _controller->shareItems();
+ } else if (e->key() == Qt::Key_Paste
+ || (e->key() == Qt::Key_V && e->modifiers() == Qt::ControlModifier)) {
+ AddProxyFromClipboard(_controller, uiShow());
+ } else {
+ BoxContent::keyPressEvent(e);
+ }
+}
+
void ProxiesBox::prepare() {
setTitle(tr::lng_proxy_settings());
@@ -631,67 +757,23 @@ void ProxiesBox::setupTopButton() {
const auto top = addTopButton(st::infoTopBarMenu);
const auto menu
= top->lifetime().make_state>();
- const auto callback = [=] {
- const auto maybeUrl = QGuiApplication::clipboard()->text();
- const auto local = Core::TryConvertUrlToLocal(maybeUrl);
- const auto proxyString = u"proxy"_q;
- const auto socksString = u"socks"_q;
- const auto protocol = u"tg://"_q;
- const auto command = base::StringViewMid(
- local,
- protocol.size(),
- 8192);
-
- if (local.startsWith(protocol + proxyString)
- || local.startsWith(protocol + socksString)) {
-
- using namespace qthelp;
- const auto options = RegExOption::CaseInsensitive;
- for (const auto &[expression, _] : Core::LocalUrlHandlers()) {
- const auto midExpression = base::StringViewMid(
- expression,
- 1);
- const auto isSocks = midExpression.startsWith(
- socksString);
- if (!midExpression.startsWith(proxyString)
- && !isSocks) {
- continue;
- }
- const auto match = regex_match(
- expression,
- command,
- options);
- if (!match) {
- continue;
- }
- const auto type = isSocks
- ? ProxyData::Type::Socks5
- : ProxyData::Type::Mtproto;
- const auto fields = url_parse_params(
- match->captured(1),
- qthelp::UrlParamNameTransform::ToLower);
- const auto proxy = ProxyDataFromFields(type, fields);
- const auto contains = _controller->contains(proxy);
- const auto toast = (contains
- ? tr::lng_proxy_add_from_clipboard_existing_toast
- : tr::lng_proxy_add_from_clipboard_good_toast)(tr::now);
- uiShow()->showToast(toast);
- if (!contains) {
- _controller->addNewItem(proxy);
- }
- break;
- }
- } else {
- uiShow()->showToast(
- tr::lng_proxy_add_from_clipboard_failed_toast(tr::now));
- }
- };
top->setClickedCallback([=] {
- *menu = base::make_unique_q(top, st::defaultPopupMenu);
- (*menu)->addAction(
- tr::lng_proxy_add_from_clipboard(tr::now),
- callback);
+ *menu = base::make_unique_q(
+ top,
+ st::popupMenuWithIcons);
+ const auto addAction = Ui::Menu::CreateAddActionCallback(*menu);
+ addAction({
+ .text = tr::lng_proxy_add_from_clipboard(tr::now),
+ .handler = [=] { AddProxyFromClipboard(_controller, uiShow()); },
+ .icon = &st::menuIconImportTheme,
+ });
+ addAction({
+ .text = tr::lng_group_invite_context_delete_all(tr::now),
+ .handler = [=] { _controller->deleteItems(); },
+ .icon = &st::menuIconDeleteAttention,
+ .isAttention = true,
+ });
(*menu)->popup(QCursor::pos());
return true;
});
@@ -791,6 +873,23 @@ void ProxiesBox::setupContent() {
refreshProxyForCalls();
_proxyForCalls->finishAnimating();
+ {
+ const auto wrap = inner->add(
+ object_ptr>(
+ inner,
+ object_ptr(inner)));
+ const auto shareList = Settings::AddButtonWithIcon(
+ wrap->entity(),
+ tr::lng_proxy_edit_share_list_button(),
+ st::settingsButton,
+ { &st::menuIconCopy });
+ shareList->setClickedCallback([=] {
+ _controller->shareItems();
+ });
+ wrap->toggleOn(_controller->listShareableChanges());
+ wrap->finishAnimating();
+ }
+
inner->resizeToWidth(st::boxWideWidth);
inner->heightValue(
@@ -898,9 +997,11 @@ void ProxiesBox::setupButtons(int id, not_null button) {
getDelegate()->show(_controller->editItemBox(id));
}, button->lifetime());
- button->shareClicks(
- ) | rpl::start_with_next([=] {
- _controller->shareItem(id);
+ rpl::merge(
+ button->shareClicks() | rpl::map_to(false),
+ button->showQrClicks() | rpl::map_to(true)
+ ) | rpl::start_with_next([=](bool qr) {
+ _controller->shareItem(id, qr);
}, button->lifetime());
button->clicks(
@@ -1407,12 +1508,32 @@ void ProxiesBoxController::deleteItem(int id) {
setDeleted(id, true);
}
+void ProxiesBoxController::deleteItems() {
+ for (const auto &item : _list) {
+ setDeleted(item.id, true);
+ }
+}
+
void ProxiesBoxController::restoreItem(int id) {
setDeleted(id, false);
}
-void ProxiesBoxController::shareItem(int id) {
- share(findById(id)->data);
+void ProxiesBoxController::shareItem(int id, bool qr) {
+ share(findById(id)->data, qr);
+}
+
+void ProxiesBoxController::shareItems() {
+ auto result = QString();
+ for (const auto &item : _list) {
+ if (!item.deleted) {
+ result += ProxyDataToString(item.data) + '\n' + '\n';
+ }
+ }
+ if (result.isEmpty()) {
+ return;
+ }
+ QGuiApplication::clipboard()->setText(result);
+ _show->showToast(tr::lng_proxy_edit_share_list_toast(tr::now));
}
void ProxiesBoxController::applyItem(int id) {
@@ -1621,6 +1742,17 @@ auto ProxiesBoxController::views() const -> rpl::producer {
return _views.events();
}
+rpl::producer ProxiesBoxController::listShareableChanges() const {
+ return _views.events_starting_with(ItemView()) | rpl::map([=] {
+ for (const auto &item : _list) {
+ if (!item.deleted) {
+ return true;
+ }
+ }
+ return false;
+ });
+}
+
void ProxiesBoxController::updateView(const Item &item) {
const auto selected = (_settings.selected() == item.data);
const auto deleted = item.deleted;
@@ -1653,22 +1785,22 @@ void ProxiesBoxController::updateView(const Item &item) {
deleted,
!deleted && supportsShare,
supportsCalls,
- state });
+ state,
+ });
}
-void ProxiesBoxController::share(const ProxyData &proxy) {
+void ProxiesBoxController::share(const ProxyData &proxy, bool qr) {
if (proxy.type == Type::Http) {
return;
}
- const auto link = u"https://t.me/"_q
- + (proxy.type == Type::Socks5 ? "socks" : "proxy")
- + "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
- + ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
- ? "&user=" + qthelp::url_encode(proxy.user) : "")
- + ((proxy.type == Type::Socks5 && !proxy.password.isEmpty())
- ? "&pass=" + qthelp::url_encode(proxy.password) : "")
- + ((proxy.type == Type::Mtproto && !proxy.password.isEmpty())
- ? "&secret=" + proxy.password : "");
+ 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());
+ }));
+ return;
+ }
QGuiApplication::clipboard()->setText(link);
_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 25da434586..97636060c7 100644
--- a/Telegram/SourceFiles/boxes/connection_box.h
+++ b/Telegram/SourceFiles/boxes/connection_box.h
@@ -72,8 +72,10 @@ public:
};
void deleteItem(int id);
+ void deleteItems();
void restoreItem(int id);
- void shareItem(int id);
+ void shareItem(int id, bool qr);
+ void shareItems();
void applyItem(int id);
object_ptr editItemBox(int id);
object_ptr addNewItemBox();
@@ -87,6 +89,8 @@ public:
rpl::producer views() const;
+ rpl::producer listShareableChanges() const;
+
~ProxiesBoxController();
private:
@@ -106,7 +110,7 @@ private:
std::vector- ::iterator findByProxy(const ProxyData &proxy);
void setDeleted(int id, bool deleted);
void updateView(const Item &item);
- void share(const ProxyData &proxy);
+ void share(const ProxyData &proxy, bool qr = false);
void saveDelayed();
void refreshChecker(Item &item);
void setupChecker(int id, const Checker &checker);
diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp
index fab9731cc8..290da2d72e 100644
--- a/Telegram/SourceFiles/boxes/create_poll_box.cpp
+++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_custom_emoji.h"
#include "history/view/history_view_schedule_box.h"
#include "lang/lang_keys.h"
+#include "main/main_app_config.h"
#include "main/main_session.h"
#include "menu/menu_send.h"
#include "ui/controls/emoji_button.h"
@@ -113,7 +114,7 @@ private:
void setPlaceholder() const;
void removePlaceholder() const;
- not_null field() const;
+ [[nodiscard]] not_null field() const;
[[nodiscard]] PollAnswer toPollAnswer(int index) const;
@@ -510,7 +511,8 @@ Options::Options(
}
bool Options::full() const {
- return (_list.size() == kMaxOptionsCount);
+ const auto limit = _controller->session().appConfig().pollOptionsLimit();
+ return (_list.size() >= limit);
}
bool Options::hasOptions() const {
@@ -1028,8 +1030,10 @@ object_ptr CreatePollBox::setupContent() {
setCloseByEscape(!count);
setCloseByOutsideClick(!count);
}) | rpl::map([=](int count) {
- return (count < kMaxOptionsCount)
- ? tr::lng_polls_create_limit(tr::now, lt_count, kMaxOptionsCount - count)
+ const auto appConfig = &_controller->session().appConfig();
+ const auto max = appConfig->pollOptionsLimit();
+ return (count < max)
+ ? tr::lng_polls_create_limit(tr::now, lt_count, max - count)
: tr::lng_polls_create_maximum(tr::now);
}) | rpl::after_next([=] {
container->resizeToWidth(container->widthNoMargins());
diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp
index 7ff851209c..b2bc215cd3 100644
--- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp
+++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp
@@ -21,8 +21,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
+#include "main/main_app_config.h"
#include "main/main_session.h"
#include "menu/menu_ttl_validator.h"
+#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
@@ -500,7 +502,58 @@ void DeleteMessagesBox::keyPressEvent(QKeyEvent *e) {
}
}
+PaidPostType DeleteMessagesBox::paidPostType() const {
+ auto result = PaidPostType::None;
+ const auto now = base::unixtime::now();
+ for (const auto &id : _ids) {
+ if (const auto item = _session->data().message(id)) {
+ const auto type = item->paidType();
+ if (type != PaidPostType::None) {
+ const auto date = item->date();
+ const auto config = &item->history()->session().appConfig();
+ const auto limit = config->suggestedPostAgeMin();
+ if (now < date || now - date <= limit) {
+ if (type == PaidPostType::Ton) {
+ return type;
+ } else if (type == PaidPostType::Stars) {
+ result = type;
+ }
+ }
+ }
+ }
+ }
+ return result;
+}
+
void DeleteMessagesBox::deleteAndClear() {
+ const auto warnPaidType = _confirmedDeletePaidSuggestedPosts
+ ? PaidPostType::None
+ : paidPostType();
+ if (warnPaidType != PaidPostType::None) {
+ const auto weak = Ui::MakeWeak(this);
+ const auto callback = [=](Fn close) {
+ close();
+ if (const auto strong = weak.data()) {
+ strong->_confirmedDeletePaidSuggestedPosts = true;
+ strong->deleteAndClear();
+ }
+ };
+ const auto ton = (warnPaidType == PaidPostType::Ton);
+ uiShow()->show(Ui::MakeConfirmBox({
+ .text = (ton
+ ? tr::lng_suggest_warn_text_ton
+ : tr::lng_suggest_warn_text_stars)(
+ tr::now,
+ Ui::Text::RichLangValue),
+ .confirmed = callback,
+ .confirmText = tr::lng_suggest_warn_delete_anyway(tr::now),
+ .confirmStyle = &st::attentionBoxButton,
+ .title = (ton
+ ? tr::lng_suggest_warn_title_ton
+ : tr::lng_suggest_warn_title_stars)(tr::now),
+ }));
+ return;
+ }
if (_revoke
&& _revokeRemember
&& _revokeRemember->toggled()
diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.h b/Telegram/SourceFiles/boxes/delete_messages_box.h
index 9987fd8b32..3762212b0d 100644
--- a/Telegram/SourceFiles/boxes/delete_messages_box.h
+++ b/Telegram/SourceFiles/boxes/delete_messages_box.h
@@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/layers/box_content.h"
+enum class PaidPostType : uchar;
+
namespace Main {
class Session;
} // namespace Main
@@ -58,6 +60,7 @@ private:
[[nodiscard]] bool hasScheduledMessages() const;
[[nodiscard]] std::optional revokeText(
not_null peer) const;
+ [[nodiscard]] PaidPostType paidPostType() const;
const not_null _session;
@@ -82,6 +85,7 @@ private:
object_ptr _autoDeleteSettings = { nullptr };
int _fullHeight = 0;
+ bool _confirmedDeletePaidSuggestedPosts = false;
Fn _deleteConfirmedCallback;
diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
index 25501bd295..cb27997d66 100644
--- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp
+++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp
@@ -232,12 +232,14 @@ EditCaptionBox::EditCaptionBox(
not_null controller,
not_null item,
TextWithTags &&text,
+ SuggestPostOptions suggest,
bool spoilered,
bool invertCaption,
Ui::PreparedList &&list,
Fn saved)
: _controller(controller)
, _historyItem(item)
+, _suggest(suggest)
, _isAllowedEditMedia(item->allowsEditMedia())
, _albumType(ComputeAlbumType(item))
, _controls(base::make_unique_q(this))
@@ -271,6 +273,7 @@ void EditCaptionBox::StartMediaReplace(
not_null controller,
FullMsgId itemId,
TextWithTags text,
+ SuggestPostOptions suggest,
bool spoilered,
bool invertCaption,
Fn saved) {
@@ -284,6 +287,7 @@ void EditCaptionBox::StartMediaReplace(
controller,
item,
std::move(text),
+ suggest,
spoilered,
invertCaption,
std::move(list),
@@ -300,6 +304,7 @@ void EditCaptionBox::StartMediaReplace(
FullMsgId itemId,
Ui::PreparedList &&list,
TextWithTags text,
+ SuggestPostOptions suggest,
bool spoilered,
bool invertCaption,
Fn saved) {
@@ -335,6 +340,7 @@ void EditCaptionBox::StartMediaReplace(
controller,
item,
std::move(text),
+ suggest,
spoilered,
invertCaption,
std::move(list),
@@ -347,6 +353,7 @@ void EditCaptionBox::StartPhotoEdit(
std::shared_ptr