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 @@ + + + Icon / Input / input_paid + + + + + \ 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 @@ + + + Icon / Filled / paid_approve + + + + \ 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 @@ + + + Icon / Filled / paid_decline + + + + \ 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 @@ + + + Icon / Filled / paid_edit + + + + \ 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 @@ + + + Icon / Filled / checklist + + + + \ 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 media, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn saved) { @@ -365,6 +372,7 @@ void EditCaptionBox::StartPhotoEdit( controller, item, std::move(text), + suggest, spoilered, invertCaption, std::move(list), @@ -1001,6 +1009,7 @@ void EditCaptionBox::save() { }; auto options = Api::SendOptions(); + options.suggest = _suggest; options.scheduled = item->isScheduled() ? item->date() : 0; options.shortcutId = item->shortcutId(); options.invertCaption = _mediaEditManager.invertCaption(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index 7453506161..48d858f971 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -39,6 +39,7 @@ public: not_null controller, not_null item, TextWithTags &&text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Ui::PreparedList &&list, @@ -49,6 +50,7 @@ public: not_null controller, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn saved); @@ -57,6 +59,7 @@ public: FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn saved); @@ -65,6 +68,7 @@ public: std::shared_ptr media, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn saved); @@ -111,6 +115,7 @@ private: const not_null _controller; const not_null _historyItem; + const SuggestPostOptions _suggest; const bool _isAllowedEditMedia; const Ui::AlbumType _albumType; diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index c3c92325a0..86816ba15b 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -45,8 +45,7 @@ namespace { constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value; constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value; -constexpr auto kStarsMin = 1; -constexpr auto kDefaultChargeStars = 10; +constexpr auto kDefaultPrivateMessagesPrice = 10; using Exceptions = Api::UserPrivacy::Exceptions; @@ -464,6 +463,7 @@ auto PrivacyExceptionsBoxController::createRow(not_null history) int valuesCount, Fn valueByIndex, int value, + int minValue, int maxValue, Fn valueProgress, Fn valueFinished) { @@ -473,7 +473,7 @@ auto PrivacyExceptionsBoxController::createRow(not_null history) const auto labels = raw->add(object_ptr(raw)); const auto min = Ui::CreateChild( raw, - QString::number(kStarsMin), + QString::number(minValue), *labelStyle); const auto max = Ui::CreateChild( raw, @@ -510,8 +510,9 @@ auto PrivacyExceptionsBoxController::createRow(not_null history) current->moveToLeft((outer - current->width()) / 2, 0, outer); }; const auto updateByValue = [=](int value) { - current->setText( - tr::lng_action_gift_for_stars(tr::now, lt_count, value)); + current->setText(value > 0 + ? tr::lng_action_gift_for_stars(tr::now, lt_count, value) + : tr::lng_manage_monoforum_free(tr::now)); state->index = 0; auto maxIndex = valuesCount - 1; @@ -1035,7 +1036,8 @@ void EditMessagesPrivacyBox( state->stars = SetupChargeSlider( chargeInner, session->user(), - savedValue); + (savedValue > 0) ? savedValue : std::optional(), + kDefaultPrivateMessagesPrice); Ui::AddSkip(chargeInner); Ui::AddSubsectionTitle( @@ -1164,25 +1166,31 @@ void EditMessagesPrivacyBox( rpl::producer SetupChargeSlider( not_null container, not_null peer, - int savedValue) { + std::optional savedValue, + int defaultValue, + bool allowZero) { struct State { rpl::variable stars; }; - const auto group = !peer->isUser(); + const auto broadcast = peer->isBroadcast(); + const auto group = !broadcast && !peer->isUser(); const auto state = container->lifetime().make_state(); - const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars; + const auto chargeStars = savedValue.value_or(defaultValue); state->stars = chargeStars; - Ui::AddSubsectionTitle(container, group + Ui::AddSubsectionTitle(container, broadcast + ? tr::lng_manage_monoforum_price() + : group ? tr::lng_rights_charge_price() : tr::lng_messages_privacy_price()); auto values = std::vector(); + const auto minStars = allowZero ? 0 : 1; const auto maxStars = peer->session().appConfig().paidMessageStarsMax(); - if (chargeStars < kStarsMin) { + if (chargeStars < minStars) { values.push_back(chargeStars); } - for (auto i = kStarsMin; i < std::min(100, maxStars); ++i) { + for (auto i = minStars; i < std::min(100, maxStars); ++i) { values.push_back(i); } for (auto i = 100; i < std::min(1000, maxStars); i += 10) { @@ -1209,6 +1217,7 @@ rpl::producer SetupChargeSlider( valuesCount, [=](int index) { return values[index]; }, chargeStars, + minStars, maxStars, setStars, setStars), @@ -1217,21 +1226,100 @@ rpl::producer SetupChargeSlider( const auto skip = 2 * st::defaultVerticalListSkip; Ui::AddSkip(container, skip); - auto dollars = state->stars.value() | rpl::map([=](int stars) { - const auto ratio = peer->session().appConfig().starsWithdrawRate(); + const auto details = container->add( + object_ptr(container)); + state->stars.value() | rpl::start_with_next([=](int stars) { + while (details->count()) { + delete details->widgetAt(0); + } + if (!stars) { + Ui::AddDivider(details); + return; + } + const auto &appConfig = peer->session().appConfig(); + const auto percent = appConfig.paidMessageCommission(); + const auto ratio = appConfig.starsWithdrawRate(); const auto dollars = int(base::SafeRound(stars * ratio)); - return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q); - }); - const auto percent = peer->session().appConfig().paidMessageCommission(); - Ui::AddDividerText( - container, - (group - ? tr::lng_rights_charge_price_about - : tr::lng_messages_privacy_price_about)( - lt_percent, - rpl::single(QString::number(percent / 10.) + '%'), - lt_amount, - std::move(dollars))); - + const auto amount = Ui::FillAmountAndCurrency(dollars, u"USD"_q); + Ui::AddDividerText( + details, + (broadcast + ? tr::lng_manage_monoforum_price_about + : group + ? tr::lng_rights_charge_price_about + : tr::lng_messages_privacy_price_about)( + lt_percent, + rpl::single(QString::number(percent / 10.) + '%'), + lt_amount, + rpl::single('~' + amount))); + }, details->lifetime()); return state->stars.value(); } + +void EditDirectMessagesPriceBox( + not_null box, + not_null channel, + std::optional savedValue, + Fn)> callback) { + box->setTitle(tr::lng_manage_monoforum()); + box->setWidth(st::boxWideWidth); + + const auto container = box->verticalLayout(); + + Settings::AddDividerTextWithLottie(container, { + .lottie = u"direct_messages"_q, + .lottieSize = st::settingsFilterIconSize, + .lottieMargins = st::settingsFilterIconPadding, + .showFinished = box->showFinishes(), + .about = tr::lng_manage_monoforum_about( + Ui::Text::RichLangValue + ), + .aboutMargins = st::settingsFilterDividerLabelPadding, + }); + + Ui::AddSkip(container); + + const auto toggle = container->add(object_ptr( + box, + tr::lng_manage_monoforum_allow(), + st::settingsButtonNoIcon)); + toggle->toggleOn(rpl::single(savedValue.has_value())); + + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + + const auto wrap = box->addRow( + object_ptr>( + box, + object_ptr(box)), + {}); + wrap->toggle(savedValue.has_value(), anim::type::instant); + wrap->toggleOn(toggle->toggledChanges()); + + const auto result = box->lifetime().make_state( + savedValue.value_or(0)); + + const auto inner = wrap->entity(); + Ui::AddSkip(inner); + SetupChargeSlider( + inner, + channel, + savedValue, + channel->session().appConfig().paidMessageChannelStarsDefault(), + true + ) | rpl::start_with_next([=](int stars) { + *result = stars; + }, box->lifetime()); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + callback(toggle->toggled() ? *result : std::optional()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 256ebe5b51..d46cbbfa8e 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -173,4 +173,12 @@ void EditMessagesPrivacyBox( [[nodiscard]] rpl::producer SetupChargeSlider( not_null container, not_null peer, - int savedValue); + std::optional savedValue, + int defaultValue, + bool allowZero = false); + +void EditDirectMessagesPriceBox( + not_null box, + not_null channel, + std::optional savedValue, + Fn)> callback); diff --git a/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp new file mode 100644 index 0000000000..65591aaf9a --- /dev/null +++ b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp @@ -0,0 +1,1215 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/edit_todo_list_box.h" + +#include "base/call_delayed.h" +#include "base/event_filter.h" +#include "base/random.h" +#include "base/unique_qptr.h" +#include "chat_helpers/emoji_suggestions_widget.h" +#include "chat_helpers/message_field.h" +#include "chat_helpers/tabbed_panel.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_changes.h" +#include "data/data_media_types.h" +#include "data/data_session.h" +#include "data/data_todo_list.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/view/history_view_schedule_box.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_send.h" +#include "ui/controls/emoji_button.h" +#include "ui/controls/emoji_button_factory.h" +#include "ui/rect.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/ui_utility.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" // defaultComposeFiles. +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace { + +constexpr auto kMaxOptionsCount = TodoListData::kMaxOptions; +constexpr auto kWarnTitleLimit = 12; +constexpr auto kWarnTaskLimit = 24; +constexpr auto kErrorLimit = 99; + +class Tasks { +public: + Tasks( + not_null box, + not_null container, + not_null controller, + ChatHelpers::TabbedPanel *emojiPanel, + std::vector existing = {}, + bool existingLocked = false); + + [[nodiscard]] bool hasTasks() const; + [[nodiscard]] bool isValid() const; + [[nodiscard]] std::vector toTodoListItems() const; + void focusFirst(); + + [[nodiscard]] rpl::producer addedCount() const; + [[nodiscard]] rpl::producer> scrollToWidget() const; + [[nodiscard]] rpl::producer<> backspaceInFront() const; + [[nodiscard]] rpl::producer<> tabbed() const; + +private: + class Task { + public: + Task( + not_null outer, + not_null container, + not_null session, + int id, + int position, + bool locked); + + Task(const Task &other) = delete; + Task &operator=(const Task &other) = delete; + + void toggleRemoveAlways(bool toggled); + + void show(anim::type animated); + void destroy(FnMut done); + + [[nodiscard]] bool hasShadow() const; + void createShadow(); + void destroyShadow(); + + [[nodiscard]] int id() const; + [[nodiscard]] bool locked() const; + [[nodiscard]] bool isEmpty() const; + [[nodiscard]] bool isGood() const; + [[nodiscard]] bool isTooLong() const; + [[nodiscard]] bool hasFocus() const; + void setFocus() const; + void clearValue(); + + void setPlaceholder() const; + void removePlaceholder() const; + + [[nodiscard]] not_null field() const; + + [[nodiscard]] TodoListItem toTodoListItem(int nextId) const; + + [[nodiscard]] rpl::producer removeClicks() const; + + private: + void createRemove(); + void createWarning(); + void updateFieldGeometry(); + + int _id = 0; + base::unique_qptr> _wrap; + not_null _content; + Ui::InputField *_field = nullptr; + base::unique_qptr _shadow; + base::unique_qptr _remove; + rpl::variable *_removeAlways = nullptr; + int _limit = 0; + + }; + + [[nodiscard]] bool full() const; + [[nodiscard]] bool correctShadows() const; + void fixShadows(); + void removeEmptyTail(); + void addEmptyTask(); + void addTask( + int id, + TextWithEntities text, + anim::type animated); + void initTaskField(not_null task, TextWithEntities text); + void checkLastTask(); + void validateState(); + void fixAfterErase(); + void destroy(std::unique_ptr task); + void removeDestroyed(not_null field); + int findField(not_null field) const; + + not_null _box; + not_null _container; + const not_null _controller; + const int _existingCount = 0; + const bool _existingLocked = false; + ChatHelpers::TabbedPanel * const _emojiPanel; + int _position = 0; + int _tasksLimit = 0; + std::vector> _list; + std::vector> _destroyed; + rpl::variable _addedCount = 0; + bool _hasTasks = false; + bool _isValid = false; + rpl::event_stream> _scrollToWidget; + rpl::event_stream<> _backspaceInFront; + rpl::event_stream<> _tabbed; + rpl::lifetime _emojiPanelLifetime; + +}; + +void InitField( + not_null container, + not_null field, + not_null session) { + field->setInstantReplaces(Ui::InstantReplaces::Default()); + field->setInstantReplacesEnabled( + Core::App().settings().replaceEmojiValue()); + auto options = Ui::Emoji::SuggestionsController::Options(); + options.suggestExactFirstWord = false; + Ui::Emoji::SuggestionsController::Init( + container, + field, + session, + options); +} + +not_null CreateWarningLabel( + not_null parent, + not_null field, + int valueLimit, + int warnLimit) { + const auto result = Ui::CreateChild( + parent.get(), + QString(), + st::createPollWarning); + result->setAttribute(Qt::WA_TransparentForMouseEvents); + field->changes( + ) | rpl::start_with_next([=] { + Ui::PostponeCall(crl::guard(field, [=] { + const auto length = field->getLastText().size(); + const auto value = valueLimit - length; + const auto shown = (value < warnLimit) + && (field->height() > st::createPollOptionField.heightMin); + if (value >= 0) { + result->setText(QString::number(value)); + } else { + constexpr auto kMinus = QChar(0x2212); + result->setMarkedText(Ui::Text::Colorized( + kMinus + QString::number(std::abs(value)))); + } + result->setVisible(shown); + })); + }, field->lifetime()); + return result; +} + +void FocusAtEnd(not_null field) { + field->setFocus(); + field->setCursorPosition(field->getLastText().size()); + field->ensureCursorVisible(); +} + +[[nodiscard]] base::unique_qptr MakeEmojiPanel( + not_null outer, + not_null controller) { + auto result = base::make_unique_q( + outer, + controller, + object_ptr( + nullptr, + controller->uiShow(), + Window::GifPauseReason::Layer, + ChatHelpers::TabbedSelector::Mode::EmojiOnly)); + result->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + result ->hide(); + result->selector()->setCurrentPeer(controller->session().user()); + return result; +} + +Tasks::Task::Task( + not_null outer, + not_null container, + not_null session, + int id, + int position, + bool locked) +: _id(id) +, _wrap(container->insert( + position, + object_ptr>( + container, + object_ptr(container)))) +, _content(_wrap->entity()) +, _field( + Ui::CreateChild( + _content.get(), + session->user()->isPremium() + ? st::createPollOptionFieldPremium + : st::createPollOptionField, + Ui::InputField::Mode::NoNewlines, + tr::lng_todo_create_list_add())) +, _limit(session->appConfig().todoListItemTextLimit()) { + InitField(outer, _field, session); + _field->setMaxLength(_limit + kErrorLimit); + _field->show(); + if (locked) { + _field->setDisabled(true); + } else { + _field->customTab(true); + } + + _wrap->hide(anim::type::instant); + + _content->widthValue( + ) | rpl::start_with_next([=] { + updateFieldGeometry(); + }, _field->lifetime()); + + _field->heightValue( + ) | rpl::start_with_next([=](int height) { + _content->resize(_content->width(), height); + }, _field->lifetime()); + + createShadow(); + if (!locked) { + createRemove(); + createWarning(); + } + updateFieldGeometry(); +} + +bool Tasks::Task::hasShadow() const { + return (_shadow != nullptr); +} + +void Tasks::Task::createShadow() { + Expects(_content != nullptr); + + if (_shadow) { + return; + } + _shadow.reset(Ui::CreateChild(field().get())); + _shadow->show(); + field()->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto left = st::createPollFieldPadding.left(); + _shadow->setGeometry( + left, + size.height() - st::lineWidth, + size.width() - left, + st::lineWidth); + }, _shadow->lifetime()); +} + +void Tasks::Task::destroyShadow() { + _shadow = nullptr; +} + +void Tasks::Task::createRemove() { + using namespace rpl::mappers; + + const auto field = this->field(); + auto &lifetime = field->lifetime(); + + const auto remove = Ui::CreateChild( + field.get(), + st::createPollOptionRemove); + remove->show(anim::type::instant); + + const auto toggle = lifetime.make_state>(false); + _removeAlways = lifetime.make_state>(false); + + field->changes( + ) | rpl::start_with_next([field, toggle] { + // Don't capture 'this'! Because Option is a value type. + *toggle = !field->getLastText().isEmpty(); + }, field->lifetime()); +#if 0 + rpl::combine( + toggle->value(), + _removeAlways->value(), + _1 || _2 + ) | rpl::start_with_next([=](bool shown) { + remove->toggle(shown, anim::type::normal); + }, remove->lifetime()); +#endif + + field->widthValue( + ) | rpl::start_with_next([=](int width) { + remove->moveToRight( + st::createPollOptionRemovePosition.x(), + st::createPollOptionRemovePosition.y(), + width); + }, remove->lifetime()); + + _remove.reset(remove); +} + +void Tasks::Task::createWarning() { + using namespace rpl::mappers; + + const auto field = this->field(); + const auto warning = CreateWarningLabel( + field, + field, + _limit, + kWarnTaskLimit); + rpl::combine( + field->sizeValue(), + warning->sizeValue() + ) | rpl::start_with_next([=](QSize size, QSize label) { + warning->moveToLeft( + (size.width() + - label.width() + - st::createPollWarningPosition.x()), + (size.height() + - label.height() + - st::createPollWarningPosition.y()), + size.width()); + }, warning->lifetime()); +} + +bool Tasks::Task::isEmpty() const { + return field()->getLastText().trimmed().isEmpty(); +} + +bool Tasks::Task::isGood() const { + return !locked() + && !field()->getLastText().trimmed().isEmpty() + && !isTooLong(); +} + +bool Tasks::Task::isTooLong() const { + return (field()->getLastText().size() > _limit); +} + +bool Tasks::Task::hasFocus() const { + return field()->hasFocus(); +} + +void Tasks::Task::setFocus() const { + if (!locked()) { + FocusAtEnd(field()); + } +} + +void Tasks::Task::clearValue() { + field()->setText(QString()); +} + +void Tasks::Task::setPlaceholder() const { + field()->setPlaceholder(tr::lng_todo_create_list_add()); +} + +void Tasks::Task::toggleRemoveAlways(bool toggled) { + if (_removeAlways) { + *_removeAlways = toggled; + } +} + +void Tasks::Task::updateFieldGeometry() { + _field->resizeToWidth(_content->width()); + _field->moveToLeft(0, 0); +} + +not_null Tasks::Task::field() const { + return _field; +} + +void Tasks::Task::removePlaceholder() const { + field()->setPlaceholder(rpl::single(QString())); +} + +int Tasks::Task::id() const { + return _id; +} + +bool Tasks::Task::locked() const { + return !_remove; +} + +TodoListItem Tasks::Task::toTodoListItem(int nextId) const { + const auto text = field()->getTextWithTags(); + auto result = TodoListItem{ + .text = TextWithEntities{ + .text = text.text, + .entities = TextUtilities::ConvertTextTagsToEntities(text.tags), + }, + .id = _id ? _id : nextId, + }; + TextUtilities::Trim(result.text); + return result; +} + +rpl::producer Tasks::Task::removeClicks() const { + return _remove ? _remove->clicks() : rpl::never(); +} + +Tasks::Tasks( + not_null box, + not_null container, + not_null controller, + ChatHelpers::TabbedPanel *emojiPanel, + std::vector existing, + bool existingLocked) +: _box(box) +, _container(container) +, _controller(controller) +, _existingCount(existing.size()) +, _existingLocked(existingLocked) +, _emojiPanel(emojiPanel) +, _position(_container->count()) +, _tasksLimit(controller->session().appConfig().todoListItemsLimit()) { + for (const auto &task : existing) { + addTask(task.id, task.text, anim::type::instant); + } + validateState(); +} + +bool Tasks::full() const { + return (_list.size() >= _tasksLimit); +} + +bool Tasks::hasTasks() const { + return _hasTasks; +} + +bool Tasks::isValid() const { + return _isValid; +} + +rpl::producer Tasks::addedCount() const { + return _addedCount.value(); +} + +rpl::producer> Tasks::scrollToWidget() const { + return _scrollToWidget.events(); +} + +rpl::producer<> Tasks::backspaceInFront() const { + return _backspaceInFront.events(); +} + +rpl::producer<> Tasks::tabbed() const { + return _tabbed.events(); +} + +void Tasks::Task::show(anim::type animated) { + _wrap->show(animated); +} + +void Tasks::Task::destroy(FnMut done) { + if (anim::Disabled() || _wrap->isHidden()) { + Ui::PostponeCall(std::move(done)); + return; + } + _wrap->hide(anim::type::normal); + base::call_delayed( + st::slideWrapDuration * 2, + _content.get(), + std::move(done)); +} + +std::vector Tasks::toTodoListItems() const { + auto result = std::vector(); + result.reserve(_list.size()); + auto usedId = 0; + for (const auto &task : _list) { + if (const auto id = task->id()) { + usedId = id; + } else if (task->isGood()) { + ++usedId; + } + if (task->isGood()) { + result.push_back(task->toTodoListItem(usedId)); + } + } + return result; +} + +void Tasks::focusFirst() { + const auto locked = _existingLocked ? _existingCount : 0; + Assert(locked < _list.size()); + FocusAtEnd((_list.begin() + locked)->get()->field()); +} + +bool Tasks::correctShadows() const { + // Last one should be without shadow. + const auto noShadow = ranges::find( + _list, + true, + ranges::not_fn(&Task::hasShadow)); + return (noShadow == end(_list) - 1); +} + +void Tasks::fixShadows() { + if (correctShadows()) { + return; + } + for (auto &option : _list) { + option->createShadow(); + } + _list.back()->destroyShadow(); +} + +void Tasks::removeEmptyTail() { + // Only one option at the end of options list can be empty. + // Remove all other trailing empty options. + // Only last empty and previous option have non-empty placeholders. + const auto focused = ranges::find_if( + _list, + &Task::hasFocus); + const auto end = _list.end(); + const auto reversed = ranges::views::reverse(_list); + const auto emptyItem = ranges::find_if( + reversed, + ranges::not_fn(&Task::isEmpty)).base(); + const auto focusLast = (focused > emptyItem) && (focused < end); + if (emptyItem == end) { + return; + } + if (focusLast) { + (*emptyItem)->setFocus(); + } + for (auto i = emptyItem + 1; i != end; ++i) { + destroy(std::move(*i)); + } + _list.erase(emptyItem + 1, end); + fixAfterErase(); +} + +void Tasks::destroy(std::unique_ptr task) { + const auto value = task.get(); + task->destroy([=] { removeDestroyed(value); }); + _destroyed.push_back(std::move(task)); +} + +void Tasks::fixAfterErase() { + Expects(!_list.empty()); + + const auto last = _list.end() - 1; + (*last)->setPlaceholder(); + (*last)->toggleRemoveAlways(false); + if (last != begin(_list)) { + (*(last - 1))->setPlaceholder(); + (*(last - 1))->toggleRemoveAlways(false); + } + fixShadows(); +} + +void Tasks::addEmptyTask() { + if (!_list.empty() && _list.back()->isEmpty()) { + return; + } + const auto locked = _existingLocked ? _existingCount : 0; + addTask( + 0, // id + TextWithEntities(), + (locked < _list.size()) ? anim::type::normal : anim::type::instant); +} + +void Tasks::addTask( + int id, + TextWithEntities text, + anim::type animated) { + if (full()) { + return; + } + if (_list.size() > 1) { + (*(_list.end() - 2))->removePlaceholder(); + (*(_list.end() - 2))->toggleRemoveAlways(true); + } + const auto locked = id && _existingLocked; + _list.push_back(std::make_unique( + _box, + _container, + &_controller->session(), + id, + _position + _list.size() + _destroyed.size(), + locked)); + const auto field = _list.back()->field(); + if (!locked) { + initTaskField(_list.back().get(), std::move(text)); + } else { + InitMessageFieldHandlers( + _controller, + field, + Window::GifPauseReason::Layer, + [](not_null) { return true; }); + field->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); + } + field->finishAnimating(); + _list.back()->show(animated); + fixShadows(); +} + +void Tasks::initTaskField(not_null task, TextWithEntities text) { + const auto field = task->field(); + if (const auto emojiPanel = _emojiPanel) { + const auto emojiToggle = Ui::AddEmojiToggleToField( + field, + _box, + _controller, + emojiPanel, + QPoint( + -st::createPollOptionFieldPremium.textMargins.right(), + st::createPollOptionEmojiPositionSkip)); + emojiToggle->shownValue() | rpl::start_with_next([=](bool shown) { + if (!shown) { + return; + } + _emojiPanelLifetime.destroy(); + emojiPanel->selector()->emojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { + if (field->hasFocus()) { + Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji); + } + }, _emojiPanelLifetime); + emojiPanel->selector()->customEmojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + if (field->hasFocus()) { + Data::InsertCustomEmoji(field, data.document); + } + }, _emojiPanelLifetime); + }, emojiToggle->lifetime()); + } + field->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); + field->submits( + ) | rpl::start_with_next([=] { + const auto index = findField(field); + if (_list[index]->isGood() && index + 1 < _list.size()) { + _list[index + 1]->setFocus(); + } + }, field->lifetime()); + field->changes( + ) | rpl::start_with_next([=] { + Ui::PostponeCall(crl::guard(field, [=] { + validateState(); + })); + }, field->lifetime()); + field->focusedChanges( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { + _scrollToWidget.fire_copy(field); + }, field->lifetime()); + field->tabbed( + ) | rpl::start_with_next([=] { + const auto index = findField(field); + if (index + 1 < _list.size()) { + _list[index + 1]->setFocus(); + } else { + _tabbed.fire({}); + } + }, field->lifetime()); + base::install_event_filter(field, [=](not_null event) { + if (event->type() != QEvent::KeyPress + || !field->getLastText().isEmpty()) { + return base::EventFilterResult::Continue; + } + const auto key = static_cast(event.get())->key(); + if (key != Qt::Key_Backspace) { + return base::EventFilterResult::Continue; + } + + const auto index = findField(field); + if (index > 0) { + _list[index - 1]->setFocus(); + } else { + _backspaceInFront.fire({}); + } + return base::EventFilterResult::Cancel; + }); + + task->removeClicks( + ) | rpl::start_with_next([=] { + Ui::PostponeCall(crl::guard(field, [=] { + Expects(!_list.empty()); + + const auto item = begin(_list) + findField(field); + if (item == _list.end() - 1) { + (*item)->clearValue(); + return; + } + if ((*item)->hasFocus()) { + (*(item + 1))->setFocus(); + } + destroy(std::move(*item)); + _list.erase(item); + fixAfterErase(); + validateState(); + })); + }, field->lifetime()); +} + +void Tasks::removeDestroyed(not_null task) { + const auto i = ranges::find( + _destroyed, + task.get(), + &std::unique_ptr::get); + Assert(i != end(_destroyed)); + _destroyed.erase(i); +} + +void Tasks::validateState() { + checkLastTask(); + _hasTasks = (ranges::count_if(_list, &Task::isGood) > 0); + _isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong); + + const auto lastEmpty = !_list.empty() && _list.back()->isEmpty(); + _addedCount = _list.size() + - (lastEmpty ? 1 : 0) + - (_existingLocked ? _existingCount : 0); +} + +int Tasks::findField(not_null field) const { + const auto result = ranges::find( + _list, + field, + &Task::field) - begin(_list); + + Ensures(result >= 0 && result < _list.size()); + return result; +} + +void Tasks::checkLastTask() { + removeEmptyTail(); + addEmptyTask(); +} + +} // namespace + +EditTodoListBox::EditTodoListBox( + QWidget*, + not_null controller, + rpl::producer starsRequired, + Api::SendType sendType, + SendMenu::Details sendMenuDetails) +: _controller(controller) +, _sendType(sendType) +, _sendMenuDetails([result = sendMenuDetails] { return result; }) +, _starsRequired(std::move(starsRequired)) +, _titleLimit(controller->session().appConfig().todoListTitleLimit()) { +} + +EditTodoListBox::EditTodoListBox( + QWidget*, + not_null controller, + not_null item) +: _controller(controller) +, _sendMenuDetails([] { return SendMenu::Details(); }) +, _editingItem(item) +, _titleLimit(controller->session().appConfig().todoListTitleLimit()) { + _controller->session().changes().messageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + if (update.item == item) { + closeBox(); + } + }, lifetime()); +} + +auto EditTodoListBox::submitRequests() const -> rpl::producer { + return _submitRequests.events(); +} + +void EditTodoListBox::setInnerFocus() { + _setInnerFocus(); +} + +void EditTodoListBox::submitFailed(const QString &error) { + showToast(error); +} + +not_null EditTodoListBox::setupTitle( + not_null container) { + using namespace Settings; + + const auto session = &_controller->session(); + const auto isPremium = session->premium(); + const auto title = container->add( + object_ptr( + container, + st::createPollField, + Ui::InputField::Mode::MultiLine, + tr::lng_todo_create_title_placeholder()), + st::createPollFieldPadding + + (isPremium + ? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0) + : QMargins())); + InitField(getDelegate()->outerContainer(), title, session); + title->setMaxLength(_titleLimit + kErrorLimit); + title->setSubmitSettings(Ui::InputField::SubmitSettings::Both); + title->customTab(true); + + if (isPremium) { + _emojiPanel = MakeEmojiPanel( + getDelegate()->outerContainer(), + _controller); + const auto emojiToggle = Ui::AddEmojiToggleToField( + title, + this, + _controller, + _emojiPanel.get(), + st::createPollOptionFieldPremiumEmojiPosition); + _emojiPanel->selector()->emojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { + if (title->hasFocus()) { + Ui::InsertEmojiAtCursor(title->textCursor(), data.emoji); + } + }, emojiToggle->lifetime()); + _emojiPanel->selector()->customEmojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + if (title->hasFocus()) { + Data::InsertCustomEmoji(title, data.document); + } + }, emojiToggle->lifetime()); + } + + const auto media = _editingItem ? _editingItem->media() : nullptr; + if (const auto todolist = media ? media->todolist() : nullptr) { + const auto &text = todolist->title; + title->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); + } + + const auto warning = CreateWarningLabel( + container, + title, + _titleLimit, + kWarnTitleLimit); + rpl::combine( + title->geometryValue(), + warning->sizeValue() + ) | rpl::start_with_next([=](QRect geometry, QSize label) { + warning->moveToLeft( + (container->width() + - label.width() + - st::createPollWarningPosition.x()), + (geometry.y() + - st::createPollFieldPadding.top() + - st::defaultSubsectionTitlePadding.bottom() + - st::defaultSubsectionTitle.style.font->height + + st::defaultSubsectionTitle.style.font->ascent + - st::createPollWarning.style.font->ascent), + geometry.width()); + }, warning->lifetime()); + + return title; +} + +object_ptr EditTodoListBox::setupContent() { + using namespace Settings; + + const auto id = FullMsgId{ + PeerId(), + _controller->session().data().nextNonHistoryEntryId(), + }; + const auto error = lifetime().make_state(Error::Title); + + auto result = object_ptr(this); + const auto container = result.data(); + + const auto title = setupTitle(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + container->add( + object_ptr( + container, + tr::lng_todo_create_list(), + st::defaultSubsectionTitle), + st::createPollFieldTitlePadding); + const auto media = _editingItem ? _editingItem->media() : nullptr; + const auto todolist = media ? media->todolist() : nullptr; + const auto tasks = lifetime().make_state( + this, + container, + _controller, + _emojiPanel ? _emojiPanel.get() : nullptr, + todolist ? todolist->items : std::vector()); + auto limit = tasks->addedCount() | rpl::after_next([=](int count) { + setCloseByEscape(!count); + setCloseByOutsideClick(!count); + }) | rpl::map([=](int count) { + const auto appConfig = &_controller->session().appConfig(); + const auto max = appConfig->todoListItemsLimit(); + return (count < max) + ? tr::lng_todo_create_limit(tr::now, lt_count, max - count) + : tr::lng_todo_create_maximum(tr::now); + }) | rpl::after_next([=] { + container->resizeToWidth(container->widthNoMargins()); + }); + container->add( + object_ptr( + container, + object_ptr( + container, + std::move(limit), + st::boxDividerLabel), + st::createPollLimitPadding)); + + title->tabbed( + ) | rpl::start_with_next([=] { + tasks->focusFirst(); + }, title->lifetime()); + + Ui::AddSkip(container); + Ui::AddSubsectionTitle(container, tr::lng_todo_create_settings()); + + const auto allowAdd = container->add( + object_ptr( + container, + tr::lng_todo_create_allow_add(tr::now), + !todolist || todolist->othersCanAppend(), + st::defaultCheckbox), + st::createPollCheckboxMargin); + const auto allowMark = container->add( + object_ptr( + container, + tr::lng_todo_create_allow_mark(tr::now), + !todolist || todolist->othersCanComplete(), + st::defaultCheckbox), + st::createPollCheckboxMargin); + + tasks->tabbed( + ) | rpl::start_with_next([=] { + title->setFocus(); + }, title->lifetime()); + + const auto isValidTitle = [=] { + const auto text = title->getLastText().trimmed(); + return !text.isEmpty() && (text.size() <= _titleLimit); + }; + title->submits( + ) | rpl::start_with_next([=] { + if (isValidTitle()) { + tasks->focusFirst(); + } + }, title->lifetime()); + + _setInnerFocus = [=] { + title->setFocusFast(); + }; + + const auto collectResult = [=] { + const auto textWithTags = title->getTextWithTags(); + using Flag = TodoListData::Flag; + auto result = TodoListData(&_controller->session().data(), id); + result.title.text = textWithTags.text; + result.title.entities = TextUtilities::ConvertTextTagsToEntities( + textWithTags.tags); + TextUtilities::Trim(result.title); + result.items = tasks->toTodoListItems(); + const auto allowAddTasks = allowAdd->checked(); + const auto allowMarkTasks = allowMark->checked(); + result.setFlags(Flag(0) + | (allowAddTasks ? Flag::OthersCanAppend : Flag(0)) + | (allowMarkTasks ? Flag::OthersCanComplete : Flag(0))); + return result; + }; + const auto collectError = [=] { + if (isValidTitle()) { + *error &= ~Error::Title; + } else { + *error |= Error::Title; + } + if (!tasks->hasTasks()) { + *error |= Error::Tasks; + } else if (!tasks->isValid()) { + *error |= Error::Other; + } else { + *error &= ~(Error::Tasks | Error::Other); + } + }; + const auto showError = [show = uiShow()]( + tr::phrase<> text) { + show->showToast(text(tr::now)); + }; + + const auto send = [=](Api::SendOptions sendOptions) { + collectError(); + if (*error & Error::Title) { + showError(tr::lng_todo_choose_title); + title->setFocus(); + } else if (*error & Error::Tasks) { + showError(tr::lng_todo_choose_tasks); + tasks->focusFirst(); + } else if (!*error) { + if (_editingItem) { + sendOptions = { + .scheduled = (_editingItem->isScheduled() + ? _editingItem->date() + : TimeId()), + .shortcutId = _editingItem->shortcutId(), + }; + } + _submitRequests.fire({ collectResult(), sendOptions }); + } + }; + const auto sendAction = SendMenu::DefaultCallback( + _controller->uiShow(), + crl::guard(this, send)); + + tasks->scrollToWidget( + ) | rpl::start_with_next([=](not_null widget) { + scrollToWidget(widget); + }, lifetime()); + + tasks->backspaceInFront( + ) | rpl::start_with_next([=] { + FocusAtEnd(title); + }, lifetime()); + + const auto isNormal = (_sendType == Api::SendType::Normal); + const auto schedule = [=] { + sendAction( + { .type = SendMenu::ActionType::Schedule }, + _sendMenuDetails()); + }; + const auto submit = addButton( + (_editingItem + ? tr::lng_settings_save() + : tr::lng_todo_create_button()), + [=] { isNormal ? send({}) : schedule(); }); + submit->setText(PaidSendButtonText(_starsRequired.value(), _editingItem + ? tr::lng_settings_save() + : isNormal + ? tr::lng_todo_create_button() + : tr::lng_schedule_button())); + const auto sendMenuDetails = [=] { + collectError(); + return (*error) ? SendMenu::Details() : _sendMenuDetails(); + }; + SendMenu::SetupMenuAndShortcuts( + submit.data(), + _controller->uiShow(), + sendMenuDetails, + sendAction); + addButton(tr::lng_cancel(), [=] { closeBox(); }); + + return result; +} + +void EditTodoListBox::prepare() { + setTitle(tr::lng_todo_create_title()); + + const auto inner = setInnerWidget(setupContent()); + + setDimensionsToContent(st::boxWideWidth, inner); +} + +AddTodoListTasksBox::AddTodoListTasksBox( + QWidget*, + not_null controller, + not_null item) +: _controller(controller) +, _item(item) { + _controller->session().changes().messageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + if (update.item == item) { + closeBox(); + } + }, lifetime()); +} + +void AddTodoListTasksBox::prepare() { + setTitle(tr::lng_todo_add_title()); + + const auto inner = setInnerWidget(setupContent()); + + setDimensionsToContent(st::boxWideWidth, inner); + + scrollToY(ScrollMax); +} + +object_ptr AddTodoListTasksBox::setupContent() { + auto result = object_ptr(this); + const auto container = result.data(); + + if (_controller->session().premium()) { + _emojiPanel = MakeEmojiPanel( + getDelegate()->outerContainer(), + _controller); + } + + const auto media = _item->media(); + const auto todolist = media ? media->todolist() : nullptr; + Assert(todolist != nullptr); + const auto tasks = lifetime().make_state( + this, + container, + _controller, + _emojiPanel ? _emojiPanel.get() : nullptr, + todolist->items, + true); + const auto already = int(todolist->items.size()); + auto limit = tasks->addedCount() | rpl::after_next([=](int count) { + setCloseByEscape(!count); + setCloseByOutsideClick(!count); + }) | rpl::map([=](int count) { + const auto appConfig = &_controller->session().appConfig(); + const auto max = appConfig->todoListItemsLimit(); + const auto total = already + count; + return (total < max) + ? tr::lng_todo_create_limit(tr::now, lt_count, max - total) + : tr::lng_todo_create_maximum(tr::now); + }) | rpl::after_next([=] { + container->resizeToWidth(container->widthNoMargins()); + }); + container->add( + object_ptr( + container, + object_ptr( + container, + std::move(limit), + st::boxDividerLabel), + st::createPollLimitPadding)); + + _setInnerFocus = [=] { + tasks->focusFirst(); + }; + + tasks->scrollToWidget( + ) | rpl::start_with_next([=](not_null widget) { + scrollToWidget(widget); + }, lifetime()); + + const auto submit = addButton(tr::lng_settings_save(), [=] { + _submitRequests.fire({ tasks->toTodoListItems() }); + }); + addButton(tr::lng_cancel(), [=] { closeBox(); }); + + return result; +} + +auto AddTodoListTasksBox::submitRequests() const -> rpl::producer { + return _submitRequests.events(); +} + +void AddTodoListTasksBox::setInnerFocus() { + _setInnerFocus(); +} diff --git a/Telegram/SourceFiles/boxes/edit_todo_list_box.h b/Telegram/SourceFiles/boxes/edit_todo_list_box.h new file mode 100644 index 0000000000..45ec17f9d8 --- /dev/null +++ b/Telegram/SourceFiles/boxes/edit_todo_list_box.h @@ -0,0 +1,112 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/layers/box_content.h" +#include "api/api_common.h" +#include "data/data_todo_list.h" +#include "base/flags.h" + +struct TodoListData; + +namespace ChatHelpers { +class TabbedPanel; +} // namespace ChatHelpers + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace SendMenu { +struct Details; +} // namespace SendMenu + +class EditTodoListBox : public Ui::BoxContent { +public: + struct Result { + TodoListData todolist; + Api::SendOptions options; + }; + + EditTodoListBox( + QWidget*, + not_null controller, + rpl::producer starsRequired, + Api::SendType sendType, + SendMenu::Details sendMenuDetails); + EditTodoListBox( + QWidget*, + not_null controller, + not_null item); + + [[nodiscard]] rpl::producer submitRequests() const; + void submitFailed(const QString &error); + + void setInnerFocus() override; + +protected: + void prepare() override; + +private: + enum class Error { + Title = 0x01, + Tasks = 0x02, + Other = 0x04, + }; + friend constexpr inline bool is_flag_type(Error) { return true; } + using Errors = base::flags; + + [[nodiscard]] object_ptr setupContent(); + [[nodiscard]] not_null setupTitle( + not_null container); + + const not_null _controller; + const Api::SendType _sendType = Api::SendType(); + const Fn _sendMenuDetails; + HistoryItem *_editingItem = nullptr; + rpl::variable _starsRequired; + base::unique_qptr _emojiPanel; + Fn _setInnerFocus; + Fn()> _dataIsValidValue; + rpl::event_stream _submitRequests; + int _titleLimit = 0; + +}; + +class AddTodoListTasksBox : public Ui::BoxContent { +public: + struct Result { + std::vector items; + }; + + AddTodoListTasksBox( + QWidget*, + not_null controller, + not_null item); + + [[nodiscard]] rpl::producer submitRequests() const; + + void setInnerFocus() override; + +protected: + void prepare() override; + +private: + [[nodiscard]] object_ptr setupContent(); + + const not_null _controller; + const not_null _item; + base::unique_qptr _emojiPanel; + Fn _setInnerFocus; + rpl::event_stream _submitRequests; + +}; diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index 761b387e53..8df343cec4 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -99,7 +99,7 @@ void GiftCreditsBox( Main::MakeSessionShow(box->uiShow(), &peer->session()), box->verticalLayout(), peer, - StarsAmount(), + CreditsAmount(), [=] { gifted(); box->uiShow()->hideLayer(); }, tr::lng_credits_summary_options_subtitle(), {}); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index cb5dee6e26..425e816af4 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -76,6 +76,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); +constexpr auto kHorizontalBar = QChar(0x2015); [[nodiscard]] QString CreateMessageLink( not_null session, @@ -95,6 +96,10 @@ constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); return QString(); }; +[[nodiscard]] QString FixupTransactionId(QString origin) { + return origin.replace(kHorizontalBar, QChar('-')); +} + [[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink( not_null session, const QString &slug) { @@ -135,9 +140,10 @@ constexpr auto kRarityTooltipDuration = 3 * crl::time(1000); [[nodiscard]] object_ptr MakeMaybeMultilineTokenValue( not_null table, - const QString &token, + QString token, Settings::CreditsEntryBoxStyleOverrides st) { constexpr auto kOneLineCount = 24; + token = token.replace(QChar('-'), kHorizontalBar); const auto oneLine = token.length() <= kOneLineCount; return object_ptr( table, @@ -411,7 +417,7 @@ void AddTableRow( table->st().defaultValue.style.font->height); const auto label = Ui::CreateChild( raw, - Lang::FormatStarsAmountDecimal(entry.credits), + Lang::FormatCreditsAmountDecimal(entry.credits), table->st().defaultValue, st::defaultPopupMenu); @@ -1381,7 +1387,7 @@ void AddStarGiftTable( auto label = MakeMaybeMultilineTokenValue(table, address, st); label->setClickHandlerFilter([=](const auto &...) { TextUtilities::SetClipboardText( - TextForMimeData::Simple(address)); + TextForMimeData::Simple(FixupTransactionId(address))); show->showToast( tr::lng_gift_unique_address_copied(tr::now)); return false; @@ -1630,8 +1636,8 @@ void AddCreditsHistoryEntryTable( const auto full = int(base::SafeRound(entry.credits.value() / (1. - (entry.starrefCommission / 1000.)))); auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored); - const auto starsText = Lang::FormatStarsAmountDecimal( - StarsAmount{ full }); + const auto starsText = Lang::FormatCreditsAmountDecimal( + CreditsAmount{ full }); AddTableRow( table, tr::lng_credits_box_history_entry_gift_full_price(), @@ -1729,7 +1735,7 @@ void AddCreditsHistoryEntryTable( (entry.gift ? tr::lng_credits_box_history_entry_peer_in : tr::lng_credits_box_history_entry_via)(), - (entry.gift + ((entry.gift && entry.credits.stars()) ? tr::lng_credits_box_history_entry_anonymous : tr::lng_credits_box_history_entry_fragment)( Ui::Text::RichLangValue)); @@ -1787,7 +1793,7 @@ void AddCreditsHistoryEntryTable( auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored); const auto full = (entry.in ? 1 : -1) * (entry.credits + entry.paidMessagesAmount); - const auto starsText = Lang::FormatStarsAmountDecimal(full); + const auto starsText = Lang::FormatCreditsAmountDecimal(full); AddTableRow( table, tr::lng_credits_paid_messages_full(), @@ -1806,7 +1812,7 @@ void AddCreditsHistoryEntryTable( auto label = MakeMaybeMultilineTokenValue(table, entry.id, st); label->setClickHandlerFilter([=](const auto &...) { TextUtilities::SetClipboardText( - TextForMimeData::Simple(entry.id)); + TextForMimeData::Simple(FixupTransactionId(entry.id))); show->showToast( tr::lng_credits_box_history_entry_id_copied(tr::now)); return false; @@ -1981,3 +1987,29 @@ void AddCreditsBoostTable( rpl::single(Ui::Text::WithEntities(langDateTime(b.expiresAt)))); } } + +void AddChannelEarnTable( + std::shared_ptr show, + not_null container, + const Data::CreditsHistoryEntry &entry) { + const auto table = container->add( + object_ptr( + container, + st::giveawayGiftCodeTable), + st::giveawayGiftCodeTableMargin); + if (!entry.id.isEmpty()) { + auto label = MakeMaybeMultilineTokenValue(table, entry.id, {}); + label->setClickHandlerFilter([=](const auto &...) { + TextUtilities::SetClipboardText( + TextForMimeData::Simple(FixupTransactionId(entry.id))); + show->showToast( + tr::lng_credits_box_history_entry_id_copied(tr::now)); + return false; + }); + AddTableRow( + table, + tr::lng_credits_box_history_entry_id(), + std::move(label), + st::giveawayGiftCodeValueMargin); + } +} diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.h b/Telegram/SourceFiles/boxes/gift_premium_box.h index ec3282e241..df0c17c1c1 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.h +++ b/Telegram/SourceFiles/boxes/gift_premium_box.h @@ -34,6 +34,7 @@ struct CreditsEntryBoxStyleOverrides; } // namespace Settings namespace Ui { +class Show; class GenericBox; class VerticalLayout; } // namespace Ui @@ -100,3 +101,8 @@ void AddCreditsBoostTable( not_null container, Settings::CreditsEntryBoxStyleOverrides st, const Data::Boost &boost); + +void AddChannelEarnTable( + std::shared_ptr show, + not_null container, + const Data::CreditsHistoryEntry &entry); diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index fbe6ee72b4..071b9dd684 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_participant_status.h" #include "data/data_histories.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" @@ -565,15 +566,7 @@ bool CanCreateModerateMessagesBox(const HistoryItemsList &items) { && !options.participants.empty(); } -void DeleteChatBox(not_null box, not_null peer) { - const auto container = box->verticalLayout(); - - const auto maybeUser = peer->asUser(); - const auto isBot = maybeUser && maybeUser->isBot(); - - Ui::AddSkip(container); - Ui::AddSkip(container); - +void SafeSubmitOnEnter(not_null box) { base::install_event_filter(box, [=](not_null event) { if (event->type() == QEvent::KeyPress) { if (const auto k = static_cast(event.get())) { @@ -587,17 +580,31 @@ void DeleteChatBox(not_null box, not_null peer) { }, .confirmText = tr::lng_box_yes(), .cancelText = tr::lng_box_no(), - })); + })); } } } return base::EventFilterResult::Continue; }); +} + +void DeleteChatBox(not_null box, not_null peer) { + const auto container = box->verticalLayout(); + + const auto userpicPeer = peer->userpicPaintingPeer(); + const auto maybeUser = peer->asUser(); + const auto isBot = maybeUser && maybeUser->isBot(); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + SafeSubmitOnEnter(box); const auto userpic = Ui::CreateChild( container, - peer, - st::mainMenuUserpic); + userpicPeer, + st::mainMenuUserpic, + peer->userpicShape()); userpic->showSavedMessagesOnSelf(true); Ui::IconWithTitle( container, @@ -609,7 +616,7 @@ void DeleteChatBox(not_null box, not_null peer) { : maybeUser ? tr::lng_profile_delete_conversation() | Ui::Text::ToBold() : rpl::single( - peer->name() + userpicPeer->name() ) | Ui::Text::ToBold() | rpl::type_erased(), box->getDelegate()->style().title)); @@ -754,3 +761,54 @@ void DeleteChatBox(not_null box, not_null peer) { }, st::attentionBoxButton); box->addButton(tr::lng_cancel(), close); } + +void DeleteSublistBox( + not_null box, + not_null sublist) { + const auto container = box->verticalLayout(); + + const auto weak = base::make_weak(sublist.get()); + const auto peer = sublist->sublistPeer(); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + SafeSubmitOnEnter(box); + + const auto userpic = Ui::CreateChild( + container, + peer, + st::mainMenuUserpic); + Ui::IconWithTitle( + container, + userpic, + Ui::CreateChild( + container, + tr::lng_profile_delete_conversation() | Ui::Text::ToBold(), + box->getDelegate()->style().title)); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + box->addRow( + object_ptr( + container, + tr::lng_sure_delete_history( + lt_contact, + rpl::single(peer->name())), + st::boxLabel)); + + Ui::AddSkip(container); + + const auto close = crl::guard(box, [=] { box->closeBox(); }); + box->addButton(tr::lng_box_delete(), [=] { + const auto strong = weak.get(); + const auto parentChat = strong ? strong->parentChat() : nullptr; + if (!parentChat) { + return; + } + peer->session().api().deleteSublistHistory(parentChat, peer); + close(); + }, st::attentionBoxButton); + box->addButton(tr::lng_cancel(), close); +} diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.h b/Telegram/SourceFiles/boxes/moderate_messages_box.h index eb24026125..64e544e4b3 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.h +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; +namespace Data { +class SavedSublist; +} // namespace Data + namespace Ui { class GenericBox; } // namespace Ui @@ -21,3 +25,6 @@ void CreateModerateMessagesBox( [[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &); void DeleteChatBox(not_null box, not_null peer); +void DeleteSublistBox( + not_null box, + not_null sublist); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 33cba88f34..41b85c2810 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -718,7 +718,7 @@ void PeerListRow::elementsPaint( } QString PeerListRow::generateName() { - return peer()->name(); + return peer()->userpicPaintingPeer()->name(); } QString PeerListRow::generateShortName() { @@ -728,12 +728,12 @@ QString PeerListRow::generateShortName() { ? tr::lng_replies_messages(tr::now) : _isVerifyCodesChat ? tr::lng_verification_codes(tr::now) - : peer()->shortName(); + : peer()->userpicPaintingPeer()->shortName(); } Ui::PeerUserpicView &PeerListRow::ensureUserpicView() { - if (!_userpic.cloud && peer()->hasUserpic()) { - _userpic = peer()->createUserpicView(); + if (!_userpic.cloud && peer()->userpicPaintingPeer()->hasUserpic()) { + _userpic = peer()->userpicPaintingPeer()->createUserpicView(); } return _userpic; } @@ -742,7 +742,7 @@ PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback( bool forceRound) { const auto saved = !_savedMessagesStatus.isEmpty(); const auto replies = _isRepliesMessagesChat; - const auto peer = this->peer(); + const auto peer = this->peer()->userpicPaintingPeer(); auto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView(); if (forceRound && peer->isForum()) { return ForceRoundUserpicCallback(peer); @@ -820,6 +820,9 @@ int PeerListRow::paintNameIconGetWidth( ? st::dialogsPremiumIcon.over : st::dialogsPremiumIcon.icon), .scam = &(selected ? st::dialogsScamFgOver : st::dialogsScamFg), + .direct = &(selected + ? st::windowSubTextFgOver + : st::windowSubTextFg), .premiumFg = &(selected ? st::dialogsVerifiedIconBgOver : st::dialogsVerifiedIconBg), diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 3b22147532..37ab361a8f 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -23,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "main/main_session.h" #include "data/data_peer_values.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_channel.h" @@ -867,6 +869,45 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { *weak = owned.data(); delegate()->peerListUiShow()->showBox(std::move(owned)); return; + } else if (const auto monoforum = peer->monoforum()) { + const auto weak = std::make_shared>(); + auto callback = [=](not_null sublist) { + const auto exists = guard.get(); + if (!exists) { + if (*weak) { + (*weak)->closeBox(); + } + return; + } + auto onstack = std::move(_callback); + onstack(sublist); + if (guard) { + _callback = std::move(onstack); + } else if (*weak) { + (*weak)->closeBox(); + } + }; + const auto filter = [=](not_null sublist) { + return guard && (!_filter || _filter(sublist)); + }; + auto owned = Box( + std::make_unique( + monoforum, + std::move(callback), + filter), + [=](not_null box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + monoforum->destroyed( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }); + *weak = owned.data(); + delegate()->peerListUiShow()->showBox(std::move(owned)); + return; } const auto history = peer->owner().history(peer); auto callback = std::move(_callback); @@ -1137,6 +1178,111 @@ auto ChooseTopicBoxController::createRow(not_null topic) return skip ? nullptr : std::make_unique(topic); }; +ChooseSublistBoxController::ChooseSublistBoxController( + not_null monoforum, + FnMut)> callback, + Fn)> filter) +: _monoforum(monoforum) +, _callback(std::move(callback)) +, _filter(std::move(filter)) { + setStyleOverrides(&st::chooseTopicList); + + _monoforum->chatsListChanges( + ) | rpl::start_with_next([=] { + refreshRows(); + }, lifetime()); + + _monoforum->sublistDestroyed( + ) | rpl::start_with_next([=](not_null sublist) { + const auto id = sublist->sublistPeer()->id.value; + if (const auto row = delegate()->peerListFindRow(id)) { + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + } + }, lifetime()); +} + +Main::Session &ChooseSublistBoxController::session() const { + return _monoforum->session(); +} + +void ChooseSublistBoxController::rowClicked(not_null row) { + const auto weak = base::make_weak(this); + auto onstack = base::take(_callback); + onstack(_monoforum->sublist(row->peer())); + if (weak) { + _callback = std::move(onstack); + } +} + +void ChooseSublistBoxController::prepare() { + delegate()->peerListSetTitle(tr::lng_forward_choose()); + setSearchNoResultsText(tr::lng_topics_not_found(tr::now)); + delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); + refreshRows(true); + + session().changes().entryUpdates( + Data::EntryUpdate::Flag::Repaint + ) | rpl::start_with_next([=](const Data::EntryUpdate &update) { + if (const auto sublist = update.entry->asSublist()) { + if (sublist->parent() == _monoforum) { + const auto id = sublist->sublistPeer()->id.value; + if (const auto row = delegate()->peerListFindRow(id)) { + delegate()->peerListUpdateRow(row); + } + } + } + }, lifetime()); +} + +void ChooseSublistBoxController::refreshRows(bool initial) { + auto added = false; + for (const auto &row : _monoforum->chatsList()->indexed()->all()) { + if (const auto sublist = row->sublist()) { + const auto id = sublist->sublistPeer()->id.value; + auto already = delegate()->peerListFindRow(id); + if (initial || !already) { + if (auto created = createRow(sublist)) { + delegate()->peerListAppendRow(std::move(created)); + added = true; + } + } else if (already->isSearchResult()) { + delegate()->peerListAppendFoundRow(already); + added = true; + } + } + } + if (added) { + delegate()->peerListRefreshRows(); + } +} + +void ChooseSublistBoxController::loadMoreRows() { + _monoforum->loadMore(); +} + +std::unique_ptr ChooseSublistBoxController::createSearchRow( + PeerListRowId id) { + const auto peer = session().data().peer(PeerId(id)); + if (const auto sublist = _monoforum->sublistLoaded(peer)) { + auto result = std::make_unique(sublist->sublistPeer()); + result->setCustomStatus(QString()); + return result; + } + return nullptr; +} + +auto ChooseSublistBoxController::createRow( + not_null sublist) +-> std::unique_ptr { + if (_filter && !_filter(sublist)) { + return nullptr; + } + auto result = std::make_unique(sublist->sublistPeer()); + result->setCustomStatus(QString()); + return result; +}; + void PaintRestrictionBadge( Painter &p, not_null st, diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index de9c67dbfe..24887d3df2 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -27,6 +27,8 @@ namespace Data { class Thread; class Forum; class ForumTopic; +class SavedSublist; +class SavedMessages; } // namespace Data namespace Ui { @@ -393,3 +395,30 @@ private: Fn)> _filter; }; + +class ChooseSublistBoxController final + : public PeerListController + , public base::has_weak_ptr { +public: + ChooseSublistBoxController( + not_null monoforum, + FnMut)> callback, + Fn)> filter = nullptr); + + Main::Session &session() const override; + void rowClicked(not_null row) override; + + void prepare() override; + void loadMoreRows() override; + std::unique_ptr createSearchRow(PeerListRowId id) override; + +private: + void refreshRows(bool initial = false); + [[nodiscard]] std::unique_ptr createRow( + not_null sublist); + + const not_null _monoforum; + FnMut)> _callback; + Fn)> _filter; + +}; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 25853e0f85..ab05d9bccb 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -1522,6 +1522,7 @@ void AddSpecialBoxController::editAdminDone( } _additional.applyAdminLocally(user, rights, rank); + // _adminDoneCallback should call changes().chatAdminUpdated. if (const auto callback = _adminDoneCallback) { callback(user, rights, rank); } diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index bc336675f8..7257a3c3f8 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -75,16 +75,12 @@ using RightsMap = std::vector>>; using Flag = ChatAdminRight; return { { Flag::ChangeInfo, tr::lng_request_group_change_info }, - { - Flag::DeleteMessages, - tr::lng_request_group_delete_messages }, + { Flag::DeleteMessages, tr::lng_request_group_delete_messages }, { Flag::BanUsers, tr::lng_request_group_ban_users }, { Flag::InviteByLinkOrAdd, tr::lng_request_group_invite }, { Flag::PinMessages, tr::lng_request_group_pin_messages }, { Flag::ManageTopics, tr::lng_request_group_manage_topics }, - { - Flag::ManageCall, - tr::lng_request_group_manage_video_chats }, + { Flag::ManageCall, tr::lng_request_group_manage_video_chats }, { Flag::Anonymous, tr::lng_request_group_anonymous }, { Flag::AddAdmins, tr::lng_request_group_add_admins }, }; @@ -94,21 +90,12 @@ using RightsMap = std::vector>>; using Flag = ChatAdminRight; return { { Flag::ChangeInfo, tr::lng_request_channel_change_info }, - { - Flag::PostMessages, - tr::lng_request_channel_post_messages }, - { - Flag::EditMessages, - tr::lng_request_channel_edit_messages }, - { - Flag::DeleteMessages, - tr::lng_request_channel_delete_messages }, - { - Flag::InviteByLinkOrAdd, - tr::lng_request_channel_add_subscribers }, - { - Flag::ManageCall, - tr::lng_request_channel_manage_livestreams }, + { Flag::PostMessages, tr::lng_request_channel_post_messages }, + { Flag::EditMessages, tr::lng_request_channel_edit_messages }, + { Flag::DeleteMessages, tr::lng_request_channel_delete_messages }, + { Flag::InviteByLinkOrAdd, tr::lng_request_channel_add_subscribers }, + { Flag::ManageCall, tr::lng_request_channel_manage_livestreams }, + { Flag::ManageDirect, tr::lng_request_channel_manage_direct }, { Flag::AddAdmins, tr::lng_request_channel_add_admins }, }; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp index ab2089b6a1..7b9779e5ba 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_discussion_link_box.cpp @@ -273,7 +273,7 @@ void Controller::choose(not_null chat) { above, tr::lng_manage_discussion_group_create(), st::infoCreateDiscussionLinkButton, - { &st::menuIconGroupCreate } + { &st::menuBlueIconGroupCreate } )->addClickHandler([=, parent = above.data()] { const auto guarded = crl::guard(parent, callback); navigation->uiShow()->showBox(Box( @@ -293,10 +293,10 @@ void Controller::choose(not_null chat) { ? tr::lng_manage_discussion_group_unlink : tr::lng_manage_linked_channel_unlink)(), st::infoUnlinkDiscussionLinkButton, - { &st::menuIconRemove } + { &st::menuIconRemoveAttention } )->addClickHandler([=] { callback(nullptr); }); - Ui::AddSkip(below); } + Ui::AddSkip(below); Ui::AddDividerText( below, (channel->isBroadcast() diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index fe675532a1..45ad6b1931 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -27,7 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "main/main_session.h" #include "history/history.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_sticker_toast.h" #include "lang/lang_keys.h" #include "info/profile/info_profile_emoji_status_panel.h" @@ -93,11 +93,12 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) { const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal) ? st::normalForumTopicIcon : st::defaultForumTopicIcon; + const auto general = Data::IsForumGeneralIconTitle(_icon.title); if (_image.isNull()) { - _image = Data::IsForumGeneralIconTitle(_icon.title) + _image = general ? Data::ForumTopicGeneralIconFrame( st.size, - Data::ParseForumGeneralIconColor(_icon.colorId)) + QColor(255, 255, 255)) : Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st); } const auto full = (_tag == Data::CustomEmojiSizeTag::Normal) @@ -106,7 +107,9 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) { const auto esize = full / style::DevicePixelRatio(); const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize); const auto skip = (customSize - st.size) / 2; - p.drawImage(context.position + QPoint(skip, skip), _image); + p.drawImage(context.position + QPoint(skip, skip), general + ? style::colorizeImage(_image, context.textColor) + : _image); } void DefaultIconEmoji::unload() { @@ -518,13 +521,15 @@ void EditForumTopicBox( title->showError(); return; } + using namespace HistoryView; controller->showSection( - std::make_shared( - forum, - channel->forum()->reserveCreatingId( + std::make_shared(ChatViewId{ + .history = forum, + .repliesRootId = channel->forum()->reserveCreatingId( title->getLastText().trimmed(), state->defaultIcon.current().colorId, - state->iconId.current())), + state->iconId.current()), + }), Window::SectionShow::Way::ClearStack); }; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index e4166a82c0..50c3254a37 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -245,7 +245,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::EditStories | Flag::DeleteStories | Flag::InviteByLinkOrAdd - | Flag::ManageCall) }; + | Flag::ManageCall + | Flag::ManageDirect) }; } void EditAdminBox::prepare() { diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index e168e800e0..03fbc87c68 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -152,8 +152,7 @@ void SaveChannelAdmin( channel->session().api().request(MTPchannels_EditAdmin( channel->inputChannel, user->inputUser, - MTP_chatAdminRights(MTP_flags( - MTPDchatAdminRights::Flags::from_raw(uint32(newRights.flags)))), + AdminRightsToMTP(newRights), MTP_string(rank) )).done([=](const MTPUpdates &result) { channel->session().api().applyUpdates(result); @@ -461,6 +460,7 @@ void ParticipantsAdditionalData::setExternal( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); _admins.erase(user); } _restrictedRights.erase(participant); @@ -538,6 +538,7 @@ void ParticipantsAdditionalData::fillFromChannel( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); _restrictedRights.emplace(user, restricted->second.rights); } } @@ -743,6 +744,7 @@ UserData *ParticipantsAdditionalData::applyRegular(UserId userId) { _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); _restrictedRights.erase(user); _kicked.erase(user); _restrictedBy.erase(user); @@ -761,6 +763,7 @@ PeerData *ParticipantsAdditionalData::applyBanned( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); } if (data.isKicked()) { _kicked.emplace(participant); @@ -1270,6 +1273,33 @@ void ParticipantsBoxController::prepare() { } else { rebuild(); } + + _peer->session().changes().chatAdminChanges( + ) | rpl::start_with_next([=](const Data::ChatAdminChange &update) { + if (update.peer != _peer) { + return; + } + const auto user = update.user; + const auto rights = ChatAdminRightsInfo(update.rights); + const auto rank = update.rank; + _additional.applyAdminLocally(user, rights, rank); + if (!_additional.isCreator(user) || !user->isSelf()) { + if (!rights.flags) { + if (_role == Role::Admins) { + removeRow(user); + } + } else { + if (_role == Role::Admins) { + prependRow(user); + } else if (_role == Role::Kicked + || _role == Role::Restricted) { + removeRow(user); + } + } + } + recomputeTypeFor(user); + refreshRows(); + }, lifetime()); } void ParticipantsBoxController::unload() { @@ -1800,23 +1830,8 @@ void ParticipantsBoxController::editAdminDone( if (_editParticipantBox) { _editParticipantBox->closeBox(); } - - _additional.applyAdminLocally(user, rights, rank); - if (!_additional.isCreator(user) || !user->isSelf()) { - if (!rights.flags) { - if (_role == Role::Admins) { - removeRow(user); - } - } else { - if (_role == Role::Admins) { - prependRow(user); - } else if (_role == Role::Kicked || _role == Role::Restricted) { - removeRow(user); - } - } - } - recomputeTypeFor(user); - refreshRows(); + const auto flags = rights.flags; + user->session().changes().chatAdminChanged(_peer, user, flags, rank); } void ParticipantsBoxController::showRestricted(not_null user) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index c1c160ac08..29019c1091 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -574,15 +574,15 @@ void Set( MTP_flags(Flag::f_color | Flag::f_background_emoji_id), MTP_int(values.colorIndex), MTP_long(values.backgroundEmojiId))); - } else if (peer->isMegagroup()) { } else if (const auto channel = peer->asChannel()) { - using Flag = MTPchannels_UpdateColor::Flag; - send(MTPchannels_UpdateColor( - MTP_flags(Flag::f_color | Flag::f_background_emoji_id), - channel->inputChannel, - MTP_int(values.colorIndex), - MTP_long(values.backgroundEmojiId))); - + if (peer->isBroadcast()) { + using Flag = MTPchannels_UpdateColor::Flag; + send(MTPchannels_UpdateColor( + MTP_flags(Flag::f_color | Flag::f_background_emoji_id), + channel->inputChannel, + MTP_int(values.colorIndex), + MTP_long(values.backgroundEmojiId))); + } if (values.statusChanged && (values.statusId || peer->emojiStatusId())) { peer->owner().emojiStatuses().set( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 49ed1a1682..b8e78881fe 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -26,8 +26,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_requests_box.h" #include "boxes/peers/edit_peer_reactions.h" #include "boxes/peers/replace_boost_box.h" +#include "boxes/peers/toggle_topics_box.h" #include "boxes/peers/verify_peers_box.h" #include "boxes/peer_list_controllers.h" +#include "boxes/edit_privacy_box.h" // EditDirectMessagesPriceBox #include "boxes/stickers_box.h" #include "boxes/username_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -80,6 +82,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "api/api_invite_links.h" #include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" @@ -131,7 +134,7 @@ void AddButtonWithCount( not_null AddButtonWithText( not_null parent, rpl::producer &&text, - rpl::producer &&label, + rpl::producer &&label, Fn callback, Settings::IconDescriptor &&descriptor) { return parent->add(EditPeerInfoBox::CreateButton( @@ -143,6 +146,20 @@ not_null AddButtonWithText( std::move(descriptor))); } +not_null AddButtonWithText( + not_null parent, + rpl::producer &&text, + rpl::producer &&label, + Fn callback, + Settings::IconDescriptor &&descriptor) { + return AddButtonWithText( + parent, + std::move(text), + std::move(label) | Ui::Text::ToWithEntities(), + std::move(callback), + std::move(descriptor)); +} + void AddButtonDelete( not_null parent, rpl::producer &&text, @@ -166,10 +183,7 @@ void SaveDefaultRestrictions( const auto requestId = api->request( MTPmessages_EditChatDefaultBannedRights( peer->input, - MTP_chatBannedRights( - MTP_flags( - MTPDchatBannedRights::Flags::from_raw(uint32(rights))), - MTP_int(0))) + RestrictionsToMTP({ rights, 0 })) ).done([=](const MTPUpdates &result) { api->clearModifyRequest(key); api->applyUpdates(result); @@ -220,27 +234,43 @@ void SaveSlowmodeSeconds( } void SaveStarsPerMessage( + std::shared_ptr show, not_null channel, int starsPerMessage, - Fn done) { + Fn done) { const auto api = &channel->session().api(); const auto key = Api::RequestKey("stars_per_message", channel->id); + const auto broadcast = channel->isBroadcast(); + + using Flag = MTPchannels_UpdatePaidMessagesPrice::Flag; + const auto broadcastAllowed = broadcast && (starsPerMessage >= 0); const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice( + MTP_flags(broadcastAllowed + ? Flag::f_broadcast_messages_allowed + : Flag(0)), channel->inputChannel, MTP_long(starsPerMessage) )).done([=](const MTPUpdates &result) { api->clearModifyRequest(key); api->applyUpdates(result); - channel->setStarsPerMessage(starsPerMessage); - done(); + if (!broadcast) { + channel->owner().editStarsPerMessage(channel, starsPerMessage); + } + done(true); }).fail([=](const MTP::Error &error) { api->clearModifyRequest(key); if (error.type() != u"CHAT_NOT_MODIFIED"_q) { - return; + show->showToast(error.type()); + done(false); + } else { + if (!broadcast) { + channel->owner().editStarsPerMessage( + channel, + starsPerMessage); + } + done(true); } - channel->setStarsPerMessage(starsPerMessage); - done(); }).send(); api->registerModifyRequest(key, requestId); @@ -280,6 +310,7 @@ void SaveBoostsUnrestrict( void ShowEditPermissions( not_null navigation, not_null peer) { + const auto show = navigation->uiShow(); auto createBox = [=](not_null box) { const auto saving = box->lifetime().make_state(0); const auto save = [=]( @@ -298,7 +329,10 @@ void ShowEditPermissions( channel, result.boostsUnrestrict, close); - SaveStarsPerMessage(channel, result.starsPerMessage, close); + const auto price = result.starsPerMessage; + SaveStarsPerMessage(show, channel, price, [=](bool ok) { + close(); + }); } }; auto done = [=](EditPeerPermissionsBoxResult result) { @@ -328,6 +362,14 @@ void ShowEditPermissions( navigation->parentController()->show(Box(std::move(createBox))); } +[[nodiscard]] int CurrentPricePerDirectMessage( + not_null broadcast) { + const auto monoforumLink = broadcast->monoforumLink(); + return (monoforumLink && !monoforumLink->monoforumDisabled()) + ? monoforumLink->commonStarsPerMessage() + : -1; +} + class Controller : public base::has_weak_ptr { public: Controller( @@ -358,6 +400,7 @@ private: std::optional description; std::optional hiddenPreHistory; std::optional forum; + std::optional forumTabs; std::optional autotranslate; std::optional signatures; std::optional signatureProfiles; @@ -365,6 +408,7 @@ private: std::optional joinToWrite; std::optional requestToJoin; std::optional discussionLink; + std::optional starsPerDirectMessage; }; [[nodiscard]] object_ptr createPhotoAndTitleEdit(); @@ -381,8 +425,10 @@ private: void showEditPeerTypeBox( std::optional> error = {}); void showEditDiscussionLinkBox(); + void showEditDirectMessagesBox(); void fillPrivacyTypeButton(); void fillDiscussionLinkButton(); + void fillDirectMessagesButton(); //void fillInviteLinkButton(); void fillForumButton(); void fillColorIndexButton(); @@ -411,6 +457,7 @@ private: [[nodiscard]] bool validateUsernamesOrder(Saving &to) const; [[nodiscard]] bool validateUsername(Saving &to) const; [[nodiscard]] bool validateDiscussionLink(Saving &to) const; + [[nodiscard]] bool validateDirectMessagesPrice(Saving &to) const; [[nodiscard]] bool validateTitle(Saving &to) const; [[nodiscard]] bool validateDescription(Saving &to) const; [[nodiscard]] bool validateHistoryVisibility(Saving &to) const; @@ -425,6 +472,7 @@ private: void saveUsernamesOrder(); void saveUsername(); void saveDiscussionLink(); + void saveDirectMessagesPrice(); void saveTitle(); void saveDescription(); void saveHistoryVisibility(); @@ -453,9 +501,11 @@ private: std::optional _discussionLinkSavedValue; ChannelData *_discussionLinkOriginalValue = nullptr; bool _channelHasLocationOriginalValue = false; + std::optional> _starsPerDirectMessageSavedValue; std::optional _historyVisibilitySavedValue; std::optional _typeDataSavedValue; std::optional _forumSavedValue; + std::optional _forumTabsSavedValue; std::optional _autotranslateSavedValue; std::optional _signaturesSavedValue; std::optional _signatureProfilesSavedValue; @@ -915,6 +965,20 @@ void Controller::showEditDiscussionLinkBox() { }).send(); } +void Controller::showEditDirectMessagesBox() { + Expects(_peer->isBroadcast()); + Expects(_starsPerDirectMessageSavedValue.has_value()); + + const auto stars = _starsPerDirectMessageSavedValue->current(); + _navigation->parentController()->show(Box( + EditDirectMessagesPriceBox, + _peer->asChannel(), + (stars >= 0) ? stars : std::optional(), + [=](std::optional value) { + *_starsPerDirectMessageSavedValue = value.value_or(-1); + })); +} + void Controller::fillPrivacyTypeButton() { Expects(_controls.buttonsLayout != nullptr); @@ -980,9 +1044,11 @@ void Controller::fillPrivacyTypeButton() { void Controller::fillDiscussionLinkButton() { Expects(_controls.buttonsLayout != nullptr); - _discussionLinkSavedValue = _discussionLinkOriginalValue = _peer->isChannel() - ? _peer->asChannel()->discussionLink() - : nullptr; + _discussionLinkSavedValue + = _discussionLinkOriginalValue + = (_peer->isChannel() + ? _peer->asChannel()->discussionLink() + : nullptr); const auto isGroup = (_peer->isChat() || _peer->isMegagroup()); auto text = !isGroup @@ -1016,6 +1082,36 @@ void Controller::fillDiscussionLinkButton() { { isGroup ? &st::menuIconChannel : &st::menuIconGroups }); _discussionLinkUpdates.fire_copy(*_discussionLinkSavedValue); } + +void Controller::fillDirectMessagesButton() { + Expects(_controls.buttonsLayout != nullptr); + + if (!_peer->isBroadcast() || !_peer->asChannel()->canEditInformation()) { + return; + } + + const auto perMessage = CurrentPricePerDirectMessage(_peer->asChannel()); + _starsPerDirectMessageSavedValue = rpl::variable(perMessage); + + auto label = _starsPerDirectMessageSavedValue->value( + ) | rpl::map([](int starsPerMessage) { + return (starsPerMessage < 0) + ? tr::lng_manage_monoforum_off(Ui::Text::WithEntities) + : !starsPerMessage + ? tr::lng_manage_monoforum_free(Ui::Text::WithEntities) + : rpl::single(Ui::Text::IconEmoji( + &st::starIconEmojiColored + ).append(' ').append( + Lang::FormatCreditsAmountDecimal( + CreditsAmount{ starsPerMessage }))); + }) | rpl::flatten_latest(); + AddButtonWithText( + _controls.buttonsLayout, + tr::lng_manage_monoforum(), + std::move(label), + [=] { showEditDirectMessagesBox(); }, + { .icon = &st::menuIconChats, .newBadge = true }); +} // //void Controller::fillInviteLinkButton() { // Expects(_controls.buttonsLayout != nullptr); @@ -1034,21 +1130,30 @@ void Controller::fillDiscussionLinkButton() { void Controller::fillForumButton() { Expects(_controls.buttonsLayout != nullptr); + _forumSavedValue = _peer->isForum(); + _forumTabsSavedValue = !_peer->isChannel() + || !_peer->isForum() + || _peer->asChannel()->useSubsectionTabs(); + + const auto changes = std::make_shared>(); + const auto label = [=] { + return !*_forumSavedValue + ? tr::lng_manage_monoforum_off(tr::now) + : *_forumTabsSavedValue + ? tr::lng_edit_topics_tabs(tr::now) + : tr::lng_edit_topics_list(tr::now); + }; const auto button = _controls.forumToggle = _controls.buttonsLayout->add( EditPeerInfoBox::CreateButton( _controls.buttonsLayout, tr::lng_forum_topics_switch(), - rpl::single(QString()), + changes->events_starting_with({}) | rpl::map(label), [] {}, st::manageGroupTopicsButton, - { &st::menuIconTopics })); - const auto unlocks = std::make_shared>(); - button->toggleOn( - rpl::single(_peer->isForum()) | rpl::then(unlocks->events()) - )->toggledValue( - ) | rpl::start_with_next([=](bool toggled) { - if (_controls.forumToggleLocked && toggled) { - unlocks->fire(false); + { .icon = &st::menuIconTopics, .newBadge = true })); + + button->setClickedCallback(crl::guard(this, [=] { + if (!*_forumSavedValue && _controls.forumToggleLocked) { if (_discussionLinkSavedValue && *_discussionLinkSavedValue) { ShowForumForDiscussionError(_navigation); } else { @@ -1060,13 +1165,21 @@ void Controller::fillForumButton() { Ui::Text::RichLangValue)); } } else { - _forumSavedValue = toggled; - if (toggled) { - _savingData.hiddenPreHistory = false; - } - refreshHistoryVisibility(); + _navigation->uiShow()->show(Box( + Ui::ToggleTopicsBox, + *_forumSavedValue, + *_forumTabsSavedValue, + crl::guard(this, [=](bool topics, bool topicsTabs) { + _forumSavedValue = topics; + _forumTabsSavedValue = !topics || topicsTabs; + if (topics) { + _savingData.hiddenPreHistory = false; + } + changes->fire({}); + refreshHistoryVisibility(); + }))); } - }, _controls.buttonsLayout->lifetime()); + })); refreshForumToggleLocked(); } @@ -1356,6 +1469,8 @@ void Controller::fillManageSection() { const auto canViewOrEditDiscussionLink = isChannel && (channel->discussionLink() || (channel->isBroadcast() && channel->canEditInformation())); + const auto canEditDirectMessages = isChannel + && (channel->isBroadcast() && channel->canEditInformation()); ::AddSkip(_controls.buttonsLayout, 0); @@ -1367,6 +1482,9 @@ void Controller::fillManageSection() { if (canViewOrEditDiscussionLink) { fillDiscussionLinkButton(); } + if (canEditDirectMessages) { + fillDirectMessagesButton(); + } if (canEditPreHistoryHidden) { fillHistoryVisibilityButton(); } @@ -1727,9 +1845,8 @@ void Controller::fillBotCurrencyButton() { auto &lifetime = _controls.buttonsLayout->lifetime(); const auto state = lifetime.make_state(); - const auto format = [=](uint64 balance) { - return Info::ChannelEarn::MajorPart(balance) - + Info::ChannelEarn::MinorPart(balance); + const auto format = [=](const CreditsAmount &balance) { + return Lang::FormatCreditsAmountDecimal(balance); }; const auto was = _peer->session().credits().balanceCurrency( _peer->id); @@ -1793,7 +1910,7 @@ void Controller::fillBotCreditsButton() { auto &lifetime = _controls.buttonsLayout->lifetime(); const auto state = lifetime.make_state(); if (const auto balance = _peer->session().credits().balance(_peer->id)) { - state->balance = Lang::FormatStarsAmountDecimal(balance); + state->balance = Lang::FormatCreditsAmountDecimal(balance); } const auto wrap = _controls.buttonsLayout->add( @@ -1818,7 +1935,7 @@ void Controller::fillBotCreditsButton() { if (data.balance) { wrap->toggle(true, anim::type::normal); } - state->balance = Lang::FormatStarsAmountDecimal(data.balance); + state->balance = Lang::FormatCreditsAmountDecimal(data.balance); }); } { @@ -1967,6 +2084,7 @@ std::optional Controller::validate() const { if (validateUsernamesOrder(result) && validateUsername(result) && validateDiscussionLink(result) + && validateDirectMessagesPrice(result) && validateTitle(result) && validateDescription(result) && validateHistoryVisibility(result) @@ -2016,6 +2134,14 @@ bool Controller::validateDiscussionLink(Saving &to) const { return true; } +bool Controller::validateDirectMessagesPrice(Saving &to) const { + if (!_starsPerDirectMessageSavedValue) { + return true; + } + to.starsPerDirectMessage = _starsPerDirectMessageSavedValue->current(); + return true; +} + bool Controller::validateTitle(Saving &to) const { if (!_controls.title) { return true; @@ -2056,6 +2182,7 @@ bool Controller::validateForum(Saving &to) const { return true; } to.forum = _forumSavedValue; + to.forumTabs = _forumTabsSavedValue; return true; } @@ -2114,6 +2241,7 @@ void Controller::save() { pushSaveStage([=] { saveUsernamesOrder(); }); pushSaveStage([=] { saveUsername(); }); pushSaveStage([=] { saveDiscussionLink(); }); + pushSaveStage([=] { saveDirectMessagesPrice(); }); pushSaveStage([=] { saveTitle(); }); pushSaveStage([=] { saveDescription(); }); pushSaveStage([=] { saveHistoryVisibility(); }); @@ -2271,6 +2399,29 @@ void Controller::saveDiscussionLink() { }).send(); } +void Controller::saveDirectMessagesPrice() { + const auto channel = _peer->asChannel(); + if (!channel) { + return continueSave(); + } + const auto current = CurrentPricePerDirectMessage(channel); + const auto desired = _savingData.starsPerDirectMessage + ? *_savingData.starsPerDirectMessage + : current; + if (desired == current) { + return continueSave(); + } + const auto show = _navigation->uiShow(); + const auto done = [=](bool ok) { + if (ok) { + continueSave(); + } else { + cancelSave(); + } + }; + SaveStarsPerMessage(show, channel, desired, crl::guard(this, done)); +} + void Controller::saveTitle() { if (!_savingData.title || *_savingData.title == _peer->name()) { return continueSave(); @@ -2477,8 +2628,13 @@ void Controller::togglePreHistoryHidden( void Controller::saveForum() { const auto channel = _peer->asChannel(); + const auto nowForum = _peer->isForum(); + const auto nowForumTabs = !channel + || !nowForum + || channel->useSubsectionTabs(); if (!_savingData.forum - || *_savingData.forum == _peer->isForum()) { + || (*_savingData.forum == nowForum + && *_savingData.forumTabs == nowForumTabs)) { return continueSave(); } else if (!channel) { const auto saveForChannel = [=](not_null channel) { @@ -2495,7 +2651,8 @@ void Controller::saveForum() { } _api.request(MTPchannels_ToggleForum( channel->inputChannel, - MTP_bool(*_savingData.forum) + MTP_bool(*_savingData.forum), + MTP_bool(*_savingData.forum && *_savingData.forumTabs) )).done([=](const MTPUpdates &result) { const auto weak = base::make_weak(this); channel->session().api().applyUpdates(result); @@ -2727,6 +2884,22 @@ object_ptr EditPeerInfoBox::CreateButton( Fn callback, const style::SettingsCountButton &st, Settings::IconDescriptor &&descriptor) { + return CreateButton( + parent, + std::move(text), + std::move(count) | Ui::Text::ToWithEntities(), + std::move(callback), + st, + std::move(descriptor)); +} + +object_ptr EditPeerInfoBox::CreateButton( + not_null parent, + rpl::producer &&text, + rpl::producer &&labelText, + Fn callback, + const style::SettingsCountButton &st, + Settings::IconDescriptor &&descriptor) { auto result = object_ptr( parent, rpl::duplicate(text), @@ -2747,37 +2920,49 @@ object_ptr EditPeerInfoBox::CreateButton( std::move(descriptor)); } - auto labelText = rpl::combine( + const auto label = Ui::CreateChild( + button, + rpl::duplicate(labelText), + st.label); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->show(); + + rpl::combine( rpl::duplicate(text), - std::move(count), + std::move(labelText), button->widthValue() - ) | rpl::map([&st](const QString &text, const QString &count, int width) { + ) | rpl::start_with_next([&st, label]( + const QString &text, + const TextWithEntities &labelText, + int width) { const auto available = width - st.button.padding.left() - (st.button.style.font->spacew * 2) - st.button.style.font->width(text) - st.labelPosition.x(); - const auto required = st.label.style.font->width(count); - return (required > available) - ? st.label.style.font->elided(count, std::max(available, 0)) - : count; - }); + const auto required = label->textMaxWidth(); + label->resizeToWidth(std::min(required, available)); + label->moveToRight( + st.labelPosition.x(), + st.labelPosition.y(), + width); + }, label->lifetime()); if (badge) { rpl::combine( std::move(text), - rpl::duplicate(labelText), + label->widthValue(), button->widthValue() ) | rpl::start_with_next([=]( const QString &text, - const QString &label, + int labelWidth, int width) { const auto space = st.button.style.font->spacew; const auto left = st.button.padding.left() + st.button.style.font->width(text) + space; const auto right = st.labelPosition.x() - + st.label.style.font->width(label) + + labelWidth + (space * 2); const auto available = width - left - right; badge->setVisible(available >= badge->width()); @@ -2791,23 +2976,6 @@ object_ptr EditPeerInfoBox::CreateButton( }, badge->lifetime()); } - const auto label = Ui::CreateChild( - button, - std::move(labelText), - st.label); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - label->show(); - - rpl::combine( - button->widthValue(), - label->widthValue() - ) | rpl::start_with_next([=, &st](int outerWidth, int width) { - label->moveToRight( - st.labelPosition.x(), - st.labelPosition.y(), - outerWidth); - }, label->lifetime()); - return result; } @@ -2824,7 +2992,9 @@ bool EditPeerInfoBox::Available(not_null peer) { // canViewAdmins() is removed, because in supergroups it is // always true and in channels it is equal to canViewBanned(). - + if (channel->isMonoforum()) { + return false; + } return false //|| channel->canViewMembers() //|| channel->canViewAdmins() diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h index b8787afac9..f918547c1b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h @@ -46,6 +46,13 @@ public: Fn callback, const style::SettingsCountButton &st, Settings::IconDescriptor &&descriptor); + [[nodiscard]] static object_ptr CreateButton( + not_null parent, + rpl::producer &&text, + rpl::producer &&labelText, + Fn callback, + const style::SettingsCountButton &st, + Settings::IconDescriptor &&descriptor); protected: void prepare() override; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 70ad41acb0..aba3714395 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_histories.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" @@ -1163,8 +1164,11 @@ void SingleRowController::prepare() { return; } const auto topic = strong->asTopic(); + const auto sublist = strong->asSublist(); auto row = topic ? ChooseTopicBoxController::MakeRow(topic) + : sublist + ? std::make_unique(sublist->sublistPeer()) : std::make_unique(strong->peer()); const auto raw = row.get(); if (_status) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 876df975d9..09b3bdce3c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -53,6 +53,7 @@ namespace { constexpr auto kSlowmodeValues = 7; constexpr auto kBoostsUnrestrictValues = 5; constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000); +constexpr auto kDefaultChargeStars = 10; [[nodiscard]] auto Dependencies(PowerSaving::Flags) -> std::vector> { @@ -163,11 +164,15 @@ constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000); auto stories = std::vector{ { Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) }, { Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) }, - { Flag::DeleteStories, tr::lng_rights_channel_delete_stories(tr::now) }, + { + Flag::DeleteStories, + tr::lng_rights_channel_delete_stories(tr::now), + }, }; auto second = std::vector{ { Flag::InviteByLinkOrAdd, tr::lng_rights_group_invite(tr::now) }, { Flag::ManageCall, tr::lng_rights_channel_manage_calls(tr::now) }, + { Flag::ManageDirect, tr::lng_rights_channel_manage_direct(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, }; return { @@ -1176,7 +1181,7 @@ void ShowEditPeerPermissionsBox( if (available) { Ui::AddSkip(inner); const auto starsPerMessage = peer->isChannel() - ? peer->asChannel()->starsPerMessage() + ? peer->asChannel()->commonStarsPerMessage() : 0; charging = inner->add(object_ptr( inner, @@ -1198,7 +1203,8 @@ void ShowEditPeerPermissionsBox( state->starsPerMessage = SetupChargeSlider( chargeInner, peer, - starsPerMessage); + (starsPerMessage > 0) ? starsPerMessage : std::optional(), + kDefaultChargeStars); } static constexpr auto kSendRestrictions = Flag::EmbedLinks diff --git a/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp new file mode 100644 index 0000000000..4d6d5454dd --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp @@ -0,0 +1,228 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/peers/toggle_topics_box.h" + +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "settings/settings_common.h" +#include "ui/effects/ripple_animation.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" +#include "ui/vertical_list.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Ui { +namespace { + +enum class LayoutType { + Tabs, + List +}; + +class LayoutButton final : public Ui::RippleButton { +public: + LayoutButton( + QWidget *parent, + LayoutType type, + std::shared_ptr> group); + +private: + void paintEvent(QPaintEvent *e) override; + + QImage prepareRippleMask() const override; + + Ui::FlatLabel _text; + Ui::Animations::Simple _activeAnimation; + bool _active = false; + +}; + +LayoutButton::LayoutButton( + QWidget *parent, + LayoutType type, + std::shared_ptr> group) +: RippleButton(parent, st::defaultRippleAnimationBgOver) +, _text(this, st::topicsLayoutButtonLabel) +, _active(group->current() == type) { + _text.setText(type == LayoutType::Tabs + ? tr::lng_edit_topics_tabs(tr::now) + : tr::lng_edit_topics_list(tr::now)); + const auto iconColorOverride = [=] { + return anim::color( + st::windowSubTextFg, + st::windowActiveTextFg, + _activeAnimation.value(_active ? 1. : 0.)); + }; + const auto iconSize = st::topicsLayoutButtonIconSize; + auto [iconWidget, iconAnimate] = Settings::CreateLottieIcon( + this, + { + .name = (type == LayoutType::Tabs + ? u"topics_tabs"_q + : u"topics_list"_q), + .color = &st::windowSubTextFg, + .sizeOverride = { iconSize, iconSize }, + .colorizeUsingAlpha = true, + }, + st::topicsLayoutButtonIconPadding, + iconColorOverride); + const auto icon = iconWidget.release(); + setClickedCallback([=] { + group->setValue(type); + iconAnimate(anim::repeat::once); + }); + group->value() | rpl::start_with_next([=](LayoutType value) { + const auto active = (value == type); + _text.setTextColorOverride(active + ? st::windowFgActive->c + : std::optional()); + + if (_active == active) { + return; + } + _active = active; + _text.update(); + _activeAnimation.start([=] { + icon->update(); + }, _active ? 0. : 1., _active ? 0. : 1., st::fadeWrapDuration); + }, lifetime()); + + _text.paintRequest() | rpl::start_with_next([=](QRect clip) { + if (_active) { + auto p = QPainter(&_text); + auto hq = PainterHighQualityEnabler(p); + const auto radius = _text.height() / 2.; + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgActive); + p.drawRoundedRect(_text.rect(), radius, radius); + } + }, _text.lifetime()); + + const auto padding = st::topicsLayoutButtonPadding; + const auto skip = st::topicsLayoutButtonSkip; + const auto text = _text.height(); + + resize( + padding.left() + icon->width() + padding.right(), + padding.top() + icon->height() + skip + text + padding.bottom()); + icon->move(padding.left(), padding.top()); + _text.move( + (width() - _text.width()) / 2, + padding.top() + icon->height() + skip); +} + +void LayoutButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto rippleBg = anim::color( + st::windowBgOver, + st::lightButtonBgOver, + _activeAnimation.value(_active ? 1. : 0.)); + paintRipple(p, QPoint(), &rippleBg); +} + +QImage LayoutButton::prepareRippleMask() const { + return Ui::RippleAnimation::RoundRectMask(size(), st::boxRadius); +} + +} // namespace + +void ToggleTopicsBox( + not_null box, + bool enabled, + bool tabs, + Fn callback) { + box->setTitle(tr::lng_forum_topics_switch()); + box->setWidth(st::boxWideWidth); + + const auto container = box->verticalLayout(); + + Settings::AddDividerTextWithLottie(container, { + .lottie = u"topics"_q, + .lottieSize = st::settingsFilterIconSize, + .lottieMargins = st::settingsFilterIconPadding, + .showFinished = box->showFinishes(), + .about = tr::lng_edit_topics_about( + Ui::Text::RichLangValue + ), + .aboutMargins = st::settingsFilterDividerLabelPadding, + }); + + Ui::AddSkip(container); + + const auto toggle = container->add( + object_ptr( + container, + tr::lng_edit_topics_enable(), + st::settingsButtonNoIcon)); + toggle->toggleOn(rpl::single(enabled)); + + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + + const auto group = std::make_shared>(tabs + ? LayoutType::Tabs + : LayoutType::List); + + const auto layoutWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto layout = layoutWrap->entity(); + + Ui::AddSubsectionTitle(layout, tr::lng_edit_topics_layout()); + const auto buttons = layout->add( + object_ptr(layout), + QMargins(0, 0, 0, st::defaultVerticalListSkip * 2)); + + const auto tabsButton = Ui::CreateChild( + buttons, + LayoutType::Tabs, + group); + const auto listButton = Ui::CreateChild( + buttons, + LayoutType::List, + group); + + buttons->resize(container->width(), tabsButton->height()); + buttons->widthValue() | rpl::start_with_next([=](int outer) { + const auto skip = st::boxRowPadding.left() - st::boxRadius; + tabsButton->moveToLeft(skip, 0, outer); + listButton->moveToRight(skip, 0, outer); + }, buttons->lifetime()); + + Ui::AddDividerText( + layout, + tr::lng_edit_topics_layout_about(Ui::Text::RichLangValue)); + + layoutWrap->toggle(enabled, anim::type::instant); + toggle->toggledChanges( + ) | rpl::start_with_next([=](bool checked) { + layoutWrap->toggle(checked, anim::type::normal); + }, layoutWrap->lifetime()); + + box->addButton(tr::lng_settings_save(), [=] { + const auto enabledValue = toggle->toggled(); + const auto tabsValue = (group->current() == LayoutType::Tabs); + callback(enabledValue, tabsValue); + box->closeBox(); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/peers/toggle_topics_box.h b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.h new file mode 100644 index 0000000000..0bd4ad3685 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.h @@ -0,0 +1,20 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/layers/generic_box.h" + +namespace Ui { + +void ToggleTopicsBox( + not_null box, + bool enabled, + bool tabs, + Fn callback); + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/pin_messages_box.cpp b/Telegram/SourceFiles/boxes/pin_messages_box.cpp index cb51efb314..c2012bde43 100644 --- a/Telegram/SourceFiles/boxes/pin_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/pin_messages_box.cpp @@ -24,10 +24,15 @@ namespace { [[nodiscard]] bool IsOldForPin( MsgId id, not_null peer, - MsgId topicRootId) { + MsgId topicRootId, + PeerId monoforumPeerId) { const auto normal = peer->migrateToOrMe(); const auto migrated = normal->migrateFrom(); - const auto top = Data::ResolveTopPinnedId(normal, topicRootId, migrated); + const auto top = Data::ResolveTopPinnedId( + normal, + topicRootId, + monoforumPeerId, + migrated); if (!top) { return false; } else if (peer == migrated) { @@ -53,7 +58,14 @@ void PinMessageBox( const auto peer = item->history()->peer; const auto msgId = item->id; const auto topicRootId = item->topic() ? item->topicRootId() : MsgId(); - const auto pinningOld = IsOldForPin(msgId, peer, topicRootId); + const auto monoforumPeerId = item->history()->peer->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId(); + const auto pinningOld = IsOldForPin( + msgId, + peer, + topicRootId, + monoforumPeerId); const auto state = box->lifetime().make_state(); const auto api = box->lifetime().make_state( &peer->session().mtp()); @@ -71,7 +83,9 @@ void PinMessageBox( object->setAllowTextLines(); state->pinForPeer = Ui::MakeWeak(object.data()); return object; - } else if (!pinningOld && (peer->isChat() || peer->isMegagroup())) { + } else if (!pinningOld + && (peer->isChat() || peer->isMegagroup()) + && !peer->isMonoforum()) { auto object = object_ptr( box, tr::lng_pinned_notify(tr::now), diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 325e92c443..da4120f0f7 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -907,6 +907,7 @@ void PinsLimitBox( limits.dialogsPinnedPremium(), PinsCount(session->data().chatsList())); } + void SublistsPinsLimitBox( not_null box, not_null session) { diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 53c6a6744e..da6f461d4b 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -133,6 +133,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_subtitle_business(); case PremiumFeature::Effects: return tr::lng_premium_summary_subtitle_effects(); + case PremiumFeature::TodoLists: + return tr::lng_premium_summary_subtitle_todo_lists(); case PremiumFeature::BusinessLocation: return tr::lng_business_subtitle_location(); @@ -198,6 +200,8 @@ void PreloadSticker(const std::shared_ptr &media) { return tr::lng_premium_summary_about_business(); case PremiumFeature::Effects: return tr::lng_premium_summary_about_effects(); + case PremiumFeature::TodoLists: + return tr::lng_premium_summary_about_todo_lists(); case PremiumFeature::BusinessLocation: return tr::lng_business_about_location(); @@ -538,6 +542,7 @@ struct VideoPreviewDocument { case PremiumFeature::LastSeen: return "last_seen"; case PremiumFeature::MessagePrivacy: return "message_privacy"; case PremiumFeature::Effects: return "effects"; + case PremiumFeature::TodoLists: return "todo"; case PremiumFeature::BusinessLocation: return "business_location"; case PremiumFeature::BusinessHours: return "business_hours"; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index e631c97897..9ac7d04ac2 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -72,6 +72,7 @@ enum class PremiumFeature { Business, Effects, FilterTags, + TodoLists, // Business features. BusinessLocation, diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index 2a4e4f242a..9e33609715 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -327,7 +327,7 @@ void SendCreditsBox( const auto ministars = box->lifetime().make_state( ministarsContainer, false, - Ui::Premium::MiniStars::Type::BiStars); + Ui::Premium::MiniStarsType::BiStars); ministars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); ministarsContainer->paintRequest( @@ -487,6 +487,7 @@ void SendCreditsBox( session->credits().load(true); const auto balance = Settings::AddBalanceWidget( content, + session, session->credits().balanceValue(), false); rpl::combine( diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index b271028766..7deadbe2f0 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/send_gif_with_caption_box.h" +#include "api/api_editing.h" #include "base/event_filter.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/field_autocomplete.h" @@ -18,13 +19,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" +#include "data/data_groups.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" +#include "history/history.h" +#include "history/history_item.h" #include "history/view/controls/history_view_characters_limit.h" +#include "history/view/history_view_message.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/clip/media_clip_reader.h" @@ -32,10 +37,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button_factory.h" #include "ui/layers/generic_box.h" -#include "ui/widgets/fields/input_field.h" #include "ui/rect.h" +#include "ui/text/text_entity.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" @@ -224,11 +230,10 @@ namespace { return input; } -} // namespace - -void SendGifWithCaptionBox( +void CaptionBox( not_null box, - not_null document, + rpl::producer confirmText, + TextWithTags initialText, not_null peer, const SendMenu::Details &details, Fn done) { @@ -237,23 +242,15 @@ void SendGifWithCaptionBox( if (!controller) { return; } - box->setTitle(tr::lng_send_gif_with_caption()); box->setWidth(st::boxWidth); box->getDelegate()->setStyle(st::sendGifBox); - const auto container = box->verticalLayout(); - [[maybe_unused]] const auto gifWidget = AddGifWidget( - container, - document, - st::boxWidth); - - Ui::AddSkip(container); - const auto input = AddInputField(box, controller); box->setFocusCallback([=] { input->setFocus(); }); + input->setTextWithTags(std::move(initialText)); input->setSubmitSettings(Core::App().settings().sendSubmitWay()); InitMessageField(controller, input, [=](not_null) { return true; @@ -318,7 +315,7 @@ void SendGifWithCaptionBox( done(std::move(options), input->getTextWithTags()); }; const auto confirm = box->addButton( - tr::lng_send_button(), + std::move(confirmText), [=] { send({}); }); SendMenu::SetupMenuAndShortcuts( confirm, @@ -339,4 +336,89 @@ void SendGifWithCaptionBox( ) | rpl::start_with_next([=] { send({}); }, input->lifetime()); } +} // namespace + +void SendGifWithCaptionBox( + not_null box, + not_null document, + not_null peer, + const SendMenu::Details &details, + Fn c) { + box->setTitle(tr::lng_send_gif_with_caption()); + [[maybe_unused]] const auto gifWidget = AddGifWidget( + box->verticalLayout(), + document, + st::boxWidth); + Ui::AddSkip(box->verticalLayout()); + CaptionBox(box, tr::lng_send_button(), {}, peer, details, std::move(c)); +} + +void EditCaptionBox( + not_null box, + not_null view) { + using namespace TextUtilities; + + box->setTitle(tr::lng_context_upload_edit_caption()); + + const auto data = &view->data()->history()->peer->owner(); + + struct State { + FullMsgId fullId; + }; + const auto state = box->lifetime().make_state(); + state->fullId = view->data()->fullId(); + + data->itemIdChanged( + ) | rpl::start_with_next([=](Data::Session::IdChange event) { + if (event.oldId == state->fullId.msg) { + state->fullId = event.newId; + } + }, box->lifetime()); + + auto done = [=, show = box->uiShow()]( + Api::SendOptions, + TextWithTags textWithTags) { + const auto item = data->message(state->fullId); + if (!item) { + show->showToast(tr::lng_message_not_found(tr::now)); + return; + } + if (!(item->media() && item->media()->allowsEditCaption())) { + show->showToast(tr::lng_edit_error(tr::now)); + return; + } + auto text = TextWithEntities{ + std::move(textWithTags.text), + ConvertTextTagsToEntities(std::move(textWithTags.tags)), + }; + if (item->isUploading()) { + item->setText(std::move(text)); + data->requestViewResize(view); + if (item->groupId()) { + data->groups().refreshMessage(item, true); + } + box->closeBox(); + } else { + Api::EditCaption( + item, + std::move(text), + { .invertCaption = item->invertMedia() }, + [=] { box->closeBox(); }, + [=](const QString &e) { box->uiShow()->showToast(e); }); + } + }; + + const auto item = view->data(); + CaptionBox( + box, + tr::lng_settings_save(), + TextWithTags{ + .text = item->originalText().text, + .tags = ConvertEntitiesToTextTags(item->originalText().entities), + }, + item->history()->peer, + {}, + std::move(done)); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h index 0247cf5e3d..1730c86714 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h @@ -18,10 +18,18 @@ namespace SendMenu { struct Details; } // namespace SendMenu +namespace HistoryView { +class Element; +} // namespace HistoryView + namespace Ui { class GenericBox; +void EditCaptionBox( + not_null box, + not_null view); + void SendGifWithCaptionBox( not_null box, not_null document, diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 7af67590c0..9d7984f357 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -45,6 +45,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_user.h" #include "data/data_peer_values.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_forum.h" @@ -120,7 +122,9 @@ private: not_null history; not_null peer; Data::ForumTopic *topic = nullptr; + Data::SavedSublist *sublist = nullptr; rpl::lifetime topicLifetime; + rpl::lifetime sublistLifetime; Ui::RoundImageCheckbox checkbox; Ui::Text::String name; Ui::Animations::Simple nameActive; @@ -149,6 +153,7 @@ private: void preloadUserpic(not_null entry); void changeCheckState(Chat *chat); void chooseForumTopic(not_null forum); + void chooseMonoforumSublist(not_null monoforum); enum class ChangeStateWay { Default, SkipCallback, @@ -644,15 +649,18 @@ void ShareBox::addPeerToMultiSelect(not_null thread) { auto addItemWay = Ui::MultiSelect::AddItemWay::Default; const auto peer = thread->peer(); const auto topic = thread->asTopic(); + const auto sublist = thread->asSublist(); _select->addItem( peer->id.value, (topic ? topic->title() + : sublist + ? sublist->sublistPeer()->shortName() : peer->isSelf() ? tr::lng_saved_short(tr::now) : peer->shortName()), st::activeButtonBg, - (topic + ((topic || sublist) ? ForceRoundUserpicCallback(peer) : PaintUserpicCallback(peer, true)), addItemWay); @@ -976,6 +984,8 @@ void ShareBox::Inner::updateChatName(not_null chat) { const auto peer = chat->peer; const auto text = chat->topic ? chat->topic->title() + : chat->sublist + ? chat->sublist->sublistPeer()->name() : peer->isSelf() ? tr::lng_saved_messages(tr::now) : peer->isRepliesChat() @@ -1215,7 +1225,7 @@ ShareBox::Inner::Chat::Chat( st.checkbox, updateCallback, PaintUserpicCallback(peer, true), - [=](int size) { return peer->isForum() + [=](int size) { return (peer->isForum() || peer->isMonoforum()) ? int(size * Ui::ForumUserpicRadiusMultiplier()) : std::optional(); }) , name(st.checkbox.imageRadius * 2) { @@ -1356,10 +1366,13 @@ void ShareBox::Inner::changeCheckState(Chat *chat) { const auto checked = chat->checkbox.checked(); const auto forum = chat->peer->forum(); - if (checked || !forum) { + const auto monoforum = chat->peer->monoforum(); + if (checked || (!forum && !monoforum)) { changePeerCheckState(chat, !checked); - } else { - chooseForumTopic(chat->peer->forum()); + } else if (forum) { + chooseForumTopic(forum); + } else if (monoforum) { + chooseMonoforumSublist(monoforum); } } @@ -1410,6 +1423,54 @@ void ShareBox::Inner::chooseForumTopic(not_null forum) { _show->showBox(std::move(box)); } +void ShareBox::Inner::chooseMonoforumSublist( + not_null monoforum) { + const auto guard = Ui::MakeWeak(this); + const auto weak = std::make_shared>(); + auto chosen = [=](not_null sublist) { + if (const auto strong = *weak) { + strong->closeBox(); + } + if (!guard) { + return; + } + const auto row = _chatsIndexed->getRow(sublist->owningHistory()); + if (!row) { + return; + } + const auto chat = getChat(row); + Assert(!chat->sublist); + chat->sublist = sublist; + chat->sublist->destroyed( + ) | rpl::start_with_next([=] { + changePeerCheckState(chat, false); + }, chat->sublistLifetime); + updateChatName(chat); + changePeerCheckState(chat, true); + }; + auto initBox = [=](not_null box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + monoforum->destroyed( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }; + auto filter = [=](not_null sublist) { + return guard && _descriptor.filterCallback(sublist); + }; + auto box = Box( + std::make_unique( + monoforum, + std::move(chosen), + std::move(filter)), + std::move(initBox)); + *weak = box.data(); + _show->showBox(std::move(box)); +} + void ShareBox::Inner::peerUnselected(not_null peer) { if (const auto i = _dataMap.find(peer); i != end(_dataMap)) { changePeerCheckState( @@ -1440,6 +1501,11 @@ void ShareBox::Inner::changePeerCheckState( chat->topic = nullptr; updateChatName(chat); } + if (chat->sublist) { + chat->sublistLifetime.destroy(); + chat->sublist = nullptr; + updateChatName(chat); + } } if (useCallback != ChangeStateWay::SkipCallback && _peerSelectedChangedCallback) { @@ -1571,6 +1637,8 @@ not_null ShareBox::Inner::chatThread( not_null chat) const { return chat->topic ? (Data::Thread*)chat->topic + : chat->sublist + ? (Data::Thread*)chat->sublist : chat->peer->owner().history(chat->peer).get(); } @@ -1723,6 +1791,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( api.sendMessage(std::move(message)); } const auto topicRootId = thread->topicRootId(); + const auto sublistPeer = thread->maybeSublistPeer(); const auto kGeneralId = Data::ForumTopic::kGeneralId; const auto topMsgId = (topicRootId == kGeneralId) ? MsgId(0) @@ -1747,7 +1816,9 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) - | (starsPaid ? Flag::f_allow_paid_stars : Flag()); + | (starsPaid ? Flag::f_allow_paid_stars : Flag()) + | (sublistPeer ? Flag::f_reply_to : Flag()) + | (options.suggest ? Flag::f_suggested_post : Flag()); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( MTP_flags(sendFlags), @@ -1756,11 +1827,15 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_vector(generateRandom()), peer->input, MTP_int(topMsgId), + (sublistPeer + ? MTP_inputReplyToMonoForum(sublistPeer->input) + : MTPInputReplyTo()), MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as Data::ShortcutIdToMTP(session, options.shortcutId), MTP_int(videoTimestamp.value_or(0)), - MTP_long(starsPaid) + MTP_long(starsPaid), + Api::SuggestToMTP(options.suggest) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index d021cba69f..6e4608a47d 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -458,7 +458,7 @@ auto GenerateGiftMedia( .sticker = sticker, .size = st::chatIntroStickerSize, .cacheTag = Tag::ChatIntroHelloSticker, - .singleTimePlayback = v::is(descriptor), + .stopOnLastFrame = v::is(descriptor), }; }; push(std::make_unique( @@ -2007,7 +2007,7 @@ void SoldOutBox( Data::CreditsHistoryEntry{ .firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate), .lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate), - .credits = StarsAmount(gift.info.stars), + .credits = CreditsAmount(gift.info.stars), .bareGiftStickerId = gift.info.document->id, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = gift.info.limitedCount, @@ -2039,8 +2039,8 @@ void AddUpgradeButton( tr::lng_gift_send_unique( lt_price, rpl::single(star.append(' ' - + Lang::FormatStarsAmountDecimal( - StarsAmount{ cost }))), + + Lang::FormatCreditsAmountDecimal( + CreditsAmount{ cost }))), Text::WithEntities), st::boxLabel, st::defaultPopupMenu, @@ -2355,9 +2355,9 @@ void SendGiftBox( tr::lng_gift_send_stars_balance( lt_amount, peer->session().credits().balanceValue( - ) | rpl::map([=](StarsAmount amount) { + ) | rpl::map([=](CreditsAmount amount) { return base::duplicate(star).append( - Lang::FormatStarsAmountDecimal(amount)); + Lang::FormatCreditsAmountDecimal(amount)); }), lt_link, tr::lng_gift_send_stars_balance_link( @@ -4614,8 +4614,8 @@ void UpgradeBox( ? tr::lng_gift_upgrade_button( lt_price, rpl::single(star.append( - ' ' + Lang::FormatStarsAmountDecimal( - StarsAmount{ cost }))), + ' ' + Lang::FormatCreditsAmountDecimal( + CreditsAmount{ cost }))), Ui::Text::WithEntities) : tr::lng_gift_upgrade_confirm(Ui::Text::WithEntities)), &controller->session(), diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 8734c6d0f8..d0bbb89a26 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -270,6 +270,11 @@ whenReadPadding: margins(34px, 3px, 17px, 4px); whenReadIconPosition: point(8px, 0px); whenReadSkip: 3px; whenReadShowPadding: margins(6px, 0px, 6px, 2px); +whoSentItem: Menu(defaultMenu) { + itemPadding: margins(17px, 3px, 17px, 4px); + itemRightSkip: 0px; + itemStyle: whenReadStyle; +} switchPmButton: RoundButton(defaultBoxButton) { width: 320px; @@ -873,6 +878,10 @@ historyGiftToChannel: IconButton(defaultIconButton) { rippleAreaSize: 40px; ripple: universalRippleAnimation; } +historyDirectMessage: IconButton(historyGiftToChannel) { + icon: icon{{ "menu/chat_discuss", windowActiveTextFg }}; + iconOver: icon{{ "menu/chat_discuss", windowActiveTextFg }}; +} historyUnblock: FlatButton(historyComposeButton) { color: attentionButtonFg; overColor: attentionButtonFgOver; @@ -1137,6 +1146,31 @@ historyGiftToUser: IconButton(historyAttach) { icon: icon {{ "chat/input_gift", historyComposeIconFg }}; iconOver: icon {{ "chat/input_gift", historyComposeIconFgOver }}; } +historySuggestPostToggle: IconButton(historyAttach) { + icon: icon{{ "chat/input_paid", historyComposeIconFg }}; + iconOver: icon{{ "chat/input_paid", historyComposeIconFgOver }}; +} +historySuggestIconPosition: point(4px, 4px); +historySuggestIconActive: icon{{ "chat/input_paid", windowActiveTextFg }}; + +suggestOptionsPrice: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(2px, 20px, 2px, 0px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; + + border: 0px; + borderActive: 0px; + + heightMin: 32px; + + style: defaultTextStyle; +} historyAttachEmojiInner: IconButton(historyAttach) { icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; @@ -1596,3 +1630,5 @@ frozenInfoBox: Box(defaultBox) { } shadowIgnoreTopSkip: true; } + +roundVideoFont: font(14px semibold); diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp index a4c98c416c..9d9f0fd8ae 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp @@ -16,14 +16,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Stickers { GiftBoxPack::GiftBoxPack(not_null session) -: _session(session) -, _localMonths({ 1, 3, 6, 12, 24 }) { +: _session(session) { + _premium.dividers = { 1, 3, 6, 12, 24 }; + _ton.dividers = { 0, 10, 50 }; } GiftBoxPack::~GiftBoxPack() = default; rpl::producer<> GiftBoxPack::updated() const { - return _updated.events(); + return _premium.updated.events(); +} + +rpl::producer<> GiftBoxPack::tonUpdated() const { + return _ton.updated.events(); } int GiftBoxPack::monthsForStars(int stars) const { @@ -37,86 +42,112 @@ int GiftBoxPack::monthsForStars(int stars) const { } DocumentData *GiftBoxPack::lookup(int months) const { - const auto it = ranges::lower_bound(_localMonths, months); - const auto fallback = _documents.empty() ? nullptr : _documents[0]; - if (it == begin(_localMonths)) { + return lookup(_premium, months, false); +} + +DocumentData *GiftBoxPack::tonLookup(int amount) const { + return lookup(_ton, amount, true); +} + +DocumentData *GiftBoxPack::lookup( + const Pack &pack, + int divider, + bool exact) const { + const auto it = ranges::lower_bound(pack.dividers, divider); + const auto fallback = pack.documents.empty() + ? nullptr + : pack.documents.front(); + if (it == begin(pack.dividers)) { return fallback; - } else if (it == end(_localMonths)) { - return _documents.back(); + } else if (it == end(pack.dividers)) { + return pack.documents.back(); } - const auto left = *(it - 1); - const auto right = *it; - const auto shift = (std::abs(months - left) < std::abs(months - right)) + const auto shift = exact + ? ((*it > divider) ? 1 : 0) + : (std::abs(divider - (*(it - 1))) < std::abs(divider - (*it))) ? -1 : 0; - const auto index = int(std::distance(begin(_localMonths), it - shift)); - return (index >= _documents.size()) ? fallback : _documents[index]; + const auto index = int(std::distance(begin(pack.dividers), it - shift)); + return (index >= pack.documents.size()) + ? fallback + : pack.documents[index]; } Data::FileOrigin GiftBoxPack::origin() const { - return Data::FileOriginStickerSet(_setId, _accessHash); + return Data::FileOriginStickerSet(_premium.id, _premium.accessHash); +} + +Data::FileOrigin GiftBoxPack::tonOrigin() const { + return Data::FileOriginStickerSet(_ton.id, _ton.accessHash); } void GiftBoxPack::load() { - if (_requestId || !_documents.empty()) { + load(_premium, MTP_inputStickerSetPremiumGifts()); +} + +void GiftBoxPack::tonLoad() { + load(_ton, MTP_inputStickerSetTonGifts()); +} + +void GiftBoxPack::load(Pack &pack, const MTPInputStickerSet &set) { + if (pack.requestId || !pack.documents.empty()) { return; } - _requestId = _session->api().request(MTPmessages_GetStickerSet( - MTP_inputStickerSetPremiumGifts(), + pack.requestId = _session->api().request(MTPmessages_GetStickerSet( + set, MTP_int(0) // Hash. - )).done([=](const MTPmessages_StickerSet &result) { - _requestId = 0; + )).done([=, &pack](const MTPmessages_StickerSet &result) { + pack.requestId = 0; result.match([&](const MTPDmessages_stickerSet &data) { - applySet(data); + applySet(pack, data); }, [](const MTPDmessages_stickerSetNotModified &) { LOG(("API Error: Unexpected messages.stickerSetNotModified.")); }); - }).fail([=] { - _requestId = 0; + }).fail([=, &pack] { + pack.requestId = 0; }).send(); } -void GiftBoxPack::applySet(const MTPDmessages_stickerSet &data) { - _setId = data.vset().data().vid().v; - _accessHash = data.vset().data().vaccess_hash().v; +void GiftBoxPack::applySet(Pack &pack, const MTPDmessages_stickerSet &data) { + pack.id = data.vset().data().vid().v; + pack.accessHash = data.vset().data().vaccess_hash().v; auto documents = base::flat_map>(); for (const auto &sticker : data.vdocuments().v) { const auto document = _session->data().processDocument(sticker); if (document->sticker()) { documents.emplace(document->id, document); - if (_documents.empty()) { + if (pack.documents.empty()) { // Fallback. - _documents.resize(1); - _documents[0] = document; + pack.documents.resize(1); + pack.documents[0] = document; } } } - for (const auto &pack : data.vpacks().v) { - pack.match([&](const MTPDstickerPack &data) { - const auto emoji = qs(data.vemoticon()); - if (emoji.isEmpty()) { - return; - } - for (const auto &id : data.vdocuments().v) { - if (const auto document = documents.take(id.v)) { - if (const auto sticker = (*document)->sticker()) { - if (!sticker->alt.isEmpty()) { - const auto ch = int(sticker->alt[0].unicode()); - const auto index = (ch - '1'); // [0, 4]; - if (index < 0 || index >= _localMonths.size()) { - return; - } - if ((index + 1) > _documents.size()) { - _documents.resize((index + 1)); - } - _documents[index] = (*document); + for (const auto &info : data.vpacks().v) { + const auto &data = info.data(); + const auto emoji = qs(data.vemoticon()); + if (emoji.isEmpty()) { + return; + } + for (const auto &id : data.vdocuments().v) { + if (const auto document = documents.take(id.v)) { + if (const auto sticker = (*document)->sticker()) { + if (!sticker->alt.isEmpty()) { + const auto ch = int(sticker->alt[0].unicode()); + const auto index = (ch - '1'); // [0, 4]; + if (index < 0 || index >= pack.dividers.size()) { + return; } + if ((index + 1) > pack.documents.size()) { + pack.documents.resize((index + 1)); + } + pack.documents[index] = (*document); } } } - }); + } } - _updated.fire({}); + pack.updated.fire({}); } } // namespace Stickers diff --git a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h index 798259c257..728b10ddb4 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h @@ -30,18 +30,36 @@ public: [[nodiscard]] Data::FileOrigin origin() const; [[nodiscard]] rpl::producer<> updated() const; + void tonLoad(); + [[nodiscard]] DocumentData *tonLookup(int amount) const; + [[nodiscard]] Data::FileOrigin tonOrigin() const; + [[nodiscard]] rpl::producer<> tonUpdated() const; + private: using SetId = uint64; - void applySet(const MTPDmessages_stickerSet &data); + + struct Pack { + SetId id = 0; + uint64 accessHash = 0; + std::vector documents; + mtpRequestId requestId = 0; + std::vector dividers; + rpl::event_stream<> updated; + }; + + void load(Pack &pack, const MTPInputStickerSet &set); + void applySet(Pack &pack, const MTPDmessages_stickerSet &data); + [[nodiscard]] DocumentData *lookup( + const Pack &pack, + int divider, + bool exact) const; const not_null _session; const std::vector _localMonths; + const std::vector _localTonAmounts; - rpl::event_stream<> _updated; - std::vector _documents; - SetId _setId = 0; - uint64 _accessHash = 0; - mtpRequestId _requestId = 0; + Pack _premium; + Pack _ton; }; diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp index 2be42e9c6e..7ab2bdc4de 100644 --- a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp @@ -56,7 +56,7 @@ public: bool elementAnimationsPaused() override; not_null elementPathShiftGradient() override; HistoryView::Context elementContext() override; - bool elementIsChatWide() override; + HistoryView::ElementChatMode elementChatMode() override; private: const not_null _parent; @@ -87,8 +87,9 @@ HistoryView::Context PreviewDelegate::elementContext() { return HistoryView::Context::TTLViewer; } -bool PreviewDelegate::elementIsChatWide() { - return _chatWide.current(); +HistoryView::ElementChatMode PreviewDelegate::elementChatMode() { + using Mode = HistoryView::ElementChatMode; + return _chatWide.current() ? Mode::Wide : Mode::Default; } class PreviewWrap final : public Ui::RpWidget { diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 4ba6f2e614..170e064fb0 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "data/data_abstract_structure.h" +#include "data/data_channel.h" #include "data/data_forum.h" #include "data/data_message_reactions.h" #include "data/data_session.h" @@ -1358,8 +1359,9 @@ Window::Controller *Application::windowForShowingHistory( Window::Controller *Application::windowForShowingForum( not_null forum) const { + const auto tabs = forum->channel()->useSubsectionTabs(); const auto id = Window::SeparateId( - Window::SeparateType::Forum, + tabs ? Window::SeparateType::Chat : Window::SeparateType::Forum, forum->history()); if (const auto separate = separateWindowFor(id)) { return separate; @@ -1367,9 +1369,15 @@ Window::Controller *Application::windowForShowingForum( auto result = (Window::Controller*)nullptr; enumerateWindows([&](not_null window) { if (const auto controller = window->sessionController()) { - const auto current = controller->shownForum().current(); - if (forum == current) { - result = window; + if (tabs) { + if (controller->windowId() == id) { + result = window; + } + } else { + const auto current = controller->shownForum().current(); + if (forum == current) { + result = window; + } } } }); diff --git a/Telegram/SourceFiles/core/credits_amount.h b/Telegram/SourceFiles/core/credits_amount.h new file mode 100644 index 0000000000..098266d887 --- /dev/null +++ b/Telegram/SourceFiles/core/credits_amount.h @@ -0,0 +1,183 @@ +/* +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/algorithm.h" +#include "base/basic_types.h" + +class MTPstarsAmount; + +namespace tl { +template +class boxed; +} // namespace tl + +using MTPStarsAmount = tl::boxed; + +inline constexpr auto kOneStarInNano = int64(1'000'000'000); + +enum class CreditsType { + Stars, + Ton, +}; + +class CreditsAmount { +public: + CreditsAmount() = default; + explicit CreditsAmount( + int64 whole, + CreditsType type = CreditsType::Stars) + : _ton((type == CreditsType::Ton) ? 1 : 0) + , _whole(whole) { + } + CreditsAmount( + int64 whole, + int64 nano, + CreditsType type = CreditsType::Stars) + : _ton((type == CreditsType::Ton) ? 1 : 0) + , _whole(whole) + , _nano(nano) { + normalize(); + } + + [[nodiscard]] int64 whole() const { + return _whole; + } + + [[nodiscard]] int64 nano() const { + return _nano; + } + + [[nodiscard]] double value() const { + return double(_whole) + double(_nano) / kOneStarInNano; + } + + [[nodiscard]] bool ton() const { + return (_ton == 1); + } + [[nodiscard]] bool stars() const { + return (_ton == 0); + } + [[nodiscard]] CreditsType type() const { + return !_ton ? CreditsType::Stars : CreditsType::Ton; + } + + [[nodiscard]] bool empty() const { + return !_whole && !_nano; + } + + [[nodiscard]] inline bool operator!() const { + return empty(); + } + [[nodiscard]] inline explicit operator bool() const { + return !empty(); + } + + [[nodiscard]] CreditsAmount multiplied(float64 rate) const { + const auto result = value() * rate; + const auto abs = std::abs(result); + const auto whole = std::floor(abs); + const auto nano = base::SafeRound((abs - whole) * kOneStarInNano); + return CreditsAmount( + (result < 0) ? -whole : whole, + (result < 0) ? -nano : nano, + type()); + } + + inline CreditsAmount &operator+=(CreditsAmount other) { + _whole += other._whole; + _nano += other._nano; + normalize(); + return *this; + } + inline CreditsAmount &operator-=(CreditsAmount other) { + _whole -= other._whole; + _nano -= other._nano; + normalize(); + return *this; + } + inline CreditsAmount &operator*=(int64 multiplier) { + _whole *= multiplier; + _nano *= multiplier; + normalize(); + return *this; + } + inline CreditsAmount operator-() const { + auto result = *this; + result *= -1; + return result; + } + +// AppleClang :/ +// friend inline auto operator<=>(CreditsAmount, CreditsAmount) +// = default; + friend inline constexpr auto operator<=>( + CreditsAmount a, + CreditsAmount b) { + if (const auto r1 = (a._whole <=> b._whole); r1 != 0) { + return r1; + } else if (const auto r2 = (a._nano <=> b._nano); r2 != 0) { + return r2; + } + return (a._whole || a._nano) + ? (int(a._ton) <=> int(b._ton)) + : std::strong_ordering::equal; + } + + friend inline bool operator==(CreditsAmount, CreditsAmount) + = default; + + [[nodiscard]] CreditsAmount abs() const { + return (_whole < 0) ? CreditsAmount(-_whole, -_nano) : *this; + } + +private: + void normalize() { + if (_nano < 0) { + const auto shifts = (-_nano + kOneStarInNano - 1) + / kOneStarInNano; + _nano += shifts * kOneStarInNano; + _whole -= shifts; + } else if (_nano >= kOneStarInNano) { + const auto shifts = _nano / kOneStarInNano; + _nano -= shifts * kOneStarInNano; + _whole += shifts; + } + } + + int64 _ton : 2 = 0; + int64 _whole : 62 = 0; + int64 _nano = 0; + +}; + +[[nodiscard]] inline CreditsAmount operator+( + CreditsAmount a, + CreditsAmount b) { + return a += b; +} + +[[nodiscard]] inline CreditsAmount operator-( + CreditsAmount a, + CreditsAmount b) { + return a -= b; +} + +[[nodiscard]] inline CreditsAmount operator*(CreditsAmount a, int64 b) { + return a *= b; +} + +[[nodiscard]] inline CreditsAmount operator*(int64 a, CreditsAmount b) { + return b *= a; +} + +[[nodiscard]] CreditsAmount CreditsAmountFromTL( + const MTPStarsAmount &amount); +[[nodiscard]] CreditsAmount CreditsAmountFromTL( + const MTPStarsAmount *amount); +[[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index d754e81e5e..3ff724a319 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -524,6 +524,8 @@ bool ShowWallPaper( result |= ChatAdminRight::AddAdmins; } else if (element == u"manage_video_chats"_q) { result |= ChatAdminRight::ManageCall; + } else if (element == u"manage_direct_messages"_q) { + result |= ChatAdminRight::ManageDirect; } else if (element == u"anonymous"_q) { result |= ChatAdminRight::Anonymous; } else if (element == u"manage_chat"_q) { @@ -851,6 +853,7 @@ bool OpenMediaTimestamp( document, context, context ? context->topicRootId() : MsgId(0), + context ? context->sublistPeerId() : PeerId(0), false, time)); } else if (document->isSong() || document->isVoiceMessage()) { diff --git a/Telegram/SourceFiles/core/stars_amount.h b/Telegram/SourceFiles/core/stars_amount.h deleted file mode 100644 index ca0e6fbe2d..0000000000 --- a/Telegram/SourceFiles/core/stars_amount.h +++ /dev/null @@ -1,108 +0,0 @@ -/* -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/basic_types.h" - -inline constexpr auto kOneStarInNano = int64(1'000'000'000); - -class StarsAmount { -public: - StarsAmount() = default; - explicit StarsAmount(int64 whole) : _whole(whole) {} - StarsAmount(int64 whole, int64 nano) : _whole(whole), _nano(nano) { - normalize(); - } - - [[nodiscard]] int64 whole() const { - return _whole; - } - - [[nodiscard]] int64 nano() const { - return _nano; - } - - [[nodiscard]] double value() const { - return double(_whole) + double(_nano) / kOneStarInNano; - } - - [[nodiscard]] bool empty() const { - return !_whole && !_nano; - } - - [[nodiscard]] inline bool operator!() const { - return empty(); - } - [[nodiscard]] inline explicit operator bool() const { - return !empty(); - } - - inline StarsAmount &operator+=(StarsAmount other) { - _whole += other._whole; - _nano += other._nano; - normalize(); - return *this; - } - inline StarsAmount &operator-=(StarsAmount other) { - _whole -= other._whole; - _nano -= other._nano; - normalize(); - return *this; - } - inline StarsAmount &operator*=(int64 multiplier) { - _whole *= multiplier; - _nano *= multiplier; - normalize(); - return *this; - } - inline StarsAmount operator-() const { - auto result = *this; - result *= -1; - return result; - } - - friend inline auto operator<=>(StarsAmount, StarsAmount) = default; - friend inline bool operator==(StarsAmount, StarsAmount) = default; - - [[nodiscard]] StarsAmount abs() const { - return (_whole < 0) ? StarsAmount(-_whole, -_nano) : *this; - } - -private: - int64 _whole = 0; - int64 _nano = 0; - - void normalize() { - if (_nano < 0) { - const auto shifts = (-_nano + kOneStarInNano - 1) - / kOneStarInNano; - _nano += shifts * kOneStarInNano; - _whole -= shifts; - } else if (_nano >= kOneStarInNano) { - const auto shifts = _nano / kOneStarInNano; - _nano -= shifts * kOneStarInNano; - _whole += shifts; - } - } -}; - -[[nodiscard]] inline StarsAmount operator+(StarsAmount a, StarsAmount b) { - return a += b; -} - -[[nodiscard]] inline StarsAmount operator-(StarsAmount a, StarsAmount b) { - return a -= b; -} - -[[nodiscard]] inline StarsAmount operator*(StarsAmount a, int64 b) { - return a *= b; -} - -[[nodiscard]] inline StarsAmount operator*(int64 a, StarsAmount b) { - return b *= a; -} diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index f0d353c94d..0284cd28a9 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -258,11 +258,11 @@ std::shared_ptr UiIntegration::createLinkHandler( case EntityType::Pre: return std::make_shared(data.text, data.type); case EntityType::Phone: - return my->session + return (my && my->session) ? std::make_shared(my->session, data.text) : nullptr; case EntityType::BankCard: - return my->session + return (my && my->session) ? std::make_shared(my->session, data.text) : nullptr; } diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 22caf86ad1..c6bac14728 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 5014003; -constexpr auto AppVersionStr = "5.14.3"; +constexpr auto AppVersion = 5016002; +constexpr auto AppVersionStr = "5.16.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index ca6f362ed2..e3701b18b9 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -54,6 +54,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), data.vdate(), data.vaction(), @@ -92,7 +93,10 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_long(data.veffect().value_or_empty()), (data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()), MTP_int(data.vreport_delivery_until_date().value_or_empty()), - MTP_long(data.vpaid_message_stars().value_or_empty())); + MTP_long(data.vpaid_message_stars().value_or_empty()), + (data.vsuggested_post() + ? *data.vsuggested_post() + : MTPSuggestedPost())); }); } diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 5ae899d77e..0bca68827f 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/credits.h" +#include "apiwrap.h" #include "api/api_credits.h" #include "data/data_user.h" #include "main/main_app_config.h" @@ -19,11 +20,6 @@ constexpr auto kReloadThreshold = 60 * crl::time(1000); } // namespace -StarsAmount FromTL(const MTPStarsAmount &value) { - const auto &data = value.data(); - return StarsAmount(data.vamount().v, data.vnanos().v); -} - Credits::Credits(not_null session) : _session(session) , _reload([=] { load(true); }) { @@ -32,7 +28,7 @@ Credits::Credits(not_null session) Credits::~Credits() = default; void Credits::apply(const MTPDupdateStarsBalance &data) { - apply(FromTL(data.vbalance())); + apply(CreditsAmountFromTL(data.vbalance())); } rpl::producer Credits::rateValue( @@ -40,6 +36,10 @@ rpl::producer Credits::rateValue( return rpl::single(_session->appConfig().starsWithdrawRate()); } +float64 Credits::usdRate() const { + return _session->appConfig().currencyWithdrawRate(); +} + void Credits::load(bool force) { if (_loader || (!force @@ -47,10 +47,23 @@ void Credits::load(bool force) { && _lastLoaded + kReloadThreshold > crl::now())) { return; } - _loader = std::make_unique(_session->user()); - _loader->request({}, [=](Data::CreditsStatusSlice slice) { - _loader = nullptr; - apply(slice.balance); + const auto self = _session->user(); + _loader = std::make_unique(); + _loader->make_state(self)->request({}, [=]( + Data::CreditsStatusSlice slice) { + const auto balance = slice.balance; + const auto apiStats + = _loader->make_state(self); + const auto finish = [=](bool statsEnabled) { + _statsEnabled = statsEnabled; + apply(balance); + _loader = nullptr; + }; + apiStats->request() | rpl::start_with_error_done([=] { + finish(false); + }, [=] { + finish(true); + }, *_loader); }); } @@ -67,33 +80,83 @@ rpl::producer Credits::loadedValue() const { ) | rpl::then(_loadedChanges.events() | rpl::map_to(true)); } -StarsAmount Credits::balance() const { +CreditsAmount Credits::balance() const { return _nonLockedBalance.current(); } -StarsAmount Credits::balance(PeerId peerId) const { +CreditsAmount Credits::balance(PeerId peerId) const { const auto it = _cachedPeerBalances.find(peerId); - return (it != _cachedPeerBalances.end()) ? it->second : StarsAmount(); + return (it != _cachedPeerBalances.end()) ? it->second : CreditsAmount(); } -uint64 Credits::balanceCurrency(PeerId peerId) const { +CreditsAmount Credits::balanceCurrency(PeerId peerId) const { const auto it = _cachedPeerCurrencyBalances.find(peerId); - return (it != _cachedPeerCurrencyBalances.end()) ? it->second : 0; + return (it != _cachedPeerCurrencyBalances.end()) + ? it->second + : CreditsAmount(0, 0, CreditsType::Ton); } -rpl::producer Credits::balanceValue() const { +rpl::producer Credits::balanceValue() const { return _nonLockedBalance.value(); } +void Credits::tonLoad(bool force) { + if (_tonRequestId + || (!force + && _tonLastLoaded + && _tonLastLoaded + kReloadThreshold > crl::now())) { + return; + } + _tonRequestId = _session->api().request(MTPpayments_GetStarsStatus( + MTP_flags(MTPpayments_GetStarsStatus::Flag::f_ton), + MTP_inputPeerSelf() + )).done([=](const MTPpayments_StarsStatus &result) { + _tonRequestId = 0; + const auto amount = CreditsAmountFromTL(result.data().vbalance()); + if (amount.ton()) { + apply(amount); + } else if (amount.empty()) { + apply(CreditsAmount(0, CreditsType::Ton)); + } else { + LOG(("API Error: Got weird balance.")); + } + }).fail([=](const MTP::Error &error) { + _tonRequestId = 0; + LOG(("API Error: Couldn't get TON balance, error: %1" + ).arg(error.type())); + }).send(); +} + +bool Credits::tonLoaded() const { + return _tonLastLoaded != 0; +} + +rpl::producer Credits::tonLoadedValue() const { + if (tonLoaded()) { + return rpl::single(true); + } + return rpl::single( + false + ) | rpl::then(_tonLoadedChanges.events() | rpl::map_to(true)); +} + +CreditsAmount Credits::tonBalance() const { + return _tonBalance.current(); +} + +rpl::producer Credits::tonBalanceValue() const { + return _tonBalance.value(); +} + void Credits::updateNonLockedValue() { _nonLockedBalance = (_balance >= _locked) ? (_balance - _locked) - : StarsAmount(); + : CreditsAmount(); } -void Credits::lock(StarsAmount count) { +void Credits::lock(CreditsAmount count) { Expects(loaded()); - Expects(count >= StarsAmount(0)); + Expects(count >= CreditsAmount(0)); Expects(_locked + count <= _balance); _locked += count; @@ -101,8 +164,8 @@ void Credits::lock(StarsAmount count) { updateNonLockedValue(); } -void Credits::unlock(StarsAmount count) { - Expects(count >= StarsAmount(0)); +void Credits::unlock(CreditsAmount count) { + Expects(count >= CreditsAmount(0)); Expects(_locked >= count); _locked -= count; @@ -110,12 +173,12 @@ void Credits::unlock(StarsAmount count) { updateNonLockedValue(); } -void Credits::withdrawLocked(StarsAmount count) { - Expects(count >= StarsAmount(0)); +void Credits::withdrawLocked(CreditsAmount count) { + Expects(count >= CreditsAmount(0)); Expects(_locked >= count); _locked -= count; - apply(_balance >= count ? (_balance - count) : StarsAmount(0)); + apply(_balance >= count ? (_balance - count) : CreditsAmount(0)); invalidate(); } @@ -123,22 +186,31 @@ void Credits::invalidate() { _reload.call(); } -void Credits::apply(StarsAmount balance) { - _balance = balance; - updateNonLockedValue(); +void Credits::apply(CreditsAmount balance) { + if (balance.ton()) { + _tonBalance = balance; - const auto was = std::exchange(_lastLoaded, crl::now()); - if (!was) { - _loadedChanges.fire({}); + const auto was = std::exchange(_tonLastLoaded, crl::now()); + if (!was) { + _tonLoadedChanges.fire({}); + } + } else { + _balance = balance; + updateNonLockedValue(); + + const auto was = std::exchange(_lastLoaded, crl::now()); + if (!was) { + _loadedChanges.fire({}); + } } } -void Credits::apply(PeerId peerId, StarsAmount balance) { +void Credits::apply(PeerId peerId, CreditsAmount balance) { _cachedPeerBalances[peerId] = balance; _refreshedByPeerId.fire_copy(peerId); } -void Credits::applyCurrency(PeerId peerId, uint64 balance) { +void Credits::applyCurrency(PeerId peerId, CreditsAmount balance) { _cachedPeerCurrencyBalances[peerId] = balance; _refreshedByPeerId.fire_copy(peerId); } @@ -148,4 +220,39 @@ rpl::producer<> Credits::refreshedByPeerId(PeerId peerId) { ) | rpl::filter(rpl::mappers::_1 == peerId) | rpl::to_empty; } +bool Credits::statsEnabled() const { + return _statsEnabled; +} + } // namespace Data + +CreditsAmount CreditsAmountFromTL(const MTPStarsAmount &amount) { + return amount.match([&](const MTPDstarsAmount &data) { + return CreditsAmount( + data.vamount().v, + data.vnanos().v, + CreditsType::Stars); + }, [&](const MTPDstarsTonAmount &data) { + const auto isNegative = (static_cast(data.vamount().v) < 0); + const auto absValue = isNegative + ? uint64(~data.vamount().v + 1) + : data.vamount().v; + const auto result = CreditsAmount( + int64(absValue / 1'000'000'000), + absValue % 1'000'000'000, + CreditsType::Ton); + return isNegative + ? CreditsAmount(0, CreditsType::Ton) - result + : result; + }); +} + +CreditsAmount CreditsAmountFromTL(const MTPStarsAmount *amount) { + return amount ? CreditsAmountFromTL(*amount) : CreditsAmount(); +} + +MTPStarsAmount StarsAmountToTL(CreditsAmount amount) { + return amount.ton() ? MTP_starsTonAmount( + MTP_long(amount.whole() * uint64(1'000'000'000) + amount.nano()) + ) : MTP_starsAmount(MTP_long(amount.whole()), MTP_int(amount.nano())); +} diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h index e2d719091c..c7a7325c4f 100644 --- a/Telegram/SourceFiles/data/components/credits.h +++ b/Telegram/SourceFiles/data/components/credits.h @@ -7,44 +7,46 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace Api { -class CreditsStatus; -} // namespace Api - namespace Main { class Session; } // namespace Main namespace Data { -[[nodiscard]] StarsAmount FromTL(const MTPStarsAmount &value); - class Credits final { public: explicit Credits(not_null session); ~Credits(); void load(bool force = false); - void apply(StarsAmount balance); - void apply(PeerId peerId, StarsAmount balance); - [[nodiscard]] bool loaded() const; [[nodiscard]] rpl::producer loadedValue() const; - - [[nodiscard]] StarsAmount balance() const; - [[nodiscard]] StarsAmount balance(PeerId peerId) const; - [[nodiscard]] rpl::producer balanceValue() const; + [[nodiscard]] CreditsAmount balance() const; + [[nodiscard]] CreditsAmount balance(PeerId peerId) const; + [[nodiscard]] rpl::producer balanceValue() const; + [[nodiscard]] float64 usdRate() const; [[nodiscard]] rpl::producer rateValue( not_null ownedBotOrChannel); [[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId); - void applyCurrency(PeerId peerId, uint64 balance); - [[nodiscard]] uint64 balanceCurrency(PeerId peerId) const; + void tonLoad(bool force = false); + [[nodiscard]] bool tonLoaded() const; + [[nodiscard]] rpl::producer tonLoadedValue() const; + [[nodiscard]] CreditsAmount tonBalance() const; + [[nodiscard]] rpl::producer tonBalanceValue() const; - void lock(StarsAmount count); - void unlock(StarsAmount count); - void withdrawLocked(StarsAmount count); + void apply(CreditsAmount balance); + void apply(PeerId peerId, CreditsAmount balance); + + [[nodiscard]] bool statsEnabled() const; + + void applyCurrency(PeerId peerId, CreditsAmount balance); + [[nodiscard]] CreditsAmount balanceCurrency(PeerId peerId) const; + + void lock(CreditsAmount count); + void unlock(CreditsAmount count); + void withdrawLocked(CreditsAmount count); void invalidate(); void apply(const MTPDupdateStarsBalance &data); @@ -54,18 +56,25 @@ private: const not_null _session; - std::unique_ptr _loader; + std::unique_ptr _loader; - base::flat_map _cachedPeerBalances; - base::flat_map _cachedPeerCurrencyBalances; + base::flat_map _cachedPeerBalances; + base::flat_map _cachedPeerCurrencyBalances; - StarsAmount _balance; - StarsAmount _locked; - rpl::variable _nonLockedBalance; + CreditsAmount _balance; + CreditsAmount _locked; + rpl::variable _nonLockedBalance; rpl::event_stream<> _loadedChanges; crl::time _lastLoaded = 0; float64 _rate = 0.; + rpl::variable _tonBalance; + rpl::event_stream<> _tonLoadedChanges; + crl::time _tonLastLoaded = false; + mtpRequestId _tonRequestId = 0; + + bool _statsEnabled = false; + rpl::event_stream _refreshedByPeerId; SingleQueuedInvokation _reload; diff --git a/Telegram/SourceFiles/data/components/promo_suggestions.cpp b/Telegram/SourceFiles/data/components/promo_suggestions.cpp index ce027c6703..72d81109a8 100644 --- a/Telegram/SourceFiles/data/components/promo_suggestions.cpp +++ b/Telegram/SourceFiles/data/components/promo_suggestions.cpp @@ -247,7 +247,9 @@ void PromoSuggestions::invalidate() { } std::optional PromoSuggestions::custom() const { - return _custom; + return (_custom && !_dismissedSuggestions.contains(_custom->suggestion)) + ? _custom + : std::nullopt; } void PromoSuggestions::requestContactBirthdays(Fn done, bool force) { @@ -314,4 +316,9 @@ std::optional PromoSuggestions::knownBirthdaysToday() const { return _contactBirthdaysToday; } +QString PromoSuggestions::SugValidatePassword() { + static const auto key = u"VALIDATE_PASSWORD"_q; + return key; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/promo_suggestions.h b/Telegram/SourceFiles/data/components/promo_suggestions.h index 33f943a958..e57c1162cf 100644 --- a/Telegram/SourceFiles/data/components/promo_suggestions.h +++ b/Telegram/SourceFiles/data/components/promo_suggestions.h @@ -51,6 +51,8 @@ public: [[nodiscard]] auto knownBirthdaysToday() const -> std::optional>; + [[nodiscard]] static QString SugValidatePassword(); + private: void setTopPromoted( History *promoted, diff --git a/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp b/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp new file mode 100644 index 0000000000..87bccbd272 --- /dev/null +++ b/Telegram/SourceFiles/data/components/recent_shared_media_gifts.cpp @@ -0,0 +1,80 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/components/recent_shared_media_gifts.h" + +#include "api/api_premium.h" +#include "apiwrap.h" +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "main/main_session.h" + +namespace Data { +namespace { + +constexpr auto kReloadThreshold = 60 * crl::time(1000); +constexpr auto kMaxGifts = 3; + +} // namespace + +RecentSharedMediaGifts::RecentSharedMediaGifts( + not_null session) +: _session(session) { +} + +RecentSharedMediaGifts::~RecentSharedMediaGifts() = default; + +void RecentSharedMediaGifts::request( + not_null peer, + Fn)> done) { + const auto it = _recent.find(peer->id); + if (it != _recent.end()) { + auto &entry = it->second; + if (entry.lastRequestTime + && entry.lastRequestTime + kReloadThreshold > crl::now()) { + done(std::vector(entry.ids.begin(), entry.ids.end())); + return; + } + if (entry.requestId) { + peer->session().api().request(entry.requestId).cancel(); + } + } + + _recent[peer->id].requestId = peer->session().api().request( + MTPpayments_GetSavedStarGifts( + MTP_flags(0), + peer->input, + MTP_string(QString()), + MTP_int(kMaxGifts) + )).done([=](const MTPpayments_SavedStarGifts &result) { + const auto &data = result.data(); + const auto owner = &peer->owner(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + auto &entry = _recent[peer->id]; + entry.lastRequestTime = crl::now(); + entry.requestId = 0; + entry.ids = {}; + + auto conter = 0; + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(peer, gift)) { + entry.ids.push_front(parsed->info.document->id); + if (entry.ids.size() > kMaxGifts) { + entry.ids.pop_back(); + } + if (++conter >= kMaxGifts) { + break; + } + } + } + done(std::vector(entry.ids.begin(), entry.ids.end())); + }).send(); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/recent_shared_media_gifts.h b/Telegram/SourceFiles/data/components/recent_shared_media_gifts.h new file mode 100644 index 0000000000..f3d81fbda4 --- /dev/null +++ b/Telegram/SourceFiles/data/components/recent_shared_media_gifts.h @@ -0,0 +1,38 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class RecentSharedMediaGifts final { +public: + explicit RecentSharedMediaGifts(not_null session); + ~RecentSharedMediaGifts(); + + void request( + not_null peer, + Fn)> done); + +private: + struct Entry { + std::deque ids; + crl::time lastRequestTime = 0; + mtpRequestId requestId = 0; + }; + + const not_null _session; + + base::flat_map _recent; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index cecdf3606c..493a3cc36f 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -59,6 +59,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), data.vdate(), data.vaction(), @@ -96,7 +97,10 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_long(data.veffect().value_or_empty()), // effect data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck(), MTP_int(data.vreport_delivery_until_date().value_or_empty()), - MTP_long(data.vpaid_message_stars().value_or_empty())); + MTP_long(data.vpaid_message_stars().value_or_empty()), + (data.vsuggested_post() + ? *data.vsuggested_post() + : MTPSuggestedPost())); }); } @@ -271,7 +275,8 @@ void ScheduledMessages::sendNowSimpleMessage( MTP_long(local->effectId()), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong()), // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost()), localFlags, NewMessageType::Unread); diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index fa0674124b..a0d308f814 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -277,7 +277,10 @@ void SponsoredMessages::request(not_null history, Fn done) { } } request.requestId = _session->api().request( - MTPmessages_GetSponsoredMessages(history->peer->input) + MTPmessages_GetSponsoredMessages( + MTP_flags(0), + history->peer->input, + MTPint()) // msg_id ).done([=](const MTPmessages_sponsoredMessages &result) { parse(history, result); if (done) { diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 773c50d5d4..5356d48065 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -204,6 +204,42 @@ void Changes::topicRemoved(not_null topic) { _topicChanges.drop(topic); } +void Changes::sublistUpdated( + not_null sublist, + SublistUpdate::Flags flags) { + const auto drop = (flags & SublistUpdate::Flag::Destroyed); + _sublistChanges.updated(sublist, flags, drop); + if (!drop) { + scheduleNotifications(); + } +} + +rpl::producer Changes::sublistUpdates( + SublistUpdate::Flags flags) const { + return _sublistChanges.updates(flags); +} + +rpl::producer Changes::sublistUpdates( + not_null sublist, + SublistUpdate::Flags flags) const { + return _sublistChanges.updates(sublist, flags); +} + +rpl::producer Changes::sublistFlagsValue( + not_null sublist, + SublistUpdate::Flags flags) const { + return _sublistChanges.flagsValue(sublist, flags); +} + +rpl::producer Changes::realtimeSublistUpdates( + SublistUpdate::Flag flag) const { + return _sublistChanges.realtimeUpdates(flag); +} + +void Changes::sublistRemoved(not_null sublist) { + _sublistChanges.drop(sublist); +} + void Changes::messageUpdated( not_null item, MessageUpdate::Flags flags) { @@ -304,6 +340,23 @@ rpl::producer Changes::realtimeStoryUpdates( return _storyChanges.realtimeUpdates(flag); } +void Changes::chatAdminChanged( + not_null peer, + not_null user, + ChatAdminRights rights, + QString rank) { + _chatAdminChanges.fire({ + .peer = peer, + .user = user, + .rights = rights, + .rank = std::move(rank), + }); +} + +rpl::producer Changes::chatAdminChanges() const { + return _chatAdminChanges.events(); +} + void Changes::scheduleNotifications() { if (!_notify) { _notify = true; @@ -323,6 +376,7 @@ void Changes::sendNotifications() { _messageChanges.sendNotifications(); _entryChanges.sendNotifications(); _topicChanges.sendNotifications(); + _sublistChanges.sendNotifications(); _storyChanges.sendNotifications(); } diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2eb84caedf..190f3f554c 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/flags.h" +#include "data/data_chat_participant_status.h" class History; class PeerData; @@ -38,6 +39,7 @@ inline constexpr int CountBit(Flag Last = Flag::LastUsedBit) { namespace Data { class ForumTopic; +class SavedSublist; class Story; struct NameUpdate { @@ -112,12 +114,13 @@ struct PeerUpdate { StickersSet = (1ULL << 46), EmojiSet = (1ULL << 47), DiscussionLink = (1ULL << 48), - ChannelLocation = (1ULL << 49), - Slowmode = (1ULL << 50), - GroupCall = (1ULL << 51), + MonoforumLink = (1ULL << 49), + ChannelLocation = (1ULL << 50), + Slowmode = (1ULL << 51), + GroupCall = (1ULL << 52), // For iteration - LastUsedBit = (1ULL << 51), + LastUsedBit = (1ULL << 52), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } @@ -183,6 +186,25 @@ struct TopicUpdate { }; +struct SublistUpdate { + enum class Flag : uint32 { + None = 0, + + UnreadView = (1U << 1), + UnreadReactions = (1U << 2), + CloudDraft = (1U << 3), + Destroyed = (1U << 4), + + LastUsedBit = (1U << 4), + }; + using Flags = base::flags; + friend inline constexpr auto is_flag_type(Flag) { return true; } + + not_null sublist; + Flags flags = 0; + +}; + struct MessageUpdate { enum class Flag : uint32 { None = 0, @@ -250,6 +272,13 @@ struct StoryUpdate { }; +struct ChatAdminChange { + not_null peer; + not_null user; + ChatAdminRights rights; + QString rank; +}; + class Changes final { public: explicit Changes(not_null session); @@ -304,6 +333,21 @@ public: TopicUpdate::Flag flag) const; void topicRemoved(not_null topic); + void sublistUpdated( + not_null sublist, + SublistUpdate::Flags flags); + [[nodiscard]] rpl::producer sublistUpdates( + SublistUpdate::Flags flags) const; + [[nodiscard]] rpl::producer sublistUpdates( + not_null sublist, + SublistUpdate::Flags flags) const; + [[nodiscard]] rpl::producer sublistFlagsValue( + not_null sublist, + SublistUpdate::Flags flags) const; + [[nodiscard]] rpl::producer realtimeSublistUpdates( + SublistUpdate::Flag flag) const; + void sublistRemoved(not_null sublist); + void messageUpdated( not_null item, MessageUpdate::Flags flags); @@ -347,6 +391,13 @@ public: [[nodiscard]] rpl::producer realtimeStoryUpdates( StoryUpdate::Flag flag) const; + void chatAdminChanged( + not_null peer, + not_null user, + ChatAdminRights rights, + QString rank); + [[nodiscard]] rpl::producer chatAdminChanges() const; + void sendNotifications(); private: @@ -395,9 +446,11 @@ private: Manager _peerChanges; Manager _historyChanges; Manager _topicChanges; + Manager _sublistChanges; Manager _messageChanges; Manager _entryChanges; Manager _storyChanges; + rpl::event_stream _chatAdminChanges; bool _notify = false; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 602b32cceb..dd00cc0954 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_group_call.h" #include "data/data_message_reactions.h" +#include "data/data_saved_messages.h" #include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" #include "main/main_session.h" @@ -89,6 +90,29 @@ std::unique_ptr MegagroupInfo::takeForumData() { return nullptr; } +void MegagroupInfo::ensureMonoforum(not_null that) { + if (!_monoforum) { + const auto history = that->owner().history(that); + _monoforum = std::make_unique( + &that->owner(), + that); + history->monoforumChanged(nullptr); + } +} + +Data::SavedMessages *MegagroupInfo::monoforum() const { + return _monoforum.get(); +} + +std::unique_ptr MegagroupInfo::takeMonoforumData() { + if (auto result = base::take(_monoforum)) { + const auto history = result->owner().history(result->parentChat()); + history->monoforumChanged(result.get()); + return result; + } + return nullptr; +} + ChannelData::ChannelData(not_null owner, PeerId id) : PeerData(owner, id) , inputChannel( @@ -161,6 +185,15 @@ void ChannelData::setAccessHash(uint64 accessHash) { } void ChannelData::setFlags(ChannelDataFlags which) { + if (which & Flag::MonoforumAdmin) { + which |= Flag::Monoforum; + } + if (which & (Flag::Forum | Flag::Monoforum)) { + which |= Flag::Megagroup; + } + if (which & Flag::Monoforum) { + which &= ~Flag::Forum; + } const auto diff = flags() ^ which; if ((which & Flag::Megagroup) && !mgInfo) { mgInfo = std::make_unique(); @@ -169,11 +202,17 @@ void ChannelData::setFlags(ChannelDataFlags which) { // Let Data::Forum live till the end of _flags.set. // That way the data can be used in changes handler. // Example: render frame for forum auto-closing animation. - const auto taken = ((diff & Flag::Forum) && !(which & Flag::Forum)) + const auto takenForum = ((diff & Flag::Forum) && !(which & Flag::Forum)) ? mgInfo->takeForumData() : nullptr; + const auto takenMonoforum = ((diff & Flag::MonoforumAdmin) + && !(which & Flag::MonoforumAdmin)) + ? mgInfo->takeMonoforumData() + : nullptr; const auto wasIn = amIn(); - if ((diff & Flag::Forum) && (which & Flag::Forum)) { + if ((diff & Flag::MonoforumAdmin) && (which & Flag::MonoforumAdmin)) { + mgInfo->ensureMonoforum(this); + } else if ((diff & Flag::Forum) && (which & Flag::Forum)) { mgInfo->ensureForum(this); } _flags.set(which); @@ -192,6 +231,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { } } if (diff & (Flag::Forum + | Flag::MonoforumAdmin | Flag::CallNotEmpty | Flag::SimilarExpanded | Flag::Signatures @@ -200,12 +240,14 @@ void ChannelData::setFlags(ChannelDataFlags which) { if (diff & Flag::CallNotEmpty) { history->updateChatListEntry(); } - if (diff & Flag::Forum) { + if (diff & (Flag::Forum | Flag::MonoforumAdmin)) { Core::App().notifications().clearFromHistory(history); history->updateChatListEntryHeight(); if (history->inChatList()) { if (const auto forum = this->forum()) { forum->preloadTopics(); + } else if (const auto monoforum = this->monoforum()) { + monoforum->preloadSublists(); } } } @@ -222,7 +264,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { } } } - if (const auto raw = taken.get()) { + if (const auto raw = takenForum.get()) { owner().forumIcons().clearUserpicsReset(raw); } } @@ -276,8 +318,9 @@ const ChannelLocation *ChannelData::getLocation() const { } void ChannelData::setDiscussionLink(ChannelData *linked) { - if (_discussionLink != linked) { + if (_discussionLink != linked || !_discussionLinkKnown) { _discussionLink = linked; + _discussionLinkKnown = true; if (const auto history = owner().historyLoaded(this)) { history->forceFullResize(); } @@ -286,11 +329,42 @@ void ChannelData::setDiscussionLink(ChannelData *linked) { } ChannelData *ChannelData::discussionLink() const { - return _discussionLink.value_or(nullptr); + return _discussionLink; } bool ChannelData::discussionLinkKnown() const { - return _discussionLink.has_value(); + return _discussionLinkKnown; +} + +void ChannelData::setMonoforumLink(ChannelData *link) { + if (_monoforumLink) { + if (isBroadcast()) { + _monoforumLink->setMonoforumLink(link ? this : nullptr); + } else if (isMonoforum()) { + if (!link && !monoforumDisabled()) { + setFlags(flags() | Flag::MonoforumDisabled); + } else if (link && monoforumDisabled()) { + setFlags(flags() & ~Flag::MonoforumDisabled); + } + } + return; + } else if (!link) { + return; + } + _monoforumLink = link; + link->setMonoforumLink(this); + session().changes().peerUpdated(this, UpdateFlag::MonoforumLink); + if (isMegagroup() && link->canAccessMonoforum()) { + setFlags(flags() | Flag::MonoforumAdmin); + } +} + +ChannelData *ChannelData::monoforumLink() const { + return _monoforumLink; +} + +bool ChannelData::monoforumDisabled() const { + return flags() & Flag::MonoforumDisabled; } void ChannelData::setMembersCount(int newMembersCount) { @@ -349,6 +423,11 @@ void ChannelData::setPendingRequestsCount( } } +bool ChannelData::useSubsectionTabs() const { + return amMonoforumAdmin() + || (isForum() && (flags() & ChannelDataFlag::ForumTabs)); +} + ChatRestrictionsInfo ChannelData::KickedRestrictedRights( not_null participant) { using Flag = ChatRestriction; @@ -563,38 +642,38 @@ void ChannelData::setAvailableMinId(MsgId availableMinId) { } bool ChannelData::canBanMembers() const { - return amCreator() - || (adminRights() & AdminRight::BanUsers); + return amCreator() || (adminRights() & AdminRight::BanUsers); } bool ChannelData::canPostMessages() const { - return amCreator() - || (adminRights() & AdminRight::PostMessages); + return amCreator() || (adminRights() & AdminRight::PostMessages); } bool ChannelData::canEditMessages() const { - return amCreator() - || (adminRights() & AdminRight::EditMessages); + return amCreator() || (adminRights() & AdminRight::EditMessages); } bool ChannelData::canDeleteMessages() const { - return amCreator() - || (adminRights() & AdminRight::DeleteMessages); + return amCreator() || (adminRights() & AdminRight::DeleteMessages); } bool ChannelData::canPostStories() const { - return amCreator() - || (adminRights() & AdminRight::PostStories); + return amCreator() || (adminRights() & AdminRight::PostStories); } bool ChannelData::canEditStories() const { - return amCreator() - || (adminRights() & AdminRight::EditStories); + if (isMonoforum()) { + return false; + } + return amCreator() || (adminRights() & AdminRight::EditStories); } bool ChannelData::canDeleteStories() const { - return amCreator() - || (adminRights() & AdminRight::DeleteStories); + return amCreator() || (adminRights() & AdminRight::DeleteStories); +} + +bool ChannelData::canAccessMonoforum() const { + return amCreator() || (adminRights() & AdminRight::ManageDirect); } bool ChannelData::canPostPaidMedia() const { @@ -610,14 +689,15 @@ bool ChannelData::hiddenPreHistory() const { } bool ChannelData::canAddMembers() const { - return isMegagroup() + return isMonoforum() + ? false + : isMegagroup() ? !amRestricted(ChatRestriction::AddParticipants) : ((adminRights() & AdminRight::InviteByLinkOrAdd) || amCreator()); } bool ChannelData::canAddAdmins() const { - return amCreator() - || (adminRights() & AdminRight::AddAdmins); + return amCreator() || (adminRights() & AdminRight::AddAdmins); } bool ChannelData::isAyuNoForwards() const { @@ -779,6 +859,11 @@ void ChannelData::setAdminRights(ChatAdminRights rights) { session().changes().peerUpdated( this, UpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers); + if (isBroadcast() && _monoforumLink) { + const auto flags = _monoforumLink->flags(); + _monoforumLink->setFlags((flags & ~Flag::MonoforumAdmin) + | (canAccessMonoforum() ? Flag::MonoforumAdmin : Flag())); + } } void ChannelData::setRestrictions(ChatRestrictionsInfo rights) { @@ -873,15 +958,16 @@ void ChannelData::growSlowmodeLastMessage(TimeId when) { } int ChannelData::starsPerMessage() const { - if (const auto info = mgInfo.get()) { - return info->_starsPerMessage; - } - return 0; + return _starsPerMessage; +} + +int ChannelData::commonStarsPerMessage() const { + return owner().commonStarsPerMessage(this); } void ChannelData::setStarsPerMessage(int stars) { - if (mgInfo && starsPerMessage() != stars) { - mgInfo->_starsPerMessage = stars; + if (_starsPerMessage != stars) { + _starsPerMessage = stars; session().changes().peerUpdated(this, UpdateFlag::StarsPerMessage); } checkTrustedPayForMessage(); @@ -1168,6 +1254,8 @@ void ApplyChannelUpdate( } channel->setMessagesTTL(update.vttl_period().value_or_empty()); + channel->setStarsPerMessage( + update.vsend_paid_messages_stars().value_or_empty()); using Flag = ChannelDataFlag; const auto mask = Flag::CanSetUsername | Flag::CanViewParticipants @@ -1182,7 +1270,9 @@ void ApplyChannelUpdate( | Flag::PaidMediaAllowed | Flag::CanViewCreditsRevenue | Flag::StargiftsAvailable - | Flag::PaidMessagesAvailable; + | Flag::PaidMessagesAvailable + | (channel->starsPerMessage() ? Flag::HasStarsPerMessage : Flag()) + | Flag::StarsPerMessageKnown; channel->setFlags((channel->flags() & ~mask) | (update.is_can_set_username() ? Flag::CanSetUsername : Flag()) | (update.is_can_view_participants() @@ -1209,7 +1299,9 @@ void ApplyChannelUpdate( : Flag()) | (update.is_paid_messages_available() ? Flag::PaidMessagesAvailable - : Flag())); + : Flag()) + | (channel->starsPerMessage() ? Flag::HasStarsPerMessage : Flag()) + | Flag::StarsPerMessageKnown); channel->setUserpicPhoto(update.vchat_photo()); if (const auto migratedFrom = update.vmigrated_from_chat_id()) { channel->addFlags(Flag::Megagroup); @@ -1384,14 +1476,16 @@ void ApplyChannelUpdate( const auto currencyLoadLifetime = std::make_shared(); const auto currencyLoad = currencyLoadLifetime->make_state(channel); - const auto apply = [=](Data::EarnInt balance) { + const auto apply = [=](const CreditsAmount &balance) { if (const auto strong = weak.get()) { strong->credits().applyCurrency(id, balance); } currencyLoadLifetime->destroy(); }; currencyLoad->request() | rpl::start_with_error_done( - [=](const QString &error) { apply(0); }, + [=](const QString &error) { + apply(CreditsAmount(0, CreditsType::Ton)); + }, [=] { apply(currencyLoad->data().currentBalance); }, *currencyLoadLifetime); base::timer_once(kTimeout) | rpl::start_with_next([=] { diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 2ad833a8fd..8629919f6e 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -16,6 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class ChannelData; +namespace Data { +class Forum; +class SavedMessages; +} // namespace Data + struct ChannelLocation { QString address; Data::LocationPoint point; @@ -74,6 +79,12 @@ enum class ChannelDataFlag : uint64 { StargiftsAvailable = (1ULL << 36), PaidMessagesAvailable = (1ULL << 37), AutoTranslation = (1ULL << 38), + Monoforum = (1ULL << 39), + MonoforumAdmin = (1ULL << 40), + MonoforumDisabled = (1ULL << 41), + ForumTabs = (1ULL << 42), + HasStarsPerMessage = (1ULL << 43), + StarsPerMessageKnown = (1ULL << 44), AyuNoForwards = (1ULL << 63), }; @@ -120,6 +131,10 @@ public: [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] std::unique_ptr takeForumData(); + void ensureMonoforum(not_null that); + [[nodiscard]] Data::SavedMessages *monoforum() const; + [[nodiscard]] std::unique_ptr takeMonoforumData(); + std::deque> lastParticipants; base::flat_map, Admin> lastAdmins; base::flat_map, Restricted> lastRestricted; @@ -156,7 +171,7 @@ private: ChannelLocation _location; Data::ChatBotCommands _botCommands; std::unique_ptr _forum; - int _starsPerMessage = 0; + std::unique_ptr _monoforum; friend class ChannelData; @@ -269,6 +284,13 @@ public: [[nodiscard]] bool paidMessagesAvailable() const { return flags() & Flag::PaidMessagesAvailable; } + [[nodiscard]] bool hasStarsPerMessage() const { + return flags() & Flag::HasStarsPerMessage; + } + [[nodiscard]] bool starsPerMessageKnown() const { + return flags() & Flag::StarsPerMessageKnown; + } + [[nodiscard]] bool useSubsectionTabs() const; [[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights( not_null participant); @@ -303,6 +325,9 @@ public: [[nodiscard]] bool isForum() const { return flags() & Flag::Forum; } + [[nodiscard]] bool isMonoforum() const { + return flags() & Flag::Monoforum; + } [[nodiscard]] bool hasUsername() const { return flags() & Flag::Username; } @@ -384,6 +409,7 @@ public: [[nodiscard]] bool canEditStories() const; [[nodiscard]] bool canDeleteStories() const; [[nodiscard]] bool canPostPaidMedia() const; + [[nodiscard]] bool canAccessMonoforum() const; [[nodiscard]] bool hiddenPreHistory() const; [[nodiscard]] bool canViewMembers() const; [[nodiscard]] bool canViewAdmins() const; @@ -416,6 +442,10 @@ public: [[nodiscard]] ChannelData *discussionLink() const; [[nodiscard]] bool discussionLinkKnown() const; + void setMonoforumLink(ChannelData *link); + [[nodiscard]] ChannelData *monoforumLink() const; + [[nodiscard]] bool monoforumDisabled() const; + void ptsInit(int32 pts) { _ptsWaiter.init(pts); } @@ -475,6 +505,7 @@ public: void setStarsPerMessage(int stars); [[nodiscard]] int starsPerMessage() const; + [[nodiscard]] int commonStarsPerMessage() const; [[nodiscard]] int peerGiftsCount() const; void setPeerGiftsCount(int count); @@ -513,6 +544,9 @@ public: [[nodiscard]] Data::Forum *forum() const { return mgInfo ? mgInfo->forum() : nullptr; } + [[nodiscard]] Data::SavedMessages *monoforum() const { + return mgInfo ? mgInfo->monoforum() : nullptr; + } void processTopics(const MTPVector &topics); @@ -549,18 +583,11 @@ private: std::vector &&reasons) override; Flags _flags = ChannelDataFlags(Flag::Forbidden); - int _peerGiftsCount = 0; PtsWaiter _ptsWaiter; Data::UsernamesInfo _username; - int _membersCount = -1; - int _adminsCount = 1; - int _restrictedCount = 0; - int _kickedCount = 0; - int _pendingRequestsCount = 0; - int _levelHint = 0; std::vector _recentRequesters; MsgId _availableMinId = 0; @@ -573,7 +600,19 @@ private: std::vector _unavailableReasons; std::unique_ptr _invitePeek; QString _inviteLink; - std::optional _discussionLink; + + ChannelData *_discussionLink = nullptr; + ChannelData *_monoforumLink = nullptr; + bool _discussionLinkKnown = false; + + int _peerGiftsCount = 0; + int _membersCount = -1; + int _adminsCount = 1; + int _restrictedCount = 0; + int _kickedCount = 0; + int _pendingRequestsCount = 0; + int _levelHint = 0; + int _starsPerMessage = 0; Data::AllowedReactions _allowedReactions; diff --git a/Telegram/SourceFiles/data/data_channel_earn.h b/Telegram/SourceFiles/data/data_channel_earn.h index c4db98f28b..eeb1977132 100644 --- a/Telegram/SourceFiles/data/data_channel_earn.h +++ b/Telegram/SourceFiles/data/data_channel_earn.h @@ -7,46 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "data/data_statistics_chart.h" - #include +#include "data/data_credits.h" +#include "data/data_statistics_chart.h" + namespace Data { using EarnInt = uint64; -constexpr auto kEarnMultiplier = EarnInt(1000000000); - -struct EarnHistoryEntry final { - enum class Type { - In, - Out, - Return, - }; - - enum class Status { - Success, - Failed, - Pending, - }; - - Type type; - Status status; - - EarnInt amount = 0; - QDateTime date; - QDateTime dateTo; - - QString provider; - - QDateTime successDate; - QString successLink; - -}; - struct EarnHistorySlice final { - using OffsetToken = int; - std::vector list; + using OffsetToken = QString; + std::vector list; int total = 0; bool allLoaded = false; OffsetToken token; @@ -58,9 +30,9 @@ struct EarnStatistics final { } Data::StatisticalGraph topHoursGraph; Data::StatisticalGraph revenueGraph; - EarnInt currentBalance = 0; - EarnInt availableBalance = 0; - EarnInt overallRevenue = 0; + CreditsAmount currentBalance; + CreditsAmount availableBalance; + CreditsAmount overallRevenue; float64 usdRate = 0.; bool switchedOff = false; diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 6d0ecd4022..b4530d82a5 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -465,6 +465,10 @@ rpl::producer ChatFilters::tagsEnabledValue() const { return _tagsEnabled.value(); } +rpl::producer ChatFilters::tagsEnabledChanges() const { + return _tagsEnabled.changes(); +} + void ChatFilters::requestToggleTags(bool value, Fn fail) { if (_toggleTagsRequestId) { return; diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index ef4c8deff7..cfba1f9a45 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -213,6 +213,7 @@ public: [[nodiscard]] bool tagsEnabled() const; [[nodiscard]] rpl::producer tagsEnabledValue() const; + [[nodiscard]] rpl::producer tagsEnabledChanges() const; void requestToggleTags(bool value, Fn fail); private: diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 1a25babb7f..e18a18e7d3 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -34,14 +34,50 @@ namespace { [[nodiscard]] ChatAdminRights ChatAdminRightsFlags( const MTPChatAdminRights &rights) { return rights.match([](const MTPDchatAdminRights &data) { - return ChatAdminRights::from_raw(int32(data.vflags().v)); + using Flag = ChatAdminRight; + return (data.is_change_info() ? Flag::ChangeInfo : Flag()) + | (data.is_post_messages() ? Flag::PostMessages : Flag()) + | (data.is_edit_messages() ? Flag::EditMessages : Flag()) + | (data.is_delete_messages() ? Flag::DeleteMessages : Flag()) + | (data.is_ban_users() ? Flag::BanUsers : Flag()) + | (data.is_invite_users() ? Flag::InviteByLinkOrAdd : Flag()) + | (data.is_pin_messages() ? Flag::PinMessages : Flag()) + | (data.is_add_admins() ? Flag::AddAdmins : Flag()) + | (data.is_anonymous() ? Flag::Anonymous : Flag()) + | (data.is_manage_call() ? Flag::ManageCall : Flag()) + | (data.is_other() ? Flag::Other : Flag()) + | (data.is_manage_topics() ? Flag::ManageTopics : Flag()) + | (data.is_post_stories() ? Flag::PostStories : Flag()) + | (data.is_edit_stories() ? Flag::EditStories : Flag()) + | (data.is_delete_stories() ? Flag::DeleteStories : Flag()) + | (data.is_manage_direct_messages() + ? Flag::ManageDirect + : Flag()); }); } [[nodiscard]] ChatRestrictions ChatBannedRightsFlags( const MTPChatBannedRights &rights) { return rights.match([](const MTPDchatBannedRights &data) { - return ChatRestrictions::from_raw(int32(data.vflags().v)); + using Flag = ChatRestriction; + return (data.is_view_messages() ? Flag::ViewMessages : Flag()) + | (data.is_send_stickers() ? Flag::SendStickers : Flag()) + | (data.is_send_gifs() ? Flag::SendGifs : Flag()) + | (data.is_send_games() ? Flag::SendGames : Flag()) + | (data.is_send_inline() ? Flag::SendInline : Flag()) + | (data.is_send_polls() ? Flag::SendPolls : Flag()) + | (data.is_send_photos() ? Flag::SendPhotos : Flag()) + | (data.is_send_videos() ? Flag::SendVideos : Flag()) + | (data.is_send_roundvideos() ? Flag::SendVideoMessages : Flag()) + | (data.is_send_audios() ? Flag::SendMusic : Flag()) + | (data.is_send_voices() ? Flag::SendVoiceMessages : Flag()) + | (data.is_send_docs() ? Flag::SendFiles : Flag()) + | (data.is_send_messages() ? Flag::SendOther : Flag()) + | (data.is_embed_links() ? Flag::EmbedLinks : Flag()) + | (data.is_change_info() ? Flag::ChangeInfo : Flag()) + | (data.is_invite_users() ? Flag::AddParticipants : Flag()) + | (data.is_pin_messages() ? Flag::PinMessages : Flag()) + | (data.is_manage_topics() ? Flag::CreateTopics : Flag()); }); } @@ -58,11 +94,63 @@ ChatAdminRightsInfo::ChatAdminRightsInfo(const MTPChatAdminRights &rights) : flags(ChatAdminRightsFlags(rights)) { } +MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info) { + using Flag = MTPDchatAdminRights::Flag; + using R = ChatAdminRight; + const auto flags = info.flags; + return MTP_chatAdminRights(MTP_flags(Flag() + | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) + | ((flags & R::PostMessages) ? Flag::f_post_messages : Flag()) + | ((flags & R::EditMessages) ? Flag::f_edit_messages : Flag()) + | ((flags & R::DeleteMessages) ? Flag::f_delete_messages : Flag()) + | ((flags & R::BanUsers) ? Flag::f_ban_users : Flag()) + | ((flags & R::InviteByLinkOrAdd) ? Flag::f_invite_users : Flag()) + | ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag()) + | ((flags & R::AddAdmins) ? Flag::f_add_admins : Flag()) + | ((flags & R::Anonymous) ? Flag::f_anonymous : Flag()) + | ((flags & R::ManageCall) ? Flag::f_manage_call : Flag()) + | ((flags & R::Other) ? Flag::f_other : Flag()) + | ((flags & R::ManageTopics) ? Flag::f_manage_topics : Flag()) + | ((flags & R::PostStories) ? Flag::f_post_stories : Flag()) + | ((flags & R::EditStories) ? Flag::f_edit_stories : Flag()) + | ((flags & R::DeleteStories) ? Flag::f_delete_stories : Flag()) + | ((flags & R::ManageDirect) + ? Flag::f_manage_direct_messages + : Flag()))); +} + ChatRestrictionsInfo::ChatRestrictionsInfo(const MTPChatBannedRights &rights) : flags(ChatBannedRightsFlags(rights)) , until(ChatBannedRightsUntilDate(rights)) { } +MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) { + using Flag = MTPDchatBannedRights::Flag; + using R = ChatRestriction; + const auto flags = info.flags; + return MTP_chatBannedRights( + MTP_flags(Flag() + | ((flags & R::ViewMessages) ? Flag::f_view_messages : Flag()) + | ((flags & R::SendStickers) ? Flag::f_send_stickers : Flag()) + | ((flags & R::SendGifs) ? Flag::f_send_gifs : Flag()) + | ((flags & R::SendGames) ? Flag::f_send_games : Flag()) + | ((flags & R::SendInline) ? Flag::f_send_inline : Flag()) + | ((flags & R::SendPolls) ? Flag::f_send_polls : Flag()) + | ((flags & R::SendPhotos) ? Flag::f_send_photos : Flag()) + | ((flags & R::SendVideos) ? Flag::f_send_videos : Flag()) + | ((flags & R::SendVideoMessages) ? Flag::f_send_roundvideos : Flag()) + | ((flags & R::SendMusic) ? Flag::f_send_audios : Flag()) + | ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag()) + | ((flags & R::SendFiles) ? Flag::f_send_docs : Flag()) + | ((flags & R::SendOther) ? Flag::f_send_messages : Flag()) + | ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag()) + | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) + | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) + | ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag()) + | ((flags & R::CreateTopics) ? Flag::f_manage_topics : Flag())), + MTP_int(info.until)); +} + namespace Data { std::vector ListOfRestrictions( @@ -163,10 +251,14 @@ bool CanSendAnyOf( } return false; } else if (const auto channel = peer->asChannel()) { + if (channel->monoforumDisabled()) { + return false; + } using Flag = ChannelDataFlag; const auto allowed = channel->amIn() || ((channel->flags() & Flag::HasLink) - && !(channel->flags() & Flag::JoinToWrite)); + && !(channel->flags() & Flag::JoinToWrite)) + || channel->isMonoforum(); if (!allowed || (forbidInForums && channel->isForum())) { return false; } else if (channel->canPostMessages()) { @@ -232,6 +324,9 @@ SendError RestrictionError( } const auto all = restricted.isWithEveryone(); const auto channel = peer->asChannel(); + if (channel && channel->monoforumDisabled()) { + return tr::lng_action_direct_messages_disabled(tr::now); + } if (!all && channel) { auto restrictedUntil = channel->restrictedUntil(); if (restrictedUntil > 0 diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index b3db584a4e..073096b09b 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -36,6 +36,7 @@ enum class ChatAdminRight { PostStories = (1 << 14), EditStories = (1 << 15), DeleteStories = (1 << 16), + ManageDirect = (1 << 17), }; inline constexpr bool is_flag_type(ChatAdminRight) { return true; } using ChatAdminRights = base::flags; @@ -75,6 +76,8 @@ struct ChatAdminRightsInfo { ChatAdminRights flags; }; +[[nodiscard]] MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info); + struct ChatRestrictionsInfo { ChatRestrictionsInfo() = default; ChatRestrictionsInfo(ChatRestrictions flags, TimeId until) @@ -87,6 +90,9 @@ struct ChatRestrictionsInfo { TimeId until = 0; }; +[[nodiscard]] MTPChatBannedRights RestrictionsToMTP( + ChatRestrictionsInfo info); + namespace Data { class Thread; @@ -190,18 +196,21 @@ struct SendError { struct Args { QString text; int boostsToLift = 0; + bool monoforumAdmin = false; bool premiumToLift = false; bool frozen = false; }; SendError(Args &&args) : text(std::move(args.text)) , boostsToLift(args.boostsToLift) + , monoforumAdmin(args.monoforumAdmin) , premiumToLift(args.premiumToLift) , frozen(args.frozen) { } QString text; int boostsToLift = 0; + bool monoforumAdmin = false; bool premiumToLift = false; bool frozen = false; @@ -210,7 +219,7 @@ struct SendError { } explicit operator bool() const { - return !text.isEmpty(); + return monoforumAdmin || !text.isEmpty(); } [[nodiscard]] bool has_value() const { return !text.isEmpty(); diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 0432097980..21f2427a4e 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -59,7 +59,7 @@ struct CreditsHistoryEntry final { QDateTime lastSaleDate; PhotoId photoId = 0; std::vector extended; - StarsAmount credits; + CreditsAmount credits; uint64 bareMsgId = 0; uint64 barePeerId = 0; uint64 bareGiveawayMsgId = 0; @@ -72,15 +72,20 @@ struct CreditsHistoryEntry final { uint64 stargiftId = 0; std::shared_ptr uniqueGift; Fn()> pinnedSavedGifts; - StarsAmount starrefAmount; + CreditsAmount starrefAmount; int starrefCommission = 0; uint64 starrefRecipientId = 0; PeerType peerType; QDateTime subscriptionUntil; + + // Currency properties. + QDateTime adsProceedsToDate; + QString provider; // Unused. + QDateTime successDate; QString successLink; int paidMessagesCount = 0; - StarsAmount paidMessagesAmount; + CreditsAmount paidMessagesAmount; int paidMessagesCommission = 0; int limitedCount = 0; int limitedLeft = 0; @@ -115,7 +120,7 @@ struct CreditsStatusSlice final { using OffsetToken = QString; std::vector list; std::vector subscriptions; - StarsAmount balance; + CreditsAmount balance; uint64 subscriptionsMissingBalance = 0; bool allLoaded = false; OffsetToken token; diff --git a/Telegram/SourceFiles/data/data_credits_earn.h b/Telegram/SourceFiles/data/data_credits_earn.h index e26e2bebc0..af1d840c72 100644 --- a/Telegram/SourceFiles/data/data_credits_earn.h +++ b/Telegram/SourceFiles/data/data_credits_earn.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "data/data_statistics_chart.h" #include @@ -22,9 +22,9 @@ struct CreditsEarnStatistics final { && overallRevenue; } Data::StatisticalGraph revenueGraph; - StarsAmount currentBalance; - StarsAmount availableBalance; - StarsAmount overallRevenue; + CreditsAmount currentBalance; + CreditsAmount availableBalance; + CreditsAmount overallRevenue; float64 usdRate = 0.; bool isWithdrawalEnabled = false; QDateTime nextWithdrawalAt; diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 4c901daa24..13b30f0bd2 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -187,7 +187,8 @@ void ResolveDocument( Window::SessionController *controller, not_null document, HistoryItem *item, - MsgId topicRootId) { + MsgId topicRootId, + PeerId monoforumPeerId) { if (document->isNull()) { return; } @@ -202,7 +203,7 @@ void ResolveDocument( controller->openDocument( document, true, - { msgId, topicRootId }); + { msgId, topicRootId, monoforumPeerId }); } }; diff --git a/Telegram/SourceFiles/data/data_document_resolver.h b/Telegram/SourceFiles/data/data_document_resolver.h index 4988297aaa..de9327312f 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.h +++ b/Telegram/SourceFiles/data/data_document_resolver.h @@ -31,6 +31,7 @@ void ResolveDocument( Window::SessionController *controller, not_null document, HistoryItem *item, - MsgId topicRootId); + MsgId topicRootId, + PeerId monoforumPeerId); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index 1bd7135e86..9aae0e8bb0 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -45,11 +45,13 @@ WebPageDraft WebPageDraft::FromItem(not_null item) { Draft::Draft( const TextWithTags &textWithTags, FullReplyTo reply, + SuggestPostOptions suggest, const MessageCursor &cursor, WebPageDraft webpage, mtpRequestId saveRequestId) : textWithTags(textWithTags) , reply(std::move(reply)) +, suggest(suggest) , cursor(cursor) , webpage(webpage) , saveRequestId(saveRequestId) { @@ -58,10 +60,12 @@ Draft::Draft( Draft::Draft( not_null field, FullReplyTo reply, + SuggestPostOptions suggest, WebPageDraft webpage, mtpRequestId saveRequestId) : textWithTags(field->getTextWithTags()) , reply(std::move(reply)) +, suggest(suggest) , cursor(field) , webpage(webpage) { } @@ -70,10 +74,11 @@ void ApplyPeerCloudDraft( not_null session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, const MTPDdraftMessage &draft) { const auto history = session->data().history(peerId); const auto date = draft.vdate().v; - if (history->skipCloudDraftUpdate(topicRootId, date)) { + if (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) { return; } const auto textWithTags = TextWithTags{ @@ -87,6 +92,7 @@ void ApplyPeerCloudDraft( ? ReplyToFromMTP(history, *draft.vreply_to()) : FullReplyTo(); replyTo.topicRootId = topicRootId; + replyTo.monoforumPeerId = monoforumPeerId; auto webpage = WebPageDraft{ .invert = draft.is_invert_media(), .removed = draft.is_no_webpage(), @@ -104,29 +110,43 @@ void ApplyPeerCloudDraft( } }, [](const auto &) {}); } + auto suggest = SuggestPostOptions(); + if (!history->suggestDraftAllowed()) { + // Don't apply suggest options in unsupported chats. + } else if (const auto suggested = draft.vsuggested_post()) { + const auto &data = suggested->data(); + suggest.exists = 1; + suggest.date = data.vschedule_date().value_or_empty(); + const auto price = CreditsAmountFromTL(data.vprice()); + suggest.priceWhole = price.whole(); + suggest.priceNano = price.nano(); + suggest.ton = price.ton() ? 1 : 0; + } auto cloudDraft = std::make_unique( textWithTags, replyTo, + suggest, MessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax), std::move(webpage)); cloudDraft->date = date; history->setCloudDraft(std::move(cloudDraft)); - history->applyCloudDraft(topicRootId); + history->applyCloudDraft(topicRootId, monoforumPeerId); } void ClearPeerCloudDraft( not_null session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, TimeId date) { const auto history = session->data().history(peerId); - if (history->skipCloudDraftUpdate(topicRootId, date)) { + if (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) { return; } - history->clearCloudDraft(topicRootId); - history->applyCloudDraft(topicRootId); + history->clearCloudDraft(topicRootId, monoforumPeerId); + history->applyCloudDraft(topicRootId, monoforumPeerId); } void SetChatLinkDraft(not_null peer, TextWithEntities draft) { @@ -146,15 +166,20 @@ void SetChatLinkDraft(not_null peer, TextWithEntities draft) { }; const auto history = peer->owner().history(peer->id); const auto topicRootId = MsgId(); - history->setLocalDraft(std::make_unique( + const auto monoforumPeerId = PeerId(); + history->setLocalDraft(std::make_unique( textWithTags, - FullReplyTo{ .topicRootId = topicRootId }, + FullReplyTo{ + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }, + SuggestPostOptions(), cursor, - Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); + WebPageDraft())); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( history, - Data::EntryUpdate::Flag::LocalDraftSet); + EntryUpdate::Flag::LocalDraftSet); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 1330995ed0..683188eadb 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -23,11 +23,13 @@ void ApplyPeerCloudDraft( not_null session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, const MTPDdraftMessage &draft); void ClearPeerCloudDraft( not_null session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, TimeId date); struct WebPageDraft { @@ -50,18 +52,21 @@ struct Draft { Draft( const TextWithTags &textWithTags, FullReplyTo reply, + SuggestPostOptions suggest, const MessageCursor &cursor, WebPageDraft webpage, mtpRequestId saveRequestId = 0); Draft( not_null field, FullReplyTo reply, + SuggestPostOptions suggest, WebPageDraft webpage, mtpRequestId saveRequestId = 0); TimeId date = 0; TextWithTags textWithTags; FullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft. + SuggestPostOptions suggest; MessageCursor cursor; WebPageDraft webpage; mtpRequestId saveRequestId = 0; @@ -72,22 +77,38 @@ public: [[nodiscard]] static constexpr DraftKey None() { return 0; } - [[nodiscard]] static constexpr DraftKey Local(MsgId topicRootId) { - return (topicRootId < 0 || topicRootId >= ServerMaxMsgId) + [[nodiscard]] static constexpr DraftKey Local( + MsgId topicRootId, + PeerId monoforumPeerId) { + return Invalid(topicRootId, monoforumPeerId) ? None() - : (topicRootId ? topicRootId.bare : kLocalDraftIndex); + : (topicRootId + ? topicRootId.bare + : monoforumPeerId + ? (monoforumPeerId.value + kMonoforumDraftBit) + : kLocalDraftIndex); } - [[nodiscard]] static constexpr DraftKey LocalEdit(MsgId topicRootId) { - return (topicRootId < 0 || topicRootId >= ServerMaxMsgId) + [[nodiscard]] static constexpr DraftKey LocalEdit( + MsgId topicRootId, + PeerId monoforumPeerId) { + return Invalid(topicRootId, monoforumPeerId) ? None() - : ((topicRootId ? topicRootId.bare : kLocalDraftIndex) - + kEditDraftShift); + : (kEditDraftShift + + (topicRootId + ? topicRootId.bare + : monoforumPeerId + ? (monoforumPeerId.value + kMonoforumDraftBit) + : kLocalDraftIndex)); } - [[nodiscard]] static constexpr DraftKey Cloud(MsgId topicRootId) { - return (topicRootId < 0 || topicRootId >= ServerMaxMsgId) + [[nodiscard]] static constexpr DraftKey Cloud( + MsgId topicRootId, + PeerId monoforumPeerId) { + return Invalid(topicRootId, monoforumPeerId) ? None() : topicRootId ? (kCloudDraftShift + topicRootId.bare) + : monoforumPeerId + ? (kCloudDraftShift + monoforumPeerId.value + kMonoforumDraftBit) : kCloudDraftIndex; } [[nodiscard]] static constexpr DraftKey Scheduled() { @@ -120,40 +141,62 @@ public: return !value ? None() : (value == kLocalDraftIndex + kEditDraftShiftOld) - ? LocalEdit(0) + ? LocalEdit(MsgId(), PeerId()) : (value == kScheduledDraftIndex + kEditDraftShiftOld) ? ScheduledEdit() : (value > 0 && value < 0x4000'0000) - ? Local(MsgId(value)) + ? Local(MsgId(value), PeerId()) : (value > kEditDraftShiftOld && value < kEditDraftShiftOld + 0x4000'000) - ? LocalEdit(int64(value - kEditDraftShiftOld)) + ? LocalEdit(MsgId(int64(value - kEditDraftShiftOld)), PeerId()) : None(); } [[nodiscard]] constexpr bool isLocal() const { return (_value == kLocalDraftIndex) - || (_value > 0 && _value < ServerMaxMsgId.bare); + || (_value > 0 + && (_value & kMonoforumDraftMask) < ServerMaxMsgId.bare); } [[nodiscard]] constexpr bool isCloud() const { return (_value == kCloudDraftIndex) - || (_value > kCloudDraftShift - && _value < kCloudDraftShift + ServerMaxMsgId.bare); + || ((_value & kMonoforumDraftMask) > kCloudDraftShift + && ((_value & kMonoforumDraftMask) + < kCloudDraftShift + ServerMaxMsgId.bare)); } [[nodiscard]] constexpr MsgId topicRootId() const { const auto max = ServerMaxMsgId.bare; - if (_value > kCloudDraftShift && _value < kCloudDraftShift + max) { + if (_value & kMonoforumDraftBit) { + return 0; + } else if ((_value > kCloudDraftShift) + && (_value < kCloudDraftShift + max)) { return (_value - kCloudDraftShift); - } else if (_value > kEditDraftShift && _value < kEditDraftShift + max) { + } else if ((_value > kEditDraftShift) + && (_value < kEditDraftShift + max)) { return (_value - kEditDraftShift); } else if (_value > 0 && _value < max) { return _value; } return 0; } - + [[nodiscard]] constexpr PeerId monoforumPeerId() const { + const auto max = ServerMaxMsgId.bare; + const auto value = _value & kMonoforumDraftMask; + if (!(_value & kMonoforumDraftBit)) { + return 0; + } else if ((value > kCloudDraftShift) + && (value < kCloudDraftShift + max)) { + return PeerId(UserId(value - kCloudDraftShift)); + } else if ((value > kEditDraftShift) + && (value < kEditDraftShift + max)) { + return PeerId(UserId(value - kEditDraftShift)); + } else if (value > 0 && value < max) { + return PeerId(UserId(value)); + } + return 0; + } friend inline constexpr auto operator<=>(DraftKey, DraftKey) = default; + friend inline constexpr bool operator==(DraftKey, DraftKey) = default; inline explicit operator bool() const { return _value != 0; @@ -163,9 +206,20 @@ private: constexpr DraftKey(int64 value) : _value(value) { } + [[nodiscard]] static constexpr bool Invalid( + MsgId topicRootId, + PeerId monoforumPeerId) { + return (topicRootId < 0) + || (topicRootId >= ServerMaxMsgId) + || !peerIsUser(monoforumPeerId) + || (monoforumPeerId.value >= ServerMaxMsgId); + } + static constexpr auto kLocalDraftIndex = -1; static constexpr auto kCloudDraftIndex = -2; static constexpr auto kScheduledDraftIndex = -3; + static constexpr auto kMonoforumDraftBit = (int64(1) << 60); + static constexpr auto kMonoforumDraftMask = (kMonoforumDraftBit - 1); static constexpr auto kEditDraftShift = ServerMaxMsgId.bare; static constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare; static constexpr auto kShortcutDraftShift = 3 * ServerMaxMsgId.bare; @@ -189,6 +243,7 @@ using HistoryDrafts = base::flat_map>; [[nodiscard]] inline bool DraftIsNull(const Draft *draft) { return !draft || (!draft->reply.messageId + && !draft->suggest.exists && DraftStringIsEmpty(draft->textWithTags.text)); } @@ -202,6 +257,7 @@ using HistoryDrafts = base::flat_map>; } return (a->textWithTags == b->textWithTags) && (a->reply == b->reply) + && (a->suggest == b->suggest) && (a->webpage == b->webpage); } diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 361135d885..3890b4c0ed 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -48,7 +48,6 @@ Forum::Forum(not_null history) , _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) { Expects(_history->peer->isChannel()); - if (_history->inChatList()) { preloadTopics(); } @@ -73,8 +72,11 @@ Forum::~Forum() { auto &changes = session().changes(); const auto peerId = _history->peer->id; for (const auto &[rootId, topic] : _topics) { - storage.unload(Storage::SharedMediaUnloadThread(peerId, rootId)); - _history->setForwardDraft(rootId, {}); + storage.unload(Storage::SharedMediaUnloadThread( + peerId, + rootId, + PeerId())); + _history->setForwardDraft(rootId, PeerId(), {}); const auto raw = topic.get(); changes.topicRemoved(raw); @@ -176,34 +178,39 @@ void Forum::applyTopicDeleted(MsgId rootId) { _topicsDeleted.emplace(rootId); const auto i = _topics.find(rootId); - if (i != end(_topics)) { - const auto raw = i->second.get(); - Core::App().notifications().clearFromTopic(raw); - owner().removeChatListEntry(raw); - - if (ranges::contains(_lastTopics, not_null(raw))) { - reorderLastTopics(); - } - - _topicDestroyed.fire(raw); - session().changes().topicUpdated( - raw, - Data::TopicUpdate::Flag::Destroyed); - session().changes().entryUpdated( - raw, - Data::EntryUpdate::Flag::Destroyed); - _topics.erase(i); - - _history->destroyMessagesByTopic(rootId); - session().storage().unload(Storage::SharedMediaUnloadThread( - _history->peer->id, - rootId)); - _history->setForwardDraft(rootId, {}); + if (i == end(_topics)) { + return; } + const auto raw = i->second.get(); + Core::App().notifications().clearFromTopic(raw); + owner().removeChatListEntry(raw); + + if (ranges::contains(_lastTopics, not_null(raw))) { + reorderLastTopics(); + } + + if (_activeSubsectionTopic == raw) { + _activeSubsectionTopic = nullptr; + } + _topicDestroyed.fire(raw); + session().changes().topicUpdated( + raw, + Data::TopicUpdate::Flag::Destroyed); + session().changes().entryUpdated( + raw, + Data::EntryUpdate::Flag::Destroyed); + _topics.erase(i); + + _history->destroyMessagesByTopic(rootId); + session().storage().unload(Storage::SharedMediaUnloadThread( + _history->peer->id, + rootId, + PeerId())); + _history->setForwardDraft(rootId, PeerId(), {}); } void Forum::reorderLastTopics() { - // We want first kShowChatNamesCount histories, by last message date. + // We want first kShowTopicNamesCount histories, by last message date. const auto pred = [](not_null a, not_null b) { const auto aItem = a->chatListMessage(); const auto bItem = b->chatListMessage(); @@ -255,6 +262,20 @@ const std::vector> &Forum::recentTopics() const { return _lastTopics; } +void Forum::saveActiveSubsectionThread(not_null thread) { + if (const auto topic = thread->asTopic()) { + Assert(topic->forum() == this); + _activeSubsectionTopic = topic->creating() ? nullptr : topic; + } else { + Assert(thread == history()); + _activeSubsectionTopic = nullptr; + } +} + +Thread *Forum::activeSubsectionThread() const { + return _activeSubsectionTopic; +} + void Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) { if (from || to) { reorderLastTopics(); diff --git a/Telegram/SourceFiles/data/data_forum.h b/Telegram/SourceFiles/data/data_forum.h index 732ef80097..ae0cc2d1a5 100644 --- a/Telegram/SourceFiles/data/data_forum.h +++ b/Telegram/SourceFiles/data/data_forum.h @@ -96,6 +96,9 @@ public: [[nodiscard]] auto recentTopics() const -> const std::vector> &; + void saveActiveSubsectionThread(not_null thread); + [[nodiscard]] Thread *activeSubsectionThread() const; + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } @@ -129,6 +132,8 @@ private: std::vector> _lastTopics; int _lastTopicsVersion = 0; + ForumTopic *_activeSubsectionTopic = nullptr; + rpl::event_stream<> _chatsListChanges; rpl::event_stream<> _chatsListLoadedEvents; diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 7e7998897c..799190714d 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -26,7 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_unread_things.h" #include "history/view/history_view_item_preview.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "main/main_session.h" #include "base/unixtime.h" #include "ui/painter.h" @@ -152,10 +152,10 @@ QImage ForumTopicGeneralIconFrame(int size, const QColor &color) { result.setDevicePixelRatio(ratio); result.fill(Qt::transparent); - const auto use = size * 0.8; - const auto skip = size * 0.1; + const auto use = size * 1.; + const auto skip = size * 0.; auto p = QPainter(&result); - svg.render(&p, QRectF(skip, 0, use, use)); + svg.render(&p, QRectF(skip, skip, use, use)); p.end(); return style::colorizeImage(result, color); @@ -362,8 +362,8 @@ void ForumTopic::subscribeToUnreadChanges() { ) | rpl::filter([=] { return inChatList(); }) | rpl::start_with_next([=]( - std::optional previous, - std::optional now) { + std::optional previous, + std::optional now) { if (previous.value_or(0) != now.value_or(0)) { _forum->recentTopicsInvalidate(this); } @@ -406,6 +406,7 @@ void ForumTopic::applyTopic(const MTPDforumTopic &data) { &session(), channel()->id, _rootId, + PeerId(), data); }, [](const MTPDdraftMessageEmpty&) {}); } @@ -709,7 +710,7 @@ void ForumTopic::requestChatListMessage() { TimeId ForumTopic::adjustedChatListTimeId() const { const auto result = chatListTimeId(); - if (const auto draft = history()->cloudDraft(_rootId)) { + if (const auto draft = history()->cloudDraft(_rootId, PeerId())) { if (!Data::DraftIsNull(draft) && !session().supportMode()) { return std::max(result, draft->date); } @@ -867,7 +868,7 @@ void ForumTopic::setMuted(bool muted) { session().changes().topicUpdated(this, UpdateFlag::Notifications); } -not_null ForumTopic::sendActionPainter() { +HistoryView::SendActionPainter *ForumTopic::sendActionPainter() { return _sendActionPainter.get(); } diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index 06423e4750..aafa12a788 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -181,7 +181,7 @@ public: void setMuted(bool muted) override; [[nodiscard]] auto sendActionPainter() - ->not_null override; + -> HistoryView::SendActionPainter* override; private: enum class Flag : uchar { diff --git a/Telegram/SourceFiles/data/data_groups.cpp b/Telegram/SourceFiles/data/data_groups.cpp index 15bb0d820e..7af7050f14 100644 --- a/Telegram/SourceFiles/data/data_groups.cpp +++ b/Telegram/SourceFiles/data/data_groups.cpp @@ -84,7 +84,7 @@ void Groups::refreshMessage( _data->requestItemViewRefresh(item); return; } - if (!item->isRegular() && !item->isScheduled()) { + if (!item->isRegular() && !item->isScheduled() && !item->isUploading()) { return; } const auto groupId = item->groupId(); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 6939217295..eb85f931b1 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -64,6 +65,14 @@ MTPInputReplyTo ReplyToForMTP( && (to->history() != history || to->id != replyingToTopicId)) ? to->topicRootId() : replyingToTopicId; + const auto possibleMonoforumPeerId = (to && to->sublistPeerId()) + ? to->sublistPeerId() + : replyTo.monoforumPeerId + ? replyTo.monoforumPeerId + : history->session().user()->id; + const auto replyToMonoforumPeerId = history->peer->amMonoforumAdmin() + ? possibleMonoforumPeerId + : PeerId(); const auto external = replyTo.messageId && (replyTo.messageId.peer != history->peer->id || replyingToTopicId != replyToTopicId); @@ -78,6 +87,9 @@ MTPInputReplyTo ReplyToForMTP( | (replyTo.quote.text.isEmpty() ? Flag() : (Flag::f_quote_text | Flag::f_quote_offset)) + | (replyToMonoforumPeerId + ? Flag::f_monoforum_peer_id + : Flag()) | (quoteEntities.v.isEmpty() ? Flag() : Flag::f_quote_entities)), @@ -88,7 +100,16 @@ MTPInputReplyTo ReplyToForMTP( : MTPInputPeer()), MTP_string(replyTo.quote.text), quoteEntities, - MTP_int(replyTo.quoteOffset)); + MTP_int(replyTo.quoteOffset), + (replyToMonoforumPeerId + ? history->owner().peer(replyToMonoforumPeerId)->input + : MTPInputPeer())); + } else if (history->peer->amMonoforumAdmin() + && replyTo.monoforumPeerId) { + const auto replyToMonoforumPeer = replyTo.monoforumPeerId + ? history->owner().peer(replyTo.monoforumPeerId) + : history->session().user(); + return MTP_inputReplyToMonoForum(replyToMonoforumPeer->input); } return MTPInputReplyTo(); } @@ -479,10 +500,29 @@ void Histories::changeDialogUnreadMark( using Flag = MTPmessages_MarkDialogUnread::Flag; session().api().request(MTPmessages_MarkDialogUnread( MTP_flags(unread ? Flag::f_unread : Flag(0)), + MTPInputPeer(), // parent_peer MTP_inputDialogPeer(history->peer->input) )).send(); } +void Histories::changeSublistUnreadMark( + not_null sublist, + bool unread) { + const auto parent = sublist->parentChat(); + if (!parent) { + return; + } + sublist->setUnreadMark(unread); + + using Flag = MTPmessages_MarkDialogUnread::Flag; + session().api().request(MTPmessages_MarkDialogUnread( + MTP_flags(Flag::f_parent_peer + | (unread ? Flag::f_unread : Flag(0))), + parent->input, + MTP_inputDialogPeer(sublist->sublistPeer()->input) + )).send(); +} + void Histories::requestFakeChatListMessage( not_null history) { if (_fakeChatListRequests.contains(history)) { @@ -687,6 +727,7 @@ void Histories::sendReadRequest(not_null history, State &state) { } else { Assert(!state->sentReadTill || state->sentReadTill > tillId); } + history->validateMonoforumUnread(tillId); sendReadRequests(); finish(); }; @@ -788,7 +829,7 @@ void Histories::deleteAllMessages( channel->inputChannel, MTP_int(deleteTillId) )).done(finish).fail(finish).send(); - } else if (revoke && chat && chat->amCreator()) { + } else if (!justClear && revoke && chat && chat->amCreator()) { return session().api().request(MTPmessages_DeleteChat( chat->inputChat )).done(finish).fail([=](const MTP::Error &error) { @@ -1070,13 +1111,12 @@ int Histories::sendPreparedMessage( _creatingTopicRequests.emplace(id); return id; } - const auto realReplyTo = FullReplyTo{ - .messageId = convertTopicReplyToId(history, replyTo.messageId), - .quote = replyTo.quote, - .storyId = replyTo.storyId, - .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), - .quoteOffset = replyTo.quoteOffset, + auto realReplyTo = replyTo; + const auto topicReplyToId = [&](const auto &id) { + return convertTopicReplyToId(history, id); }; + realReplyTo.messageId = topicReplyToId(replyTo.messageId); + realReplyTo.topicRootId = topicReplyToId(replyTo.topicRootId); return v::match(message(history, realReplyTo), [&](const auto &request) { const auto type = RequestType::Send; return sendRequest(history, type, [=](Fn finish) { diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h index eb8645c5a6..fd6ee38108 100644 --- a/Telegram/SourceFiles/data/data_histories.h +++ b/Telegram/SourceFiles/data/data_histories.h @@ -26,6 +26,7 @@ namespace Data { class Session; class Folder; struct WebPageDraft; +class SavedSublist; [[nodiscard]] MTPInputReplyTo ReplyToForMTP( not_null history, @@ -71,6 +72,9 @@ public: Fn callback = nullptr); void dialogEntryApplied(not_null history); void changeDialogUnreadMark(not_null history, bool unread); + void changeSublistUnreadMark( + not_null sublist, + bool unread); void requestFakeChatListMessage(not_null history); void requestGroupAround(not_null item); diff --git a/Telegram/SourceFiles/data/data_history_messages.cpp b/Telegram/SourceFiles/data/data_history_messages.cpp index 5d5dffa30b..6711059297 100644 --- a/Telegram/SourceFiles/data/data_history_messages.cpp +++ b/Telegram/SourceFiles/data/data_history_messages.cpp @@ -152,6 +152,7 @@ rpl::producer HistoryMergedViewer( auto createSimpleViewer = [=]( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter) { @@ -161,11 +162,10 @@ rpl::producer HistoryMergedViewer( return HistoryViewer(chosen, simpleKey, limitBefore, limitAfter); }; const auto peerId = history->peer->id; - const auto topicRootId = MsgId(); const auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0); using Key = SparseIdsMergedSlice::Key; return SparseIdsMergedSlice::CreateViewer( - Key(peerId, topicRootId, migratedPeerId, universalAroundId), + Key(peerId, MsgId(), PeerId(), migratedPeerId, universalAroundId), limitBefore, limitAfter, std::move(createSimpleViewer)); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 2686a39af9..30b3929be7 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/send_credits_box.h" // CreditsEmoji. #include "history/history.h" #include "history/history_item.h" // CreateMedia. +#include "history/history_item_components.h" #include "history/history_location_manager.h" #include "history/view/history_view_element.h" #include "history/view/history_view_item_preview.h" @@ -29,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_web_page.h" #include "history/view/media/history_view_poll.h" #include "history/view/media/history_view_theme_document.h" +#include "history/view/media/history_view_todo_list.h" #include "history/view/media/history_view_slot_machine.h" #include "history/view/media/history_view_dice.h" #include "history/view/media/history_view_service_box.h" @@ -65,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_stories.h" #include "data/data_story.h" +#include "data/data_todo_list.h" #include "data/data_user.h" #include "main/main_session.h" #include "main/main_session_settings.h" @@ -645,6 +648,10 @@ PollData *Media::poll() const { return nullptr; } +TodoListData *Media::todolist() const { + return nullptr; +} + const WallPaper *Media::paper() const { return nullptr; } @@ -922,9 +929,9 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const { const auto type = tr::lng_in_dlg_photo(tr::now); const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); + : Dialogs::Ui::DialogsPreviewText(options.translated + ? parent()->translatedText() + : parent()->originalText()); const auto hasMiniImages = !images.empty(); return { .text = WithCaptionNotificationText(type, caption, hasMiniImages), @@ -1194,9 +1201,9 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const { }(); const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); + : Dialogs::Ui::DialogsPreviewText(options.translated + ? parent()->translatedText() + : parent()->originalText()); const auto hasMiniImages = !images.empty(); return { .text = WithCaptionNotificationText(type, caption, hasMiniImages), @@ -2199,9 +2206,9 @@ ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const { const auto type = ComputeAlbumCountsString(counts); const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); + : Dialogs::Ui::DialogsPreviewText(options.translated + ? parent()->translatedText() + : parent()->originalText()); const auto hasMiniImages = !images.empty(); auto nice = Ui::Text::Colorized( Ui::CreditsEmojiSmall(&parent()->history()->session())); @@ -2315,6 +2322,74 @@ std::unique_ptr MediaPoll::createView( return std::make_unique(message, _poll); } +MediaTodoList::MediaTodoList( + not_null parent, + not_null todolist) +: Media(parent) +, _todolist(todolist) { +} + +MediaTodoList::~MediaTodoList() { +} + +std::unique_ptr MediaTodoList::clone(not_null parent) { + const auto id = parent->fullId(); + return std::make_unique( + parent, + parent->history()->owner().duplicateTodoList(id, _todolist)); +} + +TodoListData *MediaTodoList::todolist() const { + return _todolist; +} + +TextWithEntities MediaTodoList::notificationText() const { + return TextWithEntities() + .append(QChar(0x2611)) + .append(QChar(' ')) + .append(Ui::Text::Colorized(_todolist->title)); +} + +QString MediaTodoList::pinnedTextSubstring() const { + return QChar(171) + _todolist->title.text + QChar(187); +} + +TextForMimeData MediaTodoList::clipboardText() const { + auto result = TextWithEntities(); + result + .append(u"[ "_q) + .append(tr::lng_in_dlg_todo_list(tr::now)) + .append(u" : "_q) + .append(_todolist->title) + .append(u" ]"_q); + for (const auto &item : _todolist->items) { + result.append(u"\n- "_q).append(item.text); + } + return TextForMimeData::Rich(std::move(result)); +} + +bool MediaTodoList::allowsEdit() const { + return parent()->out(); +} + +bool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) { + return false; +} + +bool MediaTodoList::updateSentMedia(const MTPMessageMedia &media) { + return false; +} + +std::unique_ptr MediaTodoList::createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing) { + return std::make_unique( + message, + _todolist, + replacing); +} + MediaDice::MediaDice(not_null parent, QString emoji, int value) : Media(parent) , _emoji(emoji) @@ -2440,7 +2515,7 @@ MediaGiftBox::MediaGiftBox( not_null parent, not_null from, GiftType type, - int count) + int64 count) : MediaGiftBox(parent, from, GiftCode{ .count = count, .type = type }) { } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index a8a0a34ac9..dab3e57b2e 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -136,6 +136,7 @@ struct GiveawayResults { enum class GiftType : uchar { Premium, // count - months Credits, // count - credits + Ton, // count - nano tons StarGift, // count - stars }; @@ -155,7 +156,7 @@ struct GiftCode { int starsUpgradedBySender = 0; int limitedCount = 0; int limitedLeft = 0; - int count = 0; + int64 count = 0; GiftType type = GiftType::Premium; bool viaGiveaway : 1 = false; bool transferred : 1 = false; @@ -196,6 +197,7 @@ public: virtual const GiftCode *gift() const; virtual CloudImage *location() const; virtual PollData *poll() const; + virtual TodoListData *todolist() const; virtual const WallPaper *paper() const; virtual bool paperForBoth() const; virtual FullStoryId storyId() const; @@ -610,6 +612,34 @@ private: }; +class MediaTodoList final : public Media { +public: + MediaTodoList( + not_null parent, + not_null todolist); + ~MediaTodoList(); + + std::unique_ptr clone(not_null parent) override; + + TodoListData *todolist() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + bool allowsEdit() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr createView( + not_null message, + not_null realParent, + HistoryView::Element *replacing = nullptr) override; + +private: + not_null _todolist; + +}; + class MediaDice final : public Media { public: MediaDice(not_null parent, QString emoji, int value); @@ -649,7 +679,7 @@ public: not_null parent, not_null from, GiftType type, - int count); + int64 count); MediaGiftBox( not_null parent, not_null from, diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 8d905461cd..1d04260798 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -1033,7 +1033,7 @@ void Reactions::requestMyTags(SavedSublist *sublist) { using Flag = MTPmessages_GetSavedReactionTags::Flag; my.requestId = api.request(MTPmessages_GetSavedReactionTags( MTP_flags(sublist ? Flag::f_peer : Flag()), - (sublist ? sublist->peer()->input : MTP_inputPeerEmpty()), + (sublist ? sublist->sublistPeer()->input : MTP_inputPeerEmpty()), MTP_long(my.hash) )).done([=](const MTPmessages_SavedReactionTags &result) { auto &my = _myTags[sublist]; @@ -2255,7 +2255,7 @@ void MessageReactions::scheduleSendPaid( _paid->scheduledPrivacySet = true; } if (count > 0) { - _item->history()->session().credits().lock(StarsAmount(count)); + _item->history()->session().credits().lock(CreditsAmount(count)); } _item->history()->owner().reactions().schedulePaid(_item); } @@ -2269,7 +2269,7 @@ void MessageReactions::cancelScheduledPaid() { if (_paid->scheduledFlag) { if (const auto amount = int(_paid->scheduled)) { _item->history()->session().credits().unlock( - StarsAmount(amount)); + CreditsAmount(amount)); } _paid->scheduled = 0; _paid->scheduledFlag = 0; @@ -2332,9 +2332,9 @@ void MessageReactions::finishPaidSending( if (const auto amount = send.count) { const auto credits = &_item->history()->session().credits(); if (success) { - credits->withdrawLocked(StarsAmount(amount)); + credits->withdrawLocked(CreditsAmount(amount)); } else { - credits->unlock(StarsAmount(amount)); + credits->unlock(CreditsAmount(amount)); } } } diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 48c57091b1..bee8324194 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -177,18 +177,45 @@ struct FullReplyTo { TextWithEntities quote; FullStoryId storyId; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; int quoteOffset = 0; - [[nodiscard]] bool valid() const { + [[nodiscard]] bool replying() const { return messageId || (storyId && storyId.peer); } explicit operator bool() const { - return valid(); + return replying() || monoforumPeerId; } friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; friend inline bool operator==(FullReplyTo, FullReplyTo) = default; }; +struct SuggestPostOptions { + uint32 exists : 1 = 0; + uint32 priceWhole : 31 = 0; + uint32 priceNano : 31 = 0; + uint32 ton : 1 = 0; + TimeId date = 0; + + [[nodiscard]] CreditsAmount price() const { + return CreditsAmount( + priceWhole, + priceNano, + ton ? CreditsType::Ton : CreditsType::Stars); + } + + explicit operator bool() const { + return exists != 0; + } + + friend inline auto operator<=>( + SuggestPostOptions, + SuggestPostOptions) = default; + friend inline bool operator==( + SuggestPostOptions, + SuggestPostOptions) = default; +}; + struct GlobalMsgId { FullMsgId itemId; uint64 sessionUniqueId = 0; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index a8a59dbbed..56f6e0e497 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -397,19 +397,31 @@ QImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const { void PeerData::paintUserpic( Painter &p, Ui::PeerUserpicView &view, - int x, - int y, - int size, - bool forceCircle) const { + PaintUserpicContext context) const { + if (const auto broadcast = monoforumBroadcast()) { + if (context.shape == Ui::PeerUserpicShape::Auto) { + context.shape = Ui::PeerUserpicShape::Monoforum; + } + broadcast->paintUserpic(p, view, context); + return; + } + const auto size = context.size; const auto cloud = userpicCloudImage(view); const auto ratio = style::DevicePixelRatio(); + if (context.shape == Ui::PeerUserpicShape::Auto) { + context.shape = isForum() + ? Ui::PeerUserpicShape::Forum + : isMonoforum() + ? Ui::PeerUserpicShape::Monoforum + : Ui::PeerUserpicShape::Circle; + } Ui::ValidateUserpicCache( view, cloud, cloud ? nullptr : ensureEmptyUserpic().get(), size * ratio, - !forceCircle && isForum()); - p.drawImage(QRect(x, y, size, size), view.cached); + context.shape); + p.drawImage(QRect(context.position, QSize(size, size)), view.cached); } void PeerData::loadUserpic() { @@ -631,14 +643,25 @@ bool PeerData::canPinMessages() const { bool PeerData::canCreatePolls() const { if (const auto user = asUser()) { - return user->isBot() - && !user->isSupport() - && !user->isRepliesChat() - && !user->isVerifyCodes(); + return user->isSelf() + || (user->isBot() + && !user->isSupport() + && !user->isRepliesChat() + && !user->isVerifyCodes()); + } else if (isMonoforum()) { + return false; } return Data::CanSend(this, ChatRestriction::SendPolls); } +bool PeerData::canCreateTodoLists() const { + if (isMonoforum()) { + return false; + } + return session().premium() + && (Data::CanSend(this, ChatRestriction::SendPolls) || isUser()); +} + bool PeerData::canCreateTopics() const { if (const auto channel = asChannel()) { return channel->isForum() @@ -1088,6 +1111,16 @@ const ChannelData *PeerData::asChannelOrMigrated() const { return migrateTo(); } +ChannelData *PeerData::asMonoforum() { + const auto channel = asMegagroup(); + return (channel && channel->isMonoforum()) ? channel : nullptr; +} + +const ChannelData *PeerData::asMonoforum() const { + const auto channel = asMegagroup(); + return (channel && channel->isMonoforum()) ? channel : nullptr; +} + ChatData *PeerData::migrateFrom() const { if (const auto megagroup = asMegagroup()) { return megagroup->amIn() @@ -1120,6 +1153,35 @@ not_null PeerData::migrateToOrMe() const { return this; } +not_null PeerData::userpicPaintingPeer() { + if (const auto broadcast = monoforumBroadcast()) { + return broadcast; + } + return this; +} + +not_null PeerData::userpicPaintingPeer() const { + return const_cast(this)->userpicPaintingPeer(); +} + +Ui::PeerUserpicShape PeerData::userpicShape() const { + return isForum() + ? Ui::PeerUserpicShape::Forum + : isMonoforum() + ? Ui::PeerUserpicShape::Monoforum + : Ui::PeerUserpicShape::Circle; +} + +ChannelData *PeerData::monoforumBroadcast() const { + const auto monoforum = asMonoforum(); + return monoforum ? monoforum->monoforumLink() : nullptr; +} + +ChannelData *PeerData::broadcastMonoforum() const { + const auto broadcast = asBroadcast(); + return broadcast ? broadcast->monoforumLink() : nullptr; +} + const QString &PeerData::topBarNameText() const { if (const auto to = migrateTo()) { return to->topBarNameText(); @@ -1138,6 +1200,8 @@ int PeerData::nameVersion() const { const QString &PeerData::name() const { if (const auto to = migrateTo()) { return to->name(); + } else if (const auto broadcast = monoforumBroadcast()) { + return broadcast->name(); } return _name; } @@ -1145,6 +1209,10 @@ const QString &PeerData::name() const { const QString &PeerData::shortName() const { if (const auto user = asUser()) { return user->firstName.isEmpty() ? user->lastName : user->firstName; + } else if (const auto to = migrateTo()) { + return to->shortName(); + } else if (const auto broadcast = monoforumBroadcast()) { + return broadcast->shortName(); } return _name; } @@ -1303,6 +1371,13 @@ bool PeerData::isForum() const { return false; } +bool PeerData::isMonoforum() const { + if (const auto channel = asChannel()) { + return channel->isMonoforum(); + } + return false; +} + bool PeerData::isGigagroup() const { if (const auto channel = asChannel()) { return channel->isGigagroup(); @@ -1386,6 +1461,23 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const { return nullptr; } +Data::SavedMessages *PeerData::monoforum() const { + if (const auto channel = asChannel()) { + return channel->monoforum(); + } + return nullptr; +} + +Data::SavedSublist *PeerData::monoforumSublistFor( + PeerId sublistPeerId) const { + if (!sublistPeerId) { + return nullptr; + } else if (const auto monoforum = this->monoforum()) { + return monoforum->sublistLoaded(owner().peer(sublistPeerId)); + } + return nullptr; +} + bool PeerData::isAyuNoForwards() const { if (asUser()) { return false; @@ -1439,6 +1531,9 @@ Data::RestrictionCheckResult PeerData::amRestricted( : Result::Explicit()) : Result::Allowed(); } else if (const auto channel = asChannel()) { + if (channel->monoforumDisabled()) { + return Result::WithEveryone(); + } const auto defaultRestrictions = channel->defaultRestrictions() | (channel->isPublic() ? (ChatRestriction::PinMessages @@ -1483,7 +1578,8 @@ bool PeerData::canRevokeFullHistory() const { } else if (const auto megagroup = asMegagroup()) { return megagroup->amCreator() && megagroup->membersCountKnown() - && megagroup->canDelete(); + && megagroup->canDelete() + && !megagroup->isMonoforum(); } return false; } @@ -1539,12 +1635,22 @@ bool PeerData::canManageGroupCall() const { return chat->amCreator() || (chat->adminRights() & ChatAdminRight::ManageCall); } else if (const auto group = asChannel()) { + if (group->isMonoforum()) { + return false; + } return group->amCreator() || (group->adminRights() & ChatAdminRight::ManageCall); } return false; } +bool PeerData::amMonoforumAdmin() const { + if (const auto channel = asChannel()) { + return channel->flags() & ChannelDataFlag::MonoforumAdmin; + } + return false; +} + int PeerData::starsPerMessage() const { if (const auto user = asUser()) { return user->starsPerMessage(); @@ -1556,9 +1662,11 @@ int PeerData::starsPerMessage() const { int PeerData::starsPerMessageChecked() const { if (const auto channel = asChannel()) { - return (channel->adminRights() || channel->amCreator()) - ? 0 - : channel->starsPerMessage(); + if (channel->adminRights() + || channel->amCreator() + || amMonoforumAdmin()) { + return 0; + } } return starsPerMessage(); } @@ -1719,12 +1827,14 @@ void SetTopPinnedMessageId( session.settings().setHiddenPinnedMessageId( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId 0); session.saveSettingsDelayed(); } session.storage().add(Storage::SharedMediaAddExisting( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, messageId, { messageId, ServerMaxMsgId })); @@ -1734,22 +1844,25 @@ void SetTopPinnedMessageId( FullMsgId ResolveTopPinnedId( not_null peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated) { const auto slice = peer->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( peer->id, topicRootId, + monoforumPeerId, Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, 1)); - const auto old = (!topicRootId && migrated) + const auto old = (!topicRootId && !monoforumPeerId && migrated) ? migrated->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( migrated->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, @@ -1771,22 +1884,25 @@ FullMsgId ResolveTopPinnedId( FullMsgId ResolveMinPinnedId( not_null peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated) { const auto slice = peer->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( peer->id, topicRootId, + monoforumPeerId, Storage::SharedMediaType::Pinned, 1), 1, 1)); - const auto old = (!topicRootId && migrated) + const auto old = (!topicRootId && !monoforumPeerId && migrated) ? migrated->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( migrated->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, 1), 1, diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 5615c378a6..67113ffce9 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -37,6 +37,8 @@ class Forum; class ForumTopic; class Session; class GroupCall; +class SavedMessages; +class SavedSublist; struct ReactionId; class WallPaper; @@ -184,6 +186,12 @@ struct PeerBarDetails { int paysPerMessage = 0; }; +struct PaintUserpicContext { + QPoint position; + int size = 0; + Ui::PeerUserpicShape shape = Ui::PeerUserpicShape::Auto; +}; + class PeerData { protected: PeerData(not_null owner, PeerId id); @@ -232,6 +240,7 @@ public: [[nodiscard]] bool isMegagroup() const; [[nodiscard]] bool isBroadcast() const; [[nodiscard]] bool isForum() const; + [[nodiscard]] bool isMonoforum() const; [[nodiscard]] bool isGigagroup() const; [[nodiscard]] bool isRepliesChat() const; [[nodiscard]] bool isVerifyCodes() const; @@ -257,6 +266,10 @@ public: [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] Data::ForumTopic *forumTopicFor(MsgId rootId) const; + [[nodiscard]] Data::SavedMessages *monoforum() const; + [[nodiscard]] Data::SavedSublist *monoforumSublistFor( + PeerId sublistPeerId) const; + [[nodiscard]] Data::PeerNotifySettings ¬ify() { return _notify; } @@ -274,6 +287,7 @@ public: [[nodiscard]] rpl::producer slowmodeAppliedValue() const; [[nodiscard]] int slowmodeSecondsLeft() const; [[nodiscard]] bool canManageGroupCall() const; + [[nodiscard]] bool amMonoforumAdmin() const; [[nodiscard]] int starsPerMessage() const; [[nodiscard]] int starsPerMessageChecked() const; @@ -294,11 +308,22 @@ public: [[nodiscard]] const ChatData *asChatNotMigrated() const; [[nodiscard]] ChannelData *asChannelOrMigrated(); [[nodiscard]] const ChannelData *asChannelOrMigrated() const; + [[nodiscard]] ChannelData *asMonoforum(); + [[nodiscard]] const ChannelData *asMonoforum() const; [[nodiscard]] ChatData *migrateFrom() const; [[nodiscard]] ChannelData *migrateTo() const; [[nodiscard]] not_null migrateToOrMe(); [[nodiscard]] not_null migrateToOrMe() const; + [[nodiscard]] not_null userpicPaintingPeer(); + [[nodiscard]] not_null userpicPaintingPeer() const; + [[nodiscard]] Ui::PeerUserpicShape userpicShape() const; + + // isMonoforum() ? monoforumLink() : nullptr + [[nodiscard]] ChannelData *monoforumBroadcast() const; + + // isMonoforum() ? nullptr : monoforumLink() + [[nodiscard]] ChannelData *broadcastMonoforum() const; void updateFull(); void updateFullForced(); @@ -329,13 +354,26 @@ public: const ImageLocation &location, bool hasVideo); void setUserpicPhoto(const MTPPhoto &data); + void paintUserpic( Painter &p, Ui::PeerUserpicView &view, - int x, - int y, - int size, - bool forceCircle = false) const; + PaintUserpicContext context) const; + void paintUserpic( + Painter &p, + Ui::PeerUserpicView &view, + int x, + int y, + int size, + bool forceCircle = false) const { + paintUserpic(p, view, { + .position = { x, y }, + .size = size, + .shape = (forceCircle + ? Ui::PeerUserpicShape::Circle + : Ui::PeerUserpicShape::Auto), + }); + } void paintUserpicLeft( Painter &p, Ui::PeerUserpicView &view, @@ -392,6 +430,7 @@ public: [[nodiscard]] bool canPinMessages() const; [[nodiscard]] bool canEditMessagesIndefinitely() const; [[nodiscard]] bool canCreatePolls() const; + [[nodiscard]] bool canCreateTodoLists() const; [[nodiscard]] bool canCreateTopics() const; [[nodiscard]] bool canManageTopics() const; [[nodiscard]] bool canManageGifts() const; @@ -585,10 +624,12 @@ void SetTopPinnedMessageId( [[nodiscard]] FullMsgId ResolveTopPinnedId( not_null peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated = nullptr); [[nodiscard]] FullMsgId ResolveMinPinnedId( not_null peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated = nullptr); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 717a00bbc6..d418191fbb 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -273,10 +273,12 @@ inline auto DefaultRestrictionValue( | Flag::Left | Flag::Forum | Flag::JoinToWrite + | Flag::Monoforum | Flag::HasLink | Flag::Forbidden | Flag::Creator - | Flag::Broadcast; + | Flag::Broadcast + | Flag::MonoforumDisabled; return rpl::combine( PeerFlagsValue(channel, mask), AdminRightValue( @@ -291,12 +293,16 @@ inline auto DefaultRestrictionValue( bool unrestrictedByBoosts, ChatRestrictions sendRestriction, ChatRestrictions defaultSendRestriction) { + if (flags & Flag::MonoforumDisabled) { + return false; + } const auto notAmInFlags = Flag::Left | Flag::Forbidden; const auto forumRestriction = forbidInForums && (flags & Flag::Forum); const auto allowed = !(flags & notAmInFlags) || ((flags & Flag::HasLink) - && !(flags & Flag::JoinToWrite)); + && !(flags & Flag::JoinToWrite)) + || (flags & Flag::Monoforum); const auto restricted = sendRestriction | (defaultSendRestriction && !unrestrictedByBoosts); return allowed diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 49dd021734..03fadc862c 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -75,7 +75,7 @@ struct PollData { int totalVoters = 0; int version = 0; - static constexpr auto kMaxOptions = 10; + static constexpr auto kMaxOptions = 32; private: bool applyResultToAnswers( diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h index 42f56c1aec..f5d32ddcfb 100644 --- a/Telegram/SourceFiles/data/data_replies_list.h +++ b/Telegram/SourceFiles/data/data_replies_list.h @@ -58,8 +58,6 @@ public: [[nodiscard]] bool isServerSideUnread( not_null item) const; - [[nodiscard]] std::optional computeUnreadCountLocally( - MsgId afterId) const; void requestUnreadCount(); void readTill(not_null item); @@ -79,6 +77,8 @@ private: void subscribeToUpdates(); void appendClientSideMessages(MessagesSlice &slice); + [[nodiscard]] std::optional computeUnreadCountLocally( + MsgId afterId) const; [[nodiscard]] bool buildFromData(not_null viewer); [[nodiscard]] bool applyItemDestroyed( diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 9a4f4441f9..1cae8972db 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -8,12 +8,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_messages.h" #include "apiwrap.h" -#include "data/data_peer.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_histories.h" +#include "data/data_user.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_unread_things.h" #include "main/main_session.h" +#include "storage/storage_facade.h" +#include "storage/storage_shared_media.h" +#include "window/notifications_manager.h" namespace Data { namespace { @@ -22,24 +30,107 @@ constexpr auto kPerPage = 50; constexpr auto kFirstPerPage = 10; constexpr auto kListPerPage = 100; constexpr auto kListFirstPerPage = 20; +constexpr auto kLoadedSublistsMinCount = 20; +constexpr auto kShowSublistNamesCount = 5; +constexpr auto kStalePerRequest = 100; } // namespace -SavedMessages::SavedMessages(not_null owner) +SavedMessages::SavedMessages( + not_null owner, + ChannelData *parentChat) : _owner(owner) +, _parentChat(parentChat) +, _owningHistory(parentChat ? owner->history(parentChat).get() : nullptr) , _chatsList( - &owner->session(), + &_owner->session(), FilterId(), - owner->maxPinnedChatsLimitValue(this)) + _owner->maxPinnedChatsLimitValue(this)) , _loadMore([=] { sendLoadMoreRequests(); }) { + // We don't assign _owningHistory for my Saved Messages here, + // because the data structures are not ready yet. + if (_owningHistory && _owningHistory->inChatList()) { + preloadSublists(); + } } -SavedMessages::~SavedMessages() = default; +void SavedMessages::clear() { + for (const auto &request : base::take(_sublistRequests)) { + if (request.second.id != _staleRequestId) { + owner().histories().cancelRequest(request.second.id); + } + } + if (const auto requestId = base::take(_staleRequestId)) { + session().api().request(requestId).cancel(); + } + + auto &storage = session().storage(); + auto &changes = session().changes(); + if (_owningHistory) { + for (const auto &[peer, sublist] : base::take(_sublists)) { + storage.unload(Storage::SharedMediaUnloadThread( + _owningHistory->peer->id, + MsgId(), + peer->id)); + _owningHistory->setForwardDraft(MsgId(), peer->id, {}); + + const auto raw = sublist.get(); + changes.sublistRemoved(raw); + changes.entryRemoved(raw); + } + } + _owningHistory = nullptr; +} + +void SavedMessages::saveActiveSubsectionThread(not_null thread) { + if (const auto sublist = thread->asSublist()) { + Assert(sublist->parent() == this); + _activeSubsectionSublist = sublist; + } else { + Assert(thread == _owningHistory); + _activeSubsectionSublist = nullptr; + } +} + +Thread *SavedMessages::activeSubsectionThread() const { + return _activeSubsectionSublist; +} + +Dialogs::UnreadState SavedMessages::unreadStateWithParentMuted() const { + auto result = _chatsList.unreadState(); + if (_owningHistory->muted()) { + result.chatsMuted = result.chats; + result.marksMuted = result.marks; + result.messagesMuted = result.messages; + result.reactionsMuted = result.reactions; + } + return result; +} + +SavedMessages::~SavedMessages() { + clear(); +} bool SavedMessages::supported() const { return !_unsupported; } +void SavedMessages::markUnsupported() { + _unsupported = true; +} + +ChannelData *SavedMessages::parentChat() const { + return _parentChat; +} + +not_null SavedMessages::owningHistory() const { + if (!_owningHistory) { + const_cast(this)->_owningHistory + = _owner->history(_owner->session().user()); + } + return _owningHistory; +} + Session &SavedMessages::owner() const { return *_owner; } @@ -53,13 +144,116 @@ not_null SavedMessages::chatsList() { } not_null SavedMessages::sublist(not_null peer) { - const auto i = _sublists.find(peer); - if (i != end(_sublists)) { - return i->second.get(); + if (const auto loaded = sublistLoaded(peer)) { + return loaded; } return _sublists.emplace( peer, - std::make_unique(peer)).first->second.get(); + std::make_unique(this, peer)).first->second.get(); +} + +SavedSublist *SavedMessages::sublistLoaded(not_null peer) { + const auto i = _sublists.find(peer); + return (i != end(_sublists)) ? i->second.get() : nullptr; +} + +void SavedMessages::requestSomeStale() { + if (_staleRequestId + || (!_offset.id && _loadMoreRequestId) + || _stalePeers.empty() + || !_parentChat) { + return; + } + const auto type = Histories::RequestType::History; + auto peers = std::vector>(); + auto peerIds = QVector(); + peers.reserve(std::min(int(_stalePeers.size()), kStalePerRequest)); + peerIds.reserve(std::min(int(_stalePeers.size()), kStalePerRequest)); + for (auto i = begin(_stalePeers); i != end(_stalePeers);) { + const auto peer = *i; + i = _stalePeers.erase(i); + + peers.push_back(peer); + peerIds.push_back(peer->input); + if (peerIds.size() == kStalePerRequest) { + break; + } + } + if (peerIds.empty()) { + return; + } + const auto call = [=] { + for (const auto &peer : peers) { + finishSublistRequest(peer); + } + }; + auto &histories = owner().histories(); + _staleRequestId = histories.sendRequest(_owningHistory, type, [=]( + Fn finish) { + using Flag = MTPmessages_GetSavedDialogsByID::Flag; + return session().api().request( + MTPmessages_GetSavedDialogsByID( + MTP_flags(Flag::f_parent_peer), + _parentChat->input, + MTP_vector(peerIds)) + ).done([=](const MTPmessages_SavedDialogs &result) { + _staleRequestId = 0; + applyReceivedSublists(result); + call(); + finish(); + }).fail([=] { + _staleRequestId = 0; + call(); + finish(); + }).send(); + }); + for (const auto &peer : peers) { + _sublistRequests[peer].id = _staleRequestId; + } +} + +void SavedMessages::finishSublistRequest(not_null peer) { + if (const auto request = _sublistRequests.take(peer)) { + for (const auto &callback : request->callbacks) { + callback(); + } + } +} + +void SavedMessages::requestSublist( + not_null peer, + Fn done) { + if (!_parentChat) { + return; + } + auto &request = _sublistRequests[peer]; + if (done) { + request.callbacks.push_back(std::move(done)); + } + if (!request.id + && _stalePeers.emplace(peer).second + && (_stalePeers.size() == 1)) { + crl::on_main(&session(), [peer = _parentChat] { + if (const auto monoforum = peer->monoforum()) { + monoforum->requestSomeStale(); + } + }); + } +} + +rpl::producer<> SavedMessages::chatsListChanges() const { + return _chatsListChanges.events(); +} + +rpl::producer<> SavedMessages::chatsListLoadedEvents() const { + return _chatsListLoadedEvents.events(); +} + +void SavedMessages::preloadSublists() { + if (parentChat() + && chatsList()->indexed()->size() < kLoadedSublistsMinCount) { + loadMore(); + } } void SavedMessages::loadMore() { @@ -67,9 +261,10 @@ void SavedMessages::loadMore() { _loadMore.call(); } -void SavedMessages::loadMore(not_null sublist) { - _loadMoreSublistsScheduled.emplace(sublist); - _loadMore.call(); +void SavedMessages::clearAllUnreadReactions() { + for (const auto &[peer, sublist] : _sublists) { + sublist->unreadReactions().clear(); + } } void SavedMessages::sendLoadMore() { @@ -78,19 +273,37 @@ void SavedMessages::sendLoadMore() { } else if (!_pinnedLoaded) { loadPinned(); } + using Flag = MTPmessages_GetSavedDialogs::Flag; _loadMoreRequestId = _owner->session().api().request( MTPmessages_GetSavedDialogs( - MTP_flags(MTPmessages_GetSavedDialogs::Flag::f_exclude_pinned), - MTP_int(_offsetDate), - MTP_int(_offsetId), - _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), - MTP_int(_offsetId ? kListPerPage : kListFirstPerPage), + MTP_flags(Flag::f_exclude_pinned + | (_parentChat ? Flag::f_parent_peer : Flag(0))), + _parentChat ? _parentChat->input : MTPInputPeer(), + MTP_int(_offset.date), + MTP_int(_offset.id), + _offset.peer ? _offset.peer->input : MTP_inputPeerEmpty(), + MTP_int(_offset.id ? kListPerPage : kListFirstPerPage), MTP_long(0)) // hash ).done([=](const MTPmessages_SavedDialogs &result) { - apply(result, false); + const auto applied = applyReceivedSublists(result); + if (applied.allLoaded || _offset == applied.offset) { + _chatsList.setLoaded(); + } else if (_offset.date > 0 && applied.offset.date > _offset.date) { + LOG(("API Error: Bad order in messages.savedDialogs.")); + _chatsList.setLoaded(); + } else { + _offset = applied.offset; + } + _loadMoreRequestId = 0; + _chatsListChanges.fire({}); + if (_chatsList.loaded()) { + _chatsListLoadedEvents.fire({}); + } + reorderLastSublists(); + requestSomeStale(); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; + markUnsupported(); } _chatsList.setLoaded(); _loadMoreRequestId = 0; @@ -98,16 +311,19 @@ void SavedMessages::sendLoadMore() { } void SavedMessages::loadPinned() { - if (_pinnedRequestId) { + if (_pinnedRequestId || parentChat()) { return; } _pinnedRequestId = _owner->session().api().request( MTPmessages_GetPinnedSavedDialogs() ).done([=](const MTPmessages_SavedDialogs &result) { - apply(result, true); + _pinnedRequestId = 0; + _pinnedLoaded = true; + applyReceivedSublists(result, true); + _chatsListChanges.fire({}); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; + markUnsupported(); } else { _pinnedLoaded = true; } @@ -115,77 +331,11 @@ void SavedMessages::loadPinned() { }).send(); } -void SavedMessages::sendLoadMore(not_null sublist) { - if (_loadMoreRequests.contains(sublist) || sublist->isFullLoaded()) { - return; - } - const auto &list = sublist->messages(); - const auto offsetId = list.empty() ? MsgId(0) : list.back()->id; - const auto offsetDate = list.empty() ? MsgId(0) : list.back()->date(); - const auto limit = offsetId ? kPerPage : kFirstPerPage; - const auto requestId = _owner->session().api().request( - MTPmessages_GetSavedHistory( - sublist->peer()->input, - MTP_int(offsetId), - MTP_int(offsetDate), - MTP_int(0), // add_offset - MTP_int(limit), - MTP_int(0), // max_id - MTP_int(0), // min_id - MTP_long(0)) // hash - ).done([=](const MTPmessages_Messages &result) { - auto count = 0; - auto list = (const QVector*)nullptr; - result.match([](const MTPDmessages_channelMessages &) { - LOG(("API Error: messages.channelMessages in sublist.")); - }, [](const MTPDmessages_messagesNotModified &) { - LOG(("API Error: messages.messagesNotModified in sublist.")); - }, [&](const auto &data) { - owner().processUsers(data.vusers()); - owner().processChats(data.vchats()); - list = &data.vmessages().v; - if constexpr (MTPDmessages_messages::Is()) { - count = int(list->size()); - } else { - count = data.vcount().v; - } - }); - - _loadMoreRequests.remove(sublist); - if (!list) { - sublist->setFullLoaded(); - return; - } - auto items = std::vector>(); - items.reserve(list->size()); - for (const auto &message : *list) { - const auto item = owner().addNewMessage( - message, - {}, - NewMessageType::Existing); - if (item) { - items.push_back(item); - } - } - sublist->append(std::move(items), count); - if (result.type() == mtpc_messages_messages) { - sublist->setFullLoaded(); - } - }).fail([=](const MTP::Error &error) { - if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; - } - sublist->setFullLoaded(); - _loadMoreRequests.remove(sublist); - }).send(); - _loadMoreRequests[sublist] = requestId; -} - -void SavedMessages::apply( - const MTPmessages_SavedDialogs &result, +SavedMessages::ApplyResult SavedMessages::applyReceivedSublists( + const MTPmessages_SavedDialogs &dialogs, bool pinned) { auto list = (const QVector*)nullptr; - result.match([](const MTPDmessages_savedDialogsNotModified &) { + dialogs.match([](const MTPDmessages_savedDialogsNotModified &) { LOG(("API Error: messages.savedDialogsNotModified.")); }, [&](const auto &data) { _owner->processUsers(data.vusers()); @@ -195,69 +345,66 @@ void SavedMessages::apply( NewMessageType::Existing); list = &data.vdialogs().v; }); - if (pinned) { - _pinnedRequestId = 0; - _pinnedLoaded = true; - } else { - _loadMoreRequestId = 0; - } if (!list) { - if (!pinned) { - _chatsList.setLoaded(); - } - return; + return { .allLoaded = true }; } auto lastValid = false; - auto offsetDate = TimeId(); - auto offsetId = MsgId(); - auto offsetPeer = (PeerData*)nullptr; - const auto selfId = _owner->session().userPeerId(); + auto result = ApplyResult(); + const auto parentPeerId = _parentChat + ? _parentChat->id + : _owner->session().userPeerId(); for (const auto &dialog : *list) { - const auto &data = dialog.data(); - const auto peer = _owner->peer(peerFromMTP(data.vpeer())); - const auto topId = MsgId(data.vtop_message().v); - if (const auto item = _owner->message(selfId, topId)) { - offsetPeer = peer; - offsetDate = item->date(); - offsetId = topId; - lastValid = true; - const auto entry = sublist(peer); - const auto entryPinned = pinned || data.is_pinned(); - entry->applyMaybeLast(item); - _owner->setPinnedFromEntryList(entry, entryPinned); - } else { - lastValid = false; - } + dialog.match([&](const MTPDsavedDialog &data) { + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + const auto topId = MsgId(data.vtop_message().v); + if (const auto item = _owner->message(parentPeerId, topId)) { + result.offset.peer = peer; + result.offset.date = item->date(); + result.offset.id = topId; + lastValid = true; + const auto entry = sublist(peer); + const auto entryPinned = pinned || data.is_pinned(); + entry->applyMaybeLast(item); + _owner->setPinnedFromEntryList(entry, entryPinned); + } else { + lastValid = false; + } + }, [&](const MTPDmonoForumDialog &data) { + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + const auto topId = MsgId(data.vtop_message().v); + if (const auto item = _owner->message(parentPeerId, topId)) { + result.offset.peer = peer; + result.offset.date = item->date(); + result.offset.id = topId; + lastValid = true; + sublist(peer)->applyMonoforumDialog(data, item); + } else { + lastValid = false; + } + }); } if (pinned) { } else if (!lastValid) { LOG(("API Error: Unknown message in the end of a slice.")); - _chatsList.setLoaded(); - } else if (result.type() == mtpc_messages_savedDialogs) { - _chatsList.setLoaded(); - } else if ((_offsetDate > 0 && offsetDate > _offsetDate) - || (offsetDate == _offsetDate - && offsetId == _offsetId - && offsetPeer == _offsetPeer)) { - LOG(("API Error: Bad order in messages.savedDialogs.")); - _chatsList.setLoaded(); - } else { - _offsetDate = offsetDate; - _offsetId = offsetId; - _offsetPeer = offsetPeer; + result.allLoaded = true; + } else if (dialogs.type() == mtpc_messages_savedDialogs) { + result.allLoaded = true; } + if (!_stalePeers.empty()) { + requestSomeStale(); + } + return result; } void SavedMessages::sendLoadMoreRequests() { if (_loadMoreScheduled) { sendLoadMore(); } - for (const auto sublist : base::take(_loadMoreSublistsScheduled)) { - sendLoadMore(sublist); - } } void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { + Expects(!parentChat()); + const auto list = update.vorder(); if (!list) { loadPinned(); @@ -283,6 +430,8 @@ void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { } void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { + Expects(!parentChat()); + update.vpeer().match([&](const MTPDdialogPeer &data) { const auto peer = _owner->peer(peerFromMTP(data.vpeer())); const auto i = _sublists.find(peer); @@ -297,4 +446,146 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { }); } +void SavedMessages::applySublistDeleted(not_null sublistPeer) { + const auto i = _sublists.find(sublistPeer); + if (i == end(_sublists)) { + return; + } + const auto raw = i->second.get(); + Core::App().notifications().clearFromSublist(raw); + owner().removeChatListEntry(raw); + + if (ranges::contains(_lastSublists, not_null(raw))) { + reorderLastSublists(); + } + + _sublistDestroyed.fire(raw); + session().changes().sublistUpdated( + raw, + Data::SublistUpdate::Flag::Destroyed); + session().changes().entryUpdated( + raw, + Data::EntryUpdate::Flag::Destroyed); + _sublists.erase(i); + + const auto history = owningHistory(); + history->destroyMessagesBySublist(sublistPeer); + session().storage().unload(Storage::SharedMediaUnloadThread( + _owningHistory->peer->id, + MsgId(), + sublistPeer->id)); + history->setForwardDraft(MsgId(), sublistPeer->id, {}); +} + +void SavedMessages::reorderLastSublists() { + if (!_parentChat) { + return; + } + + // We want first kShowChatNamesCount histories, by last message date. + const auto pred = []( + not_null a, + not_null b) { + const auto aItem = a->chatListMessage(); + const auto bItem = b->chatListMessage(); + const auto aDate = aItem ? aItem->date() : TimeId(0); + const auto bDate = bItem ? bItem->date() : TimeId(0); + return aDate > bDate; + }; + _lastSublists.clear(); + _lastSublists.reserve(kShowSublistNamesCount + 1); + auto &&sublists = ranges::views::all( + *_chatsList.indexed() + ) | ranges::views::transform([](not_null row) { + return row->sublist(); + }); + auto nonPinnedChecked = 0; + for (const auto sublist : sublists) { + const auto i = ranges::upper_bound( + _lastSublists, + not_null(sublist), + pred); + if (size(_lastSublists) < kShowSublistNamesCount + || i != end(_lastSublists)) { + _lastSublists.insert(i, sublist); + } + if (size(_lastSublists) > kShowSublistNamesCount) { + _lastSublists.pop_back(); + } + if (!sublist->isPinnedDialog(FilterId()) + && ++nonPinnedChecked >= kShowSublistNamesCount) { + break; + } + } + ++_lastSublistsVersion; + owningHistory()->updateChatListEntry(); +} + +void SavedMessages::listMessageChanged(HistoryItem *from, HistoryItem *to) { + if (from || to) { + reorderLastSublists(); + } +} + +int SavedMessages::recentSublistsListVersion() const { + return _lastSublistsVersion; +} + +void SavedMessages::recentSublistsInvalidate( + not_null sublist) { + Expects(_parentChat != nullptr); + + if (ranges::contains(_lastSublists, sublist)) { + ++_lastSublistsVersion; + owningHistory()->updateChatListEntry(); + } +} + +auto SavedMessages::recentSublists() const +-> const std::vector> & { + return _lastSublists; +} + +void SavedMessages::markUnreadCountsUnknown(MsgId readTillId) { + for (const auto &[peer, sublist] : _sublists) { + if (sublist->unreadCountCurrent() > 0) { + sublist->setInboxReadTill(readTillId, std::nullopt); + } + } +} + +void SavedMessages::updateUnreadCounts( + MsgId readTillId, + const base::flat_map, int> &counts) { + for (const auto &[peer, sublist] : _sublists) { + const auto raw = sublist.get(); + const auto i = counts.find(raw); + const auto count = (i != end(counts)) ? i->second : 0; + if (raw->unreadCountCurrent() != count) { + raw->setInboxReadTill(readTillId, count); + } + } +} + +rpl::producer<> SavedMessages::destroyed() const { + if (!_parentChat) { + return rpl::never<>(); + } + return _parentChat->flagsValue( + ) | rpl::filter([=](const ChannelData::Flags::Change &update) { + using Flag = ChannelData::Flag; + return (update.diff & Flag::MonoforumAdmin) + && !(update.value & Flag::MonoforumAdmin); + }) | rpl::take(1) | rpl::to_empty; +} + +auto SavedMessages::sublistDestroyed() const +-> rpl::producer> { + return _sublistDestroyed.events(); +} + +rpl::lifetime &SavedMessages::lifetime() { + return _lifetime; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 3e09f4db0a..65f0345c7b 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_main_list.h" +namespace Dialogs { +struct UnreadState; +} // namespace Dialogs + namespace Main { class Session; } // namespace Main @@ -18,55 +22,132 @@ namespace Data { class Session; class SavedSublist; +struct SavedMessagesOffsets { + TimeId date = 0; + MsgId id = 0; + PeerData *peer = nullptr; + + friend inline constexpr auto operator<=>( + SavedMessagesOffsets, + SavedMessagesOffsets) = default; +}; + class SavedMessages final { public: - explicit SavedMessages(not_null owner); + explicit SavedMessages( + not_null owner, + ChannelData *parentChat = nullptr); ~SavedMessages(); [[nodiscard]] bool supported() const; + void markUnsupported(); + + [[nodiscard]] ChannelData *parentChat() const; + [[nodiscard]] not_null owningHistory() const; [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null chatsList(); [[nodiscard]] not_null sublist(not_null peer); + [[nodiscard]] SavedSublist *sublistLoaded(not_null peer); + void requestSublist(not_null peer, Fn done = nullptr); + [[nodiscard]] rpl::producer<> chatsListChanges() const; + [[nodiscard]] rpl::producer<> chatsListLoadedEvents() const; + + [[nodiscard]] rpl::producer<> destroyed() const; + [[nodiscard]] auto sublistDestroyed() const + -> rpl::producer>; + + void preloadSublists(); void loadMore(); - void loadMore(not_null sublist); + void clearAllUnreadReactions(); void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); + void applySublistDeleted(not_null sublistPeer); + + void listMessageChanged(HistoryItem *from, HistoryItem *to); + [[nodiscard]] int recentSublistsListVersion() const; + void recentSublistsInvalidate(not_null sublist); + [[nodiscard]] auto recentSublists() const + -> const std::vector> &; + + void markUnreadCountsUnknown(MsgId readTillId); + void updateUnreadCounts( + MsgId readTillId, + const base::flat_map, int> &counts); + + void clear(); + + void saveActiveSubsectionThread(not_null thread); + Thread *activeSubsectionThread() const; + + [[nodiscard]] Dialogs::UnreadState unreadStateWithParentMuted() const; + + [[nodiscard]] rpl::lifetime &lifetime(); private: + struct SublistRequest { + mtpRequestId id = 0; + std::vector> callbacks; + }; + struct ApplyResult { + SavedMessagesOffsets offset; + bool allLoaded = false; + }; + void loadPinned(); - void apply(const MTPmessages_SavedDialogs &result, bool pinned); + ApplyResult applyReceivedSublists( + const MTPmessages_SavedDialogs &result, + SavedMessagesOffsets &updateOffsets); + ApplyResult applyReceivedSublists( + const MTPmessages_SavedDialogs &result, + bool pinned = false); + + void reorderLastSublists(); + void requestSomeStale(); + void finishSublistRequest(not_null peer); void sendLoadMore(); - void sendLoadMore(not_null sublist); void sendLoadMoreRequests(); const not_null _owner; + ChannelData *_parentChat = nullptr; + History *_owningHistory = nullptr; + + rpl::event_stream> _sublistDestroyed; Dialogs::MainList _chatsList; base::flat_map< not_null, std::unique_ptr> _sublists; + base::flat_map, SublistRequest> _sublistRequests; + base::flat_set> _stalePeers; + mtpRequestId _staleRequestId = 0; - base::flat_map, mtpRequestId> _loadMoreRequests; mtpRequestId _loadMoreRequestId = 0; mtpRequestId _pinnedRequestId = 0; - TimeId _offsetDate = 0; - MsgId _offsetId = 0; - PeerData *_offsetPeer = nullptr; + SavedMessagesOffsets _offset; SingleQueuedInvokation _loadMore; - base::flat_set> _loadMoreSublistsScheduled; bool _loadMoreScheduled = false; + std::vector> _lastSublists; + int _lastSublistsVersion = 0; + + rpl::event_stream<> _chatsListChanges; + rpl::event_stream<> _chatsListLoadedEvents; + + SavedSublist *_activeSubsectionSublist = nullptr; + bool _pinnedLoaded = false; bool _unsupported = false; + rpl::lifetime _lifetime; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 2d129a5f67..488eab3a1a 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -7,163 +7,820 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_saved_sublist.h" +#include "api/api_unread_things.h" +#include "apiwrap.h" +#include "core/application.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_drafts.h" #include "data/data_histories.h" +#include "data/data_messages.h" #include "data/data_peer.h" #include "data/data_saved_messages.h" #include "data/data_session.h" +#include "data/data_user.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_unread_things.h" +#include "main/main_session.h" +#include "window/notifications_manager.h" namespace Data { +namespace { -SavedSublist::SavedSublist(not_null peer) -: Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist) -, _history(peer->owner().history(peer)) { +constexpr auto kMessagesPerPage = 50; +constexpr auto kReadRequestTimeout = 3 * crl::time(1000); + +} // namespace + +struct SavedSublist::Viewer { + MessagesSlice slice; + MsgId around = 0; + int limitBefore = 0; + int limitAfter = 0; + base::has_weak_ptr guard; + bool scheduled = false; +}; + +SavedSublist::SavedSublist( + not_null parent, + not_null sublistPeer) +: Thread(&sublistPeer->owner(), Dialogs::Entry::Type::SavedSublist) +, _parent(parent) +, _sublistHistory(sublistPeer->owner().history(sublistPeer)) +, _readRequestTimer([=] { sendReadTillRequest(); }) { + if (parent->parentChat()) { + _flags |= Flag::InMonoforum; + } + subscribeToUnreadChanges(); } -SavedSublist::~SavedSublist() = default; - -not_null SavedSublist::history() const { - return _history; +SavedSublist::~SavedSublist() { + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + if (_readRequestTimer.isActive()) { + sendReadTillRequest(); + } + session().api().unreadThings().cancelRequests(this); } -not_null SavedSublist::peer() const { - return _history->peer; +bool SavedSublist::inMonoforum() const { + return (_flags & Flag::InMonoforum) != 0; +} + +void SavedSublist::apply(const SublistReadTillUpdate &update) { + if (update.out) { + setOutboxReadTill(update.readTillId); + } else if (update.readTillId >= _inboxReadTillId) { + setInboxReadTill( + update.readTillId, + computeUnreadCountLocally(update.readTillId)); + } +} + +void SavedSublist::apply(const MessageUpdate &update) { + if (applyUpdate(update)) { + _instantChanges.fire({}); + } +} + +void SavedSublist::applyDifferenceTooLong() { + if (_skippedAfter.has_value()) { + _skippedAfter = std::nullopt; + _listChanges.fire({}); + } +} + +bool SavedSublist::removeOne(not_null item) { + const auto id = item->id; + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + changeUnreadCountByMessage(id, -1); + if (i == end(_list) || *i != id) { + return false; + } + _list.erase(i); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + if (*known > 0) { + _fullCount = (*known - 1); + } + } + return true; +} + +rpl::producer SavedSublist::source( + MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto around = aroundId.fullId.msg; + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto viewer = lifetime.make_state(); + const auto push = [=] { + if (viewer->scheduled) { + viewer->scheduled = false; + if (buildFromData(viewer)) { + appendClientSideMessages(viewer->slice); + consumer.put_next_copy(viewer->slice); + } + } + }; + const auto pushInstant = [=] { + viewer->scheduled = true; + push(); + }; + const auto pushDelayed = [=] { + if (!viewer->scheduled) { + viewer->scheduled = true; + crl::on_main(&viewer->guard, push); + } + }; + viewer->around = around; + viewer->limitBefore = limitBefore; + viewer->limitAfter = limitAfter; + + const auto history = owningHistory(); + history->session().changes().historyUpdates( + history, + HistoryUpdate::Flag::ClientSideMessages + ) | rpl::start_with_next(pushDelayed, lifetime); + + _listChanges.events( + ) | rpl::start_with_next(pushDelayed, lifetime); + + _instantChanges.events( + ) | rpl::start_with_next(pushInstant, lifetime); + + pushInstant(); + return lifetime; + }; +} + +not_null SavedSublist::parent() const { + return _parent; +} + +not_null SavedSublist::owningHistory() { + return _parent->owningHistory(); +} + +ChannelData *SavedSublist::parentChat() const { + return _parent->parentChat(); +} + +not_null SavedSublist::sublistPeer() const { + return _sublistHistory->peer; } bool SavedSublist::isHiddenAuthor() const { - return peer()->isSavedHiddenAuthor(); + return sublistPeer()->isSavedHiddenAuthor(); } -bool SavedSublist::isFullLoaded() const { - return (_flags & Flag::FullLoaded) != 0; -} - -auto SavedSublist::messages() const --> const std::vector> & { - return _items; +rpl::producer<> SavedSublist::destroyed() const { + using namespace rpl::mappers; + return rpl::merge( + _parent->destroyed(), + _parent->sublistDestroyed() | rpl::filter( + _1 == this + ) | rpl::to_empty); } void SavedSublist::applyMaybeLast(not_null item, bool added) { - const auto before = []( - not_null a, - not_null b) { - return IsServerMsgId(a->id) - ? (IsServerMsgId(b->id) ? (a->id < b->id) : true) - : (IsServerMsgId(b->id) ? false : (a->id < b->id)); - }; - - if (_items.empty()) { - _items.push_back(item); - } else if (_items.front() == item) { - return; - } else if (!isFullLoaded() - && _items.size() == 1 - && before(_items.front(), item)) { - _items[0] = item; - } else if (before(_items.back(), item)) { - for (auto i = begin(_items); i != end(_items); ++i) { - if (item == *i) { - return; - } else if (before(*i, item)) { - _items.insert(i, item); - break; - } - } - } - if (added && _fullCount) { - ++*_fullCount; - } - if (_items.front() == item) { - setChatListTimeId(item->date()); + growLastKnownServerMessageId(item->id); + if (!_lastServerMessage || (*_lastServerMessage)->id < item->id) { + setLastServerMessage(item); resolveChatListMessageGroup(); } - _changed.fire({}); } -void SavedSublist::removeOne(not_null item) { - if (_items.empty()) { - return; +void SavedSublist::applyItemAdded(not_null item) { + if (item->isRegular()) { + setLastServerMessage(item); + } else { + setLastMessage(item); } - const auto last = (_items.front() == item); - const auto from = ranges::remove(_items, item); - const auto removed = end(_items) - from; - if (removed) { - _items.erase(from, end(_items)); - } - if (_fullCount) { - --*_fullCount; - } - if (last) { - if (_items.empty()) { - if (isFullLoaded()) { - updateChatListExistence(); - } else { - updateChatListEntry(); - crl::on_main(this, [=] { - owner().savedMessages().loadMore(this); - }); - } - } else { - setChatListTimeId(_items.front()->date()); +} + +void SavedSublist::applyItemRemoved(MsgId id) { + if (const auto lastItem = lastMessage()) { + if (lastItem->id == id) { + _lastMessage = std::nullopt; } } - if (removed || _fullCount) { - _changed.fire({}); + if (const auto lastServerItem = lastServerMessage()) { + if (lastServerItem->id == id) { + _lastServerMessage = std::nullopt; + } } + if (const auto chatListItem = _chatListMessage.value_or(nullptr)) { + if (chatListItem->id == id) { + _chatListMessage = std::nullopt; + requestChatListMessage(); + } + } +} + +void SavedSublist::requestChatListMessage() { + if (!chatListMessageKnown()) { + parent()->requestSublist(sublistPeer()); + } +} + +void SavedSublist::readTillEnd() { + readTill(_lastKnownServerMessageId); +} + +bool SavedSublist::buildFromData(not_null viewer) { + if (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0) { + viewer->slice.ids.clear(); + viewer->slice.nearestToAround = FullMsgId(); + viewer->slice.fullCount + = viewer->slice.skippedBefore + = viewer->slice.skippedAfter + = 0; + ranges::reverse(viewer->slice.ids); + return true; + } + const auto around = (viewer->around != ShowAtUnreadMsgId) + ? viewer->around + : computeInboxReadTillFull(); + if (_list.empty() + || (!around && _skippedAfter != 0) + || (around > _list.front() && _skippedAfter != 0) + || (around > 0 && around < _list.back() && _skippedBefore != 0)) { + loadAround(around); + return false; + } + const auto i = around + ? ranges::lower_bound(_list, around, std::greater<>()) + : end(_list); + const auto availableBefore = int(end(_list) - i); + const auto availableAfter = int(i - begin(_list)); + const auto useBefore = std::min(availableBefore, viewer->limitBefore + 1); + const auto useAfter = std::min(availableAfter, viewer->limitAfter); + const auto slice = &viewer->slice; + if (_skippedBefore.has_value()) { + slice->skippedBefore + = (*_skippedBefore + (availableBefore - useBefore)); + } + if (_skippedAfter.has_value()) { + slice->skippedAfter + = (*_skippedAfter + (availableAfter - useAfter)); + } + + const auto peerId = owningHistory()->peer->id; + slice->ids.clear(); + auto nearestToAround = std::optional(); + slice->ids.reserve(useAfter + useBefore); + for (auto j = i - useAfter, e = i + useBefore; j != e; ++j) { + const auto id = *j; + if (!nearestToAround && id < around) { + nearestToAround = (j == i - useAfter) + ? id + : *(j - 1); + } + slice->ids.emplace_back(peerId, id); + } + slice->nearestToAround = FullMsgId( + peerId, + nearestToAround.value_or( + slice->ids.empty() ? 0 : slice->ids.back().msg)); + slice->fullCount = _fullCount.current(); + + ranges::reverse(viewer->slice.ids); + + if (_skippedBefore != 0 && useBefore < viewer->limitBefore + 1) { + loadBefore(); + } + if (_skippedAfter != 0 && useAfter < viewer->limitAfter) { + loadAfter(); + } + + return true; +} + +bool SavedSublist::applyUpdate(const MessageUpdate &update) { + using Flag = MessageUpdate::Flag; + + if (update.item->history() != owningHistory() + || !update.item->isRegular() + || update.item->sublistPeerId() != sublistPeer()->id) { + return false; + } else if (update.flags & Flag::Destroyed) { + return removeOne(update.item); + } + const auto id = update.item->id; + if (update.flags & Flag::NewAdded) { + changeUnreadCountByMessage(id, 1); + } + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + if (_skippedAfter != 0 + || (i != end(_list) && *i == id)) { + return false; + } + _list.insert(i, id); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + _fullCount = *known + 1; + } + return true; +} + +bool SavedSublist::processMessagesIsEmpty( + const MTPmessages_Messages &result) { + const auto guard = gsl::finally([&] { _listChanges.fire({}); }); + + const auto list = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return QVector(); + }, [&](const auto &data) { + owner().processUsers(data.vusers()); + owner().processChats(data.vchats()); + return data.vmessages().v; + }); + + const auto fullCount = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return 0; + }, [&](const MTPDmessages_messages &data) { + return int(data.vmessages().v.size()); + }, [&](const MTPDmessages_messagesSlice &data) { + return data.vcount().v; + }, [&](const MTPDmessages_channelMessages &data) { + if (const auto channel = owningHistory()->peer->asChannel()) { + channel->ptsReceived(data.vpts().v); + channel->processTopics(data.vtopics()); + } else { + LOG(("API Error: received messages.channelMessages when " + "no channel was passed! (HistoryWidget::messagesReceived)")); + } + return data.vcount().v; + }); + + if (list.isEmpty()) { + return true; + } + + const auto maxId = IdFromMessage(list.front()); + const auto wasSize = int(_list.size()); + const auto toFront = (wasSize > 0) && (maxId > _list.front()); + const auto localFlags = MessageFlags(); + const auto type = NewMessageType::Existing; + auto refreshed = std::vector(); + if (toFront) { + refreshed.reserve(_list.size() + list.size()); + } + auto skipped = 0; + for (const auto &message : list) { + if (const auto item = owner().addNewMessage(message, localFlags, type)) { + if (item->sublistPeerId() == sublistPeer()->id) { + if (toFront && item->id > _list.front()) { + refreshed.push_back(item->id); + } else if (_list.empty() || item->id < _list.back()) { + _list.push_back(item->id); + } + } else { + ++skipped; + } + } else { + ++skipped; + } + } + if (toFront) { + refreshed.insert(refreshed.end(), _list.begin(), _list.end()); + _list = std::move(refreshed); + } + + const auto nowSize = int(_list.size()); + auto &decrementFrom = toFront ? _skippedAfter : _skippedBefore; + if (decrementFrom.has_value()) { + *decrementFrom = std::max( + *decrementFrom - (nowSize - wasSize), + 0); + } + + const auto checkedCount = std::max(fullCount - skipped, nowSize); + if (_skippedBefore && _skippedAfter) { + auto &correct = toFront ? _skippedBefore : _skippedAfter; + *correct = std::max( + checkedCount - *decrementFrom - nowSize, + 0); + *decrementFrom = checkedCount - *correct - nowSize; + Assert(*decrementFrom >= 0); + } else if (_skippedBefore) { + *_skippedBefore = std::min(*_skippedBefore, checkedCount - nowSize); + _skippedAfter = checkedCount - *_skippedBefore - nowSize; + } else if (_skippedAfter) { + *_skippedAfter = std::min(*_skippedAfter, checkedCount - nowSize); + _skippedBefore = checkedCount - *_skippedAfter - nowSize; + } + _fullCount = checkedCount; + + checkReadTillEnd(); + + Ensures(list.size() >= skipped); + return (list.size() == skipped); +} + +void SavedSublist::setInboxReadTill( + MsgId readTillId, + std::optional unreadCount) { + const auto newReadTillId = std::max(readTillId.bare, int64(1)); + const auto ignore = (newReadTillId < _inboxReadTillId); + if (ignore) { + return; + } + const auto changed = (newReadTillId > _inboxReadTillId); + if (changed) { + _inboxReadTillId = newReadTillId; + } + if (_skippedAfter == 0 + && !_list.empty() + && _inboxReadTillId >= _list.front()) { + unreadCount = 0; + } else if (_lastServerMessage.value_or(nullptr) + && (*_lastServerMessage)->id <= newReadTillId) { + unreadCount = 0; + } + if (_unreadCount.current() != unreadCount + && (changed || unreadCount.has_value())) { + setUnreadCount(unreadCount); + } +} + +MsgId SavedSublist::inboxReadTillId() const { + return _inboxReadTillId; +} + +MsgId SavedSublist::computeInboxReadTillFull() const { + return _inboxReadTillId; +} + +void SavedSublist::setOutboxReadTill(MsgId readTillId) { + const auto newReadTillId = std::max(readTillId.bare, int64(1)); + if (newReadTillId > _outboxReadTillId) { + _outboxReadTillId = newReadTillId; + const auto history = owningHistory(); + history->session().changes().historyUpdated( + history, + HistoryUpdate::Flag::OutboxRead); + } +} + +MsgId SavedSublist::computeOutboxReadTillFull() const { + return _outboxReadTillId; +} + +void SavedSublist::setUnreadCount(std::optional count) { + _unreadCount = count; + if (!count && !_readRequestTimer.isActive() && !_readRequestId) { + reloadUnreadCountIfNeeded(); + } +} + +void SavedSublist::setUnreadMark(bool unread) { + if (unreadMark() == unread) { + return; + } + const auto notifier = unreadStateChangeNotifier( + !unreadCountCurrent()); + Thread::setUnreadMarkFlag(unread); +} + +bool SavedSublist::unreadCountKnown() const { + return !inMonoforum() || _unreadCount.current().has_value(); +} + +int SavedSublist::unreadCountCurrent() const { + return _unreadCount.current().value_or(0); +} + +rpl::producer> SavedSublist::unreadCountValue() const { + if (!inMonoforum()) { + return rpl::single(std::optional(0)); + } + return _unreadCount.value(); +} + +int SavedSublist::displayedUnreadCount() const { + return (_inboxReadTillId > 1) ? unreadCountCurrent() : 0; +} + +void SavedSublist::changeUnreadCountByMessage(MsgId id, int delta) { + if (!inMonoforum() || !_inboxReadTillId) { + setUnreadCount(std::nullopt); + return; + } + const auto count = _unreadCount.current(); + if (count.has_value() && (id > _inboxReadTillId)) { + setUnreadCount(std::max(*count + delta, 0)); + } +} + +bool SavedSublist::isServerSideUnread( + not_null item) const { + if (!inMonoforum()) { + return false; + } + const auto till = item->out() + ? computeOutboxReadTillFull() + : computeInboxReadTillFull(); + return (item->id > till); +} + +void SavedSublist::checkReadTillEnd() { + if (_unreadCount.current() != 0 + && _skippedAfter == 0 + && !_list.empty() + && _inboxReadTillId >= _list.front()) { + setUnreadCount(0); + } +} + +std::optional SavedSublist::computeUnreadCountLocally( + MsgId afterId) const { + Expects(afterId >= _inboxReadTillId); + + const auto currentUnreadCountAfter = _unreadCount.current(); + const auto startingMarkingAsRead = (currentUnreadCountAfter == 0) + && (_inboxReadTillId == 1) + && (afterId > 1); + const auto wasUnreadCountAfter = startingMarkingAsRead + ? _fullCount.current().value_or(0) + : currentUnreadCountAfter; + const auto readTillId = std::max(afterId, MsgId(1)); + const auto wasReadTillId = _inboxReadTillId; + const auto backLoaded = (_skippedBefore == 0); + const auto frontLoaded = (_skippedAfter == 0); + const auto fullLoaded = backLoaded && frontLoaded; + const auto allUnread = (readTillId == MsgId(1)) + || (fullLoaded && _list.empty()); + if (allUnread && fullLoaded) { + // Should not happen too often unless the list is empty. + return int(_list.size()); + } else if (frontLoaded && !_list.empty() && readTillId >= _list.front()) { + // Always "count by local data" if read till the end. + return 0; + } else if (wasReadTillId == readTillId) { + // Otherwise don't recount the same value over and over. + return wasUnreadCountAfter; + } else if (frontLoaded && !_list.empty() && readTillId >= _list.back()) { + // And count by local data if it is available and read-till changed. + return int(ranges::lower_bound(_list, readTillId, std::greater<>()) + - begin(_list)); + } else if (_list.empty()) { + return std::nullopt; + } else if (wasUnreadCountAfter.has_value() + && (frontLoaded || readTillId <= _list.front()) + && (backLoaded || wasReadTillId >= _list.back())) { + // Count how many were read since previous value. + const auto from = ranges::lower_bound( + _list, + readTillId, + std::greater<>()); + const auto till = ranges::lower_bound( + from, + end(_list), + wasReadTillId, + std::greater<>()); + return std::max(*wasUnreadCountAfter - int(till - from), 0); + } + return std::nullopt; +} + +void SavedSublist::requestUnreadCount() { + parent()->requestSublist(sublistPeer()); +} + +void SavedSublist::readTill(not_null item) { + readTill(item->id, item); +} + +void SavedSublist::readTill(MsgId tillId) { + const auto parentChat = _parent->parentChat(); + if (!parentChat) { + return; + } + readTill(tillId, owner().message(parentChat->id, tillId)); +} + +void SavedSublist::readTill( + MsgId tillId, + HistoryItem *tillIdItem) { + if (!IsServerMsgId(tillId)) { + return; + } + if (unreadMark()) { + owner().histories().changeSublistUnreadMark(this, false); + } + const auto was = computeInboxReadTillFull(); + const auto now = tillId; + if (now < was) { + return; + } + const auto unreadCount = computeUnreadCountLocally(now); + const auto fast = (tillIdItem && tillIdItem->out()) + || !unreadCount.has_value(); + if (was < now || (fast && now == was)) { + setInboxReadTill(now, unreadCount); + if (!_readRequestTimer.isActive()) { + _readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout); + } else if (fast && _readRequestTimer.remainingTime() > 0) { + _readRequestTimer.callOnce(0); + } + } + Core::App().notifications().clearIncomingFromSublist(this); +} + +void SavedSublist::sendReadTillRequest() { + const auto parentChat = _parent->parentChat(); + if (!parentChat) { + return; + } + if (_readRequestTimer.isActive()) { + _readRequestTimer.cancel(); + } + const auto api = &_parent->session().api(); + api->request(base::take(_readRequestId)).cancel(); + + _sentReadTill = computeInboxReadTillFull(); + _readRequestId = api->request(MTPmessages_ReadSavedHistory( + parentChat->input, + sublistPeer()->input, + MTP_int(_sentReadTill.bare) + )).done(crl::guard(this, [=] { + _readRequestId = 0; + reloadUnreadCountIfNeeded(); + })).send(); +} + +void SavedSublist::reloadUnreadCountIfNeeded() { + if (unreadCountKnown()) { + return; + } else if (inboxReadTillId() < computeInboxReadTillFull()) { + _readRequestTimer.callOnce(0); + } else { + requestUnreadCount(); + } +} + +void SavedSublist::subscribeToUnreadChanges() { + if (!inMonoforum()) { + return; + } + _unreadCount.value( + ) | rpl::map([=](std::optional value) { + return value ? displayedUnreadCount() : value; + }) | rpl::distinct_until_changed( + ) | rpl::combine_previous( + ) | rpl::filter([=] { + return inChatList(); + }) | rpl::start_with_next([=]( + std::optional previous, + std::optional now) { + if (previous.value_or(0) != now.value_or(0)) { + _parent->recentSublistsInvalidate(this); + } + notifyUnreadStateChange(unreadStateFor( + previous.value_or(0), + previous.has_value())); + }, _lifetime); +} + +void SavedSublist::applyMonoforumDialog( + const MTPDmonoForumDialog &data, + not_null topItem) { + if (const auto parent = parentChat()) { + if (const auto draft = data.vdraft()) { + draft->match([&](const MTPDdraftMessage &data) { + Data::ApplyPeerCloudDraft( + &session(), + parent->id, + MsgId(), + sublistPeer()->id, + data); + }, [](const MTPDdraftMessageEmpty&) {}); + } + } + + setInboxReadTill( + data.vread_inbox_max_id().v, + data.vunread_count().v); + if (!unreadCountKnown() && !_readRequestId) { + // We got read_inbox_max_id < than our current inboxReadTillId, + // we need either to send a read request with this new value, + // or to downgrade inboxReadTillId locally. + if (_sentReadTill < computeInboxReadTillFull()) { + sendReadTillRequest(); + } else { + // Just if nothing else helps. + _inboxReadTillId = 0; + setInboxReadTill( + data.vread_inbox_max_id().v, + data.vunread_count().v); + } + } + setOutboxReadTill(data.vread_outbox_max_id().v); + unreadReactions().setCount(data.vunread_reactions_count().v); + setUnreadMark(data.is_unread_mark()); + applyMaybeLast(topItem); +} + +TimeId SavedSublist::adjustedChatListTimeId() const { + const auto result = chatListTimeId(); + const auto monoforumPeerId = sublistPeer()->id; + const auto history = _parent->owningHistory(); + if (const auto draft = history->cloudDraft(MsgId(), monoforumPeerId)) { + if (!Data::DraftIsNull(draft) && !session().supportMode()) { + return std::max(result, draft->date); + } + } + return result; } rpl::producer<> SavedSublist::changes() const { - return _changed.events(); + return _listChanges.events(); +} + +void SavedSublist::loadFullCount() { + if (!_fullCount.current() && !_loadingAround) { + loadAround(0); + } +} + +void SavedSublist::appendClientSideMessages(MessagesSlice &slice) { + const auto &messages = owningHistory()->clientSideMessages(); + if (messages.empty()) { + return; + } else if (slice.ids.empty()) { + if (slice.skippedBefore != 0 || slice.skippedAfter != 0) { + return; + } + slice.ids.reserve(messages.size()); + const auto sublistPeerId = sublistPeer()->id; + for (const auto &item : messages) { + if (item->sublistPeerId() != sublistPeerId) { + continue; + } + slice.ids.push_back(item->fullId()); + } + ranges::sort(slice.ids); + return; + } + const auto sublistPeerId = sublistPeer()->id; + auto dates = std::vector(); + dates.reserve(slice.ids.size()); + for (const auto &id : slice.ids) { + const auto message = owner().message(id); + Assert(message != nullptr); + + dates.push_back(message->date()); + } + for (const auto &item : messages) { + if (item->sublistPeerId() != sublistPeerId) { + continue; + } + const auto date = item->date(); + if (date < dates.front()) { + if (slice.skippedBefore != 0) { + if (slice.skippedBefore) { + ++*slice.skippedBefore; + } + continue; + } + dates.insert(dates.begin(), date); + slice.ids.insert(slice.ids.begin(), item->fullId()); + } else { + auto to = dates.size(); + for (; to != 0; --to) { + const auto checkId = slice.ids[to - 1].msg; + if (dates[to - 1] > date) { + continue; + } else if (dates[to - 1] < date + || IsServerMsgId(checkId) + || checkId < item->id) { + break; + } + } + dates.insert(dates.begin() + to, date); + slice.ids.insert(slice.ids.begin() + to, item->fullId()); + } + } } std::optional SavedSublist::fullCount() const { - return isFullLoaded() ? int(_items.size()) : _fullCount; + return _fullCount.current(); } rpl::producer SavedSublist::fullCountValue() const { - return _changed.events_starting_with({}) | rpl::map([=] { - return fullCount(); - }) | rpl::filter_optional(); -} - -void SavedSublist::append( - std::vector> &&items, - int fullCount) { - _fullCount = fullCount; - if (items.empty()) { - setFullLoaded(); - } else if (_items.empty()) { - _items = std::move(items); - setChatListTimeId(_items.front()->date()); - _changed.fire({}); - } else if (_items.back()->id > items.front()->id) { - _items.insert(end(_items), begin(items), end(items)); - _changed.fire({}); - } else { - _items.insert(end(_items), begin(items), end(items)); - ranges::stable_sort( - _items, - ranges::greater(), - &HistoryItem::id); - ranges::unique(_items, ranges::greater(), &HistoryItem::id); - _changed.fire({}); - } -} - -void SavedSublist::setFullLoaded(bool loaded) { - if (loaded != isFullLoaded()) { - if (loaded) { - _flags |= Flag::FullLoaded; - if (_items.empty()) { - updateChatListExistence(); - } - } else { - _flags &= ~Flag::FullLoaded; - } - _changed.fire({}); - } + return _fullCount.value() | rpl::filter_optional(); } int SavedSublist::fixedOnTopIndex() const { @@ -171,55 +828,123 @@ int SavedSublist::fixedOnTopIndex() const { } bool SavedSublist::shouldBeInChatList() const { - return isPinnedDialog(FilterId()) || !_items.empty(); + if (const auto monoforum = _parent->parentChat()) { + if (monoforum == sublistPeer()) { + return false; + } + } + return isPinnedDialog(FilterId()) + || !lastMessageKnown() + || (lastMessage() != nullptr); +} + +HistoryItem *SavedSublist::lastMessage() const { + return _lastMessage.value_or(nullptr); +} + +bool SavedSublist::lastMessageKnown() const { + return _lastMessage.has_value(); +} + +HistoryItem *SavedSublist::lastServerMessage() const { + return _lastServerMessage.value_or(nullptr); +} + +bool SavedSublist::lastServerMessageKnown() const { + return _lastServerMessage.has_value(); +} + +MsgId SavedSublist::lastKnownServerMessageId() const { + return _lastKnownServerMessageId; } Dialogs::UnreadState SavedSublist::chatListUnreadState() const { - return {}; + if (!inMonoforum()) { + return {}; + } + return unreadStateFor(displayedUnreadCount(), unreadCountKnown()); } Dialogs::BadgesState SavedSublist::chatListBadgesState() const { - return {}; + if (!inMonoforum()) { + return {}; + } + auto result = Dialogs::BadgesForUnread( + chatListUnreadState(), + Dialogs::CountInBadge::Messages, + Dialogs::IncludeInBadge::All); + if (!result.unread && inboxReadTillId() < 2) { + result.unread = (_lastKnownServerMessageId + > _parent->owningHistory()->inboxReadTillId()); + result.unreadMuted = muted(); + } + if (_parent->owningHistory()->muted()) { + result.unreadMuted + = result.mentionMuted + = result.reactionMuted + = true; + } + return result; } HistoryItem *SavedSublist::chatListMessage() const { - return _items.empty() ? nullptr : _items.front().get(); + return _lastMessage.value_or(nullptr); } bool SavedSublist::chatListMessageKnown() const { - return true; + return _lastMessage.has_value(); } const QString &SavedSublist::chatListName() const { - return _history->chatListName(); + return _sublistHistory->chatListName(); } const base::flat_set &SavedSublist::chatListNameWords() const { - return _history->chatListNameWords(); + return _sublistHistory->chatListNameWords(); } const base::flat_set &SavedSublist::chatListFirstLetters() const { - return _history->chatListFirstLetters(); + return _sublistHistory->chatListFirstLetters(); } const QString &SavedSublist::chatListNameSortKey() const { - return _history->chatListNameSortKey(); + return _sublistHistory->chatListNameSortKey(); } int SavedSublist::chatListNameVersion() const { - return _history->chatListNameVersion(); + return _sublistHistory->chatListNameVersion(); } void SavedSublist::paintUserpic( Painter &p, Ui::PeerUserpicView &view, const Dialogs::Ui::PaintContext &context) const { - _history->paintUserpic(p, view, context); + _sublistHistory->paintUserpic(p, view, context); } -void SavedSublist::chatListPreloadData() { - peer()->loadUserpic(); - allowChatListMessageResolve(); +HistoryView::SendActionPainter *SavedSublist::sendActionPainter() { + return nullptr; +} + +void SavedSublist::hasUnreadMentionChanged(bool has) { + auto was = chatListUnreadState(); + if (has) { + was.mentions = 0; + } else { + was.mentions = 1; + } + notifyUnreadStateChange(was); +} + +void SavedSublist::hasUnreadReactionChanged(bool has) { + auto was = chatListUnreadState(); + if (has) { + was.reactions = was.reactionsMuted = 0; + } else { + was.reactions = 1; + was.reactionsMuted = muted() ? was.reactions : 0; + } + notifyUnreadStateChange(was); } void SavedSublist::allowChatListMessageResolve() { @@ -230,27 +955,263 @@ void SavedSublist::allowChatListMessageResolve() { resolveChatListMessageGroup(); } -bool SavedSublist::hasOrphanMediaGroupPart() const { - if (isFullLoaded() || _items.size() != 1) { - return false; - } - return (_items.front()->groupId() != MessageGroupId()); -} - void SavedSublist::resolveChatListMessageGroup() { - const auto item = chatListMessage(); - if (!(_flags & Flag::ResolveChatListMessage) - || !item - || !hasOrphanMediaGroupPart()) { + if (!(_flags & Flag::ResolveChatListMessage)) { return; } // If we set a single album part, request the full album. - const auto withImages = !item->toPreview({ - .hideSender = true, - .hideCaption = true }).images.empty(); - if (withImages) { - owner().histories().requestGroupAround(item); + const auto item = _lastServerMessage.value_or(nullptr); + if (item && item->groupId() != MessageGroupId()) { + if (owner().groups().isGroupOfOne(item) + && !item->toPreview({ + .hideSender = true, + .hideCaption = true }).images.empty() + && _requestedGroups.emplace(item->fullId()).second) { + owner().histories().requestGroupAround(item); + } } } +void SavedSublist::growLastKnownServerMessageId(MsgId id) { + _lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id); +} + +void SavedSublist::setLastServerMessage(HistoryItem *item) { + if (item) { + growLastKnownServerMessageId(item->id); + } + _lastServerMessage = item; + if (_lastMessage + && *_lastMessage + && !(*_lastMessage)->isRegular() + && (!item + || (*_lastMessage)->date() > item->date() + || (*_lastMessage)->isSending())) { + return; + } + setLastMessage(item); +} + +void SavedSublist::setLastMessage(HistoryItem *item) { + if (_lastMessage && *_lastMessage == item) { + return; + } + _lastMessage = item; + if (!item || item->isRegular()) { + _lastServerMessage = item; + if (item) { + growLastKnownServerMessageId(item->id); + } + } + setChatListMessage(item); +} + +void SavedSublist::setChatListMessage(HistoryItem *item) { + if (_chatListMessage && *_chatListMessage == item) { + return; + } + const auto was = _chatListMessage.value_or(nullptr); + if (item) { + if (item->isSponsored()) { + return; + } + if (_chatListMessage + && *_chatListMessage + && !(*_chatListMessage)->isRegular() + && (*_chatListMessage)->date() > item->date()) { + return; + } + _chatListMessage = item; + setChatListTimeId(item->date()); + } else if (!_chatListMessage || *_chatListMessage) { + _chatListMessage = nullptr; + updateChatListEntry(); + } + _parent->listMessageChanged(was, item); +} + +void SavedSublist::chatListPreloadData() { + sublistPeer()->loadUserpic(); + allowChatListMessageResolve(); +} + +Dialogs::UnreadState SavedSublist::unreadStateFor( + int count, + bool known) const { + auto result = Dialogs::UnreadState(); + const auto mark = !count && unreadMark(); + const auto muted = this->muted(); + result.messages = count; + result.chats = count ? 1 : 0; + result.marks = mark ? 1 : 0; + result.reactions = unreadReactions().has() ? 1 : 0; + result.messagesMuted = muted ? result.messages : 0; + result.chatsMuted = muted ? result.chats : 0; + result.marksMuted = muted ? result.marks : 0; + result.reactionsMuted = muted ? result.reactions : 0; + result.known = known; + return result; +} + +Histories &SavedSublist::histories() { + return owner().histories(); +} + +void SavedSublist::loadAround(MsgId id) { + if (_loadingAround && *_loadingAround == id) { + return; + } + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + + const auto send = [=](Fn finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(id), // offset_id + MTP_int(0), // offset_date + MTP_int(id ? (-kMessagesPerPage / 2) : 0), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + + if (!id) { + _skippedAfter = 0; + } else { + _skippedAfter = std::nullopt; + } + _skippedBefore = std::nullopt; + _list.clear(); + if (processMessagesIsEmpty(result)) { + _fullCount = _skippedBefore = _skippedAfter = 0; + } else if (id) { + Assert(!_list.empty()); + if (_list.front() <= id) { + _skippedAfter = 0; + } else if (_list.back() >= id) { + _skippedBefore = 0; + } + } + checkReadTillEnd(); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { + _parent->markUnsupported(); + } + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + }).send(); + }; + _loadingAround = id; + _beforeId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + +void SavedSublist::loadBefore() { + Expects(!_list.empty()); + + if (_loadingAround) { + histories().cancelRequest(base::take(_beforeId)); + } else if (_beforeId) { + return; + } + + const auto last = _list.back(); + const auto send = [=](Fn finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(last), // offset_id + MTP_int(0), // offset_date + MTP_int(0), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // min_id + MTP_int(0), // max_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.back() != last) { + loadBefore(); + } else if (processMessagesIsEmpty(result)) { + _skippedBefore = 0; + if (_skippedAfter == 0) { + _fullCount = _list.size(); + } + } + }).fail([=] { + _beforeId = 0; + finish(); + }).send(); + }; + _beforeId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + +void SavedSublist::loadAfter() { + Expects(!_list.empty()); + + if (_afterId) { + return; + } + + const auto first = _list.front(); + const auto send = [=](Fn finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(first + 1), // offset_id + MTP_int(0), // offset_date + MTP_int(-kMessagesPerPage), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // min_id + MTP_int(0), // max_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { + _afterId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.front() != first) { + loadAfter(); + } else if (processMessagesIsEmpty(result)) { + _skippedAfter = 0; + if (_skippedBefore == 0) { + _fullCount = _list.size(); + } + checkReadTillEnd(); + } + }).fail([=] { + _afterId = 0; + finish(); + }).send(); + }; + _afterId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 15c3428fff..1361f207fb 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" +#include "data/data_thread.h" #include "dialogs/ui/dialogs_message_view.h" -#include "dialogs/dialogs_entry.h" class PeerData; class History; @@ -16,31 +17,62 @@ class History; namespace Data { class Session; +class Histories; +class SavedMessages; +struct MessagePosition; +struct MessageUpdate; +struct SublistReadTillUpdate; +struct MessagesSlice; -class SavedSublist final : public Dialogs::Entry { +class SavedSublist final : public Data::Thread { public: - explicit SavedSublist(not_null peer); + SavedSublist( + not_null parent, + not_null sublistPeer); ~SavedSublist(); - [[nodiscard]] not_null history() const; - [[nodiscard]] not_null peer() const; - [[nodiscard]] bool isHiddenAuthor() const; - [[nodiscard]] bool isFullLoaded() const; + [[nodiscard]] bool inMonoforum() const; - [[nodiscard]] auto messages() const - -> const std::vector> &; + void apply(const SublistReadTillUpdate &update); + void apply(const MessageUpdate &update); + void applyDifferenceTooLong(); + bool removeOne(not_null item); + + [[nodiscard]] rpl::producer source( + MessagePosition aroundId, + int limitBefore, + int limitAfter); + + [[nodiscard]] not_null parent() const; + [[nodiscard]] not_null owningHistory() override; + [[nodiscard]] ChannelData *parentChat() const; + [[nodiscard]] not_null sublistPeer() const; + [[nodiscard]] bool isHiddenAuthor() const; + [[nodiscard]] rpl::producer<> destroyed() const; + + void growLastKnownServerMessageId(MsgId id); void applyMaybeLast(not_null item, bool added = false); - void removeOne(not_null item); - void append(std::vector> &&items, int fullCount); - void setFullLoaded(bool loaded = true); + void applyItemAdded(not_null item); + void applyItemRemoved(MsgId id); [[nodiscard]] rpl::producer<> changes() const; [[nodiscard]] std::optional fullCount() const; [[nodiscard]] rpl::producer fullCountValue() const; + void loadFullCount(); - [[nodiscard]] Dialogs::Ui::MessageView &lastItemDialogsView() { - return _lastItemDialogsView; - } + [[nodiscard]] bool unreadCountKnown() const; + [[nodiscard]] int unreadCountCurrent() const; + [[nodiscard]] int displayedUnreadCount() const; + [[nodiscard]] rpl::producer> unreadCountValue() const; + void setUnreadMark(bool unread); + + void applyMonoforumDialog( + const MTPDmonoForumDialog &dialog, + not_null topItem); + void readTillEnd(); + void requestChatListMessage(); + + TimeId adjustedChatListTimeId() const override; int fixedOnTopIndex() const override; bool shouldBeInChatList() const override; @@ -54,32 +86,110 @@ public: const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; + void hasUnreadMentionChanged(bool has) override; + void hasUnreadReactionChanged(bool has) override; + + [[nodiscard]] HistoryItem *lastMessage() const; + [[nodiscard]] HistoryItem *lastServerMessage() const; + [[nodiscard]] bool lastMessageKnown() const; + [[nodiscard]] bool lastServerMessageKnown() const; + [[nodiscard]] MsgId lastKnownServerMessageId() const; + + void setInboxReadTill(MsgId readTillId, std::optional unreadCount); + [[nodiscard]] MsgId inboxReadTillId() const; + [[nodiscard]] MsgId computeInboxReadTillFull() const; + + void setOutboxReadTill(MsgId readTillId); + [[nodiscard]] MsgId computeOutboxReadTillFull() const; + + [[nodiscard]] bool isServerSideUnread( + not_null item) const override; + + void requestUnreadCount(); + + void readTill(not_null item); + void readTill(MsgId tillId); + void chatListPreloadData() override; void paintUserpic( Painter &p, Ui::PeerUserpicView &view, const Dialogs::Ui::PaintContext &context) const override; + [[nodiscard]] auto sendActionPainter() + -> HistoryView::SendActionPainter* override; + private: + struct Viewer; + enum class Flag : uchar { ResolveChatListMessage = (1 << 0), - FullLoaded = (1 << 1), + InMonoforum = (1 << 1), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags; - bool hasOrphanMediaGroupPart() const; + [[nodiscard]] Histories &histories(); + + void subscribeToUnreadChanges(); + [[nodiscard]] Dialogs::UnreadState unreadStateFor( + int count, + bool known) const; + void setLastMessage(HistoryItem *item); + void setLastServerMessage(HistoryItem *item); + void setChatListMessage(HistoryItem *item); void allowChatListMessageResolve(); void resolveChatListMessageGroup(); - const not_null _history; + void changeUnreadCountByMessage(MsgId id, int delta); + void setUnreadCount(std::optional count); + void readTill(MsgId tillId, HistoryItem *tillIdItem); + void checkReadTillEnd(); + void sendReadTillRequest(); + void reloadUnreadCountIfNeeded(); - std::vector> _items; - std::optional _fullCount; - rpl::event_stream<> _changed; - Dialogs::Ui::MessageView _lastItemDialogsView; + [[nodiscard]] bool buildFromData(not_null viewer); + [[nodiscard]] bool applyUpdate(const MessageUpdate &update); + void appendClientSideMessages(MessagesSlice &slice); + [[nodiscard]] std::optional computeUnreadCountLocally( + MsgId afterId) const; + bool processMessagesIsEmpty(const MTPmessages_Messages &result); + void loadAround(MsgId id); + void loadBefore(); + void loadAfter(); + + const not_null _parent; + const not_null _sublistHistory; + + MsgId _lastKnownServerMessageId = 0; + + std::vector _list; + std::optional _skippedBefore; + std::optional _skippedAfter; + rpl::variable> _fullCount; + rpl::event_stream<> _listChanges; + rpl::event_stream<> _instantChanges; + std::optional _loadingAround; + rpl::variable> _unreadCount; + MsgId _inboxReadTillId = 0; + MsgId _outboxReadTillId = 0; Flags _flags; + std::optional _lastMessage; + std::optional _lastServerMessage; + std::optional _chatListMessage; + base::flat_set _requestedGroups; + int _beforeId = 0; + int _afterId = 0; + + base::Timer _readRequestTimer; + mtpRequestId _readRequestId = 0; + MsgId _sentReadTill = 0; + + mtpRequestId _reloadUnreadCountRequestId = 0; + + rpl::lifetime _lifetime; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 4a2418eb9d..6b43192805 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -132,6 +132,7 @@ GlobalMediaResult ParseGlobalMediaResult( std::optional PrepareSearchRequest( not_null peer, MsgId topicRootId, + PeerId monoforumPeerId, Storage::SharedMediaType type, const QString &query, MsgId messageId, @@ -168,11 +169,14 @@ std::optional PrepareSearchRequest( int64(0x3FFFFFFF))); using Flag = MTPmessages_Search::Flag; return MTPmessages_Search( - MTP_flags(topicRootId ? Flag::f_top_msg_id : Flag(0)), + MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (monoforumPeerId ? Flag::f_saved_peer_id : Flag(0))), peer->input, MTP_string(query), MTP_inputPeerEmpty(), - MTPInputPeer(), // saved_peer_id + (monoforumPeerId + ? peer->owner().peer(monoforumPeerId)->input + : MTPInputPeer()), MTPVector(), // saved_reaction MTP_int(topicRootId), filter, @@ -369,12 +373,14 @@ rpl::producer SearchController::idsSlice( auto createSimpleViewer = [=]( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter) { return simpleIdsSlice( peerId, topicRootId, + monoforumPeerId, simpleKey, query, limitBefore, @@ -384,6 +390,7 @@ rpl::producer SearchController::idsSlice( SparseIdsMergedSlice::Key( query.peerId, query.topicRootId, + query.monoforumPeerId, query.migratedPeerId, aroundId), limitBefore, @@ -394,6 +401,7 @@ rpl::producer SearchController::idsSlice( rpl::producer SearchController::simpleIdsSlice( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId aroundId, const Query &query, int limitBefore, @@ -402,8 +410,12 @@ rpl::producer SearchController::simpleIdsSlice( Expects(IsServerMsgId(aroundId) || (aroundId == 0)); Expects((aroundId != 0) || (limitBefore == 0 && limitAfter == 0)); - Expects((query.peerId == peerId && query.topicRootId == topicRootId) - || (query.migratedPeerId == peerId && MsgId(0) == topicRootId)); + Expects((query.peerId == peerId + && query.topicRootId == topicRootId + && query.monoforumPeerId == monoforumPeerId) + || (query.migratedPeerId == peerId + && MsgId(0) == topicRootId + && PeerId(0) == monoforumPeerId)); auto it = _cache.find(query); if (it == _cache.end()) { @@ -437,7 +449,9 @@ rpl::producer SearchController::simpleIdsSlice( _session->data().itemRemoved( ) | rpl::filter([=](not_null item) { return (item->history()->peer->id == peerId) - && (!topicRootId || item->topicRootId() == topicRootId); + && (!topicRootId || item->topicRootId() == topicRootId) + && (!monoforumPeerId + || item->sublistPeerId() == monoforumPeerId); }) | rpl::filter([=](not_null item) { return builder->removeOne(item->id); }) | rpl::start_with_next(pushNextSnapshot, lifetime); @@ -510,6 +524,7 @@ void SearchController::requestMore( auto prepared = PrepareSearchRequest( listData->peer, query.topicRootId, + query.monoforumPeerId, query.type, query.query, key.aroundId, diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index c73fb7b390..e7fb3f8e9f 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -61,6 +61,7 @@ struct GlobalMediaResult { [[nodiscard]] std::optional PrepareSearchRequest( not_null peer, MsgId topicRootId, + PeerId monoforumPeerId, Storage::SharedMediaType type, const QString &query, MsgId messageId, @@ -92,6 +93,7 @@ public: PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; PeerId migratedPeerId = 0; MediaType type = MediaType::kCount; QString query; @@ -151,6 +153,7 @@ private: rpl::producer simpleIdsSlice( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId aroundId, const Query &query, int limitBefore, diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 3c79685af9..6037051148 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -75,6 +75,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_premium_limits.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_todo_list.h" #include "base/platform/base_platform_info.h" #include "base/unixtime.h" #include "base/call_delayed.h" @@ -351,6 +352,19 @@ void Session::subscribeForTopicRepliesLists() { } }, _lifetime); + sublistReadTillUpdates( + ) | rpl::start_with_next([=](const SublistReadTillUpdate &update) { + if (const auto parentChat = channelLoaded(update.parentChatId)) { + if (const auto monoforum = parentChat->monoforum()) { + const auto sublistPeerId = update.sublistPeerId; + const auto peer = monoforum->owner().peer(sublistPeerId); + if (const auto sublist = monoforum->sublistLoaded(peer)) { + sublist->apply(update); + } + } + } + }, _lifetime); + session().changes().messageUpdates( MessageUpdate::Flag::NewAdded | MessageUpdate::Flag::NewMaybeAdded @@ -359,6 +373,11 @@ void Session::subscribeForTopicRepliesLists() { ) | rpl::start_with_next([=](const MessageUpdate &update) { if (const auto topic = update.item->topic()) { topic->replies()->apply(update); + } else if (update.flags == MessageUpdate::Flag::ReplyToTopAdded) { + // Not interested in this one for sublist. + return; + } else if (const auto sublist = update.item->savedSublist()) { + sublist->apply(update); } }, _lifetime); @@ -382,19 +401,21 @@ void Session::clear() { // Optimization: clear notifications before destroying items. Core::App().notifications().clearFromSession(_session); - // We must clear all forums before clearing customEmojiManager. + // We must clear all [mono]forums before clearing customEmojiManager. // Because in Data::ForumTopic an Ui::Text::CustomEmoji is cached. auto forums = base::flat_set>(); for (const auto &[peerId, peer] : _peers) { if (const auto channel = peer->asChannel()) { - if (channel->isForum()) { + if (channel->isForum() || channel->amMonoforumAdmin()) { forums.emplace(channel); } } } for (const auto &channel : forums) { - channel->setFlags(channel->flags() & ~ChannelDataFlag::Forum); + channel->setFlags(channel->flags() + & ~(ChannelDataFlag::Forum | ChannelDataFlag::MonoforumAdmin)); } + _savedMessages->clear(); _sendActionManager->clear(); @@ -534,6 +555,14 @@ not_null Session::processUser(const MTPUser &data) { const auto canShareThisContact = result->canShareThisContactFast(); + const auto hasRequirePremiumToWrite + = data.is_contact_require_premium(); + const auto hasStarsPerMessage + = data.vsend_paid_messages_stars().has_value(); + if (!hasStarsPerMessage) { + result->setStarsPerMessage(0); + } + using Flag = UserDataFlag; const auto flagsMask = Flag::Deleted | Flag::Verified @@ -545,19 +574,15 @@ not_null Session::processUser(const MTPUser &data) { | Flag::HasRequirePremiumToWrite | Flag::HasStarsPerMessage | Flag::MessageMoneyRestrictionsKnown + | (!hasRequirePremiumToWrite + ? Flag::RequiresPremiumToWrite + : Flag()) | (!minimal ? Flag::Contact | Flag::MutualContact | Flag::DiscardMinPhoto | Flag::StoriesHidden : Flag()); - const auto hasRequirePremiumToWrite - = data.is_contact_require_premium(); - const auto hasStarsPerMessage - = data.vsend_paid_messages_stars().has_value(); - if (!hasStarsPerMessage) { - result->setStarsPerMessage(0); - } const auto storiesState = minimal ? std::optional() : data.is_stories_unavailable() @@ -971,16 +996,32 @@ not_null Session::processChat(const MTPChat &data) { | Flag::CallActive | Flag::CallNotEmpty | Flag::Forbidden - | (!minimal ? (Flag::Left | Flag::Creator) : Flag()) + | (!minimal + ? (Flag::Left | Flag::Creator) + : Flag()) | Flag::NoForwards | Flag::AyuNoForwards | Flag::JoinToWrite | Flag::RequestToJoin | Flag::Forum + | Flag::ForumTabs | ((!minimal && !data.is_stories_hidden_min()) ? Flag::StoriesHidden : Flag()) - | Flag::AutoTranslation; + | Flag::AutoTranslation + | Flag::Monoforum + | Flag::HasStarsPerMessage + | Flag::StarsPerMessageKnown; + const auto hasStarsPerMessage + = data.vsend_paid_messages_stars().has_value(); + if (!hasStarsPerMessage) { + channel->setStarsPerMessage(0); + _commonStarsPerMessage.remove(channel); + } else if (const auto count = data.vsend_paid_messages_stars()->v) { + _commonStarsPerMessage[channel] = count; + } else { + _commonStarsPerMessage.remove(channel); + } const auto storiesState = minimal ? std::optional() : data.is_stories_unavailable() @@ -1006,22 +1047,30 @@ not_null Session::processChat(const MTPChat &data) { ? Flag::CallNotEmpty : Flag()) | (!minimal - ? (data.is_left() ? Flag::Left : Flag()) - | (data.is_creator() ? Flag::Creator : Flag()) + ? ((data.is_left() ? Flag::Left : Flag()) + | (data.is_creator() ? Flag::Creator : Flag())) : Flag()) | (data.is_noforwards() ? Flag::NoForwards : Flag()) + | (data.is_ayuNoforwards() ? Flag::AyuNoForwards : Flag()) | (data.is_join_to_send() ? Flag::JoinToWrite : Flag()) | (data.is_join_request() ? Flag::RequestToJoin : Flag()) | ((data.is_forum() && data.is_megagroup()) ? Flag::Forum : Flag()) + | (data.is_forum_tabs() ? Flag::ForumTabs : Flag()) | ((!minimal && !data.is_stories_hidden_min() && data.is_stories_hidden()) ? Flag::StoriesHidden : Flag()) | (data.is_autotranslation() ? Flag::AutoTranslation : Flag()) - | (data.is_ayuNoforwards() ? Flag::AyuNoForwards : Flag()); + | (data.is_monoforum() ? Flag::Monoforum : Flag()) + | (hasStarsPerMessage + ? (Flag::HasStarsPerMessage + | (channel->starsPerMessageKnown() + ? Flag::StarsPerMessageKnown + : Flag())) + : Flag::StarsPerMessageKnown); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setBotVerifyDetailsIcon( data.vbot_verification_icon().value_or_empty()); @@ -1034,8 +1083,9 @@ not_null Session::processChat(const MTPChat &data) { } channel->setPhoto(data.vphoto()); - channel->setStarsPerMessage( - data.vsend_paid_messages_stars().value_or_empty()); + applyMonoforumLinkedId( + channel, + data.vlinked_monoforum_id().value_or_empty()); if (wasInChannel != channel->amIn()) { flags |= UpdateFlag::ChannelAmIn; @@ -1119,12 +1169,38 @@ UserData *Session::processUsers(const MTPVector &data) { PeerData *Session::processChats(const MTPVector &data) { auto result = (PeerData*)nullptr; + _postponedMonoforumLinkedIds.emplace(); for (const auto &chat : data.v) { result = processChat(chat); } + const auto ids = base::take(_postponedMonoforumLinkedIds); + for (const auto &[channel, linkedId] : *ids) { + applyMonoforumLinkedId(channel, linkedId); + } return result; } +void Session::applyMonoforumLinkedId( + not_null channel, + ChannelId linkedId) { + if (!linkedId) { + channel->setMonoforumLink(nullptr); + } else if (_postponedMonoforumLinkedIds) { + _postponedMonoforumLinkedIds->emplace(channel, linkedId); + } else { + const auto loaded = channel->isLoaded(); + const auto linked = this->channel(linkedId); + const auto good = loaded + ? linked->isLoaded() + : linked->isMinimalLoaded(); + if (good) { + channel->setMonoforumLink(linked); + } else { + channel->updateFull(); + } + } +} + void Session::applyMaximumChatVersions(const MTPVector &data) { for (const auto &chat : data.v) { chat.match([&](const MTPDchat &data) { @@ -1688,6 +1764,16 @@ void Session::requestPollViewRepaint(not_null poll) { } } +void Session::requestTodoListViewRepaint( + not_null todolist) { + if (const auto i = _todoListViews.find(todolist) + ; i != _todoListViews.end()) { + for (const auto &view : i->second) { + requestViewResize(view); + } + } +} + void Session::documentLoadProgress(not_null document) { requestDocumentViewRepaint(document); _documentLoadProgress.fire_copy(document); @@ -2324,6 +2410,9 @@ void Session::applyDialog( bool Session::pinnedCanPin(not_null entry) const { if ([[maybe_unused]] const auto sublist = entry->asSublist()) { + if (sublist->parentChat()) { + return false; + } const auto saved = &savedMessages(); return pinnedChatsOrder(saved).size() < pinnedChatsLimit(saved); } else if (const auto topic = entry->asTopic()) { @@ -2365,6 +2454,9 @@ int Session::pinnedChatsLimit(not_null forum) const { } int Session::pinnedChatsLimit(not_null saved) const { + if (saved->parentChat()) { + return 0; + } const auto limits = Data::PremiumLimits(_session); return limits.savedSublistsPinnedCurrent(); } @@ -2405,6 +2497,9 @@ rpl::producer Session::maxPinnedChatsLimitValue( rpl::producer Session::maxPinnedChatsLimitValue( not_null saved) const { + if (saved->parentChat()) { + return rpl::single(0); + } // Premium limit from appconfig. // We always use premium limit in the MainList limit producer, // because it slices the list to that limit. We don't want to slice @@ -2950,6 +3045,15 @@ auto Session::repliesReadTillUpdates() const return _repliesReadTillUpdates.events(); } +void Session::updateSublistReadTill(SublistReadTillUpdate update) { + _sublistReadTillUpdates.fire(std::move(update)); +} + +auto Session::sublistReadTillUpdates() const +-> rpl::producer { + return _sublistReadTillUpdates.events(); +} + int Session::computeUnreadBadge(const Dialogs::UnreadState &state) const { const auto all = Core::App().settings().includeMutedCounter(); return std::max(state.marks - (all ? 0 : state.marksMuted), 0) @@ -4105,6 +4209,49 @@ not_null Session::processPoll(const MTPDmessageMediaPoll &data) { return result; } +not_null Session::todoList(TodoListId id) { + auto i = _todoLists.find(id); + if (i == _todoLists.cend()) { + i = _todoLists.emplace( + id, + std::make_unique(this, id)).first; + } + return i->second.get(); +} + +not_null Session::processTodoList( + TodoListId id, + const MTPTodoList &todolist) { + const auto &data = todolist.data(); + const auto result = todoList(id); + const auto changed = result->applyChanges(data); + if (changed) { + notifyTodoListUpdateDelayed(result); + } + return result; +} + +not_null Session::processTodoList( + TodoListId id, + const MTPDmessageMediaToDo &data) { + const auto result = processTodoList(id, data.vtodo()); + const auto changed = result->applyCompletions(data.vcompletions()); + if (changed) { + notifyTodoListUpdateDelayed(result); + } + return result; +} + +not_null Session::duplicateTodoList( + TodoListId id, + not_null existing) { + const auto result = todoList(id); + result->title = existing->title; + result->items = existing->items; + ++result->version; + return result; +} + void Session::checkPollsClosings() { const auto now = base::unixtime::now(); auto closest = 0; @@ -4315,6 +4462,24 @@ void Session::unregisterPollView( } } +void Session::registerTodoListView( + not_null todolist, + not_null view) { + _todoListViews[todolist].insert(view); +} + +void Session::unregisterTodoListView( + not_null todolist, + not_null view) { + const auto i = _todoListViews.find(todolist); + if (i != _todoListViews.end()) { + auto &items = i->second; + if (items.remove(view) && items.empty()) { + _todoListViews.erase(i); + } + } +} + void Session::registerContactView( UserId contactId, not_null view) { @@ -4495,37 +4660,54 @@ QString Session::findContactPhone(UserId contactId) const { return QString(); } -bool Session::hasPendingWebPageGamePollNotification() const { +bool Session::hasPendingWebPageGamePollTodoListNotification() const { return !_webpagesUpdated.empty() || !_gamesUpdated.empty() - || !_pollsUpdated.empty(); + || !_pollsUpdated.empty() + || !_todoListsUpdated.empty(); } void Session::notifyWebPageUpdateDelayed(not_null page) { - const auto invoke = !hasPendingWebPageGamePollNotification(); + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); _webpagesUpdated.insert(page); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); } } void Session::notifyGameUpdateDelayed(not_null game) { - const auto invoke = !hasPendingWebPageGamePollNotification(); + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); _gamesUpdated.insert(game); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); } } void Session::notifyPollUpdateDelayed(not_null poll) { - const auto invoke = !hasPendingWebPageGamePollNotification(); + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); _pollsUpdated.insert(poll); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); } } -void Session::sendWebPageGamePollNotifications() { +void Session::notifyTodoListUpdateDelayed(not_null todolist) { + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); + _todoListsUpdated.insert(todolist); + if (invoke) { + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); + } +} + +void Session::sendWebPageGamePollTodoListNotifications() { auto resize = std::vector>(); for (const auto &page : base::take(_webpagesUpdated)) { _webpageUpdates.fire_copy(page); @@ -4544,6 +4726,12 @@ void Session::sendWebPageGamePollNotifications() { resize.insert(end(resize), begin(i->second), end(i->second)); } } + for (const auto &todolist : base::take(_todoListsUpdated)) { + if (const auto i = _todoListViews.find(todolist) + ; i != _todoListViews.end()) { + resize.insert(end(resize), begin(i->second), end(i->second)); + } + } for (const auto &view : resize) { requestViewResize(view); } @@ -4627,12 +4815,12 @@ not_null Session::processFolder(const MTPDfolder &data) { not_null Session::chatsListFor( not_null entry) { - const auto topic = entry->asTopic(); - return topic - ? topic->forum()->topicsList() - : entry->asSublist() - ? _savedMessages->chatsList() - : chatsList(entry->folder()); + if (const auto topic = entry->asTopic()) { + return topic->forum()->topicsList(); + } else if (const auto sublist = entry->asSublist()) { + return sublist->parent()->chatsList(); + } + return chatsList(entry->folder()); } not_null Session::chatsList(Data::Folder *folder) { @@ -4708,7 +4896,14 @@ void Session::refreshChatListEntry(Dialogs::Key key) { } if (const auto forum = history->peer->forum()) { forum->preloadTopics(); + } else if (const auto monoforum = history->peer->monoforum()) { + monoforum->preloadSublists(); } + //if (const auto broadcast = history->peer->monoforumBroadcast()) { + // if (!broadcast->isFullLoaded()) { + // broadcast->updateFull(); + // } + //} } } @@ -4855,7 +5050,8 @@ void Session::insertCheckedServiceNotification( MTPlong(), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong()), // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost()), localFlags, NewMessageType::Unread); } @@ -5014,6 +5210,24 @@ rpl::producer Session::sentFromScheduled() const { return _sentFromScheduled.events(); } +void Session::editStarsPerMessage( + not_null channel, + int count) { + // For admin it's zero, we're admin if we can edit it. + channel->setStarsPerMessage(0); + if (count) { + _commonStarsPerMessage[channel] = count; + } else { + _commonStarsPerMessage.remove(channel); + } +} + +int Session::commonStarsPerMessage( + not_null channel) const { + const auto i = _commonStarsPerMessage.find(channel); + return (i != end(_commonStarsPerMessage)) ? i->second : 0; +} + void Session::clearLocalStorage() { _cache->close(); _cache->clear(); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 2a32df2bc2..701eaf08db 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -80,6 +80,13 @@ struct RepliesReadTillUpdate { bool out = false; }; +struct SublistReadTillUpdate { + ChannelId parentChatId; + PeerId sublistPeerId; + MsgId readTillId; + bool out = false; +}; + struct GiftUpdate { enum class Action : uchar { Save, @@ -529,6 +536,7 @@ public: void requestDocumentViewRepaint(not_null document); void markMediaRead(not_null document); void requestPollViewRepaint(not_null poll); + void requestTodoListViewRepaint(not_null todolist); void photoLoadProgress(not_null photo); void photoLoadDone(not_null photo); @@ -565,6 +573,10 @@ public: [[nodiscard]] auto repliesReadTillUpdates() const -> rpl::producer; + void updateSublistReadTill(SublistReadTillUpdate update); + [[nodiscard]] auto sublistReadTillUpdates() const + -> rpl::producer; + void selfDestructIn(not_null item, crl::time delay); [[nodiscard]] not_null photo(PhotoId id); @@ -679,6 +691,17 @@ public: not_null processPoll(const MTPPoll &data); not_null processPoll(const MTPDmessageMediaPoll &data); + [[nodiscard]] not_null todoList(TodoListId id); + not_null processTodoList( + TodoListId id, + const MTPTodoList &todolist); + not_null processTodoList( + TodoListId id, + const MTPDmessageMediaToDo &data); + [[nodiscard]] not_null duplicateTodoList( + TodoListId id, + not_null existing); + [[nodiscard]] not_null location( const LocationPoint &point); @@ -718,6 +741,12 @@ public: void unregisterPollView( not_null poll, not_null view); + void registerTodoListView( + not_null todolist, + not_null view); + void unregisterTodoListView( + not_null todolist, + not_null view); void registerContactView( UserId contactId, not_null view); @@ -747,8 +776,9 @@ public: void notifyWebPageUpdateDelayed(not_null page); void notifyGameUpdateDelayed(not_null game); void notifyPollUpdateDelayed(not_null poll); - [[nodiscard]] bool hasPendingWebPageGamePollNotification() const; - void sendWebPageGamePollNotifications(); + void notifyTodoListUpdateDelayed(not_null todolist); + [[nodiscard]] bool hasPendingWebPageGamePollTodoListNotification() const; + void sendWebPageGamePollTodoListNotifications(); [[nodiscard]] rpl::producer> webPageUpdates() const; void channelDifferenceTooLong(not_null channel); @@ -830,6 +860,10 @@ public: void sentFromScheduled(SentFromScheduled value); [[nodiscard]] rpl::producer sentFromScheduled() const; + void editStarsPerMessage(not_null channel, int count); + [[nodiscard]] int commonStarsPerMessage( + not_null channel) const; + void clearLocalStorage(); private: @@ -962,6 +996,10 @@ private: void setWallpapers(const QVector &data, uint64 hash); void highlightProcessDone(uint64 processId); + void applyMonoforumLinkedId( + not_null channel, + ChannelId linkedId); + void checkPollsClosings(); const not_null _session; @@ -1004,6 +1042,7 @@ private: rpl::event_stream _chatListEntryRefreshes; rpl::event_stream<> _unreadBadgeChanges; rpl::event_stream _repliesReadTillUpdates; + rpl::event_stream _sublistReadTillUpdates; rpl::event_stream _sentToScheduled; rpl::event_stream _sentFromScheduled; @@ -1054,6 +1093,9 @@ private: std::unordered_map< PollId, std::unique_ptr> _polls; + std::map< + TodoListId, + std::unique_ptr> _todoLists; std::unordered_map< GameId, std::unique_ptr> _games; @@ -1066,6 +1108,9 @@ private: std::unordered_map< not_null, base::flat_set>> _pollViews; + std::unordered_map< + not_null, + base::flat_set>> _todoListViews; std::unordered_map< UserId, base::flat_set>> _contactItems; @@ -1082,6 +1127,7 @@ private: base::flat_set> _webpagesUpdated; base::flat_set> _gamesUpdated; base::flat_set> _pollsUpdated; + base::flat_set> _todoListsUpdated; rpl::event_stream> _webpageUpdates; rpl::event_stream> _channelDifferenceTooLong; @@ -1119,6 +1165,13 @@ private: std::unordered_map> _peers; + std::optional, + ChannelId>> _postponedMonoforumLinkedIds; + + // This one from `channel`, not `channelFull`. + base::flat_map, int> _commonStarsPerMessage; + MessageIdsList _mimeForwardIds; std::weak_ptr _creditsSubsRebuilder; diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index 6834190bf7..5e0c3735d2 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -110,11 +110,13 @@ rpl::producer SharedMediaViewer( auto requestMediaAround = [ peer = session->data().peer(key.peerId), topicRootId = key.topicRootId, + monoforumPeerId = key.monoforumPeerId, type = key.type ](const SparseIdsSliceBuilder::AroundData &data) { peer->session().api().requestSharedMedia( peer, topicRootId, + monoforumPeerId, type, data.aroundId, data.direction); @@ -131,6 +133,7 @@ rpl::producer SharedMediaViewer( ) | rpl::filter([=](const SliceUpdate &update) { return (update.peerId == key.peerId) && (update.topicRootId == key.topicRootId) + && (update.monoforumPeerId == key.monoforumPeerId) && (update.type == key.type); }) | rpl::filter([=](const SliceUpdate &update) { return builder->applyUpdate(update.data); @@ -151,6 +154,8 @@ rpl::producer SharedMediaViewer( return (update.peerId == key.peerId) && (!update.topicRootId || update.topicRootId == key.topicRootId) + && (!update.monoforumPeerId + || update.monoforumPeerId == key.monoforumPeerId) && update.types.test(key.type); }) | rpl::filter([=] { return builder->removeAll(); @@ -236,6 +241,7 @@ rpl::producer SharedMediaMergedViewer( auto createSimpleViewer = [=]( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter) { @@ -244,6 +250,7 @@ rpl::producer SharedMediaMergedViewer( Storage::SharedMediaKey( peerId, topicRootId, + monoforumPeerId, key.type, simpleKey), limitBefore, diff --git a/Telegram/SourceFiles/data/data_shared_media.h b/Telegram/SourceFiles/data/data_shared_media.h index d9d2d8c63f..8f53b4026c 100644 --- a/Telegram/SourceFiles/data/data_shared_media.h +++ b/Telegram/SourceFiles/data/data_shared_media.h @@ -74,11 +74,13 @@ public: Key( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, PeerId migratedPeerId, Type type, UniversalMsgId universalId) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , migratedPeerId(migratedPeerId) , type(type) , universalId(universalId) { @@ -91,6 +93,7 @@ public: PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; PeerId migratedPeerId = 0; Type type = Type::kCount; UniversalMsgId universalId; @@ -120,6 +123,7 @@ public: return { key.peerId, key.topicRootId, + key.monoforumPeerId, key.migratedPeerId, v::is(key.universalId) ? v::get(key.universalId) @@ -130,6 +134,7 @@ public: return { key.peerId, key.topicRootId, + key.monoforumPeerId, key.migratedPeerId, ServerMaxMsgId - 1 }; diff --git a/Telegram/SourceFiles/data/data_sparse_ids.cpp b/Telegram/SourceFiles/data/data_sparse_ids.cpp index b48881ca3a..787c0148a3 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.cpp +++ b/Telegram/SourceFiles/data/data_sparse_ids.cpp @@ -377,7 +377,10 @@ rpl::producer SparseIdsMergedSlice::CreateViewer( int limitBefore, int limitAfter, Fn simpleViewer) { - Expects(!key.topicRootId || !key.migratedPeerId); + Expects(!key.topicRootId + || (!key.monoforumPeerId && !key.migratedPeerId)); + Expects(!key.monoforumPeerId + || (!key.topicRootId && !key.migratedPeerId)); Expects(IsServerMsgId(key.universalId) || (key.universalId == 0) || (IsServerMsgId(ServerMaxMsgId + key.universalId) && key.migratedPeerId != 0)); @@ -388,6 +391,7 @@ rpl::producer SparseIdsMergedSlice::CreateViewer( auto partViewer = simpleViewer( key.peerId, key.topicRootId, + key.monoforumPeerId, SparseIdsMergedSlice::PartKey(key), limitBefore, limitAfter @@ -405,6 +409,7 @@ rpl::producer SparseIdsMergedSlice::CreateViewer( auto migratedViewer = simpleViewer( key.migratedPeerId, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId SparseIdsMergedSlice::MigratedKey(key), limitBefore, limitAfter); diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index 6b762e2dbe..3328ee5a40 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -33,11 +33,13 @@ public: Key( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, PeerId migratedPeerId, UniversalMsgId universalId) : peerId(peerId) , topicRootId(topicRootId) - , migratedPeerId(topicRootId ? 0 : migratedPeerId) + , monoforumPeerId(monoforumPeerId) + , migratedPeerId((topicRootId || monoforumPeerId) ? 0 : migratedPeerId) , universalId(universalId) { } @@ -47,6 +49,7 @@ public: PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; PeerId migratedPeerId = 0; UniversalMsgId universalId = 0; }; @@ -72,6 +75,7 @@ public: using SimpleViewerFunction = rpl::producer( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter); diff --git a/Telegram/SourceFiles/data/data_thread.cpp b/Telegram/SourceFiles/data/data_thread.cpp index 1934c34507..47a8f22232 100644 --- a/Telegram/SourceFiles/data/data_thread.cpp +++ b/Telegram/SourceFiles/data/data_thread.cpp @@ -7,9 +7,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_thread.h" +#include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_changes.h" +#include "data/data_channel.h" #include "data/data_peer.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_unread_things.h" @@ -31,6 +35,20 @@ MsgId Thread::topicRootId() const { return MsgId(); } +PeerId Thread::monoforumPeerId() const { + if (const auto sublist = asSublist()) { + return sublist->sublistPeer()->id; + } + return PeerId(); +} + +PeerData *Thread::maybeSublistPeer() const { + if (const auto sublist = asSublist()) { + return sublist->sublistPeer(); + } + return nullptr; +} + not_null Thread::peer() const { return owningHistory()->peer; } @@ -80,6 +98,17 @@ HistoryUnreadThings::ConstProxy Thread::unreadReactions() const { }; } +bool Thread::canToggleUnread(bool nowUnread) const { + if ((asTopic() || asForum()) && !nowUnread) { + return false; + } else if (asSublist() && owningHistory()->peer->isSelf()) { + return false; + } else if (asHistory() && peer()->amMonoforumAdmin()) { + return false; + } + return true; +} + const base::flat_set &Thread::unreadMentionsIds() const { if (!_unreadThings) { static const auto Result = base::flat_set(); @@ -176,4 +205,16 @@ void Thread::setHasPinnedMessages(bool has) { EntryUpdate::Flag::HasPinnedMessages); } +void Thread::saveMeAsActiveSubsectionThread() { + if (const auto channel = owningHistory()->peer->asChannel()) { + if (channel->useSubsectionTabs()) { + if (const auto forum = channel->forum()) { + forum->saveActiveSubsectionThread(this); + } else if (const auto monoforum = channel->monoforum()) { + monoforum->saveActiveSubsectionThread(this); + } + } + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_thread.h b/Telegram/SourceFiles/data/data_thread.h index 9bbc6635fe..42fd2dca06 100644 --- a/Telegram/SourceFiles/data/data_thread.h +++ b/Telegram/SourceFiles/data/data_thread.h @@ -67,6 +67,8 @@ public: return const_cast(this)->owningHistory(); } [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] PeerId monoforumPeerId() const; + [[nodiscard]] PeerData *maybeSublistPeer() const; [[nodiscard]] not_null peer() const; [[nodiscard]] PeerNotifySettings ¬ify(); [[nodiscard]] const PeerNotifySettings ¬ify() const; @@ -78,6 +80,7 @@ public: [[nodiscard]] HistoryUnreadThings::ConstProxy unreadReactions() const; virtual void hasUnreadMentionChanged(bool has) = 0; virtual void hasUnreadReactionChanged(bool has) = 0; + bool canToggleUnread(bool nowUnread) const; void removeNotification(not_null item); void clearNotifications(); @@ -112,11 +115,13 @@ public: } [[nodiscard]] virtual auto sendActionPainter() - -> not_null = 0; + -> HistoryView::SendActionPainter* = 0; [[nodiscard]] bool hasPinnedMessages() const; void setHasPinnedMessages(bool has); + void saveMeAsActiveSubsectionThread(); + protected: void setUnreadMarkFlag(bool unread); diff --git a/Telegram/SourceFiles/data/data_todo_list.cpp b/Telegram/SourceFiles/data/data_todo_list.cpp new file mode 100644 index 0000000000..d50bcf6296 --- /dev/null +++ b/Telegram/SourceFiles/data/data_todo_list.cpp @@ -0,0 +1,233 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "data/data_todo_list.h" + +#include "api/api_text_entities.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "base/call_delayed.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "api/api_text_entities.h" +#include "ui/text/text_options.h" + +namespace { + +constexpr auto kShortPollTimeout = 30 * crl::time(1000); + +const TodoListItem *ItemById(const std::vector &list, int id) { + const auto i = ranges::find(list, id, &TodoListItem::id); + return (i != end(list)) ? &*i : nullptr; +} + +TodoListItem *ItemById(std::vector &list, int id) { + return const_cast(ItemById(std::as_const(list), id)); +} + +} // namespace + +TodoListData::TodoListData(not_null owner, TodoListId id) +: id(id) +, _owner(owner) { +} + +Data::Session &TodoListData::owner() const { + return *_owner; +} + +Main::Session &TodoListData::session() const { + return _owner->session(); +} + +bool TodoListData::applyChanges(const MTPDtodoList &todolist) { + const auto newTitle = TextWithEntities{ + .text = qs(todolist.vtitle().data().vtext()), + .entities = Api::EntitiesFromMTP( + &session(), + todolist.vtitle().data().ventities().v), + }; + const auto newFlags = (todolist.is_others_can_append() + ? Flag::OthersCanAppend + : Flag()) + | (todolist.is_others_can_complete() ? Flag::OthersCanComplete + : Flag()); + auto newItems = ranges::views::all( + todolist.vlist().v + ) | ranges::views::transform([&](const MTPTodoItem &item) { + return TodoListItemFromMTP(&session(), item); + }) | ranges::views::take( + kMaxOptions + ) | ranges::to_vector; + + const auto changed1 = (title != newTitle) || (_flags != newFlags); + const auto changed2 = (items != newItems); + if (!changed1 && !changed2) { + return false; + } + if (changed1) { + title = newTitle; + _flags = newFlags; + } + if (changed2) { + std::swap(items, newItems); + for (const auto &old : newItems) { + if (const auto current = itemById(old.id)) { + current->completedBy = old.completedBy; + current->completionDate = old.completionDate; + } + } + } + ++version; + return true; +} + +bool TodoListData::applyCompletions( + const MTPVector *completions) { + auto changed = false; + const auto lookup = [&](int id) { + if (!completions) { + return (const MTPDtodoCompletion*)nullptr; + } + const auto proj = [](const MTPTodoCompletion &completion) { + return completion.data().vid().v; + }; + const auto i = ranges::find(completions->v, id, proj); + return (i != completions->v.end()) ? &i->data() : nullptr; + }; + for (auto &item : items) { + const auto completion = lookup(item.id); + const auto by = (completion && completion->vcompleted_by().v) + ? owner().user(UserId(completion->vcompleted_by().v)).get() + : nullptr; + const auto date = completion ? completion->vdate().v : TimeId(); + if (item.completedBy != by || item.completionDate != date) { + item.completedBy = by; + item.completionDate = date; + changed = true; + } + } + if (changed) { + ++version; + } + return changed; +} + +void TodoListData::apply( + not_null item, + const MTPDmessageActionTodoCompletions &data) { + for (const auto &id : data.vcompleted().v) { + if (const auto task = itemById(id.v)) { + task->completedBy = item->from(); + task->completionDate = item->date(); + } + } + for (const auto &id : data.vincompleted().v) { + if (const auto task = itemById(id.v)) { + task->completedBy = nullptr; + task->completionDate = TimeId(); + } + } + owner().notifyTodoListUpdateDelayed(this); +} + +void TodoListData::apply(const MTPDmessageActionTodoAppendTasks &data) { + const auto limit = TodoListData::kMaxOptions; + for (const auto &task : data.vlist().v) { + if (items.size() < limit) { + const auto parsed = TodoListItemFromMTP( + &session(), + task); + if (!itemById(parsed.id)) { + items.push_back(std::move(parsed)); + } + } + } + owner().notifyTodoListUpdateDelayed(this); +} + +TodoListItem *TodoListData::itemById(int id) { + return ItemById(items, id); +} + +const TodoListItem *TodoListData::itemById(int id) const { + return ItemById(items, id); +} + +void TodoListData::setFlags(Flags flags) { + if (_flags != flags) { + _flags = flags; + ++version; + } +} + +TodoListData::Flags TodoListData::flags() const { + return _flags; +} + +bool TodoListData::othersCanAppend() const { + return (_flags & Flag::OthersCanAppend); +} + +bool TodoListData::othersCanComplete() const { + return (_flags & Flag::OthersCanComplete); +} + +MTPVector TodoListItemsToMTP( + not_null session, + const std::vector &tasks) { + const auto convert = [&](const TodoListItem &item) { + return MTP_todoItem( + MTP_int(item.id), + MTP_textWithEntities( + MTP_string(item.text.text), + Api::EntitiesToMTP(session, item.text.entities))); + }; + auto items = QVector(); + items.reserve(tasks.size()); + ranges::transform(tasks, ranges::back_inserter(items), convert); + return MTP_vector(items); +} + +MTPTodoList TodoListDataToMTP(not_null todolist) { + using Flag = MTPDtodoList::Flag; + const auto flags = Flag() + | (todolist->othersCanAppend() + ? Flag::f_others_can_append + : Flag()) + | (todolist->othersCanComplete() + ? Flag::f_others_can_complete + : Flag()); + return MTP_todoList( + MTP_flags(flags), + MTP_textWithEntities( + MTP_string(todolist->title.text), + Api::EntitiesToMTP( + &todolist->session(), + todolist->title.entities)), + TodoListItemsToMTP(&todolist->session(), todolist->items)); +} + +MTPInputMedia TodoListDataToInputMedia( + not_null todolist) { + return MTP_inputMediaTodo(TodoListDataToMTP(todolist)); +} + +TodoListItem TodoListItemFromMTP( + not_null session, + const MTPTodoItem &item) { + const auto &data = item.data(); + return { + .text = TextWithEntities{ + .text = qs(data.vtitle().data().vtext()), + .entities = Api::EntitiesFromMTP( + session, + data.vtitle().data().ventities().v), + }, + .id = data.vid().v, + }; +} diff --git a/Telegram/SourceFiles/data/data_todo_list.h b/Telegram/SourceFiles/data/data_todo_list.h new file mode 100644 index 0000000000..dd10b8878a --- /dev/null +++ b/Telegram/SourceFiles/data/data_todo_list.h @@ -0,0 +1,80 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +class Session; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +struct TodoListItem { + TextWithEntities text; + PeerData *completedBy = nullptr; + TimeId completionDate = 0; + int id = 0; + + friend inline bool operator==( + const TodoListItem &, + const TodoListItem &) = default; +}; + +struct TodoListData { + TodoListData(not_null owner, TodoListId id); + + [[nodiscard]] Data::Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + enum class Flag { + OthersCanAppend = 0x01, + OthersCanComplete = 0x02, + }; + friend inline constexpr bool is_flag_type(Flag) { return true; }; + using Flags = base::flags; + + bool applyChanges(const MTPDtodoList &todolist); + bool applyCompletions(const MTPVector *completions); + + void apply( + not_null item, + const MTPDmessageActionTodoCompletions &data); + void apply(const MTPDmessageActionTodoAppendTasks &data); + + [[nodiscard]] TodoListItem *itemById(int id); + [[nodiscard]] const TodoListItem *itemById(int id) const; + + void setFlags(Flags flags); + [[nodiscard]] Flags flags() const; + [[nodiscard]] bool othersCanAppend() const; + [[nodiscard]] bool othersCanComplete() const; + + TodoListId id; + TextWithEntities title; + std::vector items; + int version = 0; + + static constexpr auto kMaxOptions = 32; + +private: + const not_null _owner; + Flags _flags = Flags(); + +}; + +[[nodiscard]] MTPVector TodoListItemsToMTP( + not_null session, + const std::vector &tasks); +[[nodiscard]] MTPTodoList TodoListDataToMTP( + not_null todolist); +[[nodiscard]] MTPInputMedia TodoListDataToInputMedia( + not_null todolist); +[[nodiscard]] TodoListItem TodoListItemFromMTP( + not_null session, + const MTPTodoItem &item); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index b87e1dcb21..753b79d1a8 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -128,6 +128,7 @@ struct WebPageData; struct GameData; struct BotAppData; struct PollData; +struct TodoListData; using PhotoId = uint64; using VideoId = uint64; @@ -136,6 +137,7 @@ using DocumentId = uint64; using WebPageId = uint64; using GameId = uint64; using PollId = uint64; +using TodoListId = FullMsgId; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; @@ -352,6 +354,8 @@ enum class MessageFlag : uint64 { HideDisplayDate = (1ULL << 51), + StarsPaidSuggested = (1ULL << 52), + TonPaidSuggested = (1ULL << 53), AyuNoForwards = (1ULL << 63), }; @@ -385,8 +389,6 @@ struct ForwardDraft { const ForwardDraft&) = default; }; -using ForwardDrafts = base::flat_map; - struct ResolvedForwardDraft { HistoryItemsList items; ForwardOptions options = ForwardOptions::PreserveInfo; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 5cc7fe3084..84d74f25ac 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -733,6 +733,10 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { | Flag::CanPinMessages | Flag::VoiceMessagesForbidden | Flag::ReadDatesPrivate + | (update.is_contact_require_premium() + ? Flag::HasRequirePremiumToWrite + : Flag()) + | (user->starsPerMessage() ? Flag::HasStarsPerMessage : Flag()) | Flag::MessageMoneyRestrictionsKnown | Flag::RequiresPremiumToWrite; user->setFlags((user->flags() & ~mask) @@ -746,9 +750,10 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { ? Flag::VoiceMessagesForbidden : Flag()) | (update.is_read_dates_private() ? Flag::ReadDatesPrivate : Flag()) + | (user->starsPerMessage() ? Flag::HasStarsPerMessage : Flag()) | Flag::MessageMoneyRestrictionsKnown | (update.is_contact_require_premium() - ? Flag::RequiresPremiumToWrite + ? (Flag::RequiresPremiumToWrite | Flag::HasRequirePremiumToWrite) : Flag())); user->setIsBlocked(update.is_blocked()); user->setCallsStatus(update.is_phone_calls_private() @@ -802,14 +807,16 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { = std::make_shared(); const auto currencyLoad = currencyLoadLifetime->make_state(user); - const auto apply = [=](Data::EarnInt balance) { + const auto apply = [=](const CreditsAmount &balance) { if (const auto strong = weak.get()) { strong->credits().applyCurrency(id, balance); } currencyLoadLifetime->destroy(); }; currencyLoad->request() | rpl::start_with_error_done( - [=](const QString &error) { apply(0); }, + [=](const QString &error) { + apply(CreditsAmount(0, CreditsType::Ton)); + }, [=] { apply(currencyLoad->data().currentBalance); }, *currencyLoadLifetime); base::timer_once(kTimeout) | rpl::start_with_next([=] { @@ -882,9 +889,8 @@ StarRefProgram ParseStarRefProgram(const MTPStarRefProgram *program) { const auto &data = program->data(); result.commission = data.vcommission_permille().v; result.durationMonths = data.vduration_months().value_or_empty(); - result.revenuePerUser = data.vdaily_revenue_per_user() - ? Data::FromTL(*data.vdaily_revenue_per_user()) - : StarsAmount(); + result.revenuePerUser = CreditsAmountFromTL( + data.vdaily_revenue_per_user()); result.endDate = data.vend_date().value_or_empty(); return result; } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 5e57eaef90..a47340132f 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "data/components/credits.h" #include "data/data_birthday.h" #include "data/data_peer.h" @@ -28,7 +28,7 @@ using DisallowedGiftTypes = base::flags; } // namespace Api struct StarRefProgram { - StarsAmount revenuePerUser; + CreditsAmount revenuePerUser; TimeId endDate = 0; ushort commission = 0; uint8 durationMonths = 0; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 74db4d6b5a..508a45d7c0 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -372,7 +372,7 @@ void WebPageData::ApplyChanges( }, [&](const auto &) { }); } - session->data().sendWebPageGamePollNotifications(); + session->data().sendWebPageGamePollTodoListNotifications(); } QString WebPageData::displayedSiteName() const { diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index 3029f2ee5d..c11a8e255c 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -789,7 +789,7 @@ void Stickers::somethingReceived( void Stickers::setPackAndEmoji( StickersSet &set, StickersPack &&pack, - const std::vector &&dates, + std::vector &&dates, const QVector &packs) { set.stickers = std::move(pack); set.dates = std::move(dates); diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.h b/Telegram/SourceFiles/data/stickers/data_stickers.h index affff47b8a..cc2c7fbe4f 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers.h @@ -291,7 +291,7 @@ private: void setPackAndEmoji( StickersSet &set, StickersPack &&pack, - const std::vector &&dates, + std::vector &&dates, const QVector &packs); void somethingReceived( const QVector &list, diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 9a16777d3d..6780fefa54 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -500,8 +500,8 @@ dialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) } dialogsSearchInHeight: 38px; -dialogsSearchInPhotoSize: 26px; -dialogsSearchInPhotoPadding: 12px; +dialogsSearchInPhotoSize: 28px; +dialogsSearchInPhotoPadding: 10px; dialogsSearchInSkip: 10px; dialogsSearchInNameTop: 9px; dialogsSearchInDownTop: 15px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_common.h b/Telegram/SourceFiles/dialogs/dialogs_common.h index c1e1cd755d..34b844d296 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_common.h +++ b/Telegram/SourceFiles/dialogs/dialogs_common.h @@ -93,6 +93,9 @@ struct BadgesState { friend inline constexpr auto operator<=>( BadgesState, BadgesState) = default; + friend inline constexpr bool operator==( + BadgesState, + BadgesState) = default; [[nodiscard]] bool empty() const { return !unread && !mention && !reaction; diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index 501883a416..f863378136 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -84,9 +84,9 @@ Entry::Entry(not_null owner, Type type) , _flags((type == Type::History) ? (Flag::IsThread | Flag::IsHistory) : (type == Type::ForumTopic) - ? Flag::IsThread + ? (Flag::IsThread | Flag::IsForumTopic) : (type == Type::SavedSublist) - ? Flag::IsSavedSublist + ? (Flag::IsThread | Flag::IsSavedSublist) : Flag(0)) { } @@ -113,7 +113,7 @@ Data::Forum *Entry::asForum() { } Data::Folder *Entry::asFolder() { - return (_flags & (Flag::IsThread | Flag::IsSavedSublist)) + return (_flags & Flag::IsThread) ? nullptr : static_cast(this); } @@ -125,7 +125,7 @@ Data::Thread *Entry::asThread() { } Data::ForumTopic *Entry::asTopic() { - return ((_flags & Flag::IsThread) && !(_flags & Flag::IsHistory)) + return (_flags & Flag::IsForumTopic) ? static_cast(this) : nullptr; } @@ -229,6 +229,13 @@ uint64 Entry::computeSortPosition(FilterId filterId) const { } void Entry::updateChatListExistence() { + if (const auto history = asHistory()) { + if (const auto channel = history->peer->asMonoforum()) { + if (!folderKnown()) { + history->clearFolder(); + } + } + } setChatListExistence(shouldBeInChatList()); } @@ -280,6 +287,10 @@ void Entry::notifyUnreadStateChange(const UnreadState &wasState) { } } } + } else if (const auto sublist = asSublist()) { + session().changes().sublistUpdated( + sublist, + Data::SublistUpdate::Flag::UnreadView); } updateChatListEntryPostponed(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h index e52b45048d..8838bd05fb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.h +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h @@ -27,6 +27,7 @@ class Forum; class Folder; class ForumTopic; class SavedSublist; +class SavedMessages; class Thread; } // namespace Data @@ -168,9 +169,10 @@ private: enum class Flag : uchar { IsThread = (1 << 0), IsHistory = (1 << 1), - IsSavedSublist = (1 << 2), - UpdatePostponed = (1 << 3), - InUnreadChangeBlock = (1 << 4), + IsForumTopic = (1 << 2), + IsSavedSublist = (1 << 3), + UpdatePostponed = (1 << 4), + InUnreadChangeBlock = (1 << 5), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index e0c77bfe96..db2dcd57aa 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_search_tags.h" #include "dialogs/dialogs_quick_action.h" #include "history/view/history_view_context_menu.h" +#include "history/view/history_view_subsection_tabs.h" #include "history/history.h" #include "history/history_item.h" #include "core/application.h" @@ -370,7 +371,9 @@ InnerWidget::InnerWidget( rpl::merge( session().settings().archiveCollapsedChanges() | rpl::map_to(false), - session().data().chatsFilters().changed() | rpl::map_to(true) + session().data().chatsFilters().changed() | rpl::map_to(true), + session().data().chatsFilters().tagsEnabledChanges( + ) | rpl::map_to(true) ) | rpl::start_with_next([=](bool refreshHeight) { if (refreshHeight) { _chatsFilterTags.clear(); @@ -383,11 +386,8 @@ InnerWidget::InnerWidget( }, lifetime()); session().data().chatsFilters().tagsEnabledValue( - ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool tags) { + ) | rpl::start_with_next([=](bool tags) { _handleChatListEntryTagRefreshesLifetime.destroy(); - if (_shownList->updateHeights(_narrowRatio)) { - refresh(); - } if (!tags) { return; } @@ -531,7 +531,13 @@ InnerWidget::InnerWidget( RowDescriptor previous, RowDescriptor next) { updateDialogRow(previous); + if (const auto sublist = previous.key.sublist()) { + updateDialogRow({ { sublist->owningHistory() }, {} }); + } updateDialogRow(next); + if (const auto sublist = next.key.sublist()) { + updateDialogRow({ { sublist->owningHistory() }, {} }); + } }, lifetime()); _controller->activeChatsFilter( @@ -789,7 +795,7 @@ void InnerWidget::showSavedSublists() { Expects(!_geometryInited); Expects(!_savedSublists); - _savedSublists = true; + _savedSublists = &session().data().savedMessages(); stopReorderPinned(); clearSelection(); @@ -852,10 +858,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { const auto active = mayBeActive && isRowActive(row, activeEntry); const auto history = key.history(); const auto forum = history && history->isForum(); - if (forum && !_topicJumpCache) { + const auto monoforum = history && history->amMonoforumAdmin(); + if ((forum || monoforum) && !_topicJumpCache) { _topicJumpCache = std::make_unique(); } - const auto expanding = forum + const auto expanding = (forum || monoforum) && (history->peer->id == childListShown.peerId); context.rightButton = maybeCacheRightButton(row); if (history) { @@ -881,14 +888,14 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } } - context.st = (forum ? &st::forumDialogRow : _st.get()); + context.st = (forum || monoforum) ? &st::forumDialogRow : _st.get(); auto chatsFilterTags = std::vector(); if (context.narrow) { context.chatsFilterTags = nullptr; } else if (row->entry()->hasChatsFilterTags(context.filter)) { const auto a = active; - context.st = forum + context.st = (forum || monoforum) ? &st::taggedForumDialogRow : &st::taggedDialogRow; auto availableWidth = context.width @@ -1458,8 +1465,18 @@ bool InnerWidget::isRowActive( not_null row, const RowDescriptor &entry) const { const auto key = row->key(); - return (entry.key == key) - || (entry.key.sublist() && key.peer() && key.peer()->isSelf()); + if (entry.key == key) { + return true; + } else if (const auto topic = entry.key.topic()) { + if (const auto history = key.history()) { + return (history->peer == topic->channel()) + && HistoryView::SubsectionTabs::UsedFor(history); + } + return false; + } else if (const auto sublist = entry.key.sublist()) { + return key.history() && key.history() == sublist->owningHistory(); + } + return false; } bool InnerWidget::isSearchResultActive( @@ -1559,6 +1576,11 @@ void InnerWidget::paintPeerSearchResult( : context.selected ? &st::dialogsScamFgOver : &st::dialogsScamFg), + .direct = (context.active + ? &st::dialogsDraftFgActive + : context.selected + ? &st::windowSubTextFgOver + : &st::windowSubTextFg), .premiumFg = (context.active ? &st::dialogsVerifiedIconBgActive : context.selected @@ -1902,8 +1924,13 @@ RowDescriptor InnerWidget::computeChatPreviewRow() const { if (const auto peer = result.key.peer()) { const auto topicId = _pressedTopicJump ? _pressedTopicJumpRootId - : 0; - if (const auto topic = peer->forumTopicFor(topicId)) { + : MsgId(); + const auto sublistPeerId = _pressedTopicJump + ? _pressedSublistJumpPeerId + : PeerId(); + if (const auto sublist = peer->monoforumSublistFor(sublistPeerId)) { + return { sublist, FullMsgId() }; + } else if (const auto topic = peer->forumTopicFor(topicId)) { return { topic, FullMsgId() }; } } @@ -2127,7 +2154,7 @@ bool InnerWidget::addQuickActionRipple( const std::vector &InnerWidget::pinnedChatsOrder() const { const auto owner = &session().data(); return _savedSublists - ? owner->pinnedChatsOrder(&owner->savedMessages()) + ? owner->pinnedChatsOrder(_savedSublists) : _openedForum ? owner->pinnedChatsOrder(_openedForum) : _filterId @@ -2191,6 +2218,9 @@ int InnerWidget::countPinnedIndex(Row *ofRow) { } void InnerWidget::savePinnedOrder() { + if (_savedSublists && _savedSublists->parentChat()) { + return; + } const auto &newOrder = pinnedChatsOrder(); if (newOrder.size() != _pinnedOnDragStart.size()) { return; // Something has changed in the set of pinned chats. @@ -2328,8 +2358,11 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { const auto delta = [&] { if (localPosition.y() < _visibleTop) { return localPosition.y() - _visibleTop; - } else if ((_savedSublists || _openedFolder || _openedForum || _filterId) - && localPosition.y() > _visibleBottom) { + } else if ((localPosition.y() > _visibleBottom) + && (_savedSublists + || _openedFolder + || _openedForum + || _filterId)) { return localPosition.y() - _visibleBottom; } return 0; @@ -2408,6 +2441,7 @@ void InnerWidget::mousePressReleased( auto collapsedPressed = _collapsedPressed; setCollapsedPressed(-1); const auto pressedTopicRootId = _pressedTopicJumpRootId; + const auto pressedSublistPeerId = _pressedSublistJumpPeerId; const auto pressedTopicJump = _pressedTopicJump; const auto pressedRightButton = _pressedRightButton; auto pressed = _pressed; @@ -2491,7 +2525,10 @@ void InnerWidget::mousePressReleased( } else if (pressedRightButton && peerSearchPressed >= 0) { showSponsoredMenu(peerSearchPressed, globalPosition); } else { - chooseRow(modifiers, pressedTopicRootId); + chooseRow( + modifiers, + pressedTopicRootId, + pressedSublistPeerId); } } } @@ -2543,6 +2580,9 @@ void InnerWidget::setPressed( : nullptr; const auto item = history ? history->chatListMessage() : nullptr; _pressedTopicJumpRootId = item ? item->topicRootId() : MsgId(); + _pressedSublistJumpPeerId = item + ? item->sublistPeerId() + : PeerId(); } } } @@ -2589,6 +2629,9 @@ void InnerWidget::setFilteredPressed( : nullptr; const auto item = history ? history->chatListMessage() : nullptr; _pressedTopicJumpRootId = item ? item->topicRootId() : MsgId(); + _pressedSublistJumpPeerId = item + ? item->sublistPeerId() + : PeerId(); } } } @@ -2697,8 +2740,8 @@ void InnerWidget::handleChatListEntryRefreshes() { return false; } else if (const auto topic = event.key.topic()) { return (topic->forum() == _openedForum); - } else if (event.key.sublist()) { - return _savedSublists; + } else if (const auto sublist = event.key.sublist()) { + return sublist->parent() == _savedSublists; } else { return !_openedForum; } @@ -2716,7 +2759,7 @@ void InnerWidget::handleChatListEntryRefreshes() { && (key.topic() ? (key.topic()->forum() == _openedForum) : key.sublist() - ? _savedSublists + ? (key.sublist()->parent() == _savedSublists) : (entry->folder() == _openedFolder))) { _dialogMoved.fire({ from, to }); } @@ -2921,7 +2964,8 @@ void InnerWidget::enterEventHook(QEnterEvent *e) { Row *InnerWidget::shownRowByKey(Key key) { const auto entry = key.entry(); if (_savedSublists) { - if (!entry->asSublist()) { + const auto sublist = entry->asSublist(); + if (!sublist || sublist->parent() != _savedSublists) { return nullptr; } } else if (_openedForum) { @@ -2990,7 +3034,7 @@ void InnerWidget::updateSelectedRow(Key key) { void InnerWidget::refreshShownList() { const auto list = _savedSublists - ? session().data().savedMessages().chatsList()->indexed() + ? _savedSublists->chatsList()->indexed() : _openedForum ? _openedForum->topicsList()->indexed() : _filterId @@ -3394,6 +3438,9 @@ void InnerWidget::applySearchState(SearchState state) { ) | rpl::start_with_next([=] { refresh(); moveSearchIn(); + if (_loadingAnimation) { + _loadingAnimation->move(0, searchedOffset()); + } }, _searchTags->lifetime()); } else { _searchTags = nullptr; @@ -3402,7 +3449,7 @@ void InnerWidget::applySearchState(SearchState state) { _searchFromShown = ignoreInChat ? nullptr : sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : state.fromPeer; if (state.inChat) { onHashtagFilterUpdate(QStringView()); @@ -3449,8 +3496,7 @@ void InnerWidget::applySearchState(SearchState state) { }; if (_searchState.filterChatsList() && !words.isEmpty()) { if (_savedSublists) { - const auto owner = &session().data(); - append(owner->savedMessages().chatsList()->indexed()); + append(_savedSublists->chatsList()->indexed()); } else if (_openedForum) { append(_openedForum->topicsList()->indexed()); } else { @@ -4021,7 +4067,7 @@ void InnerWidget::refreshEmpty() { const auto state = !_shownList->empty() ? EmptyState::None : _savedSublists - ? (data->savedMessages().chatsList()->loaded() + ? (_savedSublists->chatsList()->loaded() ? EmptyState::EmptySavedSublists : EmptyState::Loading) : _openedForum @@ -4162,12 +4208,20 @@ void InnerWidget::updateSearchIn() { : _openedForum ? _openedForum->channel().get() : nullptr; + const auto paused = [window = _controller] { + return window->isGifPausedAtLeastFor(Window::GifPauseReason::Any); + }; + const auto textFg = [] { + return st::windowSubTextFg->c; + }; const auto topicIcon = !topic ? nullptr : topic->iconId() ? Ui::MakeEmojiThumbnail( &topic->owner(), - Data::SerializeCustomEmojiId(topic->iconId())) + Data::SerializeCustomEmojiId(topic->iconId()), + paused, + textFg) : Ui::MakeEmojiThumbnail( &topic->owner(), Data::TopicIconEmojiEntity({ @@ -4177,11 +4231,13 @@ void InnerWidget::updateSearchIn() { .colorId = (topic->isGeneral() ? Data::ForumGeneralIconColor(st::windowSubTextFg->c) : topic->colorId()), - })); + }), + paused, + textFg); const auto peerIcon = peer ? Ui::MakeUserpicThumbnail(peer) : sublist - ? Ui::MakeUserpicThumbnail(sublist->peer()) + ? Ui::MakeUserpicThumbnail(sublist->sublistPeer()) : nullptr; const auto myIcon = Ui::MakeIconThumbnail(st::menuIconChats); const auto publicIcon = (_searchHashOrCashtag != HashOrCashtag::None) @@ -4736,7 +4792,8 @@ bool InnerWidget::isUserpicPressOnWide() const { bool InnerWidget::chooseRow( Qt::KeyboardModifiers modifiers, - MsgId pressedTopicRootId) { + MsgId pressedTopicRootId, + PeerId pressedSublistPeerId) { if (chooseHashtag()) { return true; } else if (_selectedMorePosts) { @@ -4778,12 +4835,9 @@ bool InnerWidget::chooseRow( if (!chosen.message.fullId) { if (const auto history = chosen.key.history()) { if (history->peer->forum()) { - if (pressedTopicRootId) { - chosen.message.fullId = { - history->peer->id, - pressedTopicRootId, - }; - } + chosen.topicJumpRootId = pressedTopicRootId; + } else if (history->peer->amMonoforumAdmin()) { + chosen.sublistJumpPeerId = pressedSublistPeerId; } } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 9842faf67a..c3c4a6b033 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -58,6 +58,7 @@ class ChatFilter; class Thread; class Folder; class Forum; +class SavedMessages; struct ReactionId; } // namespace Data @@ -83,6 +84,8 @@ enum class ChatTypeFilter : uchar; struct ChosenRow { Key key; Data::MessagePosition message; + MsgId topicJumpRootId; + PeerId sublistJumpPeerId; QByteArray sponsoredRandomId; bool userpicClick : 1 = false; bool filteredRow : 1 = false; @@ -162,7 +165,8 @@ public: void chatPreviewShown(bool shown, RowDescriptor row = {}); bool chooseRow( Qt::KeyboardModifiers modifiers = {}, - MsgId pressedTopicRootId = {}); + MsgId pressedTopicRootId = {}, + PeerId pressedSublistPeerId = {}); void scrollToEntry(const RowDescriptor &entry); @@ -542,6 +546,7 @@ private: Row *_selected = nullptr; Row *_pressed = nullptr; MsgId _pressedTopicJumpRootId; + PeerId _pressedSublistJumpPeerId; bool _selectedTopicJump = false; bool _pressedTopicJump = false; @@ -668,7 +673,8 @@ private: float64 _narrowRatio = 0.; bool _geometryInited = false; - bool _savedSublists = false; + Data::SavedMessages *_savedSublists = nullptr; + bool _searchLoading = false; bool _searchWaiting = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 58b0284dcc..41c25beff6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -113,6 +113,7 @@ struct EntryState { Replies, SavedSublist, ContextMenu, + SubsectionTabsMenu, ShortcutMessages, }; @@ -120,6 +121,7 @@ struct EntryState { Section section = Section::History; FilterId filterId = 0; FullReplyTo currentReplyTo; + SuggestPostOptions currentSuggest; friend inline auto operator<=>( const EntryState&, diff --git a/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp index f9480a818b..5440f38843 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp @@ -28,7 +28,7 @@ MainList::MainList( std::move( pinnedLimit ) | rpl::start_with_next([=](int limit) { - _pinned.setLimit(limit); + _pinned.setLimit(std::max(limit, 1)); }, _lifetime); session->changes().realtimeNameUpdates( diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index e8f745d139..c2dab42dc3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -324,7 +324,9 @@ Row::~Row() { void Row::recountHeight(float64 narrowRatio, FilterId filterId) { if (const auto history = _id.history()) { const auto hasTags = _id.entry()->hasChatsFilterTags(filterId); - _height = history->isForum() + const auto wideRow = history->isForum() + || history->amMonoforumAdmin(); + _height = wideRow ? anim::interpolate( hasTags ? st::taggedForumDialogRow.height @@ -470,7 +472,7 @@ void Row::PaintCornerBadgeFrame( for (auto i = 0; i != storiesUnreadCount; ++i) { segments.push_back({ storiesUnreadBrush, storiesUnread }); } - if (peer && peer->forum()) { + if (peer && (peer->forum() || peer->monoforum())) { const auto radius = context.st->photoSize * Ui::ForumUserpicRadiusMultiplier(); Ui::PaintOutlineSegments(q, outline, radius, segments); diff --git a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp index 2b019dbc9e..b4f028eb12 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/ui_integration.h" #include "data/data_birthday.h" #include "data/data_changes.h" +#include "data/data_peer_values.h" // Data::AmPremiumValue. #include "data/data_session.h" #include "data/data_user.h" #include "dialogs/ui/dialogs_top_bar_suggestion_content.h" @@ -204,32 +205,22 @@ rpl::producer*> TopBarSuggestionValue( repeat(repeat); }); - const auto fontH = content->contentTitleSt().font->height; - auto customEmojiFactory = [=]( - QStringView data, - const Ui::Text::MarkedContext &context - ) -> std::unique_ptr { - return Ui::MakeCreditsIconEmoji(fontH, 1); - }; - using namespace Ui::Text; - auto context = MarkedContext{ - .customEmojiFactory = std::move(customEmojiFactory), - }; - content->setContent( tr::lng_dialogs_suggestions_credits_sub_low_title( tr::now, lt_count, float64(needed - whole), lt_emoji, - Ui::Text::SingleCustomEmoji(Ui::kCreditsCurrency), + Ui::MakeCreditsIconEntity(), lt_channels, { peers }, Ui::Text::Bold), tr::lng_dialogs_suggestions_credits_sub_low_about( tr::now, TextWithEntities::Simple), - std::move(context)); + Ui::MakeCreditsIconContext( + content->contentTitleSt().font->height, + 1)); state->desiredWrapToggle.force_assign( Toggle{ true, anim::type::normal }); }; @@ -292,7 +283,7 @@ rpl::producer*> TopBarSuggestionValue( ? tr::lng_dialogs_suggestions_birthday_contact_title( tr::now, lt_text, - { first->name() }, + { first->shortName() }, Ui::Text::RichLangValue) : tr::lng_dialogs_suggestions_birthday_contacts_title( tr::now, @@ -584,7 +575,10 @@ rpl::producer*> TopBarSuggestionValue( (was == now) ? toggle.type : anim::type::instant); }, lifetime); - session->promoSuggestions().value() | rpl::start_with_next([=] { + rpl::merge( + session->promoSuggestions().value(), + Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty + ) | rpl::start_with_next([=] { const auto was = state->wrap; processCurrentSuggestion(processCurrentSuggestion); if (was != state->wrap) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 348808135e..0c77f7eedd 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -21,10 +21,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_key.h" #include "history/history.h" #include "history/history_item.h" -#include "history/view/history_view_top_bar_widget.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_contact_status.h" -#include "history/view/history_view_requests_bar.h" #include "history/view/history_view_group_call_bar.h" +#include "history/view/history_view_requests_bar.h" +#include "history/view/history_view_top_bar_widget.h" #include "boxes/peers/edit_peer_requests_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -78,6 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_download_manager.h" #include "data/data_chat_filters.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" @@ -864,7 +866,10 @@ void Widget::chosenRow(const ChosenRow &row) { const auto history = row.key.history(); const auto topicJump = history - ? history->peer->forumTopicFor(row.message.fullId.msg) + ? history->peer->forumTopicFor(row.topicJumpRootId) + : nullptr; + const auto sublistJump = history + ? history->peer->monoforumSublistFor(row.sublistJumpPeerId) : nullptr; if (topicJump) { @@ -873,7 +878,8 @@ void Widget::chosenRow(const ChosenRow &row) { } else if (row.newWindow) { controller()->showInNewWindow(Window::SeparateId(topicJump)); } else { - if (!controller()->adaptive().isOneColumn()) { + if (!controller()->adaptive().isOneColumn() + && !topicJump->channel()->useSubsectionTabs()) { controller()->showForum( topicJump->forum(), Window::SectionShow().withChildColumn()); @@ -884,6 +890,16 @@ void Widget::chosenRow(const ChosenRow &row) { Window::SectionShow::Way::ClearStack); } return; + } else if (sublistJump) { + if (row.newWindow) { + controller()->showInNewWindow(Window::SeparateId(sublistJump)); + } else { + controller()->showThread( + sublistJump, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } + return; } else if (const auto topic = row.key.topic()) { auto params = Window::SectionShow( Window::SectionShow::Way::ClearStack); @@ -911,18 +927,23 @@ void Widget::chosenRow(const ChosenRow &row) { && history->isForum() && !row.message.fullId && (!controller()->adaptive().isOneColumn() - || !history->peer->forum()->channel()->viewForumAsMessages())) { + || !history->peer->forum()->channel()->viewForumAsMessages() + || history->peer->forum()->channel()->useSubsectionTabs())) { const auto forum = history->peer->forum(); if (controller()->shownForum().current() == forum) { controller()->closeForum(); } else if (row.newWindow) { - controller()->showInNewWindow( - Window::SeparateId(Window::SeparateType::Forum, history)); + const auto type = forum->channel()->useSubsectionTabs() + ? Window::SeparateType::Chat + : Window::SeparateType::Forum; + controller()->showInNewWindow(Window::SeparateId(type, history)); } else { controller()->showForum( forum, - Window::SectionShow().withChildColumn()); - if (forum->channel()->viewForumAsMessages()) { + Window::SectionShow( + Window::SectionShow::Way::ClearStack).withChildColumn()); + if (controller()->shownForum().current() == forum + && forum->channel()->viewForumAsMessages()) { controller()->showThread( history, ShowAtUnreadMsgId, @@ -930,6 +951,26 @@ void Widget::chosenRow(const ChosenRow &row) { } } return; + } else if (history + && history->amMonoforumAdmin() + && !row.message.fullId) { + const auto monoforum = history->peer->monoforum(); + if (row.newWindow) { + controller()->showInNewWindow( + Window::SeparateId(Window::SeparateType::Chat, history)); + } else { + if (const auto active = monoforum->activeSubsectionThread()) { + controller()->showThread( + active, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } else { + controller()->showPeerHistory( + history, + Window::SectionShow::Way::ClearStack); + } + } + return; } else if (history) { const auto peer = history->peer; const auto showAtMsgId = controller()->uniqueChatsInSearchResults() @@ -1108,7 +1149,7 @@ void Widget::updateFrozenAccountBar() { void Widget::updateTopBarSuggestions() { if (_topBarSuggestion) { - _openedFolderOrForumChanges.fire(_openedForum || _openedFolder); + _openedFolderOrForumChanges.fire(_openedFolder || _openedForum); } } @@ -1964,7 +2005,7 @@ void Widget::refreshTopBars() { ? Dialogs::Key(history) : Dialogs::Key(_openedFolder)), .section = Dialogs::EntryState::Section::ChatsList, - }, history ? history->sendActionPainter().get() : nullptr); + }, history ? history->sendActionPainter() : nullptr); if (_forumSearchRequested) { showSearchInTopBar(anim::type::instant); } @@ -2122,6 +2163,10 @@ bool Widget::searchHasFocus() const { return _searchHasFocus; } +Data::Forum *Widget::openedForum() const { + return _openedForum; +} + void Widget::jumpToTop(bool belowPinned) { if (session().supportMode()) { return; @@ -2597,7 +2642,7 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { : _searchState.inChat.sublist(); const auto fromPeer = sublist ? nullptr : _searchQueryFrom; const auto savedPeer = sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : nullptr; _historiesRequest = histories.sendRequest(history, type, [=]( Fn finish) { @@ -2773,7 +2818,7 @@ void Widget::searchMore() { : _searchState.inChat.sublist(); const auto fromPeer = sublist ? nullptr : _searchQueryFrom; const auto savedPeer = sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : nullptr; _historiesRequest = histories.sendRequest(history, type, [=]( Fn finish) { @@ -3479,7 +3524,10 @@ bool Widget::applySearchState(SearchState state) { showSearchInTopBar(anim::type::normal); } else if (_layout == Layout::Main) { _forumSearchRequested = true; - controller()->showForum(forum); + auto params = Window::SectionShow( + Window::SectionShow::Way::ClearStack); + params.forceTopicsList = true; + controller()->showForum(forum, params); } else { return false; } @@ -4176,7 +4224,7 @@ PeerData *Widget::searchInPeer() const { : _openedForum ? _openedForum->channel().get() : _searchState.inChat.sublist() - ? session().user().get() + ? _searchState.inChat.sublist()->owningHistory()->peer.get() : _searchState.inChat.peer(); } @@ -4308,6 +4356,19 @@ bool Widget::cancelSearch(CancelSearchOptions options) { } } updateForceDisplayWide(); + if (clearingInChat) { + if (const auto forum = controller()->shownForum().current()) { + if (forum->channel()->useSubsectionTabs()) { + const auto id = controller()->windowId(); + const auto initial = id.forum(); + if (!initial) { + controller()->closeForum(); + } else if (initial != forum) { + controller()->showForum(initial); + } + } + } + } return clearingQuery || clearingInChat || clearSearchFocus; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index a38ce878f8..ab4685101f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -111,6 +111,8 @@ public: void setInnerFocus(bool unfocusSearch = false); [[nodiscard]] bool searchHasFocus() const; + [[nodiscard]] Data::Forum *openedForum() const; + void jumpToTop(bool belowPinned = false); void raiseWithTooltip(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index b27f692392..1c6a0df81b 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -66,12 +66,12 @@ const auto kPsaBadgePrefix = "cloud_lng_badge_psa_"; [[nodiscard]] bool ShowSendActionInDialogs(Data::Thread *thread) { const auto history = thread ? thread->owningHistory().get() : nullptr; - if (!history) { + if (!history || thread->asSublist()) { return false; } else if (const auto user = history->peer->asUser()) { return !user->lastseen().isHidden(); } - return !history->isForum(); + return !history->isForum() && !history->amMonoforumAdmin(); } void PaintRowTopRight( @@ -462,7 +462,9 @@ void PaintRow( const auto promoted = (history && history->useTopPromotion()) && !context.search; - const auto verifyInfo = (from && !from->isSelf()) + const auto verifyInfo = (from + && (!from->isSelf() + || (!(flags & Flag::SavedMessages) && !(flags & Flag::MyNotes)))) ? from->botVerifyDetails() : nullptr; if (promoted) { @@ -760,6 +762,11 @@ void PaintRow( : context.selected ? &st::dialogsScamFgOver : &st::dialogsScamFg), + .direct = (context.active + ? &st::dialogsDraftFgActive + : context.selected + ? &st::windowSubTextFgOver + : &st::windowSubTextFg), .premiumFg = (context.active ? &st::dialogsVerifiedIconBgActive : context.selected @@ -938,7 +945,7 @@ const style::icon *ChatTypeIcon( st::dialogsForumIcon, context.active, context.selected); - } else { + } else if (!peer->isMonoforum()) { return &ThreeStateIcon( st::dialogsChatIcon, context.active, @@ -972,10 +979,12 @@ void RowPainter::Paint( if (!thread) { return nullptr; } - if ((!peer || !peer->isForum()) && (!item || !badgesState.unread)) { + if ((!peer || (!peer->isForum() && !peer->amMonoforumAdmin())) + && (!item || !badgesState.unread)) { // Draw item, if there are unread messages. const auto draft = thread->owningHistory()->cloudDraft( - thread->topicRootId()); + thread->topicRootId(), + thread->monoforumPeerId()); if (!Data::DraftIsNull(draft)) { return draft; } @@ -1004,11 +1013,11 @@ void RowPainter::Paint( ? history->peer->migrateTo() : history->peer.get()) : sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : nullptr; const auto allowUserOnline = true;// !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) - | ((sublist && from->isSelf()) + | ((sublist && !sublist->parentChat() && from->isSelf()) ? Flag::MyNotes : (peer && peer->isSelf()) ? Flag::SavedMessages @@ -1056,21 +1065,23 @@ void RowPainter::Paint( ? nullptr : thread ? &thread->lastItemDialogsView() - : sublist - ? &sublist->lastItemDialogsView() : nullptr; if (view) { - const auto forum = context.st->topicsHeight - ? row->history()->peer->forum() + const auto forum = (peer && context.st->topicsHeight) + ? peer->forum() : nullptr; - if (!view->prepared(item, forum)) { + const auto monoforum = (peer && context.st->topicsHeight) + ? peer->monoforum() + : nullptr; + if (!view->prepared(item, forum, monoforum)) { view->prepare( item, forum, + monoforum, [=] { entry->updateChatListEntry(); }, {}); } - if (forum) { + if (forum || monoforum) { rect.setHeight(context.st->topicsHeight + rect.height()); } view->paint(p, rect, context); @@ -1164,8 +1175,13 @@ void RowPainter::Paint( availableWidth, st::dialogsTextFont->height); auto &view = row->itemView(); - if (!view.prepared(item, nullptr)) { - view.prepare(item, nullptr, row->repaint(), previewOptions); + if (!view.prepared(item, nullptr, nullptr)) { + view.prepare( + item, + nullptr, + nullptr, + row->repaint(), + previewOptions); } view.paint(p, itemRect, context); }; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index 5ed84ef384..71859a700e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -138,26 +138,39 @@ bool MessageView::dependsOn(not_null item) const { bool MessageView::prepared( not_null item, - Data::Forum *forum) const { + Data::Forum *forum, + Data::SavedMessages *monoforum) const { return (_textCachedFor == item.get()) - && (!forum + && ((!forum && !monoforum) || (_topics && _topics->forum() == forum + && _topics->monoforum() == monoforum && _topics->prepared())); } void MessageView::prepare( not_null item, Data::Forum *forum, + Data::SavedMessages *monoforum, Fn customEmojiRepaint, ToPreviewOptions options) { - if (!forum) { + if (!forum && !monoforum) { _topics = nullptr; - } else if (!_topics || _topics->forum() != forum) { - _topics = std::make_unique(forum); - _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else if (!_topics + || _topics->forum() != forum + || _topics->monoforum() != monoforum) { + _topics = std::make_unique(forum, monoforum); + if (forum) { + _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else { + _topics->prepare(item->sublistPeerId(), customEmojiRepaint); + } } else if (!_topics->prepared()) { - _topics->prepare(item->topicRootId(), customEmojiRepaint); + if (forum) { + _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else { + _topics->prepare(item->sublistPeerId(), customEmojiRepaint); + } } if (_textCachedFor == item.get()) { return; @@ -254,11 +267,18 @@ int MessageView::countWidth() const { auto result = 0; if (!_senderCache.isEmpty()) { result += _senderCache.maxWidth(); - if (!_imagesCache.empty()) { + if (!_imagesCache.empty() && !_leftIcon) { result += st::dialogsMiniPreviewSkip + st::dialogsMiniPreviewRight; } } + if (_leftIcon) { + const auto w = _leftIcon->icon.icon.width(); + result += w + + (_imagesCache.empty() + ? _leftIcon->skipText + : _leftIcon->skipMedia); + } if (!_imagesCache.empty()) { result += (_imagesCache.size() * (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip)) diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h index 4ee12aebd3..1cbe888a72 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h @@ -24,6 +24,7 @@ class SpoilerAnimation; namespace Data { class Forum; +class SavedMessages; } // namespace Data namespace HistoryView { @@ -56,10 +57,12 @@ public: [[nodiscard]] bool prepared( not_null item, - Data::Forum *forum) const; + Data::Forum *forum, + Data::SavedMessages *monoforum) const; void prepare( not_null item, Data::Forum *forum, + Data::SavedMessages *monoforum, Fn customEmojiRepaint, ToPreviewOptions options); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index 82fc64a7a9..9286e40cdb 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -8,10 +8,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_topics_view.h" #include "dialogs/ui/dialogs_layout.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_peer.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" #include "core/ui_integration.h" #include "lang/lang_keys.h" +#include "main/main_session.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/text/text_options.h" @@ -26,29 +32,35 @@ constexpr auto kIconLoopCount = 1; } // namespace -TopicsView::TopicsView(not_null forum) -: _forum(forum) { +TopicsView::TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum) +: _forum(forum) +, _monoforum(monoforum) { } TopicsView::~TopicsView() = default; bool TopicsView::prepared() const { - return (_version == _forum->recentTopicsListVersion()); + const auto version = _forum + ? _forum->recentTopicsListVersion() + : _monoforum->recentSublistsListVersion(); + return (_version == version); } void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { + Expects(_forum != nullptr); + const auto &list = _forum->recentTopics(); _version = _forum->recentTopicsListVersion(); _titles.reserve(list.size()); auto index = 0; for (const auto &topic : list) { const auto from = begin(_titles) + index; - const auto rootId = topic->rootId(); + const auto key = topic->rootId().bare; const auto i = ranges::find( from, end(_titles), - rootId, - &Title::topicRootId); + key, + &Title::key); if (i != end(_titles)) { if (i != from) { ranges::rotate(from, i, i + 1); @@ -58,7 +70,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { } auto &title = _titles[index++]; const auto unread = topic->chatListBadgesState().unread; - if (title.topicRootId == rootId + if (title.key == key && title.unread == unread && title.version == topic->titleVersion()) { continue; @@ -69,7 +81,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { .customEmojiLoopLimit = kIconLoopCount, }); auto topicTitle = topic->titleWithIcon(); - title.topicRootId = rootId; + title.key = key; title.version = topic->titleVersion(); title.unread = unread; title.title.setMarkedText( @@ -87,7 +99,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { _titles.pop_back(); } const auto i = frontRootId - ? ranges::find(_titles, frontRootId, &Title::topicRootId) + ? ranges::find(_titles, frontRootId.bare, &Title::key) : end(_titles); _jumpToTopic = (i != end(_titles)); if (_jumpToTopic) { @@ -98,6 +110,80 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { _jumpToTopic = false; } } + _allLoaded = _forum->topicsList()->loaded(); +} + +void TopicsView::prepare(PeerId frontPeerId, Fn customEmojiRepaint) { + Expects(_monoforum != nullptr); + + const auto &list = _monoforum->recentSublists(); + const auto manager = &_monoforum->session().data().customEmojiManager(); + _version = _monoforum->recentSublistsListVersion(); + _titles.reserve(list.size()); + auto index = 0; + for (const auto &sublist : list) { + const auto from = begin(_titles) + index; + const auto peer = sublist->sublistPeer(); + const auto key = peer->id.value; + const auto i = ranges::find( + from, + end(_titles), + key, + &Title::key); + if (i != end(_titles)) { + if (i != from) { + ranges::rotate(from, i, i + 1); + } + } else if (index >= _titles.size()) { + _titles.emplace_back(); + } + auto &title = _titles[index++]; + const auto unread = sublist->chatListBadgesState().unread; + if (title.key == key + && title.unread == unread + && title.version == peer->nameVersion()) { + continue; + } + const auto context = Core::TextContext({ + .session = &sublist->session(), + .repaint = customEmojiRepaint, + .customEmojiLoopLimit = kIconLoopCount, + }); + auto topicTitle = TextWithEntities().append( + Ui::Text::SingleCustomEmoji( + manager->peerUserpicEmojiData(peer), + u"@"_q) + ).append(' ').append(peer->shortName()); + title.key = key; + title.version = peer->nameVersion(); + title.unread = unread; + title.title.setMarkedText( + st::dialogsTextStyle, + (unread + ? Ui::Text::Colorized( + Ui::Text::Wrapped( + std::move(topicTitle), + EntityType::Bold)) + : std::move(topicTitle)), + DialogTextOptions(), + context); + } + while (_titles.size() > index) { + _titles.pop_back(); + } + const auto i = frontPeerId + ? ranges::find(_titles, frontPeerId.value, &Title::key) + : end(_titles); + _jumpToTopic = (i != end(_titles)); + if (_jumpToTopic) { + if (i != begin(_titles)) { + ranges::rotate(begin(_titles), i, i + 1); + } + if (!_titles.front().unread) { + _jumpToTopic = false; + } + } + _allLoaded = _monoforum->chatsList()->loaded(); } int TopicsView::jumpToTopicWidth() const { @@ -123,10 +209,13 @@ void TopicsView::paint( rect.setWidth(rect.width() - _lastTopicJumpGeometry.rightCut); auto skipBig = _jumpToTopic && !context.active; if (_titles.empty()) { + const auto text = (_monoforum && _allLoaded) + ? tr::lng_filters_no_chats(tr::now) + : tr::lng_contacts_loading(tr::now); p.drawText( rect.x(), rect.y() + st::normalFont->ascent, - tr::lng_contacts_loading(tr::now)); + text); return; } for (const auto &title : _titles) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h index b90c3d5417..9dd5d3aa7b 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h @@ -16,6 +16,8 @@ struct DialogRow; namespace Data { class Forum; class ForumTopic; +class SavedMessages; +class SavedSublist; } // namespace Data namespace Ui { @@ -59,15 +61,19 @@ void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context); class TopicsView final { public: - explicit TopicsView(not_null forum); + TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum); ~TopicsView(); - [[nodiscard]] not_null forum() const { + [[nodiscard]] Data::Forum *forum() const { return _forum; } + [[nodiscard]] Data::SavedMessages *monoforum() const { + return _monoforum; + } [[nodiscard]] bool prepared() const; void prepare(MsgId frontRootId, Fn customEmojiRepaint); + void prepare(PeerId frontPeerId, Fn customEmojiRepaint); [[nodiscard]] int jumpToTopicWidth() const; @@ -99,7 +105,7 @@ public: private: struct Title { Text::String title; - MsgId topicRootId = 0; + uint64 key = 0; int version = -1; bool unread = false; }; @@ -107,13 +113,15 @@ private: [[nodiscard]] QImage topicJumpRippleMask( not_null topicJumpCache) const; - const not_null _forum; + Data::Forum * const _forum = nullptr; + Data::SavedMessages * const _monoforum = nullptr; mutable std::vector _titles; mutable std::unique_ptr<RippleAnimation> _ripple; JumpToLastGeometry _lastTopicJumpGeometry; int _version = -1; bool _jumpToTopic = false; + bool _allLoaded = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/editor/scene/scene_item_image.cpp b/Telegram/SourceFiles/editor/scene/scene_item_image.cpp index b3cc544cfd..1a939231bc 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_image.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_image.cpp @@ -10,7 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { ItemImage::ItemImage( - const QPixmap &&pixmap, + QPixmap &&pixmap, ItemBase::Data data) : ItemBase(std::move(data)) , _pixmap(std::move(pixmap)) { diff --git a/Telegram/SourceFiles/editor/scene/scene_item_image.h b/Telegram/SourceFiles/editor/scene/scene_item_image.h index 1754ac279b..320370ea27 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_image.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_image.h @@ -14,7 +14,7 @@ namespace Editor { class ItemImage : public ItemBase { public: ItemImage( - const QPixmap &&pixmap, + QPixmap &&pixmap, ItemBase::Data data); void paint( QPainter *p, diff --git a/Telegram/SourceFiles/editor/scene/scene_item_line.cpp b/Telegram/SourceFiles/editor/scene/scene_item_line.cpp index 0d40e24c7d..0b68b3969d 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_line.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_line.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { -ItemLine::ItemLine(const QPixmap &&pixmap) +ItemLine::ItemLine(QPixmap &&pixmap) : _pixmap(std::move(pixmap)) , _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) { } diff --git a/Telegram/SourceFiles/editor/scene/scene_item_line.h b/Telegram/SourceFiles/editor/scene/scene_item_line.h index d4746c8e39..f750fd4840 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_line.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_line.h @@ -13,7 +13,7 @@ namespace Editor { class ItemLine : public NumberedItem { public: - ItemLine(const QPixmap &&pixmap); + ItemLine(QPixmap &&pixmap); QRectF boundingRect() const override; void paint( QPainter *p, diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index e3bf2f2100..e4ef4a0a6f 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -335,6 +335,10 @@ Utf8String Reaction::TypeToString(const Reaction &reaction) { Unexpected("Type in Reaction::Type."); } +std::vector<TextPart> ParseText(const MTPTextWithEntities &text) { + return ParseText(text.data().vtext(), text.data().ventities().v); +} + Utf8String Reaction::Id(const Reaction &reaction) { auto id = Utf8String(); switch (reaction.type) { @@ -777,17 +781,16 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) { auto result = Poll(); data.vpoll().match([&](const MTPDpoll &poll) { result.id = poll.vid().v; - result.question = ParseString(poll.vquestion().data().vtext()); + result.question = ParseText(poll.vquestion()); result.closed = poll.is_closed(); result.answers = ranges::views::all( poll.vanswers().v ) | ranges::views::transform([](const MTPPollAnswer &answer) { - return answer.match([](const MTPDpollAnswer &answer) { - auto result = Poll::Answer(); - result.text = ParseString(answer.vtext().data().vtext()); - result.option = answer.voption().v; - return result; - }); + const auto &data = answer.data(); + auto result = Poll::Answer(); + result.text = ParseText(data.vtext()); + result.option = data.voption().v; + return result; }) | ranges::to_vector; }); data.vresults().match([&](const MTPDpollResults &results) { @@ -796,25 +799,47 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) { } if (const auto resultsList = results.vresults()) { for (const auto &single : resultsList->v) { - single.match([&](const MTPDpollAnswerVoters &voters) { - const auto i = ranges::find( - result.answers, - voters.voption().v, - &Poll::Answer::option); - if (i == end(result.answers)) { - return; - } - i->votes = voters.vvoters().v; - if (voters.is_chosen()) { - i->my = true; - } - }); + const auto &voters = single.data(); + const auto i = ranges::find( + result.answers, + voters.voption().v, + &Poll::Answer::option); + if (i == end(result.answers)) { + continue; + } + i->votes = voters.vvoters().v; + if (voters.is_chosen()) { + i->my = true; + } } } }); return result; } +TodoListItem ParseTodoListItem(const MTPTodoItem &item) { + const auto &data = item.data(); + auto result = TodoListItem(); + result.text = ParseText(data.vtitle()); + result.id = data.vid().v; + return result; +} + +TodoList ParseTodoList(const MTPDmessageMediaToDo &data) { + auto result = TodoList(); + data.vtodo().match([&](const MTPDtodoList &data) { + result.title = ParseText(data.vtitle()); + result.othersCanAppend = data.is_others_can_append(); + result.othersCanComplete = data.is_others_can_complete(); + result.items = ranges::views::all( + data.vlist().v + ) | ranges::views::transform( + ParseTodoListItem + ) | ranges::to_vector; + }); + return result; +} + GiveawayStart ParseGiveaway(const MTPDmessageMediaGiveaway &data) { auto result = GiveawayStart{ .untilDate = data.vuntil_date().v, @@ -1367,6 +1392,8 @@ Media ParseMedia( result.ttl = data.vperiod().v; }, [&](const MTPDmessageMediaPoll &data) { result.content = ParsePoll(data); + }, [&](const MTPDmessageMediaToDo &data) { + result.content = ParseTodoList(data); }, [](const MTPDmessageMediaDice &data) { // #TODO dice }, [](const MTPDmessageMediaStory &data) { @@ -1654,11 +1681,21 @@ ServiceAction ParseServiceAction( content.transactionId = data.vcharge().data().vid().v; result.content = content; }, [&](const MTPDmessageActionGiftStars &data) { - auto content = ActionGiftStars(); + auto content = ActionGiftCredits(); content.cost = Ui::FillAmountAndCurrency( data.vamount().v, qs(data.vcurrency())).toUtf8(); - content.credits = data.vstars().v; + content.amount = CreditsAmount(data.vstars().v, CreditsType::Stars); + result.content = content; + }, [&](const MTPDmessageActionGiftTon &data) { + auto content = ActionGiftCredits(); + content.cost = Ui::FillAmountAndCurrency( + data.vamount().v, + qs(data.vcurrency())).toUtf8(); + content.amount = CreditsAmount( + data.vamount().v / uint64(1'000'000'000), + data.vamount().v % uint64(1'000'000'000), + CreditsType::Ton); result.content = content; }, [&](const MTPDmessageActionPrizeStars &data) { result.content = ActionPrizeStars{ @@ -1712,6 +1749,39 @@ ServiceAction ParseServiceAction( }, [&](const MTPDmessageActionPaidMessagesPrice &data) { result.content = ActionPaidMessagesPrice{ .stars = int(data.vstars().v), + .broadcastAllowed = data.is_broadcast_messages_allowed(), + }; + }, [&](const MTPDmessageActionTodoCompletions &data) { + const auto take = [](const MTPVector<MTPint> &list) { + return list.v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; + }; + result.content = ActionTodoCompletions{ + .completed = take(data.vcompleted()), + .incompleted = take(data.vincompleted()), + }; + }, [&](const MTPDmessageActionTodoAppendTasks &data) { + result.content = ActionTodoAppendTasks{ + .items = data.vlist().v + | ranges::views::transform(ParseTodoListItem) + | ranges::to_vector, + }; + }, [&](const MTPDmessageActionSuggestedPostApproval &data) { + result.content = ActionSuggestedPostApproval{ + .rejectComment = data.vreject_comment().value_or_empty(), + .scheduleDate = data.vschedule_date().value_or_empty(), + .price = CreditsAmountFromTL(data.vprice()), + .rejected = data.is_rejected(), + .balanceTooLow = data.is_balance_too_low(), + }; + }, [&](const MTPDmessageActionSuggestedPostSuccess &data) { + result.content = ActionSuggestedPostSuccess{ + .price = CreditsAmountFromTL(data.vprice()), + }; + }, [&](const MTPDmessageActionSuggestedPostRefund &data) { + result.content = ActionSuggestedPostRefund{ + .payerInitiated = data.is_payer_initiated(), }; }, [&](const MTPDmessageActionConferenceCall &data) { auto content = ActionPhoneCall(); diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index ca9a95916d..2c650871a3 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "scheme.h" #include "base/optional.h" #include "base/variant.h" +#include "core/credits_amount.h" #include "data/data_peer_id.h" #include <QtCore/QSize> @@ -44,6 +45,39 @@ inline auto NumberToString(Type value, int length = 0, char filler = '0') filler).replace(',', '.'); } +struct TextPart { + enum class Type { + Text, + Unknown, + Mention, + Hashtag, + BotCommand, + Url, + Email, + Bold, + Italic, + Code, + Pre, + TextUrl, + MentionName, + Phone, + Cashtag, + Underline, + Strike, + Blockquote, + BankCard, + Spoiler, + CustomEmoji, + }; + Type type = Type::Text; + Utf8String text; + Utf8String additional; + + [[nodiscard]] static Utf8String UnavailableEmoji() { + return "(unavailable)"; + } +}; + struct UserpicsInfo { int count = 0; }; @@ -198,19 +232,31 @@ struct PaidMedia { struct Poll { struct Answer { - Utf8String text; + std::vector<TextPart> text; QByteArray option; int votes = 0; bool my = false; }; uint64 id = 0; - Utf8String question; + std::vector<TextPart> question; std::vector<Answer> answers; int totalVotes = 0; bool closed = false; }; +struct TodoListItem { + std::vector<TextPart> text; + int id = 0; +}; + +struct TodoList { + bool othersCanAppend = false; + bool othersCanComplete = false; + std::vector<TextPart> title; + std::vector<TodoListItem> items; +}; + struct GiveawayStart { std::vector<QString> countries; std::vector<ChannelId> channels; @@ -370,6 +416,7 @@ struct Media { Game, Invoice, Poll, + TodoList, GiveawayStart, GiveawayResults, PaidMedia, @@ -404,39 +451,6 @@ Media ParseMedia( const QString &folder, TimeId date); -struct TextPart { - enum class Type { - Text, - Unknown, - Mention, - Hashtag, - BotCommand, - Url, - Email, - Bold, - Italic, - Code, - Pre, - TextUrl, - MentionName, - Phone, - Cashtag, - Underline, - Strike, - Blockquote, - BankCard, - Spoiler, - CustomEmoji, - }; - Type type = Type::Text; - Utf8String text; - Utf8String additional; - - [[nodiscard]] static Utf8String UnavailableEmoji() { - return "(unavailable)"; - } -}; - struct ActionChatCreate { Utf8String title; std::vector<UserId> userIds; @@ -645,9 +659,9 @@ struct ActionPaymentRefunded { Utf8String transactionId; }; -struct ActionGiftStars { +struct ActionGiftCredits { Utf8String cost; - int credits = 0; + CreditsAmount amount; }; struct ActionPrizeStars { @@ -673,6 +687,32 @@ struct ActionPaidMessagesRefunded { struct ActionPaidMessagesPrice { int stars = 0; + bool broadcastAllowed = false; +}; + +struct ActionTodoCompletions { + std::vector<int> completed; + std::vector<int> incompleted; +}; + +struct ActionTodoAppendTasks { + std::vector<TodoListItem> items; +}; + +struct ActionSuggestedPostApproval { + Utf8String rejectComment; + TimeId scheduleDate = 0; + CreditsAmount price; + bool rejected = false; + bool balanceTooLow = false; +}; + +struct ActionSuggestedPostSuccess { + CreditsAmount price; +}; + +struct ActionSuggestedPostRefund { + bool payerInitiated = false; }; struct ServiceAction { @@ -718,11 +758,16 @@ struct ServiceAction { ActionGiveawayResults, ActionBoostApply, ActionPaymentRefunded, - ActionGiftStars, + ActionGiftCredits, ActionPrizeStars, ActionStarGift, ActionPaidMessagesRefunded, - ActionPaidMessagesPrice> content; + ActionPaidMessagesPrice, + ActionTodoCompletions, + ActionTodoAppendTasks, + ActionSuggestedPostApproval, + ActionSuggestedPostSuccess, + ActionSuggestedPostRefund> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index e5b58b7798..659c7f43ff 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -621,7 +621,14 @@ private: [[nodiscard]] QByteArray pushPhotoMedia( const Data::Photo &data, const QString &basePath); - [[nodiscard]] QByteArray pushPoll(const Data::Poll &data); + [[nodiscard]] QByteArray pushPoll( + const Data::Poll &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase); + [[nodiscard]] QByteArray pushTodoList( + const Data::TodoList &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase); [[nodiscard]] QByteArray pushGiveaway( const PeersMap &peers, const Data::GiveawayStart &data); @@ -1346,16 +1353,16 @@ auto HtmlWriter::Wrap::pushMessage( + " refunded back " + amount; return result; - }, [&](const ActionGiftStars &data) { - if (!data.credits || data.cost.isEmpty()) { + }, [&](const ActionGiftCredits &data) { + if (!data.amount || data.cost.isEmpty()) { return serviceFrom + " sent you a gift."; } return serviceFrom + " sent you a gift for " + data.cost + ": " - + QString::number(data.credits).toUtf8() - + " Telegram Stars."; + + QString::number(data.amount.value()).toUtf8() + + (data.amount.ton() ? " TON." : " Telegram Stars."); }, [&](const ActionPrizeStars &data) { return "You won a prize in a giveaway organized by " + peers.wrapPeerName(data.peerId) @@ -1383,10 +1390,91 @@ auto HtmlWriter::Wrap::pushMessage( + " messages to you"); return result; }, [&](const ActionPaidMessagesPrice &data) { - auto result = "Price per messages changed to " + if (isChannel) { + auto result = !data.broadcastAllowed + ? "Direct messages were disabled." + : ("Price per direct message changed to " + + QString::number(data.stars).toUtf8() + + " Telegram Stars."); + return result; + } + auto result = "Price per message changed to " + QString::number(data.stars).toUtf8() + " Telegram Stars."; return result; + }, [&](const ActionTodoCompletions &data) { + auto completed = QByteArrayList(); + for (const auto index : data.completed) { + completed.push_back(QByteArray::number(index)); + } + auto incompleted = QByteArrayList(); + for (const auto index : data.incompleted) { + incompleted.push_back(QByteArray::number(index)); + } + const auto list = [](const QByteArrayList &v) { + return v.isEmpty() + ? QByteArray() + : (v.size() > 1) + ? (v.mid(0, v.size() - 1).join(", ") + " and " + v.back()) + : v.front(); + }; + if (completed.isEmpty() && !incompleted.isEmpty()) { + return serviceFrom + + " marked " + + list(incompleted) + + " as not done yet in " + + wrapReplyToLink("this todo list") + "."; + } else if (!completed.isEmpty() && incompleted.isEmpty()) { + return serviceFrom + + " marked " + + list(completed) + + " as done in " + + wrapReplyToLink("this todo list") + "."; + } + return serviceFrom + + " marked " + + list(completed) + + " as done and " + + list(incompleted) + + " as not done yet in " + + wrapReplyToLink("this todo list") + "."; + }, [&](const ActionTodoAppendTasks &data) { + auto tasks = QByteArrayList(); + for (const auto &task : data.items) { + tasks.push_back(""" + + FormatText(task.text, internalLinksDomain, _base) + + """); + } + return serviceFrom + " added tasks: " + tasks.join(", "); + }, [&](const ActionSuggestedPostApproval &data) { + return serviceFrom + + (data.rejected ? " rejected " : " approved ") + + "your suggested post" + + (data.price + ? (", for " + + QString::number(data.price.value()).toUtf8() + + (data.price.ton() ? " TON" : " stars")) + : "") + + (data.scheduleDate + ? (", " + + FormatDateText(data.scheduleDate) + + " at " + + FormatTimeText(data.scheduleDate)) + : "") + + (data.rejectComment.isEmpty() + ? "." + : (", with comment: "" + + SerializeString(data.rejectComment) + + """)); + }, [&](const ActionSuggestedPostSuccess &data) { + return "The paid post was shown for 24 hours and " + + QString::number(data.price.value()).toUtf8() + + (data.price.ton() ? " TON" : " stars") + + " were transferred to the channel."; + }, [&](const ActionSuggestedPostRefund &data) { + return QByteArray() + (data.payerInitiated + ? "The user refunded the payment, post was deleted." + : "The admin deleted the post early, the payment was refunded."); }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -1713,7 +1801,9 @@ QByteArray HtmlWriter::Wrap::pushMedia( Assert(!message.media.ttl); return pushPhotoMedia(*photo, basePath); } else if (const auto poll = std::get_if<Poll>(&content)) { - return pushPoll(*poll); + return pushPoll(*poll, internalLinksDomain, _base); + } else if (const auto todo = std::get_if<TodoList>(&content)) { + return pushTodoList(*todo, internalLinksDomain, _base); } else if (const auto giveaway = std::get_if<GiveawayStart>(&content)) { return pushGiveaway(peers, *giveaway); } else if (const auto giveaway = std::get_if<GiveawayResults>(&content)) { @@ -1991,13 +2081,19 @@ QByteArray HtmlWriter::Wrap::pushPhotoMedia( return result; } -QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { +QByteArray HtmlWriter::Wrap::pushPoll( + const Data::Poll &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase) { using namespace Data; auto result = pushDiv("media_wrap clearfix"); result.append(pushDiv("media_poll")); result.append(pushDiv("question bold")); - result.append(SerializeString(data.question)); + result.append(FormatText( + data.question, + internalLinksDomain, + relativeLinkBase)); result.append(popTag()); result.append(pushDiv("details")); if (data.closed) { @@ -2028,7 +2124,9 @@ QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { }; for (const auto &answer : data.answers) { result.append(pushDiv("answer")); - result.append("- " + SerializeString(answer.text) + details(answer)); + result.append("- " + + FormatText(answer.text, internalLinksDomain, relativeLinkBase) + + details(answer)); result.append(popTag()); } result.append(pushDiv("total details ")); @@ -2039,6 +2137,38 @@ QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { return result; } +QByteArray HtmlWriter::Wrap::pushTodoList( + const Data::TodoList &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase) { + using namespace Data; + + auto result = pushDiv("media_wrap clearfix"); + result.append(pushDiv("media_poll")); + result.append(pushDiv("question bold")); + result.append(FormatText( + data.title, + internalLinksDomain, + relativeLinkBase)); + result.append(popTag()); + result.append(pushDiv("details")); + result.append(SerializeString("To-do List")); + result.append(popTag()); + const auto details = [&](const TodoListItem &item) { + return QByteArray(""); // #TODO todo + }; + for (const auto &item : data.items) { + result.append(pushDiv("answer")); + result.append("- " + + FormatText(item.text, internalLinksDomain, relativeLinkBase) + + details(item)); + result.append(popTag()); + } + result.append(popTag()); + result.append(popTag()); + return result; +} + QByteArray HtmlWriter::Wrap::pushGiveaway( const PeersMap &peers, const Data::GiveawayStart &data) { @@ -2428,6 +2558,7 @@ MediaData HtmlWriter::Wrap::prepareMediaData( result.description = data.description; result.status = Data::FormatMoneyAmount(data.amount, data.currency); }, [](const Poll &data) { + }, [](const TodoList &data) { }, [](const GiveawayStart &data) { }, [](const GiveawayResults &data) { }, [&](const PaidMedia &data) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 772bc6f3f2..b591bfb3dd 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -644,14 +644,17 @@ QByteArray SerializeMessage( pushBare("peer_name", wrapPeerName(data.peerId)); push("peer_id", data.peerId); push("charge_id", data.transactionId); - }, [&](const ActionGiftStars &data) { + }, [&](const ActionGiftCredits &data) { pushActor(); - pushAction("send_stars_gift"); + pushAction(data.amount.ton() + ? "send_ton_gift" + : "send_stars_gift"); if (!data.cost.isEmpty()) { push("cost", data.cost); } - if (data.credits) { - push("stars", data.credits); + if (data.amount) { + push("amount_whole", data.amount.whole()); + push("amount_nano", data.amount.nano()); } }, [&](const ActionPrizeStars &data) { pushActor(); @@ -679,6 +682,59 @@ QByteArray SerializeMessage( pushActor(); pushAction("paid_messages_price_change"); push("price_stars", data.stars); + push("is_broadcast_messages_allowed", data.broadcastAllowed); + }, [&](const ActionTodoCompletions &data) { + pushActor(); + pushAction("todo_completions"); + auto completed = QByteArrayList(); + for (const auto index : data.completed) { + completed.push_back(QByteArray::number(index)); + } + auto incompleted = QByteArrayList(); + for (const auto index : data.incompleted) { + incompleted.push_back(QByteArray::number(index)); + } + pushBare("completed", '[' + completed.join(',') + ']'); + pushBare("incompleted", '[' + incompleted.join(',') + ']'); + }, [&](const ActionTodoAppendTasks &data) { + pushActor(); + pushAction("todo_append_tasks"); + const auto items = ranges::views::all( + data.items + ) | ranges::views::transform([&](const TodoListItem &item) { + context.nesting.push_back(Context::kArray); + auto result = SerializeObject(context, { + { "text", SerializeText(context, item.text) }, + { "id", NumberToString(item.id) }, + }); + context.nesting.pop_back(); + return result; + }) | ranges::to_vector; + pushBare("items", SerializeArray(context, items)); + }, [&](const ActionSuggestedPostApproval &data) { + pushActor(); + pushAction("process_suggested_post"); + if (data.rejected) { + pushBare("rejected", "true"); + if (!data.rejectComment.isEmpty()) { + push("comment", data.rejectComment); + } + } else { + push("price_amount_whole", NumberToString(data.price.whole())); + push("price_amount_nano", NumberToString(data.price.nano())); + push("price_currency", data.price.ton() ? "TON" : "Stars"); + push("scheduled_date", data.scheduleDate); + } + }, [&](const ActionSuggestedPostSuccess &data) { + pushActor(); + pushAction("suggested_post_success"); + push("price_amount_whole", NumberToString(data.price.whole())); + push("price_amount_nano", NumberToString(data.price.nano())); + push("price_currency", data.price.ton() ? "TON" : "Stars"); + }, [&](const ActionSuggestedPostRefund &data) { + pushActor(); + pushAction("suggested_post_refund"); + push("user_initiated", data.payerInitiated); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { @@ -806,7 +862,7 @@ QByteArray SerializeMessage( ) | ranges::views::transform([&](const Poll::Answer &answer) { context.nesting.push_back(Context::kArray); auto result = SerializeObject(context, { - { "text", SerializeString(answer.text) }, + { "text", SerializeText(context, answer.text) }, { "voters", NumberToString(answer.votes) }, { "chosen", answer.my ? "true" : "false" }, }); @@ -817,11 +873,36 @@ QByteArray SerializeMessage( context.nesting.pop_back(); pushBare("poll", SerializeObject(context, { - { "question", SerializeString(data.question) }, + { "question", SerializeText(context, data.question) }, { "closed", data.closed ? "true" : "false" }, { "total_voters", NumberToString(data.totalVotes) }, { "answers", serialized } })); + }, [&](const TodoList &data) { + context.nesting.push_back(Context::kObject); + const auto items = ranges::views::all( + data.items + ) | ranges::views::transform([&](const TodoListItem &item) { + context.nesting.push_back(Context::kArray); + auto result = SerializeObject(context, { + { "text", SerializeText(context, item.text) }, + { "id", NumberToString(item.id) }, + }); + context.nesting.pop_back(); + return result; + }) | ranges::to_vector; + const auto serialized = SerializeArray(context, items); + context.nesting.pop_back(); + + pushBare("todo_list", SerializeObject(context, { + { "title", SerializeText(context, data.title) }, + { "others_can_append", data.othersCanAppend ? "true" : "false" }, + { + "others_can_complete", + data.othersCanComplete ? "true" : "false", + }, + { "answers", serialized } + })); }, [&](const GiveawayStart &data) { context.nesting.push_back(Context::kArray); const auto channels = ranges::views::all( diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 3811cbc860..9ccdb15592 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -10,10 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/algorithm.h" #include "logs.h" -#if !defined TDESKTOP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC +#if !defined Q_OS_WIN && !defined Q_OS_MAC #include "base/platform/linux/base_linux_library.h" #include <deque> -#endif // !TDESKTOP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC +#endif // !Q_OS_WIN && !Q_OS_MAC #include <QImage> @@ -26,6 +26,16 @@ extern "C" { #include <libavutil/display.h> } // extern "C" +#if !defined Q_OS_WIN && !defined Q_OS_MAC +extern "C" { +void _libvdpau_so_tramp_resolve_all(void) __attribute__((weak)); +void _libva_drm_so_tramp_resolve_all(void) __attribute__((weak)); +void _libva_x11_so_tramp_resolve_all(void) __attribute__((weak)); +void _libva_so_tramp_resolve_all(void) __attribute__((weak)); +void _libdrm_so_tramp_resolve_all(void) __attribute__((weak)); +} // extern "C" +#endif // !Q_OS_WIN && !Q_OS_MAC + namespace FFmpeg { namespace { @@ -91,23 +101,24 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { #endif // LIB_FFMPEG_USE_QT_PRIVATE_API } -#if !defined TDESKTOP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC +#if !defined Q_OS_WIN && !defined Q_OS_MAC [[nodiscard]] auto CheckHwLibs() { auto list = std::deque{ AV_PIX_FMT_CUDA, }; - if (base::Platform::LoadLibrary("libvdpau.so.1")) { + if (!_libvdpau_so_tramp_resolve_all + || base::Platform::LoadLibrary("libvdpau.so.1")) { list.push_front(AV_PIX_FMT_VDPAU); } if ([&] { const auto list = std::array{ - "libva-drm.so.2", - "libva-x11.so.2", - "libva.so.2", - "libdrm.so.2", + std::make_pair(_libva_drm_so_tramp_resolve_all, "libva-drm.so.2"), + std::make_pair(_libva_x11_so_tramp_resolve_all, "libva-x11.so.2"), + std::make_pair(_libva_so_tramp_resolve_all, "libva.so.2"), + std::make_pair(_libdrm_so_tramp_resolve_all, "libdrm.so.2"), }; - for (const auto lib : list) { - if (!base::Platform::LoadLibrary(lib)) { + for (const auto &lib : list) { + if (lib.first && !base::Platform::LoadLibrary(lib.second)) { return false; } } @@ -117,7 +128,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return list; } -#endif // !TDESKTOP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC +#endif // !Q_OS_WIN && !Q_OS_MAC [[nodiscard]] bool InitHw(AVCodecContext *context, AVHWDeviceType type) { AVCodecContext *parent = static_cast<AVCodecContext*>(context->opaque); @@ -160,9 +171,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return false; }; -#if !defined TDESKTOP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC - static const auto list = CheckHwLibs(); -#else // !TDESKTOP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC +#if defined Q_OS_WIN || defined Q_OS_MAC const auto list = std::array{ #ifdef Q_OS_WIN AV_PIX_FMT_D3D11, @@ -170,13 +179,11 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { AV_PIX_FMT_CUDA, #elif defined Q_OS_MAC // Q_OS_WIN AV_PIX_FMT_VIDEOTOOLBOX, -#else // Q_OS_WIN || Q_OS_MAC - AV_PIX_FMT_VAAPI, - AV_PIX_FMT_VDPAU, - AV_PIX_FMT_CUDA, #endif // Q_OS_WIN || Q_OS_MAC }; -#endif // TDESKTOP_USE_PACKAGED || Q_OS_WIN || Q_OS_MAC +#else // Q_OS_WIN || Q_OS_MAC + static const auto list = CheckHwLibs(); +#endif // !Q_OS_WIN && !Q_OS_MAC for (const auto format : list) { if (!has(format)) { continue; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 0b9ac614d0..babf26203a 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -740,8 +740,9 @@ void InnerWidget::elementSearchInList( void InnerWidget::elementHandleViaClick(not_null<UserData*> bot) { } -bool InnerWidget::elementIsChatWide() { - return _isChatWide; +HistoryView::ElementChatMode InnerWidget::elementChatMode() { + using Mode = HistoryView::ElementChatMode; + return _isChatWide ? Mode::Wide : Mode::Default; } not_null<Ui::PathShiftGradient*> InnerWidget::elementPathShiftGradient() { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index ed23bdad44..9f58afb921 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -131,7 +131,7 @@ public: const QString &query, const FullMsgId &context) override; void elementHandleViaClick(not_null<UserData*> bot) override; - bool elementIsChatWide() override; + HistoryView::ElementChatMode elementChatMode() override; not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override; void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction( @@ -251,7 +251,7 @@ private: // for each found message (in given direction) in the passed history with passed top offset. // // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <EnumItemsDirection direction, typename Method> void enumerateItems(Method method); diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 2e3580222e..0f656950b4 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -131,6 +131,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { const auto reply = PrepareLogReply(data.vreply_to()); const auto removeFlags = Flag::f_out | Flag::f_post + | Flag::f_saved_peer_id | Flag::f_reactions_are_possible | Flag::f_reactions | Flag::f_ttl_period @@ -140,6 +141,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + MTPPeer(), // saved_peer_id reply.value_or(MTPMessageReplyHeader()), MTP_int(newDate), data.vaction(), @@ -150,6 +152,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { const auto reply = PrepareLogReply(data.vreply_to()); const auto removeFlags = Flag::f_out | Flag::f_post + | Flag::f_saved_peer_id | (reply ? Flag() : Flag::f_reply_to) | Flag::f_replies | Flag::f_edit_date @@ -160,7 +163,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { | Flag::f_restriction_reason | Flag::f_ttl_period | Flag::f_factcheck - | Flag::f_report_delivery_until_date; + | Flag::f_report_delivery_until_date + | Flag::f_suggested_post; return MTP_message( MTP_flags(data.vflags().v & ~removeFlags), data.vid(), @@ -192,7 +196,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTP_long(data.veffect().value_or_empty()), MTPFactCheck(), MTPint(), // report_delivery_until_date - MTP_long(data.vpaid_message_stars().value_or_empty())); + MTP_long(data.vpaid_message_stars().value_or_empty()), + MTPSuggestedPost()); }); } @@ -282,6 +287,7 @@ TextWithEntities GenerateAdminChangeText( { Flag::ManageTopics, tr::lng_admin_log_admin_manage_topics }, { Flag::PinMessages, tr::lng_admin_log_admin_pin_messages }, { Flag::ManageCall, tr::lng_admin_log_admin_manage_calls }, + { Flag::ManageDirect, tr::lng_admin_log_admin_manage_direct }, { Flag::AddAdmins, tr::lng_admin_log_admin_add_admins }, { Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous }, }; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 292fdd6cb3..98740671f4 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" @@ -45,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_histories.h" #include "data/data_history_messages.h" +#include "data/data_todo_list.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "api/api_chat_participants.h" @@ -167,6 +169,9 @@ void History::itemRemoved(not_null<HistoryItem*> item) { if (const auto topic = item->topic()) { topic->applyItemRemoved(item->id); } + if (const auto sublist = item->savedSublist()) { + sublist->applyItemRemoved(item->id); + } if (const auto chat = peer->asChat()) { if (const auto to = chat->getMigrateToChannel()) { if (const auto history = owner().historyLoaded(to)) { @@ -207,33 +212,42 @@ void History::itemVanished(not_null<HistoryItem*> item) { void History::takeLocalDraft(not_null<History*> from) { const auto topicRootId = MsgId(0); - const auto i = from->_drafts.find(Data::DraftKey::Local(topicRootId)); + const auto monoforumPeerId = PeerId(0); + const auto i = from->_drafts.find( + Data::DraftKey::Local(topicRootId, monoforumPeerId)); if (i == end(from->_drafts)) { return; } auto &draft = i->second; if (!draft->textWithTags.text.isEmpty() - && !_drafts.contains(Data::DraftKey::Local(topicRootId))) { + && !_drafts.contains( + Data::DraftKey::Local(topicRootId, monoforumPeerId))) { // Edit and reply to drafts can't migrate. // Cloud drafts do not migrate automatically. draft->reply = FullReplyTo(); setLocalDraft(std::move(draft)); } - from->clearLocalDraft(topicRootId); + from->clearLocalDraft(topicRootId, monoforumPeerId); session().api().saveDraftToCloudDelayed(from); } -void History::createLocalDraftFromCloud(MsgId topicRootId) { - const auto draft = cloudDraft(topicRootId); +void History::createLocalDraftFromCloud( + MsgId topicRootId, + PeerId monoforumPeerId) { + const auto draft = cloudDraft(topicRootId, monoforumPeerId); if (!draft) { - clearLocalDraft(topicRootId); + clearLocalDraft(topicRootId, monoforumPeerId); return; } else if (Data::DraftIsNull(draft) || !draft->date) { return; } draft->reply.topicRootId = topicRootId; - auto existing = localDraft(topicRootId); + draft->reply.monoforumPeerId = monoforumPeerId; + if (!suggestDraftAllowed()) { + draft->suggest = SuggestPostOptions(); + } + auto existing = localDraft(topicRootId, monoforumPeerId); if (Data::DraftIsNull(existing) || !existing->date || draft->date >= existing->date) { @@ -241,12 +255,14 @@ void History::createLocalDraftFromCloud(MsgId topicRootId) { setLocalDraft(std::make_unique<Data::Draft>( draft->textWithTags, draft->reply, + draft->suggest, draft->cursor, draft->webpage)); - existing = localDraft(topicRootId); + existing = localDraft(topicRootId, monoforumPeerId); } else if (existing != draft) { existing->textWithTags = draft->textWithTags; existing->reply = draft->reply; + existing->suggest = draft->suggest; existing->cursor = draft->cursor; existing->webpage = draft->webpage; } @@ -269,7 +285,7 @@ void History::setDraft( return; } const auto cloudThread = key.isCloud() - ? threadFor(key.topicRootId()) + ? threadFor(key.topicRootId(), key.monoforumPeerId()) : nullptr; if (cloudThread) { cloudThread->cloudDraftTextCache().clear(); @@ -299,7 +315,7 @@ void History::clearDraft(Data::DraftKey key) { void History::clearDrafts() { for (auto &[key, draft] : base::take(_drafts)) { const auto cloudThread = key.isCloud() - ? threadFor(key.topicRootId()) + ? threadFor(key.topicRootId(), key.monoforumPeerId()) : nullptr; if (cloudThread) { cloudThread->cloudDraftTextCache().clear(); @@ -310,96 +326,125 @@ void History::clearDrafts() { Data::Draft *History::createCloudDraft( MsgId topicRootId, + PeerId monoforumPeerId, const Data::Draft *fromDraft) { if (Data::DraftIsNull(fromDraft)) { setCloudDraft(std::make_unique<Data::Draft>( TextWithTags(), - FullReplyTo{ .topicRootId = topicRootId }, + FullReplyTo{ + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); - cloudDraft(topicRootId)->date = TimeId(0); + cloudDraft(topicRootId, monoforumPeerId)->date = TimeId(0); } else { - auto existing = cloudDraft(topicRootId); + auto existing = cloudDraft(topicRootId, monoforumPeerId); if (!existing) { auto reply = fromDraft->reply; reply.topicRootId = topicRootId; + reply.monoforumPeerId = monoforumPeerId; setCloudDraft(std::make_unique<Data::Draft>( fromDraft->textWithTags, reply, + fromDraft->suggest, fromDraft->cursor, fromDraft->webpage)); - existing = cloudDraft(topicRootId); + existing = cloudDraft(topicRootId, monoforumPeerId); } else if (existing != fromDraft) { existing->textWithTags = fromDraft->textWithTags; existing->reply = fromDraft->reply; + existing->suggest = fromDraft->suggest; existing->cursor = fromDraft->cursor; existing->webpage = fromDraft->webpage; } existing->date = base::unixtime::now(); existing->reply.topicRootId = topicRootId; + existing->reply.monoforumPeerId = monoforumPeerId; + if (!suggestDraftAllowed()) { + existing->suggest = SuggestPostOptions(); + } } - if (const auto thread = threadFor(topicRootId)) { + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->cloudDraftTextCache().clear(); thread->updateChatListSortPosition(); } - return cloudDraft(topicRootId); + return cloudDraft(topicRootId, monoforumPeerId); } -bool History::skipCloudDraftUpdate(MsgId topicRootId, TimeId date) const { - const auto i = _acceptCloudDraftsAfter.find(topicRootId); - return _savingCloudDraftRequests.contains(topicRootId) +bool History::skipCloudDraftUpdate( + MsgId topicRootId, + PeerId monoforumPeerId, + TimeId date) const { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); + const auto i = _acceptCloudDraftsAfter.find(key); + return _savingCloudDraftRequests.contains(key) || (i != _acceptCloudDraftsAfter.end() && date < i->second); } -void History::startSavingCloudDraft(MsgId topicRootId) { - ++_savingCloudDraftRequests[topicRootId]; +void History::startSavingCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); + ++_savingCloudDraftRequests[key]; } -void History::finishSavingCloudDraft(MsgId topicRootId, TimeId savedAt) { - const auto i = _savingCloudDraftRequests.find(topicRootId); +void History::finishSavingCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId, + TimeId savedAt) { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); + const auto i = _savingCloudDraftRequests.find(key); if (i != _savingCloudDraftRequests.end()) { if (--i->second <= 0) { _savingCloudDraftRequests.erase(i); } } - auto &after = _acceptCloudDraftsAfter[topicRootId]; + auto &after = _acceptCloudDraftsAfter[key]; after = std::max(after, savedAt + kSkipCloudDraftsFor); } -void History::applyCloudDraft(MsgId topicRootId) { +void History::applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId) { if (!topicRootId && session().supportMode()) { updateChatListEntry(); session().supportHelper().cloudDraftChanged(this); } else { - createLocalDraftFromCloud(topicRootId); - if (const auto thread = threadFor(topicRootId)) { + createLocalDraftFromCloud(topicRootId, monoforumPeerId); + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->updateChatListSortPosition(); - if (!topicRootId) { - session().changes().historyUpdated( - this, - UpdateFlag::CloudDraft); - } else { + if (topicRootId) { session().changes().topicUpdated( thread->asTopic(), Data::TopicUpdate::Flag::CloudDraft); + } else if (monoforumPeerId) { + session().changes().sublistUpdated( + thread->asSublist(), + Data::SublistUpdate::Flag::CloudDraft); + } else { + session().changes().historyUpdated( + this, + UpdateFlag::CloudDraft); } } } } -void History::draftSavedToCloud(MsgId topicRootId) { - if (const auto thread = threadFor(topicRootId)) { +void History::draftSavedToCloud(MsgId topicRootId, PeerId monoforumPeerId) { + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->updateChatListEntry(); } session().local().writeDrafts(this); } const Data::ForwardDraft &History::forwardDraft( - MsgId topicRootId) const { + MsgId topicRootId, + PeerId monoforumPeerId) const { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); static const auto kEmpty = Data::ForwardDraft(); - const auto i = _forwardDrafts.find(topicRootId); + const auto i = _forwardDrafts.find(key); return (i != end(_forwardDrafts)) ? i->second : kEmpty; } @@ -412,11 +457,12 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( } Data::ResolvedForwardDraft History::resolveForwardDraft( - MsgId topicRootId) { - const auto &draft = forwardDraft(topicRootId); + MsgId topicRootId, + PeerId monoforumPeerId) { + const auto &draft = forwardDraft(topicRootId, monoforumPeerId); auto result = resolveForwardDraft(draft); if (result.items.size() != draft.ids.size()) { - setForwardDraft(topicRootId, { + setForwardDraft(topicRootId, monoforumPeerId, { .ids = owner().itemsToIds(result.items), .options = result.options, }); @@ -426,24 +472,23 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( void History::setForwardDraft( MsgId topicRootId, + PeerId monoforumPeerId, Data::ForwardDraft &&draft) { auto changed = false; + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); if (draft.ids.empty()) { - changed = _forwardDrafts.remove(topicRootId); + changed = _forwardDrafts.remove(key); } else { - auto &now = _forwardDrafts[topicRootId]; + auto &now = _forwardDrafts[key]; if (now != draft) { now = std::move(draft); changed = true; } } if (changed) { - const auto entry = topicRootId - ? peer->forumTopicFor(topicRootId) - : (Dialogs::Entry*)this; - if (entry) { + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { session().changes().entryUpdated( - entry, + thread, Data::EntryUpdate::Flag::ForwardDraft); } } @@ -614,8 +659,52 @@ void History::destroyMessagesByTopic(MsgId topicRootId) { } } -void History::unpinMessagesFor(MsgId topicRootId) { - if (!topicRootId) { +void History::destroyMessagesBySublist(not_null<PeerData*> sublistPeer) { + auto toDestroy = std::vector<not_null<HistoryItem*>>(); + toDestroy.reserve(_items.size()); + const auto peerId = sublistPeer->id; + for (const auto &message : _items) { + if (message->sublistPeerId() == peerId) { + toDestroy.push_back(message.get()); + } + } + for (const auto item : toDestroy) { + item->destroy(); + } +} + +void History::unpinMessagesFor(MsgId topicRootId, PeerId monoforumPeerId) { + if (topicRootId) { + session().storage().remove( + Storage::SharedMediaRemoveAll( + peer->id, + topicRootId, + Storage::SharedMediaType::Pinned)); + if (const auto topic = peer->forumTopicFor(topicRootId)) { + topic->setHasPinnedMessages(false); + } + for (const auto &item : _items) { + if (item->isPinned() && item->topicRootId() == topicRootId) { + item->setIsPinned(false); + } + } + } else if (monoforumPeerId) { + session().storage().remove( + Storage::SharedMediaRemoveAll( + peer->id, + monoforumPeerId, + Storage::SharedMediaType::Pinned)); + if (const auto sublist = peer->monoforumSublistFor( + monoforumPeerId)) { + sublist->setHasPinnedMessages(false); + } + for (const auto &item : _items) { + if (item->isPinned() + && item->sublistPeerId() == monoforumPeerId) { + item->setIsPinned(false); + } + } + } else { session().storage().remove( Storage::SharedMediaRemoveAll( peer->id, @@ -631,20 +720,6 @@ void History::unpinMessagesFor(MsgId topicRootId) { item->setIsPinned(false); } } - } else { - session().storage().remove( - Storage::SharedMediaRemoveAll( - peer->id, - topicRootId, - Storage::SharedMediaType::Pinned)); - if (const auto topic = peer->forumTopicFor(topicRootId)) { - topic->setHasPinnedMessages(false); - } - for (const auto &item : _items) { - if (item->isPinned() && item->topicRootId() == topicRootId) { - item->setIsPinned(false); - } - } } } @@ -799,11 +874,19 @@ void History::clearUnreadMentionsFor(MsgId topicRootId) { } } -void History::clearUnreadReactionsFor(MsgId topicRootId) { +void History::clearUnreadReactionsFor( + MsgId topicRootId, + Data::SavedSublist *sublist) { const auto forum = peer->forum(); - if (!topicRootId) { + const auto monoforum = peer->monoforum(); + const auto sublistPeerId = sublist ? sublist->sublistPeer()->id : 0; + if ((!topicRootId && !sublist) + || (!topicRootId && forum) + || (!sublist && monoforum)) { if (forum) { forum->clearAllUnreadReactions(); + } else if (monoforum) { + monoforum->clearAllUnreadReactions(); } unreadReactions().clear(); return; @@ -811,6 +894,8 @@ void History::clearUnreadReactionsFor(MsgId topicRootId) { if (const auto topic = forum->topicFor(topicRootId)) { topic->unreadReactions().clear(); } + } else if (monoforum) { + sublist->unreadReactions().clear(); } const auto &ids = unreadReactionsIds(); if (ids.empty()) { @@ -822,7 +907,8 @@ void History::clearUnreadReactionsFor(MsgId topicRootId) { items.reserve(ids.size()); for (const auto &id : ids) { if (const auto item = owner->message(peerId, id)) { - if (item->topicRootId() == topicRootId) { + if ((topicRootId && item->topicRootId() == topicRootId) + || (sublist && item->sublistPeerId() == sublistPeerId)) { items.emplace(id); } } @@ -850,6 +936,7 @@ not_null<HistoryItem*> History::addNewToBack( storage.add(Storage::SharedMediaAddExisting( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId types, item->id, { from, till })); @@ -861,6 +948,7 @@ not_null<HistoryItem*> History::addNewToBack( storage.add(Storage::SharedMediaAddExisting( peer->id, topic->rootId(), + PeerId(), // monoforumPeerId types, item->id, { item->id, item->id})); @@ -868,6 +956,18 @@ not_null<HistoryItem*> History::addNewToBack( topic->setHasPinnedMessages(true); } } + if (const auto sublist = item->savedSublist()) { + storage.add(Storage::SharedMediaAddExisting( + peer->id, + MsgId(), // topicRootId + item->sublistPeerId(), + types, + item->id, + { item->id, item->id })); + if (pinned) { + sublist->setHasPinnedMessages(true); + } + } } } if (item->from()->id) { @@ -1134,7 +1234,8 @@ void History::applyServiceChanges( if (id && item) { session().storage().add(Storage::SharedMediaAddSlice( peer->id, - MsgId(0), + MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, { id }, { id, ServerMaxMsgId })); @@ -1143,11 +1244,22 @@ void History::applyServiceChanges( session().storage().add(Storage::SharedMediaAddSlice( peer->id, topic->rootId(), + PeerId(), // monoforumPeerId Storage::SharedMediaType::Pinned, { id }, { id, ServerMaxMsgId })); topic->setHasPinnedMessages(true); } + if (const auto sublist = item->savedSublist()) { + session().storage().add(Storage::SharedMediaAddSlice( + peer->id, + MsgId(), // topicRootId + item->sublistPeerId(), + Storage::SharedMediaType::Pinned, + { id }, + { id, ServerMaxMsgId })); + sublist->setHasPinnedMessages(true); + } } }, [&](const MTPDmessageReplyStoryHeader &data) { LOG(("API Error: story reply in messageActionPinMessage.")); @@ -1241,6 +1353,28 @@ void History::applyServiceChanges( Core::App().calls().showConferenceInvite(user, item->id); } } + }, [&](const MTPDmessageActionTodoCompletions &data) { + if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + const auto list = done->msg + ? done->msg + : owner().message(peer, done->msgId); + if (const auto media = list ? list->media() : nullptr) { + if (const auto todolist = media->todolist()) { + todolist->apply(item, data); + } + } + } + }, [&](const MTPDmessageActionTodoAppendTasks &data) { + if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + const auto list = done->msg + ? done->msg + : owner().message(peer, done->msgId); + if (const auto media = list ? list->media() : nullptr) { + if (const auto todolist = media->todolist()) { + todolist->apply(data); + } + } + } }, [](const auto &) { }); } @@ -1320,6 +1454,9 @@ void History::newItemAdded(not_null<HistoryItem*> item) { if (const auto topic = item->topic()) { topic->applyItemAdded(item); } + if (const auto sublist = item->savedSublist()) { + sublist->applyItemAdded(item); + } if (const auto media = item->media()) { if (const auto gift = media->gift()) { if (const auto unique = gift->unique.get()) { @@ -1419,6 +1556,7 @@ void History::addEdgesToSharedMedia() { session().storage().add(Storage::SharedMediaAddSlice( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId type, {}, { from, till })); @@ -1632,6 +1770,7 @@ void History::addToSharedMedia( session().storage().add(Storage::SharedMediaAddSlice( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId type, std::move(medias[i]), { from, till })); @@ -2084,7 +2223,7 @@ void History::applyPinnedUpdate(const MTPDupdateDialogPinned &data) { TimeId History::adjustedChatListTimeId() const { const auto result = chatListTimeId(); - if (const auto draft = cloudDraft(MsgId(0))) { + if (const auto draft = cloudDraft(MsgId(), PeerId())) { if (!peer->forum() && !Data::DraftIsNull(draft) && !session().supportMode()) { @@ -2239,6 +2378,9 @@ History *History::migrateSibling() const { Dialogs::UnreadState History::chatListUnreadState() const { if (const auto forum = peer->forum()) { return AdjustedForumUnreadState(forum->topicsList()->unreadState()); + } else if (const auto monoforum = peer->monoforum()) { + return AdjustedForumUnreadState( + monoforum->unreadStateWithParentMuted()); } return computeUnreadState(); } @@ -2250,6 +2392,12 @@ Dialogs::BadgesState History::chatListBadgesState() const { forum->topicsList()->unreadState(), Dialogs::CountInBadge::Chats, Dialogs::IncludeInBadge::UnmutedOrAll)); + } else if (const auto monoforum = peer->monoforum()) { + return adjustBadgesStateByFolder( + Dialogs::BadgesForUnread( + monoforum->unreadStateWithParentMuted(), + Dialogs::CountInBadge::Chats, + Dialogs::IncludeInBadge::All)); } return computeBadgesState(); } @@ -2339,6 +2487,9 @@ bool History::chatListMessageKnown() const { } const QString &History::chatListName() const { + if (const auto broadcast = peer->monoforumBroadcast()) { + return broadcast->name(); + } return peer->name(); } @@ -2869,7 +3020,8 @@ void History::applyDialog( Data::ApplyPeerCloudDraft( &session(), peer->id, - MsgId(0), // topicRootId + MsgId(), // topicRootId + PeerId(), // monoforumPeerId draft->c_draftMessage()); } if (const auto ttl = data.vttl_period()) { @@ -2891,7 +3043,7 @@ void History::dialogEntryApplied() { return; } if (!chatListMessage()) { - clear(ClearType::Unload); + clear(ClearType::Unload, true); addNewerSlice(QVector<MTPMessage>()); addOlderSlice(QVector<MTPMessage>()); if (const auto channel = peer->asChannel()) { @@ -3004,8 +3156,70 @@ void History::applyDialogTopMessage(MsgId topMessageId) { } } +void History::tryMarkMonoforumIntervalRead( + MsgId wasInboxReadBefore, + MsgId nowInboxReadBefore) { + if (!amMonoforumAdmin() || (nowInboxReadBefore <= wasInboxReadBefore)) { + return; + } else if (loadedAtBottom() && nowInboxReadBefore >= minMsgId()) { + // Count for each sublist how many messages are still not read. + auto counts = base::flat_map<not_null<Data::SavedSublist*>, int>(); + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (!item->isRegular() || item->id < nowInboxReadBefore) { + continue; + } + if (const auto sublist = item->savedSublist()) { + ++counts[sublist]; + } + } + } + if (const auto monoforum = peer->monoforum()) { + monoforum->updateUnreadCounts(nowInboxReadBefore - 1, counts); + } + } else if (minMsgId() <= wasInboxReadBefore + && maxMsgId() >= nowInboxReadBefore) { + // Count for each sublist how many messages were read. + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (!item->isRegular() || item->id < wasInboxReadBefore) { + continue; + } else if (item->id >= nowInboxReadBefore) { + break; + } + if (const auto sublist = item->savedSublist()) { + const auto unread = sublist->unreadCountCurrent(); + if (unread > 0) { + sublist->setInboxReadTill(item->id, unread - 1); + } + } + } + } + } else { + // We can't invalidate sublist unread counts here, because no read + // request was yet sent to the server (so it can't return correct + // values yet), we need to do that after we send read request. + _flags |= Flag::MonoforumUnreadInvalidatePending; + } +} + +void History::validateMonoforumUnread(MsgId readTillId) { + if (!(_flags & Flag::MonoforumUnreadInvalidatePending)) { + return; + } + _flags &= ~Flag::MonoforumUnreadInvalidatePending; + if (!amMonoforumAdmin()) { + return; + } else if (const auto monoforum = peer->monoforum()) { + monoforum->markUnreadCountsUnknown(readTillId); + } +} + void History::setInboxReadTill(MsgId upTo) { if (_inboxReadBefore) { + tryMarkMonoforumIntervalRead(*_inboxReadBefore, upTo + 1); accumulate_max(*_inboxReadBefore, upTo + 1); } else { _inboxReadBefore = upTo + 1; @@ -3099,14 +3313,20 @@ void History::forceFullResize() { _flags |= Flag::HasPendingResizedItems; } -Data::Thread *History::threadFor(MsgId topicRootId) { +Data::Thread *History::threadFor(MsgId topicRootId, PeerId monoforumPeerId) { return topicRootId ? peer->forumTopicFor(topicRootId) + : monoforumPeerId + ? peer->monoforumSublistFor(monoforumPeerId) : static_cast<Data::Thread*>(this); } -const Data::Thread *History::threadFor(MsgId topicRootId) const { - return const_cast<History*>(this)->threadFor(topicRootId); +const Data::Thread *History::threadFor( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return const_cast<History*>(this)->threadFor( + topicRootId, + monoforumPeerId); } void History::forumChanged(Data::Forum *old) { @@ -3135,7 +3355,7 @@ void History::forumChanged(Data::Forum *old) { } else { _flags &= ~Flag::IsForum; } - if (cloudDraft(MsgId(0))) { + if (cloudDraft(MsgId(), PeerId())) { updateChatListSortPosition(); } _flags |= Flag::PendingAllItemsResize; @@ -3145,6 +3365,46 @@ bool History::isForum() const { return (_flags & Flag::IsForum); } +void History::monoforumChanged(Data::SavedMessages *old) { + if (inChatList()) { + notifyUnreadStateChange(old + ? AdjustedForumUnreadState(old->chatsList()->unreadState()) + : computeUnreadState()); + } + + if (const auto monoforum = peer->monoforum()) { + _flags |= Flag::IsMonoforumAdmin; + + monoforum->chatsList()->unreadStateChanges( + ) | rpl::filter([=] { + return (_flags & Flag::IsMonoforumAdmin) && inChatList(); + }) | rpl::map( + AdjustedForumUnreadState + ) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { + notifyUnreadStateChange(old); + }, monoforum->lifetime()); + + monoforum->chatsListChanges( + ) | rpl::start_with_next([=] { + updateChatListEntry(); + }, monoforum->lifetime()); + } else { + _flags &= ~Flag::IsMonoforumAdmin; + } + if (cloudDraft(MsgId(), PeerId())) { + updateChatListSortPosition(); + } + _flags |= Flag::PendingAllItemsResize; +} + +bool History::amMonoforumAdmin() const { + return (_flags & Flag::IsMonoforumAdmin); +} + +bool History::suggestDraftAllowed() const { + return peer->isMonoforum() && !peer->amMonoforumAdmin(); +} + not_null<History*> History::migrateToOrMe() const { if (const auto to = peer->migrateTo()) { return owner().history(to); @@ -3235,9 +3495,10 @@ Data::HistoryMessages *History::maybeMessages() { HistoryItem *History::insertJoinedMessage() { const auto channel = peer->asChannel(); if (!channel + || channel->isMonoforum() || _joinedMessage || !channel->amIn() - || (peer->isMegagroup() + || (channel->isMegagroup() && channel->mgInfo->joinedMessageFound)) { return _joinedMessage; } @@ -3557,7 +3818,7 @@ std::vector<MsgId> History::collectMessagesFromParticipantToDelete( return result; } -void History::clear(ClearType type) { +void History::clear(ClearType type, bool markEmpty) { _unreadBarView = nullptr; _firstUnreadView = nullptr; removeJoinedMessage(); @@ -3567,7 +3828,7 @@ void History::clear(ClearType type) { owner().notifyHistoryUnloaded(this); lastKeyboardInited = false; if (type == ClearType::Unload) { - _loadedAtTop = _loadedAtBottom = false; + _loadedAtTop = _loadedAtBottom = markEmpty; } else { // Leave the 'sending' messages in local messages. auto local = base::flat_set<not_null<HistoryItem*>>(); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 57b1203d02..a36d2000c6 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -27,12 +27,14 @@ struct LanguageId; namespace Data { struct Draft; +class Forum; class Session; class Folder; class ChatFilter; struct SponsoredFrom; class SponsoredMessages; class HistoryMessages; +class SavedMessages; } // namespace Data namespace Dialogs { @@ -60,8 +62,12 @@ public: [[nodiscard]] not_null<History*> owningHistory() override { return this; } - [[nodiscard]] Data::Thread *threadFor(MsgId topicRootId); - [[nodiscard]] const Data::Thread *threadFor(MsgId topicRootId) const; + [[nodiscard]] Data::Thread *threadFor( + MsgId topicRootId, + PeerId monoforumPeerId); + [[nodiscard]] const Data::Thread *threadFor( + MsgId topicRootId, + PeerId monoforumPeerId) const; [[nodiscard]] auto delegateMixin() const -> not_null<HistoryMainElementDelegateMixin*> { @@ -71,6 +77,10 @@ public: void forumChanged(Data::Forum *old); [[nodiscard]] bool isForum() const; + void monoforumChanged(Data::SavedMessages *old); + [[nodiscard]] bool amMonoforumAdmin() const; + [[nodiscard]] bool suggestDraftAllowed() const; + [[nodiscard]] not_null<History*> migrateToOrMe() const; [[nodiscard]] History *migrateFrom() const; [[nodiscard]] MsgRange rangeForDifferenceRequest() const; @@ -101,7 +111,7 @@ public: DeleteChat, ClearHistory, }; - void clear(ClearType type); + void clear(ClearType type, bool markEmpty = false); void clearUpTill(MsgId availableMinId); void applyGroupAdminChanges(const base::flat_set<UserId> &changes); @@ -130,8 +140,9 @@ public: void destroyMessage(not_null<HistoryItem*> item); void destroyMessagesByDates(TimeId minDate, TimeId maxDate); void destroyMessagesByTopic(MsgId topicRootId); + void destroyMessagesBySublist(not_null<PeerData*> sublistPeer); - void unpinMessagesFor(MsgId topicRootId); + void unpinMessagesFor(MsgId topicRootId, PeerId monoforumPeerId); not_null<HistoryItem*> addNewMessage( MsgId id, @@ -268,13 +279,15 @@ public: void setHasPendingResizedItems(); [[nodiscard]] auto sendActionPainter() - -> not_null<HistoryView::SendActionPainter*> override { + -> HistoryView::SendActionPainter* override { return &_sendActionPainter; } void clearLastKeyboard(); void clearUnreadMentionsFor(MsgId topicRootId); - void clearUnreadReactionsFor(MsgId topicRootId); + void clearUnreadReactionsFor( + MsgId topicRootId, + Data::SavedSublist *sublist); Data::Draft *draft(Data::DraftKey key) const; void setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft); @@ -283,60 +296,89 @@ public: [[nodiscard]] const Data::HistoryDrafts &draftsMap() const; void setDraftsMap(Data::HistoryDrafts &&map); - Data::Draft *localDraft(MsgId topicRootId) const { - return draft(Data::DraftKey::Local(topicRootId)); + Data::Draft *localDraft( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return draft(Data::DraftKey::Local(topicRootId, monoforumPeerId)); } - Data::Draft *localEditDraft(MsgId topicRootId) const { - return draft(Data::DraftKey::LocalEdit(topicRootId)); + Data::Draft *localEditDraft( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return draft( + Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId)); } - Data::Draft *cloudDraft(MsgId topicRootId) const { - return draft(Data::DraftKey::Cloud(topicRootId)); + Data::Draft *cloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return draft(Data::DraftKey::Cloud(topicRootId, monoforumPeerId)); } void setLocalDraft(std::unique_ptr<Data::Draft> &&draft) { setDraft( - Data::DraftKey::Local(draft->reply.topicRootId), + Data::DraftKey::Local( + draft->reply.topicRootId, + draft->reply.monoforumPeerId), std::move(draft)); } void setLocalEditDraft(std::unique_ptr<Data::Draft> &&draft) { setDraft( - Data::DraftKey::LocalEdit(draft->reply.topicRootId), + Data::DraftKey::LocalEdit( + draft->reply.topicRootId, + draft->reply.monoforumPeerId), std::move(draft)); } void setCloudDraft(std::unique_ptr<Data::Draft> &&draft) { setDraft( - Data::DraftKey::Cloud(draft->reply.topicRootId), + Data::DraftKey::Cloud( + draft->reply.topicRootId, + draft->reply.monoforumPeerId), std::move(draft)); } - void clearLocalDraft(MsgId topicRootId) { - clearDraft(Data::DraftKey::Local(topicRootId)); + void clearLocalDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + clearDraft(Data::DraftKey::Local(topicRootId, monoforumPeerId)); } - void clearCloudDraft(MsgId topicRootId) { - clearDraft(Data::DraftKey::Cloud(topicRootId)); + void clearCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + clearDraft(Data::DraftKey::Cloud(topicRootId, monoforumPeerId)); } - void clearLocalEditDraft(MsgId topicRootId) { - clearDraft(Data::DraftKey::LocalEdit(topicRootId)); + void clearLocalEditDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + clearDraft(Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId)); } void clearDrafts(); Data::Draft *createCloudDraft( MsgId topicRootId, + PeerId monoforumPeerId, const Data::Draft *fromDraft); [[nodiscard]] bool skipCloudDraftUpdate( MsgId topicRootId, + PeerId monoforumPeerId, TimeId date) const; - void startSavingCloudDraft(MsgId topicRootId); - void finishSavingCloudDraft(MsgId topicRootId, TimeId savedAt); + void startSavingCloudDraft(MsgId topicRootId, PeerId monoforumPeerId); + void finishSavingCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId, + TimeId savedAt); void takeLocalDraft(not_null<History*> from); - void applyCloudDraft(MsgId topicRootId); - void draftSavedToCloud(MsgId topicRootId); + void applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId); + void draftSavedToCloud(MsgId topicRootId, PeerId monoforumPeerId); void requestChatListMessage(); [[nodiscard]] const Data::ForwardDraft &forwardDraft( - MsgId topicRootId) const; + MsgId topicRootId, + PeerId monoforumPeerId) const; [[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft( const Data::ForwardDraft &draft) const; [[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft( - MsgId topicRootId); - void setForwardDraft(MsgId topicRootId, Data::ForwardDraft &&draft); + MsgId topicRootId, + PeerId monoforumPeerId); + void setForwardDraft( + MsgId topicRootId, + PeerId monoforumPeerId, + Data::ForwardDraft &&draft); History *migrateSibling() const; [[nodiscard]] bool useTopPromotion() const; @@ -389,6 +431,10 @@ public: // Interface for Data::Histories. void setInboxReadTill(MsgId upTo); std::optional<int> countStillUnreadLocal(MsgId readTillId) const; + void tryMarkMonoforumIntervalRead( + MsgId wasInboxReadBefore, + MsgId nowInboxReadBefore); + void validateMonoforumUnread(MsgId readTillId); [[nodiscard]] bool isTopPromoted() const; @@ -425,14 +471,16 @@ public: private: friend class HistoryBlock; - enum class Flag : uchar { + enum class Flag : ushort { HasPendingResizedItems = (1 << 0), PendingAllItemsResize = (1 << 1), IsTopPromoted = (1 << 2), IsForum = (1 << 3), - FakeUnreadWhileOpened = (1 << 4), - HasPinnedMessages = (1 << 5), - ResolveChatListMessage = (1 << 6), + IsMonoforumAdmin = (1 << 4), + FakeUnreadWhileOpened = (1 << 5), + HasPinnedMessages = (1 << 6), + ResolveChatListMessage = (1 << 7), + MonoforumUnreadInvalidatePending = (1 << 8), }; using Flags = base::flags<Flag>; friend inline constexpr auto is_flag_type(Flag) { @@ -542,7 +590,9 @@ private: void viewReplaced(not_null<const Element*> was, Element *now); - void createLocalDraftFromCloud(MsgId topicRootId); + void createLocalDraftFromCloud( + MsgId topicRootId, + PeerId monoforumPeerId); HistoryItem *insertJoinedMessage(); void insertMessageToBlocks(not_null<HistoryItem*> item); @@ -600,9 +650,9 @@ private: std::unique_ptr<HistoryTranslation> _translation; Data::HistoryDrafts _drafts; - base::flat_map<MsgId, TimeId> _acceptCloudDraftsAfter; - base::flat_map<MsgId, int> _savingCloudDraftRequests; - Data::ForwardDrafts _forwardDrafts; + base::flat_map<Data::DraftKey, TimeId> _acceptCloudDraftsAfter; + base::flat_map<Data::DraftKey, int> _savingCloudDraftRequests; + base::flat_map<Data::DraftKey, Data::ForwardDraft> _forwardDrafts; QString _topPromotedMessage; QString _topPromotedType; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 3e2a8983cb..afadf42c55 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" -#include "boxes/moderate_messages_box.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions.h" @@ -54,7 +54,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/statistics/info_statistics_widget.h" #include "boxes/about_sponsored_box.h" #include "boxes/delete_messages_box.h" +#include "boxes/moderate_messages_box.h" #include "boxes/report_messages_box.h" +#include "boxes/send_gif_with_caption_box.h" #include "boxes/star_gift_box.h" // ShowStarGiftBox #include "boxes/sticker_set_box.h" #include "boxes/translate_box.h" @@ -75,12 +77,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "apiwrap.h" #include "api/api_attached_stickers.h" +#include "api/api_suggest_post.h" #include "api/api_toggling_media.h" #include "api/api_who_reacted.h" #include "api/api_views.h" #include "lang/lang_keys.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_document.h" #include "data/data_channel.h" @@ -92,6 +96,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_click_handler.h" #include "data/data_histories.h" #include "data/data_changes.h" +#include "data/data_todo_list.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" @@ -247,8 +252,9 @@ public: _widget->elementHandleViaClick(bot); } } - bool elementIsChatWide() override { - return _widget ? _widget->elementIsChatWide() : false; + HistoryView::ElementChatMode elementChatMode() override { + using Mode = HistoryView::ElementChatMode; + return _widget ? _widget->elementChatMode() : Mode::Default; } not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override { Expects(_widget != nullptr); @@ -859,7 +865,11 @@ bool HistoryInner::canHaveFromUserpics() const { } else if (const auto channel = _peer->asBroadcast()) { return channel->signatureProfiles(); } - return true; + return _isChatWide || !_removeFromUserpics; +} + +void HistoryInner::toggleRemoveFromUserpics(bool remove) { + _removeFromUserpics = remove; } template <typename Method> @@ -967,6 +977,62 @@ void HistoryInner::enumerateDates(Method method) { enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback); } +template <typename Method> +void HistoryInner::enumerateMonoforumSenders(Method method) { + if (!_history->amMonoforumAdmin()) { + return; + } + + const auto skip = (_scrollDateOpacity.animating() || _scrollDateShown) + ? int(base::SafeRound( + (_scrollDateOpacity.value(_scrollDateShown ? 1. : 0.) + * (st::msgServicePadding.bottom() + + st::msgServiceFont->height + + st::msgServicePadding.top() + + st::msgServiceMargin.top())))) + : 0; + + // Find and remember the bottom of an single-day messages pack + // -1 means we didn't find a same-day with previous message yet. + auto lowestInOneBunchItemBottom = -1; + + auto senderCallback = [&](not_null<Element*> view, int itemtop, int itembottom) { + const auto item = view->data(); + if (lowestInOneBunchItemBottom < 0 && view->isInOneBunchWithPrevious()) { + lowestInOneBunchItemBottom = itembottom - view->marginBottom(); + } + + // Call method on a sender for all messages that have it and for those who are not showing it + // because they are in a one day together with the previous message if they are top-most visible. + if (view->displayMonoforumSender() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) { + if (lowestInOneBunchItemBottom < 0) { + lowestInOneBunchItemBottom = itembottom - view->marginBottom(); + } + // Attach sender to the top of the visible area with the same margin as it has in service message. + int senderTop = qMax(itemtop + view->displayedDateHeight(), _visibleAreaTop + skip) + st::msgServiceMargin.top(); + + // Do not let the sender go below the single-sender messages pack bottom line. + int senderHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); + senderTop = qMin(senderTop, lowestInOneBunchItemBottom - senderHeight); + + // Call the template callback function that was passed + // and return if it finished everything it needed. + if (!method(view, itemtop, senderTop)) { + return false; + } + } + + // Forget the found bottom of the pack, search for the next one from scratch. + if (!view->isInOneBunchWithPrevious()) { + lowestInOneBunchItemBottom = -1; + } + + return true; + }; + + enumerateItems<EnumItemsDirection::BottomToTop>(senderCallback); +} + TextSelection HistoryInner::computeRenderSelection( not_null<const SelectedItems*> selected, not_null<Element*> view) const { @@ -1350,14 +1416,6 @@ void HistoryInner::paintEvent(QPaintEvent *e) { const auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); - //QDate lastDate; - //if (!_history->isEmpty()) { - // lastDate = _history->blocks.back()->messages.back()->data()->date.date(); - //} - - //// if item top is before this value always show date as a floating date - //int showFloatingBefore = height() - 2 * (_visibleAreaBottom - _visibleAreaTop) - dateHeight; - auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) { // stop the enumeration if the date is above the painted rect @@ -1371,21 +1429,13 @@ void HistoryInner::paintEvent(QPaintEvent *e) { const auto correctDateTop = itemtop + st::msgServiceMargin.top(); dateInPlace = (dateTop < correctDateTop + dateHeight); } - //bool noFloatingDate = (item->date.date() == lastDate && displayDate); - //if (noFloatingDate) { - // if (itemtop < showFloatingBefore) { - // noFloatingDate = false; - // } - //} // paint the date if it intersects the painted rect if (dateTop < clip.top() + clip.height()) { - auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity; + auto opacity = dateInPlace ? 1. : scrollDateOpacity; if (opacity > 0.) { p.setOpacity(opacity); - const auto dateY = false // noFloatingDate - ? itemtop - : (dateTop - st::msgServiceMargin.top()); + const auto dateY = dateTop - st::msgServiceMargin.top(); if (const auto date = view->Get<HistoryView::DateBadge>()) { date->paint(p, context.st, dateY, _contentWidth, _isChatWide); } else { @@ -1403,6 +1453,38 @@ void HistoryInner::paintEvent(QPaintEvent *e) { }); p.setOpacity(1.); + enumerateMonoforumSenders([&](not_null<Element*> view, int itemtop, int senderTop) { + // stop the enumeration if the sender is above the painted rect + if (senderTop + dateHeight <= clip.top()) { + return false; + } + + const auto displaySender = view->displayMonoforumSender(); + auto senderInPlace = displaySender; + if (senderInPlace) { + const auto correctSenderTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top(); + senderInPlace = (senderTop < correctSenderTop + st::msgServiceMargin.top()); + } + + // paint the sender if it intersects the painted rect + if (senderTop < clip.top() + clip.height()) { + const auto senderY = senderTop - st::msgServiceMargin.top(); + if (const auto sender = view->Get<HistoryView::MonoforumSenderBar>()) { + sender->paint(p, context.st, senderY, _contentWidth, _isChatWide, !senderInPlace); + } else { + HistoryView::MonoforumSenderBar::PaintFor( + p, + context.st, + view, + _monoforumSenderUserpicView, + senderY, + _contentWidth, + _isChatWide); + } + } + return true; + }); + _reactionsManager->paint(p, context); } @@ -2715,6 +2797,20 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }; + const auto addTodoListAction = [&](HistoryItem *item) { + if (!item || !Window::PeerMenuShowAddTodoListTasks(item)) { + return; + } + const auto itemId = item->fullId(); + _menu->addAction( + tr::lng_todo_add_title(tr::now), + crl::guard(this, [=] { + if (const auto item = session->data().message(itemId)) { + Window::PeerMenuAddTodoListTasks(_controller, item); + } + }), + &st::menuIconCreateTodoList); + }; const auto lnkPhoto = link ? reinterpret_cast<PhotoData*>( link->property(kPhotoLinkMediaProperty).toULongLong()) @@ -2790,9 +2886,21 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { forwardItem(itemId); }, &st::menuIconForward); } + if (HistoryView::CanAddOfferToMessage(item)) { + _menu->addAction(tr::lng_context_add_offer(tr::now), [=] { + Api::AddOfferToMessage(_controller->uiShow(), itemId); + }, &st::menuIconTagSell); + } if (item->canDelete()) { const auto callback = [=] { deleteItem(itemId); }; if (item->isUploading()) { + if (item->media() + && item->media()->allowsEditCaption()) { + _menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { editCaptionUploadLayer(item); }, + &st::menuIconEdit); + } _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( @@ -2878,6 +2986,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { addItemActions(item, item); } else { addReplyAction(partItemOrLeader); + addTodoListAction(partItemOrLeader); addItemActions(item, albumPartItem); if (item && !isUponSelected) { const auto media = (view ? view->media() : nullptr); @@ -3032,11 +3141,23 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { forwardAsGroup(itemId); }, &st::menuIconForward); } + if (HistoryView::CanAddOfferToMessage(item)) { + _menu->addAction(tr::lng_context_add_offer(tr::now), [=] { + Api::AddOfferToMessage(_controller->uiShow(), itemId); + }, &st::menuIconTagSell); + } if (canDelete) { const auto callback = [=] { deleteAsGroup(itemId); }; if (item->isUploading()) { + if (item->media() + && item->media()->allowsEditCaption()) { + _menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { editCaptionUploadLayer(item); }, + &st::menuIconEdit); + } _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( @@ -3085,7 +3206,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { leaderOrSelf, _controller); } else if (leaderOrSelf) { - HistoryView::MaybeAddWhenEditedForwardedAction(_menu, leaderOrSelf); + HistoryView::MaybeAddWhenEditedForwardedAction( + _menu, + leaderOrSelf, + _controller); } if (_menu->empty()) { @@ -3173,6 +3297,14 @@ void HistoryInner::copySelectedText() { } } +void HistoryInner::editCaptionUploadLayer(not_null<HistoryItem*> item) { + if (const auto view = viewByItem(item)) { + if (item->isUploading()) { + _controller->uiShow()->show(Box(Ui::EditCaptionBox, view)); + } + } +} + void HistoryInner::savePhotoToFile(not_null<PhotoData*> photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { @@ -3618,6 +3750,9 @@ void HistoryInner::toggleScrollDateShown() { void HistoryInner::repaintScrollDateCallback() { int updateTop = _visibleAreaTop; int updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); + if (_history->amMonoforumAdmin()) { + updateHeight *= 2; + } update(0, updateTop, width(), updateHeight); } @@ -3907,8 +4042,13 @@ void HistoryInner::elementHandleViaClick(not_null<UserData*> bot) { _widget->insertBotCommand('@' + bot->username()); } -bool HistoryInner::elementIsChatWide() { - return _isChatWide; +HistoryView::ElementChatMode HistoryInner::elementChatMode() { + using Mode = HistoryView::ElementChatMode; + return _isChatWide + ? Mode::Wide + : _removeFromUserpics + ? Mode::Narrow + : Mode::Default; } not_null<Ui::PathShiftGradient*> HistoryInner::elementPathShiftGradient() { @@ -4508,6 +4648,10 @@ void HistoryInner::refreshAboutView(bool force) { session().api().requestFullPeer(user); } } + } else if (const auto monoforum = _peer->asChannel()) { + if (monoforum->isMonoforum() && !monoforum->amMonoforumAdmin()) { + refresh(); + } } } @@ -4834,21 +4978,26 @@ QString HistoryInner::tooltipText() const { if (const auto view = Element::Hovered()) { return HistoryView::DateTooltipText(view); } - } else if (_mouseCursorState == CursorState::Forwarded + } + if (_mouseCursorState == CursorState::Forwarded && _mouseAction == MouseAction::None) { if (const auto view = Element::Moused()) { if (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) { return forwarded->text.toString(); } } - } else if (const auto lnk = ClickHandler::getActive()) { + } + if (const auto lnk = ClickHandler::getActive()) { using namespace HistoryView::Reactions; const auto count = ReactionCountOfLink(_dragStateItem, lnk); if (count.count && count.shortened) { return Lang::FormatCountDecimal(count.count); } - return lnk->tooltip(); - } else if (const auto view = Element::Moused()) { + if (const auto text = lnk->tooltip(); !text.isEmpty()) { + return text; + } + } + if (const auto view = Element::Moused()) { StateRequest request; const auto local = mapFromGlobal(_mousePosition); const auto point = _widget->clampMousePosition(local); @@ -4938,9 +5087,15 @@ bool CanSendReply(not_null<const HistoryItem*> item) { } const auto peer = item->history()->peer; - const auto topic = item->topic(); - return topic - ? Data::CanSendAnything(topic) - : (Data::CanSendAnything(peer) - && (!peer->isChannel() || peer->asChannel()->amIn())); + if (const auto topic = item->topic()) { + return Data::CanSendAnything(topic); + } else if (!Data::CanSendAnything(peer)) { + return false; + } else if (const auto channel = peer->asChannel()) { + if (const auto sublist = item->savedSublist()) { + return (sublist->sublistPeer() != peer); + } + return channel->amIn(); + } + return true; } diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index d6364607f3..36b774e98b 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/dragging_scroll_manager.h" #include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" +#include "ui/userpic_view.h" #include "history/view/history_view_top_bar_widget.h" #include <QtGui/QPainterPath> @@ -34,6 +35,7 @@ struct SelectionModeResult; struct StateRequest; enum class CursorState : char; enum class PointState : char; +enum class ElementChatMode : char; class EmptyPainter; class Element; class TranslateTracker; @@ -164,7 +166,7 @@ public: const QString &query, const FullMsgId &context); void elementHandleViaClick(not_null<UserData*> bot); - bool elementIsChatWide(); + HistoryView::ElementChatMode elementChatMode(); not_null<Ui::PathShiftGradient*> elementPathShiftGradient(); void elementReplyTo(const FullReplyTo &to); void elementStartInteraction(not_null<const Element*> view); @@ -192,6 +194,7 @@ public: void setChooseReportReason(Data::ReportInput reportInput); void clearChooseReportReason(); + void toggleRemoveFromUserpics(bool remove); // -1 if should not be visible, -2 if bad history() [[nodiscard]] int itemTop(const HistoryItem *item) const; @@ -253,7 +256,8 @@ private: using ChosenReaction = HistoryView::Reactions::ChosenReaction; using VideoUserpic = Dialogs::Ui::VideoUserpic; - using SelectedItems = std::map<HistoryItem*, TextSelection, std::less<>>; + using SelectedItems + = base::flat_map<HistoryItem*, TextSelection, std::less<>>; enum class MouseAction { None, PrepareDrag, @@ -279,7 +283,7 @@ private: // for each found message (in given direction) in the passed history with passed top offset. // // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <bool TopToBottom, typename Method> void enumerateItemsInHistory(History *history, int historytop, Method method); @@ -299,7 +303,7 @@ private: // for each found userpic (from the top to the bottom) using enumerateItems() method. // // Method has "bool (*Method)(not_null<Element*> view, int userpicTop)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <typename Method> void enumerateUserpics(Method method); @@ -307,10 +311,18 @@ private: // for each found date element (from the bottom to the top) using enumerateItems() method. // // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int dateTop)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <typename Method> void enumerateDates(Method method); + // This function finds all monoforum sender elements that are displayed and calls template method + // for each found date element (from the bottom to the top) using enumerateItems() method. + // + // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int dateTop)" signature + // if it returns false the enumeration stops immediately. + template <typename Method> + void enumerateMonoforumSenders(Method method); + void scrollDateCheck(); void scrollDateHideByTimer(); bool canHaveFromUserpics() const; @@ -418,6 +430,7 @@ private: void blockSenderItem(FullMsgId itemId); void blockSenderAsGroup(FullMsgId itemId); void copySelectedText(); + void editCaptionUploadLayer(not_null<HistoryItem*> item); [[nodiscard]] auto reactionButtonParameters( not_null<const Element*> view, @@ -457,6 +470,7 @@ private: int _contentWidth = 0; int _historyPaddingTop = 0; int _revealHeight = 0; + Ui::PeerUserpicView _monoforumSenderUserpicView; // Save visible area coords for painting / pressing userpics. int _visibleAreaTop = 0; @@ -482,6 +496,7 @@ private: const std::unique_ptr<Ui::PathShiftGradient> _pathGradient; QPainterPath _highlightPathCache; bool _isChatWide = false; + bool _removeFromUserpics = false; base::flat_set<not_null<const HistoryItem*>> _animatedStickersPlayed; base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0fe7853547..e4c570d014 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "api/api_sensitive_content.h" #include "lang/lang_keys.h" -#include "mainwidget.h" #include "calls/calls_instance.h" // Core::App().calls().joinGroupCall. #include "history/view/history_view_item_preview.h" #include "history/view/history_view_message.h" @@ -28,8 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits_graphics.h" // ShowRefundInfoBox. #include "storage/file_upload.h" #include "storage/storage_shared_media.h" -#include "main/main_account.h" -#include "main/main_domain.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "menu/menu_ttl_validator.h" @@ -65,6 +62,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. +#include "data/data_todo_list.h" #include "data/data_stories.h" #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -72,7 +70,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_non_panel_process.h" // ProcessNonPanelPaymentFormFactory. #include "platform/platform_notifications_manager.h" #include "spellcheck/spellcheck_highlight_syntax.h" -#include "styles/style_dialogs.h" // AyuGram includes #include "ayu/ayu_settings.h" @@ -200,6 +197,7 @@ struct HistoryItem::CreateConfig { TimeId editDate = 0; HistoryMessageMarkupData markup; HistoryMessageRepliesData replies; + HistoryMessageSuggestInfo suggest; bool imported = false; // For messages created from existing messages (forwarded). @@ -368,6 +366,10 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia( return std::make_unique<Data::MediaPoll>( item, item->history()->owner().processPoll(media)); + }, [&](const MTPDmessageMediaToDo &media) -> Result { + return std::make_unique<Data::MediaTodoList>( + item, + item->history()->owner().processTodoList(item->fullId(), media)); }, [&](const MTPDmessageMediaDice &media) -> Result { return std::make_unique<Data::MediaDice>( item, @@ -596,6 +598,7 @@ HistoryItem::HistoryItem( const auto topicRootId = fields.replyTo.topicRootId; config.reply.messageId = config.reply.topMessageId = topicRootId; config.reply.topicPost = (topicRootId != 0) ? 1 : 0; + config.reply.monoforumPeerId = fields.replyTo.monoforumPeerId; if (const auto originalReply = original->Get<HistoryMessageReply>()) { if (originalReply->external()) { config.reply = originalReply->fields().clone(this); @@ -604,6 +607,7 @@ HistoryItem::HistoryItem( } } } + config.suggest = fields.suggest; if (!dropForwardInfo) { config.originalDate = original->originalDate(); if (const auto info = original->originalHiddenSenderInfo()) { @@ -847,9 +851,6 @@ HistoryItem::~HistoryItem() { if (const auto reply = Get<HistoryMessageReply>()) { reply->clearData(this); } - if (const auto saved = Get<HistoryMessageSaved>()) { - saved->sublist->removeOne(this); - } clearDependencyMessage(); applyTTL(0); } @@ -879,6 +880,14 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { return same; } else if (const auto results = Get<HistoryServiceGiveawayResults>()) { return results; + } else if (const auto done = Get<HistoryServiceTodoCompletions>()) { + return done; + } else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) { + return append; + } else if (const auto decision = Get<HistoryServiceSuggestDecision>()) { + return decision; + } else if (const auto finish = Get<HistoryServiceSuggestFinish>()) { + return finish; } return nullptr; } @@ -936,6 +945,10 @@ void HistoryItem::updateDependentServiceText() { updateServiceText(prepareGameScoreText()); } else if (Has<HistoryServicePayment>()) { updateServiceText(preparePaymentSentText()); + } else if (Has<HistoryServiceTodoCompletions>()) { + updateServiceText(prepareTodoCompletionsText()); + } else if (Has<HistoryServiceTodoAppendTasks>()) { + updateServiceText(prepareTodoAppendTasksText()); } } @@ -1115,8 +1128,58 @@ bool HistoryItem::checkDiscussionLink(ChannelId id) const { return false; } -void HistoryItem::setReplyMarkup(HistoryMessageMarkupData &&markup) { +SuggestionActions HistoryItem::computeSuggestionActions() const { + return computeSuggestionActions(Get<HistoryMessageSuggestedPost>()); +} + +SuggestionActions HistoryItem::computeSuggestionActions( + const HistoryMessageSuggestedPost *suggest) const { + return suggest + ? computeSuggestionActions(suggest->accepted, suggest->rejected) + : SuggestionActions::None; +} + +SuggestionActions HistoryItem::computeSuggestionActions( + bool accepted, + bool rejected) const { + const auto channelIsAuthor = from()->isChannel(); + const auto amMonoforumAdmin = history()->peer->amMonoforumAdmin(); + const auto broadcast = history()->peer->monoforumBroadcast(); + const auto canDecline = isRegular() + && !(accepted || rejected) + && (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin); + const auto canAccept = canDecline + && (channelIsAuthor + ? !amMonoforumAdmin + : (amMonoforumAdmin + && broadcast + && broadcast->canPostMessages())); + return canAccept + ? SuggestionActions::AcceptAndDecline + : canDecline + ? SuggestionActions::Decline + : SuggestionActions::None; +} + +void HistoryItem::updateSuggestControls( + const HistoryMessageSuggestedPost *suggest) { + if (const auto markup = Get<HistoryMessageReplyMarkup>()) { + markup->updateSuggestControls(computeSuggestionActions(suggest)); + } +} + +void HistoryItem::setReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons) { const auto requestUpdate = [&] { + const auto actions = computeSuggestionActions(); + if (actions != SuggestionActions::None + && !Has<HistoryMessageReplyMarkup>()) { + AddComponents(HistoryMessageReplyMarkup::Bit()); + } + if (const auto markup = Get<HistoryMessageReplyMarkup>()) { + markup->updateSuggestControls(actions); + } history()->owner().requestItemResize(this); history()->session().changes().messageUpdated( this, @@ -1552,6 +1615,9 @@ void HistoryItem::markReactionsRead() { if (const auto topic = this->topic()) { topic->updateChatListEntry(); topic->unreadReactions().erase(id); + } else if (const auto sublist = this->savedSublist()) { + sublist->updateChatListEntry(); + sublist->unreadReactions().erase(id); } } @@ -1586,6 +1652,7 @@ void HistoryItem::setIsPinned(bool pinned) { storage.add(Storage::SharedMediaAddExisting( _history->peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, id, { id, id })); @@ -1594,11 +1661,22 @@ void HistoryItem::setIsPinned(bool pinned) { storage.add(Storage::SharedMediaAddExisting( _history->peer->id, topic->rootId(), + PeerId(), // monoforumPeerId Storage::SharedMediaType::Pinned, id, { id, id })); topic->setHasPinnedMessages(true); } + if (const auto sublist = this->savedSublist()) { + storage.add(Storage::SharedMediaAddExisting( + _history->peer->id, + MsgId(0), // topicRootId + sublistPeerId(), + Storage::SharedMediaType::Pinned, + id, + { id, id })); + sublist->setHasPinnedMessages(true); + } } else { _flags &= ~MessageFlag::Pinned; if (_flags & MessageFlag::StoryItem) { @@ -1640,6 +1718,14 @@ bool HistoryItem::isEditingMedia() const { return Has<HistoryMessageSavedMediaData>(); } +PaidPostType HistoryItem::paidType() const { + return (_flags & MessageFlag::StarsPaidSuggested) + ? PaidPostType::Stars + : (_flags & MessageFlag::TonPaidSuggested) + ? PaidPostType::Ton + : PaidPostType::None; +} + void HistoryItem::clearSavedMedia() { RemoveComponents(HistoryMessageSavedMediaData::Bit()); } @@ -1771,6 +1857,10 @@ bool HistoryItem::isAyuNoForwards() const { return _flags & MessageFlag::AyuNoForwards; } +bool HistoryItem::canLookupMessageAuthor() const { + return isRegular() && _history->amMonoforumAdmin() && _from->isChannel(); +} + bool HistoryItem::skipNotification() const { if (isSilent() && (_flags & MessageFlag::IsContactSignUp)) { return true; @@ -1778,6 +1868,8 @@ bool HistoryItem::skipNotification() const { if (forwarded->imported) { return true; } + } else if (canLookupMessageAuthor()) { + return true; } return false; } @@ -1932,6 +2024,23 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { } } + if (!edition.useSameSuggest) { + if (edition.suggest.exists) { + if (!Has<HistoryMessageSuggestedPost>()) { + AddComponents(HistoryMessageSuggestedPost::Bit()); + } + auto suggest = Get<HistoryMessageSuggestedPost>(); + suggest->price = edition.suggest.price; + suggest->date = edition.suggest.date; + suggest->accepted = edition.suggest.accepted; + suggest->rejected = edition.suggest.rejected; + updateSuggestControls(suggest); + } else { + RemoveComponents(HistoryMessageSuggestedPost::Bit()); + updateSuggestControls(nullptr); + } + } + applyTTL(edition.ttl); setFactcheck(FromMTP(this, edition.mtpFactcheck)); @@ -1968,7 +2077,7 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { const auto wasSublist = savedSublist(); if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); - setReplyMarkup({}); + setReplyMarkup({}, true); removeFromSharedMediaIndex(); refreshMedia(nullptr); setTextValue({}); @@ -2161,6 +2270,7 @@ void HistoryItem::applyEditionToHistoryCleared() { MTP_int(id), peerToMTP(PeerId(0)), // from_id peerToMTP(_history->peer->id), + MTPPeer(), // saved_peer_id MTPMessageReplyHeader(), MTP_int(date()), MTP_messageActionHistoryClear(), @@ -2169,8 +2279,10 @@ void HistoryItem::applyEditionToHistoryCleared() { ).c_messageService()); } -void HistoryItem::updateReplyMarkup(HistoryMessageMarkupData &&markup) { - setReplyMarkup(std::move(markup)); +void HistoryItem::updateReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons) { + setReplyMarkup(std::move(markup), ignoreSuggestButtons); } void HistoryItem::contributeToSlowmode(TimeId realDate) { @@ -2237,9 +2349,13 @@ void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) { } } if (reaction) { + const auto sublist = this->savedSublist(); const auto toHistory = history->unreadReactions().add(id, type); const auto toTopic = topic && topic->unreadReactions().add(id, type); - if (toHistory || toTopic) { + const auto toSublist = sublist + && sublist->parentChat() + && sublist->unreadReactions().add(id, type); + if (toHistory || toTopic || toSublist) { if (type == HistoryUnreadThings::AddType::New) { changes->messageUpdated( this, @@ -2256,6 +2372,11 @@ void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) { topic, Data::TopicUpdate::Flag::UnreadReactions); } + if (toSublist) { + changes->sublistUpdated( + sublist, + Data::SublistUpdate::Flag::UnreadReactions); + } } } } @@ -2272,6 +2393,8 @@ void HistoryItem::destroyHistoryEntry() { history()->unreadReactions().erase(id); if (const auto topic = this->topic()) { topic->unreadReactions().erase(id); + } else if (const auto sublist = this->savedSublist()) { + sublist->unreadReactions().erase(id); } } if (isRegular() && _history->peer->isMegagroup()) { @@ -2310,6 +2433,7 @@ void HistoryItem::addToSharedMediaIndex() { _history->session().storage().add(Storage::SharedMediaAddNew( _history->peer->id, topicRootId(), + sublistPeerId(), types, id)); if (types.test(Storage::SharedMediaType::Pinned)) { @@ -2317,6 +2441,9 @@ void HistoryItem::addToSharedMediaIndex() { if (const auto topic = this->topic()) { topic->setHasPinnedMessages(true); } + if (const auto sublist = this->savedSublist()) { + sublist->setHasPinnedMessages(true); + } } } } @@ -2437,7 +2564,8 @@ bool HistoryItem::allowsSendNow() const { && isScheduled() && !isSending() && !hasFailed() - && !isEditingMedia(); + && !isEditingMedia() + && (paidType() == PaidPostType::None); } bool HistoryItem::allowsReschedule() const { @@ -2468,7 +2596,8 @@ bool HistoryItem::allowsEdit(TimeId now) const { && !isTooOldForEdit(now) && (!_media || _media->allowsEdit()) && !isLegacyMessage() - && !isEditingMedia(); + && !isEditingMedia() + && (paidType() == PaidPostType::None); } bool HistoryItem::allowsEditMedia() const { @@ -2676,11 +2805,26 @@ Data::SendError HistoryItem::errorTextForForward( } else if (requiresInline && !Data::CanSend(to, kInline)) { const auto forInline = Data::RestrictionError(peer, kInline); return forInline ? forInline : tr::lng_forward_cant(tr::now); - } else if (_media + } else if (const auto specific = errorTextForForwardIgnoreRights(to)) { + return specific; + } else if (!Data::CanSend(to, requiredRight, false)) { + return tr::lng_forward_cant(tr::now); + } + return {}; +} + +Data::SendError HistoryItem::errorTextForForwardIgnoreRights( + not_null<Data::Thread*> to) const { + const auto peer = to->peer(); + if (_media && _media->poll() && _media->poll()->publicVotes() && peer->isBroadcast()) { return tr::lng_restricted_send_public_polls(tr::now); + } else if (_media + && _media->todolist() + && (peer->isBroadcast() || peer->isMonoforum())) { + return tr::lng_restricted_send_todo_lists(tr::now); } else if (_media && _media->invoice() && _media->invoice()->isPaidMedia @@ -2688,8 +2832,6 @@ Data::SendError HistoryItem::errorTextForForward( && peer->isFullLoaded() && !peer->asBroadcast()->canPostPaidMedia()) { return tr::lng_restricted_send_paid_media(tr::now); - } else if (!Data::CanSend(to, requiredRight, false)) { - return tr::lng_forward_cant(tr::now); } return {}; } @@ -3617,8 +3759,12 @@ FullStoryId HistoryItem::replyToStory() const { } FullReplyTo HistoryItem::replyTo() const { + const auto monoforumPeerId = _history->peer->amMonoforumAdmin() + ? sublistPeerId() + : PeerId(); auto result = FullReplyTo{ .topicRootId = topicRootId(), + .monoforumPeerId = monoforumPeerId, }; if (const auto reply = Get<HistoryMessageReply>()) { const auto &fields = reply->fields(); @@ -3740,24 +3886,43 @@ bool HistoryItem::isEmpty() const { } Data::SavedSublist *HistoryItem::savedSublist() const { - if (const auto saved = Get<HistoryMessageSaved>()) { - return saved->sublist; + if (isBusinessShortcut()) { + return nullptr; + } else if (const auto saved = Get<HistoryMessageSaved>()) { + if (saved->savedMessagesSublist) { + return saved->savedMessagesSublist; + } else if (const auto monoforum = _history->peer->monoforum()) { + const auto peer = _history->owner().peer(saved->sublistPeerId); + return monoforum->sublist(peer).get(); + } } else if (_history->peer->isSelf()) { const auto sublist = _history->owner().savedMessages().sublist( _history->peer); const auto that = const_cast<HistoryItem*>(this); that->AddComponents(HistoryMessageSaved::Bit()); - that->Get<HistoryMessageSaved>()->sublist = sublist; + const auto saved = that->Get<HistoryMessageSaved>(); + saved->sublistPeerId = _history->peer->id; + saved->savedMessagesSublist = sublist; + return sublist; + } else if (const auto monoforum = _history->peer->monoforum()) { + const auto sublist = monoforum->sublist(_from); + const auto that = const_cast<HistoryItem*>(this); + that->AddComponents(HistoryMessageSaved::Bit()); + that->Get<HistoryMessageSaved>()->sublistPeerId = _from->id; return sublist; } return nullptr; } -PeerData *HistoryItem::savedSublistPeer() const { - if (const auto sublist = savedSublist()) { - return sublist->peer(); +PeerId HistoryItem::sublistPeerId() const { + if (const auto saved = Get<HistoryMessageSaved>()) { + return saved->sublistPeerId; + } else if (_history->peer->isSelf()) { + return _history->peer->id; + } else if (_history->peer->monoforum()) { + return _from->id; } - return nullptr; + return PeerId(); } PeerData *HistoryItem::savedFromSender() const { @@ -3946,7 +4111,11 @@ void HistoryItem::createComponents(CreateConfig &&config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } - if (_history->peer->isSelf()) { + const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin(); + if (!isBusinessShortcut() + && (_history->peer->isSelf() + || config.savedSublistPeer + || requiresMonoforumPeer)) { mask |= HistoryMessageSaved::Bit(); } if (!config.restrictions.empty()) { @@ -3955,12 +4124,25 @@ void HistoryItem::createComponents(CreateConfig &&config) { mask |= HistoryMessageRestrictions::Bit(); } } + if (config.suggest.exists) { + mask |= HistoryMessageSuggestedPost::Bit(); + if (computeSuggestionActions( + config.suggest.accepted, + config.suggest.rejected + ) != SuggestionActions::None) { + mask |= HistoryMessageReplyMarkup::Bit(); + } + } UpdateComponents(mask); if (const auto saved = Get<HistoryMessageSaved>()) { if (!config.savedSublistPeer) { - if (config.savedFromPeer) { + if (config.reply.monoforumPeerId) { + config.savedSublistPeer = config.reply.monoforumPeerId; + } else if (!_history->peer->isSelf()) { + config.savedSublistPeer = _from->id; + } else if (config.savedFromPeer) { config.savedSublistPeer = config.savedFromPeer; } else if (config.originalSenderId) { config.savedSublistPeer = config.originalSenderId; @@ -3970,8 +4152,12 @@ void HistoryItem::createComponents(CreateConfig &&config) { config.savedSublistPeer = _history->session().userPeerId(); } } - const auto peer = _history->owner().peer(config.savedSublistPeer); - saved->sublist = _history->owner().savedMessages().sublist(peer); + saved->sublistPeerId = config.savedSublistPeer; + if (_history->peer->isSelf()) { + saved->savedMessagesSublist + = _history->owner().savedMessages().sublist( + _history->owner().peer(saved->sublistPeerId)); + } } if (const auto reply = Get<HistoryMessageReply>()) { @@ -4041,6 +4227,14 @@ void HistoryItem::createComponents(CreateConfig &&config) { flagSensitiveContent(); } + if (const auto suggest = Get<HistoryMessageSuggestedPost>()) { + suggest->price = config.suggest.price; + suggest->date = config.suggest.date; + suggest->accepted = config.suggest.accepted; + suggest->rejected = config.suggest.rejected; + updateSuggestControls(suggest); + } + if (out() && isSending()) { if (const auto channel = _history->peer->asMegagroup()) { _boostsApplied = channel->mgInfo->boostsApplied; @@ -4090,14 +4284,6 @@ void HistoryItem::setupForwardedComponent(const CreateConfig &config) { forwarded->savedFromMsgId = config.savedFromMsgId; forwarded->savedFromSender = _history->owner().peerLoaded( config.savedFromSenderId); - if (forwarded->savedFromPeer - && !forwarded->savedFromPeer->isFullLoaded() - && forwarded->savedFromPeer->isChannel()) { - _history->session().api().requestFullPeer(forwarded->savedFromPeer); - } else if (config.savedFromPeer) { - _history->session().api().requestFullPeer( - _history->owner().peer(config.savedFromPeer)); - } forwarded->savedFromOutgoing = config.savedFromOutgoing; if (!forwarded->savedFromSender && !config.savedFromSenderName.isEmpty()) { @@ -4201,6 +4387,11 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { ? replyTo.messageId.peer : PeerId(); const auto to = LookupReplyTo(_history, replyTo.messageId); + config.reply.monoforumPeerId = (to && to->sublistPeerId()) + ? to->sublistPeerId() + : replyTo.monoforumPeerId + ? replyTo.monoforumPeerId + : PeerId(); const auto replyToTop = replyTo.topicRootId ? replyTo.topicRootId : LookupReplyToTop(_history, to); @@ -4238,6 +4429,9 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { if (fields.flags & MessageFlag::HasViews) { config.viewsCount = 1; } + if (fields.suggest.exists) { + config.suggest = fields.suggest; + } createComponents(std::move(config)); } @@ -4362,6 +4556,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { config.postAuthor = qs(data.vpost_author().value_or_empty()); config.restrictions = Data::UnavailableReason::Extract( data.vrestriction_reason()); + config.suggest = HistoryMessageSuggestInfo(data.vsuggested_post()); createComponents(std::move(config)); } @@ -4640,12 +4835,53 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { refund->transactionId = qs(data.vcharge().data().vid()); const auto id = fullId(); refund->link = std::make_shared<LambdaClickHandler>([=]( - ClickContext context) { + ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); if (const auto window = my.sessionWindow.get()) { Settings::ShowRefundInfoBox(window, id); } }); + } else if (type == mtpc_messageActionTodoCompletions) { + const auto &data = action.c_messageActionTodoCompletions(); + UpdateComponents(HistoryServiceTodoCompletions::Bit()); + const auto done = Get<HistoryServiceTodoCompletions>(); + done->completed = data.vcompleted().v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; + done->incompleted = data.vincompleted().v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; + } else if (type == mtpc_messageActionTodoAppendTasks) { + const auto session = &_history->session(); + const auto &data = action.c_messageActionTodoAppendTasks(); + UpdateComponents(HistoryServiceTodoAppendTasks::Bit()); + const auto append = Get<HistoryServiceTodoAppendTasks>(); + append->list = ranges::views::all( + data.vlist().v + ) | ranges::views::transform([&](const MTPTodoItem &item) { + return TodoListItemFromMTP(session, item); + }) | ranges::to_vector; + } else if (type == mtpc_messageActionSuggestedPostApproval) { + const auto &data = action.c_messageActionSuggestedPostApproval(); + UpdateComponents(HistoryServiceSuggestDecision::Bit()); + const auto decision = Get<HistoryServiceSuggestDecision>(); + decision->price = CreditsAmountFromTL(data.vprice()); + decision->balanceTooLow = data.is_balance_too_low(); + decision->rejected = data.is_rejected(); + decision->rejectComment = qs(data.vreject_comment().value_or_empty()); + decision->date = data.vschedule_date().value_or_empty(); + } else if (type == mtpc_messageActionSuggestedPostSuccess) { + const auto &data = action.c_messageActionSuggestedPostSuccess(); + UpdateComponents(HistoryServiceSuggestFinish::Bit()); + const auto finish = Get<HistoryServiceSuggestFinish>(); + finish->successPrice = CreditsAmountFromTL(data.vprice()); + } else if (type == mtpc_messageActionSuggestedPostRefund) { + const auto &data = action.c_messageActionSuggestedPostRefund(); + UpdateComponents(HistoryServiceSuggestFinish::Bit()); + const auto finish = Get<HistoryServiceSuggestFinish>(); + finish->refundType = data.is_payer_initiated() + ? SuggestRefundType::User + : SuggestRefundType::Admin; } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -4668,6 +4904,25 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { }, [](const MTPDmessageReplyStoryHeader &data) { }); } + + const auto savedSublistPeer = message.vsaved_peer_id() + ? peerFromMTP(*message.vsaved_peer_id()) + : PeerId(); + const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin(); + if (!isBusinessShortcut() + && (savedSublistPeer || requiresMonoforumPeer)) { + AddComponents(HistoryMessageSaved::Bit()); + const auto saved = Get<HistoryMessageSaved>(); + saved->sublistPeerId = savedSublistPeer + ? savedSublistPeer + : _from->id; + if (_history->peer->isSelf()) { + saved->savedMessagesSublist + = _history->owner().savedMessages().sublist( + _history->owner().peer(saved->sublistPeerId)); + } + } + setServiceMessageByAction(action); } @@ -4788,20 +5043,30 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareChatCreate = [this](const MTPDmessageActionChatCreate &action) { auto result = PreparedServiceText(); - result.links.push_back(fromLink()); - result.text = tr::lng_action_created_chat( - tr::now, - lt_from, - fromLinkText(), // Link 1. - lt_title, - { .text = qs(action.vtitle()) }, - Ui::Text::WithEntities); + if (_history->peer->isMonoforum()) { + result.text = tr::lng_action_created_monoforum( + tr::now, + Ui::Text::WithEntities); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_created_chat( + tr::now, + lt_from, + fromLinkText(), // Link 1. + lt_title, + { .text = qs(action.vtitle()) }, + Ui::Text::WithEntities); + } return result; }; auto prepareChannelCreate = [this](const MTPDmessageActionChannelCreate &action) { auto result = PreparedServiceText(); - if (isPost()) { + if (_history->peer->isMonoforum()) { + result.text = tr::lng_action_created_monoforum( + tr::now, + Ui::Text::WithEntities); + } else if (isPost()) { result.text = tr::lng_action_created_channel( tr::now, Ui::Text::WithEntities); @@ -5680,6 +5945,43 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareGiftTon = [&]( + const MTPDmessageActionGiftTon &action) { + auto result = PreparedServiceText(); + const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto peer = isSelf ? _history->peer : _from; + _history->session().giftBoxStickersPacks().tonLoad(); + const auto amount = action.vamount().v; + const auto currency = qs(action.vcurrency()); + const auto cost = AmountAndStarCurrency( + &_history->session(), + amount, + currency); + const auto anonymous = _from->isServiceUser(); + if (anonymous) { + result.text = tr::lng_action_gift_received_anonymous( + tr::now, + lt_cost, + cost, + Ui::Text::WithEntities); + } else { + result.links.push_back(peer->createOpenLink()); + result.text = isSelf + ? tr::lng_action_gift_sent(tr::now, + lt_cost, + cost, + Ui::Text::WithEntities) + : tr::lng_action_gift_received( + tr::now, + lt_user, + Ui::Text::Link(peer->shortName(), 1), // Link 1. + lt_cost, + cost, + Ui::Text::WithEntities); + } + return result; + }; + auto prepareGiftPrize = [&]( const MTPDmessageActionPrizeStars &action) { auto result = PreparedServiceText(); @@ -5930,8 +6232,23 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto preparePaidMessagesPrice = [&](const MTPDmessageActionPaidMessagesPrice &action) { const auto stars = action.vstars().v; + const auto broadcastAllowed = action.is_broadcast_messages_allowed(); auto result = PreparedServiceText(); - result.text = stars + result.text = _history->peer->isBroadcast() + ? (stars > 0 + ? tr::lng_action_direct_messages_paid( + tr::now, + lt_count, + stars, + Ui::Text::WithEntities) + : broadcastAllowed + ? tr::lng_action_direct_messages_enabled( + tr::now, + Ui::Text::WithEntities) + : tr::lng_action_direct_messages_disabled( + tr::now, + Ui::Text::WithEntities)) + : stars ? tr::lng_action_message_price_paid( tr::now, lt_count, @@ -5943,6 +6260,45 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &) { + return prepareTodoCompletionsText(); + }; + + auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &) { + return prepareTodoAppendTasksText(); + }; + + auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &data) { + return PreparedServiceText{ { data.is_rejected() + ? tr::lng_action_post_rejected(tr::now) + : data.is_balance_too_low() + ? tr::lng_action_not_enough_funds(tr::now) + : tr::lng_suggest_action_agreement(tr::now) } }; + }; + + auto prepareSuggestedPostSuccess = [&](const MTPDmessageActionSuggestedPostSuccess &data) { + const auto price = CreditsAmountFromTL(&data.vprice()); + auto result = PreparedServiceText(); + result.links.push_back(_from->createOpenLink()); + result.text = (price.stars() + ? tr::lng_action_suggest_success_stars + : tr::lng_action_suggest_success_ton)( + tr::now, + lt_count_decimal, + price.value(), + lt_from, + Ui::Text::Link(_from->shortName(), 1), + Ui::Text::WithEntities); + return result; + }; + + auto prepareSuggestedPostRefund = [&](const MTPDmessageActionSuggestedPostRefund &data) { + return PreparedServiceText{ { data.is_payer_initiated() + ? tr::lng_action_suggest_refund_user(tr::now) + : tr::lng_action_suggest_refund_admin(tr::now) + } }; + }; + auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { Unexpected("PhoneCall type in setServiceMessageFromMtp."); }; @@ -5991,12 +6347,18 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareBoostApply, preparePaymentRefunded, prepareGiftStars, + prepareGiftTon, prepareGiftPrize, prepareStarGift, prepareStarGiftUnique, preparePaidMessagesRefunded, preparePaidMessagesPrice, prepareConferenceCall, + prepareTodoCompletions, + prepareTodoAppendTasks, + prepareSuggestedPostApproval, + prepareSuggestedPostSuccess, + prepareSuggestedPostRefund, PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>, PrepareErrorText<MTPDmessageActionEmpty>)); @@ -6109,6 +6471,12 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { _from, Data::GiftType::Credits, data.vstars().v); + }, [&](const MTPDmessageActionGiftTon &data) { + _media = std::make_unique<Data::MediaGiftBox>( + this, + _from, + Data::GiftType::Ton, + data.vcrypto_amount().v); }, [&](const MTPDmessageActionPrizeStars &data) { _media = std::make_unique<Data::MediaGiftBox>( this, @@ -6606,6 +6974,92 @@ PreparedServiceText HistoryItem::prepareCallScheduledText( return result; } +PreparedServiceText HistoryItem::composeTodoIncompleted( + not_null<HistoryServiceTodoCompletions*> done) { + const auto tasks = ComposeTodoTasksList(done->msg, done->incompleted); + if (out()) { + return { + tr::lng_action_todo_marked_not_done_self( + tr::now, + lt_tasks, + tasks, + Ui::Text::WithEntities), + }; + } + return { + .text = tr::lng_action_todo_marked_not_done( + tr::now, + lt_from, + fromLinkText(), + lt_tasks, + tasks, + Ui::Text::WithEntities), + .links = { fromLink() }, + }; +} + +PreparedServiceText HistoryItem::composeTodoCompleted( + not_null<HistoryServiceTodoCompletions*> done) { + const auto tasks = ComposeTodoTasksList(done->msg, done->completed); + if (out()) { + return { + tr::lng_action_todo_marked_done_self( + tr::now, + lt_tasks, + tasks, + Ui::Text::WithEntities), + }; + } + return { + .text = tr::lng_action_todo_marked_done( + tr::now, + lt_from, + fromLinkText(), + lt_tasks, + tasks, + Ui::Text::WithEntities), + .links = { fromLink() }, + }; +} + +PreparedServiceText HistoryItem::prepareTodoCompletionsText() { + auto result = PreparedServiceText(); + const auto done = Get<HistoryServiceTodoCompletions>(); + Assert(done != nullptr); + + return done->completed.empty() + ? composeTodoIncompleted(done) + : composeTodoCompleted(done); +} + +PreparedServiceText HistoryItem::prepareTodoAppendTasksText() { + auto result = PreparedServiceText(); + auto append = Get<HistoryServiceTodoAppendTasks>(); + Assert(append != nullptr); + + const auto tasks = ComposeTodoTasksList(append); + if (out()) { + return { + tr::lng_action_todo_added_self( + tr::now, + lt_tasks, + tasks, + Ui::Text::WithEntities), + }; + } + return { + .text = tr::lng_action_todo_added( + tr::now, + lt_from, + fromLinkText(), + lt_tasks, + tasks, + Ui::Text::WithEntities), + .links = { fromLink() }, + }; + return result; +} + TextWithEntities HistoryItem::fromLinkText() const { return Ui::Text::Link(st::wrap_rtl(_from->name()), 1); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 63822da59e..b107d96512 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -22,18 +22,15 @@ struct HistoryMessageMarkupData; struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; struct HistoryMessageForwarded; -struct HistoryMessageSavedMediaData; -struct HistoryMessageFactcheck; +struct HistoryMessageSuggestedPost; struct HistoryServiceDependentData; +struct HistoryServiceTodoCompletions; enum class HistorySelfDestructType; struct PreparedServiceText; struct MessageFactcheck; class ReplyKeyboard; struct LanguageId; - -namespace Api { -struct SendOptions; -} // namespace Api +enum class SuggestionActions : uchar; namespace base { template <typename Enum> @@ -45,15 +42,6 @@ enum class SharedMediaType : signed char; using SharedMediaTypesMask = base::enum_mask<SharedMediaType>; } // namespace Storage -namespace Ui { -class RippleAnimation; -} // namespace Ui - -namespace style { -struct BotKeyboardButton; -struct RippleAnimation; -} // namespace style - namespace Data { struct MessagePosition; struct RecentReaction; @@ -71,24 +59,11 @@ struct PaidReactionSend; struct SendError; } // namespace Data -namespace Main { -class Session; -} // namespace Main - -namespace Window { -class SessionController; -} // namespace Window - namespace HistoryUnreadThings { enum class AddType; } // namespace HistoryUnreadThings namespace HistoryView { -struct TextState; -struct StateRequest; -enum class CursorState : char; -enum class PointState : char; -enum class Context : char; class ElementDelegate; class Element; class Message; @@ -109,6 +84,7 @@ struct HistoryItemCommonFields { uint64 groupedId = 0; EffectId effectId = 0; HistoryMessageMarkupData markup; + HistoryMessageSuggestInfo suggest; bool ignoreForwardFrom = false; bool ignoreForwardCaptions = false; }; @@ -119,6 +95,12 @@ enum class HistoryReactionSource : char { Existing, }; +enum class PaidPostType : uchar { + None, + Stars, + Ton, +}; + class HistoryItem final : public RuntimeComposer<HistoryItem> { public: [[nodiscard]] static std::unique_ptr<Data::Media> CreateMedia( @@ -207,9 +189,7 @@ public: [[nodiscard]] bool isFromScheduled() const; [[nodiscard]] bool isScheduled() const; [[nodiscard]] bool isSponsored() const; - - [[nodiscard]] bool isAyuNoForwards() const; - + [[nodiscard]] bool canLookupMessageAuthor() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; [[nodiscard]] BusinessShortcutId shortcutId() const; @@ -217,6 +197,8 @@ public: void setRealShortcutId(BusinessShortcutId id); void setCustomServiceLink(ClickHandlerPtr link); + [[nodiscard]] bool isAyuNoForwards() const; + void addLogEntryOriginal( WebPageId localId, const QString &label, @@ -350,6 +332,7 @@ public: [[nodiscard]] bool hasUnpaidContent() const; [[nodiscard]] bool inHighlightProcess() const; void highlightProcessDone(); + [[nodiscard]] PaidPostType paidType() const; void setCommentsInboxReadTill(MsgId readTillId); void setCommentsMaxId(MsgId maxId); @@ -380,7 +363,9 @@ public: void overrideMedia(std::unique_ptr<Data::Media> media); void applyEditionToHistoryCleared(); - void updateReplyMarkup(HistoryMessageMarkupData &&markup); + void updateReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons = false); void contributeToSlowmode(TimeId realDate = 0); void clearMediaAsExpired(); @@ -458,6 +443,8 @@ public: [[nodiscard]] bool requiresSendInlineRight() const; [[nodiscard]] Data::SendError errorTextForForward( not_null<Data::Thread*> to) const; + [[nodiscard]] Data::SendError errorTextForForwardIgnoreRights( + not_null<Data::Thread*> to) const; [[nodiscard]] const HistoryMessageTranslation *translation() const; [[nodiscard]] bool translationShowRequiresCheck(LanguageId to) const; bool translationShowRequiresRequest(LanguageId to); @@ -526,7 +513,7 @@ public: [[nodiscard]] MsgId originalId() const; [[nodiscard]] Data::SavedSublist *savedSublist() const; - [[nodiscard]] PeerData *savedSublistPeer() const; + [[nodiscard]] PeerId sublistPeerId() const; [[nodiscard]] PeerData *savedFromSender() const; [[nodiscard]] const HiddenSenderInfo *savedFromHiddenSenderInfo() const; @@ -571,9 +558,12 @@ public: [[nodiscard]] bool canUpdateDate() const; void customEmojiRepaint(); - [[nodiscard]] int unsupportedTTL() const { - return _unsupportedTTL; - } + [[nodiscard]] SuggestionActions computeSuggestionActions() const; + [[nodiscard]] SuggestionActions computeSuggestionActions( + const HistoryMessageSuggestedPost *suggest) const; + [[nodiscard]] SuggestionActions computeSuggestionActions( + bool accepted, + bool rejected) const; [[nodiscard]] bool needsUpdateForVideoQualities(const MTPMessage &data); @@ -581,6 +571,10 @@ public: return _ttlDestroyAt; } + [[nodiscard]] int unsupportedTTL() const { + return _unsupportedTTL; + } + [[nodiscard]] int boostsApplied() const { return _boostsApplied; } @@ -610,7 +604,10 @@ private: [[nodiscard]] bool checkDiscussionLink(ChannelId id) const; - void setReplyMarkup(HistoryMessageMarkupData &&markup); + void setReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons = false); + void updateSuggestControls(const HistoryMessageSuggestedPost *suggest); void changeReplyToTopCounter( not_null<HistoryMessageReply*> reply, @@ -680,6 +677,13 @@ private: CallId linkCallId); [[nodiscard]] PreparedServiceText prepareCallScheduledText( TimeId scheduleDate); + [[nodiscard]] PreparedServiceText prepareTodoCompletionsText(); + [[nodiscard]] PreparedServiceText prepareTodoAppendTasksText(); + + [[nodiscard]] PreparedServiceText composeTodoIncompleted( + not_null<HistoryServiceTodoCompletions*> done); + [[nodiscard]] PreparedServiceText composeTodoCompleted( + not_null<HistoryServiceTodoCompletions*> done); [[nodiscard]] PreparedServiceText prepareServiceTextForMessage( const MTPMessageMedia &media, diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 8ec2505a50..0c7d1f0cff 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_click_handler.h" #include "data/data_session.h" #include "data/data_stories.h" +#include "data/data_todo_list.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" @@ -74,6 +75,38 @@ base::options::toggle FastButtonsModeOption({ .description = "Trigger inline keyboard buttons by 1-9 keyboard keys.", }); +[[nodiscard]] TextWithEntities ComposeTodoTasksList( + int fullCount, + const std::vector<TextWithEntities> &names) { + const auto count = int(names.size()); + if (!count) { + return tr::lng_action_todo_tasks_fallback( + tr::now, + lt_count, + fullCount, + Ui::Text::WithEntities); + } else if (count == 1) { + return names.front(); + } + auto full = names.front(); + for (auto i = 1; i != count - 1; ++i) { + full = tr::lng_action_todo_tasks_and_one( + tr::now, + lt_tasks, + full, + lt_task, + names[i], + Ui::Text::WithEntities); + } + return tr::lng_action_todo_tasks_and_last( + tr::now, + lt_tasks, + full, + lt_task, + names.back(), + Ui::Text::WithEntities); +} + } // namespace const char kOptionFastButtonsMode[] = "fast-buttons-mode"; @@ -200,7 +233,7 @@ bool HiddenSenderInfo::paintCustomUserpic( image.isNull() ? nullptr : &image, image.isNull() ? &emptyUserpic : nullptr, size * style::DevicePixelRatio(), - false); + Ui::PeerUserpicShape::Circle); p.drawImage(QRect(x, y, size, size), view.cached); return valid; } @@ -421,6 +454,13 @@ FullReplyTo ReplyToFromMTP( }; } return FullReplyTo(); + }, [&](const MTPDinputReplyToMonoForum &data) { + const auto parsed = Data::PeerFromInputMTP( + &history->owner(), + data.vmonoforum_peer_id()); + return FullReplyTo{ + .monoforumPeerId = parsed ? parsed->id : PeerId(), + }; }); } @@ -741,17 +781,26 @@ ReplyKeyboard::ReplyKeyboard( for (auto j = 0; j != rowSize; ++j) { auto button = Button(); using Type = HistoryMessageMarkupButton::Type; - const auto isBuy = (row[j].type == Type::Buy); static const auto RegExp = QRegularExpression("\\b" + Ui::kCreditsCurrency + "\\b"); - const auto text = isBuy + const auto type = row[j].type; + const auto text = (type == Type::Buy) ? base::duplicate(row[j].text).replace( RegExp, QChar(0x2B50)) : row[j].text; + const auto withEmoji = [&](const style::IconEmoji &icon) { + return Ui::Text::IconEmoji(&icon).append(text); + }; const auto textWithEntities = [&] { - if (!isBuy) { + if (type == Type::SuggestAccept) { + return withEmoji(st::chatSuggestAcceptIcon); + } else if (type == Type::SuggestDecline) { + return withEmoji(st::chatSuggestDeclineIcon); + } else if (type == Type::SuggestChange) { + return withEmoji(st::chatSuggestChangeIcon); + } else if (type != Type::Buy) { return TextWithEntities(); } auto result = TextWithEntities(); @@ -767,7 +816,7 @@ ReplyKeyboard::ReplyKeyboard( ? TextWithEntities() : result; }(); - button.type = row.at(j).type; + button.type = type; button.link = std::make_shared<ReplyMarkupClickHandler>( owner, i, @@ -1196,6 +1245,95 @@ bool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const { return false; } +void HistoryMessageReplyMarkup::updateSuggestControls( + SuggestionActions actions) { + if (actions == SuggestionActions::AcceptAndDecline) { + data.flags |= ReplyMarkupFlag::SuggestionAccept; + } else { + data.flags &= ~ReplyMarkupFlag::SuggestionAccept; + } + if (actions == SuggestionActions::None) { + data.flags &= ~ReplyMarkupFlag::SuggestionDecline; + } else { + data.flags |= ReplyMarkupFlag::Inline + | ReplyMarkupFlag::SuggestionDecline; + } + using Type = HistoryMessageMarkupButton::Type; + const auto has = [&](Type type) { + return !data.rows.empty() + && ranges::contains( + data.rows.back(), + type, + &HistoryMessageMarkupButton::type); + }; + if (actions == SuggestionActions::AcceptAndDecline) { + // ... rows ... + // [decline] | [accept] + // [suggestchanges] + if (has(Type::SuggestChange)) { + // Nothing changed. + } else { + if (has(Type::SuggestDecline)) { + data.rows.pop_back(); + } + data.rows.push_back({ + { + Type::SuggestDecline, + tr::lng_suggest_action_decline(tr::now), + }, + { + Type::SuggestAccept, + tr::lng_suggest_action_accept(tr::now), + }, + }); + data.rows.push_back({ { + Type::SuggestChange, + tr::lng_suggest_action_change(tr::now), + } }); + data.flags |= ReplyMarkupFlag::SuggestionAccept + | ReplyMarkupFlag::SuggestionDecline; + } + if (data.rows.size() > 2) { + data.flags |= ReplyMarkupFlag::SuggestionSeparator; + } else { + data.flags &= ~ReplyMarkupFlag::SuggestionSeparator; + } + } else { + while (!data.rows.empty()) { + if (has(Type::SuggestChange) || has(Type::SuggestAccept)) { + data.rows.pop_back(); + } else if (has(Type::SuggestDecline) + && actions == SuggestionActions::None) { + data.rows.pop_back(); + } else { + break; + } + } + data.flags &= ~ReplyMarkupFlag::SuggestionAccept; + if (actions == SuggestionActions::None) { + data.flags &= ~ReplyMarkupFlag::SuggestionDecline; + data.flags &= ~ReplyMarkupFlag::SuggestionSeparator; + } else { + if (!has(Type::SuggestDecline)) { + // ... rows ... + // [decline] + data.rows.push_back({ { + Type::SuggestDecline, + tr::lng_suggest_action_decline(tr::now), + } }); + data.flags |= ReplyMarkupFlag::SuggestionDecline; + } + if (data.rows.size() > 1) { + data.flags |= ReplyMarkupFlag::SuggestionSeparator; + } else { + data.flags &= ~ReplyMarkupFlag::SuggestionSeparator; + } + } + } + + inlineKeyboard = nullptr; +} + HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() = default; HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal( @@ -1240,6 +1378,38 @@ MessageFactcheck FromMTP( return result; } +TextWithEntities ComposeTodoTasksList( + HistoryItem *itemWithList, + const std::vector<int> &ids) { + const auto media = itemWithList ? itemWithList->media() : nullptr; + const auto list = media ? media->todolist() : nullptr; + auto names = std::vector<TextWithEntities>(); + if (list) { + names.reserve(ids.size()); + for (const auto &id : ids) { + const auto i = ranges::find(list->items, id, &TodoListItem::id); + if (i == end(list->items)) { + names.clear(); + break; + } + names.push_back( + TextWithEntities().append('"').append(i->text).append('"')); + } + } + return ComposeTodoTasksList(ids.size(), names); +} + +TextWithEntities ComposeTodoTasksList( + not_null<HistoryServiceTodoAppendTasks*> append) { + auto names = std::vector<TextWithEntities>(); + names.reserve(append->list.size()); + for (const auto &task : append->list) { + names.push_back( + TextWithEntities().append('"').append(task.text).append('"')); + } + return ComposeTodoTasksList(names.size(), names); +} + HistoryDocumentCaptioned::HistoryDocumentCaptioned() : caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) { } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 331ee24dfe..4ca58bde84 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bubble.h" struct WebPageData; +struct TodoListItem; class VoiceSeekClickHandler; class ReplyKeyboard; @@ -57,7 +58,13 @@ struct BotKeyboardButton; extern const char kOptionFastButtonsMode[]; [[nodiscard]] bool FastButtonsMode(); -struct HistoryMessageVia : public RuntimeComponent<HistoryMessageVia, HistoryItem> { +enum class SuggestionActions : uchar { + None, + Decline, + AcceptAndDecline, +}; + +struct HistoryMessageVia : RuntimeComponent<HistoryMessageVia, HistoryItem> { void create(not_null<Data::Session*> owner, UserId userId); void resize(int32 availw) const; @@ -68,7 +75,8 @@ struct HistoryMessageVia : public RuntimeComponent<HistoryMessageVia, HistoryIte ClickHandlerPtr link; }; -struct HistoryMessageViews : public RuntimeComponent<HistoryMessageViews, HistoryItem> { +struct HistoryMessageViews +: RuntimeComponent<HistoryMessageViews, HistoryItem> { static constexpr auto kMaxRecentRepliers = 3; struct Part { @@ -87,13 +95,15 @@ struct HistoryMessageViews : public RuntimeComponent<HistoryMessageViews, Histor int forwardsCount = 0; }; -struct HistoryMessageSigned : public RuntimeComponent<HistoryMessageSigned, HistoryItem> { +struct HistoryMessageSigned +: RuntimeComponent<HistoryMessageSigned, HistoryItem> { QString author; UserData *viaBusinessBot = nullptr; bool isAnonymousRank = false; }; -struct HistoryMessageEdited : public RuntimeComponent<HistoryMessageEdited, HistoryItem> { +struct HistoryMessageEdited +: RuntimeComponent<HistoryMessageEdited, HistoryItem> { TimeId date = 0; }; @@ -134,7 +144,8 @@ private: }; -struct HistoryMessageForwarded : public RuntimeComponent<HistoryMessageForwarded, HistoryItem> { +struct HistoryMessageForwarded +: RuntimeComponent<HistoryMessageForwarded, HistoryItem> { void create( const HistoryMessageVia *via, not_null<const HistoryItem*> item) const; @@ -162,13 +173,19 @@ struct HistoryMessageForwarded : public RuntimeComponent<HistoryMessageForwarded bool story = false; }; -struct HistoryMessageSavedMediaData : public RuntimeComponent<HistoryMessageSavedMediaData, HistoryItem> { +struct HistoryMessageSavedMediaData +: RuntimeComponent<HistoryMessageSavedMediaData, HistoryItem> { TextWithEntities text; std::unique_ptr<Data::Media> media; }; -struct HistoryMessageSaved : public RuntimeComponent<HistoryMessageSaved, HistoryItem> { - Data::SavedSublist *sublist = nullptr; +struct HistoryMessageSaved +: RuntimeComponent<HistoryMessageSaved, HistoryItem> { + PeerId sublistPeerId = 0; + + // This can't change after the message is created, but is required + // frequently in reactions, so we cache the value here. + Data::SavedSublist *savedMessagesSublist = nullptr; }; class ReplyToMessagePointer final { @@ -256,6 +273,7 @@ struct ReplyFields { QString externalSenderName; QString externalPostAuthor; PeerId externalPeerId = 0; + PeerId monoforumPeerId = 0; MsgId messageId = 0; MsgId topMessageId = 0; StoryId storyId = 0; @@ -273,7 +291,7 @@ struct ReplyFields { const MTPInputReplyTo &reply); struct HistoryMessageReply - : public RuntimeComponent<HistoryMessageReply, HistoryItem> { +: RuntimeComponent<HistoryMessageReply, HistoryItem> { HistoryMessageReply(); HistoryMessageReply(const HistoryMessageReply &other) = delete; HistoryMessageReply(HistoryMessageReply &&other) = delete; @@ -357,7 +375,7 @@ private: }; struct HistoryMessageTranslation - : public RuntimeComponent<HistoryMessageTranslation, HistoryItem> { +: RuntimeComponent<HistoryMessageTranslation, HistoryItem> { TextWithEntities text; LanguageId to; bool requested = false; @@ -366,11 +384,12 @@ struct HistoryMessageTranslation }; struct HistoryMessageReplyMarkup - : public RuntimeComponent<HistoryMessageReplyMarkup, HistoryItem> { +: RuntimeComponent<HistoryMessageReplyMarkup, HistoryItem> { using Button = HistoryMessageMarkupButton; void createForwarded(const HistoryMessageReplyMarkup &original); void updateData(HistoryMessageMarkupData &&markup); + void updateSuggestControls(SuggestionActions actions); [[nodiscard]] bool hiddenBy(Data::Media *media) const; @@ -564,7 +583,7 @@ private: // Special type of Component for the channel actions log. struct HistoryMessageLogEntryOriginal -: public RuntimeComponent<HistoryMessageLogEntryOriginal, HistoryItem> { +: RuntimeComponent<HistoryMessageLogEntryOriginal, HistoryItem> { HistoryMessageLogEntryOriginal(); HistoryMessageLogEntryOriginal(HistoryMessageLogEntryOriginal &&other); HistoryMessageLogEntryOriginal &operator=(HistoryMessageLogEntryOriginal &&other); @@ -596,19 +615,28 @@ struct MessageFactcheck { const tl::conditional<MTPFactCheck> &factcheck); struct HistoryMessageFactcheck -: public RuntimeComponent<HistoryMessageFactcheck, HistoryItem> { +: RuntimeComponent<HistoryMessageFactcheck, HistoryItem> { MessageFactcheck data; WebPageData *page = nullptr; bool requested = false; }; +struct HistoryMessageSuggestedPost +: RuntimeComponent<HistoryMessageSuggestedPost, HistoryItem> { + CreditsAmount price; + TimeId date = 0; + mtpRequestId requestId = 0; + bool accepted = false; + bool rejected = false; +}; + struct HistoryMessageRestrictions -: public RuntimeComponent<HistoryMessageRestrictions, HistoryItem> { +: RuntimeComponent<HistoryMessageRestrictions, HistoryItem> { std::vector<Data::UnavailableReason> reasons; }; struct HistoryServiceData -: public RuntimeComponent<HistoryServiceData, HistoryItem> { +: RuntimeComponent<HistoryServiceData, HistoryItem> { std::vector<ClickHandlerPtr> textLinks; }; @@ -624,13 +652,13 @@ struct HistoryServiceDependentData { }; struct HistoryServicePinned -: public RuntimeComponent<HistoryServicePinned, HistoryItem> -, public HistoryServiceDependentData { +: RuntimeComponent<HistoryServicePinned, HistoryItem> +, HistoryServiceDependentData { }; struct HistoryServiceTopicInfo -: public RuntimeComponent<HistoryServiceTopicInfo, HistoryItem> -, public HistoryServiceDependentData { +: RuntimeComponent<HistoryServiceTopicInfo, HistoryItem> +, HistoryServiceDependentData { QString title; DocumentId iconId = 0; bool closed = false; @@ -650,15 +678,58 @@ struct HistoryServiceTopicInfo } }; +struct HistoryServiceTodoCompletions +: RuntimeComponent<HistoryServiceTodoCompletions, HistoryItem> +, HistoryServiceDependentData { + std::vector<int> completed; + std::vector<int> incompleted; +}; + +[[nodiscard]] TextWithEntities ComposeTodoTasksList( + HistoryItem *itemWithList, + const std::vector<int> &ids); + +struct HistoryServiceTodoAppendTasks +: RuntimeComponent<HistoryServiceTodoAppendTasks, HistoryItem> +, HistoryServiceDependentData { + std::vector<TodoListItem> list; +}; + +[[nodiscard]] TextWithEntities ComposeTodoTasksList( + not_null<HistoryServiceTodoAppendTasks*> append); + +struct HistoryServiceSuggestDecision +: RuntimeComponent<HistoryServiceSuggestDecision, HistoryItem> +, HistoryServiceDependentData { + CreditsAmount price; + TimeId date = 0; + QString rejectComment; + bool rejected = false; + bool balanceTooLow = false; +}; + +enum class SuggestRefundType { + None, + User, + Admin, +}; + +struct HistoryServiceSuggestFinish +: RuntimeComponent<HistoryServiceSuggestFinish, HistoryItem> +, HistoryServiceDependentData { + CreditsAmount successPrice; + SuggestRefundType refundType = SuggestRefundType::None; +}; + struct HistoryServiceGameScore -: public RuntimeComponent<HistoryServiceGameScore, HistoryItem> -, public HistoryServiceDependentData { +: RuntimeComponent<HistoryServiceGameScore, HistoryItem> +, HistoryServiceDependentData { int score = 0; }; struct HistoryServicePayment -: public RuntimeComponent<HistoryServicePayment, HistoryItem> -, public HistoryServiceDependentData { +: RuntimeComponent<HistoryServicePayment, HistoryItem> +, HistoryServiceDependentData { QString slug; TextWithEntities amount; ClickHandlerPtr invoiceLink; @@ -668,22 +739,22 @@ struct HistoryServicePayment }; struct HistoryServiceSameBackground -: public RuntimeComponent<HistoryServiceSameBackground, HistoryItem> -, public HistoryServiceDependentData { +: RuntimeComponent<HistoryServiceSameBackground, HistoryItem> +, HistoryServiceDependentData { }; struct HistoryServiceGiveawayResults -: public RuntimeComponent<HistoryServiceGiveawayResults, HistoryItem> -, public HistoryServiceDependentData { +: RuntimeComponent<HistoryServiceGiveawayResults, HistoryItem> +, HistoryServiceDependentData { }; struct HistoryServiceCustomLink -: public RuntimeComponent<HistoryServiceCustomLink, HistoryItem> { +: RuntimeComponent<HistoryServiceCustomLink, HistoryItem> { ClickHandlerPtr link; }; struct HistoryServicePaymentRefund -: public RuntimeComponent<HistoryServicePaymentRefund, HistoryItem> { +: RuntimeComponent<HistoryServicePaymentRefund, HistoryItem> { ClickHandlerPtr link; PeerData *peer = nullptr; QString transactionId; @@ -706,7 +777,7 @@ struct TimeToLiveSingleView { }; struct HistoryServiceSelfDestruct -: public RuntimeComponent<HistoryServiceSelfDestruct, HistoryItem> { +: RuntimeComponent<HistoryServiceSelfDestruct, HistoryItem> { using Type = HistorySelfDestructType; Type type = Type::Photo; @@ -715,24 +786,25 @@ struct HistoryServiceSelfDestruct }; struct HistoryServiceOngoingCall -: public RuntimeComponent<HistoryServiceOngoingCall, HistoryItem> { +: RuntimeComponent<HistoryServiceOngoingCall, HistoryItem> { CallId id = 0; ClickHandlerPtr link; rpl::lifetime lifetime; }; struct HistoryServiceChatThemeChange -: public RuntimeComponent<HistoryServiceChatThemeChange, HistoryItem> { +: RuntimeComponent<HistoryServiceChatThemeChange, HistoryItem> { ClickHandlerPtr link; }; struct HistoryServiceTTLChange -: public RuntimeComponent<HistoryServiceTTLChange, HistoryItem> { +: RuntimeComponent<HistoryServiceTTLChange, HistoryItem> { ClickHandlerPtr link; }; class FileClickHandler; -struct HistoryDocumentThumbed : public RuntimeComponent<HistoryDocumentThumbed, HistoryView::Document> { +struct HistoryDocumentThumbed +: RuntimeComponent<HistoryDocumentThumbed, HistoryView::Document> { std::shared_ptr<FileClickHandler> linksavel; std::shared_ptr<FileClickHandler> linkopenwithl; std::shared_ptr<FileClickHandler> linkcancell; @@ -744,13 +816,15 @@ struct HistoryDocumentThumbed : public RuntimeComponent<HistoryDocumentThumbed, mutable bool blurred : 1 = false; }; -struct HistoryDocumentCaptioned : public RuntimeComponent<HistoryDocumentCaptioned, HistoryView::Document> { +struct HistoryDocumentCaptioned +: RuntimeComponent<HistoryDocumentCaptioned, HistoryView::Document> { HistoryDocumentCaptioned(); Ui::Text::String caption; }; -struct HistoryDocumentNamed : public RuntimeComponent<HistoryDocumentNamed, HistoryView::Document> { +struct HistoryDocumentNamed +: RuntimeComponent<HistoryDocumentNamed, HistoryView::Document> { Ui::Text::String name; }; @@ -762,7 +836,8 @@ struct HistoryDocumentVoicePlayback { Ui::Animations::Basic progressAnimation; }; -class HistoryDocumentVoice : public RuntimeComponent<HistoryDocumentVoice, HistoryView::Document> { +class HistoryDocumentVoice +: public RuntimeComponent<HistoryDocumentVoice, HistoryView::Document> { // We don't use float64 because components should align to pointer even on 32bit systems. static constexpr float64 kFloatToIntMultiplier = 65536.; diff --git a/Telegram/SourceFiles/history/history_item_edition.cpp b/Telegram/SourceFiles/history/history_item_edition.cpp index 1fb3482394..968a429c76 100644 --- a/Telegram/SourceFiles/history/history_item_edition.cpp +++ b/Telegram/SourceFiles/history/history_item_edition.cpp @@ -11,8 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" HistoryMessageEdition::HistoryMessageEdition( - not_null<Main::Session*> session, - const MTPDmessage &message) { + not_null<Main::Session*> session, + const MTPDmessage &message) +: suggest(HistoryMessageSuggestInfo(message.vsuggested_post())) { isEditHide = message.is_edit_hide(); isMediaUnread = message.is_media_unread(); editDate = message.vedit_date().value_or(-1); diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index c22ce713c8..8d0cd03aa2 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -30,11 +30,13 @@ struct HistoryMessageEdition { bool useSameReplies = false; bool useSameMarkup = false; bool useSameReactions = false; + bool useSameSuggest = false; bool savePreviousMedia = false; bool invertMedia = false; TextWithEntities textWithEntities; HistoryMessageMarkupData replyMarkup; HistoryMessageRepliesData replies; + HistoryMessageSuggestInfo suggest; const MTPMessageMedia *mtpMedia = nullptr; const MTPMessageReactions *mtpReactions = nullptr; const MTPFactCheck *mtpFactcheck = nullptr; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 31710fa986..f274356674 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/history.h" #include "history/history_item_components.h" #include "main/main_account.h" @@ -193,20 +194,27 @@ Data::SendErrorWithThread GetErrorForSending( std::optional<SendPaymentDetails> ComputePaymentDetails( not_null<PeerData*> peer, int messagesCount) { - if (const auto user = peer->asUser()) { - if (user->hasStarsPerMessage() - && !user->messageMoneyRestrictionsKnown()) { - user->updateFull(); - return {}; - } - } else if (const auto channel = peer->asChannel()) { - if (!channel->isFullLoaded()) { - channel->updateFull(); - return {}; - } + const auto user = peer->asUser(); + const auto channel = user ? nullptr : peer->asChannel(); + const auto has = (user && user->hasStarsPerMessage()) + || (channel && channel->hasStarsPerMessage()); + if (!has) { + return SendPaymentDetails(); } - if (!peer->session().credits().loaded()) { + + const auto known1 = peer->session().credits().loaded(); + if (!known1) { peer->session().credits().load(); + } + + const auto known2 = user + ? user->messageMoneyRestrictionsKnown() + : channel->starsPerMessageKnown(); + if (!known2) { + peer->updateFull(); + } + + if (!known1 || !known2) { return {}; } else if (const auto perMessage = peer->starsPerMessageChecked()) { return SendPaymentDetails{ @@ -217,6 +225,21 @@ std::optional<SendPaymentDetails> ComputePaymentDetails( return SendPaymentDetails(); } +bool SuggestPaymentDataReady( + not_null<PeerData*> peer, + SuggestPostOptions suggest) { + if (!suggest.exists || !suggest.price() || peer->amMonoforumAdmin()) { + return true; + } else if (suggest.ton && !peer->session().credits().tonLoaded()) { + peer->session().credits().tonLoad(); + return false; + } else if (!suggest.ton && !peer->session().credits().loaded()) { + peer->session().credits().load(); + return false; + } + return true; +} + object_ptr<Ui::BoxContent> MakeSendErrorBox( const Data::SendErrorWithThread &error, bool withTitle) { @@ -254,13 +277,15 @@ void ShowSendPaidConfirm( not_null<PeerData*> peer, SendPaymentDetails details, Fn<void()> confirmed, - PaidConfirmStyles styles) { + PaidConfirmStyles styles, + int suggestStarsPrice) { return ShowSendPaidConfirm( navigation->uiShow(), peer, details, confirmed, - styles); + styles, + suggestStarsPrice); } void ShowSendPaidConfirm( @@ -268,13 +293,15 @@ void ShowSendPaidConfirm( not_null<PeerData*> peer, SendPaymentDetails details, Fn<void()> confirmed, - PaidConfirmStyles styles) { + PaidConfirmStyles styles, + int suggestStarsPrice) { ShowSendPaidConfirm( std::move(show), std::vector<not_null<PeerData*>>{ peer }, details, confirmed, - styles); + styles, + suggestStarsPrice); } void ShowSendPaidConfirm( @@ -282,7 +309,8 @@ void ShowSendPaidConfirm( const std::vector<not_null<PeerData*>> &peers, SendPaymentDetails details, Fn<void()> confirmed, - PaidConfirmStyles styles) { + PaidConfirmStyles styles, + int suggestStarsPrice) { Expects(!peers.empty()); const auto singlePeer = (peers.size() > 1) @@ -290,7 +318,7 @@ void ShowSendPaidConfirm( : peers.front().get(); const auto singlePeerId = singlePeer ? singlePeer->id : PeerId(); const auto check = [=] { - const auto required = details.stars; + const auto required = details.stars + suggestStarsPrice; if (!required) { return; } @@ -300,10 +328,13 @@ void ShowSendPaidConfirm( confirmed(); } }; - Settings::MaybeRequestBalanceIncrease( + using namespace Settings; + MaybeRequestBalanceIncrease( show, required, - Settings::SmallBalanceForMessage{ .recipientId = singlePeerId }, + (suggestStarsPrice + ? SmallBalanceSource(SmallBalanceForSuggest{ singlePeerId }) + : SmallBalanceForMessage{ singlePeerId }), done); }; auto usersOnly = true; @@ -392,15 +423,15 @@ void ShowSendPaidConfirm( bool SendPaymentHelper::check( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn<void(int)> resend, PaidConfirmStyles styles) { return check( navigation->uiShow(), peer, + options, messagesCount, - starsApproved, std::move(resend), styles); } @@ -408,17 +439,28 @@ bool SendPaymentHelper::check( bool SendPaymentHelper::check( std::shared_ptr<Main::SessionShow> show, not_null<PeerData*> peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn<void(int)> resend, PaidConfirmStyles styles) { clear(); + const auto admin = peer->amMonoforumAdmin(); + const auto suggest = options.suggest; + const auto starsApproved = options.starsApproved; + const auto checkSuggestPriceStars = (admin || suggest.ton) + ? 0 + : int(base::SafeRound(suggest.price().value())); + const auto checkSuggestPriceTon = (!admin && suggest.ton) + ? suggest.price() + : CreditsAmount(); const auto details = ComputePaymentDetails(peer, messagesCount); - if (!details) { + const auto suggestDetails = SuggestPaymentDataReady(peer, suggest); + if (!details || !suggestDetails) { _resend = [=] { resend(starsApproved); }; - if (!peer->session().credits().loaded()) { + if ((!details || !suggest.ton) + && !peer->session().credits().loaded()) { peer->session().credits().loadedValue( ) | rpl::filter( rpl::mappers::_1 @@ -429,6 +471,18 @@ bool SendPaymentHelper::check( }, _lifetime); } + if ((!suggestDetails && suggest.ton) + && !peer->session().credits().tonLoaded()) { + peer->session().credits().tonLoadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + if (const auto callback = base::take(_resend)) { + callback(); + } + }, _lifetime); + } + peer->session().changes().peerUpdates( peer, Data::PeerUpdate::Flag::FullInfo @@ -442,7 +496,33 @@ bool SendPaymentHelper::check( } else if (const auto stars = details->stars; stars > starsApproved) { ShowSendPaidConfirm(show, peer, *details, [=] { resend(stars); - }, styles); + }, styles, checkSuggestPriceStars); + return false; + } else if (checkSuggestPriceStars + && (CreditsAmount(details->stars + checkSuggestPriceStars) + > peer->session().credits().balance())) { + using namespace Settings; + const auto broadcast = peer->monoforumBroadcast(); + const auto broadcastId = (broadcast ? broadcast : peer)->id; + const auto forMessages = details->stars; + const auto required = forMessages + checkSuggestPriceStars; + const auto done = [=](SmallBalanceResult result) { + if (result == SmallBalanceResult::Success + || result == SmallBalanceResult::Already) { + resend(forMessages); + } + }; + MaybeRequestBalanceIncrease( + show, + required, + SmallBalanceForSuggest{ broadcastId }, + done); + return false; + } + if (checkSuggestPriceTon + && checkSuggestPriceTon > peer->session().credits().tonBalance()) { + using namespace HistoryView; + show->show(Box(InsufficientTonBox, peer, checkSuggestPriceTon)); return false; } return true; @@ -507,6 +587,8 @@ TimeId NewMessageDate(const Api::SendOptions &options) { PeerId NewMessageFromId(const Api::SendAction &action) { return action.options.sendAs ? action.options.sendAs->id + : action.history->peer->amMonoforumAdmin() + ? action.history->peer->monoforumBroadcast()->id : action.history->peer->amAnonymous() ? PeerId() : action.history->session().userPeerId(); @@ -771,6 +853,11 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag()) | ((flags & MTP::f_video_processing_pending) ? Flag::EstimatedDate + : Flag()) + | ((flags & MTP::f_paid_suggested_post_ton) + ? Flag::TonPaidSuggested + : (flags & MTP::f_paid_suggested_post_stars) + ? Flag::StarsPaidSuggested : Flag()); } @@ -906,6 +993,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaPoll &) { return Result::Good; + }, [](const MTPDmessageMediaToDo &) { + return Result::Good; }, [](const MTPDmessageMediaDice &) { return Result::Good; }, [](const MTPDmessageMediaStory &data) { diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 11270500f6..d1c472dd58 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -149,6 +149,10 @@ struct SendPaymentDetails { not_null<PeerData*> peer, int messagesCount); +[[nodiscard]] bool SuggestPaymentDataReady( + not_null<PeerData*> peer, + SuggestPostOptions suggest); + struct PaidConfirmStyles { const style::FlatLabel *label = nullptr; const style::Checkbox *checkbox = nullptr; @@ -158,34 +162,37 @@ void ShowSendPaidConfirm( not_null<PeerData*> peer, SendPaymentDetails details, Fn<void()> confirmed, - PaidConfirmStyles styles = {}); + PaidConfirmStyles styles = {}, + int suggestStarsPrice = 0); void ShowSendPaidConfirm( std::shared_ptr<Main::SessionShow> show, not_null<PeerData*> peer, SendPaymentDetails details, Fn<void()> confirmed, - PaidConfirmStyles styles = {}); + PaidConfirmStyles styles = {}, + int suggestStarsPrice = 0); void ShowSendPaidConfirm( std::shared_ptr<Main::SessionShow> show, const std::vector<not_null<PeerData*>> &peers, SendPaymentDetails details, Fn<void()> confirmed, - PaidConfirmStyles styles = {}); + PaidConfirmStyles styles = {}, + int suggestStarsPrice = 0); class SendPaymentHelper final { public: [[nodiscard]] bool check( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn<void(int)> resend, PaidConfirmStyles styles = {}); [[nodiscard]] bool check( std::shared_ptr<Main::SessionShow> show, not_null<PeerData*> peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn<void(int)> resend, PaidConfirmStyles styles = {}); diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index 319a77238d..8ca15fe623 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -312,7 +312,7 @@ HistoryMessageRepliesData::HistoryMessageRepliesData( if (!data) { return; } - const auto &fields = data->c_messageReplies(); + const auto &fields = data->data(); if (const auto list = fields.vrecent_repliers()) { recentRepliers.reserve(list->v.size()); for (const auto &id : list->v) { @@ -326,3 +326,31 @@ HistoryMessageRepliesData::HistoryMessageRepliesData( isNull = false; pts = fields.vreplies_pts().v; } + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + const MTPSuggestedPost *data) { + if (!data) { + return; + } + const auto &fields = data->data(); + price = CreditsAmountFromTL(fields.vprice()); + date = fields.vschedule_date().value_or_empty(); + accepted = fields.is_accepted(); + rejected = fields.is_rejected(); + exists = true; +} + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + const Api::SendOptions &options) +: HistoryMessageSuggestInfo(options.suggest) { +} + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + SuggestPostOptions options) { + if (!options.exists) { + return; + } + price = options.price(); + date = options.date; + exists = true; +} diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 77895c3aa1..03740b1ea6 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flags.h" #include "data/data_chat_participant_status.h" +namespace Api { +struct SendOptions; +} // namespace Api + namespace Data { class Session; } // namespace Data @@ -33,6 +37,9 @@ enum class ReplyMarkupFlag : uint32 { IsNull = (1U << 7), OnlyBuyButton = (1U << 8), Persistent = (1U << 9), + SuggestionDecline = (1U << 10), + SuggestionAccept = (1U << 11), + SuggestionSeparator = (1U << 12), }; inline constexpr bool is_flag_type(ReplyMarkupFlag) { return true; } using ReplyMarkupFlags = base::flags<ReplyMarkupFlag>; @@ -81,6 +88,10 @@ struct HistoryMessageMarkupButton { WebView, SimpleWebView, CopyText, + + SuggestDecline, + SuggestAccept, + SuggestChange, }; HistoryMessageMarkupButton( @@ -136,3 +147,16 @@ struct HistoryMessageRepliesData { bool isNull = true; int pts = 0; }; + +struct HistoryMessageSuggestInfo { + HistoryMessageSuggestInfo() = default; + explicit HistoryMessageSuggestInfo(const MTPSuggestedPost *data); + explicit HistoryMessageSuggestInfo(const Api::SendOptions &options); + explicit HistoryMessageSuggestInfo(SuggestPostOptions options); + + CreditsAmount price; + TimeId date = 0; + bool accepted = false; + bool rejected = false; + bool exists = false; +}; diff --git a/Telegram/SourceFiles/history/history_unread_things.cpp b/Telegram/SourceFiles/history/history_unread_things.cpp index 4cca97437e..1ba1678f3a 100644 --- a/Telegram/SourceFiles/history/history_unread_things.cpp +++ b/Telegram/SourceFiles/history/history_unread_things.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_unread_things.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -38,6 +39,12 @@ template <typename Update> return UpdateFlag<Data::TopicUpdate>(type); } +[[nodiscard]] Data::SublistUpdate::Flag SublistUpdateFlag(Type type) { + Expects(type == Type::Reactions); + + return Data::SublistUpdate::Flag::UnreadReactions; +} + } // namespace void Proxy::setCount(int count) { @@ -224,6 +231,10 @@ void Proxy::notifyUpdated() { topic->session().changes().topicUpdated( topic, TopicUpdateFlag(_type)); + } else if (const auto sublist = _thread->asSublist()) { + sublist->session().changes().sublistUpdated( + sublist, + SublistUpdateFlag(_type)); } } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 38c6c2cd79..5405381324 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -97,8 +97,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_search.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" -#include "history/view/controls/history_view_voice_record_bar.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/controls/history_view_ttl_button.h" +#include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_webpage_processor.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_cursor_state.h" @@ -117,6 +118,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_reply.h" #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_sticker_toast.h" +#include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" @@ -252,6 +254,7 @@ HistoryWidget::HistoryWidget( , _api(&controller->session().mtp()) , _updateEditTimeLeftDisplay([=] { updateField(); }) , _fieldBarCancel(this, st::historyReplyCancel) +, _topBars(std::make_unique<Ui::RpWidget>(this)) , _topBar(this, controller) , _scroll( this, @@ -416,6 +419,7 @@ HistoryWidget::HistoryWidget( _muteUnmute->addClickHandler([=] { toggleMuteUnmute(); }); _discuss->addClickHandler([=] { goToDiscussionGroup(); }); setupGiftToChannelButton(); + setupDirectMessageButton(); _reportMessages->addClickHandler([=] { reportSelectedMessages(); }); _field->submits( ) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) { @@ -1049,7 +1053,7 @@ HistoryWidget::HistoryWidget( action.replyTo.messageId); if (action.replaceMediaOf) { } else if (action.options.scheduled) { - cancelReply(lastKeyboardUsed); + cancelReplyOrSuggest(lastKeyboardUsed); if (!AyuSettings::isUseScheduledMessages()) { crl::on_main(this, [=, history = action.history] { @@ -1060,7 +1064,7 @@ HistoryWidget::HistoryWidget( } else { fastShowAtEnd(action.history); if (!_justMarkingAsRead - && cancelReply(lastKeyboardUsed) + && cancelReplyOrSuggest(lastKeyboardUsed) && !action.clearDraft) { saveCloudDraft(); } @@ -1111,6 +1115,7 @@ Dialogs::EntryState HistoryWidget::computeDialogsEntryState() const { .key = _history, .section = Dialogs::EntryState::Section::History, .currentReplyTo = replyTo(), + .currentSuggest = suggestOptions(), }; } @@ -1125,19 +1130,37 @@ void HistoryWidget::refreshJoinChannelText() { } void HistoryWidget::refreshGiftToChannelShown() { - if (!_giftToChannelIn || !_giftToChannelOut || !_giftToChannelDiscuss) { + if (!_giftToChannel || !_peer) { return; } // AyuGram: hide gift button almost everywhere // still accessible via the menu in peer window const auto channel = _peer->asChannel(); - const auto shown = channel + _giftToChannel->setVisible(channel && channel->isBroadcast() && channel->stargiftsAvailable() - && isExteraPeer(getBareID(channel)); - _giftToChannelIn->setVisible(shown); - _giftToChannelOut->setVisible(shown); - _giftToChannelDiscuss->setVisible(shown); + && isExteraPeer(getBareID(channel))); +} + +void HistoryWidget::refreshDirectMessageShown() { + if (!_directMessage || !_peer) { + return; + } + const auto channel = _peer->asChannel(); + const auto monoforum = channel ? channel->broadcastMonoforum() : nullptr; + const auto visible = monoforum && !monoforum->monoforumDisabled(); + _directMessage->setVisible(visible); + if (visible) { + using Flags = Data::Flags<ChannelDataFlags>; + _directMessageLifetime = monoforum->flagsValue( + ) | rpl::skip( + 1 + ) | rpl::start_with_next([=](Flags::Change change) { + if (change.diff & ChannelDataFlag::MonoforumDisabled) { + refreshDirectMessageShown(); + } + }); + } } void HistoryWidget::refreshTopBarActiveChat() { @@ -1188,7 +1211,7 @@ void HistoryWidget::initVoiceRecordBar() { }); const auto applyLocalDraft = [=] { - if (_history && _history->localDraft({})) { + if (_history && _history->localDraft(MsgId(), PeerId())) { applyDraft(); } }; @@ -1783,6 +1806,7 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { void HistoryWidget::orderWidgets() { _voiceRecordBar->raise(); _send->raise(); + _topBars->raise(); if (_businessBotStatus) { _businessBotStatus->bar().raise(); } @@ -1810,6 +1834,9 @@ void HistoryWidget::orderWidgets() { if (_chooseTheme) { _chooseTheme->raise(); } + if (_subsectionTabs) { + _subsectionTabs->raise(); + } _topShadow->raise(); if (_autocomplete) { _autocomplete->raise(); @@ -1944,25 +1971,30 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { } const auto topicRootId = MsgId(); + const auto monoforumPeerId = PeerId(); if (_editMsgId) { _history->setLocalEditDraft(std::make_unique<Data::Draft>( _field, FullReplyTo{ .messageId = FullMsgId(_history->peer->id, _editMsgId), .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, }, + suggestOptions(true), _preview->draft(), _saveEditMsgRequestId)); } else { - if (_replyTo || !_field->empty()) { + const auto suggest = suggestOptions(); + if (_replyTo || suggest.exists || !_field->empty()) { _history->setLocalDraft(std::make_unique<Data::Draft>( _field, _replyTo, + suggest, _preview->draft())); } else { - _history->clearLocalDraft(topicRootId); + _history->clearLocalDraft(topicRootId, monoforumPeerId); } - _history->clearLocalEditDraft(topicRootId); + _history->clearLocalEditDraft(topicRootId, monoforumPeerId); } } @@ -2159,22 +2191,66 @@ void HistoryWidget::setupShortcuts() { } void HistoryWidget::setupGiftToChannelButton() { - const auto setupButton = [=](not_null<Ui::RpWidget*> parent) { - auto *button = Ui::CreateChild<Ui::IconButton>( - parent.get(), - st::historyGiftToChannel); - parent->widthValue() | rpl::start_with_next([=](int width) { - button->moveToRight(0, 0); - }, button->lifetime()); - button->setClickedCallback([=] { - if (_peer) { - Ui::ShowStarGiftBox(controller(), _peer); + _giftToChannel = Ui::CreateChild<Ui::IconButton>( + _muteUnmute.data(), + st::historyGiftToChannel); + widthValue() | rpl::start_with_next([=](int width) { + _giftToChannel->moveToRight(0, 0, width); + }, _giftToChannel->lifetime()); + _giftToChannel->setClickedCallback([=] { + Ui::ShowStarGiftBox(controller(), _peer); + }); + rpl::combine( + _muteUnmute->shownValue(), + _joinChannel->shownValue() + ) | rpl::start_with_next([=](bool muteUnmute, bool joinChannel) { + const auto newParent = (muteUnmute && !joinChannel) + ? _muteUnmute.data() + : (joinChannel && !muteUnmute) + ? _joinChannel.data() + : nullptr; + if (newParent) { + _giftToChannel->setParent(newParent); + _giftToChannel->moveToRight(0, 0); + refreshGiftToChannelShown(); + } + }, _giftToChannel->lifetime()); +} + +void HistoryWidget::setupDirectMessageButton() { + _directMessage = Ui::CreateChild<Ui::IconButton>( + _muteUnmute.data(), + st::historyDirectMessage); + widthValue() | rpl::start_with_next([=](int width) { + _directMessage->moveToLeft(0, 0, width); + }, _directMessage->lifetime()); + _directMessage->setClickedCallback([=] { + if (const auto channel = _peer ? _peer->asChannel() : nullptr) { + if (channel->invitePeekExpires()) { + controller()->showToast( + tr::lng_channel_invite_private(tr::now)); + } else if (const auto monoforum = channel->monoforumLink()) { + controller()->showPeerHistory( + monoforum, + Window::SectionShow::Way::Forward); } - }); - return button; - }; - _giftToChannelIn = setupButton(_muteUnmute); - _giftToChannelOut = setupButton(_joinChannel); + } + }); + rpl::combine( + _muteUnmute->shownValue(), + _joinChannel->shownValue() + ) | rpl::start_with_next([=](bool muteUnmute, bool joinChannel) { + const auto newParent = (muteUnmute && !joinChannel) + ? _muteUnmute.data() + : (joinChannel && !muteUnmute) + ? _joinChannel.data() + : nullptr; + if (newParent) { + _directMessage->setParent(newParent); + _directMessage->moveToLeft(0, 0); + refreshDirectMessageShown(); + } + }, _directMessage->lifetime()); _giftToChannelDiscuss = setupButton(_discuss); } @@ -2209,6 +2285,10 @@ void HistoryWidget::fastShowAtEnd(not_null<History*> history) { _pinnedClickedId = FullMsgId(); _minPinnedId = std::nullopt; if (_history->isReadyFor(_showAtMsgId)) { + _history->forgetScrollState(); + if (_migrated) { + _migrated->forgetScrollState(); + } historyLoaded(); } else { firstLoadMessages(); @@ -2223,11 +2303,13 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { } }); - const auto editDraft = _history ? _history->localEditDraft({}) : nullptr; + const auto editDraft = _history + ? _history->localEditDraft(MsgId(), PeerId()) + : nullptr; const auto draft = editDraft ? editDraft : _history - ? _history->localDraft({}) + ? _history->localDraft(MsgId(), PeerId()) : nullptr; auto fieldAvailable = canWriteMessage(); const auto editMsgId = editDraft ? editDraft->reply.messageId.msg : 0; @@ -2276,12 +2358,21 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { if (!_replyEditMsg) { requestMessageData(_editMsgId); } + if (editDraft && editDraft->suggest) { + using namespace HistoryView; + applySuggestOptions(editDraft->suggest, SuggestMode::Change); + } else { + cancelSuggestPost(); + } } else { - const auto draft = _history->localDraft({}); + const auto draft = _history->localDraft(MsgId(), PeerId()); _processingReplyTo = draft ? draft->reply : FullReplyTo(); if (_processingReplyTo) { _processingReplyItem = session().data().message( _processingReplyTo.messageId); + } else if (draft && draft->suggest) { + using namespace HistoryView; + applySuggestOptions(draft->suggest, SuggestMode::New); } processReply(); } @@ -2357,7 +2448,9 @@ void HistoryWidget::showHistory( if (_peer->id == peerId) { updateForwarding(); - if (showAtMsgId == ShowAtUnreadMsgId + if (params.reapplyLocalDraft) { + return; + } else if (showAtMsgId == ShowAtUnreadMsgId && insideJumpToEndInsteadOfToUnread()) { DEBUG_LOG(("JumpToEnd(%1, %2, %3): " "Jumping to end instead of unread." @@ -2444,7 +2537,7 @@ void HistoryWidget::showHistory( info->inlineReturnTo = wasState; } sendBotStartCommand(); - _history->clearLocalDraft({}); + _history->clearLocalDraft(MsgId(), PeerId()); applyDraft(); _send->finishAnimating(); } @@ -2492,6 +2585,7 @@ void HistoryWidget::showHistory( setHistory(nullptr); _list = nullptr; _peer = nullptr; + _suggestOptions = nullptr; _sendPayment.clear(); _topicsRequested.clear(); _canSendMessages = false; @@ -2499,6 +2593,12 @@ void HistoryWidget::showHistory( _fieldDisabled = nullptr; _silent.destroy(); updateBotKeyboard(); + + _subsectionCheckLifetime.destroy(); + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + controller()->saveSubsectionTabs(base::take(_subsectionTabs)); + } } else { Assert(_list == nullptr); } @@ -2533,7 +2633,7 @@ void HistoryWidget::showHistory( _peer = session().data().peer(peerId); _contactStatus = std::make_unique<ContactStatus>( controller(), - this, + _topBars.get(), _peer, false); _contactStatus->bar().heightValue( @@ -2542,10 +2642,11 @@ void HistoryWidget::showHistory( }, _contactStatus->bar().lifetime()); refreshGiftToChannelShown(); + refreshDirectMessageShown(); if (const auto user = _peer->asUser()) { _paysStatus = std::make_unique<PaysStatus>( controller(), - this, + _topBars.get(), user); _paysStatus->bar().heightValue( ) | rpl::start_with_next([=] { @@ -2553,7 +2654,7 @@ void HistoryWidget::showHistory( }, _paysStatus->bar().lifetime()); _businessBotStatus = std::make_unique<BusinessBotStatus>( controller(), - this, + _topBars.get(), user); _businessBotStatus->bar().heightValue( ) | rpl::start_with_next([=] { @@ -2611,6 +2712,7 @@ void HistoryWidget::showHistory( } else if (_peer->isRepliesChat() || _peer->isVerifyCodes()) { updateNotifyControls(); } + refreshSuggestPostToggle(); refreshScheduledToggle(); refreshSendGiftToggle(); refreshSendAsToggle(); @@ -2640,13 +2742,19 @@ void HistoryWidget::showHistory( if (const auto channel = _peer->asChannel()) { channel->updateFull(); if (!channel->isBroadcast()) { - channel->flagsValue( - ) | rpl::start_with_next([=] { + using Flags = Data::Flags<ChannelDataFlags>; + channel->flagsValue() | rpl::skip( + 1 + ) | rpl::start_with_next([=](Flags::Change change) { refreshJoinChannelText(); + if (change.diff & ChannelDataFlag::MonoforumDisabled) { + updateCanSendMessage(); + updateSendRestriction(); + updateHistoryGeometry(); + } }, _list->lifetime()); - } else { - refreshJoinChannelText(); } + refreshJoinChannelText(); } controller()->adaptive().changes( @@ -2904,10 +3012,10 @@ void HistoryWidget::unregisterDraftSources() { } session().local().unregisterDraftSource( _history, - Data::DraftKey::Local({})); + Data::DraftKey::Local(MsgId(), PeerId())); session().local().unregisterDraftSource( _history, - Data::DraftKey::LocalEdit({})); + Data::DraftKey::LocalEdit(MsgId(), PeerId())); } void HistoryWidget::registerDraftSource() { @@ -2921,6 +3029,7 @@ void HistoryWidget::registerDraftSource() { (editMsgId ? FullReplyTo{ FullMsgId(peerId, editMsgId) } : _replyTo), + suggestOptions(editMsgId != 0), _field->getTextWithTags(), _preview->draft(), }; @@ -2932,8 +3041,8 @@ void HistoryWidget::registerDraftSource() { session().local().registerDraftSource( _history, (editMsgId - ? Data::DraftKey::LocalEdit({}) - : Data::DraftKey::Local({})), + ? Data::DraftKey::LocalEdit(MsgId(), PeerId()) + : Data::DraftKey::Local(MsgId(), PeerId())), std::move(draftSource)); } @@ -3015,6 +3124,7 @@ bool HistoryWidget::updateReplaceMediaButton() { controller(), { _history->peer->id, _editMsgId }, _field->getTextWithTags(), + suggestOptions(), _mediaEditManager.spoilered(), _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); @@ -3158,6 +3268,53 @@ void HistoryWidget::refreshSendGiftToggle() { } } +void HistoryWidget::applySuggestOptions( + SuggestPostOptions suggest, + HistoryView::SuggestMode mode) { + Expects(suggest.exists); + + using namespace HistoryView; + _suggestOptions = std::make_unique<SuggestOptions>( + controller()->uiShow(), + _peer, + suggest, + mode); + _suggestOptions->updates() | rpl::start_with_next([=] { + updateField(); + saveDraftWithTextNow(); + }, _suggestOptions->lifetime()); + saveDraftWithTextNow(); +} + +void HistoryWidget::saveDraftWithTextNow() { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); +} + +void HistoryWidget::refreshSuggestPostToggle() { + const auto has = _peer + && _peer->isMonoforum() + && !_peer->amMonoforumAdmin(); + if (!_toggleSuggestPost && has) { + _toggleSuggestPost.create(this, st::historySuggestPostToggle); + _toggleSuggestPost->setVisible(!_suggestOptions); + _toggleSuggestPost->addClickHandler([=] { + using namespace HistoryView; + applySuggestOptions({ .exists = 1 }, SuggestMode::New); + cancelReply(); + _processingReplyTo = FullReplyTo(); + _processingReplyItem = nullptr; + updateControlsVisibility(); + updateControlsGeometry(); + }); + orderWidgets(); + } else if (_toggleSuggestPost && !has) { + _toggleSuggestPost.destroy(); + cancelSuggestPost(); + } +} + void HistoryWidget::setupSendAsToggle() { session().sendAsPeers().updated( ) | rpl::filter([=](not_null<PeerData*> peer) { @@ -3232,21 +3389,10 @@ void HistoryWidget::updateControlsVisibility() { } else if (!_firstLoadRequest && _scroll->isHidden()) { _scroll->show(); } - if (_pinnedBar) { - _pinnedBar->show(); - } + _topBars->show(); if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) { _sponsoredMessageBar->toggle(true, anim::type::normal); } - if (_translateBar) { - _translateBar->show(); - } - if (_groupCallBar) { - _groupCallBar->show(); - } - if (_requestsBar) { - _requestsBar->show(); - } if (_paysStatus) { _paysStatus->show(); } @@ -3256,6 +3402,9 @@ void HistoryWidget::updateControlsVisibility() { if (_businessBotStatus) { _businessBotStatus->show(); } + if (_subsectionTabs) { + _subsectionTabs->show(); + } if (isChoosingTheme() || (!editingMessage() && (isSearching() @@ -3328,6 +3477,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->hide(); + } if (_giftToUser) { _giftToUser->hide(); } @@ -3443,6 +3595,14 @@ void HistoryWidget::updateControlsVisibility() { rightButtonsChanged = true; } } + if (_toggleSuggestPost) { + const auto was = _toggleSuggestPost->isVisible(); + const auto now = !_suggestOptions; + if (was != now) { + _toggleSuggestPost->setVisible(now); + rightButtonsChanged = true; + } + } if (_giftToUser) { const auto was = _giftToUser->isVisible(); const auto now = (!_editMsgId) && (!hideExtraButtons); @@ -3472,7 +3632,8 @@ void HistoryWidget::updateControlsVisibility() { || _replyTo || readyToForward() || _previewDrawPreview - || _kbReplyTo) { + || _kbReplyTo + || _suggestOptions) { if (_fieldBarCancel->isHidden()) { _fieldBarCancel->show(); updateControlsGeometry(); @@ -3502,6 +3663,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->hide(); + } if (_giftToUser) { _giftToUser->hide(); } @@ -3697,6 +3861,8 @@ void HistoryWidget::unreadCountUpdated() { || !_history->trackUnreadMessages(); _cornerButtons.updateJumpDownVisibility(hideCounter ? 0 + : _history->amMonoforumAdmin() + ? _history->chatListUnreadState().messages : _history->chatListBadgesState().unreadCounter); } } @@ -4388,7 +4554,7 @@ TextWithEntities HistoryWidget::prepareTextForEditMsg() const { return left; } -void HistoryWidget::saveEditMsg() { +void HistoryWidget::saveEditMessage(Api::SendOptions options) { Expects(_history != nullptr); if (_saveEditMsgRequestId) { @@ -4411,9 +4577,11 @@ void HistoryWidget::saveEditMsg() { || webPageDraft.url.isEmpty() || !webPageDraft.manual) && !hasMediaWithCaption) { - const auto suggestModerateActions = false; - controller()->show( - Box<DeleteMessagesBox>(item, suggestModerateActions)); + if (item->computeSuggestionActions() == SuggestionActions::None) { + const auto suggestModerateActions = false; + controller()->show( + Box<DeleteMessagesBox>(item, suggestModerateActions)); + } return; } else { const auto maxCaptionSize = !hasMediaWithCaption @@ -4443,16 +4611,16 @@ void HistoryWidget::saveEditMsg() { cancelEdit(); } })(); - if (const auto editDraft = history->localEditDraft({})) { + if (const auto editDraft = history->localEditDraft({}, {})) { if (editDraft->saveRequestId == requestId) { - history->clearLocalEditDraft({}); + history->clearLocalEditDraft(MsgId(), PeerId()); history->session().local().writeDrafts(history); } } }; const auto fail = [=](const QString &error, mtpRequestId requestId) { - if (const auto editDraft = history->localEditDraft({})) { + if (const auto editDraft = history->localEditDraft({}, {})) { if (editDraft->saveRequestId == requestId) { editDraft->saveRequestId = 0; } @@ -4475,11 +4643,27 @@ void HistoryWidget::saveEditMsg() { })(); }; + options.invertCaption = _mediaEditManager.invertCaption(); + options.suggest = suggestOptions(true); + + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + saveEditMessage(copy); + }; + const auto checked = checkSendPayment( + 1 + int(_forwardPanel->items().size()), + options, + withPaymentApproved); + if (!checked) { + return; + } + _saveEditMsgRequestId = Api::EditTextMessage( item, sending, webPageDraft, - { .invertCaption = _mediaEditManager.invertCaption() }, + options, done, fail, _mediaEditManager.spoilered()); @@ -4493,20 +4677,12 @@ void HistoryWidget::hideChildWidgets() { if (_tabbedPanel) { _tabbedPanel->hideFast(); } - if (_pinnedBar) { - _pinnedBar->hide(); - } if (_sponsoredMessageBar) { _sponsoredMessageBar->toggle(false, anim::type::instant); } - if (_translateBar) { - _translateBar->hide(); - } - if (_groupCallBar) { - _groupCallBar->hide(); - } - if (_requestsBar) { - _requestsBar->hide(); + _topBars->hide(); + if (_subsectionTabs) { + _subsectionTabs->hide(); } if (_voiceRecordBar) { _voiceRecordBar->hideFast(); @@ -4548,6 +4724,7 @@ Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); + result.options.suggest = suggestOptions(); result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -4565,15 +4742,15 @@ void HistoryWidget::sendVoice(const VoiceToSend &data) { copy.options.starsApproved = approved; sendVoice(copy); }; + auto action = prepareSendAction(data.options); const auto checked = checkSendPayment( 1 + int(_forwardPanel->items().size()), - data.options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - auto action = prepareSendAction(data.options); session().api().sendVoiceMessage( data.bytes, data.waveform, @@ -4598,7 +4775,7 @@ void HistoryWidget::send(Api::SendOptions options) { if (!_history) { return; } else if (_editMsgId) { - saveEditMsg(); + saveEditMessage({}); return; } else if (!options.scheduled && showSlowmodeError()) { return; @@ -4625,7 +4802,7 @@ void HistoryWidget::send(Api::SendOptions options) { message.textWithTags, ignoreSlowmodeCountdown, withPaymentApproved, - options.starsApproved)) { + message.action.options)) { return; } @@ -4639,9 +4816,7 @@ void HistoryWidget::send(Api::SendOptions options) { if (_preview) { _preview->apply({ .removed = true }); } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); hideSelectorControlsAnimated(); @@ -4841,6 +5016,8 @@ MsgId HistoryWidget::msgId() const { void HistoryWidget::showAnimated( Window::SlideDirection direction, const Window::SectionSlideParams ¶ms) { + validateSubsectionTabs(); + _showAnimation = nullptr; // If we show pinned bar here, we don't want it to change the @@ -4885,6 +5062,11 @@ void HistoryWidget::showAnimated( activate(); } +void HistoryWidget::showFast() { + validateSubsectionTabs(); + show(); +} + void HistoryWidget::showFinished() { _cornerButtons.finishAnimations(); if (_pinnedBar) { @@ -4939,6 +5121,10 @@ void HistoryWidget::doneShow() { controller()->widget()->setInnerFocus(); _preserveScrollTop = false; checkSuggestToGigagroup(); + + if (_history) { + _history->saveMeAsActiveSubsectionThread(); + } } void HistoryWidget::cornerButtonsShowAtPosition( @@ -4970,14 +5156,14 @@ FullMsgId HistoryWidget::cornerButtonsCurrentId() { bool HistoryWidget::checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn<void(int)> withPaymentApproved) { return _peer && _sendPayment.check( controller(), _peer, + options, messagesCount, - starsApproved, std::move(withPaymentApproved)); } @@ -5106,7 +5292,11 @@ void HistoryWidget::updateOverStates(QPoint pos) { st::historyReplyHeight); const auto hasWebPage = !!_previewDrawPreview; const auto inDetails = detailsRect.contains(pos) - && (_editMsgId || replyTo() || isReadyToForward || hasWebPage); + && (_editMsgId + || replyTo() + || isReadyToForward + || hasWebPage + || _suggestOptions); const auto inPhotoEdit = inDetails && _photoEditMedia && QRect( @@ -5164,9 +5354,11 @@ void HistoryWidget::sendBotCommand( copy.starsApproved = approved; sendBotCommand(request, copy); }; + + const auto action = prepareSendAction(options); const auto checked = checkSendPayment( 1, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; @@ -5181,7 +5373,7 @@ void HistoryWidget::sendBotCommand( ? request.command : Bot::WrapCommandInChat(_peer, request.command, request.context); - auto message = Api::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(action); message.textWithTags = { toSend, TextWithTags::Tags() }; message.action.replyTo = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) @@ -5370,7 +5562,10 @@ bool HistoryWidget::isBlocked() const { } bool HistoryWidget::isJoinChannel() const { - return _peer && _peer->isChannel() && !_peer->asChannel()->amIn(); + if (const auto channel = _peer ? _peer->asChannel() : nullptr) { + return !channel->amIn() && !channel->isMonoforum(); + } + return false; } bool HistoryWidget::isChoosingTheme() const { @@ -5417,7 +5612,7 @@ void HistoryWidget::updateSendButtonType() { using Type = Ui::SendButton::Type; const auto type = computeSendButtonType(); - // This logic is duplicated in RepliesWidget. + // This logic is duplicated in ChatWidget. const auto disabledBySlowmode = _peer && _peer->slowmodeApplied() && (_history->latestSendingMessage() != nullptr); @@ -5660,7 +5855,8 @@ void HistoryWidget::toggleKeyboard(bool manual) { if (!readyToForward() && !_previewDrawPreview && !_editMsgId - && !_replyTo) { + && !_replyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -5879,7 +6075,7 @@ void HistoryWidget::moveFieldControls() { } // (_botMenu.button) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel -// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send +// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_toggleSuggestPost) (_kbHide|_tabbedSelectorToggle) _send // (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages) auto buttonsBottom = bottom - _attachToggle->height(); @@ -5923,6 +6119,10 @@ void HistoryWidget::moveFieldControls() { if (kbShowShown || (_cmdStartShown && settings.showCommandsButtonInMessageField) || _silent) { right += _botCommandStart->width(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->moveToRight(right, buttonsBottom); + right += _toggleSuggestPost->width(); + } if (_giftToUser) { _giftToUser->moveToRight(right, buttonsBottom); right += _giftToUser->width(); @@ -5990,6 +6190,9 @@ void HistoryWidget::updateFieldSize() { if (_silent && !_silent->isHidden()) { fieldWidth -= _silent->width(); } + if (_toggleSuggestPost && !_toggleSuggestPost->isHidden()) { + fieldWidth -= _toggleSuggestPost->width(); + } if (_giftToUser && !_giftToUser->isHidden()) { fieldWidth -= _giftToUser->width(); } @@ -6068,11 +6271,9 @@ void HistoryWidget::updateFieldPlaceholder() { && !_keyboard->placeholder().isEmpty()) { return rpl::single(_keyboard->placeholder()); } else if (const auto stars = peer->starsPerMessageChecked()) { - return tr::lng_message_paid_ph( - lt_amount, - tr::lng_prize_credits_amount( - lt_count, - rpl::single(stars * 1.))); + return tr::lng_message_stars_ph( + lt_count, + rpl::single(stars * 1.)); } else if (const auto channel = peer->asChannel()) { const auto topic = resolveReplyToTopic(); const auto topicRootId = topic @@ -6192,7 +6393,7 @@ bool HistoryWidget::showSendMessageError( const TextWithTags &textWithTags, bool ignoreSlowmodeCountdown, Fn<void(int starsApproved)> withPaymentApproved, - int starsApproved) { + Api::SendOptions options) { if (!_canSendMessages) { return false; } @@ -6213,7 +6414,7 @@ bool HistoryWidget::showSendMessageError( return withPaymentApproved && !checkSendPayment( request.messagesCount, - starsApproved, + options, withPaymentApproved); } @@ -6244,6 +6445,7 @@ bool HistoryWidget::confirmSendingFiles( { _history->peer->id, _editMsgId }, std::move(list), _field->getTextWithTags(), + suggestOptions(), _mediaEditManager.spoilered(), _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); @@ -6328,6 +6530,11 @@ void HistoryWidget::sendingFilesConfirmed( void HistoryWidget::sendingFilesConfirmed( std::shared_ptr<Ui::PreparedBundle> bundle, Api::SendOptions options) { + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + const auto withPaymentApproved = [=](int approved) { auto copy = options; copy.starsApproved = approved; @@ -6335,16 +6542,12 @@ void HistoryWidget::sendingFilesConfirmed( }; const auto checked = checkSendPayment( bundle->totalCount, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - const auto compress = bundle->way.sendImagesAsPhotos(); - const auto type = compress ? SendMediaType::Photo : SendMediaType::File; - auto action = prepareSendAction(options); - action.clearDraft = false; if (bundle->sendComment) { auto message = Api::MessageToSend(action); message.textWithTags = base::take(bundle->caption); @@ -6527,40 +6730,50 @@ void HistoryWidget::resizeEvent(QResizeEvent *e) { } void HistoryWidget::updateControlsGeometry() { - _topBar->resizeToWidth(width()); + const auto width = this->width(); + + _topBar->resizeToWidth(width); _topBar->moveToLeft(0, 0); - _voiceRecordBar->resizeToWidth(width()); + + const auto tabsLeftSkip = _subsectionTabs + ? _subsectionTabs->leftSkip() + : 0; + const auto innerWidth = width - tabsLeftSkip; + + _voiceRecordBar->resizeToWidth(width); moveFieldControls(); - const auto groupCallTop = _topBar->bottomNoMargins(); + _topBars->move(tabsLeftSkip, _topBar->bottomNoMargins() + + (_subsectionTabs ? _subsectionTabs->topSkip() : 0)); + const auto groupCallTop = 0; if (_groupCallBar) { _groupCallBar->move(0, groupCallTop); - _groupCallBar->resizeToWidth(width()); + _groupCallBar->resizeToWidth(innerWidth); } const auto requestsTop = groupCallTop + (_groupCallBar ? _groupCallBar->height() : 0); if (_requestsBar) { _requestsBar->move(0, requestsTop); - _requestsBar->resizeToWidth(width()); + _requestsBar->resizeToWidth(innerWidth); } const auto pinnedBarTop = requestsTop + (_requestsBar ? _requestsBar->height() : 0); if (_pinnedBar) { _pinnedBar->move(0, pinnedBarTop); - _pinnedBar->resizeToWidth(width()); + _pinnedBar->resizeToWidth(innerWidth); } const auto sponsoredMessageBarTop = pinnedBarTop + (_pinnedBar ? _pinnedBar->height() : 0); if (_sponsoredMessageBar) { _sponsoredMessageBar->move(0, sponsoredMessageBarTop); - _sponsoredMessageBar->resizeToWidth(width()); + _sponsoredMessageBar->resizeToWidth(innerWidth); } const auto translateTop = sponsoredMessageBarTop + (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0); if (_translateBar) { _translateBar->move(0, translateTop); - _translateBar->resizeToWidth(width()); + _translateBar->resizeToWidth(innerWidth); } const auto paysStatusTop = translateTop + (_translateBar ? _translateBar->height() : 0); @@ -6570,17 +6783,21 @@ void HistoryWidget::updateControlsGeometry() { const auto contactStatusTop = paysStatusTop + (_paysStatus ? _paysStatus->bar().height() : 0); if (_contactStatus) { - _contactStatus->bar().move(0, contactStatusTop); + _contactStatus->bar().move(tabsLeftSkip, contactStatusTop); } const auto businessBotTop = contactStatusTop + (_contactStatus ? _contactStatus->bar().height() : 0); if (_businessBotStatus) { - _businessBotStatus->bar().move(0, businessBotTop); + _businessBotStatus->bar().move(tabsLeftSkip, businessBotTop); } - const auto scrollAreaTop = businessBotTop + const auto scrollAreaTop = _topBars->y() + + businessBotTop + (_businessBotStatus ? _businessBotStatus->bar().height() : 0); - if (_scroll->y() != scrollAreaTop) { - _scroll->moveToLeft(0, scrollAreaTop); + _topBars->resize( + innerWidth, + scrollAreaTop - _topBars->y() + st::lineWidth); + if (_scroll->y() != scrollAreaTop || _scroll->x() != tabsLeftSkip) { + _scroll->moveToLeft(tabsLeftSkip, scrollAreaTop); if (_autocomplete) { _autocomplete->setBoundings(_scroll->geometry()); } @@ -6610,7 +6827,7 @@ void HistoryWidget::updateControlsGeometry() { _topShadow->setGeometryToLeft( topShadowLeft, _topBar->bottomNoMargins(), - width() - topShadowLeft - topShadowRight, + width - topShadowLeft - topShadowRight, st::lineWidth); } @@ -6656,6 +6873,15 @@ FullReplyTo HistoryWidget::replyTo() const { : FullReplyTo(); } +SuggestPostOptions HistoryWidget::suggestOptions( + bool skipNoAdminCheck) const { + const auto checked = skipNoAdminCheck + || (_history && _history->suggestDraftAllowed()); + return (checked && _suggestOptions) + ? _suggestOptions->values() + : SuggestPostOptions(); +} + bool HistoryWidget::hasSavedScroll() const { Expects(_history != nullptr); @@ -6746,6 +6972,14 @@ int HistoryWidget::countAutomaticScrollTop() { } Data::SendError HistoryWidget::computeSendRestriction() const { + if (!_canSendMessages + && _peer->amMonoforumAdmin() + && !_peer->asChannel()->monoforumDisabled()) { + return Data::SendError({ + .text = tr::lng_monoforum_choose_to_reply(tr::now), + .monoforumAdmin = true, + }); + } const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; return (_peer && !Data::CanSendAnyOf(_peer, allWithoutPolls)) @@ -6808,7 +7042,12 @@ void HistoryWidget::updateHistoryGeometry( return; } - auto newScrollHeight = height() - _topBar->height(); + const auto newScrollWidth = width() + - (_subsectionTabs ? _subsectionTabs->leftSkip() : 0); + const auto subsectionTabsTop = _topBar->bottomNoMargins(); + auto newScrollHeight = height() + - subsectionTabsTop + - (_subsectionTabs ? _subsectionTabs->topSkip() : 0); if (_translateBar) { newScrollHeight -= _translateBar->height(); } @@ -6852,7 +7091,8 @@ void HistoryWidget::updateHistoryGeometry( if (_editMsgId || replyTo() || readyToForward() - || _previewDrawPreview) { + || _previewDrawPreview + || _suggestOptions) { newScrollHeight -= st::historyReplyHeight; } if (_kbShown) { @@ -6864,10 +7104,10 @@ void HistoryWidget::updateHistoryGeometry( } const auto wasScrollTop = _scroll->scrollTop(); const auto wasAtBottom = (wasScrollTop == _scroll->scrollTopMax()); - const auto needResize = (_scroll->width() != width()) + const auto needResize = (_scroll->width() != newScrollWidth) || (_scroll->height() != newScrollHeight); if (needResize) { - _scroll->resize(width(), newScrollHeight); + _scroll->resize(newScrollWidth, newScrollHeight); // on initial updateListSize we didn't put the _scroll->scrollTop // correctly yet so visibleAreaUpdated() call will erase it // with the new (undefined) value @@ -6885,6 +7125,12 @@ void HistoryWidget::updateHistoryGeometry( _cornerButtons.updatePositions(); controller()->floatPlayerAreaUpdated(); } + if (_subsectionTabs) { + const auto scrollBottom = _scroll->y() + newScrollHeight; + const auto areaHeight = scrollBottom - subsectionTabsTop; + _subsectionTabs->setBoundingRect( + { 0, subsectionTabsTop, width(), areaHeight }); + } updateListSize(); _updateHistoryGeometryRequired = false; @@ -7194,7 +7440,8 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _kbReplyTo = nullptr; if (!readyToForward() && !_previewDrawPreview - && !_replyTo) { + && !_replyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7214,7 +7461,8 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { if (!readyToForward() && !_previewDrawPreview && !_replyTo - && !_editMsgId) { + && !_editMsgId + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7342,6 +7590,7 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { _photoEditMedia, { _history->peer->id, _editMsgId }, _field->getTextWithTags(), + suggestOptions(), _mediaEditManager.spoilered(), _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); @@ -7350,10 +7599,14 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { } else if (_previewDrawPreview) { editDraftOptions(); } else if (_editMsgId) { - controller()->showPeerHistory( - _peer, - Window::SectionShow::Way::Forward, - _editMsgId); + if (_suggestOptions) { + _suggestOptions->edit(); + } else { + controller()->showPeerHistory( + _peer, + Window::SectionShow::Way::Forward, + _editMsgId); + } } else if (_replyTo && ((e->modifiers() & Qt::ControlModifier) || (e->button() != Qt::LeftButton))) { @@ -7368,6 +7621,8 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { _kbReplyTo->history()->peer->id, Window::SectionShow::Way::Forward, _kbReplyTo->id); + } else if (_suggestOptions) { + _suggestOptions->edit(); } } @@ -7376,6 +7631,7 @@ void HistoryWidget::editDraftOptions() { const auto history = _history; const auto reply = _replyTo; + const auto suggest = suggestOptions(); const auto webpage = _preview->draft(); const auto forward = _forwardPanel->draft(); @@ -7388,7 +7644,7 @@ void HistoryWidget::editDraftOptions() { } else { cancelReply(); } - history->setForwardDraft({}, std::move(forward)); + history->setForwardDraft(MsgId(), PeerId(), std::move(forward)); _preview->apply(webpage); }; const auto replyToId = reply.messageId; @@ -7400,14 +7656,16 @@ void HistoryWidget::editDraftOptions() { EditDraftOptions({ .show = controller()->uiShow(), .history = history, - .draft = Data::Draft(_field, reply, _preview->draft()), + .draft = Data::Draft(_field, reply, suggest, _preview->draft()), .usedLink = _preview->link(), .forward = _forwardPanel->draft(), .links = _preview->links(), .resolver = _preview->resolver(), .done = done, .highlight = highlight, - .clearOldDraft = [=] { ClearDraftReplyTo(history, 0, replyToId); }, + .clearOldDraft = [=] { + ClearDraftReplyTo(history, MsgId(), PeerId(), replyToId); + }, }); } @@ -7625,9 +7883,13 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { copy.options.starsApproved = approved; sendInlineResult(copy); }; + + auto action = prepareSendAction(result.options); + action.generateLocal = true; + const auto checked = checkSendPayment( 1, - result.options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; @@ -7635,9 +7897,6 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { controller()->sendingAnimation().appendSending( result.messageSendingFrom); - - auto action = prepareSendAction(result.options); - action.generateLocal = true; session().api().sendInlineResult( result.bot, result.result.get(), @@ -7645,9 +7904,7 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { result.messageSendingFrom.localId); clearFieldText(); - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); auto &bots = cRefRecentInlineBots(); const auto index = bots.indexOf(result.bot); @@ -7696,6 +7953,7 @@ void HistoryWidget::updatePinnedViewer() { _minPinnedId = Data::ResolveMinPinnedId( _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId _migrated ? _migrated->peer.get() : nullptr); } if (_pinnedClickedId @@ -7730,7 +7988,7 @@ void HistoryWidget::setupTranslateBar() { Expects(_history != nullptr); _translateBar = std::make_unique<HistoryView::TranslateBar>( - this, + _topBars.get(), controller(), _history); @@ -7755,10 +8013,6 @@ void HistoryWidget::setupTranslateBar() { }, _translateBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _translateBar->hide(); - } } void HistoryWidget::setupPinnedTracker() { @@ -7779,6 +8033,7 @@ void HistoryWidget::checkPinnedBarState() { const auto currentPinnedId = Data::ResolveTopPinnedId( _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId _migrated ? _migrated->peer.get() : nullptr); const auto universalPinnedId = !currentPinnedId ? int32(0) @@ -7805,13 +8060,14 @@ void HistoryWidget::checkPinnedBarState() { } clearHidingPinnedBar(); - _pinnedBar = std::make_unique<Ui::PinnedBar>(this, [=] { + _pinnedBar = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); auto pinnedRefreshed = Info::Profile::SharedMediaCountValue( _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId nullptr, Storage::SharedMediaType::Pinned ) | rpl::distinct_until_changed( @@ -7901,10 +8157,6 @@ void HistoryWidget::checkPinnedBarState() { }, _pinnedBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _pinnedBar->hide(); - } } void HistoryWidget::clearHidingPinnedBar() { @@ -8026,7 +8278,7 @@ void HistoryWidget::setupGroupCallBar() { return; } _groupCallBar = std::make_unique<Ui::GroupCallBar>( - this, + _topBars.get(), HistoryView::GroupCallBarContentByPeer( peer, st::historyGroupCallUserpics.size, @@ -8064,10 +8316,6 @@ void HistoryWidget::setupGroupCallBar() { }, _groupCallBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _groupCallBar->hide(); - } } void HistoryWidget::setupRequestsBar() { @@ -8079,7 +8327,7 @@ void HistoryWidget::setupRequestsBar() { return; } _requestsBar = std::make_unique<Ui::RequestsBar>( - this, + _topBars.get(), HistoryView::RequestsBarContentByPeer( peer, st::historyRequestsUserpics.size, @@ -8111,10 +8359,6 @@ void HistoryWidget::setupRequestsBar() { }, _requestsBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _requestsBar->hide(); - } } void HistoryWidget::requestMessageData(MsgId msgId) { @@ -8192,7 +8436,7 @@ void HistoryWidget::checkSponsoredMessageBar() { void HistoryWidget::createSponsoredMessageBar() { _sponsoredMessageBar = base::make_unique_q<Ui::SlideWrap<>>( - this, + _topBars.get(), object_ptr<Ui::RpWidget>(this)); _sponsoredMessageBar->entity()->resizeToWidth(_scroll->width()); @@ -8261,7 +8505,7 @@ bool HistoryWidget::sendExistingDocument( }; const auto checked = checkSendPayment( 1, - messageToSend.action.options.starsApproved, + messageToSend.action.options, withPaymentApproved); if (!checked) { return false; @@ -8274,9 +8518,7 @@ bool HistoryWidget::sendExistingDocument( if (_autocomplete && _autocomplete->stickersShown()) { clearFieldText(); - //_saveDraftText = true; - //_saveDraftStart = crl::now(); - //saveDraft(); + //saveDraftWithTextNow(); // won't be needed if SendInlineBotResult will clear the cloud draft saveCloudDraft(); @@ -8302,6 +8544,7 @@ bool HistoryWidget::sendExistingPhoto( } else if (showSlowmodeError()) { return false; } + const auto action = prepareSendAction(options); const auto withPaymentApproved = [=](int approved) { auto copy = options; @@ -8310,15 +8553,13 @@ bool HistoryWidget::sendExistingPhoto( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return false; } - Api::SendExistingPhoto( - Api::MessageToSend(prepareSendAction(options)), - photo); + Api::SendExistingPhoto(Api::MessageToSend(action), photo); hideSelectorControlsAnimated(); @@ -8355,6 +8596,61 @@ void HistoryWidget::showPremiumToast(not_null<DocumentData*> document) { _stickerToast->showFor(document); } +void HistoryWidget::validateSubsectionTabs() { + if (!_subsectionCheckLifetime + && _history + && _history->peer->isMegagroup()) { + _subsectionCheckLifetime = _history->peer->asChannel()->flagsValue( + ) | rpl::skip( + 1 + ) | rpl::filter([=](Data::Flags<ChannelDataFlags>::Change change) { + const auto mask = ChannelDataFlag::Forum + | ChannelDataFlag::ForumTabs + | ChannelDataFlag::MonoforumAdmin; + return change.diff & mask; + }) | rpl::start_with_next([=] { + validateSubsectionTabs(); + }); + } + if (!_history || !HistoryView::SubsectionTabs::UsedFor(_history)) { + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + updateControlsGeometry(); + if (const auto forum = _history->asForum()) { + controller()->showForum(forum, { + Window::SectionShow::Way::Backward, + anim::type::normal, + anim::activation::background, + }); + } + } + return; + } else if (_subsectionTabs) { + return; + } + _subsectionTabs = controller()->restoreSubsectionTabsFor(this, _history); + if (!_subsectionTabs) { + _subsectionTabs = std::make_unique<HistoryView::SubsectionTabs>( + controller(), + this, + _history); + } + _subsectionTabs->removeRequests() | rpl::start_with_next([=] { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + updateControlsGeometry(); + }, _subsectionTabsLifetime); + _subsectionTabs->layoutRequests() | rpl::start_with_next([=] { + _list->toggleRemoveFromUserpics(_subsectionTabs->leftSkip() > 0); + updateControlsGeometry(); + orderWidgets(); + }, _subsectionTabsLifetime); + _list->toggleRemoveFromUserpics(_subsectionTabs->leftSkip() > 0); + updateControlsGeometry(); + orderWidgets(); +} + void HistoryWidget::checkCharsCount() { _fieldCharsCountManager.setCount(Ui::ComputeFieldCharacterCount(_field)); checkCharsLimitation(); @@ -8473,7 +8769,9 @@ void HistoryWidget::processReply() { if (!_peer || !_processingReplyTo) { return processCancel(); - } else if (!_processingReplyItem) { + } + cancelSuggestPost(); + if (!_processingReplyItem) { session().api().requestMessageData( session().data().peer(_processingReplyTo.messageId.peer), _processingReplyTo.messageId.msg, @@ -8530,18 +8828,21 @@ void HistoryWidget::setReplyFieldsFromProcessing() { const auto id = base::take(_processingReplyTo); const auto item = base::take(_processingReplyItem); if (_editMsgId) { - if (const auto localDraft = _history->localDraft({})) { + if (const auto localDraft = _history->localDraft({}, {})) { localDraft->reply = id; + localDraft->suggest = SuggestPostOptions(); } else { _history->setLocalDraft(std::make_unique<Data::Draft>( TextWithTags(), id, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); } } else { _replyEditMsg = item; _replyTo = id; + cancelSuggestPost(); updateReplyEditText(_replyEditMsg); updateCanSendMessage(); updateBotKeyboard(); @@ -8552,10 +8853,7 @@ void HistoryWidget::setReplyFieldsFromProcessing() { refreshTopBarActiveChat(); } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); - + saveDraftWithTextNow(); setInnerFocus(); } @@ -8567,6 +8865,11 @@ void HistoryWidget::editMessage( } else if (_voiceRecordBar->isActive()) { controller()->showToast(tr::lng_edit_caption_voice(tr::now)); return; + } else if (const auto media = item->media()) { + if (const auto todolist = media->todolist()) { + Window::PeerMenuEditTodoList(controller(), item); + return; + } } else if (_composeSearch) { _composeSearch->hideAnimated(); } @@ -8576,13 +8879,15 @@ void HistoryWidget::editMessage( _send->clearState(); } if (!_editMsgId) { - if (_replyTo || !_field->empty()) { + const auto suggest = suggestOptions(); + if (_replyTo || suggest.exists || !_field->empty()) { _history->setLocalDraft(std::make_unique<Data::Draft>( _field, _replyTo, + suggest, _preview->draft())); } else { - _history->clearLocalDraft({}); + _history->clearLocalDraft(MsgId(), PeerId()); } } @@ -8596,6 +8901,7 @@ void HistoryWidget::editMessage( _history->setLocalEditDraft(std::make_unique<Data::Draft>( editData, FullReplyTo{ item->fullId() }, + SuggestPostOptions(), cursor, previewDraft)); applyDraft(); @@ -8612,10 +8918,7 @@ void HistoryWidget::editMessage( updateField(); SelectTextInFieldWithMargins(_field, selection); - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); - + saveDraftWithTextNow(); setInnerFocus(); } @@ -8650,6 +8953,7 @@ void HistoryWidget::hidePinnedMessage() { controller(), _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId crl::guard(this, callback)); } } @@ -8671,6 +8975,12 @@ bool HistoryWidget::lastForceReplyReplied() const { == FullMsgId(_peer->id, _history->lastKeyboardId)); } +bool HistoryWidget::cancelReplyOrSuggest(bool lastKeyboardUsed) { + const auto ok1 = cancelReply(lastKeyboardUsed); + const auto ok2 = cancelSuggestPost(); + return ok1 || ok2; +} + bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { bool wasReply = false; if (_replyTo) { @@ -8681,7 +8991,8 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { mouseMoveEvent(0); if (!readyToForward() && !_previewDrawPreview - && !_kbReplyTo) { + && !_kbReplyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -8692,19 +9003,17 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { updateControlsGeometry(); update(); } else if (const auto localDraft - = (_history ? _history->localDraft({}) : nullptr)) { + = (_history ? _history->localDraft({}, {}) : nullptr)) { if (localDraft->reply) { if (localDraft->textWithTags.text.isEmpty()) { - _history->clearLocalDraft({}); + _history->clearLocalDraft(MsgId(), PeerId()); } else { localDraft->reply = {}; } } } if (wasReply) { - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } if (!_editMsgId && _keyboard->singleUse() @@ -8718,7 +9027,7 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { } void HistoryWidget::cancelReplyAfterMediaSend(bool lastKeyboardUsed) { - if (cancelReply(lastKeyboardUsed)) { + if (cancelReplyOrSuggest(lastKeyboardUsed)) { saveCloudDraft(); } } @@ -8741,7 +9050,8 @@ void HistoryWidget::cancelEdit() { updateReplaceMediaButton(); _replyEditMsg = nullptr; setEditMsgId(0); - _history->clearLocalEditDraft({}); + _history->clearLocalEditDraft(MsgId(), PeerId()); + cancelSuggestPost(); applyDraft(); if (_saveEditMsgRequestId) { @@ -8749,14 +9059,13 @@ void HistoryWidget::cancelEdit() { _saveEditMsgRequestId = 0; } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); mouseMoveEvent(nullptr); if (!readyToForward() && !_previewDrawPreview - && !replyTo()) { + && !replyTo() + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -8783,12 +9092,25 @@ void HistoryWidget::cancelFieldAreaState() { } else if (_replyTo) { cancelReply(); } else if (readyToForward()) { - _history->setForwardDraft(MsgId(), {}); + _history->setForwardDraft(MsgId(), PeerId(), {}); } else if (_kbReplyTo) { toggleKeyboard(); + } else if (_suggestOptions) { + cancelSuggestPost(); } } +bool HistoryWidget::cancelSuggestPost() { + if (!_suggestOptions) { + return false; + } + _suggestOptions = nullptr; + updateControlsVisibility(); + updateControlsGeometry(); + saveDraftWithTextNow(); + return true; +} + void HistoryWidget::fullInfoUpdated() { auto refresh = false; if (_list) { @@ -8811,6 +9133,7 @@ void HistoryWidget::fullInfoUpdated() { sendBotStartCommand(); } refreshGiftToChannelShown(); + refreshDirectMessageShown(); } if (updateCmdStartShown()) { refresh = true; @@ -8870,10 +9193,17 @@ bool HistoryWidget::updateCanSendMessage() { const auto topic = resolveReplyToTopic(); const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; - const auto newCanSendMessages = topic + const auto onlyReplies = _peer->amMonoforumAdmin(); + const auto restrictedOnlyReplies = onlyReplies + && (!_replyTo.messageId || _replyTo.messageId.peer != _peer->id); + const auto newCanSendMessages = restrictedOnlyReplies + ? false + : topic ? Data::CanSendAnyOf(topic, allWithoutPolls) : Data::CanSendAnyOf(_peer, allWithoutPolls); - const auto newCanSendTexts = topic + const auto newCanSendTexts = restrictedOnlyReplies + ? false + : topic ? Data::CanSend(topic, ChatRestriction::SendOther) : Data::CanSend(_peer, ChatRestriction::SendOther); if (_canSendMessages == newCanSendMessages @@ -8883,8 +9213,9 @@ bool HistoryWidget::updateCanSendMessage() { _canSendMessages = newCanSendMessages; _canSendTexts = newCanSendTexts; if (!_canSendMessages) { - cancelReply(); + cancelReplyOrSuggest(); } + refreshSuggestPostToggle(); refreshScheduledToggle(); refreshSendGiftToggle(); refreshSilentToggle(); @@ -8985,8 +9316,9 @@ void HistoryWidget::escape() { } } else if (_autocomplete && !_autocomplete->isHidden()) { _autocomplete->hideAnimated(); - } else if (_replyTo && _field->getTextWithTags().text.isEmpty()) { - cancelReply(); + } else if ((_replyTo || _suggestOptions) + && _field->getTextWithTags().empty()) { + cancelReplyOrSuggest(); } else if (auto &voice = _voiceRecordBar; voice->isActive()) { voice->showDiscardBox(nullptr, anim::type::normal); } else { @@ -9171,7 +9503,7 @@ void HistoryWidget::updateReplyEditTexts(bool force) { void HistoryWidget::updateForwarding() { _forwardPanel->update(_history, _history - ? _history->resolveForwardDraft(MsgId()) + ? _history->resolveForwardDraft(MsgId(), PeerId()) : Data::ResolvedForwardDraft()); updateControlsVisibility(); updateControlsGeometry(); @@ -9212,13 +9544,12 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { auto backh = fieldHeight() + 2 * st::historySendPadding; auto hasForward = readyToForward(); auto drawMsgText = (_editMsgId || _replyTo) ? _replyEditMsg : _kbReplyTo; - if (_editMsgId || _replyTo || (!hasForward && _kbReplyTo)) { - backy -= st::historyReplyHeight; - backh += st::historyReplyHeight; - } else if (hasForward) { - backy -= st::historyReplyHeight; - backh += st::historyReplyHeight; - } else if (_previewDrawPreview) { + if (_editMsgId + || _replyTo + || hasForward + || _kbReplyTo + || _previewDrawPreview + || _suggestOptions) { backy -= st::historyReplyHeight; backh += st::historyReplyHeight; } @@ -9282,14 +9613,18 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { const auto paused = p.inactive(); const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler); auto replyLeft = st::historyReplySkip; - (_editMsgId - ? st::historyEditIcon - : (_replyTo && !_replyTo.quote.empty()) - ? st::historyQuoteIcon - : st::historyReplyIcon).paint( - p, - st::historyReplyIconPosition + QPoint(0, backy), - width()); + if (_suggestOptions) { + _suggestOptions->paintIcon(p, 0, backy, width()); + } else { + (_editMsgId + ? st::historyEditIcon + : (_replyTo && !_replyTo.quote.empty()) + ? st::historyQuoteIcon + : st::historyReplyIcon).paint( + p, + st::historyReplyIconPosition + QPoint(0, backy), + width()); + } if (drawMsgText) { if (hasPreview) { if (preview) { @@ -9327,37 +9662,41 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { } replyLeft += st::historyReplyPreview + st::msgReplyBarSkip; } - p.setPen(st::historyReplyNameFg); - if (_editMsgId) { - paintEditHeader(p, rect, replyLeft, backy); + if (_suggestOptions) { + _suggestOptions->paintLines(p, replyLeft, backy, width()); } else { - _replyToName.drawElided( - p, - replyLeft, - backy + st::msgReplyPadding.top(), - width() + p.setPen(st::historyReplyNameFg); + if (_editMsgId) { + paintEditHeader(p, rect, replyLeft, backy); + } else { + _replyToName.drawElided( + p, + replyLeft, + backy + st::msgReplyPadding.top(), + width() + - replyLeft + - _fieldBarCancel->width() + - st::msgReplyPadding.right()); + } + p.setPen(st::historyComposeAreaFg); + _replyEditMsgText.draw(p, { + .position = QPoint( + replyLeft, + st::msgReplyPadding.top() + + st::msgServiceNameFont->height + + backy), + .availableWidth = width() - replyLeft - _fieldBarCancel->width() - - st::msgReplyPadding.right()); + - st::msgReplyPadding.right(), + .palette = &st::historyComposeAreaPalette, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = now, + .pausedEmoji = paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = pausedSpoiler, + .elisionLines = 1, + }); } - p.setPen(st::historyComposeAreaFg); - _replyEditMsgText.draw(p, { - .position = QPoint( - replyLeft, - st::msgReplyPadding.top() - + st::msgServiceNameFont->height - + backy), - .availableWidth = width() - - replyLeft - - _fieldBarCancel->width() - - st::msgReplyPadding.right(), - .palette = &st::historyComposeAreaPalette, - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = now, - .pausedEmoji = paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = pausedSpoiler, - .elisionLines = 1, - }); } else { p.setFont(st::msgDateFont); p.setPen(st::historyComposeAreaFgService); @@ -9383,6 +9722,8 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { - _fieldBarCancel->width() - st::msgReplyPadding.right(); _forwardPanel->paint(p, x, backy, available, width()); + } else if (_suggestOptions) { + _suggestOptions->paintBar(p, 0, backy, width()); } } @@ -9484,7 +9825,8 @@ void HistoryWidget::paintEvent(QPaintEvent *e) { if (restrictionHidden || replyTo() || readyToForward() - || _kbShown) { + || _kbShown + || _suggestOptions) { if (!isSearching()) { drawField(p, clip); } @@ -9564,5 +9906,7 @@ HistoryWidget::~HistoryWidget() { session().data().itemVisibilitiesUpdated(); } + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; setTabbedPanel(nullptr); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index b52b49bbb8..37ac772c18 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -107,7 +107,10 @@ class Element; class PinnedTracker; class TranslateBar; class ComposeSearch; +class SubsectionTabs; struct SelectedQuote; +class SuggestOptions; +enum class SuggestMode; } // namespace HistoryView namespace HistoryView::Controls { @@ -183,6 +186,7 @@ public: void showAnimated( Window::SlideDirection direction, const Window::SectionSlideParams ¶ms); + void showFast(); void finishAnimating(); void doneShow(); @@ -212,9 +216,13 @@ public: not_null<PeerData*> peer); [[nodiscard]] FullReplyTo replyTo() const; + [[nodiscard]] SuggestPostOptions suggestOptions( + bool skipNoAdminCheck = false) const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; + bool cancelReplyOrSuggest(bool lastKeyboardUsed = false); bool cancelReply(bool lastKeyboardUsed = false); + bool cancelSuggestPost(); void cancelEdit(); void updateForwarding(); @@ -364,7 +372,7 @@ private: [[nodiscard]] bool checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn<void(int)> withPaymentApproved); void checkSuggestToGigagroup(); @@ -387,6 +395,7 @@ private: void saveDraft(bool delayed = false); void saveCloudDraft(); void saveDraftDelayed(); + void saveDraftWithTextNow(); void showMembersDropdown(); void windowIsVisibleChanged(); void saveFieldToHistoryLocalDraft(); @@ -407,6 +416,7 @@ private: void refreshJoinChannelText(); void refreshGiftToChannelShown(); + void refreshDirectMessageShown(); void requestMessageData(MsgId msgId); void messageDataReceived(not_null<PeerData*> peer, MsgId msgId); @@ -483,7 +493,7 @@ private: const TextWithTags &textWithTags, bool ignoreSlowmodeCountdown, Fn<void(int starsApproved)> withPaymentApproved = nullptr, - int starsApproved = 0); + Api::SendOptions options = {}); void sendingFilesConfirmed( Ui::PreparedList &&list, @@ -539,6 +549,7 @@ private: void setupShortcuts(); void setupGiftToChannelButton(); + void setupDirectMessageButton(); void handlePeerMigration(); @@ -584,7 +595,7 @@ private: void createUnreadBarAndResize(); [[nodiscard]] TextWithEntities prepareTextForEditMsg() const; - void saveEditMsg(); + void saveEditMessage(Api::SendOptions options = {}); void setupPreview(); void editDraftOptions(); @@ -676,6 +687,10 @@ private: void setupScheduledToggle(); void refreshScheduledToggle(); void refreshSendGiftToggle(); + void refreshSuggestPostToggle(); + void applySuggestOptions( + SuggestPostOptions suggest, + HistoryView::SuggestMode mode); void setupSendAsToggle(); void refreshSendAsToggle(); void refreshAttachBotsMenu(); @@ -686,6 +701,8 @@ private: void switchToSearch(QString query); + void validateSubsectionTabs(); + void checkCharsCount(); void checkCharsLimitation(); @@ -707,8 +724,12 @@ private: std::unique_ptr<Ui::SpoilerAnimation> _replySpoiler; mutable base::Timer _updateEditTimeLeftDisplay; + std::unique_ptr<HistoryView::SuggestOptions> _suggestOptions; + object_ptr<Ui::IconButton> _fieldBarCancel; + std::unique_ptr<Ui::RpWidget> _topBars; + std::unique_ptr<HistoryView::TranslateBar> _translateBar; int _translateBarHeight = 0; @@ -801,8 +822,9 @@ private: object_ptr<Ui::FlatButton> _botStart; object_ptr<Ui::FlatButton> _joinChannel; object_ptr<Ui::FlatButton> _muteUnmute; - QPointer<Ui::IconButton> _giftToChannelIn; - QPointer<Ui::IconButton> _giftToChannelOut; + QPointer<Ui::IconButton> _giftToChannel; + QPointer<Ui::IconButton> _directMessage; + rpl::lifetime _directMessageLifetime; QPointer<Ui::IconButton> _giftToChannelDiscuss; object_ptr<Ui::FlatButton> _discuss; object_ptr<Ui::FlatButton> _reportMessages; @@ -818,6 +840,7 @@ private: object_ptr<Ui::IconButton> _botKeyboardShow; object_ptr<Ui::IconButton> _botKeyboardHide; object_ptr<Ui::IconButton> _botCommandStart; + object_ptr<Ui::IconButton> _toggleSuggestPost = { nullptr }; object_ptr<Ui::IconButton> _giftToUser = { nullptr }; object_ptr<Ui::SilentToggle> _silent = { nullptr }; object_ptr<Ui::IconButton> _scheduled = { nullptr }; @@ -825,6 +848,9 @@ private: const std::unique_ptr<VoiceRecordBar> _voiceRecordBar; const std::unique_ptr<ForwardPanel> _forwardPanel; std::unique_ptr<HistoryView::ComposeSearch> _composeSearch; + std::unique_ptr<HistoryView::SubsectionTabs> _subsectionTabs; + rpl::lifetime _subsectionTabsLifetime; + rpl::lifetime _subsectionCheckLifetime; bool _cmdStartShown = false; object_ptr<Ui::InputField> _field; base::unique_qptr<Ui::RpWidget> _fieldDisabled; diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index ea4bcb3178..3e76145f2e 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -66,6 +66,7 @@ struct WriteRestriction { struct SetHistoryArgs { required<History*> history; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; Fn<bool()> showSlowmodeError; Fn<Api::SendAction()> sendActionFactory; rpl::producer<int> slowmodeSecondsLeft; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 024d96c50a..6cca420dbf 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_drafts.h" #include "data/data_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_chat.h" @@ -53,8 +54,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_media_edit_manager.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" -#include "history/view/controls/history_view_voice_record_bar.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/controls/history_view_ttl_button.h" +#include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_webpage_processor.h" #include "history/view/history_view_reply.h" #include "history/view/history_view_webpage_preview.h" @@ -83,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/spoiler_mess.h" #include "webrtc/webrtc_environment.h" #include "window/window_adaptive.h" +#include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "mainwindow.h" #include "styles/style_chat.h" @@ -132,7 +135,10 @@ public: void updateTopicRootId(MsgId topicRootId); void init(); - void editMessage(FullMsgId id, bool photoEditAllowed = false); + void editMessage( + FullMsgId id, + SuggestPostOptions suggest, + bool photoEditAllowed = false); void replyToMessage(FullReplyTo id); void updateForwarding( Data::Thread *thread, @@ -157,6 +163,7 @@ public: [[nodiscard]] SendMenu::Details saveMenuDetails(bool hasSendText) const; [[nodiscard]] FullReplyTo getDraftReply() const; + [[nodiscard]] SuggestPostOptions suggestOptions() const; [[nodiscard]] rpl::producer<> editCancelled() const { return _editCancelled.events(); } @@ -169,6 +176,9 @@ public: [[nodiscard]] rpl::producer<> previewCancelled() const { return _previewCancelled.events(); } + [[nodiscard]] rpl::producer<> saveDraftRequests() const { + return _saveDraftRequests.events(); + } [[nodiscard]] rpl::producer<bool> visibleChanged(); @@ -186,6 +196,9 @@ private: bool hasPreview() const; + void applySuggestOptions(SuggestPostOptions suggest, SuggestMode mode); + void cancelSuggestPost(); + struct Preview { Controls::WebpageParsed parsed; Ui::Text::String title; @@ -197,17 +210,20 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; Preview _preview; rpl::event_stream<> _editCancelled; rpl::event_stream<> _replyCancelled; rpl::event_stream<> _forwardCancelled; rpl::event_stream<> _previewCancelled; + rpl::event_stream<> _saveDraftRequests; rpl::lifetime _previewLifetime; rpl::variable<FullMsgId> _editMsgId; rpl::variable<FullReplyTo> _replyTo; std::unique_ptr<ForwardPanel> _forwardPanel; + std::unique_ptr<SuggestOptions> _suggestOptions; rpl::producer<> _toForwardUpdated; HistoryItem *_shownMessage = nullptr; @@ -254,6 +270,7 @@ FieldHeader::FieldHeader( void FieldHeader::setHistory(const SetHistoryArgs &args) { _history = *args.history; _topicRootId = args.topicRootId; + _monoforumPeerId = args.monoforumPeerId; } void FieldHeader::updateTopicRootId(MsgId topicRootId) { @@ -278,11 +295,13 @@ void FieldHeader::init() { p.fillRect(rect(), st::historyComposeAreaBg); const auto position = st::historyReplyIconPosition; - if (_preview.parsed) { + if (_suggestOptions) { + _suggestOptions->paintIcon(p, 0, 0, width()); + } else if (_preview.parsed) { st::historyLinkIcon.paint(p, position, width()); } else if (isEditingMessage()) { st::historyEditIcon.paint(p, position, width()); - } else if (const auto reply = replyingToMessage()) { + } else if (const auto reply = replyingToMessage(); reply.replying()) { if (!reply.quote.empty()) { st::historyQuoteIcon.paint(p, position, width()); } else { @@ -401,9 +420,13 @@ void FieldHeader::init() { if (_preview.parsed) { _editOptionsRequests.fire({}); } else if (isEditingMessage()) { - _jumpToItemRequests.fire(FullReplyTo{ - .messageId = _editMsgId.current() - }); + if (_suggestOptions) { + _suggestOptions->edit(); + } else { + _jumpToItemRequests.fire(FullReplyTo{ + .messageId = _editMsgId.current() + }); + } } else if (reply && (e->modifiers() & Qt::ControlModifier)) { _jumpToItemRequests.fire_copy(reply); } else if (reply || readyToForward()) { @@ -643,7 +666,11 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { st::historyEditMedia.paintInCenter(p, to); p.setOpacity(1.); } + } + if (_suggestOptions) { + _suggestOptions->paintLines(p, textLeft, 0, width()); + return; } p.setPen(st::historyReplyNameFg); @@ -730,6 +757,12 @@ FullReplyTo FieldHeader::getDraftReply() const { : _replyTo.current(); } +SuggestPostOptions FieldHeader::suggestOptions() const { + return _suggestOptions + ? _suggestOptions->values() + : SuggestPostOptions(); +} + void FieldHeader::updateControlsGeometry(QSize size) { _cancel->moveToRight(0, 0); _clickableRect = QRect( @@ -744,7 +777,10 @@ void FieldHeader::updateControlsGeometry(QSize size) { st::historyReplyPreview); } -void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { +void FieldHeader::editMessage( + FullMsgId id, + SuggestPostOptions suggest, + bool photoEditAllowed) { _photoEditAllowed = photoEditAllowed; _editMsgId = id; if (!id) { @@ -756,10 +792,40 @@ void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { _inPhotoEdit = false; _inPhotoEditOver.stop(); } + if (id && suggest) { + applySuggestOptions(suggest, SuggestMode::Change); + } else { + cancelSuggestPost(); + } update(); } +void FieldHeader::applySuggestOptions( + SuggestPostOptions suggest, + SuggestMode mode) { + Expects(suggest.exists); + + using namespace HistoryView; + _suggestOptions = std::make_unique<SuggestOptions>( + _show, + _history->peer, + suggest, + mode); + _suggestOptions->updates() | rpl::start_with_next([=] { + update(); + _saveDraftRequests.fire({}); + }, _suggestOptions->lifetime()); +} + +void FieldHeader::cancelSuggestPost() { + if (!_suggestOptions) { + return; + } + _suggestOptions = nullptr; +} + void FieldHeader::replyToMessage(FullReplyTo id) { + id.monoforumPeerId = 0; _replyTo = id; } @@ -797,6 +863,7 @@ MessageToEdit FieldHeader::queryToEdit() { .scheduled = item->isScheduled() ? item->date() : 0, .shortcutId = item->shortcutId(), .invertCaption = _mediaEditManager.invertCaption(), + .suggest = suggestOptions(), }, .spoilered = _mediaEditManager.spoilered(), }; @@ -956,6 +1023,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { unregisterDraftSources(); _history = history; _topicRootId = args.topicRootId; + _monoforumPeerId = args.monoforumPeerId; _historyLifetime.destroy(); _header->setHistory(args); registerDraftSource(); @@ -999,6 +1067,8 @@ void ComposeControls::setCurrentDialogsEntryState( Dialogs::EntryState state) { unregisterDraftSources(); state.currentReplyTo.topicRootId = _topicRootId; + state.currentReplyTo.monoforumPeerId = _monoforumPeerId; + state.currentSuggest = SuggestPostOptions(); _currentDialogsEntryState = state; updateForwarding(); registerDraftSource(); @@ -1161,6 +1231,7 @@ bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { _editingId, std::move(list), _field->getTextWithTags(), + _header->suggestOptions(), queryToEdit.spoilered, queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); @@ -1292,7 +1363,11 @@ void ComposeControls::saveFieldToHistoryLocalDraft() { const auto key = draftKeyCurrent(); _history->setDraft( key, - std::make_unique<Data::Draft>(_field, id, _preview->draft())); + std::make_unique<Data::Draft>( + _field, + id, + SuggestPostOptions(), + _preview->draft())); } else { _history->clearDraft(draftKeyCurrent()); } @@ -1396,6 +1471,7 @@ void ComposeControls::init() { _photoEditMedia, _editingId, _field->getTextWithTags(), + _header->suggestOptions(), queryToEdit.spoilered, queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); @@ -1405,7 +1481,9 @@ void ComposeControls::init() { ) | rpl::start_with_next([=] { const auto history = _history; const auto topicRootId = _topicRootId; + const auto monoforumPeerId = _monoforumPeerId; const auto reply = _header->replyingToMessage(); + const auto suggest = SuggestPostOptions(); const auto webpage = _preview->draft(); const auto done = [=]( @@ -1417,7 +1495,10 @@ void ComposeControls::init() { } else { cancelReplyMessage(); } - history->setForwardDraft(topicRootId, std::move(forward)); + history->setForwardDraft( + topicRootId, + monoforumPeerId, + std::move(forward)); _preview->apply(webpage); _field->setFocus(); }; @@ -1430,7 +1511,7 @@ void ComposeControls::init() { EditDraftOptions({ .show = _show, .history = history, - .draft = Data::Draft(_field, reply, _preview->draft()), + .draft = Data::Draft(_field, reply, suggest, _preview->draft()), .usedLink = _preview->link(), .forward = _header->forwardDraft(), .links = _preview->links(), @@ -1440,6 +1521,7 @@ void ComposeControls::init() { .clearOldDraft = [=] { ClearDraftReplyTo( history, topicRootId, + monoforumPeerId, replyToId); }, }); }, _wrap->lifetime()); @@ -1449,9 +1531,12 @@ void ComposeControls::init() { if (_preview) { _preview->apply({ .removed = true }); } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); + }, _wrap->lifetime()); + + _header->saveDraftRequests( + ) | rpl::start_with_next([=] { + saveDraftWithTextNow(); }, _wrap->lifetime()); _header->editCancelled( @@ -1686,9 +1771,7 @@ void ComposeControls::initFieldAutocomplete() { if (!_showSlowmodeError || !_showSlowmodeError()) { setText({}); } - //_saveDraftText = true; - //_saveDraftStart = crl::now(); - //saveDraft(); + //saveDraftWithTextNow(); // Won't be needed if SendInlineBotResult clears the cloud draft. //saveCloudDraft(); _fileChosen.fire(std::move(data)); @@ -1732,11 +1815,9 @@ void ComposeControls::updateFieldPlaceholder() { } else if (!peer) { return tr::lng_message_ph(); } else if (const auto stars = peer->starsPerMessageChecked()) { - return tr::lng_message_paid_ph( - lt_amount, - tr::lng_prize_credits_amount( - lt_count, - rpl::single(stars * 1.))); + return tr::lng_message_stars_ph( + lt_count, + rpl::single(stars * 1.)); } else if (const auto channel = peer->asChannel()) { if (channel->isBroadcast()) { return session().data().notifySettings().silentPosts(channel) @@ -1807,9 +1888,10 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const { switch (_currentDialogsEntryState.section) { case Section::History: case Section::Replies: + case Section::SavedSublist: return (type == DraftType::Edit) - ? Key::LocalEdit(_topicRootId) - : Key::Local(_topicRootId); + ? Key::LocalEdit(_topicRootId, _monoforumPeerId) + : Key::Local(_topicRootId, _monoforumPeerId); case Section::Scheduled: return (type == DraftType::Edit) ? Key::ScheduledEdit() @@ -1826,6 +1908,12 @@ Data::DraftKey ComposeControls::draftKeyCurrent() const { return draftKey(isEditingMessage() ? DraftType::Edit : DraftType::Normal); } +void ComposeControls::saveDraftWithTextNow() { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); +} + void ComposeControls::saveDraft(bool delayed) { if (delayed) { const auto now = crl::now(); @@ -1878,6 +1966,7 @@ void ComposeControls::registerDraftSource() { const auto draft = [=] { return Storage::MessageDraft{ _header->getDraftReply(), + _header->suggestOptions(), _field->getTextWithTags(), _preview->draft(), }; @@ -1928,6 +2017,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { const auto editingId = (draft && draft == editDraft) ? draft->reply.messageId : FullMsgId(); + const auto editingSuggest = (draft && draft == editDraft) + ? draft->suggest + : SuggestPostOptions(); InvokeQueued(_autocomplete.get(), [=] { if (_autocomplete) { @@ -1948,7 +2040,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { if (hadFocus) { _field->setFocus(); } - _header->editMessage({}); + _header->editMessage({}, {}); _header->replyToMessage({}); if (_preview) { _preview->apply({ .removed = true }); @@ -1995,7 +2087,10 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { Data::PhotoSize::Large, item->fullId()); } - _header->editMessage(editingId, _photoEditMedia != nullptr); + _header->editMessage( + editingId, + editingSuggest, + _photoEditMedia != nullptr); if (_preview) { _preview->apply( Data::WebPageDraft::FromItem(item), @@ -2006,7 +2101,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; - _header->editMessage(editingId, false); + _header->editMessage(editingId, SuggestPostOptions(), false); return false; }; if (!resolve()) { @@ -2028,7 +2123,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; _header->replyToMessage(draft->reply); - _header->editMessage({}); + _header->editMessage({}, {}); if (_preview) { _preview->setDisabled(false); } @@ -2037,7 +2132,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } void ComposeControls::cancelForward() { - _history->setForwardDraft(_topicRootId, {}); + _history->setForwardDraft(_topicRootId, _monoforumPeerId, {}); updateForwarding(); } @@ -2901,9 +2996,11 @@ void ComposeControls::toggleTabbedSelectorMode() { && !_regularWindow->adaptive().isOneColumn()) { Core::App().settings().setTabbedSelectorSectionEnabled(true); Core::App().saveSettingsDelayed(); - const auto topic = _history->peer->forumTopicFor(_topicRootId); + const auto thread = _history->threadFor( + _topicRootId, + _monoforumPeerId); pushTabbedSelectorToThirdSection( - (topic ? topic : (Data::Thread*)_history), + thread ? thread : _history, Window::SectionShow::Way::ClearStack); } else { _tabbedPanel->toggleAnimated(); @@ -2938,6 +3035,12 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) { if (_voiceRecordBar->isActive()) { _show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice())); return; + } else if (const auto media = item->media()) { + if (const auto todolist = media->todolist()) { + Assert(_regularWindow != nullptr); + Window::PeerMenuEditTodoList(_regularWindow, item); + return; + } } if (!isEditingMessage()) { @@ -2957,7 +3060,9 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) { FullReplyTo{ .messageId = item->fullId(), .topicRootId = key.topicRootId(), + .monoforumPeerId = key.monoforumPeerId(), }, + SuggestPostOptions(), cursor, Data::WebPageDraft::FromItem(item))); applyDraft(); @@ -2992,6 +3097,7 @@ bool ComposeControls::updateReplaceMediaButton() { _regularWindow, _editingId, _field->getTextWithTags(), + _header->suggestOptions(), queryToEdit.spoilered, queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); @@ -3006,10 +3112,7 @@ void ComposeControls::cancelEditMessage() { _history->clearDraft(draftKey(DraftType::Edit)); applyDraft(); - - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } void ComposeControls::maybeCancelEditMessage() { @@ -3037,6 +3140,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { Expects(draftKeyCurrent() != Data::DraftKey::None()); id.topicRootId = _topicRootId; + id.monoforumPeerId = _monoforumPeerId; if (!id) { cancelReplyMessage(); return; @@ -3044,6 +3148,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { if (isEditingMessage()) { const auto key = draftKey(DraftType::Normal); Assert(key.topicRootId() == id.topicRootId); + Assert(key.monoforumPeerId() == id.monoforumPeerId); if (const auto localDraft = _history->draft(key)) { localDraft->reply = id; } else { @@ -3052,16 +3157,14 @@ void ComposeControls::replyToMessage(FullReplyTo id) { std::make_unique<Data::Draft>( TextWithTags(), id, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); } } else { _header->replyToMessage(id); } - - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } void ComposeControls::cancelReplyMessage() { @@ -3079,20 +3182,17 @@ void ComposeControls::cancelReplyMessage() { } } if (wasReply) { - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } } } void ComposeControls::updateForwarding() { - const auto rootId = _topicRootId; - const auto thread = (_history && rootId) - ? _history->peer->forumTopicFor(rootId) + const auto thread = (_history && (_topicRootId || _monoforumPeerId)) + ? _history->threadFor(_topicRootId, _monoforumPeerId) : (Data::Thread*)_history; _header->updateForwarding(thread, thread - ? _history->resolveForwardDraft(rootId) + ? _history->resolveForwardDraft(_topicRootId, _monoforumPeerId) : Data::ResolvedForwardDraft()); updateSendButtonType(); } @@ -3107,7 +3207,7 @@ bool ComposeControls::handleCancelRequest() { } else if (isEditingMessage()) { maybeCancelEditMessage(); return true; - } else if (replyingToMessage()) { + } else if (replyingToMessage().replying()) { cancelReplyMessage(); return true; } else if (readyToForward()) { @@ -3185,6 +3285,11 @@ void ComposeControls::initForwardProcess() { && topic->rootId() == _topicRootId) { updateForwarding(); } + } else if (const auto sublist = update.entry->asSublist()) { + if (sublist->owningHistory() == _history + && sublist->sublistPeer()->id == _monoforumPeerId) { + updateForwarding(); + } } }, _wrap->lifetime()); @@ -3208,6 +3313,7 @@ bool ComposeControls::isEditingMessage() const { FullReplyTo ComposeControls::replyingToMessage() const { auto result = _header->replyingToMessage(); result.topicRootId = _topicRootId; + result.monoforumPeerId = _monoforumPeerId; return result; } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 2b97c4f985..4efc974160 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -331,6 +331,7 @@ private: [[nodiscard]] Data::DraftKey draftKeyCurrent() const; void saveDraft(bool delayed = false); void saveDraftDelayed(); + void saveDraftWithTextNow(); void saveCloudDraft(); void writeDrafts(); @@ -365,6 +366,7 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; BusinessShortcutId _shortcutId = 0; Fn<bool()> _showSlowmodeError; Fn<Api::SendAction()> _sendActionFactory; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 549d252331..f398211636 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -1375,18 +1375,21 @@ void ShowReplyToChatBox( auto chosen = [=](not_null<Data::Thread*> thread) mutable { const auto history = thread->owningHistory(); const auto topicRootId = thread->topicRootId(); - const auto draft = history->localDraft(topicRootId); + const auto monoforumPeerId = thread->monoforumPeerId(); + const auto draft = history->localDraft(topicRootId, monoforumPeerId); const auto textWithTags = draft ? draft->textWithTags : TextWithTags(); const auto cursor = draft ? draft->cursor : MessageCursor(); reply.topicRootId = topicRootId; + reply.monoforumPeerId = monoforumPeerId; history->setLocalDraft(std::make_unique<Data::Draft>( textWithTags, reply, + SuggestPostOptions(), cursor, Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( thread, Data::EntryUpdate::Flag::LocalDraftSet); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index 44e059ff34..de53aeb673 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/history_item_components.h" #include "history/view/history_view_item_preview.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_forum_topic.h" @@ -74,6 +75,11 @@ void ForwardPanel::update( ) | rpl::start_with_next([=] { update(nullptr, {}); }, _dataLifetime); + } else if (const auto sublist = _to->asSublist()) { + sublist->destroyed( + ) | rpl::start_with_next([=] { + update(nullptr, {}); + }, _dataLifetime); } updateTexts(); @@ -231,8 +237,10 @@ void ForwardPanel::applyOptions(Data::ForwardOptions options) { if (_data.items.empty()) { return; } else if (_data.options != options) { + const auto topicRootId = _to->topicRootId(); + const auto monoforumPeerId = _to->monoforumPeerId(); _data.options = options; - _to->owningHistory()->setForwardDraft(_to->topicRootId(), { + _to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, { .ids = _to->owner().itemsToIds(_data.items), .options = options, }); @@ -256,7 +264,9 @@ void ForwardPanel::editToNextOption() { ? Options::NoNamesAndCaptions : Options::PreserveInfo; - _to->owningHistory()->setForwardDraft(_to->topicRootId(), { + const auto topicRootId = _to->topicRootId(); + const auto monoforumPeerId = _to->monoforumPeerId(); + _to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, { .ids = _to->owner().itemsToIds(_data.items), .options = next, }); @@ -332,20 +342,26 @@ void ForwardPanel::paint( void ClearDraftReplyTo( not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, FullMsgId equalTo) { - const auto local = history->localDraft(topicRootId); + const auto local = history->localDraft(topicRootId, monoforumPeerId); if (!local || (equalTo && local->reply.messageId != equalTo)) { return; } auto draft = *local; - draft.reply = { .topicRootId = topicRootId }; + draft.reply = { + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }; + draft.suggest = SuggestPostOptions(); if (Data::DraftIsNull(&draft)) { - history->clearLocalDraft(topicRootId); + history->clearLocalDraft(topicRootId, monoforumPeerId); } else { history->setLocalDraft( std::make_unique<Data::Draft>(std::move(draft))); } - if (const auto thread = history->threadFor(topicRootId)) { + const auto thread = history->threadFor(topicRootId, monoforumPeerId); + if (thread) { history->session().api().saveDraftToCloudDelayed(thread); } } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index 54624d47d8..2e8dcc36d5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -76,6 +76,7 @@ private: void ClearDraftReplyTo( not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, FullMsgId equalTo); void EditWebPageOptions( diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp new file mode 100644 index 0000000000..f599f8a63b --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -0,0 +1,877 @@ +/* +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 "history/view/controls/history_view_suggest_options.h" + +#include "base/event_filter.h" +#include "base/unixtime.h" +#include "chat_helpers/compose/compose_show.h" +#include "core/ui_integration.h" +#include "data/components/credits.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" +#include "data/data_media_types.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "info/channel_statistics/earn/earn_format.h" +#include "info/channel_statistics/earn/earn_icons.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "settings/settings_credits_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/boxes/choose_date_time.h" +#include "ui/controls/ton_common.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/basic_click_handlers.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/vertical_list.h" +#include "styles/style_boxes.h" +#include "styles/style_channel_earn.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace HistoryView { + +void ChooseSuggestTimeBox( + not_null<Ui::GenericBox*> box, + SuggestTimeBoxArgs &&args) { + const auto now = base::unixtime::now(); + const auto min = args.session->appConfig().suggestedPostDelayMin() + 60; + const auto max = args.session->appConfig().suggestedPostDelayMax(); + const auto value = args.value + ? std::clamp(args.value, now + min, now + max) + : (now + 86400); + const auto done = args.done; + Ui::ChooseDateTimeBox(box, { + .title = ((args.mode == SuggestMode::New + || args.mode == SuggestMode::Publish) + ? tr::lng_suggest_options_date() + : tr::lng_suggest_menu_edit_time()), + .submit = ((args.mode == SuggestMode::Publish) + ? tr::lng_suggest_options_date_publish() + : (args.mode == SuggestMode::New) + ? tr::lng_settings_save() + : tr::lng_suggest_options_update_date()), + .done = done, + .min = [=] { return now + min; }, + .time = value, + .max = [=] { return now + max; }, + }); + + box->addLeftButton((args.mode == SuggestMode::Publish) + ? tr::lng_suggest_options_date_now() + : tr::lng_suggest_options_date_any(), [=] { + done(TimeId()); + }); +} + +void AddApproximateUsd( + not_null<QWidget*> field, + not_null<Main::Session*> session, + rpl::producer<CreditsAmount> price) { + auto value = std::move(price) | rpl::map([=](CreditsAmount amount) { + if (!amount) { + return QString(); + } + const auto appConfig = &session->appConfig(); + const auto rate = amount.ton() + ? appConfig->currencyWithdrawRate() + : (appConfig->starsWithdrawRate() / 100.); + return Info::ChannelEarn::ToUsd(amount, rate, 2); + }); + const auto usd = Ui::CreateChild<Ui::FlatLabel>( + field, + std::move(value), + st::suggestPriceEstimate); + const auto move = [=] { + usd->moveToRight(0, st::suggestPriceEstimateTop); + }; + base::install_event_filter(field, [=](not_null<QEvent*> e) { + if (e->type() == QEvent::Resize) { + move(); + } + return base::EventFilterResult::Continue; + }); + usd->widthValue() | rpl::start_with_next(move, usd->lifetime()); +} + +void ChooseSuggestPriceBox( + not_null<Ui::GenericBox*> box, + SuggestPriceBoxArgs &&args) { + struct Button { + QRect geometry; + Ui::Text::String text; + bool active = false; + }; + struct State { + std::vector<Button> buttons; + rpl::event_stream<> fieldsChanges; + rpl::variable<CreditsAmount> price; + rpl::variable<TimeId> date; + rpl::variable<bool> ton; + Fn<std::optional<CreditsAmount>()> computePrice; + Fn<void()> save; + bool savePending = false; + bool inButton = false; + }; + const auto state = box->lifetime().make_state<State>(); + state->date = args.value.date; + state->ton = (args.value.ton != 0); + state->price = args.value.price(); + const auto updatePrice = [=] { + if (const auto price = state->computePrice()) { + state->price = *price; + } + }; + + const auto starsPrice = [=] { + return rpl::single( + CreditsAmount() + ) | rpl::then(state->price.value( + ) | rpl::filter([=](CreditsAmount amount) { + return amount.stars(); + })); + }; + const auto tonPrice = [=] { + return rpl::single( + CreditsAmount(0, 0, CreditsType::Ton) + ) | rpl::then(state->price.value( + ) | rpl::filter([=](CreditsAmount amount) { + return amount.ton(); + })); + }; + + const auto peer = args.peer; + const auto admin = peer->amMonoforumAdmin(); + const auto broadcast = peer->monoforumBroadcast(); + const auto usePeer = broadcast ? broadcast : peer; + const auto session = &peer->session(); + if (!admin) { + session->credits().load(); + session->credits().tonLoad(); + } + const auto starsMin = session->appConfig().suggestedPostStarsMin(); + const auto starsMax = session->appConfig().suggestedPostStarsMax(); + const auto nanoTonMin = session->appConfig().suggestedPostNanoTonMin(); + const auto nanoTonMax = session->appConfig().suggestedPostNanoTonMax(); + const auto container = box->verticalLayout(); + + box->setStyle(st::suggestPriceBox); + + auto title = (args.mode == SuggestMode::New) + ? tr::lng_suggest_options_title() + : tr::lng_suggest_options_change(); + if (admin) { + box->setTitle(std::move(title)); + } else { + box->setNoContentMargin(true); + + Ui::AddSkip(container, st::boxTitleHeight * 1.1); + box->addRow(object_ptr<Ui::CenterWrap<>>( + box, + object_ptr<Ui::FlatLabel>( + box, + std::move(title), + st::settingsPremiumUserTitle))); + } + + state->buttons.push_back({ + .text = Ui::Text::String( + st::semiboldTextStyle, + (admin + ? tr::lng_suggest_options_stars_request(tr::now) + : tr::lng_suggest_options_stars_offer(tr::now))), + .active = !state->ton.current(), + }); + state->buttons.push_back({ + .text = Ui::Text::String( + st::semiboldTextStyle, + (admin + ? tr::lng_suggest_options_ton_request(tr::now) + : tr::lng_suggest_options_ton_offer(tr::now))), + .active = state->ton.current(), + }); + + auto x = 0; + auto y = st::giftBoxTabsMargin.top(); + const auto padding = st::giftBoxTabPadding; + for (auto &button : state->buttons) { + const auto width = button.text.maxWidth(); + const auto height = st::semiboldTextStyle.font->height; + const auto r = QRect(0, 0, width, height).marginsAdded(padding); + button.geometry = QRect(QPoint(x, y), r.size()); + x += r.width() + st::giftBoxTabSkip; + } + const auto buttonsSkip = admin ? 0 : st::normalFont->height; + const auto buttons = box->addRow( + object_ptr<Ui::RpWidget>(box), + (st::boxRowPadding + - QMargins( + padding.left() / 2, + -buttonsSkip, + padding.right() / 2, + 0))); + const auto height = y + + state->buttons.back().geometry.height() + + st::giftBoxTabsMargin.bottom(); + buttons->resize(buttons->width(), height); + + buttons->setMouseTracking(true); + buttons->events() | rpl::start_with_next([=](not_null<QEvent*> e) { + const auto type = e->type(); + switch (type) { + case QEvent::MouseMove: { + const auto in = [&] { + const auto me = static_cast<QMouseEvent*>(e.get()); + const auto position = me->pos(); + for (const auto &button : state->buttons) { + if (button.geometry.contains(position)) { + return true; + } + } + return false; + }(); + if (state->inButton != in) { + state->inButton = in; + buttons->setCursor(in + ? style::cur_pointer + : style::cur_default); + } + } break; + case QEvent::MouseButtonPress: { + const auto me = static_cast<QMouseEvent*>(e.get()); + if (me->button() != Qt::LeftButton) { + break; + } + const auto position = me->pos(); + for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) { + if (state->buttons[i].geometry.contains(position)) { + state->ton = (i != 0); + state->buttons[i].active = true; + state->buttons[1 - i].active = false; + buttons->update(); + updatePrice(); + break; + } + } + } break; + } + }, buttons->lifetime()); + + buttons->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(buttons); + auto hq = PainterHighQualityEnabler(p); + const auto padding = st::giftBoxTabPadding; + for (const auto &button : state->buttons) { + const auto geometry = button.geometry; + if (button.active) { + p.setBrush(st::giftBoxTabBgActive); + p.setPen(Qt::NoPen); + const auto radius = geometry.height() / 2.; + p.drawRoundedRect(geometry, radius, radius); + p.setPen(st::giftBoxTabFgActive); + } else { + p.setPen(st::giftBoxTabFg); + } + button.text.draw(p, { + .position = geometry.marginsRemoved(padding).topLeft(), + .availableWidth = button.text.maxWidth(), + }); + } + }, buttons->lifetime()); + + Ui::AddSkip(container); + + const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding; + const auto manager = &session->data().customEmojiManager(); + const auto makeIcon = [&]( + not_null<QWidget*> parent, + TextWithEntities text) { + return Ui::CreateChild<Ui::FlatLabel>( + parent, + rpl::single(text), + st::defaultFlatLabel, + st::defaultPopupMenu, + Core::TextContext({ .session = session })); + }; + + const auto starsWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto starsInner = starsWrap->entity(); + + Ui::AddSubsectionTitle( + starsInner, + tr::lng_suggest_options_stars_price(), + QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom())); + + const auto starsFieldWrap = starsInner->add( + object_ptr<Ui::FixedHeightWidget>( + box, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedStarsField = object_ptr<Ui::NumberInput>( + starsFieldWrap, + st::editTagField, + rpl::single(u"0"_q), + ((args.value.exists && args.value.priceWhole && !args.value.ton) + ? QString::number(args.value.priceWhole) + : QString()), + starsMax); + const auto starsField = ownedStarsField.data(); + const auto starsIcon = makeIcon(starsField, manager->creditsEmoji()); + + QObject::connect(starsField, &Ui::NumberInput::changed, updatePrice); + + starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) { + starsIcon->move(st::starsFieldIconPosition); + starsField->move(0, 0); + starsField->resize(width, starsField->height()); + starsFieldWrap->resize(width, starsField->height()); + }, starsFieldWrap->lifetime()); + + AddApproximateUsd(starsField, session, starsPrice()); + + Ui::AddSkip(starsInner); + Ui::AddSkip(starsInner); + const auto computePrice = [peer = args.peer](CreditsAmount amount) { + return PriceAfterCommission(&peer->session(), amount).value(); + }; + const auto formatCommission = [peer = args.peer](CreditsAmount amount) { + return FormatAfterCommissionPercent(&peer->session(), amount); + }; + const auto youGet = [=](rpl::producer<CreditsAmount> price, bool stars) { + return (stars + ? tr::lng_suggest_options_you_get_stars + : tr::lng_suggest_options_you_get_ton)( + lt_count_decimal, + rpl::duplicate(price) | rpl::map(computePrice), + lt_percent, + rpl::duplicate(price) | rpl::map(formatCommission)); + }; + Ui::AddDividerText(starsInner, admin + ? rpl::combine( + youGet(starsPrice(), true), + tr::lng_suggest_options_stars_warning(Ui::Text::RichLangValue) + ) | rpl::map([=](const QString &t1, const TextWithEntities &t2) { + return TextWithEntities{ t1 }.append("\n\n").append(t2); + }) + : tr::lng_suggest_options_stars_price_about(Ui::Text::WithEntities)); + + const auto tonWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto tonInner = tonWrap->entity(); + + Ui::AddSubsectionTitle( + tonInner, + tr::lng_suggest_options_ton_price(), + QMargins(added.left(), 0, added.right(), -st::defaultSubsectionTitlePadding.bottom())); + + const auto tonFieldWrap = tonInner->add( + object_ptr<Ui::FixedHeightWidget>( + box, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedTonField = object_ptr<Ui::InputField>::fromRaw( + Ui::CreateTonAmountInput( + tonFieldWrap, + rpl::single('0' + Ui::TonAmountSeparator() + '0'), + ((args.value.price() && args.value.ton) + ? (int64(args.value.priceWhole) * Ui::kNanosInOne + + int64(args.value.priceNano)) + : 0))); + const auto tonField = ownedTonField.data(); + const auto tonIcon = makeIcon(tonField, Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::currencyFg->c), + st::channelEarnCurrencyCommonMargins, + false))); + + tonField->changes( + ) | rpl::start_with_next(updatePrice, tonField->lifetime()); + + tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) { + tonIcon->move(st::tonFieldIconPosition); + tonField->move(0, 0); + tonField->resize(width, tonField->height()); + tonFieldWrap->resize(width, tonField->height()); + }, tonFieldWrap->lifetime()); + + AddApproximateUsd(tonField, session, tonPrice()); + + Ui::AddSkip(tonInner); + Ui::AddSkip(tonInner); + Ui::AddDividerText( + tonInner, + (admin + ? youGet(tonPrice(), false) + : tr::lng_suggest_options_ton_price_about())); + + tonWrap->toggleOn(state->ton.value(), anim::type::instant); + starsWrap->toggleOn( + state->ton.value() | rpl::map(!rpl::mappers::_1), + anim::type::instant); + + state->computePrice = [=]() -> std::optional<CreditsAmount> { + auto nanos = int64(); + const auto ton = uint32(state->ton.current() ? 1 : 0); + if (ton) { + const auto text = tonField->getLastText(); + const auto now = Ui::ParseTonAmountString(text); + if (now + && *now + && ((*now < nanoTonMin) || (*now > nanoTonMax))) { + tonField->showError(); + return {}; + } + nanos = now.value_or(0); + } else { + const auto now = starsField->getLastText().toLongLong(); + if (now && (now < starsMin || now > starsMax)) { + starsField->showError(); + return {}; + } + nanos = now * Ui::kNanosInOne; + } + return CreditsAmount( + nanos / Ui::kNanosInOne, + nanos % Ui::kNanosInOne, + ton ? CreditsType::Ton : CreditsType::Stars); + }; + + box->setFocusCallback([=] { + if (state->ton.current()) { + tonField->selectAll(); + tonField->setFocusFast(); + } else { + starsField->selectAll(); + starsField->setFocusFast(); + } + }); + + Ui::AddSkip(container); + + const auto time = Settings::AddButtonWithLabel( + container, + tr::lng_suggest_options_date(), + state->date.value() | rpl::map([](TimeId date) { + return date + ? langDateTime(base::unixtime::parse(date)) + : tr::lng_suggest_options_date_any(tr::now); + }), + st::settingsButtonNoIcon); + + time->setClickedCallback([=] { + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto parentWeak = Ui::MakeWeak(box); + const auto done = [=](TimeId result) { + if (parentWeak) { + state->date = result; + } + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ + .session = session, + .done = done, + .value = state->date.current(), + .mode = args.mode, + }); + *weak = dateBox.data(); + box->uiShow()->show(std::move(dateBox)); + }); + + Ui::AddSkip(container); + Ui::AddDividerText(container, tr::lng_suggest_options_date_about()); + + state->save = [=] { + const auto ton = uint32(state->ton.current() ? 1 : 0); + const auto price = state->computePrice(); + if (!price) { + return; + } + const auto value = *price; + const auto credits = &session->credits(); + if (!admin && ton) { + if (!credits->tonLoaded()) { + state->savePending = true; + return; + } else if (credits->tonBalance() < value) { + box->uiShow()->show(Box(InsufficientTonBox, usePeer, value)); + return; + } + } else if (!admin) { + if (!credits->loaded()) { + state->savePending = true; + return; + } + const auto required = peer->starsPerMessageChecked() + + int(base::SafeRound(value.value())); + if (credits->balance() < CreditsAmount(required)) { + using namespace Settings; + const auto done = [=](SmallBalanceResult result) { + if (result == SmallBalanceResult::Success + || result == SmallBalanceResult::Already) { + state->save(); + } + }; + MaybeRequestBalanceIncrease( + Main::MakeSessionShow(box->uiShow(), session), + required, + SmallBalanceForSuggest{ usePeer->id }, + done); + return; + } + } + args.done({ + .exists = true, + .priceWhole = uint32(value.whole()), + .priceNano = uint32(value.nano()), + .ton = ton, + .date = state->date.current(), + }); + }; + + const auto credits = &session->credits(); + rpl::combine( + credits->tonBalanceValue(), + credits->balanceValue() + ) | rpl::filter([=] { + return state->savePending; + }) | rpl::start_with_next([=] { + state->savePending = false; + if (const auto onstack = state->save) { + onstack(); + } + }, box->lifetime()); + + QObject::connect( + starsField, + &Ui::NumberInput::submitted, + box, + state->save); + tonField->submits( + ) | rpl::start_with_next(state->save, tonField->lifetime()); + + const auto button = box->addButton(rpl::single(QString()), state->save); + const auto coloredTonIcon = Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::currencyFg->c), + st::suggestPriceTonIconMargins)); + button->setContext(Core::TextContext({ .session = &peer->session() })); + button->setText(state->price.value( + ) | rpl::map([=](CreditsAmount price) { + if (args.mode == SuggestMode::Change) { + return tr::lng_suggest_options_update( + tr::now, + Ui::Text::WithEntities); + } else if (price.empty()) { + return tr::lng_suggest_options_offer_free( + tr::now, + Ui::Text::WithEntities); + } else if (price.ton()) { + return tr::lng_suggest_options_offer( + tr::now, + lt_amount, + TextWithEntities{ coloredTonIcon }.append( + Lang::FormatCreditsAmountDecimal(price)), + Ui::Text::WithEntities); + } + return tr::lng_suggest_options_offer( + tr::now, + lt_amount, + Ui::Text::IconEmoji(&st::starIconEmoji).append( + Lang::FormatCreditsAmountDecimal(price)), + Ui::Text::WithEntities); + })); + const auto buttonWidth = st::boxWidth + - rect::m::sum::h(st::suggestPriceBox.buttonPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); + + if (admin) { + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + } else { + const auto close = Ui::CreateChild<Ui::IconButton>( + container, + st::boxTitleClose); + close->setClickedCallback([=] { box->closeBox(); }); + container->widthValue() | rpl::start_with_next([=](int) { + close->moveToRight(0, 0); + }, close->lifetime()); + + session->credits().load(true); + session->credits().tonLoad(true); + const auto balance = Settings::AddBalanceWidget( + container, + session, + rpl::conditional( + state->ton.value(), + session->credits().tonBalanceValue(), + session->credits().balanceValue()), + false); + rpl::combine( + balance->sizeValue(), + container->sizeValue() + ) | rpl::start_with_next([=](const QSize &, const QSize &) { + balance->moveToLeft( + st::creditsHistoryRightSkip * 2, + st::creditsHistoryRightSkip); + balance->update(); + }, balance->lifetime()); + } +} + +bool CanEditSuggestedMessage(not_null<HistoryItem*> item) { + const auto media = item->media(); + return !media || media->allowsEditCaption(); +} + +bool CanAddOfferToMessage(not_null<HistoryItem*> item) { + const auto history = item->history(); + const auto broadcast = history->peer->monoforumBroadcast(); + return broadcast + && !history->amMonoforumAdmin() + && !item->Get<HistoryMessageSuggestedPost>() + && !item->groupId() + && item->isRegular() + && !item->isService() + && !item->errorTextForForwardIgnoreRights( + history->owner().history(broadcast)).has_value(); +} + +CreditsAmount PriceAfterCommission( + not_null<Main::Session*> session, + CreditsAmount price) { + const auto appConfig = &session->appConfig(); + const auto mul = price.stars() + ? appConfig->suggestedPostCommissionStars() + : appConfig->suggestedPostCommissionTon(); + const auto exact = price.multiplied(mul / 1000.); + return price.stars() + ? CreditsAmount(exact.whole(), 0, CreditsType::Stars) + : exact; +} + +QString FormatAfterCommissionPercent( + not_null<Main::Session*> session, + CreditsAmount price) { + const auto appConfig = &session->appConfig(); + const auto mul = price.stars() + ? appConfig->suggestedPostCommissionStars() + : appConfig->suggestedPostCommissionTon(); + return QString::number(mul / 10.) + '%'; +} + +void InsufficientTonBox( + not_null<Ui::GenericBox*> box, + not_null<PeerData*> peer, + CreditsAmount required) { + box->setStyle(st::suggestPriceBox); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + auto icon = Settings::CreateLottieIcon( + box->verticalLayout(), + { + .name = u"diamond"_q, + .sizeOverride = Size(st::changePhoneIconSize), + }, + {}); + box->setShowFinishedCallback([animate = std::move(icon.animate)] { + animate(anim::repeat::loop); + }); + box->addRow(std::move(icon.widget), st::lowTonIconPadding); + const auto add = required - peer->session().credits().tonBalance(); + const auto nano = add.whole() * Ui::kNanosInOne + add.nano(); + const auto amount = Ui::FormatTonAmount(nano).full; + box->addRow( + object_ptr<Ui::CenterWrap<Ui::FlatLabel>>( + box, + object_ptr<Ui::FlatLabel>( + box, + tr::lng_suggest_low_ton_title(tr::now, lt_amount, amount), + st::boxTitle)), + st::boxRowPadding + st::lowTonTitlePadding); + const auto label = box->addRow( + object_ptr<Ui::FlatLabel>( + box, + tr::lng_suggest_low_ton_text(Ui::Text::RichLangValue), + st::lowTonText), + st::boxRowPadding + st::lowTonTextPadding); + label->setTryMakeSimilarLines(true); + label->resizeToWidth( + st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right()); + + const auto url = tr::lng_suggest_low_ton_fragment_url(tr::now); + const auto button = box->addButton( + tr::lng_suggest_low_ton_fragment(), + [=] { UrlClickHandler::Open(url); }); + const auto buttonWidth = st::boxWidth + - rect::m::sum::h(st::suggestPriceBox.buttonPadding); + button->widthValue() | rpl::filter([=] { + return (button->widthNoMargins() != buttonWidth); + }) | rpl::start_with_next([=] { + button->resizeToWidth(buttonWidth); + }, button->lifetime()); +} + +SuggestOptions::SuggestOptions( + std::shared_ptr<ChatHelpers::Show> show, + not_null<PeerData*> peer, + SuggestPostOptions values, + SuggestMode mode) +: _show(std::move(show)) +, _peer(peer) +, _mode(mode) +, _values(values) { + updateTexts(); +} + +SuggestOptions::~SuggestOptions() = default; + +void SuggestOptions::paintIcon(QPainter &p, int x, int y, int outerWidth) { + st::historySuggestIconActive.paint( + p, + QPoint(x, y) + st::historySuggestIconPosition, + outerWidth); +} + +void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) { + paintIcon(p, x, y, outerWidth); + paintLines(p, x + st::historyReplySkip, y, outerWidth); +} + +void SuggestOptions::paintLines(QPainter &p, int x, int y, int outerWidth) { + auto available = outerWidth + - x + - st::historyReplyCancel.width + - st::msgReplyPadding.right(); + p.setPen(st::windowActiveTextFg); + _title.draw(p, { + .position = QPoint(x, y + st::msgReplyPadding.top()), + .availableWidth = available, + }); + p.setPen(st::windowSubTextFg); + _text.draw(p, { + .position = QPoint( + x, + y + st::msgReplyPadding.top() + st::msgServiceNameFont->height), + .availableWidth = available, + }); +} + +void SuggestOptions::edit() { + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto apply = [=](SuggestPostOptions values) { + _values = values; + updateTexts(); + _updates.fire({}); + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + *weak = _show->show(Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ + .peer = _peer, + .done = apply, + .value = _values, + .mode = _mode, + })); +} + +void SuggestOptions::updateTexts() { + _title.setText( + st::semiboldTextStyle, + ((_mode == SuggestMode::New) + ? tr::lng_suggest_bar_title(tr::now) + : tr::lng_suggest_options_change(tr::now))); + _text.setMarkedText( + st::defaultTextStyle, + composeText(), + kMarkupTextOptions, + Core::TextContext({ .session = &_peer->session() })); +} + +TextWithEntities SuggestOptions::composeText() const { + const auto manager = &_peer->owner().customEmojiManager(); + const auto top = st::giftBoxByStarsStarTop; + const auto amount = _values.price().ton() + ? Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st::suggestBarTonIconSize, + st::currencyFg->c), + st::suggestBarTonIconMargins, + false)).append( + Lang::FormatCreditsAmountDecimal(_values.price())) + : manager->ministarEmoji({ 0, top, 0, 0 }).append( + Lang::FormatCreditsAmountDecimal(_values.price())); + const auto date = langDateTime(base::unixtime::parse(_values.date)); + if (!_values.price() && !_values.date) { + return tr::lng_suggest_bar_text(tr::now, Ui::Text::WithEntities); + } else if (!_values.date) { + return tr::lng_suggest_bar_priced( + tr::now, + lt_amount, + amount, + Ui::Text::WithEntities); + } else if (!_values.price()) { + return tr::lng_suggest_bar_dated( + tr::now, + lt_date, + TextWithEntities{ date }, + Ui::Text::WithEntities); + } + return TextWithEntities().append( + amount + ).append(" ").append( + QString::fromUtf8("\xf0\x9f\x93\x86 ") + ).append(date); +} + +SuggestPostOptions SuggestOptions::values() const { + auto result = _values; + result.exists = 1; + return result; +} + +rpl::producer<> SuggestOptions::updates() const { + return _updates.events(); +} + +rpl::lifetime &SuggestOptions::lifetime() { + return _lifetime; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h new file mode 100644 index 0000000000..7b270393fd --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h @@ -0,0 +1,113 @@ +/* +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 "api/api_common.h" + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView { + +enum class SuggestMode { + New, + Change, + Publish, +}; + +struct SuggestTimeBoxArgs { + not_null<Main::Session*> session; + Fn<void(TimeId)> done; + TimeId value = 0; + SuggestMode mode = SuggestMode::New; +}; +void ChooseSuggestTimeBox( + not_null<Ui::GenericBox*> box, + SuggestTimeBoxArgs &&args); + +struct SuggestPriceBoxArgs { + not_null<PeerData*> peer; + bool updating = false; + Fn<void(SuggestPostOptions)> done; + SuggestPostOptions value; + SuggestMode mode = SuggestMode::New; +}; +void ChooseSuggestPriceBox( + not_null<Ui::GenericBox*> box, + SuggestPriceBoxArgs &&args); + +[[nodiscard]] bool CanEditSuggestedMessage(not_null<HistoryItem*> item); + +[[nodiscard]] bool CanAddOfferToMessage(not_null<HistoryItem*> item); + +[[nodiscard]] CreditsAmount PriceAfterCommission( + not_null<Main::Session*> session, + CreditsAmount price); +[[nodiscard]] QString FormatAfterCommissionPercent( + not_null<Main::Session*> session, + CreditsAmount price); + +void InsufficientTonBox( + not_null<Ui::GenericBox*> box, + not_null<PeerData*> peer, + CreditsAmount required); + +class SuggestOptions final { +public: + SuggestOptions( + std::shared_ptr<ChatHelpers::Show> show, + not_null<PeerData*> peer, + SuggestPostOptions values, + SuggestMode mode); + ~SuggestOptions(); + + void paintBar(QPainter &p, int x, int y, int outerWidth); + void edit(); + + void paintIcon(QPainter &p, int x, int y, int outerWidth); + void paintLines(QPainter &p, int x, int y, int outerWidth); + + [[nodiscard]] SuggestPostOptions values() const; + + [[nodiscard]] rpl::producer<> updates() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void updateTexts(); + + [[nodiscard]] TextWithEntities composeText() const; + + const std::shared_ptr<ChatHelpers::Show> _show; + const not_null<PeerData*> _peer; + const SuggestMode _mode = SuggestMode::New; + + Ui::Text::String _title; + Ui::Text::String _text; + + SuggestPostOptions _values; + rpl::event_stream<> _updates; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 486bcd984f..e09fc425d5 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_sending.h" #include "apiwrap.h" #include "base/random.h" +#include "ui/effects/premium_stars.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/stickers_lottie.h" #include "core/click_handler_types.h" @@ -18,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "countries/countries_instance.h" #include "data/business/data_business_common.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" @@ -61,6 +63,7 @@ public: enum class Type { PremiumRequired, StarsCharged, + FreeDirect, }; EmptyChatLockedBox(not_null<Element*> parent, Type type); @@ -73,7 +76,7 @@ public: TextWithEntities subtitle() override; int buttonSkip() override; rpl::producer<QString> button() override; - bool buttonMinistars() override; + std::optional<Ui::Premium::MiniStarsType> buttonMinistars() override; void draw( Painter &p, const PaintContext &context, @@ -401,7 +404,9 @@ EmptyChatLockedBox::EmptyChatLockedBox(not_null<Element*> parent, Type type) EmptyChatLockedBox::~EmptyChatLockedBox() = default; int EmptyChatLockedBox::width() { - return st::premiumRequiredWidth; + return (_type == Type::PremiumRequired) + ? st::premiumRequiredWidth + : st::starsPerMessageWidth; } int EmptyChatLockedBox::top() { @@ -421,13 +426,16 @@ int EmptyChatLockedBox::buttonSkip() { } rpl::producer<QString> EmptyChatLockedBox::button() { - return (_type == Type::PremiumRequired) + return (_type == Type::FreeDirect) + ? nullptr + : (_type == Type::PremiumRequired) ? tr::lng_send_non_premium_go() : tr::lng_send_charges_stars_go(); } -bool EmptyChatLockedBox::buttonMinistars() { - return true; +auto EmptyChatLockedBox::buttonMinistars() +-> std::optional<Ui::Premium::MiniStarsType> { + return Ui::Premium::MiniStarsType::SlowStars; } TextWithEntities EmptyChatLockedBox::subtitle() { @@ -456,7 +464,9 @@ void EmptyChatLockedBox::draw( p.setBrush(context.st->msgServiceBg()); // ? p.setPen(Qt::NoPen); p.drawEllipse(geometry); - st::premiumRequiredIcon.paintInCenter(p, geometry); + (_type == Type::PremiumRequired + ? st::premiumRequiredIcon + : st::directMessagesIcon).paintInCenter(p, geometry); } void EmptyChatLockedBox::stickerClearLoopPlayed() { @@ -512,6 +522,9 @@ bool AboutView::refresh() { return true; } const auto user = _history->peer->asUser(); + const auto monoforum = _history->peer->isMonoforum() + ? _history->peer->asChannel() + : nullptr; const auto info = user ? user->botInfo.get() : nullptr; if (!info) { if (user @@ -539,6 +552,14 @@ bool AboutView::refresh() { makeIntro(user); } return true; + } else if (monoforum && _history->isDisplayedEmpty()) { + if (_item) { + return false; + } + setItem( + makeStarsPerMessage(monoforum->starsPerMessageChecked()), + nullptr); + return true; } if (_item) { setItem({}, nullptr); @@ -600,6 +621,8 @@ void AboutView::make(Data::ChatIntro data, bool preview) { const auto sendIntroSticker = [=](not_null<DocumentData*> sticker) { _sendIntroSticker.fire_copy(sticker); }; + owned->data()->setCustomServiceLink( + std::make_shared<LambdaClickHandler>(handler)); owned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>( owned.get(), GenerateChatIntro( @@ -610,7 +633,6 @@ void AboutView::make(Data::ChatIntro data, bool preview) { sendIntroSticker), HistoryView::MediaGenericDescriptor{ .maxWidth = st::chatIntroWidth, - .serviceLink = std::make_shared<LambdaClickHandler>(handler), .service = true, .hideServiceText = preview || text.isEmpty(), })); @@ -813,28 +835,46 @@ AdminLog::OwnedItem AboutView::makePremiumRequired() { } AdminLog::OwnedItem AboutView::makeStarsPerMessage(int stars) { + auto name = Ui::Text::Bold(_history->peer->shortName()); + auto cost = Ui::Text::IconEmoji( + &st::starIconEmoji + ).append(Ui::Text::Bold(Lang::FormatCountDecimal(stars))); const auto item = _history->makeMessage({ .id = _history->nextNonHistoryEntryId(), .flags = (MessageFlag::FakeAboutView | MessageFlag::FakeHistoryItem | MessageFlag::Local), .from = _history->peer->id, - }, PreparedServiceText{ tr::lng_send_charges_stars_text( - tr::now, - lt_user, - Ui::Text::Bold(_history->peer->shortName()), - lt_amount, - Ui::Text::IconEmoji( - &st::starIconEmoji - ).append(Ui::Text::Bold(Lang::FormatCountDecimal(stars))), - Ui::Text::RichLangValue), + }, PreparedServiceText{ !_history->peer->isMonoforum() + ? tr::lng_send_charges_stars_text( + tr::now, + lt_user, + std::move(name), + lt_amount, + std::move(cost), + Ui::Text::RichLangValue) + : stars + ? tr::lng_send_charges_stars_channel( + tr::now, + lt_channel, + std::move(name), + lt_amount, + std::move(cost), + Ui::Text::RichLangValue) + : tr::lng_send_free_channel( + tr::now, + lt_channel, + std::move(name), + Ui::Text::RichLangValue), }); auto result = AdminLog::OwnedItem(_delegate, item); result->overrideMedia(std::make_unique<ServiceBox>( result.get(), std::make_unique<EmptyChatLockedBox>( result.get(), - EmptyChatLockedBox::Type::StarsCharged))); + (stars + ? EmptyChatLockedBox::Type::StarsCharged + : EmptyChatLockedBox::Type::FreeDirect)))); return result; } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp index 6b62e06a93..5e9d3eecdd 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -366,8 +366,9 @@ void Item::setupTop() { ? nullptr : Ui::CreateChild<Ui::UserpicButton>( _top.get(), - _thread->peer(), - st::previewUserpic); + _thread->peer()->userpicPaintingPeer(), + st::previewUserpic, + _thread->peer()->userpicShape()); if (userpic) { userpic->showSavedMessagesOnSelf(true); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp similarity index 68% rename from Telegram/SourceFiles/history/view/history_view_replies_section.cpp rename to Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 8502aff92a..12dff183d2 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -5,7 +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 */ -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "history/view/controls/history_view_compose_controls.h" #include "history/view/controls/history_view_compose_search.h" @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_contact_status.h" #include "history/view/history_view_scheduled_section.h" #include "history/view/history_view_service_message.h" +#include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_pinned_tracker.h" #include "history/view/history_view_pinned_section.h" #include "history/view/history_view_translate_bar.h" @@ -46,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/delete_messages_box.h" #include "boxes/send_files_box.h" #include "boxes/premium_limits_box.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "base/call_delayed.h" @@ -57,6 +59,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "data/components/scheduled_messages.h" +#include "data/data_histories.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_chat.h" @@ -115,32 +120,34 @@ rpl::producer<Ui::MessageBarContent> RootViewContent( } // namespace -RepliesMemento::RepliesMemento( - not_null<History*> history, - MsgId rootId, +ChatMemento::ChatMemento( + ChatViewId id, MsgId highlightId, const TextWithEntities &highlightPart, int highlightPartOffsetHint) -: _history(history) -, _rootId(rootId) +: _id(id) , _highlightPart(highlightPart) , _highlightPartOffsetHint(highlightPartOffsetHint) , _highlightId(highlightId) { - if (highlightId) { + if (highlightId || _id.sublist) { _list.setAroundPosition({ - .fullId = FullMsgId(_history->peer->id, highlightId), + .fullId = FullMsgId(_id.history->peer->id, highlightId), .date = TimeId(0), }); } } -RepliesMemento::RepliesMemento( +ChatMemento::ChatMemento( + Comments, not_null<HistoryItem*> commentsItem, MsgId commentId) -: RepliesMemento(commentsItem->history(), commentsItem->id, commentId) { +: ChatMemento({ + .history = commentsItem->history(), + .repliesRootId = commentsItem->id, +}, commentId) { } -void RepliesMemento::setFromTopic(not_null<Data::ForumTopic*> topic) { +void ChatMemento::setFromTopic(not_null<Data::ForumTopic*> topic) { _replies = topic->replies(); if (!_list.aroundPosition()) { _list = *topic->listMemento(); @@ -148,31 +155,39 @@ void RepliesMemento::setFromTopic(not_null<Data::ForumTopic*> topic) { } -Data::ForumTopic *RepliesMemento::topicForRemoveRequests() const { - return _history->peer->forumTopicFor(_rootId); +Data::ForumTopic *ChatMemento::topicForRemoveRequests() const { + return _id.repliesRootId + ? _id.history->peer->forumTopicFor(_id.repliesRootId) + : nullptr; } -void RepliesMemento::setReadInformation( +Data::SavedSublist *ChatMemento::sublistForRemoveRequests() const { + return _id.sublist; +} + +void ChatMemento::setReadInformation( MsgId inboxReadTillId, int unreadCount, MsgId outboxReadTillId) { - if (!_replies) { - if (const auto forum = _history->asForum()) { - if (const auto topic = forum->topicFor(_rootId)) { + if (!_id.repliesRootId) { + return; + } else if (!_replies) { + if (const auto forum = _id.history->asForum()) { + if (const auto topic = forum->topicFor(_id.repliesRootId)) { _replies = topic->replies(); } } if (!_replies) { _replies = std::make_shared<Data::RepliesList>( - _history, - _rootId); + _id.history, + _id.repliesRootId); } } _replies->setInboxReadTill(inboxReadTillId, unreadCount); _replies->setOutboxReadTill(outboxReadTillId); } -object_ptr<Window::SectionWidget> RepliesMemento::createWidget( +object_ptr<Window::SectionWidget> ChatMemento::createWidget( QWidget *parent, not_null<Window::SessionController*> controller, Window::Column column, @@ -187,43 +202,56 @@ object_ptr<Window::SectionWidget> RepliesMemento::createWidget( _list.setScrollTopState(ListMemento::ScrollTopState{ Data::MinMessagePosition }); + } else if (!_list.aroundPosition().fullId + && _id.sublist + && _id.sublist->computeInboxReadTillFull() == MsgId(1)) { + _list.setAroundPosition(Data::MinMessagePosition); + _list.setScrollTopState(ListMemento::ScrollTopState{ + Data::MinMessagePosition + }); } - auto result = object_ptr<RepliesWidget>( - parent, - controller, - _history, - _rootId); + auto result = object_ptr<ChatWidget>(parent, controller, _id); result->setInternalState(geometry, this); return result; } -void RepliesMemento::setupTopicViewer() { - _history->owner().itemIdChanged( - ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { - if (_rootId == change.oldId) { - _rootId = change.newId.msg; - _replies = nullptr; - } - }, _lifetime); +void ChatMemento::setupTopicViewer() { + if (_id.repliesRootId) { + _id.history->owner().itemIdChanged( + ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { + if (_id.repliesRootId == change.oldId) { + _id.repliesRootId = change.newId.msg; + _replies = nullptr; + } + }, _lifetime); + } } -RepliesWidget::RepliesWidget( +ChatWidget::ChatWidget( QWidget *parent, not_null<Window::SessionController*> controller, - not_null<History*> history, - MsgId rootId) -: Window::SectionWidget(parent, controller, history->peer) + ChatViewId id) +: Window::SectionWidget(parent, controller, id.history->peer) , WindowListDelegate(controller) -, _history(history) -, _rootId(rootId) -, _root(lookupRoot()) +, _history(id.history) +, _peer(_history->peer) +, _id(id) +, _repliesRootId(_id.repliesRootId) +, _repliesRoot(lookupRepliesRoot()) , _topic(lookupTopic()) , _areComments(computeAreComments()) -, _sendAction(history->owner().sendActionManager().repliesPainter( - history, - rootId)) +, _sublist(_id.sublist) +, _monoforumPeerId((_sublist && _sublist->parentChat()) + ? _sublist->sublistPeer()->id + : PeerId()) +, _sendAction(_repliesRootId + ? _history->owner().sendActionManager().repliesPainter( + _history, + _repliesRootId) + : nullptr) , _topBar(this, controller) , _topBarShadow(this) +, _topBars(std::make_unique<Ui::RpWidget>(this)) , _composeControls(std::make_unique<ComposeControls>( this, ComposeControlsDescriptor{ @@ -243,7 +271,8 @@ RepliesWidget::RepliesWidget( }) | rpl::type_erased() : rpl::single(false), })) -, _translateBar(std::make_unique<TranslateBar>(this, controller, history)) +, _translateBar( + std::make_unique<TranslateBar>(_topBars.get(), controller, _history)) , _scroll(std::make_unique<Ui::ScrollArea>( this, controller->chatStyle()->value(lifetime(), st::historyScroll), @@ -259,7 +288,7 @@ RepliesWidget::RepliesWidget( Window::ChatThemeValueFromPeer( controller, - history->peer + _peer ) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) { _theme = std::move(theme); controller->setChatStyleTheme(_theme); @@ -267,10 +296,12 @@ RepliesWidget::RepliesWidget( setupRoot(); setupRootView(); + setupOpenChatButton(); + setupAboutHiddenAuthor(); setupShortcuts(); setupTranslateBar(); - _history->peer->updateFull(); + _peer->updateFull(); refreshTopBarActiveChat(); @@ -278,8 +309,8 @@ RepliesWidget::RepliesWidget( _topBar->resizeToWidth(width()); _topBar->show(); - if (_rootView) { - _rootView->move(0, _topBar->height()); + if (_repliesRootView) { + _repliesRootView->move(0, 0); } _topBar->deleteSelectionRequest( @@ -300,9 +331,7 @@ RepliesWidget::RepliesWidget( }, _topBar->lifetime()); _topBar->searchRequest( ) | rpl::start_with_next([=] { - if (!preventsClose(crl::guard(this, [=]{ searchInTopic(); }))) { - searchInTopic(); - } + searchRequested(); }, _topBar->lifetime()); controller->adaptive().value( @@ -331,6 +360,8 @@ RepliesWidget::RepliesWidget( _composeControls->editMessage( fullId, _inner->getSelectedTextRange(item)); + } else if (media->todolist()) { + Window::PeerMenuEditTodoList(controller, item); } } }, _inner->lifetime()); @@ -339,7 +370,7 @@ RepliesWidget::RepliesWidget( ) | rpl::start_with_next([=](ListWidget::ReplyToMessageRequest request) { const auto canSendReply = _topic ? Data::CanSendAnything(_topic) - : Data::CanSendAnything(_history->peer); + : Data::CanSendAnything(_peer); const auto &to = request.to; const auto still = _history->owner().message(to.messageId); const auto allowInAnotherChat = still && still->allowsForward(); @@ -364,16 +395,18 @@ RepliesWidget::RepliesWidget( _composeControls->sendActionUpdates( ) | rpl::start_with_next([=](ComposeControls::SendActionUpdate &&data) { - if (!data.cancel) { + if (!_repliesRootId) { + return; + } else if (!data.cancel) { session().sendProgressManager().update( _history, - _rootId, + _repliesRootId, data.type, data.progress); } else { session().sendProgressManager().cancel( _history, - _rootId, + _repliesRootId, data.type); } }, lifetime()); @@ -381,8 +414,8 @@ RepliesWidget::RepliesWidget( _history->session().changes().messageUpdates( Data::MessageUpdate::Flag::Destroyed ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { - if (update.item == _root) { - _root = nullptr; + if (update.item == _repliesRoot) { + _repliesRoot = nullptr; updatePinnedVisibility(); if (!_topic) { controller->showBackFromStack(); @@ -390,7 +423,9 @@ RepliesWidget::RepliesWidget( } }, lifetime()); - if (!_topic) { + if (_sublist) { + subscribeToSublist(); + } else if (!_topic) { _history->session().changes().historyUpdates( _history, Data::HistoryUpdate::Flag::OutboxRead @@ -423,10 +458,18 @@ RepliesWidget::RepliesWidget( } } -RepliesWidget::~RepliesWidget() { +ChatWidget::~ChatWidget() { base::take(_sendAction); - session().api().saveCurrentDraftToCloud(); - controller()->sendingAnimation().clear(); + if (_repliesRootId || _sublist) { + session().api().saveCurrentDraftToCloud(); + } + if (_repliesRootId) { + controller()->sendingAnimation().clear(); + } + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + controller()->saveSubsectionTabs(base::take(_subsectionTabs)); + } if (_topic) { if (_topic->creating()) { _emptyPainter = nullptr; @@ -436,104 +479,111 @@ RepliesWidget::~RepliesWidget() { _inner->saveState(_topic->listMemento()); } } - _history->owner().sendActionManager().repliesPainterRemoved( - _history, - _rootId); + if (_repliesRootId) { + _history->owner().sendActionManager().repliesPainterRemoved( + _history, + _repliesRootId); + } } -void RepliesWidget::orderWidgets() { +void ChatWidget::orderWidgets() { + _topBars->raise(); _translateBar->raise(); if (_topicReopenBar) { _topicReopenBar->bar().raise(); } - if (_rootView) { - _rootView->raise(); + if (_repliesRootView) { + _repliesRootView->raise(); } if (_pinnedBar) { _pinnedBar->raise(); } - if (_topBar) { - _topBar->raise(); + if (_subsectionTabs) { + _subsectionTabs->raise(); } + _topBar->raise(); _topBarShadow->raise(); _composeControls->raisePanels(); } -void RepliesWidget::setupRoot() { - if (!_root) { +void ChatWidget::setupRoot() { + if (_repliesRootId && !_repliesRoot) { const auto done = crl::guard(this, [=] { - _root = lookupRoot(); - if (_root) { + _repliesRoot = lookupRepliesRoot(); + if (_repliesRoot) { _areComments = computeAreComments(); _inner->update(); } updatePinnedVisibility(); }); _history->session().api().requestMessageData( - _history->peer, - _rootId, + _peer, + _repliesRootId, done); } } -void RepliesWidget::setupRootView() { - if (_topic) { +void ChatWidget::setupRootView() { + if (_topic || !_repliesRootId) { return; } - _rootView = std::make_unique<Ui::PinnedBar>(this, [=] { + _repliesRootView = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); - _rootView->setContent(rpl::combine( + _repliesRootView->setContent(rpl::combine( RootViewContent( _history, - _rootId, - [bar = _rootView.get()] { bar->customEmojiRepaint(); }), - _rootVisible.value() + _repliesRootId, + [bar = _repliesRootView.get()] { bar->customEmojiRepaint(); }), + _repliesRootVisible.value() ) | rpl::map([=](Ui::MessageBarContent &&content, bool show) { const auto shown = !content.title.isEmpty() && !content.text.empty(); _shownPinnedItem = shown - ? _history->owner().message(_history->peer->id, _rootId) + ? _history->owner().message(_peer->id, _repliesRootId) : nullptr; return show ? std::move(content) : Ui::MessageBarContent(); })); controller()->adaptive().oneColumnValue( ) | rpl::start_with_next([=](bool one) { - _rootView->setShadowGeometryPostprocess([=](QRect geometry) { + _repliesRootView->setShadowGeometryPostprocess([=](QRect geometry) { if (!one) { geometry.setLeft(geometry.left() + st::lineWidth); } return geometry; }); - }, _rootView->lifetime()); + }, _repliesRootView->lifetime()); - _rootView->barClicks( + _repliesRootView->barClicks( ) | rpl::start_with_next([=] { showAtStart(); }, lifetime()); - _rootViewHeight = 0; - _rootView->heightValue( + _repliesRootViewHeight = 0; + _repliesRootView->heightValue( ) | rpl::start_with_next([=](int height) { - if (const auto delta = height - _rootViewHeight) { - _rootViewHeight = height; + if (const auto delta = height - _repliesRootViewHeight) { + _repliesRootViewHeight = height; setGeometryWithTopMoved(geometry(), delta); } - }, _rootView->lifetime()); + }, _repliesRootView->lifetime()); } -void RepliesWidget::setupTopicViewer() { +void ChatWidget::setupTopicViewer() { + if (!_repliesRootId) { + return; + } const auto owner = &_history->owner(); owner->itemIdChanged( ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { - if (_rootId == change.oldId) { - _rootId = change.newId.msg; - _composeControls->updateTopicRootId(_rootId); + if (_repliesRootId == change.oldId) { + _repliesRootId = _id.repliesRootId = change.newId.msg; + _composeControls->updateTopicRootId(_repliesRootId); _sendAction = owner->sendActionManager().repliesPainter( _history, - _rootId); - _root = lookupRoot(); + _repliesRootId); + _repliesRoot = lookupRepliesRoot(); if (_topic && _topic->rootId() == change.oldId) { setTopic(_topic->forum()->topicFor(change.newId.msg)); } else { @@ -552,10 +602,12 @@ void RepliesWidget::setupTopicViewer() { } } -void RepliesWidget::subscribeToTopic() { +void ChatWidget::subscribeToTopic() { Expects(_topic != nullptr); - _topicReopenBar = std::make_unique<TopicReopenBar>(this, _topic); + _topicReopenBar = std::make_unique<TopicReopenBar>( + _topBars.get(), + _topic); _topicReopenBar->bar().setVisible(!animatingShow()); _topicReopenBarHeight = _topicReopenBar->bar().height(); _topicReopenBar->bar().heightValue( @@ -586,9 +638,7 @@ void RepliesWidget::subscribeToTopic() { _topic->destroyed( ) | rpl::start_with_next([=] { - controller()->showBackFromStack(Window::SectionShow( - anim::type::normal, - anim::activation::background)); + closeCurrent(); }, _topicLifetime); if (!_topic->creating()) { @@ -602,14 +652,26 @@ void RepliesWidget::subscribeToTopic() { _cornerButtons.updateUnreadThingsVisibility(); } -void RepliesWidget::subscribeToPinnedMessages() { +void ChatWidget::closeCurrent() { + const auto thread = controller()->windowId().chat(); + if ((_sublist && thread == _sublist) || (_topic && thread == _topic)) { + controller()->window().close(); + } else { + controller()->showBackFromStack(Window::SectionShow( + anim::type::normal, + anim::activation::background)); + } +} + +void ChatWidget::subscribeToPinnedMessages() { using EntryUpdateFlag = Data::EntryUpdate::Flag; session().changes().entryUpdates( EntryUpdateFlag::HasPinnedMessages ) | rpl::start_with_next([=](const Data::EntryUpdate &update) { if (_pinnedTracker && (update.flags & EntryUpdateFlag::HasPinnedMessages) - && (_topic == update.entry.get())) { + && (_topic == update.entry.get() + || _sublist == update.entry.get())) { checkPinnedBarState(); } }, lifetime()); @@ -617,7 +679,7 @@ void RepliesWidget::subscribeToPinnedMessages() { setupPinnedTracker(); } -void RepliesWidget::setTopic(Data::ForumTopic *topic) { +void ChatWidget::setTopic(Data::ForumTopic *topic) { if (_topic == topic) { return; } @@ -625,11 +687,12 @@ void RepliesWidget::setTopic(Data::ForumTopic *topic) { _topic = topic; refreshReplies(); refreshTopBarActiveChat(); + validateSubsectionTabs(); if (_topic) { - if (_rootView) { + if (_repliesRootView) { _shownPinnedItem = nullptr; - _rootView = nullptr; - _rootViewHeight = 0; + _repliesRootView = nullptr; + _repliesRootViewHeight = 0; } subscribeToTopic(); } @@ -640,18 +703,22 @@ void RepliesWidget::setTopic(Data::ForumTopic *topic) { } } -HistoryItem *RepliesWidget::lookupRoot() const { - return _history->owner().message(_history->peer, _rootId); +HistoryItem *ChatWidget::lookupRepliesRoot() const { + return _repliesRootId + ? _history->owner().message(_peer, _repliesRootId) + : nullptr; } -Data::ForumTopic *RepliesWidget::lookupTopic() { - if (const auto forum = _history->asForum()) { - if (const auto result = forum->topicFor(_rootId)) { +Data::ForumTopic *ChatWidget::lookupTopic() { + if (!_repliesRootId) { + return nullptr; + } else if (const auto forum = _history->asForum()) { + if (const auto result = forum->topicFor(_repliesRootId)) { return result; } else { - forum->requestTopic(_rootId, crl::guard(this, [=] { + forum->requestTopic(_repliesRootId, crl::guard(this, [=] { if (const auto forum = _history->asForum()) { - setTopic(forum->topicFor(_rootId)); + setTopic(forum->topicFor(_repliesRootId)); } })); } @@ -659,21 +726,21 @@ Data::ForumTopic *RepliesWidget::lookupTopic() { return nullptr; } -bool RepliesWidget::computeAreComments() const { - return _root && _root->isDiscussionPost(); +bool ChatWidget::computeAreComments() const { + return _repliesRoot && _repliesRoot->isDiscussionPost(); } -void RepliesWidget::setupComposeControls() { +void ChatWidget::setupComposeControls() { auto topicWriteRestrictions = rpl::single( ) | rpl::then(session().changes().topicUpdates( Data::TopicUpdate::Flag::Closed ) | rpl::filter([=](const Data::TopicUpdate &update) { return (update.topic->history() == _history) - && (update.topic->rootId() == _rootId); + && (update.topic->rootId() == _repliesRootId); }) | rpl::to_empty) | rpl::map([=] { const auto topic = _topic ? _topic - : _history->peer->forumTopicFor(_rootId); + : _peer->forumTopicFor(_repliesRootId); return (!topic || topic->canToggleClosed() || !topic->closed()) ? Data::SendError() : tr::lng_forum_topic_closed(tr::now); @@ -681,10 +748,12 @@ void RepliesWidget::setupComposeControls() { auto writeRestriction = rpl::combine( session().frozenValue(), session().changes().peerFlagsValue( - _history->peer, + _peer, Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer), - std::move(topicWriteRestrictions) + Data::CanSendAnythingValue(_peer), + (_repliesRootId + ? std::move(topicWriteRestrictions) + : (rpl::single(Data::SendError()) | rpl::type_erased())) ) | rpl::map([=]( const Main::FreezeInfo &info, auto, @@ -699,9 +768,9 @@ void RepliesWidget::setupComposeControls() { & ~ChatRestriction::SendPolls; const auto canSendAnything = _topic ? Data::CanSendAnyOf(_topic, allWithoutPolls) - : Data::CanSendAnyOf(_history->peer, allWithoutPolls); + : Data::CanSendAnyOf(_peer, allWithoutPolls); const auto restriction = Data::RestrictionError( - _history->peer, + _peer, ChatRestriction::SendOther); auto text = !canSendAnything ? (restriction @@ -721,11 +790,12 @@ void RepliesWidget::setupComposeControls() { _composeControls->setHistory({ .history = _history.get(), - .topicRootId = _topic ? _topic->rootId() : MsgId(0), + .topicRootId = _topic ? _topic->rootId() : MsgId(), + .monoforumPeerId = _monoforumPeerId, .showSlowmodeError = [=] { return showSlowmodeError(); }, .sendActionFactory = [=] { return prepareSendAction({}); }, - .slowmodeSecondsLeft = SlowmodeSecondsLeft(_history->peer), - .sendDisabledBySlowmode = SendDisabledBySlowmode(_history->peer), + .slowmodeSecondsLeft = SlowmodeSecondsLeft(_peer), + .sendDisabledBySlowmode = SendDisabledBySlowmode(_peer), .writeRestriction = std::move(writeRestriction), }); @@ -880,7 +950,7 @@ void RepliesWidget::setupComposeControls() { _composeControls->finishAnimating(); - if (const auto channel = _history->peer->asChannel()) { + if (const auto channel = _peer->asChannel()) { channel->updateFull(); if (!channel->isBroadcast()) { rpl::combine( @@ -895,11 +965,11 @@ void RepliesWidget::setupComposeControls() { } } -void RepliesWidget::setupSwipeReplyAndBack() { +void ChatWidget::setupSwipeReplyAndBack() { const auto can = [=](not_null<HistoryItem*> still) { const auto canSendReply = _topic ? Data::CanSendAnything(_topic) - : Data::CanSendAnything(_history->peer); + : Data::CanSendAnything(_peer); const auto allowInAnotherChat = still && still->allowsForward(); if (allowInAnotherChat && (_joinGroup || !canSendReply)) { return true; @@ -934,8 +1004,8 @@ void RepliesWidget::setupSwipeReplyAndBack() { || (_gestureHorizontal.reachRatio != data.reachRatio); if (changed) { _gestureHorizontal = data; - const auto item = _history->peer->owner().message( - _history->peer->id, + const auto item = _peer->owner().message( + _peer->id, MsgId{ data.msgBareId }); if (item) { _history->owner().requestItemRepaint(item); @@ -993,11 +1063,11 @@ void RepliesWidget::setupSwipeReplyAndBack() { }); } -void RepliesWidget::chooseAttach( +void ChatWidget::chooseAttach( std::optional<bool> overrideSendImagesAsPhotos) { _choosingAttach = false; - if (const auto error = Data::AnyFileRestrictionError(_history->peer)) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + if (const auto error = Data::AnyFileRestrictionError(_peer)) { + Data::ShowSendErrorToast(controller(), _peer, error); return; } else if (showSlowmodeError()) { return; @@ -1036,7 +1106,7 @@ void RepliesWidget::chooseAttach( }), nullptr); } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( not_null<const QMimeData*> data, std::optional<bool> overrideSendImagesAsPhotos, const QString &insertTextOnCancel) { @@ -1070,7 +1140,7 @@ bool RepliesWidget::confirmSendingFiles( return false; } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel) { if (_composeControls->confirmMediaEdit(list)) { @@ -1083,7 +1153,7 @@ bool RepliesWidget::confirmSendingFiles( controller(), std::move(list), _composeControls->getTextWithAppliedMarkdown(), - _history->peer, + _peer, Api::SendType::Normal, sendMenuDetails(), [=](const TextWithTags &text) { _composeControls->setText(text); }); @@ -1110,7 +1180,7 @@ bool RepliesWidget::confirmSendingFiles( return true; } -void RepliesWidget::sendingFilesConfirmed( +void ChatWidget::sendingFilesConfirmed( Ui::PreparedList &&list, Ui::SendFilesWay way, TextWithTags &&caption, @@ -1124,7 +1194,7 @@ void RepliesWidget::sendingFilesConfirmed( auto groups = DivideByGroups( std::move(list), way, - _history->peer->slowmodeApplied()); + _peer->slowmodeApplied()); auto bundle = PrepareFilesBundle( std::move(groups), way, @@ -1133,19 +1203,19 @@ void RepliesWidget::sendingFilesConfirmed( sendingFilesConfirmed(std::move(bundle), options); } -bool RepliesWidget::checkSendPayment( +bool ChatWidget::checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn<void(int)> withPaymentApproved) { return _sendPayment.check( controller(), - _history->peer, + _peer, + options, messagesCount, - starsApproved, std::move(withPaymentApproved)); } -void RepliesWidget::sendingFilesConfirmed( +void ChatWidget::sendingFilesConfirmed( std::shared_ptr<Ui::PreparedBundle> bundle, Api::SendOptions options) { const auto withPaymentApproved = [=](int approved) { @@ -1155,7 +1225,7 @@ void RepliesWidget::sendingFilesConfirmed( }; const auto checked = checkSendPayment( bundle->totalCount, - options.starsApproved, + options, withPaymentApproved); if (!checked) { return; @@ -1189,7 +1259,7 @@ void RepliesWidget::sendingFilesConfirmed( finishSending(); } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( QImage &&image, QByteArray &&content, std::optional<bool> overrideSendImagesAsPhotos, @@ -1206,14 +1276,14 @@ bool RepliesWidget::confirmSendingFiles( return confirmSendingFiles(std::move(list), insertTextOnCancel); } -bool RepliesWidget::showSlowmodeError() { +bool ChatWidget::showSlowmodeError() { const auto text = [&] { - if (const auto left = _history->peer->slowmodeSecondsLeft()) { + if (const auto left = _peer->slowmodeSecondsLeft()) { return tr::lng_slowmode_enabled( tr::now, lt_left, Ui::FormatDurationWordsSlowmode(left)); - } else if (_history->peer->slowmodeApplied()) { + } else if (_peer->slowmodeApplied()) { if (const auto item = _history->latestSendingMessage()) { showAtPosition(item->position()); return tr::lng_slowmode_no_many(tr::now); @@ -1228,13 +1298,15 @@ bool RepliesWidget::showSlowmodeError() { return true; } -void RepliesWidget::pushReplyReturn(not_null<HistoryItem*> item) { - if (item->history() == _history && item->inThread(_rootId)) { - _cornerButtons.pushReplyReturn(item); +void ChatWidget::pushReplyReturn(not_null<HistoryItem*> item) { + if (_repliesRootId) { + if (item->history() == _history && item->inThread(_repliesRootId)) { + _cornerButtons.pushReplyReturn(item); + } } } -void RepliesWidget::checkReplyReturns() { +void ChatWidget::checkReplyReturns() { const auto currentTop = _scroll->scrollTop(); while (const auto replyReturn = _cornerButtons.replyReturn()) { const auto position = replyReturn->position(); @@ -1250,26 +1322,26 @@ void RepliesWidget::checkReplyReturns() { } } -void RepliesWidget::uploadFile( +void ChatWidget::uploadFile( const QByteArray &fileContent, SendMediaType type) { session().api().sendFile(fileContent, type, prepareSendAction({})); } -bool RepliesWidget::showSendingFilesError( +bool ChatWidget::showSendingFilesError( const Ui::PreparedList &list) const { return showSendingFilesError(list, std::nullopt); } -bool RepliesWidget::showSendingFilesError( +bool ChatWidget::showSendingFilesError( const Ui::PreparedList &list, std::optional<bool> compress) const { const auto error = [&]() -> Data::SendError { - const auto peer = _history->peer; + const auto peer = _peer; const auto error = Data::FileRestrictionError(peer, list, compress); if (error) { return error; - } else if (const auto left = _history->peer->slowmodeSecondsLeft()) { + } else if (const auto left = _peer->slowmodeSecondsLeft()) { return tr::lng_slowmode_enabled( tr::now, lt_left, @@ -1297,11 +1369,11 @@ bool RepliesWidget::showSendingFilesError( return true; } - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return true; } -Api::SendAction RepliesWidget::prepareSendAction( +Api::SendAction ChatWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); @@ -1309,14 +1381,14 @@ Api::SendAction RepliesWidget::prepareSendAction( return result; } -void RepliesWidget::send() { +void ChatWidget::send() { if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) { return; } send({}); } -void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) { +void ChatWidget::sendVoice(const ComposeControls::VoiceToSend &data) { const auto withPaymentApproved = [=](int approved) { auto copy = data; copy.options.starsApproved = approved; @@ -1324,7 +1396,7 @@ void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) { }; const auto checked = checkSendPayment( 1, - data.options.starsApproved, + data.options, withPaymentApproved); if (!checked) { return; @@ -1343,7 +1415,7 @@ void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) { finishSending(); } -void RepliesWidget::send(Api::SendOptions options) { +void ChatWidget::send(Api::SendOptions options) { if (!options.scheduled && showSlowmodeError()) { return; } @@ -1363,9 +1435,9 @@ void RepliesWidget::send(Api::SendOptions options) { .ignoreSlowmodeCountdown = (options.scheduled != 0), }; request.messagesCount = ComputeSendingMessagesCount(_history, request); - const auto error = GetErrorForSending(_history->peer, request); + const auto error = GetErrorForSending(_peer, request); if (error) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return; } if (!options.scheduled) { @@ -1376,7 +1448,7 @@ void RepliesWidget::send(Api::SendOptions options) { }; const auto checked = checkSendPayment( request.messagesCount, - options.starsApproved, + options, withPaymentApproved); if (!checked) { return; @@ -1385,11 +1457,13 @@ void RepliesWidget::send(Api::SendOptions options) { session().api().sendMessage(std::move(message)); _composeControls->clear(); - session().sendProgressManager().update( - _history, - _rootId, - Api::SendProgressType::Typing, - -1); + if (_repliesRootId) { + session().sendProgressManager().update( + _history, + _repliesRootId, + Api::SendProgressType::Typing, + -1); + } //_saveDraftText = true; //_saveDraftStart = crl::now(); @@ -1398,7 +1472,7 @@ void RepliesWidget::send(Api::SendOptions options) { finishSending(); } -void RepliesWidget::edit( +void ChatWidget::edit( not_null<HistoryItem*> item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, @@ -1477,7 +1551,68 @@ void RepliesWidget::edit( doSetInnerFocus(); } -void RepliesWidget::refreshJoinGroupButton() { +void ChatWidget::validateSubsectionTabs() { + if (!_subsectionCheckLifetime && _history->peer->isMegagroup()) { + _subsectionCheckLifetime = _history->peer->asChannel()->flagsValue( + ) | rpl::skip( + 1 + ) | rpl::filter([=](Data::Flags<ChannelDataFlags>::Change change) { + const auto mask = ChannelDataFlag::Forum + | ChannelDataFlag::ForumTabs + | ChannelDataFlag::MonoforumAdmin; + return change.diff & mask; + }) | rpl::start_with_next([=] { + validateSubsectionTabs(); + }); + } + const auto thread = _topic ? (Data::Thread*)_topic : _sublist; + if (!thread || !HistoryView::SubsectionTabs::UsedFor(_history)) { + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + updateControlsGeometry(); + if (const auto forum = _history->asForum()) { + controller()->showForum(forum, { + Window::SectionShow::Way::Backward, + anim::type::normal, + anim::activation::background, + }); + } + } + return; + } else if (_subsectionTabs) { + return; + } + _subsectionTabs = controller()->restoreSubsectionTabsFor(this, thread); + if (!_subsectionTabs) { + _subsectionTabs = std::make_unique<HistoryView::SubsectionTabs>( + controller(), + this, + thread); + } + _subsectionTabs->removeRequests() | rpl::start_with_next([=] { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + updateControlsGeometry(); + }, _subsectionTabsLifetime); + _subsectionTabs->layoutRequests() | rpl::start_with_next([=] { + _inner->overrideChatMode((_subsectionTabs->leftSkip() > 0) + ? ElementChatMode::Narrow + : std::optional<ElementChatMode>()); + updateControlsGeometry(); + orderWidgets(); + }, _subsectionTabsLifetime); + _inner->overrideChatMode((_subsectionTabs->leftSkip() > 0) + ? ElementChatMode::Narrow + : std::optional<ElementChatMode>()); + updateControlsGeometry(); + orderWidgets(); +} + +void ChatWidget::refreshJoinGroupButton() { + if (!_repliesRootId) { + return; + } const auto set = [&](std::unique_ptr<Ui::FlatButton> button) { if (!button && !_joinGroup) { return; @@ -1497,13 +1632,15 @@ void RepliesWidget::refreshJoinGroupButton() { listScrollTo(_scroll->scrollTopMax()); } }; - const auto channel = _history->peer->asChannel(); + const auto channel = _peer->asChannel(); const auto canSend = !channel->isForum() ? Data::CanSendAnything(channel) : (_topic && Data::CanSendAnything(_topic)); if (channel->amIn() || canSend) { + _canSendTexts = true; set(nullptr); } else { + _canSendTexts = false; if (!_joinGroup) { set(std::make_unique<Ui::FlatButton>( this, @@ -1521,15 +1658,15 @@ void RepliesWidget::refreshJoinGroupButton() { } } -bool RepliesWidget::sendExistingDocument( +bool ChatWidget::sendExistingDocument( not_null<DocumentData*> document, Api::MessageToSend messageToSend, std::optional<MsgId> localId) { const auto error = Data::RestrictionError( - _history->peer, + _peer, ChatRestriction::SendStickers); if (error) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return false; } else if (showSlowmodeError() || ShowSendPremiumError(controller(), document)) { @@ -1542,7 +1679,7 @@ bool RepliesWidget::sendExistingDocument( }; const auto checked = checkSendPayment( 1, - messageToSend.action.options.starsApproved, + messageToSend.action.options, withPaymentApproved); if (!checked) { return false; @@ -1558,18 +1695,18 @@ bool RepliesWidget::sendExistingDocument( return true; } -void RepliesWidget::sendExistingPhoto(not_null<PhotoData*> photo) { +void ChatWidget::sendExistingPhoto(not_null<PhotoData*> photo) { sendExistingPhoto(photo, {}); } -bool RepliesWidget::sendExistingPhoto( +bool ChatWidget::sendExistingPhoto( not_null<PhotoData*> photo, Api::SendOptions options) { const auto error = Data::RestrictionError( - _history->peer, + _peer, ChatRestriction::SendPhotos); if (error) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return false; } else if (showSlowmodeError()) { return false; @@ -1582,7 +1719,7 @@ bool RepliesWidget::sendExistingPhoto( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + options, withPaymentApproved); if (!checked) { return false; @@ -1597,11 +1734,11 @@ bool RepliesWidget::sendExistingPhoto( return true; } -void RepliesWidget::sendInlineResult( +void ChatWidget::sendInlineResult( std::shared_ptr<InlineBots::Result> result, not_null<UserData*> bot) { if (const auto error = result->getErrorOnSend(_history)) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return; } sendInlineResult(std::move(result), bot, {}, std::nullopt); @@ -1613,7 +1750,7 @@ void RepliesWidget::sendInlineResult( // Ui::LayerOption::KeepOther); } -void RepliesWidget::sendInlineResult( +void ChatWidget::sendInlineResult( std::shared_ptr<InlineBots::Result> result, not_null<UserData*> bot, Api::SendOptions options, @@ -1625,7 +1762,7 @@ void RepliesWidget::sendInlineResult( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + options, withPaymentApproved); if (!checked) { return; @@ -1658,45 +1795,66 @@ void RepliesWidget::sendInlineResult( finishSending(); } -SendMenu::Details RepliesWidget::sendMenuDetails() const { +SendMenu::Details ChatWidget::sendMenuDetails() const { using Type = SendMenu::Type; - const auto type = (_topic && !_history->peer->starsPerMessageChecked()) + const auto type = (_topic && !_peer->starsPerMessageChecked()) ? Type::Scheduled : Type::SilentOnly; return SendMenu::Details{ .type = type }; } -FullReplyTo RepliesWidget::replyTo() const { +FullReplyTo ChatWidget::replyTo() const { if (auto custom = _composeControls->replyingToMessage()) { - custom.topicRootId = _rootId; - return custom; + const auto item = custom.messageId + ? session().data().message(custom.messageId) + : nullptr; + const auto sublistPeerId = item ? item->sublistPeerId() : PeerId(); + if (!item + || !_monoforumPeerId + || (sublistPeerId == _monoforumPeerId)) { + // Never answer to a message in a wrong monoforum peer id. + custom.topicRootId = _repliesRootId; + custom.monoforumPeerId = _monoforumPeerId; + return custom; + } } return FullReplyTo{ - .messageId = FullMsgId(_history->peer->id, _rootId), - .topicRootId = _rootId, + .messageId = (_repliesRootId + ? FullMsgId(_peer->id, _repliesRootId) + : FullMsgId()), + .topicRootId = _repliesRootId, + .monoforumPeerId = _monoforumPeerId, }; } -void RepliesWidget::refreshTopBarActiveChat() { +void ChatWidget::refreshTopBarActiveChat() { using namespace Dialogs; + const auto state = EntryState{ - .key = (_topic ? Key{ _topic } : Key{ _history }), - .section = EntryState::Section::Replies, + .key = (_sublist + ? Key{ _sublist } + : _topic + ? Key{ _topic } + : Key{ _history }), + .section = _sublist + ? EntryState::Section::SavedSublist + : EntryState::Section::Replies, .currentReplyTo = replyTo(), + .currentSuggest = SuggestPostOptions(), }; _topBar->setActiveChat(state, _sendAction.get()); _composeControls->setCurrentDialogsEntryState(state); controller()->setDialogsEntryState(state); } -void RepliesWidget::refreshUnreadCountBadge(std::optional<int> count) { +void ChatWidget::refreshUnreadCountBadge(std::optional<int> count) { if (count.has_value()) { _cornerButtons.updateJumpDownVisibility(count); } } -void RepliesWidget::updatePinnedViewer() { - if (_scroll->isHidden() || !_topic || !_pinnedTracker) { +void ChatWidget::updatePinnedViewer() { + if (_scroll->isHidden() || (!_topic && !_sublist) || !_pinnedTracker) { return; } const auto visibleBottom = _scroll->scrollTop() + _scroll->height(); @@ -1713,7 +1871,10 @@ void RepliesWidget::updatePinnedViewer() { _pinnedClickedId = FullMsgId(); } if (_pinnedClickedId && !_minPinnedId) { - _minPinnedId = Data::ResolveMinPinnedId(_history->peer, _rootId); + _minPinnedId = Data::ResolveMinPinnedId( + _peer, + _repliesRootId, + _monoforumPeerId); } if (_pinnedClickedId && _minPinnedId && _minPinnedId >= _pinnedClickedId) { // After click on the last pinned message we should the top one. @@ -1723,10 +1884,10 @@ void RepliesWidget::updatePinnedViewer() { } } -void RepliesWidget::checkLastPinnedClickedIdReset( +void ChatWidget::checkLastPinnedClickedIdReset( int wasScrollTop, int nowScrollTop) { - if (_scroll->isHidden() || !_topic) { + if (_scroll->isHidden() || (!_topic && !_sublist)) { return; } if (wasScrollTop < nowScrollTop && _pinnedClickedId) { @@ -1737,7 +1898,54 @@ void RepliesWidget::checkLastPinnedClickedIdReset( } } -void RepliesWidget::setupTranslateBar() { +void ChatWidget::setupOpenChatButton() { + if (!_sublist || _sublist->sublistPeer()->isSavedHiddenAuthor()) { + return; + } else if (_sublist->parentChat()) { + _canSendTexts = true; + return; + } + _openChatButton = std::make_unique<Ui::FlatButton>( + this, + (_sublist->sublistPeer()->isBroadcast() + ? tr::lng_saved_open_channel(tr::now) + : _sublist->sublistPeer()->isUser() + ? tr::lng_saved_open_chat(tr::now) + : tr::lng_saved_open_group(tr::now)), + st::historyComposeButton); + + _openChatButton->setClickedCallback([=] { + controller()->showPeerHistory( + _sublist->sublistPeer(), + Window::SectionShow::Way::Forward); + }); +} + +void ChatWidget::setupAboutHiddenAuthor() { + if (!_sublist || !_sublist->sublistPeer()->isSavedHiddenAuthor()) { + return; + } else if (_sublist->parentChat()) { + _canSendTexts = true; + return; + } + _aboutHiddenAuthor = std::make_unique<Ui::RpWidget>(this); + _aboutHiddenAuthor->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_aboutHiddenAuthor.get()); + auto rect = _aboutHiddenAuthor->rect(); + + p.fillRect(rect, st::historyReplyBg); + + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawText( + rect.marginsRemoved( + QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), + tr::lng_saved_about_hidden(tr::now), + style::al_center); + }, _aboutHiddenAuthor->lifetime()); +} + +void ChatWidget::setupTranslateBar() { controller()->adaptive().oneColumnValue( ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { raw->setShadowGeometryPostprocess([=](QRect geometry) { @@ -1760,17 +1968,19 @@ void RepliesWidget::setupTranslateBar() { _translateBar->finishAnimating(); } -void RepliesWidget::setupPinnedTracker() { - Expects(_topic != nullptr); +void ChatWidget::setupPinnedTracker() { + Expects(_topic || _sublist); - _pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(_topic); + const auto thread = _topic ? (Data::Thread*)_topic : _sublist; + _pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(thread); _pinnedBar = nullptr; SharedMediaViewer( - &_topic->session(), + &session(), Storage::SharedMediaKey( - _topic->channel()->id, - _rootId, + _peer->id, + _repliesRootId, + _monoforumPeerId, Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, @@ -1778,34 +1988,42 @@ void RepliesWidget::setupPinnedTracker() { ) | rpl::filter([=](const SparseIdsSlice &result) { return result.fullCount().has_value(); }) | rpl::start_with_next([=](const SparseIdsSlice &result) { - _topic->setHasPinnedMessages(*result.fullCount() != 0); + thread->setHasPinnedMessages(*result.fullCount() != 0); if (result.skippedAfter() == 0) { auto &settings = _history->session().settings(); - const auto peerId = _history->peer->id; + const auto peerId = _peer->id; const auto hiddenId = settings.hiddenPinnedMessageId( peerId, - _rootId); + _repliesRootId, + _monoforumPeerId); const auto last = result.size() ? result[result.size() - 1] : 0; if (hiddenId && hiddenId != last) { - settings.setHiddenPinnedMessageId(peerId, _rootId, 0); + settings.setHiddenPinnedMessageId( + peerId, + _repliesRootId, + _monoforumPeerId, + 0); _history->session().saveSettingsDelayed(); } } checkPinnedBarState(); - }, _topicLifetime); + }, lifetime()); } -void RepliesWidget::checkPinnedBarState() { +void ChatWidget::checkPinnedBarState() { Expects(_pinnedTracker != nullptr); Expects(_inner != nullptr); - const auto peer = _history->peer; - const auto hiddenId = peer->canPinMessages() + const auto hiddenId = _peer->canPinMessages() ? MsgId(0) - : peer->session().settings().hiddenPinnedMessageId( - peer->id, - _rootId); - const auto currentPinnedId = Data::ResolveTopPinnedId(peer, _rootId); + : _peer->session().settings().hiddenPinnedMessageId( + _peer->id, + _repliesRootId, + _monoforumPeerId); + const auto currentPinnedId = Data::ResolveTopPinnedId( + _peer, + _repliesRootId, + _monoforumPeerId); const auto universalPinnedId = !currentPinnedId ? MsgId(0) : currentPinnedId.msg; @@ -1829,13 +2047,14 @@ void RepliesWidget::checkPinnedBarState() { } clearHidingPinnedBar(); - _pinnedBar = std::make_unique<Ui::PinnedBar>(this, [=] { + _pinnedBar = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); auto pinnedRefreshed = Info::Profile::SharedMediaCountValue( - _history->peer, - _rootId, + _peer, + _repliesRootId, + _monoforumPeerId, nullptr, Storage::SharedMediaType::Pinned ) | rpl::distinct_until_changed( @@ -1864,7 +2083,7 @@ void RepliesWidget::checkPinnedBarState() { [bar = _pinnedBar.get()] { bar->customEmojiRepaint(); }), std::move(pinnedRefreshed), std::move(customButtonItem), - _rootVisible.value() + _repliesRootVisible.value() ) | rpl::map([=](Ui::MessageBarContent &&content, auto, auto, bool show) { const auto shown = !content.title.isEmpty() && !content.text.empty(); _shownPinnedItem = shown @@ -1913,13 +2132,9 @@ void RepliesWidget::checkPinnedBarState() { }, _pinnedBar->lifetime()); orderWidgets(); - - if (animatingShow()) { - _pinnedBar->hide(); - } } -void RepliesWidget::clearHidingPinnedBar() { +void ChatWidget::clearHidingPinnedBar() { if (!_hidingPinnedBar) { return; } @@ -1930,7 +2145,7 @@ void RepliesWidget::clearHidingPinnedBar() { _hidingPinnedBar = nullptr; } -void RepliesWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { +void ChatWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { if (!_pinnedBar) { return; // It can be in process of hiding. } @@ -1941,8 +2156,9 @@ void RepliesWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { if (!id.message) { return; } + const auto thread = _topic ? (Data::Thread*)_topic : _sublist; controller()->showSection( - std::make_shared<PinnedMemento>(_topic, id.message.msg)); + std::make_shared<PinnedMemento>(thread, id.message.msg)); }; const auto context = [copy = _inner](FullMsgId itemId) { if (const auto raw = copy.data()) { @@ -1983,14 +2199,14 @@ void RepliesWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { _pinnedBar->setRightButton(std::move(button)); } -void RepliesWidget::hidePinnedMessage() { +void ChatWidget::hidePinnedMessage() { Expects(_pinnedBar != nullptr); const auto id = _pinnedTracker->currentMessageId(); if (!id.message) { return; } - if (_history->peer->canPinMessages()) { + if (_peer->canPinMessages()) { Window::ToggleMessagePinned(controller(), id.message, false); } else { const auto callback = [=] { @@ -2000,30 +2216,35 @@ void RepliesWidget::hidePinnedMessage() { }; Window::HidePinnedBar( controller(), - _history->peer, - _rootId, + _peer, + _repliesRootId, + _monoforumPeerId, crl::guard(this, callback)); } } -void RepliesWidget::cornerButtonsShowAtPosition( +void ChatWidget::cornerButtonsShowAtPosition( Data::MessagePosition position) { showAtPosition(position); } -Data::Thread *RepliesWidget::cornerButtonsThread() { - return _topic ? static_cast<Data::Thread*>(_topic) : _history; +Data::Thread *ChatWidget::cornerButtonsThread() { + return _sublist + ? static_cast<Data::Thread*>(_sublist) + : _topic + ? static_cast<Data::Thread*>(_topic) + : _history; } -FullMsgId RepliesWidget::cornerButtonsCurrentId() { +FullMsgId ChatWidget::cornerButtonsCurrentId() { return _lastShownAt; } -bool RepliesWidget::cornerButtonsIgnoreVisibility() { +bool ChatWidget::cornerButtonsIgnoreVisibility() { return animatingShow(); } -std::optional<bool> RepliesWidget::cornerButtonsDownShown() { +std::optional<bool> ChatWidget::cornerButtonsDownShown() { if (_composeControls->isLockPresent() || _composeControls->isTTLButtonShown()) { return false; @@ -2037,25 +2258,27 @@ std::optional<bool> RepliesWidget::cornerButtonsDownShown() { return std::nullopt; } -bool RepliesWidget::cornerButtonsUnreadMayBeShown() { +bool ChatWidget::cornerButtonsUnreadMayBeShown() { return _loaded && !_composeControls->isLockPresent() && !_composeControls->isTTLButtonShown(); } -bool RepliesWidget::cornerButtonsHas(CornerButtonType type) { - return _topic || (type == CornerButtonType::Down); +bool ChatWidget::cornerButtonsHas(CornerButtonType type) { + return _topic + || (_sublist && type == CornerButtonType::Reactions) + || (type == CornerButtonType::Down); } -void RepliesWidget::showAtStart() { +void ChatWidget::showAtStart() { showAtPosition(Data::MinMessagePosition); } -void RepliesWidget::showAtEnd() { +void ChatWidget::showAtEnd() { showAtPosition(Data::MaxMessagePosition); } -void RepliesWidget::finishSending() { +void ChatWidget::finishSending() { _composeControls->hidePanelsAnimated(); //if (_previewData && _previewData->pendingTill) previewCancel(); doSetInnerFocus(); @@ -2063,46 +2286,45 @@ void RepliesWidget::finishSending() { refreshTopBarActiveChat(); } -void RepliesWidget::showAtPosition( +void ChatWidget::showAtPosition( Data::MessagePosition position, FullMsgId originItemId) { showAtPosition(position, originItemId, {}); } -void RepliesWidget::showAtPosition( +void ChatWidget::showAtPosition( Data::MessagePosition position, FullMsgId originItemId, const Window::SectionShow ¶ms) { _lastShownAt = position.fullId; controller()->setActiveChatEntry(activeChat()); - const auto ignore = (position.fullId.msg == _rootId); + const auto ignore = _repliesRootId + && (position.fullId.msg == _repliesRootId); _inner->showAtPosition( position, params, _cornerButtons.doneJumpFrom(position.fullId, originItemId, ignore)); } -void RepliesWidget::updateAdaptiveLayout() { +void ChatWidget::updateAdaptiveLayout() { _topBarShadow->moveToLeft( controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, _topBar->height()); } -not_null<History*> RepliesWidget::history() const { - return _history; -} - -Dialogs::RowDescriptor RepliesWidget::activeChat() const { +Dialogs::RowDescriptor ChatWidget::activeChat() const { const auto messageId = _lastShownAt ? _lastShownAt - : FullMsgId(_history->peer->id, ShowAtUnreadMsgId); - if (_topic) { + : FullMsgId(_peer->id, ShowAtUnreadMsgId); + if (_sublist) { + return { _sublist, messageId }; + } else if (_topic) { return { _topic, messageId }; } return { _history, messageId }; } -bool RepliesWidget::preventsClose(Fn<void()> &&continueCallback) const { +bool ChatWidget::preventsClose(Fn<void()> &&continueCallback) const { if (_composeControls->preventsClose(base::duplicate(continueCallback))) { return true; } else if (!_newTopicDiscarded @@ -2129,7 +2351,7 @@ bool RepliesWidget::preventsClose(Fn<void()> &&continueCallback) const { return false; } -QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { +QPixmap ChatWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { _topBar->updateControlsVisibility(); if (params.withTopBarShadow) _topBarShadow->hide(); if (_joinGroup) { @@ -2141,21 +2363,18 @@ QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams &pa if (params.withTopBarShadow) { _topBarShadow->show(); } - if (_rootView) { - _rootView->hide(); + _topBars->hide(); + if (_subsectionTabs) { + _subsectionTabs->hide(); } - if (_pinnedBar) { - _pinnedBar->hide(); - } - _translateBar->hide(); return result; } -void RepliesWidget::checkActivation() { +void ChatWidget::checkActivation() { _inner->checkActivation(); } -void RepliesWidget::doSetInnerFocus() { +void ChatWidget::doSetInnerFocus() { if (_composeSearch && _inner->getSelectedText().rich.text.isEmpty() && _inner->getSelectedItems().empty()) { @@ -2167,19 +2386,19 @@ void RepliesWidget::doSetInnerFocus() { } } -bool RepliesWidget::showInternal( +bool ChatWidget::showInternal( not_null<Window::SectionMemento*> memento, const Window::SectionShow ¶ms) { - if (auto logMemento = dynamic_cast<RepliesMemento*>(memento.get())) { - if (logMemento->getHistory() == history() - && logMemento->getRootId() == _rootId) { - restoreState(logMemento); - if (!logMemento->highlightId()) { - showAtPosition(Data::UnreadMessagePosition); - } + if (auto logMemento = dynamic_cast<ChatMemento*>(memento.get())) { + if (logMemento->id() == _id) { if (params.reapplyLocalDraft) { _composeControls->applyDraft( ComposeControls::FieldHistoryAction::NewEntry); + } else { + restoreState(logMemento); + if (!logMemento->highlightId()) { + showAtPosition(Data::UnreadMessagePosition); + } } return true; } @@ -2187,15 +2406,19 @@ bool RepliesWidget::showInternal( return false; } -void RepliesWidget::setInternalState( +bool ChatWidget::sameTypeAs(not_null<Window::SectionMemento*> memento) { + return dynamic_cast<ChatMemento*>(memento.get()) != nullptr; +} + +void ChatWidget::setInternalState( const QRect &geometry, - not_null<RepliesMemento*> memento) { + not_null<ChatMemento*> memento) { setGeometry(geometry); Ui::SendPendingMoveResizeEvents(this); restoreState(memento); } -bool RepliesWidget::pushTabbedSelectorToThirdSection( +bool ChatWidget::pushTabbedSelectorToThirdSection( not_null<Data::Thread*> thread, const Window::SectionShow ¶ms) { return _composeControls->pushTabbedSelectorToThirdSection( @@ -2203,26 +2426,32 @@ bool RepliesWidget::pushTabbedSelectorToThirdSection( params); } -bool RepliesWidget::returnTabbedSelector() { +bool ChatWidget::returnTabbedSelector() { return _composeControls->returnTabbedSelector(); } -std::shared_ptr<Window::SectionMemento> RepliesWidget::createMemento() { - auto result = std::make_shared<RepliesMemento>(history(), _rootId); +std::shared_ptr<Window::SectionMemento> ChatWidget::createMemento() { + auto result = std::make_shared<ChatMemento>(_id); saveState(result.get()); return result; } -bool RepliesWidget::showMessage( +bool ChatWidget::showMessage( PeerId peerId, const Window::SectionShow ¶ms, MsgId messageId) { - if (peerId != _history->peer->id) { + if (peerId != _peer->id) { return false; } - const auto id = FullMsgId(_history->peer->id, messageId); + const auto id = FullMsgId(_peer->id, messageId); const auto message = _history->owner().message(id); - if (!message || (!message->inThread(_rootId) && id.msg != _rootId)) { + if (!message) { + return false; + } else if (_repliesRootId + && !message->inThread(_repliesRootId) + && id.msg != _repliesRootId) { + return false; + } else if (_sublist && message->savedSublist() != _sublist) { return false; } const auto originMessage = [&]() -> HistoryItem* { @@ -2231,7 +2460,11 @@ bool RepliesWidget::showMessage( if (const auto returnTo = session().data().message(origin->id)) { if (returnTo->history() != _history) { return nullptr; - } else if (returnTo->inThread(_rootId)) { + } else if (_repliesRootId + && returnTo->inThread(_repliesRootId)) { + return returnTo; + } else if (_sublist + && returnTo->savedSublist() == _sublist) { return returnTo; } } @@ -2248,24 +2481,26 @@ bool RepliesWidget::showMessage( return true; } -Window::SectionActionResult RepliesWidget::sendBotCommand( +Window::SectionActionResult ChatWidget::sendBotCommand( Bot::SendCommandRequest request) { - if (request.peer != _history->peer) { + if (!_repliesRootId) { + return Window::SectionActionResult::Fallback; + } else if (request.peer != _peer) { return Window::SectionActionResult::Ignore; } listSendBotCommand(request.command, request.context); return Window::SectionActionResult::Handle; } -bool RepliesWidget::confirmSendingFiles(const QStringList &files) { +bool ChatWidget::confirmSendingFiles(const QStringList &files) { return confirmSendingFiles(files, QString()); } -bool RepliesWidget::confirmSendingFiles(not_null<const QMimeData*> data) { +bool ChatWidget::confirmSendingFiles(not_null<const QMimeData*> data) { return confirmSendingFiles(data, std::nullopt); } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( const QStringList &files, const QString &insertTextOnCancel) { const auto premium = controller()->session().user()->isPremium(); @@ -2274,28 +2509,31 @@ bool RepliesWidget::confirmSendingFiles( insertTextOnCancel); } -void RepliesWidget::replyToMessage(FullReplyTo id) { +void ChatWidget::replyToMessage(FullReplyTo id) { _composeControls->replyToMessage(std::move(id)); refreshTopBarActiveChat(); } -void RepliesWidget::saveState(not_null<RepliesMemento*> memento) { +void ChatWidget::saveState(not_null<ChatMemento*> memento) { memento->setReplies(_replies); memento->setReplyReturns(_cornerButtons.replyReturns()); _inner->saveState(memento->list()); } -void RepliesWidget::refreshReplies() { +void ChatWidget::refreshReplies() { + if (!_repliesRootId) { + return; + } auto old = base::take(_replies); setReplies(_topic ? _topic->replies() - : std::make_shared<Data::RepliesList>(_history, _rootId)); + : std::make_shared<Data::RepliesList>(_history, _repliesRootId)); if (old) { _inner->refreshViewer(); } } -void RepliesWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { +void ChatWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { _replies = std::move(replies); _repliesLifetime.destroy(); @@ -2304,9 +2542,7 @@ void RepliesWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { refreshUnreadCountBadge(count); }, lifetime()); - refreshUnreadCountBadge(_replies->unreadCountKnown() - ? _replies->unreadCountCurrent() - : std::optional<int>()); + unreadCountUpdated(); const auto isTopic = (_topic != nullptr); const auto isTopicCreating = isTopic && _topic->creating(); @@ -2338,10 +2574,72 @@ void RepliesWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { }, _repliesLifetime); } -void RepliesWidget::restoreState(not_null<RepliesMemento*> memento) { +void ChatWidget::subscribeToSublist() { + Expects(_sublist != nullptr); + + // Must be done before unreadCountUpdated(), or we auto-close. + if (_sublist->unreadMark()) { + _sublist->owner().histories().changeSublistUnreadMark( + _sublist, + false); + } + + _sublist->unreadCountValue( + ) | rpl::start_with_next([=](std::optional<int> count) { + refreshUnreadCountBadge(count); + }, lifetime()); + + using Flag = Data::SublistUpdate::Flag; + session().changes().sublistUpdates( + _sublist, + Flag::UnreadView | Flag::UnreadReactions | Flag::CloudDraft + ) | rpl::start_with_next([=](const Data::SublistUpdate &update) { + if (update.flags & Flag::UnreadView) { + unreadCountUpdated(); + } + if (update.flags & Flag::UnreadReactions) { + _cornerButtons.updateUnreadThingsVisibility(); + } + if (update.flags & Flag::CloudDraft) { + _composeControls->applyCloudDraft(); + } + }, lifetime()); + + _sublist->destroyed( + ) | rpl::start_with_next([=] { + closeCurrent(); + }, lifetime()); + + unreadCountUpdated(); + subscribeToPinnedMessages(); +} + +void ChatWidget::unreadCountUpdated() { + if (_sublist && _sublist->unreadMark()) { + crl::on_main(this, [=] { + const auto guard = Ui::MakeWeak(this); + controller()->showPeerHistory(_sublist->owningHistory()); + if (guard) { + closeCurrent(); + } + }); + } else { + refreshUnreadCountBadge(_replies + ? (_replies->unreadCountKnown() + ? _replies->unreadCountCurrent() + : std::optional<int>()) + : _sublist + ? (_sublist->unreadCountKnown() + ? _sublist->unreadCountCurrent() + : std::optional<int>()) + : std::optional<int>()); + } +} + +void ChatWidget::restoreState(not_null<ChatMemento*> memento) { if (auto replies = memento->getReplies()) { setReplies(std::move(replies)); - } else if (!_replies) { + } else if (!_replies && _repliesRootId) { refreshReplies(); } _cornerButtons.setReplyReturns(memento->replyReturns()); @@ -2353,13 +2651,13 @@ void RepliesWidget::restoreState(not_null<RepliesMemento*> memento) { params.highlightPart = memento->highlightPart(); params.highlightPartOffsetHint = memento->highlightPartOffsetHint(); showAtPosition(Data::MessagePosition{ - .fullId = FullMsgId(_history->peer->id, highlight), + .fullId = FullMsgId(_peer->id, highlight), .date = TimeId(0), }, {}, params); } } -void RepliesWidget::resizeEvent(QResizeEvent *e) { +void ChatWidget::resizeEvent(QResizeEvent *e) { if (!width() || !height()) { return; } @@ -2368,32 +2666,37 @@ void RepliesWidget::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } -void RepliesWidget::recountChatWidth() { +void ChatWidget::recountChatWidth() { auto layout = (width() < st::adaptiveChatWideWidth) ? Window::Adaptive::ChatLayout::Normal : Window::Adaptive::ChatLayout::Wide; controller()->adaptive().setChatLayout(layout); } -void RepliesWidget::updateControlsGeometry() { +void ChatWidget::updateControlsGeometry() { const auto contentWidth = width(); - const auto newScrollTop = _scroll->isHidden() + const auto newScrollDelta = _scroll->isHidden() ? std::nullopt : _scroll->scrollTop() - ? base::make_optional(_scroll->scrollTop() - + topDelta() - + _scrollTopDelta) + ? base::make_optional(topDelta() + _scrollTopDelta) : 0; _topBar->resizeToWidth(contentWidth); _topBarShadow->resize(contentWidth, st::lineWidth); - if (_rootView) { - _rootView->resizeToWidth(contentWidth); + const auto tabsLeftSkip = _subsectionTabs + ? _subsectionTabs->leftSkip() + : 0; + const auto innerWidth = contentWidth - tabsLeftSkip; + const auto subsectionTabsTop = _topBar->bottomNoMargins(); + _topBars->move(tabsLeftSkip, subsectionTabsTop + + (_subsectionTabs ? _subsectionTabs->topSkip() : 0)); + if (_repliesRootView) { + _repliesRootView->resizeToWidth(innerWidth); } - auto top = _topBar->height() + _rootViewHeight; + auto top = _repliesRootViewHeight; if (_pinnedBar) { _pinnedBar->move(0, top); - _pinnedBar->resizeToWidth(contentWidth); + _pinnedBar->resizeToWidth(innerWidth); top += _pinnedBarHeight; } if (_topicReopenBar) { @@ -2401,42 +2704,61 @@ void RepliesWidget::updateControlsGeometry() { top += _topicReopenBar->bar().height(); } _translateBar->move(0, top); - _translateBar->resizeToWidth(contentWidth); + _translateBar->resizeToWidth(innerWidth); top += _translateBarHeight; - const auto bottom = height(); - const auto controlsHeight = _joinGroup - ? _joinGroup->height() - : _composeControls->heightCurrent(); - const auto scrollHeight = bottom - top - controlsHeight; - const auto scrollSize = QSize(contentWidth, scrollHeight); + auto bottom = height(); + if (_openChatButton) { + _openChatButton->resizeToWidth(width()); + bottom -= _openChatButton->height(); + _openChatButton->move(0, bottom); + } else if (_aboutHiddenAuthor) { + _aboutHiddenAuthor->resize(width(), st::historyUnblock.height); + bottom -= _aboutHiddenAuthor->height(); + _aboutHiddenAuthor->move(0, bottom); + } else if (_joinGroup) { + _joinGroup->resizeToWidth(width()); + bottom -= _joinGroup->height(); + _joinGroup->move(0, bottom); + } else { + bottom -= _composeControls->heightCurrent(); + } + + _topBars->resize(innerWidth, top + st::lineWidth); + top += _topBars->y(); + + const auto scrollHeight = bottom - top; + const auto scrollSize = QSize(innerWidth, scrollHeight); if (_scroll->size() != scrollSize) { _skipScrollEvent = true; _scroll->resize(scrollSize); _inner->resizeToWidth(scrollSize.width(), _scroll->height()); _skipScrollEvent = false; } - _scroll->move(0, top); + _scroll->move(tabsLeftSkip, top); if (!_scroll->isHidden()) { + const auto newScrollTop = (newScrollDelta && _scroll->scrollTop()) + ? (_scroll->scrollTop() + *newScrollDelta) + : std::optional<int>(); if (newScrollTop) { _scroll->scrollToY(*newScrollTop); } updateInnerVisibleArea(); } - if (_joinGroup) { - _joinGroup->setGeometry( - 0, - bottom - _joinGroup->height(), - contentWidth, - _joinGroup->height()); - } - _composeControls->move(0, bottom - controlsHeight); + _composeControls->move(0, bottom); _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); + if (_subsectionTabs) { + const auto scrollBottom = _scroll->y() + scrollHeight; + const auto areaHeight = scrollBottom - subsectionTabsTop; + _subsectionTabs->setBoundingRect( + { 0, subsectionTabsTop, width(), areaHeight }); + } + _cornerButtons.updatePositions(); } -void RepliesWidget::paintEvent(QPaintEvent *e) { +void ChatWidget::paintEvent(QPaintEvent *e) { if (animatingShow()) { SectionWidget::paintEvent(e); return; @@ -2450,20 +2772,20 @@ void RepliesWidget::paintEvent(QPaintEvent *e) { SectionWidget::PaintBackground(controller(), _theme.get(), this, bg); } -bool RepliesWidget::emptyShown() const { +bool ChatWidget::emptyShown() const { return _topic && (_inner->isEmpty() - || (_topic->lastKnownServerMessageId() == _rootId)); + || (_topic->lastKnownServerMessageId() == _repliesRootId)); } -void RepliesWidget::onScroll() { +void ChatWidget::onScroll() { if (_skipScrollEvent) { return; } updateInnerVisibleArea(); } -void RepliesWidget::updateInnerVisibleArea() { +void ChatWidget::updateInnerVisibleArea() { if (!_inner->animatedScrolling()) { checkReplyReturns(); } @@ -2481,18 +2803,21 @@ void RepliesWidget::updateInnerVisibleArea() { } } -void RepliesWidget::updatePinnedVisibility() { - if (!_loaded) { +void ChatWidget::updatePinnedVisibility() { + if (_sublist) { + setPinnedVisibility(true); return; - } else if (!_topic && (!_root || _root->isEmpty())) { - setPinnedVisibility(!_root); + } else if (!_loaded || !_repliesRootId) { + return; + } else if (!_topic && (!_repliesRoot || _repliesRoot->isEmpty())) { + setPinnedVisibility(!_repliesRoot); return; } const auto rootItem = [&] { - if (const auto group = _history->owner().groups().find(_root)) { + if (const auto group = _history->owner().groups().find(_repliesRoot)) { return group->items.front().get(); } - return _root; + return _repliesRoot; }; const auto view = _inner->viewByPosition(_topic ? Data::MinMessagePosition @@ -2502,14 +2827,17 @@ void RepliesWidget::updatePinnedVisibility() { setPinnedVisibility(visible || (_topic && !view->data()->isPinned())); } -void RepliesWidget::setPinnedVisibility(bool shown) { +void ChatWidget::setPinnedVisibility(bool shown) { if (animatingShow()) { + } else if (_sublist) { + _repliesRootVisible = shown; + } else if (!_repliesRootId) { return; } else if (!_topic) { - if (!_rootViewInitScheduled) { + if (!_repliesRootViewInitScheduled) { const auto height = shown ? st::historyReplyHeight : 0; - if (const auto delta = height - _rootViewHeight) { - _rootViewHeight = height; + if (const auto delta = height - _repliesRootViewHeight) { + _repliesRootViewHeight = height; if (_scroll->scrollTop() == _scroll->scrollTopMax()) { setGeometryWithTopMoved(geometry(), delta); } else { @@ -2517,22 +2845,22 @@ void RepliesWidget::setPinnedVisibility(bool shown) { } } } - _rootVisible = shown; - if (!_rootViewInited) { - _rootView->finishAnimating(); - if (!_rootViewInitScheduled) { - _rootViewInitScheduled = true; + _repliesRootVisible = shown; + if (!_repliesRootViewInited) { + _repliesRootView->finishAnimating(); + if (!_repliesRootViewInitScheduled) { + _repliesRootViewInitScheduled = true; InvokeQueued(this, [=] { - _rootViewInited = true; + _repliesRootViewInited = true; }); } } } else { - _rootVisible = shown; + _repliesRootVisible = shown; } } -void RepliesWidget::showAnimatedHook( +void ChatWidget::showAnimatedHook( const Window::SectionSlideParams ¶ms) { _topBar->setAnimatingMode(true); if (params.withTopBarShadow) { @@ -2541,9 +2869,9 @@ void RepliesWidget::showAnimatedHook( _composeControls->showStarted(); } -void RepliesWidget::showFinishedHook() { +void ChatWidget::showFinishedHook() { _topBar->setAnimatingMode(false); - if (_joinGroup) { + if (_joinGroup || _openChatButton || _aboutHiddenAuthor) { if (Ui::InFocusChain(this)) { _inner->setFocus(); } @@ -2552,15 +2880,9 @@ void RepliesWidget::showFinishedHook() { _composeControls->showFinished(); } _inner->showFinished(); - if (_rootView) { - _rootView->show(); - } - if (_pinnedBar) { - _pinnedBar->show(); - } - _translateBar->show(); - if (_topicReopenBar) { - _topicReopenBar->bar().show(); + _topBars->show(); + if (_subsectionTabs) { + _subsectionTabs->show(); } // We should setup the drag area only after @@ -2568,21 +2890,31 @@ void RepliesWidget::showFinishedHook() { // because after that the method showChildren() is called. setupDragArea(); updatePinnedVisibility(); + + if (_topic) { + _topic->saveMeAsActiveSubsectionThread(); + } else if (_sublist) { + _sublist->saveMeAsActiveSubsectionThread(); + } } -bool RepliesWidget::floatPlayerHandleWheelEvent(QEvent *e) { +bool ChatWidget::floatPlayerHandleWheelEvent(QEvent *e) { return _scroll->viewportEvent(e); } -QRect RepliesWidget::floatPlayerAvailableRect() { +QRect ChatWidget::floatPlayerAvailableRect() { return mapToGlobal(_scroll->geometry()); } -Context RepliesWidget::listContext() { - return Context::Replies; +Context ChatWidget::listContext() { + return !_sublist + ? Context::Replies + : _sublist->parentChat() + ? Context::Monoforum + : Context::SavedSublist; } -bool RepliesWidget::listScrollTo(int top, bool syntetic) { +bool ChatWidget::listScrollTo(int top, bool syntetic) { top = std::clamp(top, 0, _scroll->scrollTopMax()); const auto scrolled = (_scroll->scrollTop() != top); _synteticScrollEvent = syntetic; @@ -2595,7 +2927,7 @@ bool RepliesWidget::listScrollTo(int top, bool syntetic) { return scrolled; } -void RepliesWidget::listCancelRequest() { +void ChatWidget::listCancelRequest() { if (_composeSearch) { if (_inner && (!_inner->getSelectedItems().empty() @@ -2616,15 +2948,36 @@ void RepliesWidget::listCancelRequest() { controller()->showBackFromStack(); } -void RepliesWidget::listDeleteRequest() { +void ChatWidget::listDeleteRequest() { confirmDeleteSelected(); } -void RepliesWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) { +void ChatWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) { _composeControls->tryProcessKeyInput(e); } -rpl::producer<Data::MessagesSlice> RepliesWidget::listSource( +void ChatWidget::markLoaded() { + if (!_loaded) { + _loaded = true; + crl::on_main(this, [=] { + updatePinnedVisibility(); + }); + } +} + +rpl::producer<Data::MessagesSlice> ChatWidget::listSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + if (_replies) { + return repliesSource(aroundId, limitBefore, limitAfter); + } else if (_sublist) { + return sublistSource(aroundId, limitBefore, limitAfter); + } + Unexpected("ChatWidget::listSource in unknown mode"); +} + +rpl::producer<Data::MessagesSlice> ChatWidget::repliesSource( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { @@ -2633,31 +2986,48 @@ rpl::producer<Data::MessagesSlice> RepliesWidget::listSource( limitBefore, limitAfter ) | rpl::before_next([=] { // after_next makes a copy of value. - if (!_loaded) { - _loaded = true; - crl::on_main(this, [=] { - updatePinnedVisibility(); - }); - } + markLoaded(); }); } -bool RepliesWidget::listAllowsMultiSelect() { +rpl::producer<Data::MessagesSlice> ChatWidget::sublistSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + return _sublist->source( + aroundId, + limitBefore, + limitAfter + ) | rpl::before_next([=](const Data::MessagesSlice &result) { + // after_next makes a copy of value. + _topBar->setCustomTitle(result.fullCount + ? tr::lng_forum_messages( + tr::now, + lt_count_decimal, + *result.fullCount) + : tr::lng_contacts_loading(tr::now)); + markLoaded(); + }); +} + +bool ChatWidget::listAllowsMultiSelect() { return true; } -bool RepliesWidget::listIsItemGoodForSelection( +bool ChatWidget::listIsItemGoodForSelection( not_null<HistoryItem*> item) { return item->isRegular() && !item->isService(); } -bool RepliesWidget::listIsLessInOrder( +bool ChatWidget::listIsLessInOrder( not_null<HistoryItem*> first, not_null<HistoryItem*> second) { - return first->position() < second->position(); + return _sublist + ? (first->id < second->id) + : first->position() < second->position(); } -void RepliesWidget::listSelectionChanged(SelectedItems &&items) { +void ChatWidget::listSelectionChanged(SelectedItems &&items) { HistoryView::TopBarWidget::SelectedState state; state.count = items.size(); for (const auto &item : items) { @@ -2677,27 +3047,37 @@ void RepliesWidget::listSelectionChanged(SelectedItems &&items) { } } -void RepliesWidget::listMarkReadTill(not_null<HistoryItem*> item) { - _replies->readTill(item); +void ChatWidget::listMarkReadTill(not_null<HistoryItem*> item) { + if (_replies) { + _replies->readTill(item); + } else if (_sublist) { + _sublist->readTill(item); + } } -void RepliesWidget::listMarkContentsRead( +void ChatWidget::listMarkContentsRead( const base::flat_set<not_null<HistoryItem*>> &items) { session().api().markContentsRead(items); } -MessagesBarData RepliesWidget::listMessagesBar( +MessagesBarData ChatWidget::listMessagesBar( const std::vector<not_null<Element*>> &elements) { - if (elements.empty()) { + if ((!_sublist && !_replies) || elements.empty()) { return {}; } - const auto till = _replies->computeInboxReadTillFull(); + const auto till = _replies + ? _replies->computeInboxReadTillFull() + : _sublist->computeInboxReadTillFull(); const auto hidden = (till < 2); for (auto i = 0, count = int(elements.size()); i != count; ++i) { const auto item = elements[i]->data(); if (item->isRegular() && item->id > till) { - if (item->out() || !item->replyToId()) { - _replies->readTill(item); + if (item->out() || (_replies && !item->replyToId())) { + if (_replies) { + _replies->readTill(item); + } else { + _sublist->readTill(item); + } } else { return { .bar = { @@ -2713,10 +3093,10 @@ MessagesBarData RepliesWidget::listMessagesBar( return {}; } -void RepliesWidget::listContentRefreshed() { +void ChatWidget::listContentRefreshed() { } -void RepliesWidget::listUpdateDateLink( +void ChatWidget::listUpdateDateLink( ClickHandlerPtr &link, not_null<Element*> view) { if (!_topic) { @@ -2731,17 +3111,19 @@ void RepliesWidget::listUpdateDateLink( } } -bool RepliesWidget::listElementHideReply(not_null<const Element*> view) { - if (const auto reply = view->data()->Get<HistoryMessageReply>()) { +bool ChatWidget::listElementHideReply(not_null<const Element*> view) { + if (_sublist) { + return false; + } else if (const auto reply = view->data()->Get<HistoryMessageReply>()) { const auto replyToPeerId = reply->externalPeerId() ? reply->externalPeerId() - : _history->peer->id; + : _peer->id; if (reply->fields().manualQuote) { return false; - } else if (replyToPeerId == _history->peer->id) { - return (reply->messageId() == _rootId); - } else if (_root) { - const auto forwarded = _root->Get<HistoryMessageForwarded>(); + } else if (replyToPeerId == _peer->id) { + return (_repliesRootId && reply->messageId() == _repliesRootId); + } else if (const auto root = _repliesRoot) { + const auto forwarded = root->Get<HistoryMessageForwarded>(); if (forwarded && forwarded->savedFromPeer && forwarded->savedFromPeer->id == replyToPeerId @@ -2753,22 +3135,29 @@ bool RepliesWidget::listElementHideReply(not_null<const Element*> view) { return false; } -bool RepliesWidget::listElementShownUnread(not_null<const Element*> view) { - return _replies->isServerSideUnread(view->data()); +bool ChatWidget::listElementShownUnread(not_null<const Element*> view) { + const auto item = view->data(); + return _replies + ? _replies->isServerSideUnread(item) + : _sublist + ? _sublist->isServerSideUnread(item) + : item->unread(item->history()); } -bool RepliesWidget::listIsGoodForAroundPosition( +bool ChatWidget::listIsGoodForAroundPosition( not_null<const Element*> view) { return view->data()->isRegular(); } -void RepliesWidget::listSendBotCommand( +void ChatWidget::listSendBotCommand( const QString &command, const FullMsgId &context) { - sendBotCommandWithOptions(command, context, {}); + if (!_sublist || _sublist->parentChat()) { + sendBotCommandWithOptions(command, context, {}); + } } -void RepliesWidget::sendBotCommandWithOptions( +void ChatWidget::sendBotCommandWithOptions( const QString &command, const FullMsgId &context, Api::SendOptions options) { @@ -2779,14 +3168,14 @@ void RepliesWidget::sendBotCommandWithOptions( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + options, withPaymentApproved); if (!checked) { return; } const auto text = Bot::WrapCommandInChat( - _history->peer, + _peer, command, context); auto message = Api::MessageToSend(prepareSendAction(options)); @@ -2795,40 +3184,47 @@ void RepliesWidget::sendBotCommandWithOptions( finishSending(); } -void RepliesWidget::listSearch( +void ChatWidget::listSearch( const QString &query, const FullMsgId &context) { - controller()->searchMessages(query, _history); + const auto inChat = !_sublist + ? Dialogs::Key(_history) + : Data::SearchTagFromQuery(query) + ? Dialogs::Key(_sublist) + : Dialogs::Key(); + controller()->searchMessages(query, inChat); } -void RepliesWidget::listHandleViaClick(not_null<UserData*> bot) { - _composeControls->setText({ '@' + bot->username() + ' ' }); +void ChatWidget::listHandleViaClick(not_null<UserData*> bot) { + if (_canSendTexts) { + _composeControls->setText({ '@' + bot->username() + ' ' }); + } } -not_null<Ui::ChatTheme*> RepliesWidget::listChatTheme() { +not_null<Ui::ChatTheme*> ChatWidget::listChatTheme() { return _theme.get(); } -CopyRestrictionType RepliesWidget::listCopyRestrictionType( +CopyRestrictionType ChatWidget::listCopyRestrictionType( HistoryItem *item) { - return CopyRestrictionTypeFor(_history->peer, item); + return CopyRestrictionTypeFor(_peer, item); } -CopyRestrictionType RepliesWidget::listCopyMediaRestrictionType( +CopyRestrictionType ChatWidget::listCopyMediaRestrictionType( not_null<HistoryItem*> item) { - return CopyMediaRestrictionTypeFor(_history->peer, item); + return CopyMediaRestrictionTypeFor(_peer, item); } -CopyRestrictionType RepliesWidget::listSelectRestrictionType() { - return SelectRestrictionTypeFor(_history->peer); +CopyRestrictionType ChatWidget::listSelectRestrictionType() { + return SelectRestrictionTypeFor(_peer); } -auto RepliesWidget::listAllowedReactionsValue() +auto ChatWidget::listAllowedReactionsValue() -> rpl::producer<Data::AllowedReactions> { - return Data::PeerAllowedReactionsValue(_history->peer); + return Data::PeerAllowedReactionsValue(_peer); } -void RepliesWidget::listShowPremiumToast(not_null<DocumentData*> document) { +void ChatWidget::listShowPremiumToast(not_null<DocumentData*> document) { if (!_stickerToast) { _stickerToast = std::make_unique<HistoryView::StickerToast>( controller(), @@ -2838,23 +3234,25 @@ void RepliesWidget::listShowPremiumToast(not_null<DocumentData*> document) { _stickerToast->showFor(document); } -void RepliesWidget::listOpenPhoto( +void ChatWidget::listOpenPhoto( not_null<PhotoData*> photo, FullMsgId context) { - controller()->openPhoto(photo, { context, _rootId }); + controller()->openPhoto( + photo, + { context, _repliesRootId, _monoforumPeerId }); } -void RepliesWidget::listOpenDocument( +void ChatWidget::listOpenDocument( not_null<DocumentData*> document, FullMsgId context, bool showInMediaView) { controller()->openDocument( document, showInMediaView, - { context, _rootId }); + { context, _repliesRootId, _monoforumPeerId }); } -void RepliesWidget::listPaintEmpty( +void ChatWidget::listPaintEmpty( Painter &p, const Ui::ChatPaintContext &context) { if (!emptyShown()) { @@ -2865,29 +3263,29 @@ void RepliesWidget::listPaintEmpty( _emptyPainter->paint(p, context.st, width(), _scroll->height()); } -QString RepliesWidget::listElementAuthorRank(not_null<const Element*> view) { +QString ChatWidget::listElementAuthorRank(not_null<const Element*> view) { return (_topic && view->data()->from()->id == _topic->creatorId()) ? tr::lng_topic_author_badge(tr::now) : QString(); } -bool RepliesWidget::listElementHideTopicButton( +bool ChatWidget::listElementHideTopicButton( not_null<const Element*> view) { return true; } -History *RepliesWidget::listTranslateHistory() { +History *ChatWidget::listTranslateHistory() { return _history; } -void RepliesWidget::listAddTranslatedItems( +void ChatWidget::listAddTranslatedItems( not_null<TranslateTracker*> tracker) { if (_shownPinnedItem) { tracker->add(_shownPinnedItem); } } -Ui::ChatPaintContext RepliesWidget::listPreparePaintContext( +Ui::ChatPaintContext ChatWidget::listPreparePaintContext( Ui::ChatPaintContextArgs &&args) { auto context = WindowListDelegate::listPreparePaintContext( std::move(args)); @@ -2895,7 +3293,7 @@ Ui::ChatPaintContext RepliesWidget::listPreparePaintContext( return context; } -base::unique_qptr<Ui::PopupMenu> RepliesWidget::listFillSenderUserpicMenu( +base::unique_qptr<Ui::PopupMenu> ChatWidget::listFillSenderUserpicMenu( PeerId userpicPeerId) { const auto searchInEntry = _topic ? Dialogs::Key(_topic) @@ -2912,7 +3310,7 @@ base::unique_qptr<Ui::PopupMenu> RepliesWidget::listFillSenderUserpicMenu( return menu->empty() ? nullptr : std::move(menu); } -void RepliesWidget::setupEmptyPainter() { +void ChatWidget::setupEmptyPainter() { Expects(_topic != nullptr); _emptyPainter = std::make_unique<EmptyPainter>(_topic, [=] { @@ -2927,27 +3325,26 @@ void RepliesWidget::setupEmptyPainter() { }); } -void RepliesWidget::confirmDeleteSelected() { +void ChatWidget::confirmDeleteSelected() { ConfirmDeleteSelectedItems(_inner); } -void RepliesWidget::confirmForwardSelected() { +void ChatWidget::confirmForwardSelected() { ConfirmForwardSelectedItems(_inner); } -void RepliesWidget::clearSelected() { +void ChatWidget::clearSelected() { _inner->cancelSelection(); } -void RepliesWidget::setupDragArea() { +void ChatWidget::setupDragArea() { const auto filter = [=](const auto &d) { if (!_history || _composeControls->isRecording()) { return false; } - const auto peer = _history->peer; return _topic ? Data::CanSendAnyOf(_topic, Data::FilesSendRestrictions()) - : Data::CanSendAnyOf(peer, Data::FilesSendRestrictions()); + : Data::CanSendAnyOf(_peer, Data::FilesSendRestrictions()); }; const auto areas = DragArea::SetupDragAreaToContainer( this, @@ -2965,7 +3362,7 @@ void RepliesWidget::setupDragArea() { areas.photo->setDroppedCallback(droppedCallback(true)); } -void RepliesWidget::setupShortcuts() { +void ChatWidget::setupShortcuts() { Shortcuts::Requests( ) | rpl::filter([=] { return Ui::AppInFocus() @@ -2975,15 +3372,21 @@ void RepliesWidget::setupShortcuts() { }) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) { using Command = Shortcuts::Command; request->check(Command::Search, 1) && request->handle([=] { - if (!preventsClose(crl::guard(this, [=]{ searchInTopic(); }))) { - searchInTopic(); - } + searchRequested(); return true; }); }, lifetime()); } -void RepliesWidget::searchInTopic() { +void ChatWidget::searchRequested() { + if (_sublist) { + controller()->searchInChat(_sublist); + } else if (!preventsClose(crl::guard(this, [=] { searchInTopic(); }))) { + searchInTopic(); + } +} + +void ChatWidget::searchInTopic() { if (_topic) { controller()->searchInChat(_topic); } else { @@ -3001,7 +3404,7 @@ void RepliesWidget::searchInTopic() { controller(), _history, from); - _composeSearch->setTopMsgId(_rootId); + _composeSearch->setTopMsgId(_repliesRootId); update(); doSetInnerFocus(); @@ -3022,4 +3425,52 @@ void RepliesWidget::searchInTopic() { } } +bool ChatWidget::searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom) { + const auto sublist = chat.sublist(); + if (!sublist || sublist != _sublist) { + return false; + } else if (_composeSearch) { + _composeSearch->setQuery(query); + _composeSearch->setInnerFocus(); + return true; + } + _composeSearch = std::make_unique<ComposeSearch>( + this, + controller(), + _history, + sublist->sublistPeer(), + query); + + updateControlsGeometry(); + setInnerFocus(); + + _composeSearch->activations( + ) | rpl::start_with_next([=](ComposeSearch::Activation activation) { + const auto item = activation.item; + auto params = ::Window::SectionShow( + ::Window::SectionShow::Way::ClearStack); + params.highlightPart = { activation.query }; + params.highlightPartOffsetHint = kSearchQueryOffsetHint; + controller()->showPeerHistory( + item->history()->peer->id, + params, + item->fullId().msg); + }, _composeSearch->lifetime()); + + _composeSearch->destroyRequests( + ) | rpl::take( + 1 + ) | rpl::start_with_next([=] { + _composeSearch = nullptr; + + updateControlsGeometry(); + setInnerFocus(); + }, _composeSearch->lifetime()); + + return true; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h similarity index 85% rename from Telegram/SourceFiles/history/view/history_view_replies_section.h rename to Telegram/SourceFiles/history/view/history_view_chat_section.h index 3bbe7f5c59..87b4604c04 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -64,7 +64,7 @@ struct VoiceToSend; class Element; class TopBarWidget; -class RepliesMemento; +class ChatMemento; class ComposeControls; class ComposeSearch; class SendActionPainter; @@ -73,20 +73,30 @@ class TopicReopenBar; class EmptyPainter; class PinnedTracker; class TranslateBar; +class SubsectionTabs; -class RepliesWidget final +struct ChatViewId { + not_null<History*> history; + MsgId repliesRootId; + Data::SavedSublist *sublist = nullptr; + + friend inline bool operator==(ChatViewId, ChatViewId) = default; +}; + +class ChatWidget final : public Window::SectionWidget , private WindowListDelegate , private CornerButtonsDelegate { public: - RepliesWidget( + ChatWidget( QWidget *parent, not_null<Window::SessionController*> controller, - not_null<History*> history, - MsgId rootId); - ~RepliesWidget(); + ChatViewId id); + ~ChatWidget(); - [[nodiscard]] not_null<History*> history() const; + [[nodiscard]] ChatViewId id() const { + return _id; + } Dialogs::RowDescriptor activeChat() const override; bool preventsClose(Fn<void()> &&continueCallback) const override; @@ -100,6 +110,7 @@ public: bool showInternal( not_null<Window::SectionMemento*> memento, const Window::SectionShow ¶ms) override; + bool sameTypeAs(not_null<Window::SectionMemento*> memento) override; std::shared_ptr<Window::SectionMemento> createMemento() override; bool showMessage( PeerId peerId, @@ -109,12 +120,17 @@ public: Window::SectionActionResult sendBotCommand( Bot::SendCommandRequest request) override; + bool searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom = nullptr) override; + bool confirmSendingFiles(const QStringList &files) override; bool confirmSendingFiles(not_null<const QMimeData*> data) override; void setInternalState( const QRect &geometry, - not_null<RepliesMemento*> memento); + not_null<ChatMemento*> memento); // Tabbed selector management. bool pushTabbedSelectorToThirdSection( @@ -211,15 +227,27 @@ private: [[nodiscard]] bool checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn<void(int)> withPaymentApproved); + void markLoaded(); + [[nodiscard]] rpl::producer<Data::MessagesSlice> repliesSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter); + [[nodiscard]] rpl::producer<Data::MessagesSlice> sublistSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter); + void onScroll(); + void closeCurrent(); + void unreadCountUpdated(); void updateInnerVisibleArea(); void updateControlsGeometry(); void updateAdaptiveLayout(); - void saveState(not_null<RepliesMemento*> memento); - void restoreState(not_null<RepliesMemento*> memento); + void saveState(not_null<ChatMemento*> memento); + void restoreState(not_null<ChatMemento*> memento); void setReplies(std::shared_ptr<Data::RepliesList> replies); void refreshReplies(); void showAtStart(); @@ -240,12 +268,18 @@ private: void setupRootView(); void setupTopicViewer(); void subscribeToTopic(); + void subscribeToSublist(); void subscribeToPinnedMessages(); void setTopic(Data::ForumTopic *topic); + + void setupOpenChatButton(); + void setupAboutHiddenAuthor(); + void setupDragArea(); void setupShortcuts(); void setupTranslateBar(); + void searchRequested(); void searchInTopic(); void updatePinnedVisibility(); @@ -267,7 +301,7 @@ private: void chooseAttach(std::optional<bool> overrideSendImagesAsPhotos); [[nodiscard]] SendMenu::Details sendMenuDetails() const; [[nodiscard]] FullReplyTo replyTo() const; - [[nodiscard]] HistoryItem *lookupRoot() const; + [[nodiscard]] HistoryItem *lookupRepliesRoot() const; [[nodiscard]] Data::ForumTopic *lookupTopic(); [[nodiscard]] bool computeAreComments() const; void orderWidgets(); @@ -341,31 +375,45 @@ private: Api::SendOptions options, std::optional<MsgId> localMessageId); + void validateSubsectionTabs() override; void setupEmptyPainter(); void refreshJoinGroupButton(); [[nodiscard]] bool emptyShown() const; [[nodiscard]] bool showSlowmodeError(); const not_null<History*> _history; - MsgId _rootId = 0; - std::shared_ptr<Ui::ChatTheme> _theme; - HistoryItem *_root = nullptr; + const not_null<PeerData*> _peer; + ChatViewId _id; + + MsgId _repliesRootId = 0; + HistoryItem *_repliesRoot = nullptr; Data::ForumTopic *_topic = nullptr; mutable bool _newTopicDiscarded = false; - std::shared_ptr<Data::RepliesList> _replies; rpl::lifetime _repliesLifetime; rpl::variable<bool> _areComments = false; + + Data::SavedSublist *_sublist = nullptr; + PeerId _monoforumPeerId; + std::shared_ptr<SendActionPainter> _sendAction; + std::shared_ptr<Ui::ChatTheme> _theme; QPointer<ListWidget> _inner; object_ptr<TopBarWidget> _topBar; object_ptr<Ui::PlainShadow> _topBarShadow; + std::unique_ptr<Ui::RpWidget> _topBars; std::unique_ptr<ComposeControls> _composeControls; std::unique_ptr<ComposeSearch> _composeSearch; std::unique_ptr<Ui::FlatButton> _joinGroup; std::unique_ptr<Ui::FlatButton> _payForMessage; std::unique_ptr<TopicReopenBar> _topicReopenBar; + std::unique_ptr<Ui::FlatButton> _openChatButton; + std::unique_ptr<Ui::RpWidget> _aboutHiddenAuthor; std::unique_ptr<EmptyPainter> _emptyPainter; + std::unique_ptr<SubsectionTabs> _subsectionTabs; + rpl::lifetime _subsectionTabsLifetime; + rpl::lifetime _subsectionCheckLifetime; + bool _canSendTexts = false; bool _skipScrollEvent = false; bool _synteticScrollEvent = false; @@ -380,11 +428,11 @@ private: std::optional<FullMsgId> _minPinnedId; HistoryItem *_shownPinnedItem = nullptr; - std::unique_ptr<Ui::PinnedBar> _rootView; - int _rootViewHeight = 0; - bool _rootViewInited = false; - bool _rootViewInitScheduled = false; - rpl::variable<bool> _rootVisible = false; + std::unique_ptr<Ui::PinnedBar> _repliesRootView; + int _repliesRootViewHeight = 0; + bool _repliesRootViewInited = false; + bool _repliesRootViewInitScheduled = false; + rpl::variable<bool> _repliesRootVisible = false; std::unique_ptr<Ui::ScrollArea> _scroll; std::unique_ptr<HistoryView::StickerToast> _stickerToast; @@ -408,15 +456,18 @@ private: }; -class RepliesMemento final : public Window::SectionMemento { +class ChatMemento final : public Window::SectionMemento { public: - RepliesMemento( - not_null<History*> history, - MsgId rootId, + explicit ChatMemento( + ChatViewId id, MsgId highlightId = 0, const TextWithEntities &highlightPart = {}, int highlightPartOffsetHint = 0); - explicit RepliesMemento( + + struct Comments { + }; + explicit ChatMemento( + Comments, not_null<HistoryItem*> commentsItem, MsgId commentId = 0); @@ -431,11 +482,8 @@ public: Window::Column column, const QRect &geometry) override; - [[nodiscard]] not_null<History*> getHistory() const { - return _history; - } - [[nodiscard]] MsgId getRootId() const { - return _rootId; + [[nodiscard]] ChatViewId id() const { + return _id; } void setReplies(std::shared_ptr<Data::RepliesList> replies) { @@ -455,6 +503,7 @@ public: } Data::ForumTopic *topicForRemoveRequests() const override; + Data::SavedSublist *sublistForRemoveRequests() const override; [[nodiscard]] not_null<ListMemento*> list() { return &_list; @@ -472,8 +521,7 @@ public: private: void setupTopicViewer(); - const not_null<History*> _history; - MsgId _rootId = 0; + ChatViewId _id; const TextWithEntities _highlightPart; const int _highlightPartOffsetHint = 0; const MsgId _highlightId = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 13e5ce6786..f5710124bb 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -1222,10 +1222,13 @@ void PaysStatus::setupHandlers() { ) | rpl::start_with_next([=] { const auto user = _user; const auto exception = [=](bool refund) { - using Flag = MTPaccount_AddNoPaidMessagesException::Flag; + using Flag = MTPaccount_ToggleNoPaidMessagesException::Flag; const auto api = &user->session().api(); - api->request(MTPaccount_AddNoPaidMessagesException( - MTP_flags(refund ? Flag::f_refund_charged : Flag()), + const auto require = false; + api->request(MTPaccount_ToggleNoPaidMessagesException( + MTP_flags((refund ? Flag::f_refund_charged : Flag()) + | (require ? Flag::f_require_payment : Flag())), + MTPInputPeer(), // parent_peer // #TODO monoforum user->inputUser )).done([=] { user->clearPaysPerMessage(); @@ -1268,6 +1271,8 @@ void PaysStatus::setupHandlers() { }, box->lifetime()); user->session().api().request(MTPaccount_GetPaidMessagesRevenue( + MTP_flags(0), + MTPInputPeer(), // parent_peer // #TODO monoforum user->inputUser )).done(crl::guard(_inner, [=]( const MTPaccount_PaidMessagesRevenue &result) { diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 6ab10c6a2e..859608d249 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -118,7 +118,8 @@ bool HasEditMessageAction( || (context != Context::History && context != Context::Replies && context != Context::ShortcutMessages - && context != Context::ScheduledTopic)) { + && context != Context::ScheduledTopic + && context != Context::Monoforum)) { return false; } const auto peer = item->history()->peer; @@ -630,7 +631,9 @@ bool AddReplyToMessageAction( const auto peer = item ? item->history()->peer.get() : nullptr; if (!item || !item->isRegular() - || (context != Context::History && context != Context::Replies)) { + || (context != Context::History + && context != Context::Replies + && context != Context::Monoforum)) { return false; } const auto canSendReply = topic @@ -657,6 +660,30 @@ bool AddReplyToMessageAction( return true; } +bool AddTodoListAction( + not_null<Ui::PopupMenu*> menu, + const ContextMenuRequest &request, + not_null<ListWidget*> list) { + const auto context = list->elementContext(); + const auto item = request.item; + if (!item + || !Window::PeerMenuShowAddTodoListTasks(item) + || (context != Context::History + && context != Context::Replies + && context != Context::Monoforum + && context != Context::Pinned)) { + return false; + } + const auto itemId = item->fullId(); + const auto controller = list->controller(); + menu->addAction(tr::lng_todo_add_title(tr::now), [=] { + if (const auto item = controller->session().data().message(itemId)) { + Window::PeerMenuAddTodoListTasks(controller, item); + } + }, &st::menuIconCreateTodoList); + return true; +} + bool AddViewRepliesAction( not_null<Ui::PopupMenu*> menu, const ContextMenuRequest &request, @@ -761,8 +788,12 @@ bool AddPinMessageAction( return false; } const auto topic = item->topic(); + const auto sublist = item->savedSublist(); if (context != Context::History && context != Context::Pinned) { - if (context != Context::Replies || !topic) { + if ((context != Context::Replies || !topic) + && (context != Context::Monoforum + || !sublist + || !item->history()->amMonoforumAdmin())) { return false; } } @@ -1167,6 +1198,97 @@ void ShowWhoReadInfo( controller->showSection(std::move(memento)); } +[[nodiscard]] rpl::producer<not_null<UserData*>> LookupMessageAuthor( + not_null<HistoryItem*> item) { + struct Author { + UserData *user = nullptr; + std::vector<Fn<void(UserData*)>> callbacks; + }; + struct Authors { + base::flat_map<FullMsgId, Author> map; + }; + static auto Cache = base::flat_map<not_null<Main::Session*>, Authors>(); + + const auto channel = item->history()->peer->asChannel(); + const auto session = &channel->session(); + const auto id = item->fullId(); + if (!Cache.contains(session)) { + Cache.emplace(session); + session->lifetime().add([session] { + Cache.remove(session); + }); + } + + return [channel, id](auto consumer) { + const auto session = &channel->session(); + auto &map = Cache[session].map; + auto i = map.find(id); + if (i == end(map)) { + i = map.emplace(id).first; + const auto finishWith = [=](UserData *user) { + auto &entry = Cache[session].map[id]; + entry.user = user; + for (const auto &callback : base::take(entry.callbacks)) { + callback(user); + } + }; + session->api().request(MTPchannels_GetMessageAuthor( + channel->inputChannel, + MTP_int(id.msg.bare) + )).done([=](const MTPUser &result) { + finishWith(session->data().processUser(result)); + }).fail([=] { + finishWith(nullptr); + }).send(); + } else if (const auto user = i->second.user + ; user || i->second.callbacks.empty()) { + if (user) { + consumer.put_next(not_null(user)); + } + return rpl::lifetime(); + } + + auto lifetime = rpl::lifetime(); + const auto done = [=](UserData *result) { + if (result) { + consumer.put_next(not_null(result)); + } + }; + const auto guard = lifetime.make_state<base::has_weak_ptr>(); + i->second.callbacks.push_back(crl::guard(guard, done)); + return lifetime; + }; +} + +[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeMessageAuthorAction( + not_null<Ui::PopupMenu*> menu, + not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller) { + const auto parent = menu->menu(); + const auto user = std::make_shared<UserData*>(nullptr); + const auto action = Ui::Menu::CreateAction( + parent, + tr::lng_contacts_loading(tr::now), + [=] { if (*user) { controller->showPeerInfo(*user); } }); + action->setDisabled(true); + auto lifetime = LookupMessageAuthor( + item + ) | rpl::start_with_next([=](not_null<UserData*> author) { + action->setText( + tr::lng_context_sent_by(tr::now, lt_user, author->name())); + action->setDisabled(false); + *user = author; + }); + auto result = base::make_unique_q<Ui::Menu::Action>( + menu->menu(), + st::whoSentItem, + action, + nullptr, + nullptr); + result->lifetime().add(std::move(lifetime)); + return result; +} + } // namespace ContextMenuRequest::ContextMenuRequest( @@ -1202,6 +1324,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu( st::popupMenuWithIcons); AddReplyToMessageAction(result, request, list); + AddTodoListAction(result, request, list); if (request.overSelection && !list->hasCopyRestrictionForSelected() @@ -1305,7 +1428,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu( if (hasWhoReactedItem) { AddWhoReactedAction(result, list, item, list->controller()); } else if (item) { - MaybeAddWhenEditedForwardedAction(result, item); + MaybeAddWhenEditedForwardedAction(result, item, list->controller()); } return result; @@ -1479,9 +1602,10 @@ void AddSaveSoundForNotifications( }, &st::menuIconSoundAdd); } -void AddWhenEditedForwardedActionHelper( +void AddWhenEditedForwardedAuthorActionHelper( not_null<Ui::PopupMenu*> menu, not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller, bool insertSeparator) { if (const auto forwarded = item->Get<HistoryMessageForwarded>()) { if (!forwarded->story && forwarded->psaType.isEmpty()) { @@ -1502,6 +1626,12 @@ void AddWhenEditedForwardedActionHelper( Api::WhenEdited(item->from(), edited->date))); } } + if (item->canLookupMessageAuthor()) { + if (insertSeparator && !menu->empty()) { + menu->addSeparator(&st::expandedMenuSeparator); + } + menu->addAction(MakeMessageAuthorAction(menu, item, controller)); + } } void AddWhoReactedAction( @@ -1557,7 +1687,11 @@ void AddWhoReactedAction( menu->addSeparator(&st::expandedMenuSeparator); } if (item->history()->peer->isUser()) { - AddWhenEditedForwardedActionHelper(menu, item, false); + AddWhenEditedForwardedAuthorActionHelper( + menu, + item, + controller, + false); menu->addAction(Ui::WhenReadContextAction( menu.get(), Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds), @@ -1569,14 +1703,19 @@ void AddWhoReactedAction( Data::ReactedMenuFactory(&controller->session()), participantChosen, showAllChosen)); - AddWhenEditedForwardedActionHelper(menu, item, true); + AddWhenEditedForwardedAuthorActionHelper( + menu, + item, + controller, + true); } } void MaybeAddWhenEditedForwardedAction( not_null<Ui::PopupMenu*> menu, - not_null<HistoryItem*> item) { - AddWhenEditedForwardedActionHelper(menu, item, true); + not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller) { + AddWhenEditedForwardedAuthorActionHelper(menu, item, controller, true); } void AddEditTagAction( diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 0e14fa8034..fb26345500 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -88,7 +88,8 @@ void AddWhoReactedAction( not_null<Window::SessionController*> controller); void MaybeAddWhenEditedForwardedAction( not_null<Ui::PopupMenu*> menu, - not_null<HistoryItem*> item); + not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller); void ShowWhoReactedMenu( not_null<base::unique_qptr<Ui::PopupMenu>*> menu, QPoint position, diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 532d1d94f7..a6f6f5ac78 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_service_message.h" #include "history/view/history_view_message.h" +#include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_similar_channels.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" #include "history/view/media/history_view_custom_emoji.h" +#include "history/view/media/history_view_suggest_decision.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions.h" #include "history/view/history_view_cursor_state.h" @@ -41,6 +43,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/rect.h" #include "data/components/sponsored_messages.h" +#include "data/data_channel.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" @@ -267,8 +271,8 @@ void DefaultElementDelegate::elementHandleViaClick( not_null<UserData*> bot) { } -bool DefaultElementDelegate::elementIsChatWide() { - return false; +ElementChatMode DefaultElementDelegate::elementChatMode() { + return ElementChatMode::Default; } void DefaultElementDelegate::elementReplyTo(const FullReplyTo &to) { @@ -420,11 +424,10 @@ void UnreadBar::paint( const PaintContext &context, int y, int w, - bool chatWide) const { + ElementChatMode mode) const { if (AyuFeatures::MessageShot::isTakingShot()) { return; } - const auto previousTranslation = p.transform().dx(); if (previousTranslation != 0) { p.translate(-previousTranslation, 0); @@ -448,7 +451,7 @@ void UnreadBar::paint( p.setPen(st->historyUnreadBarFg()); int maxwidth = w; - if (chatWide) { + if (mode == ElementChatMode::Wide) { maxwidth = qMin( maxwidth, st::msgMaxWidth @@ -491,45 +494,187 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } -void ServicePreMessage::init(PreparedServiceText string) { +void MonoforumSenderBar::init( + not_null<PeerData*> parentChat, + not_null<PeerData*> peer) { + sender = peer; + text.setText(st::semiboldTextStyle, peer->name()); + const auto skip = st::monoforumBarUserpicSkip; + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + width = skip + userpic + skip * 2 + text.maxWidth() + st::msgServicePadding.right(); +} + +int MonoforumSenderBar::height() const { + return st::msgServiceMargin.top() + + st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + + st::msgServiceMargin.bottom(); +} + +void MonoforumSenderBar::paint( + Painter &p, + not_null<const Ui::ChatStyle*> st, + int y, + int w, + bool chatWide, + bool skipPatternLine) const { + Paint(p, st, sender, text, width, view, y, w, chatWide, skipPatternLine); +} + +void MonoforumSenderBar::PaintFor( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<Element*> itemView, + Ui::PeerUserpicView &userpicView, + int y, + int w, + bool chatWide) { + const auto sublist = itemView->data()->savedSublist(); + const auto sender = (sublist && sublist->parentChat()) + ? sublist->sublistPeer().get() + : nullptr; + if (!sender || sender->isMonoforum()) { + return; + } + auto text = Ui::Text::String(st::semiboldTextStyle, sender->name()); + const auto skip = st::monoforumBarUserpicSkip; + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + const auto width = skip + + userpic + + skip * 2 + + text.maxWidth() + + st::msgServicePadding.right(); + Paint(p, st, sender, text, width, userpicView, y, w, chatWide, true); +} + +void MonoforumSenderBar::Paint( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<PeerData*> sender, + const Ui::Text::String &text, + int width, + Ui::PeerUserpicView &view, + int y, + int w, + bool chatWide, + bool skipPatternLine) { + Expects(sender != nullptr); + + int left = st::msgServiceMargin.left(); + const auto maxwidth = chatWide + ? std::min(w, WideChatWidth()) + : w; + w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); + + const auto use = std::min(w, width); + + left += (w - use) / 2; + int h = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); + ServiceMessagePainter::PaintBubble( + p, + st->msgServiceBg(), + st->serviceBgCornersNormal(), + QRect(left, y + st::msgServiceMargin.top(), use, h)); + + const auto skip = st::monoforumBarUserpicSkip; + if (!skipPatternLine) { + auto pen = st->msgServiceBg()->p; + pen.setWidthF(skip); + pen.setCapStyle(Qt::RoundCap); + pen.setDashPattern({ 2., 2. }); + p.setPen(pen); + const auto top = y + st::msgServiceMargin.top() + (h / 2); + p.drawLine(0, top, left, top); + p.drawLine(left + use, top, 2 * w, top); + } + + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + const auto available = use - (skip + userpic + skip * 2 + st::msgServicePadding.right()); + + sender->paintUserpic(p, view, left + skip, y + st::msgServiceMargin.top() + skip, userpic); + + p.setFont(st::msgServiceFont); + p.setPen(st->msgServiceFg()); + text.draw(p, { + .position = { + left + skip + userpic + skip * 2, + y + st::msgServiceMargin.top() + st::msgServicePadding.top(), + }, + .availableWidth = available, + .elisionLines = 1, + }); +} + +void ServicePreMessage::init( + not_null<Element*> view, + PreparedServiceText string, + ClickHandlerPtr fullClickHandler, + std::unique_ptr<Media> media) { text = Ui::Text::String( st::serviceTextStyle, string.text, kMarkupTextOptions, - st::msgMinWidth); + st::msgMinWidth, + Core::TextContext({ + .session = &view->history()->session(), + .repaint = [=] { view->customEmojiRepaint(); }, + })); + handler = std::move(fullClickHandler); for (auto i = 0; i != int(string.links.size()); ++i) { text.setLink(i + 1, string.links[i]); } + this->media = std::move(media); } -int ServicePreMessage::resizeToWidth(int newWidth, bool chatWide) { +int ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) { width = newWidth; - if (chatWide) { + if (mode == ElementChatMode::Wide) { accumulate_min( width, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } - auto contentWidth = width; - contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins - if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { - contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + + if (media) { + media->initDimensions(); + media->resizeGetHeight(width); } - auto maxWidth = text.maxWidth() - + st::msgServicePadding.left() - + st::msgServicePadding.right(); - auto minHeight = text.minHeight(); + if (media && media->hideServiceText()) { + height = media->height() + st::msgServiceMargin.bottom(); + } else { + auto contentWidth = width; + contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.right(); + if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { + contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + } + + auto maxWidth = text.maxWidth() + + st::msgServicePadding.left() + + st::msgServicePadding.right(); + auto minHeight = text.minHeight(); + + auto nwidth = qMax(contentWidth + - st::msgServicePadding.left() + - st::msgServicePadding.right(), 0); + height = (contentWidth >= maxWidth) + ? minHeight + : text.countHeight(nwidth); + height += st::msgServicePadding.top() + + st::msgServicePadding.bottom() + + st::msgServiceMargin.top() + + st::msgServiceMargin.bottom(); + } - auto nwidth = qMax(contentWidth - - st::msgServicePadding.left() - - st::msgServicePadding.right(), 0); - height = (contentWidth >= maxWidth) - ? minHeight - : text.countHeight(nwidth); - height += st::msgServicePadding.top() - + st::msgServicePadding.bottom() - + st::msgServiceMargin.top() - + st::msgServiceMargin.bottom(); return height; } @@ -537,42 +682,59 @@ void ServicePreMessage::paint( Painter &p, const PaintContext &context, QRect g, - bool chatWide) const { - const auto top = g.top() - height - st::msgMargin.top(); - p.translate(0, top); + ElementChatMode mode) const { + if (media && media->hideServiceText()) { + const auto left = (width - media->width()) / 2; + const auto top = g.top() - height - st::msgMargin.bottom(); + const auto position = QPoint(left, top); + p.translate(position); + media->draw(p, context.selected() + ? context.translated(-position) + : context.translated(-position).withSelection({})); + p.translate(-position); + } else { + const auto top = g.top() - height - st::msgMargin.top(); + p.translate(0, top); - const auto rect = QRect(0, 0, width, height) - - st::msgServiceMargin; - const auto trect = rect - st::msgServicePadding; + const auto rect = QRect(0, 0, width, height) + - st::msgServiceMargin; + const auto trect = rect - st::msgServicePadding; - ServiceMessagePainter::PaintComplexBubble( - p, - context.st, - rect.left(), - rect.width(), - text, - trect); + ServiceMessagePainter::PaintComplexBubble( + p, + context.st, + rect.left(), + rect.width(), + text, + trect); - p.setBrush(Qt::NoBrush); - p.setPen(context.st->msgServiceFg()); - p.setFont(st::msgServiceFont); - text.draw(p, { - .position = trect.topLeft(), - .availableWidth = trect.width(), - .align = style::al_top, - .palette = &context.st->serviceTextPalette(), - .now = context.now, - .fullWidthSelection = false, - //.selection = context.selection, - }); + p.setBrush(Qt::NoBrush); + p.setPen(context.st->msgServiceFg()); + p.setFont(st::msgServiceFont); + text.draw(p, { + .position = trect.topLeft(), + .availableWidth = trect.width(), + .align = style::al_top, + .palette = &context.st->serviceTextPalette(), + .now = context.now, + .fullWidthSelection = false, + //.selection = context.selection, + }); - p.translate(0, -top); + p.translate(0, -top); + } } ClickHandlerPtr ServicePreMessage::textState( QPoint point, const StateRequest &request, QRect g) const { + if (media && media->hideServiceText()) { + const auto left = (width - media->width()) / 2; + const auto top = g.top() - height - st::msgMargin.bottom(); + const auto position = QPoint(left, top); + return media->textState(point - position, request).link; + } const auto top = g.top() - height - st::msgMargin.top(); const auto rect = QRect(0, top, width, height) - st::msgServiceMargin; @@ -580,10 +742,16 @@ ClickHandlerPtr ServicePreMessage::textState( if (trect.contains(point)) { auto textRequest = request.forText(); textRequest.align = style::al_center; - return text.getState( + const auto link = text.getState( point - trect.topLeft(), trect.width(), textRequest).link; + if (link) { + return link; + } + } + if (handler && rect.contains(point)) { + return handler; } return {}; } @@ -884,7 +1052,8 @@ not_null<PurchasedTag*> Element::enforcePurchasedTag() { int Element::AdditionalSpaceForSelectionCheckbox( not_null<const Element*> view, QRect countedGeometry) { - if (!view->hasOutLayout() || view->delegate()->elementIsChatWide()) { + if (!view->hasOutLayout() + || view->delegate()->elementChatMode() == ElementChatMode::Wide) { return 0; } if (countedGeometry.isEmpty()) { @@ -959,6 +1128,16 @@ void Element::refreshMedia(Element *replacing) { this, std::make_unique<LargeEmoji>(this, emoji)); } + } else if (const auto decision = item->Get<HistoryServiceSuggestDecision>()) { + _media = std::make_unique<MediaGeneric>( + this, + GenerateSuggestDecisionMedia(this, decision), + MediaGenericDescriptor{ + .maxWidth = st::chatSuggestInfoWidth, + .fullAreaLink = decision->lnk, + .service = true, + .hideServiceText = true, + }); } else { _media = nullptr; } @@ -1178,6 +1357,16 @@ void Element::validateText() { ? _textItem->customTextLinks() : contextDependentText.links; setTextWithLinks(markedText, customLinks); + + if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + if (!done->completed.empty() && !done->incompleted.empty()) { + setServicePreMessage( + item->composeTodoIncompleted(done), + done->lnk); + } else { + setServicePreMessage({}); + } + } } else { const auto unavailable = item->computeUnavailableReason(); if (!unavailable.isEmpty()) { @@ -1240,6 +1429,7 @@ void Element::validateTextSkipBlock(bool has, int width, int height) { } void Element::previousInBlocksChanged() { + recountMonoforumSenderBarInBlocks(); recountDisplayDateInBlocks(); recountAttachToPreviousInBlocks(); } @@ -1275,7 +1465,8 @@ bool Element::computeIsAttachToPrevious(not_null<Element*> previous) { const auto item = data(); if (!Has<DateBadge>() && !Has<UnreadBar>() - && !Has<ServicePreMessage>()) { + && !Has<ServicePreMessage>() + && !Has<MonoforumSenderBar>()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) @@ -1391,6 +1582,14 @@ bool Element::isInOneDayWithPrevious() const { return !data()->isEmpty() && !displayDate(); } +bool Element::displayMonoforumSender() const { + return Has<MonoforumSenderBar>(); +} + +bool Element::isInOneBunchWithPrevious() const { + return !data()->isEmpty() && !displayMonoforumSender(); +} + void Element::recountAttachToPreviousInBlocks() { if (isHidden() || data()->isEmpty()) { if (const auto next = nextDisplayedInBlocks()) { @@ -1409,6 +1608,37 @@ void Element::recountAttachToPreviousInBlocks() { setAttachToPrevious(attachToPrevious, previous); } +void Element::recountMonoforumSenderBarInBlocks() { + const auto item = data(); + const auto sublist = item->savedSublist(); + const auto parentChat = sublist ? sublist->parentChat() : nullptr; + const auto barPeer = [&]() -> PeerData* { + if (!parentChat + || isHidden() + || item->isEmpty() + || item->isSponsored()) { + return nullptr; + } + const auto sublistPeer = sublist->sublistPeer(); + if (const auto previous = previousDisplayedInBlocks()) { + const auto prev = previous->data(); + if (const auto prevSublist = prev->savedSublist()) { + Assert(prevSublist->parentChat() == parentChat); + if (prevSublist->sublistPeer() == sublistPeer) { + return nullptr; + } + } + } + return (sublistPeer == parentChat) ? nullptr : sublistPeer.get(); + }(); + if (barPeer && !Has<MonoforumSenderBar>()) { + AddComponents(MonoforumSenderBar::Bit()); + Get<MonoforumSenderBar>()->init(parentChat, barPeer); + } else if (!barPeer && Has<MonoforumSenderBar>()) { + RemoveComponents(MonoforumSenderBar::Bit()); + } +} + void Element::recountDisplayDateInBlocks() { setDisplayDate([&] { const auto item = data(); @@ -1465,11 +1695,18 @@ void Element::setDisplayDate(bool displayDate) { } } -void Element::setServicePreMessage(PreparedServiceText text) { - if (!text.text.empty()) { +void Element::setServicePreMessage( + PreparedServiceText text, + ClickHandlerPtr fullClickHandler, + std::unique_ptr<Media> media) { + if (!text.text.empty() || media) { AddComponents(ServicePreMessage::Bit()); const auto service = Get<ServicePreMessage>(); - service->init(std::move(text)); + service->init( + this, + std::move(text), + std::move(fullClickHandler), + std::move(media)); setPendingResize(); } else if (Has<ServicePreMessage>()) { RemoveComponents(ServicePreMessage::Bit()); @@ -1558,7 +1795,8 @@ bool Element::hasOutLayout() const { } bool Element::hasRightLayout() const { - return hasOutLayout() && !_delegate->elementIsChatWide(); + return hasOutLayout() + && (_delegate->elementChatMode() != ElementChatMode::Wide); } bool Element::drawBubble() const { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index af62b3f3b6..6f7a4edea0 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/runtime_composer.h" #include "base/flags.h" #include "base/weak_ptr.h" +#include "ui/userpic_view.h" class History; class HistoryBlock; @@ -58,6 +59,7 @@ enum class Context : char { Pinned, AdminLog, ContactPreview, + Monoforum, SavedSublist, TTLViewer, ShortcutMessages, @@ -76,6 +78,12 @@ struct SelectionModeResult { float64 progress = 0.0; }; +enum class ElementChatMode : char { + Default, + Wide, + Narrow, // monoforum with left tabs +}; + class Element; class ElementDelegate { public: @@ -112,7 +120,7 @@ public: const QString &query, const FullMsgId &context) = 0; virtual void elementHandleViaClick(not_null<UserData*> bot) = 0; - virtual bool elementIsChatWide() = 0; + virtual ElementChatMode elementChatMode() = 0; virtual not_null<Ui::PathShiftGradient*> elementPathShiftGradient() = 0; virtual void elementReplyTo(const FullReplyTo &to) = 0; virtual void elementStartInteraction(not_null<const Element*> view) = 0; @@ -167,7 +175,7 @@ public: const QString &query, const FullMsgId &context) override; void elementHandleViaClick(not_null<UserData*> bot) override; - bool elementIsChatWide() override; + ElementChatMode elementChatMode() override; void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction(not_null<const Element*> view) override; void elementStartPremium( @@ -220,7 +228,7 @@ QString DateTooltipText(not_null<Element*> view); // Any HistoryView::Element can have this Component for // displaying the unread messages bar above the message. -struct UnreadBar : public RuntimeComponent<UnreadBar, Element> { +struct UnreadBar : RuntimeComponent<UnreadBar, Element> { void init(const QString &string); static int height(); @@ -231,7 +239,7 @@ struct UnreadBar : public RuntimeComponent<UnreadBar, Element> { const PaintContext &context, int y, int w, - bool chatWide) const; + ElementChatMode mode) const; QString text; int width = 0; @@ -241,7 +249,7 @@ struct UnreadBar : public RuntimeComponent<UnreadBar, Element> { // Any HistoryView::Element can have this Component for // displaying the day mark above the message. -struct DateBadge : public RuntimeComponent<DateBadge, Element> { +struct DateBadge : RuntimeComponent<DateBadge, Element> { void init(const QString &date); int height() const; @@ -257,31 +265,77 @@ struct DateBadge : public RuntimeComponent<DateBadge, Element> { }; +struct MonoforumSenderBar : RuntimeComponent<MonoforumSenderBar, Element> { + void init(not_null<PeerData*> parentChat, not_null<PeerData*> peer); + + int height() const; + void paint( + Painter &p, + not_null<const Ui::ChatStyle*> st, + int y, + int w, + bool chatWide, + bool skipPatternLine) const; + static void PaintFor( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<Element*> itemView, + Ui::PeerUserpicView &userpicView, + int y, + int w, + bool chatWide); + + PeerData *sender = nullptr; + Ui::Text::String text; + ClickHandlerPtr link; + mutable Ui::PeerUserpicView view; + int width = 0; + +private: + static void Paint( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<PeerData*> sender, + const Ui::Text::String &text, + int width, + Ui::PeerUserpicView &view, + int y, + int w, + bool chatWide, + bool skipPatternLine); + +}; + // Any HistoryView::Element can have this Component for // displaying some text in layout of a service message above the message. -struct ServicePreMessage - : public RuntimeComponent<ServicePreMessage, Element> { - void init(PreparedServiceText string); +struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> { + void init( + not_null<Element*> view, + PreparedServiceText string, + ClickHandlerPtr fullClickHandler, + std::unique_ptr<Media> media = nullptr); - int resizeToWidth(int newWidth, bool chatWide); + int resizeToWidth(int newWidth, ElementChatMode mode); void paint( Painter &p, const PaintContext &context, QRect g, - bool chatWide) const; + ElementChatMode mode) const; [[nodiscard]] ClickHandlerPtr textState( QPoint point, const StateRequest &request, QRect g) const; + std::unique_ptr<Media> media; Ui::Text::String text; + ClickHandlerPtr handler; int width = 0; int height = 0; }; -struct FakeBotAboutTop : public RuntimeComponent<FakeBotAboutTop, Element> { +struct FakeBotAboutTop : RuntimeComponent<FakeBotAboutTop, Element> { void init(); Ui::Text::String text; @@ -289,7 +343,7 @@ struct FakeBotAboutTop : public RuntimeComponent<FakeBotAboutTop, Element> { int height = 0; }; -struct PurchasedTag : public RuntimeComponent<PurchasedTag, Element> { +struct PurchasedTag : RuntimeComponent<PurchasedTag, Element> { Ui::Text::String text; }; @@ -408,7 +462,10 @@ public: // For blocks context this should be called only from recountDisplayDate(). void setDisplayDate(bool displayDate); - void setServicePreMessage(PreparedServiceText text); + void setServicePreMessage( + PreparedServiceText text, + ClickHandlerPtr fullClickHandler = nullptr, + std::unique_ptr<Media> media = nullptr); bool computeIsAttachToPrevious(not_null<Element*> previous); @@ -419,6 +476,9 @@ public: [[nodiscard]] bool displayDate() const; [[nodiscard]] bool isInOneDayWithPrevious() const; + [[nodiscard]] bool displayMonoforumSender() const; + [[nodiscard]] bool isInOneBunchWithPrevious() const; + virtual void draw(Painter &p, const PaintContext &context) const = 0; [[nodiscard]] virtual PointState pointState(QPoint point) const = 0; [[nodiscard]] virtual TextState textState( @@ -629,14 +689,17 @@ protected: std::unique_ptr<Reactions::InlineList> _reactions; private: + void recountMonoforumSenderBarInBlocks(); + // This should be called only from previousInBlocksChanged() // to add required bits to the Composer mask // after that always use Has<DateBadge>(). void recountDisplayDateInBlocks(); // This should be called only from previousInBlocksChanged() or when - // DateBadge or UnreadBar bit is changed in the Composer mask - // then the result should be cached in a client side flag + // DateBadge or UnreadBar or MonoforumSenderBar bit + // is changed in the Composer mask then the result + // should be cached in a client side flag // HistoryView::Element::Flag::AttachedToPrevious. void recountAttachToPreviousInBlocks(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 94d949ee25..b7477cddfa 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1906,8 +1906,10 @@ void ListWidget::elementHandleViaClick(not_null<UserData*> bot) { _delegate->listHandleViaClick(bot); } -bool ListWidget::elementIsChatWide() { - return _overrideIsChatWide.value_or(_isChatWide); +ElementChatMode ListWidget::elementChatMode() { + return _overrideChatMode.value_or(_isChatWide + ? ElementChatMode::Wide + : ElementChatMode::Default); } not_null<Ui::PathShiftGradient*> ListWidget::elementPathShiftGradient() { @@ -4294,8 +4296,8 @@ void ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) { } } -void ListWidget::overrideIsChatWide(bool isWide) { - _overrideIsChatWide = isWide; +void ListWidget::overrideChatMode(std::optional<ElementChatMode> mode) { + _overrideChatMode = mode; } ListWidget::~ListWidget() { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 71db29cd53..8f4a354b8c 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -428,7 +428,7 @@ public: const QString &query, const FullMsgId &context) override; void elementHandleViaClick(not_null<UserData*> bot) override; - bool elementIsChatWide() override; + ElementChatMode elementChatMode() override; not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override; void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction(not_null<const Element*> view) override; @@ -443,7 +443,7 @@ public: bool elementHideTopicButton(not_null<const Element*> view) override; void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w); - void overrideIsChatWide(bool isWide); + void overrideChatMode(std::optional<ElementChatMode> mode); ~ListWidget(); @@ -834,7 +834,7 @@ private: bool _refreshingViewer = false; bool _showFinished = false; bool _resizePending = false; - std::optional<bool> _overrideIsChatWide; + std::optional<ElementChatMode> _overrideChatMode; // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 74e4d1831d..3dde8462fc 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -7,12 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_message.h" +#include "api/api_suggest_post.h" +#include "base/unixtime.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "core/ui_integration.h" #include "history/view/history_view_cursor_state.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" +#include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_web_page.h" +#include "history/view/media/history_view_suggest_decision.h" #include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_group_call_bar.h" // UserpicInRow. @@ -23,11 +27,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "ui/effects/glare.h" #include "ui/effects/reaction_fly_animation.h" -#include "ui/rect.h" -#include "ui/round_rect.h" #include "ui/text/text_utilities.h" #include "ui/text/text_extended_data.h" #include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/round_rect.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" @@ -422,7 +426,9 @@ Message::Message( , _bottomInfo( &data->history()->owner().reactions(), BottomInfoDataFromMessage(this)) { - if (const auto media = data->media()) { + if (data->Get<HistoryMessageSuggestedPost>()) { + _hideReply = 1; + } else if (const auto media = data->media()) { if (media->giveawayResults()) { _hideReply = 1; } @@ -456,9 +462,33 @@ Message::~Message() { } } +void Message::refreshSuggestedInfo( + not_null<HistoryItem*> item, + not_null<const HistoryMessageSuggestedPost*> suggest, + const HistoryMessageReply *replyData) { + const auto link = (replyData && replyData->resolvedMessage) + ? JumpToMessageClickHandler( + replyData->resolvedMessage.get(), + item->fullId()) + : ClickHandlerPtr(); + setServicePreMessage({}, link, std::make_unique<MediaGeneric>( + this, + GenerateSuggestRequestMedia(this, suggest), + MediaGenericDescriptor{ + .maxWidth = st::chatSuggestWidth, + .fullAreaLink = link, + .service = true, + .hideServiceText = true, + })); +} + void Message::initPaidInformation() { const auto item = data(); - if (!item->history()->peer->isUser()) { + if (item->history()->peer->isMonoforum()) { + if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { + const auto replyData = item->Get<HistoryMessageReply>(); + refreshSuggestedInfo(item, suggest, replyData); + } return; } const auto media = this->media(); @@ -839,6 +869,29 @@ QSize Message::performCountOptimalSize() { RemoveComponents(Reply::Bit()); } + if (item->history()->peer->isMonoforum()) { + if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { + if (const auto service = Get<ServicePreMessage>()) { + // Ok, we didn't have the message, but now we have. + // That means this is not a plain post suggestion, + // but a suggestion of changes to previous suggestion. + if (service->media + && !service->handler + && replyData + && replyData->resolvedMessage) { + refreshSuggestedInfo(item, suggest, replyData); + } + } + } + } + + if (const auto postSender = item->discussionPostOriginalSender()) { + if (!postSender->isFullLoaded()) { + // We need it for available reactions list. + postSender->updateFull(); + } + } + const auto factcheck = item->Get<HistoryMessageFactcheck>(); if (factcheck && !factcheck->data.text.empty()) { AddComponents(Factcheck::Bit()); @@ -1118,6 +1171,9 @@ int Message::marginTop() const { result += bar->height(); } } + if (const auto monoforumBar = Get<MonoforumSenderBar>()) { + result += monoforumBar->height(); + } if (const auto service = Get<ServicePreMessage>()) { if (!AyuFeatures::MessageShot::isTakingShot()) { result += service->height; @@ -1162,24 +1218,22 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (const auto bar = Get<UnreadBar>()) { auto unreadbarh = bar->height(); - auto dateh = 0; + auto aboveh = 0; if (const auto date = Get<DateBadge>()) { - dateh = date->height(); + aboveh += date->height(); } - if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) { - p.translate(0, dateh); - bar->paint( - p, - context, - 0, - width(), - delegate()->elementIsChatWide()); - p.translate(0, -dateh); + if (const auto sender = Get<MonoforumSenderBar>()) { + aboveh += sender->height(); + } + if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { + p.translate(0, aboveh); + bar->paint(p, context, 0, width(), delegate()->elementChatMode()); + p.translate(0, -aboveh); } } if (const auto service = Get<ServicePreMessage>()) { - service->paint(p, context, g, delegate()->elementIsChatWide()); + service->paint(p, context, g, delegate()->elementChatMode()); } if (isHidden()) { @@ -1575,8 +1629,8 @@ void Message::draw(Painter &p, const PaintContext &context) const { constexpr auto kMaxHeightRatio = 3.5; constexpr auto kStrokeWidth = 2.; constexpr auto kWaveWidth = 10.; - const auto isLeftSize = (!context.outbg) - || delegate()->elementIsChatWide(); + const auto isLeftSize = !context.outbg + || (delegate()->elementChatMode() == ElementChatMode::Wide); const auto ratio = std::min(context.gestureHorizontal.ratio, 1.); const auto reachRatio = context.gestureHorizontal.reachRatio; const auto size = st::historyFastShareSize; @@ -1661,7 +1715,8 @@ void Message::draw(Painter &p, const PaintContext &context) const { } const auto o = ScopedPainterOpacity(p, progress); const auto &st = st::msgSelectionCheck; - const auto right = delegate()->elementIsChatWide() + const auto right = (delegate()->elementChatMode() + == ElementChatMode::Wide) ? std::min( int(_bubbleWidthLimit + st::msgPhotoSkip @@ -2503,6 +2558,8 @@ bool Message::hasFromPhoto() const { switch (context()) { case Context::AdminLog: return true; + case Context::Monoforum: + return (delegate()->elementChatMode() == ElementChatMode::Wide); case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -2521,8 +2578,10 @@ bool Message::hasFromPhoto() const { || item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost())) { return false; - } else if (delegate()->elementIsChatWide()) { - return true; + } + const auto mode = delegate()->elementChatMode(); + if (mode != ElementChatMode::Default) { + return (mode == ElementChatMode::Wide); } else if (item->history()->peer->isVerifyCodes()) { return !hasOutLayout(); } else if (item->Has<HistoryMessageForwarded>()) { @@ -3730,6 +3789,8 @@ bool Message::hasFromName() const { switch (context()) { case Context::AdminLog: return true; + case Context::Monoforum: + return data()->out() || data()->from()->isChannel(); case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -3888,6 +3949,9 @@ int Message::minWidthForMedia() const { accumulate_max(result, added + st::semiboldFont->width( tr::lng_replies_view_original(tr::now))); } + if (const auto keyboard = data()->inlineReplyKeyboard()) { + accumulate_max(result, keyboard->naturalWidth()); + } return result; } @@ -4000,6 +4064,8 @@ bool Message::displayFastShare() const { bool Message::displayGoToOriginal() const { if (isPinnedContext()) { return !hasOutLayout(); + } else if (context() == Context::Monoforum) { + return false; } const auto item = data(); if (const auto forwarded = item->Get<HistoryMessageForwarded>()) { @@ -4420,12 +4486,15 @@ QRect Message::countGeometry() const { ? media->width() : width(); const auto outbg = hasOutLayout(); + const auto useMoreSpace = (delegate()->elementChatMode() + == ElementChatMode::Narrow); + const auto wideSkip = useMoreSpace + ? st::msgMargin.left() + : st::msgMargin.right(); const auto availableWidth = width() - st::msgMargin.left() - - (centeredView ? st::msgMargin.left() : st::msgMargin.right()); - auto contentLeft = hasRightLayout() - ? st::msgMargin.right() - : st::msgMargin.left(); + - (centeredView ? st::msgMargin.left() : wideSkip); + auto contentLeft = hasRightLayout() ? wideSkip : st::msgMargin.left(); auto contentWidth = availableWidth; if (hasFromPhoto()) { contentLeft += st::msgPhotoSkip; @@ -4446,7 +4515,8 @@ QRect Message::countGeometry() const { contentWidth = mediaWidth; } } - if (contentWidth < availableWidth && !delegate()->elementIsChatWide()) { + if (contentWidth < availableWidth + && delegate()->elementChatMode() != ElementChatMode::Wide) { if (outbg) { contentLeft += availableWidth - contentWidth; } else if (centeredView) { @@ -4535,7 +4605,7 @@ int Message::resizeContentGetHeight(int newWidth) { auto newHeight = minHeight(); if (const auto service = Get<ServicePreMessage>()) { - service->resizeToWidth(newWidth, delegate()->elementIsChatWide()); + service->resizeToWidth(newWidth, delegate()->elementChatMode()); } const auto botTop = item->isFakeAboutView() @@ -4550,9 +4620,14 @@ int Message::resizeContentGetHeight(int newWidth) { // This code duplicates countGeometry() but also resizes media. const auto centeredView = item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost()); + const auto useMoreSpace = (delegate()->elementChatMode() + == ElementChatMode::Narrow); + const auto wideSkip = useMoreSpace + ? st::msgMargin.left() + : st::msgMargin.right(); auto contentWidth = newWidth - st::msgMargin.left() - - (centeredView ? st::msgMargin.left() : st::msgMargin.right()); + - (centeredView ? st::msgMargin.left() : wideSkip); if (hasFromPhoto()) { if (const auto size = rightActionSize()) { contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index a2996d8c38..ad3dfd226e 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -15,6 +15,8 @@ class HistoryItem; struct HistoryMessageEdited; struct HistoryMessageForwarded; struct HistoryMessageReplyMarkup; +struct HistoryMessageSuggestedPost; +struct HistoryMessageReply; namespace Data { struct ReactionId; @@ -35,8 +37,7 @@ class InlineList; } // namespace Reactions // Special type of Component for the channel actions log. -struct LogEntryOriginal - : public RuntimeComponent<LogEntryOriginal, Element> { +struct LogEntryOriginal : RuntimeComponent<LogEntryOriginal, Element> { LogEntryOriginal(); LogEntryOriginal(LogEntryOriginal &&other); LogEntryOriginal &operator=(LogEntryOriginal &&other); @@ -45,13 +46,12 @@ struct LogEntryOriginal std::unique_ptr<WebPage> page; }; -struct Factcheck -: public RuntimeComponent<Factcheck, Element> { +struct Factcheck : RuntimeComponent<Factcheck, Element> { std::unique_ptr<WebPage> page; bool expanded = false; }; -struct PsaTooltipState : public RuntimeComponent<PsaTooltipState, Element> { +struct PsaTooltipState : RuntimeComponent<PsaTooltipState, Element> { QString type; mutable ClickHandlerPtr link; mutable Ui::Animations::Simple buttonVisibleAnimation; @@ -177,6 +177,10 @@ private: bool updateBottomInfo(); void initPaidInformation(); + void refreshSuggestedInfo( + not_null<HistoryItem*> item, + not_null<const HistoryMessageSuggestedPost*> suggest, + const HistoryMessageReply *reply); void initLogEntryOriginal(); void initPsa(); void fromNameUpdated(int width) const; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 5a3006e327..16bdd701c1 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -94,6 +94,10 @@ Data::ForumTopic *PinnedMemento::topicForRemoveRequests() const { return _thread->asTopic(); } +Data::SavedSublist *PinnedMemento::sublistForRemoveRequests() const { + return _thread->asSublist(); +} + PinnedWidget::PinnedWidget( QWidget *parent, not_null<Window::SessionController*> controller, @@ -203,6 +207,7 @@ void PinnedWidget::setupClearButton() { controller(), _history->peer, _thread->topicRootId(), + _thread->monoforumPeerId(), crl::guard(this, callback)); } else { Window::UnpinAllMessages(controller(), _thread); @@ -525,6 +530,7 @@ rpl::producer<Data::MessagesSlice> PinnedWidget::listSource( SparseIdsMergedSlice::Key( _history->peer->id, _thread->topicRootId(), + _thread->monoforumPeerId(), _migratedPeer ? _migratedPeer->id : 0, messageId), Storage::SharedMediaType::Pinned), diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 75956e403d..be7a515d72 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -225,6 +225,7 @@ public: } Data::ForumTopic *topicForRemoveRequests() const override; + Data::SavedSublist *sublistForRemoveRequests() const override; private: const not_null<Data::Thread*> _thread; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp index 24abaa03fe..fcc7226566 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp @@ -86,6 +86,7 @@ void PinnedTracker::refreshViewer() { SparseIdsMergedSlice::Key( peer->id, _thread->topicRootId(), + _thread->monoforumPeerId(), _migratedPeer ? _migratedPeer->id : 0, _viewerAroundId), Storage::SharedMediaType::Pinned), diff --git a/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp b/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp index e10a50b8a7..d0ddb48baf 100644 --- a/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp @@ -109,7 +109,9 @@ rpl::producer<Ui::RequestsBarContent> RequestsBarContentByPeer( auto state = lifetime.make_state<State>(peer); const auto pushNext = [=](bool now = false) { - if ((!showInForum && peer->isForum()) + if ((!showInForum + && peer->isForum() + && !peer->asChannel()->useSubsectionTabs()) || (std::min(state->current.count, kRecentRequestsLimit) != state->users.size())) { return; diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index b5709fb505..d888247952 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -239,6 +239,8 @@ ScheduledWidget::ScheduledWidget( _composeControls->editMessage( fullId, _inner->getSelectedTextRange(item)); + } else if (media->todolist()) { + Window::PeerMenuEditTodoList(controller, item); } } }, _inner->lifetime()); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 02a4aed234..47dd11eb8d 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_abstract_structure.h" #include "data/data_chat.h" #include "data/data_channel.h" +#include "data/data_todo_list.h" #include "info/profile/info_profile_cover.h" #include "ui/chat/chat_style.h" #include "ui/effects/reaction_fly_animation.h" @@ -423,7 +424,7 @@ bool Service::consumeHorizontalScroll(QPoint position, int delta) { QRect Service::countGeometry() const { auto result = QRect(0, 0, width(), height()); - if (delegate()->elementIsChatWide()) { + if (delegate()->elementChatMode() == ElementChatMode::Wide) { result.setWidth(qMin(result.width(), st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } auto margins = st::msgServiceMargin; @@ -448,13 +449,14 @@ void Service::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { } QSize Service::performCountCurrentSize(int newWidth) { - auto newHeight = displayedDateHeight(); - if (const auto bar = Get<UnreadBar>()) { - newHeight += bar->height(); - } + auto newHeight = marginTop(); data()->resolveDependent(); + if (const auto service = Get<ServicePreMessage>()) { + service->resizeToWidth(newWidth, delegate()->elementChatMode()); + } + if (isHidden()) { return { newWidth, newHeight }; } @@ -462,11 +464,9 @@ QSize Service::performCountCurrentSize(int newWidth) { const auto mediaDisplayed = media && media->isDisplayed(); auto contentWidth = newWidth; if (mediaDisplayed && media->hideServiceText()) { - newHeight += st::msgServiceMargin.top() - + media->resizeGetHeight(newWidth) - + st::msgServiceMargin.bottom(); + newHeight += media->resizeGetHeight(newWidth) + marginBottom(); } else if (!text().isEmpty()) { - if (delegate()->elementIsChatWide()) { + if (delegate()->elementChatMode() == ElementChatMode::Wide) { accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins @@ -478,12 +478,15 @@ QSize Service::performCountCurrentSize(int newWidth) { newHeight += (contentWidth >= maxWidth()) ? minHeight() : textHeightFor(nwidth); - newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom(); + newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom(); if (mediaDisplayed) { const auto mediaWidth = std::min(media->maxWidth(), nwidth); newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(mediaWidth); } + newHeight += marginBottom(); + } else { + newHeight -= st::msgServiceMargin.top(); } if (_reactions) { @@ -520,11 +523,17 @@ bool Service::isHidden() const { } int Service::marginTop() const { - auto result = st::msgServiceMargin.top(); + auto result = isHidden() ? 0 : st::msgServiceMargin.top(); result += displayedDateHeight(); if (const auto bar = Get<UnreadBar>()) { result += bar->height(); } + if (const auto monoforumBar = Get<MonoforumSenderBar>()) { + result += monoforumBar->height(); + } + if (const auto service = Get<ServicePreMessage>()) { + result += service->height; + } return result; } @@ -541,22 +550,29 @@ void Service::draw(Painter &p, const PaintContext &context) const { const auto st = context.st; if (const auto bar = Get<UnreadBar>()) { auto unreadbarh = bar->height(); - auto dateh = 0; + auto aboveh = 0; if (const auto date = Get<DateBadge>()) { - dateh = date->height(); + aboveh += date->height(); } - if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) { - p.translate(0, dateh); + if (const auto sender = Get<MonoforumSenderBar>()) { + aboveh += sender->height(); + } + if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { + p.translate(0, aboveh); bar->paint( p, context, 0, width(), - delegate()->elementIsChatWide()); - p.translate(0, -dateh); + delegate()->elementChatMode()); + p.translate(0, -aboveh); } } + if (const auto service = Get<ServicePreMessage>()) { + service->paint(p, context, g, delegate()->elementChatMode()); + } + if (isHidden()) { return; } @@ -658,6 +674,13 @@ TextState Service::textState(QPoint point, StateRequest request) const { return result; } + if (const auto service = Get<ServicePreMessage>()) { + result.link = service->textState(point, request, g); + if (result.link) { + return result; + } + } + if (_reactions) { const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); const auto reactionsLeft = 0; @@ -715,6 +738,12 @@ TextState Service::textState(QPoint point, StateRequest request) const { result.link = custom->link; } else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) { result.link = payment->link; + } else if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + result.link = done->lnk; + } else if (const auto append = item->Get<HistoryServiceTodoAppendTasks>()) { + result.link = append->lnk; + } else if (const auto finish = item->Get<HistoryServiceSuggestFinish>()) { + result.link = finish->lnk; } else if (media && data()->showSimilarChannels()) { result = media->textState(mediaPoint, request); } diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp deleted file mode 100644 index 0262545cef..0000000000 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ /dev/null @@ -1,803 +0,0 @@ -/* -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 "history/view/history_view_sublist_section.h" - -#include "main/main_session.h" -#include "core/application.h" -#include "core/shortcuts.h" -#include "data/data_message_reaction_id.h" -#include "data/data_saved_messages.h" -#include "data/data_saved_sublist.h" -#include "data/data_session.h" -#include "data/data_peer_values.h" -#include "data/data_user.h" -#include "history/view/controls/history_view_compose_search.h" -#include "history/view/history_view_top_bar_widget.h" -#include "history/view/history_view_translate_bar.h" -#include "history/view/history_view_list_widget.h" -#include "history/history.h" -#include "history/history_item.h" -#include "history/history_view_swipe_back_session.h" -#include "lang/lang_keys.h" -#include "ui/chat/chat_style.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/scroll_area.h" -#include "ui/widgets/shadow.h" -#include "ui/ui_utility.h" -#include "window/window_session_controller.h" -#include "styles/style_chat.h" -#include "styles/style_chat_helpers.h" -#include "styles/style_window.h" - -// AyuGram includes -#include "ayu/features/messageshot/message_shot.h" - - -namespace HistoryView { -namespace { - -} // namespace - -SublistMemento::SublistMemento(not_null<Data::SavedSublist*> sublist) -: _sublist(sublist) { - const auto selfId = sublist->session().userPeerId(); - _list.setAroundPosition({ - .fullId = FullMsgId(selfId, ShowAtUnreadMsgId), - .date = TimeId(0), - }); -} - -object_ptr<Window::SectionWidget> SublistMemento::createWidget( - QWidget *parent, - not_null<Window::SessionController*> controller, - Window::Column column, - const QRect &geometry) { - if (column == Window::Column::Third) { - return nullptr; - } - auto result = object_ptr<SublistWidget>( - parent, - controller, - _sublist); - result->setInternalState(geometry, this); - return result; -} - -SublistWidget::SublistWidget( - QWidget *parent, - not_null<Window::SessionController*> controller, - not_null<Data::SavedSublist*> sublist) -: Window::SectionWidget(parent, controller, sublist->peer()) -, WindowListDelegate(controller) -, _sublist(sublist) -, _history(sublist->owner().history(sublist->session().user())) -, _topBar(this, controller) -, _topBarShadow(this) -, _translateBar(std::make_unique<TranslateBar>(this, controller, _history)) -, _scroll(std::make_unique<Ui::ScrollArea>( - this, - controller->chatStyle()->value(lifetime(), st::historyScroll), - false)) -, _cornerButtons( - _scroll.get(), - controller->chatStyle(), - static_cast<HistoryView::CornerButtonsDelegate*>(this)) { - controller->chatStyle()->paletteChanged( - ) | rpl::start_with_next([=] { - _scroll->updateBars(); - }, _scroll->lifetime()); - - setupOpenChatButton(); - setupAboutHiddenAuthor(); - - Window::ChatThemeValueFromPeer( - controller, - sublist->peer() - ) | rpl::start_with_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) { - _theme = std::move(theme); - controller->setChatStyleTheme(_theme); - }, lifetime()); - - _topBar->setActiveChat( - TopBarWidget::ActiveChat{ - .key = sublist, - .section = Dialogs::EntryState::Section::SavedSublist, - }, - nullptr); - - _topBar->move(0, 0); - _topBar->resizeToWidth(width()); - _topBar->show(); - - _topBar->deleteSelectionRequest( - ) | rpl::start_with_next([=] { - confirmDeleteSelected(); - }, _topBar->lifetime()); - _topBar->messageShotSelectionRequest( - ) | rpl::start_with_next([=] { - AyuFeatures::MessageShot::Wrapper(_inner, [=] { clearSelected(); }); - }, _topBar->lifetime()); - _topBar->forwardSelectionRequest( - ) | rpl::start_with_next([=] { - confirmForwardSelected(); - }, _topBar->lifetime()); - _topBar->clearSelectionRequest( - ) | rpl::start_with_next([=] { - clearSelected(); - }, _topBar->lifetime()); - _topBar->searchRequest( - ) | rpl::start_with_next([=] { - searchInSublist(); - }, _topBar->lifetime()); - - _translateBar->raise(); - _topBarShadow->raise(); - controller->adaptive().value( - ) | rpl::start_with_next([=] { - updateAdaptiveLayout(); - }, lifetime()); - - _inner = _scroll->setOwnedWidget(object_ptr<ListWidget>( - this, - &controller->session(), - static_cast<ListDelegate*>(this))); - _scroll->move(0, _topBar->height()); - _scroll->show(); - _scroll->scrolls( - ) | rpl::start_with_next([=] { - onScroll(); - }, lifetime()); - - setupShortcuts(); - setupTranslateBar(); - Window::SetupSwipeBackSection(this, _scroll.get(), _inner); -} - -SublistWidget::~SublistWidget() = default; - -void SublistWidget::setupOpenChatButton() { - if (_sublist->peer()->isSavedHiddenAuthor()) { - return; - } - _openChatButton = std::make_unique<Ui::FlatButton>( - this, - (_sublist->peer()->isBroadcast() - ? tr::lng_saved_open_channel(tr::now) - : _sublist->peer()->isUser() - ? tr::lng_saved_open_chat(tr::now) - : tr::lng_saved_open_group(tr::now)), - st::historyComposeButton); - - _openChatButton->setClickedCallback([=] { - controller()->showPeerHistory( - _sublist->peer(), - Window::SectionShow::Way::Forward); - }); -} - -void SublistWidget::setupAboutHiddenAuthor() { - if (!_sublist->peer()->isSavedHiddenAuthor()) { - return; - } - _aboutHiddenAuthor = std::make_unique<Ui::RpWidget>(this); - _aboutHiddenAuthor->paintRequest() | rpl::start_with_next([=] { - auto p = QPainter(_aboutHiddenAuthor.get()); - auto rect = _aboutHiddenAuthor->rect(); - - p.fillRect(rect, st::historyReplyBg); - - p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); - p.drawText( - rect.marginsRemoved( - QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), - tr::lng_saved_about_hidden(tr::now), - style::al_center); - }, _aboutHiddenAuthor->lifetime()); -} - -void SublistWidget::setupTranslateBar() { - controller()->adaptive().oneColumnValue( - ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { - raw->setShadowGeometryPostprocess([=](QRect geometry) { - if (!one) { - geometry.setLeft(geometry.left() + st::lineWidth); - } - return geometry; - }); - }, _translateBar->lifetime()); - - _translateBarHeight = 0; - _translateBar->heightValue( - ) | rpl::start_with_next([=](int height) { - if (const auto delta = height - _translateBarHeight) { - _translateBarHeight = height; - setGeometryWithTopMoved(geometry(), delta); - } - }, _translateBar->lifetime()); - - _translateBar->finishAnimating(); -} - -void SublistWidget::cornerButtonsShowAtPosition( - Data::MessagePosition position) { - showAtPosition(position); -} - -Data::Thread *SublistWidget::cornerButtonsThread() { - return nullptr; -} - -FullMsgId SublistWidget::cornerButtonsCurrentId() { - return _lastShownAt; -} - -bool SublistWidget::cornerButtonsIgnoreVisibility() { - return animatingShow(); -} - -std::optional<bool> SublistWidget::cornerButtonsDownShown() { - const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; - if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { - return true; - } else if (_inner->loadedAtBottomKnown()) { - return !_inner->loadedAtBottom(); - } - return std::nullopt; -} - -bool SublistWidget::cornerButtonsUnreadMayBeShown() { - return _inner->loadedAtBottomKnown(); -} - -bool SublistWidget::cornerButtonsHas(CornerButtonType type) { - return (type == CornerButtonType::Down); -} - -void SublistWidget::showAtPosition( - Data::MessagePosition position, - FullMsgId originId) { - showAtPosition(position, originId, {}); -} - -void SublistWidget::showAtPosition( - Data::MessagePosition position, - FullMsgId originItemId, - const Window::SectionShow ¶ms) { - _lastShownAt = position.fullId; - controller()->setActiveChatEntry(activeChat()); - _inner->showAtPosition( - position, - params, - _cornerButtons.doneJumpFrom(position.fullId, originItemId)); -} -void SublistWidget::updateAdaptiveLayout() { - _topBarShadow->moveToLeft( - controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, - _topBar->height()); -} - -not_null<Data::SavedSublist*> SublistWidget::sublist() const { - return _sublist; -} - -Dialogs::RowDescriptor SublistWidget::activeChat() const { - const auto messageId = _lastShownAt - ? _lastShownAt - : FullMsgId(_history->peer->id, ShowAtUnreadMsgId); - return { _sublist, messageId }; -} - -QPixmap SublistWidget::grabForShowAnimation( - const Window::SectionSlideParams ¶ms) { - _topBar->updateControlsVisibility(); - if (params.withTopBarShadow) _topBarShadow->hide(); - auto result = Ui::GrabWidget(this); - if (params.withTopBarShadow) _topBarShadow->show(); - _translateBar->hide(); - return result; -} - -void SublistWidget::checkActivation() { - _inner->checkActivation(); -} - -void SublistWidget::doSetInnerFocus() { - if (_composeSearch) { - _composeSearch->setInnerFocus(); - } else { - _inner->setFocus(); - } -} - -bool SublistWidget::showInternal( - not_null<Window::SectionMemento*> memento, - const Window::SectionShow ¶ms) { - if (auto logMemento = dynamic_cast<SublistMemento*>(memento.get())) { - if (logMemento->getSublist() == sublist()) { - restoreState(logMemento); - return true; - } - } - return false; -} - -bool SublistWidget::sameTypeAs(not_null<Window::SectionMemento*> memento) { - return dynamic_cast<SublistMemento*>(memento.get()) != nullptr; -} - -void SublistWidget::setInternalState( - const QRect &geometry, - not_null<SublistMemento*> memento) { - setGeometry(geometry); - Ui::SendPendingMoveResizeEvents(this); - restoreState(memento); -} - -bool SublistWidget::searchInChatEmbedded( - QString query, - Dialogs::Key chat, - PeerData *searchFrom) { - const auto sublist = chat.sublist(); - if (!sublist || sublist != _sublist) { - return false; - } else if (_composeSearch) { - _composeSearch->setQuery(query); - _composeSearch->setInnerFocus(); - return true; - } - _composeSearch = std::make_unique<ComposeSearch>( - this, - controller(), - _history, - sublist->peer(), - query); - - updateControlsGeometry(); - setInnerFocus(); - - _composeSearch->activations( - ) | rpl::start_with_next([=](ComposeSearch::Activation activation) { - const auto item = activation.item; - auto params = ::Window::SectionShow( - ::Window::SectionShow::Way::ClearStack); - params.highlightPart = { activation.query }; - params.highlightPartOffsetHint = kSearchQueryOffsetHint; - controller()->showPeerHistory( - item->history()->peer->id, - params, - item->fullId().msg); - }, _composeSearch->lifetime()); - - _composeSearch->destroyRequests( - ) | rpl::take( - 1 - ) | rpl::start_with_next([=] { - _composeSearch = nullptr; - - updateControlsGeometry(); - setInnerFocus(); - }, _composeSearch->lifetime()); - - return true; -} - -std::shared_ptr<Window::SectionMemento> SublistWidget::createMemento() { - auto result = std::make_shared<SublistMemento>(sublist()); - saveState(result.get()); - return result; -} - -bool SublistWidget::showMessage( - PeerId peerId, - const Window::SectionShow ¶ms, - MsgId messageId) { - const auto id = FullMsgId(_history->peer->id, messageId); - const auto message = _history->owner().message(id); - if (!message || message->savedSublist() != _sublist) { - return false; - } - const auto originMessage = [&]() -> HistoryItem* { - using OriginMessage = Window::SectionShow::OriginMessage; - if (const auto origin = std::get_if<OriginMessage>(¶ms.origin)) { - if (const auto returnTo = session().data().message(origin->id)) { - if (returnTo->savedSublist() == _sublist) { - return returnTo; - } - } - } - return nullptr; - }(); - const auto currentReplyReturn = _cornerButtons.replyReturn(); - const auto originItemId = !originMessage - ? FullMsgId() - : (currentReplyReturn != originMessage) - ? originMessage->fullId() - : FullMsgId(); - showAtPosition(message->position(), originItemId, params); - return true; -} - -void SublistWidget::saveState(not_null<SublistMemento*> memento) { - _inner->saveState(memento->list()); -} - -void SublistWidget::restoreState(not_null<SublistMemento*> memento) { - _inner->restoreState(memento->list()); -} - -void SublistWidget::resizeEvent(QResizeEvent *e) { - if (!width() || !height()) { - return; - } - recountChatWidth(); - updateControlsGeometry(); -} - -void SublistWidget::recountChatWidth() { - auto layout = (width() < st::adaptiveChatWideWidth) - ? Window::Adaptive::ChatLayout::Normal - : Window::Adaptive::ChatLayout::Wide; - controller()->adaptive().setChatLayout(layout); -} - -void SublistWidget::updateControlsGeometry() { - const auto contentWidth = width(); - - const auto newScrollTop = _scroll->isHidden() - ? std::nullopt - : base::make_optional(_scroll->scrollTop() + topDelta()); - _topBar->resizeToWidth(contentWidth); - _topBarShadow->resize(contentWidth, st::lineWidth); - - auto bottom = height(); - if (_openChatButton) { - _openChatButton->resizeToWidth(width()); - bottom -= _openChatButton->height(); - _openChatButton->move(0, bottom); - } - if (_aboutHiddenAuthor) { - _aboutHiddenAuthor->resize(width(), st::historyUnblock.height); - bottom -= _aboutHiddenAuthor->height(); - _aboutHiddenAuthor->move(0, bottom); - } - const auto controlsHeight = 0; - auto top = _topBar->height(); - _translateBar->move(0, top); - _translateBar->resizeToWidth(contentWidth); - top += _translateBarHeight; - const auto scrollHeight = bottom - top - controlsHeight; - const auto scrollSize = QSize(contentWidth, scrollHeight); - if (_scroll->size() != scrollSize) { - _skipScrollEvent = true; - _scroll->resize(scrollSize); - _inner->resizeToWidth(scrollSize.width(), _scroll->height()); - _skipScrollEvent = false; - } - _scroll->move(0, top); - if (!_scroll->isHidden()) { - if (newScrollTop) { - _scroll->scrollToY(*newScrollTop); - } - updateInnerVisibleArea(); - } - - _cornerButtons.updatePositions(); -} - -void SublistWidget::paintEvent(QPaintEvent *e) { - if (animatingShow()) { - SectionWidget::paintEvent(e); - return; - } else if (controller()->contentOverlapped(this, e)) { - return; - } - - const auto aboveHeight = _topBar->height(); - const auto bg = e->rect().intersected( - QRect(0, aboveHeight, width(), height() - aboveHeight)); - SectionWidget::PaintBackground(controller(), _theme.get(), this, bg); -} - -void SublistWidget::onScroll() { - if (_skipScrollEvent) { - return; - } - updateInnerVisibleArea(); -} - -void SublistWidget::updateInnerVisibleArea() { - const auto scrollTop = _scroll->scrollTop(); - _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); - _cornerButtons.updateJumpDownVisibility(); - _cornerButtons.updateUnreadThingsVisibility(); -} - -void SublistWidget::showAnimatedHook( - const Window::SectionSlideParams ¶ms) { - _topBar->setAnimatingMode(true); - if (params.withTopBarShadow) { - _topBarShadow->show(); - } -} - -void SublistWidget::showFinishedHook() { - _topBar->setAnimatingMode(false); - _inner->showFinished(); - _translateBar->show(); -} - -bool SublistWidget::floatPlayerHandleWheelEvent(QEvent *e) { - return _scroll->viewportEvent(e); -} - -QRect SublistWidget::floatPlayerAvailableRect() { - return mapToGlobal(_scroll->geometry()); -} - -Context SublistWidget::listContext() { - return Context::SavedSublist; -} - -bool SublistWidget::listScrollTo(int top, bool syntetic) { - top = std::clamp(top, 0, _scroll->scrollTopMax()); - if (_scroll->scrollTop() == top) { - updateInnerVisibleArea(); - return false; - } - _scroll->scrollToY(top); - return true; -} - -void SublistWidget::listCancelRequest() { - if (_inner && !_inner->getSelectedIds().empty()) { - clearSelected(); - return; - } - controller()->showBackFromStack(); -} - -void SublistWidget::listDeleteRequest() { - confirmDeleteSelected(); -} - -void SublistWidget::listTryProcessKeyInput(not_null<QKeyEvent*> e) { -} - -rpl::producer<Data::MessagesSlice> SublistWidget::listSource( - Data::MessagePosition aroundId, - int limitBefore, - int limitAfter) { - const auto messageId = aroundId.fullId.msg - ? aroundId.fullId.msg - : (ServerMaxMsgId - 1); - return [=](auto consumer) { - const auto pushSlice = [=] { - auto result = Data::MessagesSlice(); - result.fullCount = _sublist->fullCount(); - _topBar->setCustomTitle(result.fullCount - ? tr::lng_forum_messages( - tr::now, - lt_count_decimal, - *result.fullCount) - : tr::lng_contacts_loading(tr::now)); - const auto &messages = _sublist->messages(); - const auto i = ranges::lower_bound( - messages, - messageId, - ranges::greater(), - [](not_null<HistoryItem*> item) { return item->id; }); - const auto before = int(end(messages) - i); - const auto useBefore = std::min(before, limitBefore); - const auto after = int(i - begin(messages)); - const auto useAfter = std::min(after, limitAfter); - const auto from = i - useAfter; - const auto till = i + useBefore; - auto nearestDistance = std::numeric_limits<int64>::max(); - result.ids.reserve(useAfter + useBefore); - for (auto j = till; j != from;) { - const auto item = *--j; - result.ids.push_back(item->fullId()); - const auto distance = std::abs((messageId - item->id).bare); - if (nearestDistance > distance) { - nearestDistance = distance; - result.nearestToAround = result.ids.back(); - } - } - result.skippedAfter = after - useAfter; - result.skippedBefore = result.fullCount - ? (*result.fullCount - after - useBefore) - : std::optional<int>(); - if (!result.fullCount || useBefore < limitBefore) { - _sublist->owner().savedMessages().loadMore(_sublist); - } - consumer.put_next(std::move(result)); - }; - auto lifetime = rpl::lifetime(); - _sublist->changes() | rpl::start_with_next(pushSlice, lifetime); - pushSlice(); - return lifetime; - }; -} - -bool SublistWidget::listAllowsMultiSelect() { - return true; -} - -bool SublistWidget::listIsItemGoodForSelection( - not_null<HistoryItem*> item) { - return item->isRegular() && !item->isService(); -} - -bool SublistWidget::listIsLessInOrder( - not_null<HistoryItem*> first, - not_null<HistoryItem*> second) { - return first->id < second->id; -} - -void SublistWidget::listSelectionChanged(SelectedItems &&items) { - HistoryView::TopBarWidget::SelectedState state; - state.count = items.size(); - for (const auto &item : items) { - if (item.canDelete) { - ++state.canDeleteCount; - } - if (item.canForward) { - ++state.canForwardCount; - } - } - _topBar->showSelected(state); - if ((state.count > 0) && _composeSearch) { - _composeSearch->hideAnimated(); - } -} - -void SublistWidget::listMarkReadTill(not_null<HistoryItem*> item) { -} - -void SublistWidget::listMarkContentsRead( - const base::flat_set<not_null<HistoryItem*>> &items) { -} - -MessagesBarData SublistWidget::listMessagesBar( - const std::vector<not_null<Element*>> &elements) { - return {}; -} - -void SublistWidget::listContentRefreshed() { -} - -void SublistWidget::listUpdateDateLink( - ClickHandlerPtr &link, - not_null<Element*> view) { -} - -bool SublistWidget::listElementHideReply(not_null<const Element*> view) { - return false; -} - -bool SublistWidget::listElementShownUnread(not_null<const Element*> view) { - return view->data()->unread(view->data()->history()); -} - -bool SublistWidget::listIsGoodForAroundPosition( - not_null<const Element*> view) { - return view->data()->isRegular(); -} - -void SublistWidget::listSendBotCommand( - const QString &command, - const FullMsgId &context) { -} - -void SublistWidget::listSearch( - const QString &query, - const FullMsgId &context) { - const auto inChat = Data::SearchTagFromQuery(query) - ? Dialogs::Key(_sublist) - : Dialogs::Key(); - controller()->searchMessages(query, inChat); -} - -void SublistWidget::listHandleViaClick(not_null<UserData*> bot) { -} - -not_null<Ui::ChatTheme*> SublistWidget::listChatTheme() { - return _theme.get(); -} - -CopyRestrictionType SublistWidget::listCopyRestrictionType( - HistoryItem *item) { - return CopyRestrictionTypeFor(_history->peer, item); -} - -CopyRestrictionType SublistWidget::listCopyMediaRestrictionType( - not_null<HistoryItem*> item) { - return CopyMediaRestrictionTypeFor(_history->peer, item); -} - -CopyRestrictionType SublistWidget::listSelectRestrictionType() { - return SelectRestrictionTypeFor(_history->peer); -} - -auto SublistWidget::listAllowedReactionsValue() --> rpl::producer<Data::AllowedReactions> { - return Data::PeerAllowedReactionsValue(_history->peer); -} - -void SublistWidget::listShowPremiumToast(not_null<DocumentData*> document) { -} - -void SublistWidget::listOpenPhoto( - not_null<PhotoData*> photo, - FullMsgId context) { - controller()->openPhoto(photo, { context }); -} - -void SublistWidget::listOpenDocument( - not_null<DocumentData*> document, - FullMsgId context, - bool showInMediaView) { - controller()->openDocument(document, showInMediaView, { context }); -} - -void SublistWidget::listPaintEmpty( - Painter &p, - const Ui::ChatPaintContext &context) { -} - -QString SublistWidget::listElementAuthorRank(not_null<const Element*> view) { - return {}; -} - -bool SublistWidget::listElementHideTopicButton( - not_null<const Element*> view) { - return true; -} - -History *SublistWidget::listTranslateHistory() { - return _history; -} - -void SublistWidget::listAddTranslatedItems( - not_null<TranslateTracker*> tracker) { -} - -void SublistWidget::confirmDeleteSelected() { - ConfirmDeleteSelectedItems(_inner); -} - -void SublistWidget::confirmForwardSelected() { - ConfirmForwardSelectedItems(_inner); -} - -void SublistWidget::clearSelected() { - _inner->cancelSelection(); -} - -void SublistWidget::setupShortcuts() { - Shortcuts::Requests( - ) | rpl::filter([=] { - return Ui::AppInFocus() - && Ui::InFocusChain(this) - && !controller()->isLayerShown() - && (Core::App().activeWindow() == &controller()->window()); - }) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) { - using Command = Shortcuts::Command; - request->check(Command::Search, 1) && request->handle([=] { - searchInSublist(); - return true; - }); - }, lifetime()); -} - -void SublistWidget::searchInSublist() { - controller()->searchInChat(_sublist); -} - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h deleted file mode 100644 index 33b655720e..0000000000 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.h +++ /dev/null @@ -1,237 +0,0 @@ -/* -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 "window/section_widget.h" -#include "window/section_memento.h" -#include "history/view/history_view_list_widget.h" -#include "history/view/history_view_corner_buttons.h" -#include "data/data_messages.h" -#include "base/weak_ptr.h" -#include "base/timer.h" - -class History; - -namespace Ui { -class ScrollArea; -class PlainShadow; -class FlatButton; -} // namespace Ui - -namespace Profile { -class BackButton; -} // namespace Profile - -namespace HistoryView { - -class Element; -class TopBarWidget; -class SublistMemento; -class TranslateBar; -class ComposeSearch; - -class SublistWidget final - : public Window::SectionWidget - , private WindowListDelegate - , private CornerButtonsDelegate { -public: - SublistWidget( - QWidget *parent, - not_null<Window::SessionController*> controller, - not_null<Data::SavedSublist*> sublist); - ~SublistWidget(); - - [[nodiscard]] not_null<Data::SavedSublist*> sublist() const; - Dialogs::RowDescriptor activeChat() const override; - - bool hasTopBarShadow() const override { - return true; - } - - QPixmap grabForShowAnimation( - const Window::SectionSlideParams ¶ms) override; - - bool showInternal( - not_null<Window::SectionMemento*> memento, - const Window::SectionShow ¶ms) override; - bool sameTypeAs(not_null<Window::SectionMemento*> memento) override; - - std::shared_ptr<Window::SectionMemento> createMemento() override; - bool showMessage( - PeerId peerId, - const Window::SectionShow ¶ms, - MsgId messageId) override; - - void setInternalState( - const QRect &geometry, - not_null<SublistMemento*> memento); - - Window::SectionActionResult sendBotCommand( - Bot::SendCommandRequest request) override { - return Window::SectionActionResult::Fallback; - } - - bool searchInChatEmbedded( - QString query, - Dialogs::Key chat, - PeerData *searchFrom = nullptr) override; - - // Float player interface. - bool floatPlayerHandleWheelEvent(QEvent *e) override; - QRect floatPlayerAvailableRect() override; - - // ListDelegate interface. - Context listContext() override; - bool listScrollTo(int top, bool syntetic = true) override; - void listCancelRequest() override; - void listDeleteRequest() override; - void listTryProcessKeyInput(not_null<QKeyEvent*> e) override; - rpl::producer<Data::MessagesSlice> listSource( - Data::MessagePosition aroundId, - int limitBefore, - int limitAfter) override; - bool listAllowsMultiSelect() override; - bool listIsItemGoodForSelection(not_null<HistoryItem*> item) override; - bool listIsLessInOrder( - not_null<HistoryItem*> first, - not_null<HistoryItem*> second) override; - void listSelectionChanged(SelectedItems &&items) override; - void listMarkReadTill(not_null<HistoryItem*> item) override; - void listMarkContentsRead( - const base::flat_set<not_null<HistoryItem*>> &items) override; - MessagesBarData listMessagesBar( - const std::vector<not_null<Element*>> &elements) override; - void listContentRefreshed() override; - void listUpdateDateLink( - ClickHandlerPtr &link, - not_null<Element*> view) override; - bool listElementHideReply(not_null<const Element*> view) override; - bool listElementShownUnread(not_null<const Element*> view) override; - bool listIsGoodForAroundPosition(not_null<const Element*> view) override; - void listSendBotCommand( - const QString &command, - const FullMsgId &context) override; - void listSearch( - const QString &query, - const FullMsgId &context) override; - void listHandleViaClick(not_null<UserData*> bot) override; - not_null<Ui::ChatTheme*> listChatTheme() override; - CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; - CopyRestrictionType listCopyMediaRestrictionType( - not_null<HistoryItem*> item) override; - CopyRestrictionType listSelectRestrictionType() override; - auto listAllowedReactionsValue() - -> rpl::producer<Data::AllowedReactions> override; - void listShowPremiumToast(not_null<DocumentData*> document) override; - void listOpenPhoto( - not_null<PhotoData*> photo, - FullMsgId context) override; - void listOpenDocument( - not_null<DocumentData*> document, - FullMsgId context, - bool showInMediaView) override; - void listPaintEmpty( - Painter &p, - const Ui::ChatPaintContext &context) override; - QString listElementAuthorRank(not_null<const Element*> view) override; - bool listElementHideTopicButton(not_null<const Element*> view) override; - History *listTranslateHistory() override; - void listAddTranslatedItems( - not_null<TranslateTracker*> tracker) override; - - // CornerButtonsDelegate delegate. - void cornerButtonsShowAtPosition( - Data::MessagePosition position) override; - Data::Thread *cornerButtonsThread() override; - FullMsgId cornerButtonsCurrentId() override; - bool cornerButtonsIgnoreVisibility() override; - std::optional<bool> cornerButtonsDownShown() override; - bool cornerButtonsUnreadMayBeShown() override; - bool cornerButtonsHas(CornerButtonType type) override; - -private: - void resizeEvent(QResizeEvent *e) override; - void paintEvent(QPaintEvent *e) override; - - void showAnimatedHook( - const Window::SectionSlideParams ¶ms) override; - void showFinishedHook() override; - void doSetInnerFocus() override; - void checkActivation() override; - - void onScroll(); - void updateInnerVisibleArea(); - void updateControlsGeometry(); - void updateAdaptiveLayout(); - void saveState(not_null<SublistMemento*> memento); - void restoreState(not_null<SublistMemento*> memento); - void showAtPosition( - Data::MessagePosition position, - FullMsgId originId = {}); - void showAtPosition( - Data::MessagePosition position, - FullMsgId originItemId, - const Window::SectionShow ¶ms); - - void setupOpenChatButton(); - void setupAboutHiddenAuthor(); - void setupTranslateBar(); - void setupShortcuts(); - - void confirmDeleteSelected(); - void confirmForwardSelected(); - void clearSelected(); - void recountChatWidth(); - void searchInSublist(); - - const not_null<Data::SavedSublist*> _sublist; - const not_null<History*> _history; - std::shared_ptr<Ui::ChatTheme> _theme; - QPointer<ListWidget> _inner; - object_ptr<TopBarWidget> _topBar; - object_ptr<Ui::PlainShadow> _topBarShadow; - - std::unique_ptr<TranslateBar> _translateBar; - int _translateBarHeight = 0; - - bool _skipScrollEvent = false; - std::unique_ptr<Ui::ScrollArea> _scroll; - std::unique_ptr<Ui::FlatButton> _openChatButton; - std::unique_ptr<Ui::RpWidget> _aboutHiddenAuthor; - std::unique_ptr<ComposeSearch> _composeSearch; - - FullMsgId _lastShownAt; - CornerButtons _cornerButtons; - -}; - -class SublistMemento : public Window::SectionMemento { -public: - explicit SublistMemento(not_null<Data::SavedSublist*> sublist); - - object_ptr<Window::SectionWidget> createWidget( - QWidget *parent, - not_null<Window::SessionController*> controller, - Window::Column column, - const QRect &geometry) override; - - [[nodiscard]] not_null<Data::SavedSublist*> getSublist() const { - return _sublist; - } - - [[nodiscard]] not_null<ListMemento*> list() { - return &_list; - } - -private: - const not_null<Data::SavedSublist*> _sublist; - ListMemento _list; - -}; - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp new file mode 100644 index 0000000000..234bbca061 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -0,0 +1,813 @@ +/* +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 "history/view/history_view_subsection_tabs.h" + +#include "base/qt/qt_key_modifiers.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" +#include "data/data_forum.h" +#include "data/data_forum_topic.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" +#include "data/data_thread.h" +#include "data/data_user.h" +#include "dialogs/dialogs_main_list.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "main/main_session_settings.h" +#include "ui/controls/subsection_tabs_slider.h" +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/popup_menu.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/shadow.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" +#include "window/window_peer_menu.h" +#include "window/window_separate_id.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +constexpr auto kDefaultLimit = 12; + +} // namespace + +SubsectionTabs::SubsectionTabs( + not_null<Window::SessionController*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread) +: _controller(controller) +, _history(thread->owningHistory()) +, _active(thread) +, _around(thread) +, _beforeLimit(kDefaultLimit) +, _afterLimit(kDefaultLimit) { + track(); + refreshSlice(); + setup(parent); + + dataChanged() | rpl::start_with_next([=] { + if (_loading) { + _loading = false; + refreshSlice(); + } + }, _lifetime); +} + +SubsectionTabs::~SubsectionTabs() { + delete base::take(_horizontal); + delete base::take(_vertical); + delete base::take(_shadow); +} + +void SubsectionTabs::setup(not_null<Ui::RpWidget*> parent) { + const auto peerId = _history->peer->id; + if (session().settings().verticalSubsectionTabs(peerId)) { + setupVertical(parent); + } else { + setupHorizontal(parent); + } +} + +void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { + delete base::take(_vertical); + _horizontal = Ui::CreateChild<Ui::RpWidget>(parent); + _horizontal->show(); + + if (!_shadow) { + _shadow = Ui::CreateChild<Ui::PlainShadow>(parent); + _shadow->show(); + } else { + _shadow->raise(); + } + + const auto toggle = Ui::CreateChild<Ui::IconButton>( + _horizontal, + st::chatTabsToggle); + toggle->show(); + toggle->setClickedCallback([=] { + toggleModes(); + }); + toggle->move(0, 0); + const auto scroll = Ui::CreateChild<Ui::ScrollArea>( + _horizontal, + st::chatTabsScroll, + true); + scroll->show(); + const auto shadow = Ui::CreateChild<Ui::PlainShadow>(_horizontal); + const auto slider = scroll->setOwnedWidget( + object_ptr<Ui::HorizontalSlider>(scroll)); + setupSlider(scroll, slider, false); + + shadow->showOn(rpl::single( + rpl::empty + ) | rpl::then( + scroll->scrolls() + ) | rpl::map([=] { return scroll->scrollLeft() > 0; })); + shadow->setAttribute(Qt::WA_TransparentForMouseEvents); + + _horizontal->resize( + _horizontal->width(), + std::max(toggle->height(), slider->height())); + + scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) { + const auto pixelDelta = e->pixelDelta(); + const auto angleDelta = e->angleDelta(); + if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) { + return false; + } + const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y(); + scroll->scrollToX(scroll->scrollLeft() - y); + return true; + }); + + _horizontal->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto togglew = toggle->width(); + const auto height = size.height(); + scroll->setGeometry(togglew, 0, size.width() - togglew, height); + shadow->setGeometry(togglew, 0, st::lineWidth, height); + }, scroll->lifetime()); + + _horizontal->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(_horizontal).fillRect( + clip.intersected( + _horizontal->rect().marginsRemoved( + { 0, 0, 0, st::lineWidth })), + st::windowBg); + }, _horizontal->lifetime()); +} + +void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { + delete base::take(_horizontal); + _vertical = Ui::CreateChild<Ui::RpWidget>(parent); + _vertical->show(); + + if (!_shadow) { + _shadow = Ui::CreateChild<Ui::PlainShadow>(parent); + _shadow->show(); + } + + const auto toggle = Ui::CreateChild<Ui::IconButton>( + _vertical, + st::chatTabsToggle); + toggle->show(); + const auto active = &st::chatTabsToggleActive; + toggle->setIconOverride(active, active); + toggle->setClickedCallback([=] { + toggleModes(); + }); + toggle->move(0, 0); + const auto scroll = Ui::CreateChild<Ui::ScrollArea>( + _vertical, + st::chatTabsScroll); + scroll->show(); + const auto shadow = Ui::CreateChild<Ui::PlainShadow>(_vertical); + const auto slider = scroll->setOwnedWidget( + object_ptr<Ui::VerticalSlider>(scroll)); + setupSlider(scroll, slider, true); + + shadow->showOn(rpl::single( + rpl::empty + ) | rpl::then( + scroll->scrolls() + ) | rpl::map([=] { return scroll->scrollTop() > 0; })); + shadow->setAttribute(Qt::WA_TransparentForMouseEvents); + + _vertical->resize( + std::max(toggle->width(), slider->width()), + _vertical->height()); + + _vertical->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto toggleh = toggle->height(); + const auto width = size.width(); + scroll->setGeometry(0, toggleh, width, size.height() - toggleh); + shadow->setGeometry(0, toggleh, width, st::lineWidth); + }, scroll->lifetime()); + + _vertical->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(_vertical).fillRect(clip, st::windowBg); + }, _vertical->lifetime()); +} + +void SubsectionTabs::setupSlider( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider, + bool vertical) { + slider->sectionActivated() | rpl::start_with_next([=](int active) { + const auto newWindow = base::IsCtrlPressed(); + if (active >= 0 + && active < _slice.size() + && (newWindow || _active != _slice[active].thread)) { + const auto thread = _slice[active].thread; + if (newWindow) { + _controller->showInNewWindow(Window::SeparateId(thread)); + _refreshed.fire({}); // This should activate current section. + } else { + auto params = Window::SectionShow(); + params.way = Window::SectionShow::Way::ClearStack; + params.animated = anim::type::instant; + _controller->showThread(thread, ShowAtUnreadMsgId, params); + } + } + }, slider->lifetime()); + + slider->sectionContextMenu() | rpl::start_with_next([=](int index) { + if (index >= 0 && index < _slice.size()) { + showThreadContextMenu(_slice[index].thread); + } + }, slider->lifetime()); + + slider->requestShown( + ) | rpl::start_with_next([=](Ui::ScrollToRequest request) { + const auto full = vertical ? scroll->height() : scroll->width(); + const auto scrollValue = vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + if (request.ymin < scrollValue) { + if (vertical) { + scroll->scrollToY(request.ymin); + } else { + scroll->scrollToX(request.ymin); + } + } else if (request.ymax > scrollValue + full) { + const auto value = std::min(request.ymin, request.ymax - full); + if (vertical) { + scroll->scrollToY(value); + } else { + scroll->scrollToX(value); + } + } + }, slider->lifetime()); + + rpl::merge( + scroll->scrolls(), + _scrollCheckRequests.events(), + (vertical + ? scroll->heightValue() + : scroll->widthValue()) | rpl::skip(1) | rpl::map_to(rpl::empty) + ) | rpl::start_with_next([=] { + const auto full = vertical ? scroll->height() : scroll->width(); + const auto scrollValue = vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + const auto scrollMax = vertical + ? scroll->scrollTopMax() + : scroll->scrollLeftMax(); + const auto availableFrom = scrollValue; + const auto availableTill = (scrollMax - scrollValue); + const auto needMore = (scrollMax <= 3 * full && _afterAvailable > 0); + if (needMore) { + _beforeLimit *= 2; + _afterLimit *= 2; + } + if (availableFrom < full + && _beforeSkipped.value_or(0) > 0 + && !_slice.empty()) { + refreshAroundMiddle(scroll, slider); + } else if (availableTill < full) { + if (_afterAvailable > 0) { + refreshAroundMiddle(scroll, slider); + } else if (!_afterSkipped.has_value()) { + _loading = true; + loadMore(); + } + } else if (needMore) { + refreshAroundMiddle(scroll, slider); + } + }, scroll->lifetime()); + + using ImagePointer = std::shared_ptr<Ui::DynamicImage>; + struct Cache { + base::flat_map<not_null<PeerData*>, ImagePointer> userpics; + }; + const auto cache = std::make_shared<Cache>(); + + _refreshed.events_starting_with_copy( + rpl::empty + ) | rpl::start_with_next([=] { + const auto manager = &_history->owner().customEmojiManager(); + const auto paused = [=] { + return _controller->isGifPausedAtLeastFor( + Window::GifPauseReason::Any); + }; + auto updated = Cache(); + auto sections = std::vector<Ui::SubsectionTab>(); + auto activeIndex = -1; + for (const auto &item : _slice) { + const auto index = int(sections.size()); + if (item.thread == _active) { + activeIndex = index; + } + const auto textFg = [=] { + return anim::color( + st::windowSubTextFg, + st::windowActiveTextFg, + slider->buttonActive(slider->buttonAt(index))); + }; + if (const auto topic = item.thread->asTopic()) { + if (vertical) { + const auto general = topic->isGeneral(); + sections.push_back({ + .text = { item.name }, + .userpic = (item.iconId + ? Ui::MakeEmojiThumbnail( + &topic->owner(), + Data::SerializeCustomEmojiId(item.iconId), + paused, + textFg) + : Ui::MakeEmojiThumbnail( + &topic->owner(), + Data::TopicIconEmojiEntity({ + .title = (general + ? Data::ForumGeneralIconTitle() + : item.name), + .colorId = (general + ? Data::ForumGeneralIconColor( + st::windowSubTextFg->c) + : topic->colorId()), + }), + paused, + textFg)), + }); + } else { + sections.push_back({ + .text = topic->titleWithIcon(), + }); + } + } else if (const auto sublist = item.thread->asSublist()) { + const auto peer = sublist->sublistPeer(); + if (vertical) { + auto was = cache->userpics[peer]; + auto userpic = updated.userpics[peer] = was + ? was + : Ui::MakeUserpicThumbnail(peer); + sections.push_back({ + .text = { peer->shortName() }, + .userpic = std::move(userpic), + }); + } else { + sections.push_back({ + .text = TextWithEntities().append( + Ui::Text::SingleCustomEmoji( + manager->peerUserpicEmojiData(peer), + u"@"_q) + ).append(' ').append(peer->shortName()), + }); + } + } else { + sections.push_back({ + .text = { tr::lng_filters_all_short(tr::now) }, + .userpic = Ui::MakeAllSubsectionsThumbnail(textFg), + }); + } + auto §ion = sections.back(); + section.badges = item.badges; + } + *cache = std::move(updated); + + auto scrollSavingThread = (Data::Thread*)nullptr; + auto scrollSavingShift = 0; + auto scrollSavingIndex = -1; + if (const auto count = slider->sectionsCount()) { + const auto scrollValue = vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + auto indexPosition = slider->lookupSectionPosition(0); + for (auto index = 0; index != count; ++index) { + const auto nextPosition = (index + 1 != count) + ? slider->lookupSectionPosition(index + 1) + : (indexPosition + scrollValue + 1); + if (indexPosition <= scrollValue && nextPosition > scrollValue) { + scrollSavingThread = _sectionsSlice[index].thread; + scrollSavingShift = scrollValue - indexPosition; + break; + } + indexPosition = nextPosition; + } + scrollSavingIndex = scrollSavingThread + ? int(ranges::find( + _slice, + not_null(scrollSavingThread), + &Item::thread + ) - begin(_slice)) + : -1; + if (scrollSavingIndex == _slice.size()) { + scrollSavingIndex = -1; + for (auto index = 0; index != count; ++index) { + const auto thread = _sectionsSlice[index].thread; + const auto i = ranges::find( + _slice, + thread, + &Item::thread); + if (i != end(_slice)) { + scrollSavingThread = thread; + scrollSavingShift = scrollValue + - slider->lookupSectionPosition(index); + scrollSavingIndex = int(i - begin(_slice)); + break; + } + } + } + } + slider->setSections({ + .tabs = std::move(sections), + .context = Core::TextContext({ + .session = &session(), + }), + }, paused); + slider->setActiveSectionFast(activeIndex); + + _sectionsSlice = _slice; + if (scrollSavingIndex >= 0) { + const auto position = scrollSavingShift + + slider->lookupSectionPosition(scrollSavingIndex); + if (vertical) { + scroll->scrollToY(position); + } else { + scroll->scrollToX(position); + } + } + + _scrollCheckRequests.fire({}); + }, scroll->lifetime()); +} + +void SubsectionTabs::showThreadContextMenu(not_null<Data::Thread*> thread) { + _menu = nullptr; + _menu = base::make_unique_q<Ui::PopupMenu>( + _horizontal ? _horizontal : _vertical, + st::popupMenuExpandedSeparator); + + const auto addAction = Ui::Menu::CreateAddActionCallback(_menu); + Window::FillDialogsEntryMenu( + _controller, + Dialogs::EntryState{ + .key = Dialogs::Key{ thread }, + .section = Dialogs::EntryState::Section::SubsectionTabsMenu, + }, + addAction); + if (_menu->empty()) { + _menu = nullptr; + } else { + _menu->popup(QCursor::pos()); + } +} + +void SubsectionTabs::loadMore() { + if (const auto forum = _history->peer->forum()) { + forum->requestTopics(); + } else if (const auto monoforum = _history->peer->monoforum()) { + monoforum->loadMore(); + } else { + Unexpected("Peer in SubsectionTabs::loadMore."); + } +} + +rpl::producer<> SubsectionTabs::dataChanged() const { + if (const auto forum = _history->peer->forum()) { + return forum->chatsListChanges(); + } else if (const auto monoforum = _history->peer->monoforum()) { + return monoforum->chatsListChanges(); + } else { + Unexpected("Peer in SubsectionTabs::dataChanged."); + } +} + +void SubsectionTabs::toggleModes() { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + setupVertical(_horizontal->parentWidget()); + } else { + setupHorizontal(_vertical->parentWidget()); + } + const auto peerId = _history->peer->id; + const auto vertical = (_vertical != nullptr); + session().settings().setVerticalSubsectionTabs(peerId, vertical); + session().saveSettingsDelayed(); + + _layoutRequests.fire({}); +} + +rpl::producer<> SubsectionTabs::removeRequests() const { + if (const auto forum = _history->peer->forum()) { + return forum->destroyed(); + } else if (const auto monoforum = _history->peer->monoforum()) { + return monoforum->destroyed(); + } else { + Unexpected("Peer in SubsectionTabs::removeRequests."); + } +} + +void SubsectionTabs::extractToParent(not_null<Ui::RpWidget*> parent) { + Expects((_horizontal || _vertical) && _shadow); + + if (_vertical) { + _vertical->hide(); + _vertical->setParent(parent); + } else { + _horizontal->hide(); + _horizontal->setParent(parent); + } + _shadow->hide(); + _shadow->setParent(parent); +} + +void SubsectionTabs::setBoundingRect(QRect boundingRect) { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + _horizontal->setGeometry( + boundingRect.x(), + boundingRect.y(), + boundingRect.width(), + _horizontal->height()); + _shadow->setGeometry( + boundingRect.x(), + _horizontal->y() + _horizontal->height() - st::lineWidth, + boundingRect.width(), + st::lineWidth); + } else { + _vertical->setGeometry( + boundingRect.x(), + boundingRect.y(), + _vertical->width(), + boundingRect.height()); + _shadow->setGeometry( + _vertical->x() + _vertical->width(), + boundingRect.y(), + st::lineWidth, + boundingRect.height()); + } +} + +rpl::producer<> SubsectionTabs::layoutRequests() const { + return _layoutRequests.events(); +} + +int SubsectionTabs::leftSkip() const { + return _vertical ? _vertical->width() : 0; +} + +int SubsectionTabs::topSkip() const { + return _horizontal ? (_horizontal->height() - st::lineWidth) : 0; +} + +void SubsectionTabs::raise() { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + _horizontal->raise(); + } else { + _vertical->raise(); + } + _shadow->raise(); +} + +void SubsectionTabs::show() { + setVisible(true); +} + +void SubsectionTabs::hide() { + setVisible(false); +} + +void SubsectionTabs::setVisible(bool shown) { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + _horizontal->setVisible(shown); + } else { + _vertical->setVisible(shown); + } + _shadow->setVisible(shown); +} + +void SubsectionTabs::track() { + using Event = Data::Session::ChatListEntryRefresh; + if (const auto forum = _history->peer->forum()) { + forum->topicDestroyed( + ) | rpl::start_with_next([=](not_null<Data::ForumTopic*> topic) { + if (_around == topic) { + _around = _history; + refreshSlice(); + } + }, _lifetime); + + forum->topicsList()->unreadStateChanges( + ) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + + forum->owner().chatListEntryRefreshes( + ) | rpl::filter([=](const Event &event) { + const auto topic = event.filterId ? nullptr : event.key.topic(); + return (topic && topic->forum() == forum); + }) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + } else if (const auto monoforum = _history->peer->monoforum()) { + monoforum->sublistDestroyed( + ) | rpl::start_with_next([=](not_null<Data::SavedSublist*> sublist) { + if (_around == sublist) { + _around = _history; + refreshSlice(); + } + }, _lifetime); + + monoforum->chatsList()->unreadStateChanges( + ) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + + monoforum->owner().chatListEntryRefreshes( + ) | rpl::filter([=](const Event &event) { + const auto sublist = event.filterId + ? nullptr + : event.key.sublist(); + return (sublist && sublist->parent() == monoforum); + }) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + } else { + Unexpected("Peer in SubsectionTabs::track."); + } +} + +void SubsectionTabs::refreshAroundMiddle( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider) { + Expects(!_slice.empty()); + + const auto full = _vertical ? scroll->height() : scroll->width(); + const auto scrollValue = _vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + const auto scrollMax = _vertical + ? scroll->scrollTopMax() + : scroll->scrollLeftMax(); + + auto best = -1; + auto bestDistance = -1; + const auto ideal = scrollValue + (full / 2); + for (auto i = 0, count = int(_slice.size()); i != count; ++i) { + const auto a = slider->lookupSectionPosition(i); + const auto b = (i + 1 == count) + ? (full + scrollMax) + : slider->lookupSectionPosition(i + 1); + const auto middle = (a + b) / 2; + const auto distance = std::abs(middle - ideal); + if (best < 0 || distance < bestDistance) { + best = i; + bestDistance = distance; + } + } + + Assert(best >= 0 && best < _slice.size()); + + _around = _slice[best].thread; + refreshSlice(); +} + +void SubsectionTabs::refreshSlice() { + _refreshScheduled = false; + + const auto forum = _history->peer->forum(); + const auto monoforum = _history->peer->monoforum(); + Assert(forum || monoforum); + + const auto list = forum + ? forum->topicsList() + : monoforum->chatsList(); + auto slice = std::vector<Item>(); + slice.reserve(_slice.size() + 10); + const auto guard = gsl::finally([&] { + if (_slice != slice) { + _slice = std::move(slice); + _refreshed.fire({}); + } + }); + const auto push = [&](not_null<Data::Thread*> thread) { + const auto topic = thread->asTopic(); + const auto sublist = thread->asSublist(); + auto badges = [&] { + if (!topic && !sublist) { + return Dialogs::BadgesState(); + } else if (thread->chatListUnreadState().known) { + return thread->chatListBadgesState(); + } + const auto i = ranges::find(_slice, thread, &Item::thread); + if (i != end(_slice)) { + // While the unread count is unknown (possibly loading) + // we can preserve the old badges state, because it won't + // glitch that way when we stop knowing it for a moment. + return i->badges; + } + return thread->chatListBadgesState(); + }(); + if (topic) { + // Don't show the small indicators for non-visited unread topics. + badges.unread = false; + } + slice.push_back({ + .thread = thread, + .badges = badges, + .iconId = topic ? topic->iconId() : DocumentId(), + .name = thread->chatListName(), + }); + }; + if (!list) { + push(_history); + _beforeSkipped = _afterSkipped = 0; + _afterAvailable = 0; + return; + } + const auto &chats = list->indexed()->all(); + auto i = (_around == _history) + ? chats.end() + : ranges::find(chats, _around, [](not_null<Dialogs::Row*> row) { + return not_null(row->thread()); + }); + if (i == chats.end()) { + i = chats.begin(); + } + const auto takeBefore = std::min(_beforeLimit, int(i - chats.begin())); + const auto takeAfter = std::min(_afterLimit, int(chats.end() - i)); + const auto from = i - takeBefore; + const auto till = i + takeAfter; + _beforeSkipped = std::max(0, int(from - chats.begin())); + _afterAvailable = std::max(0, int(chats.end() - till)); + _afterSkipped = list->loaded() ? _afterAvailable : std::optional<int>(); + if (from == chats.begin()) { + push(_history); + } + for (auto i = from; i != till; ++i) { + push((*i)->thread()); + } +} + +void SubsectionTabs::scheduleRefresh() { + if (_refreshScheduled) { + return; + } + _refreshScheduled = true; + InvokeQueued(_shadow, [=] { + if (_refreshScheduled) { + refreshSlice(); + } + }); +} + +Main::Session &SubsectionTabs::session() { + return _history->session(); +} + +bool SubsectionTabs::switchTo( + not_null<Data::Thread*> thread, + not_null<Ui::RpWidget*> parent) { + Expects((_horizontal || _vertical) && _shadow); + + if (thread->owningHistory() != _history) { + return false; + } + _active = thread; + if (_vertical) { + _vertical->setParent(parent); + _vertical->show(); + } else { + _horizontal->setParent(parent); + _horizontal->show(); + } + _shadow->setParent(parent); + _shadow->show(); + _refreshed.fire({}); + return true; +} + +bool SubsectionTabs::UsedFor(not_null<Data::Thread*> thread) { + const auto history = thread->owningHistory(); + if (history->amMonoforumAdmin()) { + return true; + } + const auto channel = history->peer->asChannel(); + return channel && channel->useSubsectionTabs(); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h new file mode 100644 index 0000000000..75a37104ea --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -0,0 +1,130 @@ +/* +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/unique_qptr.h" +#include "dialogs/dialogs_common.h" + +class History; + +namespace Data { +class Thread; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class RpWidget; +class PopupMenu; +class ScrollArea; +class SubsectionSlider; +} // namespace Ui + +namespace HistoryView { + +class SubsectionTabs final { +public: + SubsectionTabs( + not_null<Window::SessionController*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread); + ~SubsectionTabs(); + + [[nodiscard]] Main::Session &session(); + + [[nodiscard]] bool switchTo( + not_null<Data::Thread*> thread, + not_null<Ui::RpWidget*> parent); + + [[nodiscard]] static bool UsedFor(not_null<Data::Thread*> thread); + + [[nodiscard]] rpl::producer<> removeRequests() const; + + void extractToParent(not_null<Ui::RpWidget*> parent); + + void setBoundingRect(QRect boundingRect); + [[nodiscard]] rpl::producer<> layoutRequests() const; + [[nodiscard]] int leftSkip() const; + [[nodiscard]] int topSkip() const; + + void raise(); + void show(); + void hide(); + +private: + struct Item { + not_null<Data::Thread*> thread; + Dialogs::BadgesState badges; + DocumentId iconId = 0; + QString name; + + friend inline auto operator<=>( + const Item &, + const Item &) = default; + friend inline bool operator==( + const Item &, + const Item &) = default; + }; + + void track(); + void setupHorizontal(not_null<QWidget*> parent); + void setupVertical(not_null<QWidget*> parent); + void toggleModes(); + void setVisible(bool shown); + void refreshSlice(); + void refreshAroundMiddle( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider); + void scheduleRefresh(); + void loadMore(); + void setup(not_null<Ui::RpWidget*> parent); + [[nodiscard]] rpl::producer<> dataChanged() const; + + void setupSlider( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider, + bool vertical); + void showThreadContextMenu(not_null<Data::Thread*> thread); + + const not_null<Window::SessionController*> _controller; + const not_null<History*> _history; + + base::unique_qptr<Ui::PopupMenu> _menu; + + Ui::RpWidget *_horizontal = nullptr; + Ui::RpWidget *_vertical = nullptr; + Ui::RpWidget *_shadow = nullptr; + + std::vector<Item> _slice; + std::vector<Item> _sectionsSlice; + + not_null<Data::Thread*> _active; + not_null<Data::Thread*> _around; + int _beforeLimit = 0; + int _afterLimit = 0; + int _afterAvailable = 0; + bool _loading = false; + bool _refreshScheduled = false; + std::optional<int> _beforeSkipped; + std::optional<int> _afterSkipped; + + rpl::event_stream<> _layoutRequests; + rpl::event_stream<> _refreshed; + rpl::event_stream<> _scrollCheckRequests; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 20d8d5232b..11fb594b0e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" // GroupCall::input. #include "data/data_folder.h" #include "data/data_forum.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories.h" @@ -55,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_forum_topic.h" #include "data/data_send_action.h" +#include "dialogs/dialogs_main_list.h" #include "chat_helpers/emoji_interactions.h" #include "base/unixtime.h" #include "support/support_helper.h" @@ -88,8 +90,10 @@ constexpr auto kEmojiInteractionSeenDuration = 3 * crl::time(1000); QString TopBarNameText( not_null<PeerData*> peer, - Dialogs::EntryState::Section section) { - if (section == Dialogs::EntryState::Section::SavedSublist) { + const Dialogs::EntryState &state) { + if (state.section == Dialogs::EntryState::Section::SavedSublist + && state.key.sublist() + && state.key.sublist()->owningHistory()->peer->isSelf()) { if (peer->isSelf()) { return tr::lng_my_notes(tr::now); } else if (peer->isSavedHiddenAuthor()) { @@ -419,6 +423,10 @@ void TopBarWidget::toggleInfoSection() { (_activeChat.key.topic() ? std::make_shared<Info::Memento>( _activeChat.key.topic()) + : (_activeChat.key.sublist() + && _activeChat.key.sublist()->parentChat()) + ? std::make_shared<Info::Memento>( + _activeChat.key.sublist()) : Info::Memento::Default(_activeChat.key.peer())), Window::SectionShow().withThirdColumn()); } else { @@ -515,9 +523,15 @@ void TopBarWidget::paintTopBar(Painter &p) { const auto sublist = _activeChat.key.sublist(); const auto topic = _activeChat.key.topic(); const auto history = _activeChat.key.history(); - const auto namePeer = history + const auto broadcastForMonoforum = history + ? history->peer->monoforumBroadcast() + : nullptr; + const auto namePeer = broadcastForMonoforum + ? broadcastForMonoforum + : history ? history->peer.get() - : sublist ? sublist->peer().get() + : sublist + ? sublist->sublistPeer().get() : nullptr; if (topic && _activeChat.section == Section::Replies) { p.setPen(st::dialogsNameFg); @@ -596,7 +610,7 @@ void TopBarWidget::paintTopBar(Painter &p) { _titleNameVersion = namePeer->nameVersion(); _title.setText( st::msgNameStyle, - TopBarNameText(namePeer, _activeChat.section), + TopBarNameText(namePeer, _activeChat), Ui::NameTextOptions()); } if (const auto info = namePeer->botVerifyDetails()) { @@ -625,6 +639,7 @@ void TopBarWidget::paintTopBar(Painter &p) { .exteraSupporter = &st::dialogsExteraSupporterIcon.icon, .premium = &st::dialogsPremiumIcon.icon, .scam = &st::attentionButtonFg, + .direct = &st::windowSubTextFg, .premiumFg = &st::dialogsVerifiedIconBg, .customEmojiRepaint = [=] { update(); }, .now = now, @@ -769,10 +784,8 @@ void TopBarWidget::infoClicked() { return; } else if (const auto topic = key.topic()) { _controller->showSection(std::make_shared<Info::Memento>(topic)); - } else if ([[maybe_unused]] const auto sublist = key.sublist()) { - _controller->showSection(std::make_shared<Info::Memento>( - _controller->session().user(), - Info::Section(Storage::SharedMediaType::Photo))); + } else if (const auto sublist = key.sublist()) { + _controller->showSection(std::make_shared<Info::Memento>(sublist)); } else if (key.peer()->savedSublistsInfo()) { _controller->showSection(std::make_shared<Info::Memento>( key.peer(), @@ -954,10 +967,16 @@ void TopBarWidget::refreshInfoButton() { && !rootChatsListBar())) { _info.destroy(); } else if (const auto peer = _activeChat.key.peer()) { + const auto sublist = _activeChat.key.sublist(); + const auto infoPeer = sublist ? sublist->sublistPeer().get() : peer; auto info = object_ptr<Ui::UserpicButton>( this, - peer, - st::topBarInfoButton); + _controller, + infoPeer->userpicPaintingPeer(), + Ui::UserpicButton::Role::Custom, + Ui::UserpicButton::Source::PeerPhoto, + st::topBarInfoButton, + infoPeer->userpicShape()); info->showSavedMessagesOnSelf(true); _info.destroy(); _info = std::move(info); @@ -1185,6 +1204,9 @@ void TopBarWidget::updateControlsVisibility() { const auto hasPollsMenu = (_activeChat.key.peer() && _activeChat.key.peer()->canCreatePolls()) || (topic && Data::CanSend(topic, ChatRestriction::SendPolls)); + const auto hasTodoListsMenu = (_activeChat.key.peer() + && _activeChat.key.peer()->canCreateTodoLists()) + || (topic && Data::CanSend(topic, ChatRestriction::SendPolls)); const auto hasTopicMenu = [&] { if (!topic || section != Section::Replies) { return false; @@ -1204,9 +1226,9 @@ void TopBarWidget::updateControlsVisibility() { && (section == Section::History ? true : (section == Section::Scheduled) - ? hasPollsMenu + ? (hasPollsMenu || hasTodoListsMenu) : (section == Section::Replies) - ? (hasPollsMenu || hasTopicMenu) + ? (hasPollsMenu || hasTodoListsMenu || hasTopicMenu) : (section == Section::ChatsList) ? (_activeChat.key.peer() && _activeChat.key.peer()->isForum()) : false); @@ -1215,6 +1237,9 @@ void TopBarWidget::updateControlsVisibility() { ? true : (section == Section::Replies) ? (_activeChat.key.topic() != nullptr) + : (section == Section::SavedSublist) + ? (_activeChat.key.sublist() != nullptr + && _activeChat.key.sublist()->parentChat()) : false); updateSearchVisibility(); if (_searchMode) { @@ -1316,7 +1341,8 @@ void TopBarWidget::updateMembersShowArea() { } else if (const auto chat = peer->asChat()) { return chat->amIn(); } else if (const auto megagroup = peer->asMegagroup()) { - return megagroup->canViewMembers() + return !megagroup->isMonoforum() + && megagroup->canViewMembers() && (megagroup->membersCount() < megagroup->session().serverConfig().chatSizeMax); } @@ -1769,6 +1795,14 @@ void TopBarWidget::updateOnlineDisplay() { text = tr::lng_group_status(tr::now); } } + } else if (const auto monoforum = peer->monoforum()) { + const auto chats = monoforum->chatsList(); + const auto count = chats->fullSize().current(); + text = (count > 0) + ? tr::lng_filters_chats_count(tr::now, lt_count, count) + : tr::lng_filters_no_chats(tr::now); + } else if (peer->isMonoforum()) { + text = tr::lng_chat_status_direct(tr::now); } else if (const auto channel = peer->asChannel()) { if (channel->isMegagroup() && channel->canViewMembers() diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 576c1ac3d1..11e9cc8d7a 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -232,7 +232,7 @@ TranslateBar::TranslateBar( : _controller(controller) , _history(history) , _wrap(parent, object_ptr<Ui::AbstractButton>(parent)) -, _shadow(std::make_unique<Ui::PlainShadow>(_wrap.parentWidget())) { +, _shadow(std::make_unique<Ui::PlainShadow>(parent)) { _wrap.hide(anim::type::instant); _shadow->hide(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 7f3e132cbf..93aa428774 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -48,7 +48,7 @@ auto GenerateGiveawayStart( return Data{ .sticker = packs.lookup(months), .size = st::msgServiceGiftBoxStickerSize, - .singleTimePlayback = true, + .stopOnLastFrame = true, }; }; push(std::make_unique<StickerWithBadgePart>( @@ -222,7 +222,7 @@ auto GenerateGiveawayResults( .sticker = packs.lookup(emoji, 0), .skipTop = st::chatGiveawayWinnersTopSkip, .size = st::maxAnimatedEmojiSize, - .singleTimePlayback = true, + .stopOnLastFrame = true, }; }; push(std::make_unique<StickerWithBadgePart>( diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 1d3554972a..8741c51f69 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -589,6 +589,10 @@ QImage Media::locationTakeImage() { return QImage(); } +std::vector<Media::TodoTaskInfo> Media::takeTasksInfo() { + return {}; +} + TextState Media::getStateGrouped( const QRect &geometry, RectParts sides, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 844f0465c4..adf66c7c3d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -209,6 +209,14 @@ public: not_null<DocumentData*> data, const Lottie::ColorReplacements *replacements); virtual QImage locationTakeImage(); + + struct TodoTaskInfo { + int id = 0; + PeerData *completedBy = nullptr; + TimeId completionDate = TimeId(); + }; + virtual std::vector<TodoTaskInfo> takeTasksInfo(); + virtual void checkAnimation() { } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index bfac91b07d..198cd9980a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -77,6 +77,7 @@ MediaGeneric::MediaGeneric( MediaGenericDescriptor &&descriptor) : Media(parent) , _paintBg(std::move(descriptor.paintBg)) +, _fullAreaLink(descriptor.fullAreaLink) , _maxWidthCap(descriptor.maxWidth) , _service(descriptor.service) , _hideServiceText(descriptor.hideServiceText) { @@ -85,10 +86,6 @@ MediaGeneric::MediaGeneric( .object = std::move(part), }); }); - if (descriptor.serviceLink) { - parent->data()->setCustomServiceLink( - std::move(descriptor.serviceLink)); - } } MediaGeneric::~MediaGeneric() { @@ -133,7 +130,12 @@ void MediaGeneric::draw(Painter &p, const PaintContext &context) const { const auto radius = st::msgServiceGiftBoxRadius; p.setPen(Qt::NoPen); p.setBrush(context.st->msgServiceBg()); - p.drawRoundedRect(QRect(0, 0, width(), height()), radius, radius); + const auto rect = QRect(0, 0, width(), height()); + p.drawRoundedRect(rect, radius, radius); + //if (context.selected()) { + // p.setBrush(context.st->serviceTextPalette().selectBg); + // p.drawRoundedRect(rect, radius, radius); + //} } auto translated = 0; @@ -157,6 +159,11 @@ TextState MediaGeneric::textState( return result; } + if (_fullAreaLink && QRect(0, 0, width(), height()).contains(point)) { + result.link = _fullAreaLink; + return result; + } + for (const auto &entry : _entries) { const auto raw = entry.object.get(); const auto height = raw->height(); @@ -236,9 +243,11 @@ MediaGenericTextPart::MediaGenericTextPart( QMargins margins, const style::TextStyle &st, const base::flat_map<uint16, ClickHandlerPtr> &links, - const Ui::Text::MarkedContext &context) + const Ui::Text::MarkedContext &context, + style::align align) : _text(st::msgMinWidth) -, _margins(margins) { +, _margins(margins) +, _align(align) { _text.setMarkedText( st, text, @@ -254,12 +263,18 @@ void MediaGenericTextPart::draw( not_null<const MediaGeneric*> owner, const PaintContext &context, int outerWidth) const { + const auto use = (width() - _margins.left() - _margins.right()); setupPen(p, owner, context); _text.draw(p, { - .position = { (outerWidth - width()) / 2, _margins.top() }, + .position = { + ((_align == style::al_top) + ? ((outerWidth - use) / 2) + : _margins.left()), + _margins.top(), + }, .outerWidth = outerWidth, - .availableWidth = width(), - .align = style::al_top, + .availableWidth = use, + .align = _align, .palette = &(owner->service() ? context.st->serviceTextPalette() : context.messageStyle()->textPalette), @@ -267,6 +282,7 @@ void MediaGenericTextPart::draw( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .elisionLines = elisionLines(), }); } @@ -280,34 +296,54 @@ void MediaGenericTextPart::setupPen( : context.messageStyle()->historyTextFg); } +int MediaGenericTextPart::elisionLines() const { + return 0; +} + TextState MediaGenericTextPart::textState( QPoint point, StateRequest request, int outerWidth) const { - point -= QPoint{ (outerWidth - width()) / 2, _margins.top() }; + const auto use = (width() - _margins.left() - _margins.right()); + point -= QPoint{ + ((_align == style::al_top) + ? ((outerWidth - use) / 2) + : _margins.left()), + _margins.top(), + }; auto result = TextState(); auto forText = request.forText(); - forText.align = style::al_top; - result.link = _text.getState(point, width(), forText).link; + forText.align = _align; + result.link = _text.getState(point, use, forText).link; return result; } QSize MediaGenericTextPart::countOptimalSize() { + const auto lines = elisionLines(); + const auto height = lines + ? std::min(_text.minHeight(), lines * _text.style()->font->height) + : _text.minHeight(); return { _margins.left() + _text.maxWidth() + _margins.right(), - _margins.top() + _text.minHeight() + _margins.bottom(), + _margins.top() + height + _margins.bottom(), }; } QSize MediaGenericTextPart::countCurrentSize(int newWidth) { auto skip = _margins.left() + _margins.right(); - const auto size = CountOptimalTextSize( - _text, - st::msgMinWidth, - std::max(st::msgMinWidth, newWidth - skip)); + const auto size = (_align == style::al_top) + ? CountOptimalTextSize( + _text, + st::msgMinWidth, + std::max(st::msgMinWidth, newWidth - skip)) + : QSize(newWidth - skip, _text.countHeight(newWidth - skip)); + const auto lines = elisionLines(); + const auto height = lines + ? std::min(size.height(), lines * _text.style()->font->height) + : size.height(); return { size.width() + skip, - _margins.top() + size.height() + _margins.bottom(), + _margins.top() + height + _margins.bottom(), }; } @@ -453,8 +489,8 @@ void StickerInBubblePart::ensureCreated(Element *replacing) const { _link = data.link; _skipTop = data.skipTop; _sticker.emplace(_parent, sticker, skipPremiumEffect, replacing); - if (data.singleTimePlayback) { - _sticker->setPlayingOnce(true); + if (data.stopOnLastFrame) { + _sticker->setStopOnLastFrame(true); } _sticker->initSize(data.size); _sticker->setCustomCachingTag(data.cacheTag); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index 0fd011914f..27cce6de31 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -57,7 +57,7 @@ struct MediaGenericDescriptor { Painter&, const PaintContext&, not_null<const MediaGeneric*>)> paintBg; - ClickHandlerPtr serviceLink; + ClickHandlerPtr fullAreaLink; bool service = false; bool hideServiceText = false; }; @@ -128,6 +128,7 @@ private: Painter&, const PaintContext&, not_null<const MediaGeneric*>)> _paintBg; + ClickHandlerPtr _fullAreaLink; int _maxWidthCap = 0; bool _service : 1 = false; bool _hideServiceText : 1 = false; @@ -141,7 +142,8 @@ public: QMargins margins, const style::TextStyle &st = st::defaultTextStyle, const base::flat_map<uint16, ClickHandlerPtr> &links = {}, - const Ui::Text::MarkedContext &context = {}); + const Ui::Text::MarkedContext &context = {}, + style::align align = style::al_top); void draw( Painter &p, @@ -161,10 +163,12 @@ protected: Painter &p, not_null<const MediaGeneric*> owner, const PaintContext &context) const; + virtual int elisionLines() const; private: Ui::Text::String _text; QMargins _margins; + style::align _align = {}; }; @@ -194,7 +198,7 @@ public: int skipTop = 0; int size = 0; ChatHelpers::StickerLottieSize cacheTag = {}; - bool singleTimePlayback = false; + bool stopOnLastFrame = false; ClickHandlerPtr link; explicit operator bool() const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 39679cb448..d3ef5e26a1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits_graphics.h" // GiftedCreditsBox #include "settings/settings_premium.h" // Settings::ShowGiftPremium #include "ui/chat/chat_style.h" +#include "ui/controls/ton_common.h" // kNanosInOne #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "window/window_session_controller.h" @@ -47,7 +48,7 @@ PremiumGift::PremiumGift( PremiumGift::~PremiumGift() = default; int PremiumGift::top() { - return starGift() + return (starGift() || tonGift()) ? st::msgServiceStarGiftStickerTop : st::msgServiceGiftBoxStickerTop; } @@ -57,7 +58,7 @@ int PremiumGift::width() { } QSize PremiumGift::size() { - return starGift() + return (starGift() || tonGift()) ? QSize( st::msgServiceStarGiftStickerSize, st::msgServiceStarGiftStickerSize) @@ -68,7 +69,13 @@ QSize PremiumGift::size() { TextWithEntities PremiumGift::title() { using namespace Ui::Text; - if (starGift()) { + if (tonGift()) { + return tr::lng_gift_ton_amount( + tr::now, + lt_count_decimal, + CreditsAmount(0, _data.count, CreditsType::Ton).value(), + Ui::Text::WithEntities); + } else if (starGift()) { const auto peer = _parent->history()->peer; return peer->isSelf() ? tr::lng_action_gift_self_subtitle(tr::now, WithEntities) @@ -114,7 +121,9 @@ TextWithEntities PremiumGift::title() { } TextWithEntities PremiumGift::subtitle() { - if (starGift()) { + if (tonGift()) { + return tr::lng_action_gift_got_ton(tr::now, Ui::Text::WithEntities); + } else if (starGift()) { const auto toChannel = _data.channel && _parent->history()->peer->isServiceUser(); return !_data.message.empty() @@ -237,11 +246,34 @@ rpl::producer<QString> PremiumGift::button() { : tr::lng_prize_open(); } -bool PremiumGift::buttonMinistars() { - return true; +std::optional<Ui::Premium::MiniStarsType> PremiumGift::buttonMinistars() { + return tonGift() + ? Ui::Premium::MiniStarsType::SlowDiamondStars + : Ui::Premium::MiniStarsType::SlowStars; } ClickHandlerPtr PremiumGift::createViewLink() { + if (tonGift()) { + const auto lifetime = std::make_shared<rpl::lifetime>(); + return std::make_shared<LambdaClickHandler>([=](ClickContext context) { + const auto my = context.other.value<ClickHandlerContext>(); + const auto weak = my.sessionWindow; + if (const auto window = weak.get()) { + window->session().credits().tonLoad(); + *lifetime = window->session().credits().tonLoadedValue( + ) | rpl::filter([=] { + if (const auto window = weak.get()) { + return window->session().credits().tonLoaded(); + } + return false; + }) | rpl::take(1) | rpl::start_with_next([=] { + if (const auto window = weak.get()) { + window->showSettings(Settings::CurrencyId()); + } + }); + } + }); + } if (auto link = OpenStarGiftLink(_parent->data())) { return link; } @@ -376,6 +408,10 @@ bool PremiumGift::gift() const { return _data.slug.isEmpty() || !_data.channel; } +bool PremiumGift::tonGift() const { + return (_data.type == Data::GiftType::Ton); +} + bool PremiumGift::starGift() const { return (_data.type == Data::GiftType::StarGift); } @@ -397,6 +433,19 @@ int PremiumGift::credits() const { void PremiumGift::ensureStickerCreated() const { if (_sticker) { return; + } else if (tonGift()) { + const auto &session = _parent->history()->session(); + auto &packs = session.giftBoxStickersPacks(); + const auto count = _data.count / Ui::kNanosInOne; + if (const auto document = packs.tonLookup(count)) { + if (document->sticker()) { + const auto skipPremiumEffect = false; + _sticker.emplace(_parent, document, skipPremiumEffect, _parent); + _sticker->setStopOnLastFrame(true); + _sticker->initSize(st::msgServiceGiftBoxStickerSize); + } + } + return; } else if (const auto document = _data.document) { const auto sticker = document->sticker(); Assert(sticker != nullptr); @@ -414,7 +463,7 @@ void PremiumGift::ensureStickerCreated() const { if (document->sticker()) { const auto skipPremiumEffect = false; _sticker.emplace(_parent, document, skipPremiumEffect, _parent); - _sticker->setPlayingOnce(true); + _sticker->setStopOnLastFrame(true); _sticker->initSize(st::msgServiceGiftBoxStickerSize); } } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index 86a30c094a..ef0bf67612 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -31,7 +31,7 @@ public: TextWithEntities title() override; TextWithEntities subtitle() override; rpl::producer<QString> button() override; - bool buttonMinistars() override; + std::optional<Ui::Premium::MiniStarsType> buttonMinistars() override; QImage cornerTag(const PaintContext &context) override; int buttonSkip() override; void draw( @@ -52,6 +52,7 @@ public: private: [[nodiscard]] bool incomingGift() const; [[nodiscard]] bool outgoingGift() const; + [[nodiscard]] bool tonGift() const; [[nodiscard]] bool starGift() const; [[nodiscard]] bool starGiftUpgrade() const; [[nodiscard]] bool gift() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp index 3cf6d7b776..5e078f7d19 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.cpp @@ -104,10 +104,10 @@ ServiceBox::ServiceBox( } }, _lifetime); } - if (_content->buttonMinistars()) { + if (const auto type = _content->buttonMinistars()) { _button.stars = std::make_unique<Ui::Premium::ColoredMiniStars>( [=](const QRect &) { repaint(); }, - Ui::Premium::MiniStars::Type::SlowStars); + *type); _button.lastFg = std::make_unique<QColor>(); } } diff --git a/Telegram/SourceFiles/history/view/media/history_view_service_box.h b/Telegram/SourceFiles/history/view/media/history_view_service_box.h index 7aced59258..fe7018e849 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_service_box.h +++ b/Telegram/SourceFiles/history/view/media/history_view_service_box.h @@ -15,6 +15,7 @@ class RippleAnimation; namespace Ui::Premium { class ColoredMiniStars; +enum class MiniStarsType; } // namespace Ui::Premium namespace HistoryView { @@ -32,8 +33,9 @@ public: return top(); } [[nodiscard]] virtual rpl::producer<QString> button() = 0; - [[nodiscard]] virtual bool buttonMinistars() { - return false; + [[nodiscard]] virtual auto buttonMinistars() + -> std::optional<Ui::Premium::MiniStarsType> { + return std::nullopt; } [[nodiscard]] virtual QImage cornerTag(const PaintContext &context) { return {}; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp index e262802beb..b8f4fd9f62 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp @@ -173,7 +173,7 @@ QSize Sticker::countOptimalSize() { } bool Sticker::readyToDrawAnimationFrame() { - if (!_lastDiceFrame.isNull()) { + if (!_lastFrameCached.isNull()) { return true; } const auto sticker = _data->sticker(); @@ -266,7 +266,7 @@ void Sticker::paintAnimationFrame( const QRect &r) { const auto colored = (customEmojiPart() && _data->emojiUsesTextColor()) ? ComputeEmojiTextColor(context) - : (context.selected() && !_nextLastDiceFrame) + : (context.selected() && !_nextLastFrame) ? context.st->msgStickerOverlay()->c : QColor(0, 0, 0, 0); const auto powerSavingFlag = (emojiSticker() || _diceIndex >= 0) @@ -281,13 +281,15 @@ void Sticker::paintAnimationFrame( context.now, paused) : StickerPlayer::FrameInfo(); - if (_nextLastDiceFrame) { - _nextLastDiceFrame = false; - _lastDiceFrame = CacheDiceImage(_diceEmoji, _diceIndex, frame.image); + if (_nextLastFrame) { + _nextLastFrame = false; + _lastFrameCached = (_diceIndex > 0) + ? CacheDiceImage(_diceEmoji, _diceIndex, frame.image) + : frame.image; } - const auto &image = _lastDiceFrame.isNull() + const auto &image = _lastFrameCached.isNull() ? frame.image - : _lastDiceFrame; + : _lastFrameCached; const auto rounding = Ui::BubbleRounding{ .topLeft = Ui::BubbleCornerRounding::Small, @@ -295,7 +297,7 @@ void Sticker::paintAnimationFrame( .bottomLeft = Ui::BubbleCornerRounding::Small, .bottomRight = Ui::BubbleCornerRounding::Small, }; - auto prepared = (!_lastDiceFrame.isNull() && context.selected()) + auto prepared = (!_lastFrameCached.isNull() && context.selected()) ? Images::Colored( base::duplicate(image), context.st->msgStickerOverlay()->c) @@ -309,25 +311,25 @@ void Sticker::paintAnimationFrame( r.y() + (r.height() - size.height()) / 2), size), prepared); - if (!_lastDiceFrame.isNull()) { + if (!_lastFrameCached.isNull()) { return; } const auto count = _player->framesCount(); _frameIndex = frame.index; _framesCount = count; - _nextLastDiceFrame = !paused - && (_diceIndex > 0) + _nextLastFrame = !paused + && _stopOnLastFrame && (_frameIndex + 2 == count); - const auto playOnce = (_playingOnce || _diceIndex > 0) + const auto playOnce = _playingOnce ? true : (_diceIndex == 0) ? false : ((!customEmojiPart() && emojiSticker()) || !Core::App().settings().loopAnimatedStickers()); - const auto lastDiceFrame = (_diceIndex > 0) && atTheEnd(); + const auto lastFrame = _stopOnLastFrame && atTheEnd(); const auto switchToNext = !playOnce - || (!lastDiceFrame && (_frameIndex != 0 || !_oncePlayed)); + || (!lastFrame && (_frameIndex != 0 || !_oncePlayed)); if (!paused && switchToNext && _player->markFrameShown() @@ -533,12 +535,19 @@ void Sticker::dataMediaCreated() const { void Sticker::setDiceIndex(const QString &emoji, int index) { _diceEmoji = emoji; _diceIndex = index; + _playingOnce = (index >= 0); + _stopOnLastFrame = (index > 0); } void Sticker::setPlayingOnce(bool once) { _playingOnce = once; } +void Sticker::setStopOnLastFrame(bool stop) { + _stopOnLastFrame = stop; + _playingOnce = true; +} + void Sticker::setCustomCachingTag(ChatHelpers::StickerLottieSize tag) { _cachingTag = tag; } @@ -609,8 +618,8 @@ void Sticker::unloadPlayer() { if (!_player) { return; } - if (_diceIndex > 0 && _lastDiceFrame.isNull()) { - _nextLastDiceFrame = false; + if (_stopOnLastFrame && _lastFrameCached.isNull()) { + _nextLastFrame = false; _oncePlayed = false; } _player = nullptr; diff --git a/Telegram/SourceFiles/history/view/media/history_view_sticker.h b/Telegram/SourceFiles/history/view/media/history_view_sticker.h index d2709d1539..629d330311 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_sticker.h +++ b/Telegram/SourceFiles/history/view/media/history_view_sticker.h @@ -67,6 +67,7 @@ public: void setDiceIndex(const QString &emoji, int index); void setPlayingOnce(bool once); + void setStopOnLastFrame(bool stop); void setCustomCachingTag(ChatHelpers::StickerLottieSize tag); void setCustomEmojiPart(); void setEmojiSticker(); @@ -128,7 +129,7 @@ private: mutable std::shared_ptr<Data::DocumentMedia> _dataMedia; ClickHandlerPtr _link; QSize _size; - QImage _lastDiceFrame; + QImage _lastFrameCached; QString _diceEmoji; int _diceIndex = -1; mutable int _frameIndex = -1; @@ -137,12 +138,13 @@ private: mutable bool _oncePlayed : 1 = false; mutable bool _premiumEffectPlayed : 1 = false; mutable bool _premiumEffectSkipped : 1 = false; - mutable bool _nextLastDiceFrame : 1 = false; + mutable bool _nextLastFrame : 1 = false; bool _skipPremiumEffect : 1 = false; bool _customEmojiPart : 1 = false; bool _emojiSticker : 1 = false; bool _webpagePart : 1 = false; bool _playingOnce : 1 = false; + bool _stopOnLastFrame : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp new file mode 100644 index 0000000000..3b7f24b0f8 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -0,0 +1,370 @@ +/* +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 "history/view/media/history_view_suggest_decision.h" + +#include "base/unixtime.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "history/view/media/history_view_media_generic.h" +#include "history/view/media/history_view_unique_gift.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "ui/chat/chat_style.h" +#include "ui/text/text_utilities.h" +#include "ui/text/format_values.h" +#include "styles/style_chat.h" +#include "styles/style_credits.h" + +namespace HistoryView { +namespace { + +constexpr auto kFadedOpacity = 0.85; + +enum EmojiType { + kAgreement, + kCalendar, + kMoney, + kHourglass, + kReload, + kDecline, + kDiscard, + kWarning, +}; + +[[nodiscard]] const char *Raw(EmojiType type) { + switch (type) { + case EmojiType::kAgreement: return "\xf0\x9f\xa4\x9d"; + case EmojiType::kCalendar: return "\xf0\x9f\x93\x86"; + case EmojiType::kMoney: return "\xf0\x9f\x92\xb0"; + case EmojiType::kHourglass: return "\xe2\x8c\x9b\xef\xb8\x8f"; + case EmojiType::kReload: return "\xf0\x9f\x94\x84"; + case EmojiType::kDecline: return "\xe2\x9d\x8c"; + case EmojiType::kDiscard: return "\xf0\x9f\x9a\xab"; + case EmojiType::kWarning: return "\xe2\x9a\xa0\xef\xb8\x8f"; + } + Unexpected("EmojiType in Raw."); +} + +[[nodiscard]] QString Emoji(EmojiType type) { + return QString::fromUtf8(Raw(type)); +} + +struct Changes { + bool date = false; + bool price = false; + bool message = true; +}; +[[nodiscard]] std::optional<Changes> ResolveChanges( + not_null<HistoryItem*> changed, + HistoryItem *original) { + const auto wasSuggest = original + ? original->Get<HistoryMessageSuggestedPost>() + : nullptr; + const auto nowSuggest = changed->Get<HistoryMessageSuggestedPost>(); + if (!wasSuggest || !nowSuggest) { + return {}; + } + auto result = Changes(); + if (wasSuggest->date != nowSuggest->date) { + result.date = true; + } + if (wasSuggest->price != nowSuggest->price) { + result.price = true; + } + const auto wasText = original->originalText(); + const auto nowText = changed->originalText(); + const auto mediaSame = [&] { + const auto wasMedia = original->media(); + const auto nowMedia = changed->media(); + if (!wasMedia && !nowMedia) { + return true; + } else if (!wasMedia + || !nowMedia + || !wasMedia->allowsEditCaption() + || !nowMedia->allowsEditCaption()) { + return false; + } + // We treat as "same" only same photo or same file. + return (wasMedia->photo() == nowMedia->photo()) + && (wasMedia->document() == nowMedia->document()); + }; + if (!result.price && !result.date) { + result.message = true; + } else if (wasText == nowText && mediaSame()) { + result.message = false; + } + return result; +} + +} // namespace + +auto GenerateSuggestDecisionMedia( + not_null<Element*> parent, + not_null<const HistoryServiceSuggestDecision*> decision) + -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)> { + return [=]( + not_null<MediaGeneric*> media, + Fn<void(std::unique_ptr<MediaGenericPart>)> push) { + const auto peer = parent->history()->peer; + const auto broadcast = peer->monoforumBroadcast(); + if (!broadcast) { + return; + } + + const auto sublistPeerId = parent->data()->sublistPeerId(); + const auto sublistPeer = peer->owner().peer(sublistPeerId); + + auto pushText = [&]( + TextWithEntities text, + QMargins margins = {}, + style::align align = style::al_left, + const base::flat_map<uint16, ClickHandlerPtr> &links = {}) { + push(std::make_unique<MediaGenericTextPart>( + std::move(text), + margins, + st::defaultTextStyle, + links, + Ui::Text::MarkedContext(), + align)); + }; + + if (decision->balanceTooLow) { + pushText( + TextWithEntities( + ).append(Emoji(kWarning)).append(' ').append( + (sublistPeer->isSelf() + ? (decision->price.ton() + ? tr::lng_suggest_action_your_not_enough_ton + : tr::lng_suggest_action_your_not_enough_stars) + : (decision->price.ton() + ? tr::lng_suggest_action_his_not_enough_ton + : tr::lng_suggest_action_his_not_enough_stars))( + tr::now, + Ui::Text::RichLangValue)), + st::chatSuggestInfoFullMargin, + style::al_top); + } else if (decision->rejected) { + const auto withComment = !decision->rejectComment.isEmpty(); + pushText( + TextWithEntities( + ).append(Emoji(kDecline)).append(' ').append( + (withComment + ? tr::lng_suggest_action_declined_reason + : tr::lng_suggest_action_declined)( + tr::now, + lt_from, + Ui::Text::Bold(broadcast->name()), + Ui::Text::WithEntities)), + (withComment + ? st::chatSuggestInfoTitleMargin + : st::chatSuggestInfoFullMargin)); + if (withComment) { + const auto fadedFg = [](const PaintContext &context) { + auto result = context.st->msgServiceFg()->c; + result.setAlphaF(result.alphaF() * kFadedOpacity); + return result; + }; + push(std::make_unique<TextPartColored>( + TextWithEntities().append('"').append( + decision->rejectComment + ).append('"'), + st::chatSuggestInfoLastMargin, + fadedFg)); + } + } else { + const auto price = decision->price; + pushText( + TextWithEntities( + ).append(Emoji(kAgreement)).append(' ').append( + Ui::Text::Bold(tr::lng_suggest_action_agreement(tr::now)) + ), + st::chatSuggestInfoTitleMargin, + style::al_top); + const auto date = base::unixtime::parse(decision->date); + pushText( + TextWithEntities( + ).append(Emoji(kCalendar)).append(' ').append( + tr::lng_suggest_action_agree_date( + tr::now, + lt_channel, + Ui::Text::Bold(broadcast->name()), + lt_date, + Ui::Text::Bold(tr::lng_mediaview_date_time( + tr::now, + lt_date, + QLocale().toString( + date.date(), + QLocale::ShortFormat), + lt_time, + QLocale().toString( + date.time(), + QLocale::ShortFormat))), + Ui::Text::WithEntities)), + (price + ? st::chatSuggestInfoMiddleMargin + : st::chatSuggestInfoLastMargin)); + if (price) { + pushText( + TextWithEntities( + ).append(Emoji(kMoney)).append(' ').append( + (sublistPeer->isSelf() + ? (price.stars() + ? tr::lng_suggest_action_your_charged_stars + : tr::lng_suggest_action_your_charged_ton)( + tr::now, + lt_count_decimal, + price.value(), + Ui::Text::RichLangValue) + : (price.stars() + ? tr::lng_suggest_action_his_charged_stars + : tr::lng_suggest_action_his_charged_ton)( + tr::now, + lt_count_decimal, + price.value(), + lt_from, + Ui::Text::Bold(sublistPeer->shortName()), + Ui::Text::RichLangValue))), + st::chatSuggestInfoMiddleMargin); + + pushText( + TextWithEntities( + ).append(Emoji(kHourglass)).append(' ').append( + (price.ton() + ? tr::lng_suggest_action_agree_receive_ton + : tr::lng_suggest_action_agree_receive_stars)( + tr::now, + lt_channel, + Ui::Text::Bold(broadcast->name()), + Ui::Text::WithEntities)), + st::chatSuggestInfoMiddleMargin); + + pushText( + TextWithEntities( + ).append(Emoji(kReload)).append(' ').append( + (price.ton() + ? tr::lng_suggest_action_agree_removed_ton + : tr::lng_suggest_action_agree_removed_stars)( + tr::now, + lt_channel, + Ui::Text::Bold(broadcast->name()), + Ui::Text::WithEntities)), + st::chatSuggestInfoLastMargin); + } + } + }; +} + +auto GenerateSuggestRequestMedia( + not_null<Element*> parent, + not_null<const HistoryMessageSuggestedPost*> suggest) + -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)> { + return [=]( + not_null<MediaGeneric*> media, + Fn<void(std::unique_ptr<MediaGenericPart>)> push) { + const auto normalFg = [](const PaintContext &context) { + return context.st->msgServiceFg()->c; + }; + const auto fadedFg = [](const PaintContext &context) { + auto result = context.st->msgServiceFg()->c; + result.setAlphaF(result.alphaF() * kFadedOpacity); + return result; + }; + const auto item = parent->data(); + const auto replyData = item->Get<HistoryMessageReply>(); + const auto original = replyData + ? replyData->resolvedMessage.get() + : nullptr; + const auto changes = ResolveChanges(item, original); + const auto from = item->from(); + + auto pushText = [&]( + TextWithEntities text, + QMargins margins = {}, + style::align align = style::al_left, + const base::flat_map<uint16, ClickHandlerPtr> &links = {}) { + push(std::make_unique<MediaGenericTextPart>( + std::move(text), + margins, + st::defaultTextStyle, + links, + Ui::Text::MarkedContext(), + align)); + }; + + pushText( + ((!changes && from->isSelf()) + ? tr::lng_suggest_action_your( + tr::now, + Ui::Text::WithEntities) + : (!changes + ? tr::lng_suggest_action_his + : changes->message + ? tr::lng_suggest_change_content + : (changes->date && changes->price) + ? tr::lng_suggest_change_price_time + : changes->price + ? tr::lng_suggest_change_price + : tr::lng_suggest_change_time)( + tr::now, + lt_from, + Ui::Text::Bold(from->shortName()), + Ui::Text::WithEntities)), + st::chatSuggestInfoTitleMargin, + style::al_top); + + auto entries = std::vector<AttributeTable::Entry>(); + entries.push_back({ + ((changes && changes->price) + ? tr::lng_suggest_change_price_label + : tr::lng_suggest_action_price_label)(tr::now), + Ui::Text::Bold(!suggest->price + ? tr::lng_suggest_action_price_free(tr::now) + : suggest->price.stars() + ? tr::lng_suggest_stars_amount( + tr::now, + lt_count_decimal, + suggest->price.value()) + : tr::lng_suggest_ton_amount( + tr::now, + lt_count_decimal, + suggest->price.value())), + }); + entries.push_back({ + ((changes && changes->date) + ? tr::lng_suggest_change_time_label + : tr::lng_suggest_action_time_label)(tr::now), + Ui::Text::Bold(suggest->date + ? Ui::FormatDateTime(base::unixtime::parse(suggest->date)) + : tr::lng_suggest_action_time_any(tr::now)), + }); + push(std::make_unique<AttributeTable>( + std::move(entries), + ((changes && changes->message) + ? st::chatSuggestTableMiddleMargin + : st::chatSuggestTableLastMargin), + fadedFg, + normalFg)); + if (changes && changes->message) { + push(std::make_unique<TextPartColored>( + tr::lng_suggest_change_text_label( + tr::now, + Ui::Text::WithEntities), + st::chatSuggestInfoLastMargin, + fadedFg)); + } + }; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h new file mode 100644 index 0000000000..56fc4f58f1 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h @@ -0,0 +1,33 @@ +/* +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 + +struct HistoryMessageSuggestedPost; +struct HistoryServiceSuggestDecision; + +namespace HistoryView { + +class Element; +class MediaGeneric; +class MediaGenericPart; + +auto GenerateSuggestDecisionMedia( + not_null<Element*> parent, + not_null<const HistoryServiceSuggestDecision*> decision +) -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)>; + +auto GenerateSuggestRequestMedia( + not_null<Element*> parent, + not_null<const HistoryMessageSuggestedPost*> suggest +) -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)>; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp new file mode 100644 index 0000000000..4f37d36d0c --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -0,0 +1,871 @@ +/* +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 "history/view/media/history_view_todo_list.h" + +#include "base/unixtime.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "core/ui_integration.h" // TextContext +#include "lang/lang_keys.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "history/view/history_view_message.h" +#include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_text_helper.h" +#include "calls/calls_instance.h" +#include "ui/chat/message_bubble.h" +#include "ui/chat/chat_style.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" +#include "ui/text/format_values.h" +#include "ui/effects/animations.h" +#include "ui/effects/radial_animation.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/fireworks_animation.h" +#include "ui/toast/toast.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "data/data_media_types.h" +#include "data/data_poll.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "base/unixtime.h" +#include "base/timer.h" +#include "main/main_session.h" +#include "apiwrap.h" +#include "api/api_todo_lists.h" +#include "window/window_peer_menu.h" +#include "styles/style_chat.h" +#include "styles/style_widgets.h" +#include "styles/style_window.h" + +namespace HistoryView { +namespace { + +constexpr auto kShowRecentVotersCount = 3; +constexpr auto kRotateSegments = 8; +constexpr auto kRotateAmplitude = 3.; +constexpr auto kScaleSegments = 2; +constexpr auto kScaleAmplitude = 0.03; +constexpr auto kLargestRadialDuration = 30 * crl::time(1000); +constexpr auto kCriticalCloseDuration = 5 * crl::time(1000); + +} // namespace + +struct TodoList::Task { + Task(); + + void fillData( + not_null<Element*> view, + not_null<TodoListData*> todolist, + const TodoListItem &original, + Ui::Text::MarkedContext context); + void setCompletedBy(PeerData *by); + + Ui::Text::String text; + Ui::Text::String name; + PeerData *completedBy = nullptr; + mutable Ui::PeerUserpicView userpic; + TimeId completionDate = 0; + int id = 0; + ClickHandlerPtr handler; + Ui::Animations::Simple selectedAnimation; + mutable std::unique_ptr<Ui::RippleAnimation> ripple; +}; + +TodoList::Task::Task() +: text(st::msgMinWidth / 2) +, name(st::msgMinWidth / 2) { +} + +void TodoList::Task::fillData( + not_null<Element*> view, + not_null<TodoListData*> todolist, + const TodoListItem &original, + Ui::Text::MarkedContext context) { + id = original.id; + setCompletedBy(original.completedBy); + completionDate = original.completionDate; + if (!text.isEmpty() && text.toTextWithEntities() == original.text) { + return; + } + text.setMarkedText( + st::historyPollAnswerStyle, + original.text, + Ui::WebpageTextTitleOptions(), + context); + InitElementTextPart(view, text); +} + +void TodoList::Task::setCompletedBy(PeerData *by) { + if (!by || completedBy == by) { + return; + } + completedBy = by; + name.setText(st::historyPollAnswerStyle, completedBy->name()); +} + +TodoList::TodoList( + not_null<Element*> parent, + not_null<TodoListData*> todolist, + Element *replacing) +: Media(parent) +, _todolist(todolist) +, _title(st::msgMinWidth / 2) { + history()->owner().registerTodoListView(_todolist, _parent); + if (const auto media = replacing ? replacing->media() : nullptr) { + const auto info = media->takeTasksInfo(); + if (!info.empty()) { + setupPreviousState(info); + } + } +} + +void TodoList::setupPreviousState(const std::vector<TodoTaskInfo> &info) { + // If we restore state from the view we're replacing we'll be able to + // animate the changes properly. + updateTasks(true); + for (auto &task : _tasks) { + const auto i = ranges::find(info, task.id, &TodoTaskInfo::id); + if (i != end(info)) { + task.setCompletedBy(i->completedBy); + task.completionDate = i->completionDate; + } + } +} + +QSize TodoList::countOptimalSize() { + updateTexts(); + + const auto paddings = st::msgPadding.left() + st::msgPadding.right(); + + auto maxWidth = st::msgFileMinWidth; + accumulate_max(maxWidth, paddings + _title.maxWidth()); + for (const auto &task : _tasks) { + accumulate_max( + maxWidth, + paddings + + st::historyChecklistTaskPadding.left() + + task.text.maxWidth() + + st::historyChecklistTaskPadding.right()); + } + + const auto tasksHeight = ranges::accumulate(ranges::views::all( + _tasks + ) | ranges::views::transform([](const Task &task) { + return st::historyChecklistTaskPadding.top() + + task.text.minHeight() + + st::historyChecklistTaskPadding.bottom(); + }), 0); + + const auto bottomButtonHeight = st::historyPollBottomButtonSkip; + auto minHeight = st::historyPollQuestionTop + + _title.minHeight() + + st::historyPollSubtitleSkip + + st::msgDateFont->height + + st::historyPollAnswersSkip + + tasksHeight + + st::historyPollTotalVotesSkip + + bottomButtonHeight + + st::msgDateFont->height + + st::msgPadding.bottom(); + if (!isBubbleTop()) { + minHeight -= st::msgFileTopMinus; + } + return { maxWidth, minHeight }; +} + +bool TodoList::canComplete() const { + return (_parent->data()->out() + || _parent->history()->peer->isSelf() + || _todolist->othersCanComplete()) + && _parent->data()->isRegular() + && !_parent->data()->Has<HistoryMessageForwarded>(); +} + +int TodoList::countTaskTop( + const Task &task, + int innerWidth) const { + auto tshift = st::historyPollQuestionTop; + if (!isBubbleTop()) { + tshift -= st::msgFileTopMinus; + } + tshift += _title.countHeight(innerWidth) + st::historyPollSubtitleSkip; + tshift += st::msgDateFont->height + st::historyPollAnswersSkip; + const auto i = ranges::find( + _tasks, + &task, + [](const Task &task) { return &task; }); + const auto countHeight = [&](const Task &task) { + return countTaskHeight(task, innerWidth); + }; + tshift += ranges::accumulate( + begin(_tasks), + i, + 0, + ranges::plus(), + countHeight); + return tshift; +} + +int TodoList::countTaskHeight( + const Task &task, + int innerWidth) const { + const auto answerWidth = innerWidth + - st::historyChecklistTaskPadding.left() + - st::historyChecklistTaskPadding.right(); + return st::historyChecklistTaskPadding.top() + + task.text.countHeight(answerWidth) + + st::historyChecklistTaskPadding.bottom(); +} + +QSize TodoList::countCurrentSize(int newWidth) { + accumulate_min(newWidth, maxWidth()); + const auto innerWidth = newWidth + - st::msgPadding.left() + - st::msgPadding.right(); + + const auto tasksHeight = ranges::accumulate(ranges::views::all( + _tasks + ) | ranges::views::transform([&](const Task &task) { + return countTaskHeight(task, innerWidth); + }), 0); + + const auto bottomButtonHeight = st::historyPollBottomButtonSkip; + auto newHeight = st::historyPollQuestionTop + + _title.countHeight(innerWidth) + + st::historyPollSubtitleSkip + + st::msgDateFont->height + + st::historyPollAnswersSkip + + tasksHeight + + st::historyPollTotalVotesSkip + + bottomButtonHeight + + st::msgDateFont->height + + st::msgPadding.bottom(); + if (!isBubbleTop()) { + newHeight -= st::msgFileTopMinus; + } + return { newWidth, newHeight }; +} + +void TodoList::updateTexts() { + if (_todoListVersion == _todolist->version) { + return; + } + const auto skipAnimations = _tasks.empty(); + _todoListVersion = _todolist->version; + + if (_title.toTextWithEntities() != _todolist->title) { + auto options = Ui::WebpageTextTitleOptions(); + options.maxw = options.maxh = 0; + _title.setMarkedText( + st::historyPollQuestionStyle, + _todolist->title, + options, + Core::TextContext({ + .session = &_todolist->session(), + .repaint = [=] { repaint(); }, + .customEmojiLoopLimit = 2, + })); + InitElementTextPart(_parent, _title); + } + if (_flags != _todolist->flags() || _subtitle.isEmpty()) { + _flags = _todolist->flags(); + _subtitle.setText( + st::msgDateTextStyle, + (_todolist->othersCanComplete() + ? tr::lng_todo_title_group(tr::now) + : tr::lng_todo_title(tr::now))); + } + updateTasks(skipAnimations); +} + +void TodoList::updateTasks(bool skipAnimations) { + const auto context = Core::TextContext({ + .session = &_todolist->session(), + .repaint = [=] { repaint(); }, + .customEmojiLoopLimit = 2, + }); + const auto changed = !ranges::equal( + _tasks, + _todolist->items, + ranges::equal_to(), + &Task::id, + &TodoListItem::id); + if (!changed) { + auto animated = false; + auto &&tasks = ranges::views::zip(_tasks, _todolist->items); + for (auto &&[task, original] : tasks) { + const auto wasDate = task.completionDate; + task.fillData(_parent, _todolist, original, context); + if (!skipAnimations && (!wasDate != !task.completionDate)) { + startToggleAnimation(task); + animated = true; + } + } + updateCompletionStatus(); + if (animated) { + maybeStartFireworks(); + } + return; + } + _tasks = ranges::views::all( + _todolist->items + ) | ranges::views::transform([&](const TodoListItem &item) { + auto result = Task(); + result.id = item.id; + result.fillData(_parent, _todolist, item, context); + return result; + }) | ranges::to_vector; + + for (auto &task : _tasks) { + task.handler = createTaskClickHandler(task); + } + + updateCompletionStatus(); +} + +ClickHandlerPtr TodoList::createTaskClickHandler( + const Task &task) { + const auto id = task.id; + return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] { + toggleCompletion(id); + })); +} + +void TodoList::startToggleAnimation(Task &task) { + const auto selected = (task.completionDate != 0); + task.selectedAnimation.start( + [=] { repaint(); }, + selected ? 0. : 1., + selected ? 1. : 0., + st::defaultCheck.duration); +} + +void TodoList::toggleCompletion(int id) { + if (_parent->data()->isBusinessShortcut()) { + return; + } else if (_parent->data()->Has<HistoryMessageForwarded>()) { + _parent->delegate()->elementShowTooltip( + tr::lng_todo_mark_forwarded(tr::now, Ui::Text::RichLangValue), + [] {}); + return; + } else if (!canComplete()) { + _parent->delegate()->elementShowTooltip( + tr::lng_todo_mark_restricted( + tr::now, + lt_user, + Ui::Text::Bold(_parent->data()->from()->shortName()), + Ui::Text::RichLangValue), [] {}); + return; + } else if (!_parent->history()->session().premium()) { + Window::PeerMenuTodoWantsPremium(Window::TodoWantsPremium::Mark); + return; + } + const auto i = ranges::find( + _tasks, + id, + &Task::id); + if (i == end(_tasks)) { + return; + } + const auto selected = (i->completionDate != 0); + i->completionDate = selected ? TimeId() : base::unixtime::now(); + if (!selected) { + i->setCompletedBy(_parent->history()->session().user()); + } + startToggleAnimation(*i); + repaint(); + + history()->session().api().todoLists().toggleCompletion( + _parent->data()->fullId(), + id, + !selected); + + maybeStartFireworks(); +} + +void TodoList::maybeStartFireworks() { + if (!ranges::contains(_tasks, TimeId(), &Task::completionDate)) { + _fireworksAnimation = std::make_unique<Ui::FireworksAnimation>( + [=] { repaint(); }); + } +} + +void TodoList::updateCompletionStatus() { + const auto incompleted = int(ranges::count( + _todolist->items, + nullptr, + &TodoListItem::completedBy)); + const auto total = int(_todolist->items.size()); + if (_total == total + && _incompleted == incompleted + && !_completionStatusLabel.isEmpty()) { + return; + } + _total = total; + _incompleted = incompleted; + const auto totalText = QString::number(total); + const auto string = (incompleted == total) + ? tr::lng_todo_completed_none(tr::now, lt_total, totalText) + : tr::lng_todo_completed( + tr::now, + lt_count, + total - incompleted, + lt_total, + totalText); + _completionStatusLabel.setText(st::msgDateTextStyle, string); +} + +void TodoList::draw(Painter &p, const PaintContext &context) const { + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + auto paintw = width(); + + const auto stm = context.messageStyle(); + const auto padding = st::msgPadding; + auto tshift = st::historyPollQuestionTop; + if (!isBubbleTop()) { + tshift -= st::msgFileTopMinus; + } + paintw -= padding.left() + padding.right(); + + p.setPen(stm->historyTextFg); + _title.draw(p, { + .position = { padding.left(), tshift }, + .availableWidth = paintw, + .palette = &stm->textPalette, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + .selection = context.selection, + }); + tshift += _title.countHeight(paintw) + st::historyPollSubtitleSkip; + + p.setPen(stm->msgDateFg); + _subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width()); + tshift += st::msgDateFont->height + st::historyPollAnswersSkip; + + auto heavy = false; + auto created = false; + auto &&tasks = ranges::views::zip( + _tasks, + ranges::views::ints(0, int(_tasks.size()))); + for (const auto &[task, index] : tasks) { + const auto was = !task.userpic.null(); + const auto height = paintTask( + p, + task, + padding.left(), + tshift, + paintw, + width(), + context); + if (was) { + heavy = true; + } else if (!task.userpic.null()) { + created = true; + } + tshift += height; + } + if (!heavy && created) { + history()->owner().registerHeavyViewPart(_parent); + } + paintBottom(p, padding.left(), tshift, paintw, context); +} + +void TodoList::paintBottom( + Painter &p, + int left, + int top, + int paintw, + const PaintContext &context) const { + const auto stringtop = top + + st::msgPadding.bottom() + + st::historyPollBottomButtonTop; + const auto stm = context.messageStyle(); + + p.setPen(stm->msgDateFg); + _completionStatusLabel.draw(p, left, stringtop, paintw, style::al_top); +} + +void TodoList::radialAnimationCallback() const { + if (!anim::Disabled()) { + repaint(); + } +} + +int TodoList::paintTask( + Painter &p, + const Task &task, + int left, + int top, + int width, + int outerWidth, + const PaintContext &context) const { + const auto height = countTaskHeight(task, width); + const auto stm = context.messageStyle(); + const auto aleft = left + st::historyChecklistTaskPadding.left(); + const auto awidth = width + - st::historyChecklistTaskPadding.left() + - st::historyChecklistTaskPadding.right(); + + if (task.ripple) { + p.setOpacity(st::historyPollRippleOpacity); + task.ripple->paint( + p, + left - st::msgPadding.left(), + top, + outerWidth, + &stm->msgWaveformInactive->c); + if (task.ripple->empty()) { + task.ripple.reset(); + } + p.setOpacity(1.); + } + + if (canComplete()) { + paintRadio(p, task, left, top, context); + } else { + paintStatus(p, task, left, top, context); + } + + top += task.completionDate + ? st::historyChecklistCheckedTop + : st::historyChecklistTaskPadding.top(); + p.setPen(stm->historyTextFg); + task.text.draw(p, { + .position = { aleft, top }, + .availableWidth = awidth, + .palette = &stm->textPalette, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), + }); + if (task.completionDate) { + const auto nameTop = top + + height + - st::historyChecklistTaskPadding.bottom() + + st::historyChecklistCheckedTop + - st::normalFont->height; + p.setPen(stm->msgDateFg); + task.name.drawLeft(p, aleft, nameTop, awidth, outerWidth); + } + return height; +} + +void TodoList::paintRadio( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const { + top += st::historyChecklistTaskPadding.top(); + + const auto stm = context.messageStyle(); + + PainterHighQualityEnabler hq(p); + const auto &radio = st::historyPollRadio; + const auto over = ClickHandler::showAsActive(task.handler); + const auto ®ular = stm->msgDateFg; + + const auto checkmark = task.selectedAnimation.value( + task.completionDate ? 1. : 0.); + + const auto o = p.opacity(); + if (checkmark < 1.) { + p.setBrush(Qt::NoBrush); + p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity)); + } + + const auto rect = QRectF(left, top, radio.diameter, radio.diameter).marginsRemoved(QMarginsF(radio.thickness / 2., radio.thickness / 2., radio.thickness / 2., radio.thickness / 2.)); + if (checkmark > 0. && task.completedBy) { + const auto skip = st::lineWidth; + const auto userpic = QRect( + left + (radio.diameter / 2) + skip, + top + skip, + radio.diameter - 2 * skip, + radio.diameter - 2 * skip); + if (checkmark < 1.) { + p.save(); + p.setOpacity(checkmark); + p.translate(QRectF(userpic).center()); + const auto ratio = 0.4 + 0.6 * checkmark; + p.scale(ratio, ratio); + p.translate(-QRectF(userpic).center()); + } + task.completedBy->paintUserpic( + p, + task.userpic, + userpic.left(), + userpic.top(), + userpic.width()); + if (checkmark < 1.) { + p.restore(); + } + } + if (checkmark < 1.) { + auto pen = regular->p; + pen.setWidth(radio.thickness); + p.setPen(pen); + p.drawEllipse(rect); + } + + if (checkmark > 0.) { + const auto removeFull = (radio.diameter / 2 - radio.thickness); + const auto removeNow = removeFull * (1. - checkmark); + const auto color = stm->msgFileThumbLinkFg; + auto pen = color->p; + pen.setWidth(radio.thickness); + p.setPen(pen); + p.setBrush(color); + p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow })); + const auto &icon = stm->historyPollChosen; + icon.paint(p, left + (radio.diameter - icon.width()) / 2, top + (radio.diameter - icon.height()) / 2, width()); + + const auto stm = context.messageStyle(); + auto bgpen = stm->msgBg->p; + bgpen.setWidth(st::lineWidth); + const auto outline = QRect(left, top, radio.diameter, radio.diameter); + const auto paintContent = [&](QPainter &p) { + p.setPen(bgpen); + p.setBrush(Qt::NoBrush); + PainterHighQualityEnabler hq(p); + p.drawEllipse(outline); + }; + if (usesBubblePattern(context)) { + const auto add = st::lineWidth * 3; + const auto target = outline.marginsAdded( + { add, add, add, add }); + Ui::PaintPatternBubblePart( + p, + context.viewport, + context.bubblesPattern->pixmap, + target, + paintContent, + _userpicCircleCache); + } else { + paintContent(p); + } + } + + p.setOpacity(o); +} + +void TodoList::paintStatus( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const { + top += st::historyChecklistTaskPadding.top(); + + const auto stm = context.messageStyle(); + + const auto &radio = st::historyPollRadio; + const auto completed = (task.completionDate != 0); + + const auto rect = QRect(left, top, radio.diameter, radio.diameter); + if (completed) { + const auto &icon = stm->historyPollChosen; + icon.paint( + p, + left + (radio.diameter - icon.width()) / 2, + top + (radio.diameter - icon.height()) / 2, + width(), + stm->msgFileBg->c); + } else { + p.setPen(Qt::NoPen); + p.setBrush(stm->msgFileBg); + + PainterHighQualityEnabler hq(p); + p.drawEllipse(style::centerrect( + rect, + QRect(0, 0, st::mediaUnreadSize, st::mediaUnreadSize))); + } +} + +TextSelection TodoList::adjustSelection( + TextSelection selection, + TextSelectType type) const { + return _title.adjustSelection(selection, type); +} + +uint16 TodoList::fullSelectionLength() const { + return _title.length(); +} + +TextForMimeData TodoList::selectedText(TextSelection selection) const { + return _title.toTextForMimeData(selection); +} + +TextState TodoList::textState(QPoint point, StateRequest request) const { + auto result = TextState(_parent); + const auto padding = st::msgPadding; + auto paintw = width(); + auto tshift = st::historyPollQuestionTop; + if (!isBubbleTop()) { + tshift -= st::msgFileTopMinus; + } + paintw -= padding.left() + padding.right(); + + const auto questionH = _title.countHeight(paintw); + if (QRect(padding.left(), tshift, paintw, questionH).contains(point)) { + result = TextState(_parent, _title.getState( + point - QPoint(padding.left(), tshift), + paintw, + request.forText())); + return result; + } + const auto aleft = padding.left() + + st::historyChecklistTaskPadding.left(); + const auto awidth = paintw + - st::historyChecklistTaskPadding.left() + - st::historyChecklistTaskPadding.right(); + tshift += questionH + st::historyPollSubtitleSkip; + tshift += st::msgDateFont->height + st::historyPollAnswersSkip; + for (const auto &task : _tasks) { + const auto height = countTaskHeight(task, paintw); + if (point.y() >= tshift && point.y() < tshift + height) { + const auto atop = tshift + + (task.completionDate + ? st::historyChecklistCheckedTop + : st::historyChecklistTaskPadding.top()); + auto taskTextResult = task.text.getState( + point - QPoint(aleft, atop), + awidth, + request.forText()); + if (taskTextResult.link) { + result.link = taskTextResult.link; + } else { + _lastLinkPoint = point; + result.link = task.handler; + } + if (task.completionDate) { + result.customTooltip = true; + using Flag = Ui::Text::StateRequest::Flag; + if (request.flags & Flag::LookupCustomTooltip) { + result.customTooltipText = langDateTimeFull( + base::unixtime::parse(task.completionDate)); + } + } + return result; + } + tshift += height; + } + return result; +} + +void TodoList::paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const { + if (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) { + return; + } + _fireworksAnimation = nullptr; +} + +void TodoList::clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) { + if (!handler) return; + + const auto i = ranges::find( + _tasks, + handler, + &Task::handler); + if (i != end(_tasks)) { + toggleRipple(*i, pressed); + } +} + +void TodoList::unloadHeavyPart() { + for (auto &task : _tasks) { + task.userpic = {}; + } +} + +bool TodoList::hasHeavyPart() const { + for (auto &task : _tasks) { + if (!task.userpic.null()) { + return true; + } + } + return false; +} + +void TodoList::hideSpoilers() { + if (_title.hasSpoilers()) { + _title.setSpoilerRevealed(false, anim::type::instant); + } + for (auto &task : _tasks) { + if (task.text.hasSpoilers()) { + task.text.setSpoilerRevealed(false, anim::type::instant); + } + } +} + +std::vector<Media::TodoTaskInfo> TodoList::takeTasksInfo() { + if (_tasks.empty()) { + return {}; + } + return _tasks | ranges::views::transform([](const Task &task) { + return TodoTaskInfo{ + .id = task.id, + .completedBy = task.completedBy, + .completionDate = task.completionDate, + }; + }) | ranges::to_vector; +} + +void TodoList::toggleRipple(Task &task, bool pressed) { + if (pressed) { + const auto outerWidth = width(); + const auto innerWidth = outerWidth + - st::msgPadding.left() + - st::msgPadding.right(); + if (!task.ripple) { + auto mask = Ui::RippleAnimation::RectMask(QSize( + outerWidth, + countTaskHeight(task, innerWidth))); + task.ripple = std::make_unique<Ui::RippleAnimation>( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + } + const auto top = countTaskTop(task, innerWidth); + task.ripple->add(_lastLinkPoint - QPoint(0, top)); + } else if (task.ripple) { + task.ripple->lastStop(); + } +} + +int TodoList::bottomButtonHeight() const { + const auto skip = st::historyPollChoiceRight.height() + - st::historyPollFillingBottom + - st::historyPollFillingHeight + - (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2; + return st::historyPollTotalVotesSkip + - skip + + st::historyPollBottomButtonSkip + + st::msgDateFont->height + + st::msgPadding.bottom(); +} + +TodoList::~TodoList() { + history()->owner().unregisterTodoListView(_todolist, _parent); + if (hasHeavyPart()) { + unloadHeavyPart(); + _parent->checkHeavyPart(); + } +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h new file mode 100644 index 0000000000..733c5c2ab0 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h @@ -0,0 +1,147 @@ +/* +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 "history/view/media/history_view_media.h" +#include "ui/effects/animations.h" +#include "data/data_todo_list.h" +#include "base/weak_ptr.h" + +namespace Ui { +class RippleAnimation; +class FireworksAnimation; +} // namespace Ui + +namespace HistoryView { + +class Message; + +class TodoList final : public Media { +public: + TodoList( + not_null<Element*> parent, + not_null<TodoListData*> todolist, + Element *replacing); + ~TodoList(); + + void draw(Painter &p, const PaintContext &context) const override; + TextState textState(QPoint point, StateRequest request) const override; + + bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { + return true; + } + bool dragItemByHandler(const ClickHandlerPtr &p) const override { + return true; + } + + bool needsBubble() const override { + return true; + } + bool customInfoLayout() const override { + return false; + } + + [[nodiscard]] TextSelection adjustSelection( + TextSelection selection, + TextSelectType type) const override; + uint16 fullSelectionLength() const override; + TextForMimeData selectedText(TextSelection selection) const override; + + void paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const override; + + void clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) override; + + void unloadHeavyPart() override; + bool hasHeavyPart() const override; + + void hideSpoilers() override; + + std::vector<TodoTaskInfo> takeTasksInfo() override; + +private: + struct Task; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + + [[nodiscard]] bool canComplete() const; + + [[nodiscard]] int countTaskTop( + const Task &task, + int innerWidth) const; + [[nodiscard]] int countTaskHeight( + const Task &task, + int innerWidth) const; + [[nodiscard]] ClickHandlerPtr createTaskClickHandler( + const Task &task); + void updateTexts(); + void updateTasks(bool skipAnimations); + void startToggleAnimation(Task &task); + void updateCompletionStatus(); + void maybeStartFireworks(); + void setupPreviousState(const std::vector<TodoTaskInfo> &info); + + int paintTask( + Painter &p, + const Task &task, + int left, + int top, + int width, + int outerWidth, + const PaintContext &context) const; + void paintRadio( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const; + void paintStatus( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const; + void paintBottom( + Painter &p, + int left, + int top, + int paintw, + const PaintContext &context) const; + + void radialAnimationCallback() const; + + void toggleRipple(Task &task, bool pressed); + void toggleCompletion(int id); + + [[nodiscard]] int bottomButtonHeight() const; + + const not_null<TodoListData*> _todolist; + int _todoListVersion = 0; + int _total = 0; + int _incompleted = 0; + TodoListData::Flags _flags = TodoListData::Flags(); + + Ui::Text::String _title; + Ui::Text::String _subtitle; + + std::vector<Task> _tasks; + Ui::Text::String _completionStatusLabel; + + mutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation; + mutable QPoint _lastLinkPoint; + mutable QImage _userpicCircleCache; + mutable QImage _fillingIconCache; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp index abda42b785..b44d10f000 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -99,7 +99,7 @@ ButtonPart::ButtonPart( , _link(std::move(link)) , _stars([=](const QRect &) { repaint(); -}, Ui::Premium::MiniStars::Type::SlowStars) +}, Ui::Premium::MiniStarsType::SlowStars) , _repaint(std::move(repaint)) { } diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index ca88904a62..c20ebcf45b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -289,9 +289,9 @@ WebPage::WebPage( : st::historyPagePreview) , _data(data) , _flags(flags) -, _siteName(st::msgMinWidth - _st.padding.left() - _st.padding.right()) -, _title(st::msgMinWidth - _st.padding.left() - _st.padding.right()) -, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) { +, _siteName(st::minPhotoSize - rect::m::sum::h(_st.padding)) +, _title(st::minPhotoSize - rect::m::sum::h(_st.padding)) +, _description(st::minPhotoSize - rect::m::sum::h(_st.padding)) { history()->owner().registerWebPageView(_data, _parent); } @@ -409,7 +409,7 @@ QSize WebPage::countOptimalSize() { _attach = nullptr; const auto item = _parent->data(); _collage = PrepareCollageMedia(item, _data->collage); - const auto min = st::msgMinWidth - rect::m::sum::h(_st.padding); + const auto min = st::minPhotoSize - rect::m::sum::h(_st.padding); _siteName = Ui::Text::String(min); _title = Ui::Text::String(min); _description = Ui::Text::String(min); @@ -538,13 +538,6 @@ QSize WebPage::countOptimalSize() { && !_data->description.text.isEmpty() && !_data->uniqueGift) { const auto &text = _data->description; - - if (isLogEntryOriginal()) { - // Fix layout for small bubbles - // (narrow media caption edit log entries). - _description = Ui::Text::String(st::minPhotoSize - - rect::m::sum::h(padding)); - } using Type = Core::TextContextDetails::HashtagMentionType; auto context = Core::TextContext({ .session = &history()->session(), diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp index c8674009f0..620ff8ada9 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -118,7 +118,7 @@ void InnerWidget::fill() { using namespace Info::ChannelEarn; const auto container = this; const auto &data = _state; - const auto multiplier = data.usdRate * Data::kEarnMultiplier; + const auto multiplier = data.usdRate; constexpr auto kMinorLength = 3; auto availableBalanceValue = rpl::single( @@ -128,8 +128,15 @@ void InnerWidget::fill() { return _state.availableBalance; }) ); - auto valueToString = [](StarsAmount v) { - return Lang::FormatStarsAmountDecimal(v); + auto overallBalanceValue = rpl::single( + data.overallRevenue + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.overallRevenue; + }) + ); + auto valueToString = [](CreditsAmount v) { + return Lang::FormatCreditsAmountDecimal(v); }; if (data.revenueGraph.chart) { @@ -154,7 +161,7 @@ void InnerWidget::fill() { Ui::AddSkip(container, st::channelEarnOverviewTitleSkip); const auto addOverview = [&]( - rpl::producer<StarsAmount> value, + rpl::producer<CreditsAmount> value, const tr::phrase<> &text) { const auto line = container->add( Ui::CreateSkipWidget(container, 0), @@ -170,8 +177,10 @@ void InnerWidget::fill() { line, std::move( value - ) | rpl::map([=](StarsAmount v) { - return v ? ToUsd(v, multiplier, kMinorLength) : QString(); + ) | rpl::map([=](CreditsAmount v) { + return v + ? ToUsd(v, multiplier, kMinorLength) + : QString(); }), st::channelEarnOverviewSubMinorLabel); rpl::combine( @@ -211,13 +220,7 @@ void InnerWidget::fill() { Ui::AddSkip(container); Ui::AddSkip(container); addOverview( - rpl::single( - data.overallRevenue - ) | rpl::then( - _stateUpdated.events() | rpl::map([=] { - return _state.overallRevenue; - }) - ), + rpl::duplicate(overallBalanceValue), tr::lng_bot_earn_total); Ui::AddSkip(container); Ui::AddSkip(container); @@ -245,17 +248,20 @@ void InnerWidget::fill() { return _state.buyAdsUrl; }) ), - rpl::duplicate(availableBalanceValue), + peer()->isSelf() + ? rpl::duplicate(overallBalanceValue) | rpl::type_erased() + : rpl::duplicate(availableBalanceValue), rpl::duplicate(dateValue), _state.isWithdrawalEnabled, - rpl::duplicate( - availableBalanceValue - ) | rpl::map([=](StarsAmount v) { + (peer()->isSelf() + ? rpl::duplicate(overallBalanceValue) | rpl::type_erased() + : rpl::duplicate(availableBalanceValue) + ) | rpl::map([=](CreditsAmount v) { return v ? ToUsd(v, multiplier, kMinorLength) : QString(); })); container->resizeToWidth(container->width()); } - if (BotStarRef::Join::Allowed(peer())) { + if (BotStarRef::Join::Allowed(peer()) && !peer()->isSelf()) { const auto button = BotStarRef::AddViewListButton( container, tr::lng_credits_summary_earn_title(), @@ -267,7 +273,9 @@ void InnerWidget::fill() { Ui::AddSkip(container); Ui::AddDivider(container); } - fillHistory(); + if (!peer()->isSelf()) { + fillHistory(); + } } void InnerWidget::fillHistory() { diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp index 26130b96f0..89dcda70a3 100644 --- a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp @@ -603,7 +603,7 @@ object_ptr<Ui::BoxContent> JoinStarRefBox( const auto layout = box->verticalLayout(); const auto session = &initialRecipient->session(); auto text = Ui::Text::Colorized(Ui::CreditsEmoji(session)); - text.append(Lang::FormatStarsAmountRounded(average)); + text.append(Lang::FormatCreditsAmountRounded(average)); layout->add( object_ptr<Ui::FlatLabel>( box, diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp index a5a73614e6..09f5cdeb60 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp @@ -1250,7 +1250,7 @@ void CreateGiveawayBox( rpl::duplicate(creditsValueType), tr::lng_giveaway_additional_credits_about(), tr::lng_giveaway_additional_about() - ) | rpl::map(Ui::Text::WithEntities))); + ) | Ui::Text::ToWithEntities())); Ui::AddSkip(additionalWrap); } diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp index 70e54d9bb7..9e1ba75d52 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp @@ -22,6 +22,10 @@ QString MajorPart(EarnInt value) { return (diff <= 0) ? QString(kZero) : string.mid(0, diff); } +QString MajorPart(CreditsAmount value) { + return QString::number(int64(value.value())); +} + QString MinorPart(EarnInt value) { if (!value) { return QString(kDot) + kZero + kZero; @@ -46,26 +50,29 @@ QString MinorPart(EarnInt value) { return result.chopped(zeroCount); } +QString MinorPart(CreditsAmount value) { + return QString::number(value.value(), 'f', 2).right(3); +} + QString ToUsd( Data::EarnInt value, float64 rate, int afterFloat) { - return ToUsd(StarsAmount(value), rate, afterFloat); + return ToUsd(CreditsAmount(value), rate, afterFloat); } QString ToUsd( - StarsAmount value, + CreditsAmount value, float64 rate, int afterFloat) { constexpr auto kApproximately = QChar(0x2248); - const auto result = int64(base::SafeRound(value.value() * rate)); return QString(kApproximately) + QChar('$') - + MajorPart(result) - + ((afterFloat > 0) - ? MinorPart(result).left(afterFloat) - : MinorPart(result)); + + QString::number( + value.value() * rate, + 'f', + afterFloat ? afterFloat : 2); } } // namespace Info::ChannelEarn diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h index 2f0b14848f..02ac4cee32 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h @@ -12,13 +12,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::ChannelEarn { [[nodiscard]] QString MajorPart(Data::EarnInt value); +[[nodiscard]] QString MajorPart(CreditsAmount value); [[nodiscard]] QString MinorPart(Data::EarnInt value); +[[nodiscard]] QString MinorPart(CreditsAmount value); [[nodiscard]] QString ToUsd( Data::EarnInt value, float64 rate, int afterFloat); [[nodiscard]] QString ToUsd( - StarsAmount value, + CreditsAmount value, float64 rate, int afterFloat); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp index f6ca314bcc..fa5b2444da 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp @@ -47,10 +47,8 @@ namespace { } // namespace -QImage IconCurrencyColored( - const style::font &font, - const QColor &c) { - const auto s = Size(font->ascent); +QImage IconCurrencyColored(int size, const QColor &c) { + const auto s = Size(size); auto svg = QSvgRenderer(CurrencySvg(c)); auto image = QImage( s * style::DevicePixelRatio(), @@ -64,6 +62,12 @@ QImage IconCurrencyColored( return image; } +QImage IconCurrencyColored( + const style::font &font, + const QColor &c) { + return IconCurrencyColored(font->ascent, c); +} + QByteArray CurrencySvgColored(const QColor &c) { return CurrencySvg(c); } diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h index eb82e11fd5..0e5a419ec7 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h @@ -13,6 +13,7 @@ class CustomEmoji; namespace Ui::Earn { +[[nodiscard]] QImage IconCurrencyColored(int size, const QColor &c); [[nodiscard]] QImage IconCurrencyColored( const style::font &font, const QColor &c); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp index 3d28ab3025..1563bef9dd 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "api/api_updates.h" #include "base/unixtime.h" +#include "boxes/gift_premium_box.h" #include "boxes/peers/edit_peer_color_box.h" // AddLevelBadge. #include "chat_helpers/stickers_emoji_pack.h" #include "core/application.h" @@ -249,7 +250,6 @@ void InnerWidget::load() { ) | rpl::start_with_next([=, peerId = _peer->id]( const MTPUpdates &updates) { using TLCreditsUpdate = MTPDupdateStarsRevenueStatus; - using TLCurrencyUpdate = MTPDupdateBroadcastRevenueTransactions; using TLNotificationUpdate = MTPDupdateServiceNotification; Api::PerformForUpdate<TLCreditsUpdate>(updates, [&]( const TLCreditsUpdate &d) { @@ -257,29 +257,39 @@ void InnerWidget::load() { return; } const auto &data = d.vstatus().data(); - auto &e = _state.creditsEarn; - e.currentBalance = Data::FromTL(data.vcurrent_balance()); - e.availableBalance = Data::FromTL(data.vavailable_balance()); - e.overallRevenue = Data::FromTL(data.voverall_revenue()); - e.isWithdrawalEnabled = data.is_withdrawal_enabled(); - e.nextWithdrawalAt = data.vnext_withdrawal_at() - ? base::unixtime::parse( - data.vnext_withdrawal_at()->v) - : QDateTime(); - state->apiCreditsHistory.request({}, [=]( - const Data::CreditsStatusSlice &data) { - _state.creditsStatusSlice = data; - _stateUpdated.fire({}); + const auto isCredits = data.vcurrent_balance().match([]( + const MTPDstarsAmount &) { + return true; + }, [](const MTPDstarsTonAmount &) { + return false; }); - }); - Api::PerformForUpdate<TLCurrencyUpdate>(updates, [&]( - const TLCurrencyUpdate &d) { - if (peerId == peerFromMTP(d.vpeer())) { - const auto &data = d.vbalances().data(); - auto &e = _state.currencyEarn; - e.currentBalance = data.vcurrent_balance().v; - e.availableBalance = data.vavailable_balance().v; - e.overallRevenue = data.voverall_revenue().v; + if (isCredits) { + auto &credits = _state.creditsEarn; + credits.currentBalance = CreditsAmountFromTL( + data.vcurrent_balance()); + credits.availableBalance = CreditsAmountFromTL( + data.vavailable_balance()); + credits.overallRevenue = CreditsAmountFromTL( + data.voverall_revenue()); + credits.isWithdrawalEnabled + = data.is_withdrawal_enabled(); + credits.nextWithdrawalAt = data.vnext_withdrawal_at() + ? base::unixtime::parse( + data.vnext_withdrawal_at()->v) + : QDateTime(); + state->apiCreditsHistory.request({}, [=]( + const Data::CreditsStatusSlice &data) { + _state.creditsStatusSlice = data; + _stateUpdated.fire({}); + }); + } else { + auto ¤cy = _state.currencyEarn; + currency.currentBalance = CreditsAmountFromTL( + data.vcurrent_balance()); + currency.availableBalance = CreditsAmountFromTL( + data.vavailable_balance()); + currency.overallRevenue = CreditsAmountFromTL( + data.voverall_revenue()); _stateUpdated.fire({}); } }); @@ -395,9 +405,8 @@ void InnerWidget::fill() { //constexpr auto kApproximately = QChar(0x2248); const auto multiplier = data.usdRate; - const auto creditsToUsdMap = [=](StarsAmount c) { - const auto creditsMultiplier = _state.creditsEarn.usdRate - * Data::kEarnMultiplier; + const auto creditsToUsdMap = [=](CreditsAmount c) { + const auto creditsMultiplier = _state.creditsEarn.usdRate; return c ? ToUsd(c, creditsMultiplier, 0) : QString(); }; @@ -405,7 +414,7 @@ void InnerWidget::fill() { const auto withdrawalEnabled = WithdrawalEnabled(session); const auto addEmojiToMajor = [=]( not_null<Ui::FlatLabel*> label, - rpl::producer<EarnInt> value, + rpl::producer<CreditsAmount> value, std::optional<bool> isIn, std::optional<QMargins> margins) { const auto &st = label->st(); @@ -414,7 +423,7 @@ void InnerWidget::fill() { Ui::Earn::IconCurrencyColored( st.style.font, !isIn - ? st::activeButtonBg->c + ? st::currencyFg->c : (*isIn) ? st::boxTextFgGood->c : st::menuIconAttentionColor->c), @@ -425,7 +434,7 @@ void InnerWidget::fill() { : TextWithEntities::Simple((*isIn) ? QChar('+') : kMinus); std::move( value - ) | rpl::start_with_next([=](EarnInt v) { + ) | rpl::start_with_next([=](CreditsAmount v) { label->setMarkedText( base::duplicate(prepended).append(icon).append(MajorPart(v)), Core::TextContext({ .session = session })); @@ -436,7 +445,7 @@ void InnerWidget::fill() { session->data().customEmojiManager().registerInternalEmoji( Ui::Earn::IconCurrencyColored( st::boxTitle.style.font, - st::activeButtonBg->c), + st::currencyFg->c), st::channelEarnCurrencyLearnMargins, false)); @@ -706,8 +715,8 @@ void InnerWidget::fill() { Ui::AddSkip(container, st::channelEarnOverviewTitleSkip); const auto addOverview = [&]( - rpl::producer<EarnInt> currencyValue, - rpl::producer<StarsAmount> creditsValue, + rpl::producer<CreditsAmount> currencyValue, + rpl::producer<CreditsAmount> creditsValue, const tr::phrase<> &text, bool showCurrency, bool showCredits) { @@ -724,15 +733,17 @@ void InnerWidget::fill() { {}); const auto minorLabel = Ui::CreateChild<Ui::FlatLabel>( line, - rpl::duplicate(currencyValue) | rpl::map([=](EarnInt v) { - return MinorPart(v).left(kMinorLength); + rpl::duplicate( + currencyValue + ) | rpl::map([](CreditsAmount v) { + return MinorPart(v); }), st::channelEarnOverviewMinorLabel); const auto secondMinorLabel = Ui::CreateChild<Ui::FlatLabel>( line, std::move( currencyValue - ) | rpl::map([=](EarnInt value) { + ) | rpl::map([=](CreditsAmount value) { return value ? ToUsd(value, multiplier, kMinorLength) : QString(); @@ -741,8 +752,10 @@ void InnerWidget::fill() { const auto creditsLabel = Ui::CreateChild<Ui::FlatLabel>( line, - rpl::duplicate(creditsValue) | rpl::map([](StarsAmount value) { - return Lang::FormatStarsAmountDecimal(value); + rpl::duplicate( + creditsValue + ) | rpl::map([](CreditsAmount value) { + return Lang::FormatCreditsAmountDecimal(value); }), st::channelEarnOverviewMajorLabel); const auto icon = Ui::CreateSingleStarWidget( @@ -761,7 +774,7 @@ void InnerWidget::fill() { int available, const QSize &size, const QSize &creditsSize, - StarsAmount credits) { + CreditsAmount credits) { const auto skip = st::channelEarnOverviewSubMinorLabelPos.x(); line->resize(line->width(), size.height()); minorLabel->moveToLeft( @@ -1088,7 +1101,7 @@ void InnerWidget::fill() { const auto historyList = tabCurrencyList->entity(); const auto addHistoryEntry = [=]( - const Data::EarnHistoryEntry &entry, + const Data::CreditsHistoryEntry &entry, const tr::phrase<> &text) { const auto wrap = historyList->add( object_ptr<Ui::PaddingWrap<Ui::VerticalLayout>>( @@ -1102,8 +1115,7 @@ void InnerWidget::fill() { text(), st::channelEarnSemiboldLabel)); - const auto isIn - = (entry.type == Data::EarnHistoryEntry::Type::In); + const auto isIn = entry.in; const auto recipient = Ui::Text::Wrapped( { entry.provider }, EntityType::Code); @@ -1120,18 +1132,17 @@ void InnerWidget::fill() { Ui::AddSkip(inner, st::channelEarnHistoryTwoSkip); } - const auto isFailed = entry.status - == Data::EarnHistoryEntry::Status::Failed; - const auto isPending = entry.status - == Data::EarnHistoryEntry::Status::Pending; - const auto dateText = (!entry.dateTo.isNull() || isFailed) + const auto isFailed = entry.failed; + const auto isPending = entry.pending; + const auto dateText = (!entry.adsProceedsToDate.isNull() + || isFailed) ? (FormatDate(entry.date) + ' ' + QChar(8212) + ' ' + (isFailed ? tr::lng_channel_earn_history_out_failed(tr::now) - : FormatDate(entry.dateTo))) + : FormatDate(entry.adsProceedsToDate))) : isPending ? tr::lng_channel_earn_history_pending(tr::now) : FormatDate(entry.date); @@ -1152,12 +1163,12 @@ void InnerWidget::fill() { st::channelEarnHistoryMajorLabel); addEmojiToMajor( majorLabel, - rpl::single(entry.amount), + rpl::single(entry.credits), isIn, {}); majorLabel->setAttribute(Qt::WA_TransparentForMouseEvents); majorLabel->setTextColorOverride(color); - const auto minorText = MinorPart(entry.amount); + const auto minorText = MinorPart(entry.credits); const auto minorLabel = Ui::CreateChild<Ui::FlatLabel>( wrap, rpl::single(minorText), @@ -1186,7 +1197,7 @@ void InnerWidget::fill() { st::channelEarnOverviewMajorLabel); addEmojiToMajor( majorLabel, - rpl::single(entry.amount), + rpl::single(entry.credits), isIn, {}); majorLabel->setAttribute( @@ -1223,6 +1234,11 @@ void InnerWidget::fill() { st::channelEarnHistorySubLabel))); Ui::AddSkip(box->verticalLayout()); Ui::AddSkip(box->verticalLayout()); + AddChannelEarnTable( + box->uiShow(), + box->verticalLayout(), + entry); + Ui::AddSkip(box->verticalLayout()); Ui::AddSkip(box->verticalLayout()); box->addRow(object_ptr<Ui::CenterWrap<>>( box, @@ -1303,15 +1319,14 @@ void InnerWidget::fill() { }, wrap->lifetime()); }; const auto handleSlice = [=](const Data::EarnHistorySlice &s) { - using Type = Data::EarnHistoryEntry::Type; for (const auto &entry : s.list) { addHistoryEntry( entry, - (entry.type == Type::In) - ? tr::lng_channel_earn_history_in - : (entry.type == Type::Return) + (entry.refunded ? tr::lng_channel_earn_history_return - : tr::lng_channel_earn_history_out); + : entry.in + ? tr::lng_channel_earn_history_in + : tr::lng_channel_earn_history_out)); } historyList->resizeToWidth(listsContainer->width()); }; diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp b/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp index f81019bb77..46ad5a394a 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp @@ -21,7 +21,7 @@ namespace Info { namespace CommonGroups { Memento::Memento(not_null<UserData*> user) -: ContentMemento(user, nullptr, PeerId()) { +: ContentMemento(user, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index a6d8e22e04..e20c3d861e 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1177,3 +1177,13 @@ infoGiftTooltip: ImportantTooltip(defaultImportantTooltip) { padding: margins(8px, 2px, 8px, 3px); } infoGiftTooltipFont: font(11px semibold); + +topicsLayoutButtonLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + margin: margins(10px, 4px, 10px, 4px); + textFg: windowSubTextFg; +} +topicsLayoutButtonIconPadding: margins(4px, 0px, 4px, 0px); +topicsLayoutButtonIconSize: 140px; +topicsLayoutButtonPadding: margins(4px, 0px, 4px, 12px); +topicsLayoutButtonSkip: 0px; diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index f392e132ed..efbfbdc14a 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -307,6 +307,7 @@ QRect ContentWidget::floatPlayerAvailableRect() const { void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { const auto peer = _controller->key().peer(); const auto topic = _controller->key().topic(); + const auto sublist = _controller->key().sublist(); if (!peer && !topic) { return; } @@ -316,6 +317,8 @@ void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { Dialogs::EntryState{ .key = (topic ? Dialogs::Key{ topic } + : sublist + ? Dialogs::Key{ sublist } : Dialogs::Key{ peer->owner().history(peer) }), .section = Dialogs::EntryState::Section::Profile, }, @@ -465,6 +468,8 @@ void ContentWidget::setupSwipeHandler(not_null<Ui::RpWidget*> widget) { Key ContentMemento::key() const { if (const auto topic = this->topic()) { return Key(topic); + } else if (const auto sublist = this->sublist()) { + return Key(sublist); } else if (const auto peer = this->peer()) { return Key(peer); } else if (const auto poll = this->poll()) { @@ -489,12 +494,14 @@ Key ContentMemento::key() const { ContentMemento::ContentMemento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId) : _peer(peer) -, _migratedPeerId((!topic && peer->migrateFrom()) +, _migratedPeerId((!topic && !sublist && peer->migrateFrom()) ? peer->migrateFrom()->id : 0) -, _topic(topic) { +, _topic(topic) +, _sublist(sublist) { if (_topic) { _peer->owner().itemIdChanged( ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 359f46787a..29c376dfa4 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -210,6 +210,7 @@ public: ContentMemento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); @@ -240,6 +241,9 @@ public: Data::ForumTopic *topic() const { return _topic; } + Data::SavedSublist *sublist() const { + return _sublist; + } UserData *settingsSelf() const { return _settingsSelf; } @@ -311,6 +315,7 @@ private: PeerData * const _peer = nullptr; const PeerId _migratedPeerId = 0; Data::ForumTopic *_topic = nullptr; + Data::SavedSublist *_sublist = nullptr; UserData * const _settingsSelf = nullptr; PeerData * const _storiesPeer = nullptr; Stories::Tab _storiesTab = {}; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index b1ec130dc5..49054d63f1 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/search_field_controller.h" #include "data/data_shared_media.h" +#include "history/history.h" #include "info/info_content_widget.h" #include "info/info_memento.h" #include "info/global_media/info_global_media_widget.h" @@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_download_manager.h" @@ -35,6 +37,9 @@ Key::Key(not_null<PeerData*> peer) : _value(peer) { Key::Key(not_null<Data::ForumTopic*> topic) : _value(topic) { } +Key::Key(not_null<Data::SavedSublist*> sublist) : _value(sublist) { +} + Key::Key(Settings::Tag settings) : _value(settings) { } @@ -69,6 +74,8 @@ PeerData *Key::peer() const { return *peer; } else if (const auto topic = this->topic()) { return topic->channel(); + } else if (const auto sublist = this->sublist()) { + return sublist->owningHistory()->peer; } return nullptr; } @@ -81,6 +88,14 @@ Data::ForumTopic *Key::topic() const { return nullptr; } +Data::SavedSublist *Key::sublist() const { + if (const auto sublist = std::get_if<not_null<Data::SavedSublist*>>( + &_value)) { + return *sublist; + } + return nullptr; +} + UserData *Key::settingsSelf() const { if (const auto tag = std::get_if<Settings::Tag>(&_value)) { return tag->self; @@ -195,6 +210,7 @@ rpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource( SparseIdsMergedSlice::Key( peer()->id, topicId, + sublist() ? sublist()->sublistPeer()->id : PeerId(), migratedPeerId(), aroundId), section().mediaType()), @@ -487,6 +503,7 @@ rpl::producer<SparseIdsMergedSlice> Controller::mediaSource( SparseIdsMergedSlice::Key( query.peerId, query.topicRootId, + query.monoforumPeerId, query.migratedPeerId, aroundId), query.type), diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index c82a8b9f5a..5b29ac1ce6 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -18,6 +18,7 @@ struct WhoReadList; namespace Data { class ForumTopic; +class SavedSublist; } // namespace Data namespace Ui { @@ -94,6 +95,7 @@ class Key { public: explicit Key(not_null<PeerData*> peer); explicit Key(not_null<Data::ForumTopic*> topic); + explicit Key(not_null<Data::SavedSublist*> sublist); Key(Settings::Tag settings); Key(Downloads::Tag downloads); Key(Stories::Tag stories); @@ -108,6 +110,7 @@ public: PeerData *peer() const; Data::ForumTopic *topic() const; + Data::SavedSublist *sublist() const; UserData *settingsSelf() const; bool isDownloads() const; bool isGlobalMedia() const; @@ -135,6 +138,7 @@ private: std::variant< not_null<PeerData*>, not_null<Data::ForumTopic*>, + not_null<Data::SavedSublist*>, Settings::Tag, Downloads::Tag, Stories::Tag, @@ -225,6 +229,9 @@ public: [[nodiscard]] Data::ForumTopic *topic() const { return key().topic(); } + [[nodiscard]] Data::SavedSublist *sublist() const { + return key().sublist(); + } [[nodiscard]] UserData *settingsSelf() const { return key().settingsSelf(); } diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index 40f7733dd6..945bda8736 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "main/main_session.h" @@ -48,6 +49,14 @@ Memento::Memento(not_null<Data::ForumTopic*> topic, Section section) : Memento(DefaultStack(topic, section)) { } +Memento::Memento(not_null<Data::SavedSublist*> sublist) +: Memento(sublist, Section::Type::Profile) { +} + +Memento::Memento(not_null<Data::SavedSublist*> sublist, Section section) +: Memento(DefaultStack(sublist, section)) { +} + Memento::Memento(Settings::Tag settings, Section section) : Memento(DefaultStack(settings, section)) { } @@ -66,9 +75,12 @@ Memento::Memento( Memento::Memento(std::vector<std::shared_ptr<ContentMemento>> stack) : _stack(std::move(stack)) { auto topics = base::flat_set<not_null<Data::ForumTopic*>>(); + auto sublists = base::flat_set<not_null<Data::SavedSublist*>>(); for (auto &entry : _stack) { if (const auto topic = entry->topic()) { topics.emplace(topic); + } else if (const auto sublist = entry->sublist()) { + sublists.emplace(sublist); } } for (const auto &topic : topics) { @@ -86,6 +98,21 @@ Memento::Memento(std::vector<std::shared_ptr<ContentMemento>> stack) } }, _lifetime); } + for (const auto &sublist : sublists) { + sublist->destroyed( + ) | rpl::start_with_next([=] { + for (auto i = begin(_stack); i != end(_stack);) { + if (i->get()->sublist() == sublist) { + i = _stack.erase(i); + } else { + ++i; + } + } + if (_stack.empty()) { + _removeRequests.fire({}); + } + }, _lifetime); + } } std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( @@ -104,6 +131,14 @@ std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( return result; } +std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( + not_null<Data::SavedSublist*> sublist, + Section section) { + auto result = std::vector<std::shared_ptr<ContentMemento>>(); + result.push_back(DefaultContent(sublist, section)); + return result; +} + std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( Settings::Tag settings, Section section) { @@ -205,6 +240,20 @@ std::shared_ptr<ContentMemento> Memento::DefaultContent( Unexpected("Wrong section type in Info::Memento::DefaultContent()"); } +std::shared_ptr<ContentMemento> Memento::DefaultContent( + not_null<Data::SavedSublist*> sublist, + Section section) { + switch (section.type()) { + case Section::Type::Profile: + return std::make_shared<Profile::Memento>(sublist); + case Section::Type::Media: + return std::make_shared<Media::Memento>( + sublist, + section.mediaType()); + } + Unexpected("Wrong section type in Info::Memento::DefaultContent()"); +} + object_ptr<Window::SectionWidget> Memento::createWidget( QWidget *parent, not_null<Window::SessionController*> controller, diff --git a/Telegram/SourceFiles/info/info_memento.h b/Telegram/SourceFiles/info/info_memento.h index dc50f2f89c..fd0e5946aa 100644 --- a/Telegram/SourceFiles/info/info_memento.h +++ b/Telegram/SourceFiles/info/info_memento.h @@ -23,6 +23,7 @@ enum class SharedMediaType : signed char; namespace Data { class ForumTopic; +class SavedSublist; struct ReactionId; } // namespace Data @@ -49,6 +50,8 @@ public: Memento(not_null<PeerData*> peer, Section section); explicit Memento(not_null<Data::ForumTopic*> topic); Memento(not_null<Data::ForumTopic*> topic, Section section); + explicit Memento(not_null<Data::SavedSublist*> sublist); + Memento(not_null<Data::SavedSublist*> sublist, Section section); Memento(Settings::Tag settings, Section section); Memento(not_null<PollData*> poll, FullMsgId contextId); Memento( @@ -94,6 +97,9 @@ private: static std::vector<std::shared_ptr<ContentMemento>> DefaultStack( not_null<Data::ForumTopic*> topic, Section section); + static std::vector<std::shared_ptr<ContentMemento>> DefaultStack( + not_null<Data::SavedSublist*> sublist, + Section section); static std::vector<std::shared_ptr<ContentMemento>> DefaultStack( Settings::Tag settings, Section section); @@ -111,6 +117,9 @@ private: static std::shared_ptr<ContentMemento> DefaultContent( not_null<Data::ForumTopic*> topic, Section section); + static std::shared_ptr<ContentMemento> DefaultContent( + not_null<Data::SavedSublist*> sublist, + Section section); std::vector<std::shared_ptr<ContentMemento>> _stack; rpl::event_stream<> _removeRequests; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index ae10a739e6..ad908c7e43 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -10,16 +10,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/call_delayed.h" #include "base/qt/qt_key_modifiers.h" #include "core/application.h" +#include "core/ui_integration.h" +#include "data/components/recent_shared_media_gifts.h" #include "data/data_channel.h" #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories_ids.h" #include "data/data_user.h" -#include "history/view/history_view_sublist_section.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/history.h" +#include "history/view/history_view_chat_section.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "info/profile/info_profile_values.h" #include "info/stories/info_stories_widget.h" +#include "main/main_session.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/wrap/slide_wrap.h" @@ -32,39 +39,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Media { namespace { -[[nodiscard]] Window::SeparateSharedMediaType ToSeparateType( - Storage::SharedMediaType type) { +[[nodiscard]] bool SeparateSupported(Storage::SharedMediaType type) { using Type = Storage::SharedMediaType; - using SeparatedType = Window::SeparateSharedMediaType; return (type == Type::Photo) - ? SeparatedType::Photos - : (type == Type::Video) - ? SeparatedType::Videos - : (type == Type::File) - ? SeparatedType::Files - : (type == Type::MusicFile) - ? SeparatedType::Audio - : (type == Type::Link) - ? SeparatedType::Links - : (type == Type::RoundVoiceFile) - ? SeparatedType::Voices - : (type == Type::GIF) - ? SeparatedType::GIF - : SeparatedType::None; + || (type == Type::Video) + || (type == Type::File) + || (type == Type::MusicFile) + || (type == Type::Link) + || (type == Type::RoundVoiceFile) + || (type == Type::GIF); } [[nodiscard]] Window::SeparateId SeparateId( not_null<PeerData*> peer, MsgId topicRootId, Storage::SharedMediaType type) { - if (peer->isSelf()) { + if (peer->isSelf() || !SeparateSupported(type)) { return { nullptr }; } - const auto separateType = ToSeparateType(type); - if (separateType == Window::SeparateSharedMediaType::None) { + const auto topic = topicRootId + ? peer->forumTopicFor(topicRootId) + : nullptr; + if (topicRootId && !topic) { return { nullptr }; } - return { Window::SeparateSharedMedia{ separateType, peer, topicRootId } }; + const auto thread = topic + ? (Data::Thread*)topic + : peer->owner().history(peer); + return { thread, type }; } void AddContextMenuToButton( @@ -150,12 +152,18 @@ not_null<Ui::SettingsButton*> AddButton( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Type type, Ui::MultiSlideTracker &tracker) { auto result = AddCountedButton( parent, - Profile::SharedMediaCountValue(peer, topicRootId, migrated, type), + Profile::SharedMediaCountValue( + peer, + topicRootId, + monoforumPeerId, + migrated, + type), MediaText(type), tracker)->entity(); const auto separateId = SeparateId(peer, topicRootId, type); @@ -274,9 +282,13 @@ not_null<Ui::SettingsButton*> AddSavedSublistButton( }, tracker)->entity(); result->addClickHandler([=] { + using namespace HistoryView; + const auto sublist = peer->owner().savedMessages().sublist(peer); navigation->showSection( - std::make_shared<HistoryView::SublistMemento>( - peer->owner().savedMessages().sublist(peer))); + std::make_shared<ChatMemento>(ChatViewId{ + .history = sublist->owningHistory(), + .sublist = sublist, + })); }); return result; } @@ -286,14 +298,90 @@ not_null<Ui::SettingsButton*> AddPeerGiftsButton( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, Ui::MultiSlideTracker &tracker) { - auto result = AddCountedButton( - parent, - Profile::PeerGiftsCountValue(peer), - [](int count) { - return tr::lng_profile_peer_gifts(tr::now, lt_count, count); - }, - tracker)->entity(); - result->addClickHandler([=] { + + auto count = Profile::PeerGiftsCountValue(peer); + auto textFromCount = [](int count) { + return tr::lng_profile_peer_gifts(tr::now, lt_count, count); + }; + + using namespace ::Settings; + auto forked = std::move(count) + | start_spawning(parent->lifetime()); + auto text = rpl::duplicate( + forked + ) | rpl::map([textFromCount](int count) { + return (count > 0) + ? textFromCount(count) + : QString(); + }); + + struct State final { + std::vector<std::unique_ptr<Ui::Text::CustomEmoji>> emojiList; + rpl::event_stream<> textRefreshed; + QPointer<Ui::SettingsButton> button; + rpl::lifetime appearedLifetime; + }; + const auto state = parent->lifetime().make_state<State>(); + + const auto refresh = [=] { + if (state->button) { + state->button->update(); + } + }; + + auto customs = state->textRefreshed.events( + ) | rpl::map([=]() -> TextWithEntities { + auto result = TextWithEntities(); + for (const auto &custom : state->emojiList) { + result.append(Ui::Text::SingleCustomEmoji(custom->entityData())); + } + return result; + }); + + const auto wrap = parent->add( + object_ptr<Ui::SlideWrap<Ui::SettingsButton>>( + parent, + object_ptr<Ui::SettingsButton>( + parent, + rpl::combine( + std::move(text), + std::move(customs) + ) | rpl::map([=](QString text, TextWithEntities customs) { + return TextWithEntities() + .append(std::move(text)) + .append(QChar(' ')) + .append(std::move(customs)); + }), + st::infoSharedMediaButton, + Core::TextContext({ + .session = &navigation->session(), + .details = { .session = &navigation->session() }, + .repaint = refresh, + .customEmojiLoopLimit = 1, + })))); + wrap->setDuration(st::infoSlideDuration); + wrap->toggleOn(rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0)); + tracker.track(wrap); + + rpl::duplicate(forked) | rpl::filter( + rpl::mappers::_1 > 0 + ) | rpl::start_with_next([=] { + state->appearedLifetime.destroy(); + const auto requestDone = crl::guard(wrap, [=]( + std::vector<DocumentId> ids) { + state->emojiList.clear(); + for (const auto &id : ids) { + state->emojiList.push_back( + peer->owner().customEmojiManager().create(id, refresh)); + } + state->textRefreshed.fire({}); + }); + navigation->session().recentSharedGifts().request(peer, requestDone); + }, state->appearedLifetime); + + state->button = wrap->entity(); + + wrap->entity()->addClickHandler([=] { if (navigation->showFrozenError()) { return; } @@ -302,7 +390,7 @@ not_null<Ui::SettingsButton*> AddPeerGiftsButton( peer, Section::Type::PeerGifts)); }); - return result; + return wrap->entity(); } } // namespace Info::Media diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index e8927c0970..a8a24feb47 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -42,6 +42,7 @@ using Type = Storage::SharedMediaType; not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Type type, Ui::MultiSlideTracker &tracker); diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index 6f2bedac48..faccefcd74 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "data/data_forum_topic.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" @@ -79,7 +80,11 @@ void InnerWidget::createTypeButtons() { auto tracker = Ui::MultiSlideTracker(); const auto peer = _controller->key().peer(); const auto topic = _controller->key().topic(); + const auto sublist = _controller->key().sublist(); const auto topicRootId = topic ? topic->rootId() : MsgId(); + const auto monoforumPeerId = sublist + ? sublist->sublistPeer()->id + : PeerId(); const auto migrated = _controller->migrated(); const auto addMediaButton = [&]( Type buttonType, @@ -92,6 +97,7 @@ void InnerWidget::createTypeButtons() { _controller, peer, topicRootId, + monoforumPeerId, migrated, buttonType, tracker); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 355fa13181..dbf7ef776c 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_download_manager.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/history.h" @@ -512,7 +513,7 @@ void ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) { : Data::StoriesContext{ Data::StoriesContextSaved() }; _controller->parentController()->openPhoto( photo, - { id, topicRootId() }, + { id, topicRootId(), monoforumPeerId() }, _controller->storiesPeer() ? &context : nullptr); } @@ -527,7 +528,7 @@ void ListWidget::openDocument( _controller->parentController()->openDocument( document, showInMediaView, - { id, topicRootId() }, + { id, topicRootId(), monoforumPeerId() }, _controller->storiesPeer() ? &context : nullptr); } @@ -796,6 +797,11 @@ MsgId ListWidget::topicRootId() const { return topic ? topic->rootId() : MsgId(0); } +PeerId ListWidget::monoforumPeerId() const { + const auto sublist = _controller->key().sublist(); + return sublist ? sublist->sublistPeer()->id : PeerId(0); +} + QMargins ListWidget::padding() const { return st::infoMediaMargin; } diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index 5b486c1de7..4f4a78e966 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -158,6 +158,7 @@ private: void setupSelectRestriction(); [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] PeerId monoforumPeerId() const; QMargins padding() const; bool isItemLayout( diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index ec5dc252c9..ef815a3c05 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_peer_values.h" #include "data/data_document.h" +#include "data/data_saved_sublist.h" #include "styles/style_info.h" #include "styles/style_overview.h" @@ -45,7 +46,10 @@ Provider::Provider(not_null<AbstractController*> controller) , _peer(_controller->key().peer()) , _topicRootId(_controller->key().topic() ? _controller->key().topic()->rootId() - : 0) + : MsgId()) +, _monoforumPeerId(_controller->key().sublist() + ? _controller->key().sublist()->sublistPeer()->id + : PeerId()) , _migrated(_controller->migrated()) , _type(_controller->section().mediaType()) , _slice(sliceKey(_universalAroundId)) { @@ -336,13 +340,23 @@ SparseIdsMergedSlice::Key Provider::sliceKey( UniversalMsgId universalId) const { using Key = SparseIdsMergedSlice::Key; if (!_topicRootId && _migrated) { - return Key(_peer->id, _topicRootId, _migrated->id, universalId); + return Key( + _peer->id, + _topicRootId, + _monoforumPeerId, + _migrated->id, + universalId); } if (universalId < 0) { // Convert back to plain id for non-migrated histories. universalId = universalId + ServerMaxMsgId; } - return Key(_peer->id, _topicRootId, 0, universalId); + return Key( + _peer->id, + _topicRootId, + _monoforumPeerId, + PeerId(), + universalId); } void Provider::itemRemoved(not_null<const HistoryItem*> item) { diff --git a/Telegram/SourceFiles/info/media/info_media_provider.h b/Telegram/SourceFiles/info/media/info_media_provider.h index b544e2b8d7..ed09954e83 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.h +++ b/Telegram/SourceFiles/info/media/info_media_provider.h @@ -105,6 +105,7 @@ private: const not_null<PeerData*> _peer; const MsgId _topicRootId = 0; + const PeerId _monoforumPeerId = 0; PeerData * const _migrated = nullptr; const Type _type = Type::Photo; diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index 5d2af53daa..58d9e8e95e 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/media/info_media_widget.h" +#include "history/history.h" #include "info/media/info_media_inner_widget.h" #include "info/info_controller.h" #include "main/main_session.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "lang/lang_keys.h" #include "styles/style_info.h" @@ -40,6 +42,28 @@ Type TabIndexToType(int index) { Unexpected("Index in Info::Media::TabIndexToType()"); } +tr::phrase<> SharedMediaTitle(Type type) { + switch (type) { + case Type::Photo: + return tr::lng_media_type_photos; + case Type::GIF: + return tr::lng_media_type_gifs; + case Type::Video: + return tr::lng_media_type_videos; + case Type::MusicFile: + return tr::lng_media_type_songs; + case Type::File: + return tr::lng_media_type_files; + case Type::RoundVoiceFile: + return tr::lng_media_type_audios; + case Type::Link: + return tr::lng_media_type_links; + case Type::RoundFile: + return tr::lng_media_type_rounds; + } + Unexpected("Bad media type in Info::TitleValue()"); +} + Memento::Memento(not_null<Controller*> controller) : Memento( (controller->peer() @@ -48,6 +72,7 @@ Memento::Memento(not_null<Controller*> controller) ? controller->storiesPeer() : controller->parentController()->session().user()), controller->topic(), + controller->sublist(), controller->migratedPeerId(), (controller->section().type() == Section::Type::Downloads ? Type::File @@ -57,23 +82,31 @@ Memento::Memento(not_null<Controller*> controller) } Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type) -: Memento(peer, nullptr, migratedPeerId, type) { +: Memento(peer, nullptr, nullptr, migratedPeerId, type) { } Memento::Memento(not_null<Data::ForumTopic*> topic, Type type) -: Memento(topic->channel(), topic, PeerId(), type) { +: Memento(topic->channel(), topic, nullptr, PeerId(), type) { +} + +Memento::Memento(not_null<Data::SavedSublist*> sublist, Type type) +: Memento(sublist->owningHistory()->peer, nullptr, sublist, PeerId(), type) { } Memento::Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Type type) -: ContentMemento(peer, topic, migratedPeerId) +: ContentMemento(peer, topic, sublist, migratedPeerId) , _type(type) { _searchState.query.type = type; _searchState.query.peerId = peer->id; - _searchState.query.topicRootId = topic ? topic->rootId() : 0; + _searchState.query.topicRootId = topic ? topic->rootId() : MsgId(); + _searchState.query.monoforumPeerId = sublist + ? sublist->sublistPeer()->id + : PeerId(); _searchState.query.migratedPeerId = migratedPeerId; if (migratedPeerId) { _searchState.migratedList = Storage::SparseIdsList(); @@ -119,25 +152,7 @@ rpl::producer<QString> Widget::title() { if (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) { return tr::lng_profile_shared_media(); } - switch (controller()->section().mediaType()) { - case Section::MediaType::Photo: - return tr::lng_media_type_photos(); - case Section::MediaType::GIF: - return tr::lng_media_type_gifs(); - case Section::MediaType::Video: - return tr::lng_media_type_videos(); - case Section::MediaType::MusicFile: - return tr::lng_media_type_songs(); - case Section::MediaType::File: - return tr::lng_media_type_files(); - case Section::MediaType::RoundVoiceFile: - return tr::lng_media_type_audios(); - case Section::MediaType::Link: - return tr::lng_media_type_links(); - case Section::MediaType::RoundFile: - return tr::lng_media_type_rounds(); - } - Unexpected("Bad media type in Info::TitleValue()"); + return SharedMediaTitle(controller()->section().mediaType())(); } void Widget::setIsStackBottom(bool isStackBottom) { diff --git a/Telegram/SourceFiles/info/media/info_media_widget.h b/Telegram/SourceFiles/info/media/info_media_widget.h index 693d01aeed..1a84139190 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_widget.h @@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_shared_media.h" #include "data/data_search_controller.h" +namespace tr { +template <typename ...Tags> +struct phrase; +} // namespace tr + namespace Data { class ForumTopic; } // namespace Data @@ -19,8 +24,9 @@ namespace Info::Media { using Type = Storage::SharedMediaType; -std::optional<int> TypeToTabIndex(Type type); -Type TabIndexToType(int index); +[[nodiscard]] std::optional<int> TypeToTabIndex(Type type); +[[nodiscard]] Type TabIndexToType(int index); +[[nodiscard]] tr::phrase<> SharedMediaTitle(Type type); class InnerWidget; @@ -29,6 +35,7 @@ public: explicit Memento(not_null<Controller*> controller); Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type); Memento(not_null<Data::ForumTopic*> topic, Type type); + Memento(not_null<Data::SavedSublist*> sublist, Type type); using SearchState = Api::DelayedSearchController::SavedState; @@ -86,6 +93,7 @@ private: Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Type type); diff --git a/Telegram/SourceFiles/info/members/info_members_widget.cpp b/Telegram/SourceFiles/info/members/info_members_widget.cpp index 0a00a7344d..07609a430e 100644 --- a/Telegram/SourceFiles/info/members/info_members_widget.cpp +++ b/Telegram/SourceFiles/info/members/info_members_widget.cpp @@ -26,7 +26,7 @@ Memento::Memento(not_null<Controller*> controller) } Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId) -: ContentMemento(peer, nullptr, migratedPeerId) { +: ContentMemento(peer, nullptr, nullptr, migratedPeerId) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index 19012f3e96..51c83a2f77 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -92,7 +92,7 @@ void GiftButton::setDescriptor(const GiftDescriptor &descriptor, Mode mode) { return; } auto player = base::take(_player); - const auto starsType = Ui::Premium::MiniStars::Type::SlowStars; + const auto starsType = Ui::Premium::MiniStarsType::SlowStars; _mediaLifetime.destroy(); unsubscribe(); diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index b3454d1e17..86b7d7b527 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -368,7 +368,9 @@ void InnerWidget::loadMore() { } else { _allLoaded = true; } - _totalCount = data.vcount().v; + if (!filter.skipsSomething()) { + _totalCount = data.vcount().v; + } const auto owner = &_peer->owner(); owner->processUsers(data.vusers()); @@ -583,11 +585,7 @@ void InnerWidget::refreshAbout() { const auto filter = _filter.current(); const auto filteredEmpty = _allLoaded && _entries.empty() - && (filter.skipLimited - || filter.skipUnlimited - || filter.skipSaved - || filter.skipUnsaved - || filter.skipUnique); + && filter.skipsSomething(); if (filteredEmpty) { auto text = tr::lng_peer_gifts_empty_search( @@ -680,7 +678,7 @@ void InnerWidget::restoreState(not_null<Memento*> memento) { } Memento::Memento(not_null<PeerData*> peer) -: ContentMemento(peer, nullptr, PeerId()) { +: ContentMemento(peer, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h index 2624658016..2ef1fb67cd 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h @@ -34,6 +34,14 @@ struct Filter { bool skipSaved : 1 = false; bool skipUnsaved : 1 = false; + [[nodiscard]] bool skipsSomething() const { + return skipLimited + || skipUnlimited + || skipSaved + || skipUnsaved + || skipUnique; + } + friend inline bool operator==(Filter, Filter) = default; }; diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index acc48462c5..4fd2822aee 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -34,8 +34,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_folder.h" +#include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_peer_values.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/notify/data_notify_settings.h" @@ -855,19 +857,20 @@ template <typename Text, typename ToggleOn, typename Callback> st)); } -rpl::producer<uint64> AddCurrencyAction( +rpl::producer<CreditsAmount> AddCurrencyAction( not_null<UserData*> user, not_null<Ui::VerticalLayout*> wrap, not_null<Controller*> controller) { struct State final { - rpl::variable<uint64> balance; + rpl::variable<CreditsAmount> balance; }; const auto state = wrap->lifetime().make_state<State>(); const auto parentController = controller->parentController(); const auto wrapButton = AddActionButton( wrap, tr::lng_manage_peer_bot_balance_currency(), - state->balance.value() | rpl::map(rpl::mappers::_1 > 0), + state->balance.value( + ) | rpl::map(rpl::mappers::_1 > CreditsAmount(0)), [=] { parentController->showSection(Info::ChannelEarn::Make(user)); }, nullptr); { @@ -895,14 +898,16 @@ rpl::producer<uint64> AddCurrencyAction( = std::make_shared<rpl::lifetime>(); const auto currencyLoad = currencyLoadLifetime->make_state<Api::EarnStatistics>(user); - const auto done = [=](Data::EarnInt balance) { + const auto done = [=](CreditsAmount balance) { if ([[maybe_unused]] const auto strong = weak.data()) { state->balance = balance; currencyLoadLifetime->destroy(); } }; currencyLoad->request() | rpl::start_with_error_done( - [=](const QString &error) { done(0); }, + [=](const QString &error) { + done(CreditsAmount(0, CreditsType::Ton)); + }, [=] { done(currencyLoad->data().currentBalance); }, *currencyLoadLifetime); } @@ -924,7 +929,7 @@ rpl::producer<uint64> AddCurrencyAction( ) | rpl::start_with_next([=, &st]( int width, const QString &button, - Data::EarnInt balance) { + CreditsAmount balance) { const auto available = width - rect::m::sum::h(st.padding) - st.style.font->width(button) @@ -946,19 +951,20 @@ rpl::producer<uint64> AddCurrencyAction( return state->balance.value(); } -rpl::producer<StarsAmount> AddCreditsAction( +rpl::producer<CreditsAmount> AddCreditsAction( not_null<UserData*> user, not_null<Ui::VerticalLayout*> wrap, not_null<Controller*> controller) { struct State final { - rpl::variable<StarsAmount> balance; + rpl::variable<CreditsAmount> balance; }; const auto state = wrap->lifetime().make_state<State>(); const auto parentController = controller->parentController(); const auto wrapButton = AddActionButton( wrap, tr::lng_manage_peer_bot_balance_credits(), - state->balance.value() | rpl::map(rpl::mappers::_1 > StarsAmount(0)), + state->balance.value( + ) | rpl::map(rpl::mappers::_1 > CreditsAmount(0)), [=] { parentController->showSection(Info::BotEarn::Make(user)); }, nullptr); { @@ -998,7 +1004,7 @@ rpl::producer<StarsAmount> AddCreditsAction( ) | rpl::start_with_next([=, &st]( int width, const QString &button, - StarsAmount balance) { + CreditsAmount balance) { const auto available = width - rect::m::sum::h(st.padding) - st.style.font->width(button) @@ -1006,7 +1012,7 @@ rpl::producer<StarsAmount> AddCreditsAction( name->setMarkedText( base::duplicate(icon) .append(QChar(' ')) - .append(Lang::FormatStarsAmountDecimal(balance)), + .append(Lang::FormatCreditsAmountDecimal(balance)), Core::TextContext({ .session = &user->session(), .repaint = [=] { name->update(); }, @@ -1026,6 +1032,10 @@ public: not_null<Ui::RpWidget*> parent, not_null<PeerData*> peer, Origin origin); + DetailsFiller( + not_null<Controller*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::SavedSublist*> sublist); DetailsFiller( not_null<Controller*> controller, not_null<Ui::RpWidget*> parent, @@ -1048,6 +1058,12 @@ private: not_null<ChannelData*> channel); Ui::MultiSlideTracker fillDiscussionButtons( not_null<ChannelData*> channel); + void addShowTopicsListButton( + Ui::MultiSlideTracker &tracker, + not_null<Data::Forum*> forum); + void addViewChannelButton( + Ui::MultiSlideTracker &tracker, + not_null<ChannelData*> channel); void addReportReaction(Ui::MultiSlideTracker &tracker); void addReportReaction( @@ -1071,6 +1087,7 @@ private: not_null<Ui::RpWidget*> _parent; not_null<PeerData*> _peer; Data::ForumTopic *_topic = nullptr; + Data::SavedSublist *_sublist = nullptr; Origin _origin; object_ptr<Ui::VerticalLayout> _wrap; @@ -1170,6 +1187,17 @@ DetailsFiller::DetailsFiller( , _wrap(_parent) { } +DetailsFiller::DetailsFiller( + not_null<Controller*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::SavedSublist*> sublist) +: _controller(controller) +, _parent(parent) +, _peer(sublist->sublistPeer()) +, _sublist(sublist) +, _wrap(_parent) { +} + DetailsFiller::DetailsFiller( not_null<Controller*> controller, not_null<Ui::RpWidget*> parent, @@ -1896,9 +1924,14 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupPersonalChannel( style::al_left); return; } - if (!state->view.prepared(item, nullptr)) { + if (!state->view.prepared(item, nullptr, nullptr)) { const auto repaint = [=] { preview->update(); }; - state->view.prepare(item, nullptr, repaint, {}); + state->view.prepare( + item, + nullptr, + nullptr, + repaint, + {}); } state->view.paint(p, preview->rect(), { .st = &st::defaultDialogRow, @@ -2217,23 +2250,37 @@ void DetailsFiller::addReportReaction( } Ui::MultiSlideTracker DetailsFiller::fillTopicButtons() { + Ui::MultiSlideTracker tracker; + addShowTopicsListButton(tracker, _topic->forum()); + return tracker; +} + +void DetailsFiller::addShowTopicsListButton( + Ui::MultiSlideTracker &tracker, + not_null<Data::Forum*> forum) { using namespace rpl::mappers; - Ui::MultiSlideTracker tracker; const auto window = _controller->parentController(); - - const auto forum = _topic->forum(); + const auto channel = forum->channel(); auto showTopicsVisible = rpl::combine( window->adaptive().oneColumnValue(), window->shownForum().value(), _1 || (_2 != forum)); + const auto callback = [=] { + if (const auto forum = channel->forum()) { + if (channel->useSubsectionTabs()) { + window->searchInChat(forum->history()); + } else { + window->showForum(forum); + } + } + }; AddMainButton( _wrap, tr::lng_forum_show_topics_list(), std::move(showTopicsVisible), - [=] { window->showForum(forum); }, + callback, tracker); - return tracker; } Ui::MultiSlideTracker DetailsFiller::fillUserButtons( @@ -2271,16 +2318,25 @@ Ui::MultiSlideTracker DetailsFiller::fillUserButtons( if (!user->isVerifyCodes()) { addSendMessageButton(); } - addReportReaction(tracker); + if (!_sublist) { + addReportReaction(tracker); + } return tracker; } Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( not_null<ChannelData*> channel) { + Ui::MultiSlideTracker tracker; + addViewChannelButton(tracker, channel); + return tracker; +} + +void DetailsFiller::addViewChannelButton( + Ui::MultiSlideTracker &tracker, + not_null<ChannelData*> channel) { using namespace rpl::mappers; - Ui::MultiSlideTracker tracker; auto window = _controller->parentController(); auto activePeerValue = window->activeChatValue( ) | rpl::map([](Dialogs::Key key) { @@ -2301,8 +2357,6 @@ Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( std::move(viewChannelVisible), std::move(viewChannel), tracker); - - return tracker; } Ui::MultiSlideTracker DetailsFiller::fillDiscussionButtons( @@ -2330,6 +2384,14 @@ Ui::MultiSlideTracker DetailsFiller::fillDiscussionButtons( std::move(viewDiscussion), tracker); + if (const auto forum = channel->forum()) { + if (channel->useSubsectionTabs()) { + addShowTopicsListButton(tracker, forum); + } + } else if (const auto broadcast = channel->monoforumBroadcast()) { + addViewChannelButton(tracker, broadcast); + } + return tracker; } @@ -2341,7 +2403,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() { } else { add(object_ptr<Ui::BoxContentDivider>(_wrap)); } - if (const auto user = _peer->asUser()) { + if (const auto user = _sublist ? nullptr : _peer->asUser()) { add(setupPersonalChannel(user)); } add(CreateSkipWidget(_wrap)); @@ -2356,7 +2418,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() { } } } - if (!_peer->isSelf()) { + if (!_sublist && !_peer->isSelf()) { add(setupMuteToggle()); } setupMainButtons(); @@ -2464,8 +2526,8 @@ void ActionsFiller::addBalanceActions(not_null<UserData*> user) { rpl::combine( std::move(currencyBalance), std::move(creditsBalance) - ) | rpl::map((rpl::mappers::_1 > 0) - || (rpl::mappers::_2 > StarsAmount(0)))); + ) | rpl::map((rpl::mappers::_1 > CreditsAmount(0)) + || (rpl::mappers::_2 > CreditsAmount(0)))); } void ActionsFiller::addInviteToGroupAction(not_null<UserData*> user) { @@ -2819,6 +2881,14 @@ object_ptr<Ui::RpWidget> SetupDetails( return filler.fill(); } +object_ptr<Ui::RpWidget> SetupDetails( + not_null<Controller*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::SavedSublist*> sublist) { + DetailsFiller filler(controller, parent, sublist); + return filler.fill(); +} + object_ptr<Ui::RpWidget> SetupDetails( not_null<Controller*> controller, not_null<Ui::RpWidget*> parent, @@ -2967,18 +3037,20 @@ object_ptr<Ui::RpWidget> SetupChannelMembersAndManage( auto creditsValue = rpl::single( rpl::empty_value() ) | rpl::then(rpl::duplicate(refreshed)) | rpl::map([=] { - return channel->session().credits().balance(channel->id).whole(); + return channel->session().credits().balance(channel->id); }); auto currencyValue = rpl::single( rpl::empty_value() ) | rpl::then(rpl::duplicate(refreshed)) | rpl::map([=] { return channel->session().credits().balanceCurrency(channel->id); }); + const auto emptyAmount = CreditsAmount(0); balanceWrap->toggleOn( rpl::combine( rpl::duplicate(creditsValue), rpl::duplicate(currencyValue) - ) | rpl::map(rpl::mappers::_1 > 0 || rpl::mappers::_2 > 0), + ) | rpl::map(rpl::mappers::_1 > emptyAmount + || rpl::mappers::_2 > emptyAmount), anim::type::normal); balanceWrap->finishAnimating(); @@ -3013,13 +3085,16 @@ object_ptr<Ui::RpWidget> SetupChannelMembersAndManage( rpl::combine( std::move(creditsValue), std::move(currencyValue) - ) | rpl::map([](uint64 credits, uint64 currency) { - auto creditsText = (credits > 0) - ? Ui::Text::SingleCustomEmoji(Ui::kCreditsCurrency) + ) | rpl::map([](CreditsAmount credits, CreditsAmount currency) { + auto creditsText = (credits > CreditsAmount(0)) + ? Ui::MakeCreditsIconEntity() .append(QChar(' ')) - .append(QString::number(credits)) + .append(Info::ChannelEarn::MajorPart(credits)) + .append(credits.nano() + ? Info::ChannelEarn::MinorPart(credits) + : QString()) : TextWithEntities(); - auto currencyText = (currency > 0) + auto currencyText = (currency > CreditsAmount(0)) ? Ui::Text::SingleCustomEmoji("_") .append(QChar(' ')) .append(Info::ChannelEarn::MajorPart(currency)) @@ -3068,7 +3143,9 @@ Cover *AddCover( not_null<Ui::VerticalLayout*> container, not_null<Controller*> controller, not_null<PeerData*> peer, - Data::ForumTopic *topic) { + Data::ForumTopic *topic, + Data::SavedSublist *sublist) { + const auto shown = sublist ? sublist->sublistPeer() : peer; const auto result = topic ? container->add(object_ptr<Cover>( container, @@ -3077,13 +3154,13 @@ Cover *AddCover( : container->add(object_ptr<Cover>( container, controller->parentController(), - peer, + shown, [=] { return controller->wrapWidget(); })); result->showSection( ) | rpl::start_with_next([=](Section section) { controller->showSection(topic ? std::make_shared<Info::Memento>(topic, section) - : std::make_shared<Info::Memento>(peer, section)); + : std::make_shared<Info::Memento>(shown, section)); }, result->lifetime()); result->setOnlineCount(rpl::single(0)); return result; @@ -3094,9 +3171,12 @@ void AddDetails( not_null<Controller*> controller, not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, Origin origin) { if (topic) { container->add(SetupDetails(controller, container, topic)); + } else if (sublist) { + container->add(SetupDetails(controller, container, sublist)); } else { container->add(SetupDetails(controller, container, peer, origin)); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.h b/Telegram/SourceFiles/info/profile/info_profile_actions.h index 584f2d7f86..70f5ef054f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.h +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.h @@ -16,6 +16,7 @@ class VerticalLayout; namespace Data { class ForumTopic; +class SavedSublist; } // namespace Data namespace Info { @@ -55,12 +56,14 @@ Cover *AddCover( not_null<Ui::VerticalLayout*> container, not_null<Controller*> controller, not_null<PeerData*> peer, - Data::ForumTopic *topic); + Data::ForumTopic *topic, + Data::SavedSublist *sublist); void AddDetails( not_null<Ui::VerticalLayout*> container, not_null<Controller*> controller, not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, Origin origin); } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp index b492ad7b6b..ed103202b7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp @@ -137,9 +137,14 @@ void Badge::setContent(Content content) { }, _view->lifetime()); } break; case BadgeType::Scam: - case BadgeType::Fake: { - const auto fake = (_content.badge == BadgeType::Fake); - const auto size = Ui::ScamBadgeSize(fake); + case BadgeType::Fake: + case BadgeType::Direct: { + const auto type = (_content.badge == BadgeType::Direct) + ? Ui::TextBadgeType::Direct + : (_content.badge == BadgeType::Fake) + ? Ui::TextBadgeType::Fake + : Ui::TextBadgeType::Scam; + const auto size = Ui::TextBadgeSize(type); const auto skip = st::infoVerifiedCheckPosition.x(); _view->resize( size.width() + 2 * skip, @@ -147,12 +152,14 @@ void Badge::setContent(Content content) { _view->paintRequest( ) | rpl::start_with_next([=, badge = _view.data()]{ Painter p(badge); - Ui::DrawScamBadge( - fake, + Ui::DrawTextBadge( + type, p, badge->rect().marginsRemoved({ skip, skip, skip, skip }), badge->width(), - st::attentionButtonFg); + (type == Ui::TextBadgeType::Direct + ? st::windowSubTextFg + : st::attentionButtonFg)); }, _view->lifetime()); } break; case BadgeType::Extera: diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.h b/Telegram/SourceFiles/info/profile/info_profile_badge.h index 805acb4dcf..8e986e1e0c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.h +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.h @@ -35,15 +35,16 @@ namespace Info::Profile { class EmojiStatusPanel; -enum class BadgeType { +enum class BadgeType : uchar { None = 0x00, Verified = 0x01, BotVerified = 0x02, Premium = 0x04, Scam = 0x08, Fake = 0x10, - Extera = 0x20, - ExteraSupporter = 0x40, + Direct = 0x20, + Extera = 0x40, + ExteraSupporter = 0x80, // todo: remove `uchar` if they add more badges }; inline constexpr bool is_flag_type(BadgeType) { return true; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index f25d90e5d7..48552b067c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -645,10 +645,11 @@ Cover::Cover( : object_ptr<Ui::UserpicButton>( this, controller, - _peer, + _peer->userpicPaintingPeer(), Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, - _st.photo)) + _st.photo, + _peer->userpicShape())) , _changePersonal((role == Role::Info || topic || !_peer->isUser() @@ -664,6 +665,9 @@ Cover::Cover( , _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) , _refreshStatusTimer([this] { refreshStatusText(); }) { _peer->updateFull(); + if (const auto broadcast = _peer->monoforumBroadcast()) { + broadcast->updateFull(); + } _name->setSelectable(true); _name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now)); @@ -872,7 +876,8 @@ void Cover::refreshUploadPhotoOverlay() { if (const auto chat = _peer->asChat()) { return chat->canEditInformation(); } else if (const auto channel = _peer->asChannel()) { - return channel->canEditInformation(); + return channel->canEditInformation() + && !channel->isMonoforum(); } else if (const auto user = _peer->asUser()) { return user->isSelf() || (user->isContact() @@ -1025,6 +1030,12 @@ void Cover::refreshStatusText() { chat->count, int(chat->participants.size())); return { .text = ChatStatusText(fullCount, onlineCount, true) }; + } else if (auto broadcast = _peer->monoforumBroadcast()) { + auto result = ChatStatusText( + qMax(broadcast->membersCount(), 1), + 0, + false); + return TextWithEntities{ .text = result }; } else if (auto channel = _peer->asChannel()) { const auto onlineCount = _onlineCount.current(); const auto fullCount = qMax(channel->membersCount(), 1); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 80ceb966a1..00ee314ae7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_file_origin.h" #include "data/data_user.h" +#include "data/data_saved_sublist.h" #include "main/main_session.h" #include "apiwrap.h" #include "api/api_peer_photo.h" @@ -50,6 +51,7 @@ InnerWidget::InnerWidget( , _peer(_controller->key().peer()) , _migrated(_controller->migrated()) , _topic(_controller->key().topic()) +, _sublist(_controller->key().sublist()) , _content(setupContent(this, origin)) { _content->heightValue( ) | rpl::start_with_next([this](int height) { @@ -79,14 +81,14 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent( } auto result = object_ptr<Ui::VerticalLayout>(parent); - _cover = AddCover(result, _controller, _peer, _topic); + _cover = AddCover(result, _controller, _peer, _topic, _sublist); if (_topic && _topic->creating()) { return result; } - AddDetails(result, _controller, _peer, _topic, origin); + AddDetails(result, _controller, _peer, _topic, _sublist, origin); result->add(setupSharedMedia(result.data())); - if (_topic) { + if (_topic || _sublist) { return result; } { @@ -103,7 +105,9 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent( result->add(std::move(actions)); } if (_peer->isChat() || _peer->isMegagroup()) { - setupMembers(result.data()); + if (!_peer->isMonoforum()) { + setupMembers(result.data()); + } } return result; } @@ -151,7 +155,8 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia( content, _controller, _peer, - _topic ? _topic->rootId() : 0, + _topic ? _topic->rootId() : MsgId(), + _sublist ? _sublist->sublistPeer()->id : PeerId(), _migrated, type, tracker); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h index a1b257801b..65936bcae5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { class ForumTopic; +class SavedSublist; class PhotoMedia; } // namespace Data @@ -74,6 +75,7 @@ private: const not_null<PeerData*> _peer; PeerData * const _migrated = nullptr; Data::ForumTopic * const _topic = nullptr; + Data::SavedSublist * const _sublist = nullptr; PeerData *_reactionGroup = nullptr; diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp index 1987be5d49..619a5206a8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp @@ -39,6 +39,10 @@ void MemberListRow::setType(Type type) { : QString()); } +MemberListRow::Type MemberListRow::type() const { + return _type; +} + bool MemberListRow::rightActionDisabled() const { return true; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h index 37d04cb7c6..c867d731d5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h @@ -34,6 +34,7 @@ public: MemberListRow(not_null<UserData*> user, Type type); void setType(Type type); + [[nodiscard]] Type type() const; bool rightActionDisabled() const override; QMargins rightActionMargins() const override; void refreshStatus() override; diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 8caddbf0c7..ac25b6c436 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -93,6 +93,9 @@ void StripExternalLinks(TextWithEntities &text) { } // namespace rpl::producer<QString> NameValue(not_null<PeerData*> peer) { + if (const auto broadcast = peer->monoforumBroadcast()) { + return NameValue(broadcast); + } return peer->session().changes().peerFlagsValue( peer, UpdateFlag::Name @@ -540,6 +543,7 @@ rpl::producer<int> KickedCountValue(not_null<ChannelData*> channel) { rpl::producer<int> SharedMediaCountValue( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Storage::SharedMediaType type) { auto aroundId = 0; @@ -550,6 +554,7 @@ rpl::producer<int> SharedMediaCountValue( SparseIdsMergedSlice::Key( peer->id, topicRootId, + monoforumPeerId, migrated ? migrated->id : 0, aroundId), type), @@ -588,8 +593,8 @@ rpl::producer<int> SavedSublistCountValue( not_null<PeerData*> peer) { const auto saved = &peer->owner().savedMessages(); const auto sublist = saved->sublist(peer); - if (!sublist->fullCount()) { - saved->loadMore(sublist); + if (!sublist->fullCount().has_value()) { + sublist->loadFullCount(); return rpl::single(0) | rpl::then(sublist->fullCountValue()); } return sublist->fullCountValue(); @@ -659,6 +664,8 @@ rpl::producer<BadgeType> BadgeValueFromFlags(Peer peer) { ? BadgeType::Scam : (value & Flag::Fake) ? BadgeType::Fake + : peer->isMonoforum() + ? BadgeType::Direct : (value & Flag::Verified) ? BadgeType::Verified : premium diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index dab3cf5065..16e9ed5d64 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -113,6 +113,7 @@ struct LinkWithUrl { [[nodiscard]] rpl::producer<int> SharedMediaCountValue( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Storage::SharedMediaType type); [[nodiscard]] rpl::producer<int> CommonGroupsCountValue( @@ -130,7 +131,7 @@ struct LinkWithUrl { [[nodiscard]] rpl::producer<bool> CanViewParticipantsValue( not_null<ChannelData*> megagroup); -enum class BadgeType; +enum class BadgeType : uchar; [[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer); [[nodiscard]] rpl::producer<EmojiStatusId> EmojiStatusIdValue( not_null<PeerData*> peer); diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp index 6f62b98975..a11face226 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_widget.h" #include "dialogs/ui/dialogs_stories_content.h" +#include "history/history.h" #include "info/profile/info_profile_inner_widget.h" #include "info/profile/info_profile_members.h" #include "ui/widgets/scroll_area.h" @@ -15,6 +16,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_user.h" #include "lang/lang_keys.h" #include "info/info_controller.h" @@ -25,6 +27,7 @@ Memento::Memento(not_null<Controller*> controller) : Memento( controller->peer(), controller->topic(), + controller->sublist(), controller->migratedPeerId(), { v::null }) { } @@ -33,20 +36,25 @@ Memento::Memento( not_null<PeerData*> peer, PeerId migratedPeerId, Origin origin) -: Memento(peer, nullptr, migratedPeerId, origin) { +: Memento(peer, nullptr, nullptr, migratedPeerId, origin) { } Memento::Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Origin origin) -: ContentMemento(peer, topic, migratedPeerId) +: ContentMemento(peer, topic, sublist, migratedPeerId) , _origin(origin) { } Memento::Memento(not_null<Data::ForumTopic*> topic) -: ContentMemento(topic->channel(), topic, 0) { +: ContentMemento(topic->channel(), topic, nullptr, 0) { +} + +Memento::Memento(not_null<Data::SavedSublist*> sublist) +: ContentMemento(sublist->owningHistory()->peer, nullptr, sublist, 0) { } Section Memento::section() const { @@ -102,6 +110,9 @@ void Widget::setInnerFocus() { rpl::producer<QString> Widget::title() { if (controller()->key().topic()) { return tr::lng_info_topic_title(); + } else if (controller()->key().sublist() + && controller()->key().sublist()->parentChat()) { + return tr::lng_profile_direct_messages(); } const auto peer = controller()->key().peer(); if (const auto user = peer->asUser()) { @@ -109,7 +120,9 @@ rpl::producer<QString> Widget::title() { ? tr::lng_info_bot_title() : tr::lng_info_user_title(); } else if (const auto channel = peer->asChannel()) { - return channel->isMegagroup() + return channel->isMonoforum() + ? tr::lng_profile_direct_messages() + : channel->isMegagroup() ? tr::lng_info_group_title() : tr::lng_info_channel_title(); } else if (peer->isChat()) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.h b/Telegram/SourceFiles/info/profile/info_profile_widget.h index b6e4da66a9..6d1bfcefbf 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.h +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.h @@ -35,6 +35,7 @@ public: PeerId migratedPeerId, Origin origin = { v::null }); explicit Memento(not_null<Data::ForumTopic*> topic); + explicit Memento(not_null<Data::SavedSublist*> sublist); object_ptr<ContentWidget> createWidget( QWidget *parent, @@ -56,6 +57,7 @@ private: Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Origin origin); diff --git a/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp index 2fae27d580..b006dbbe28 100644 --- a/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp +++ b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp @@ -188,7 +188,7 @@ std::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() { } Memento::Memento(not_null<PeerData*> peer) -: ContentMemento(peer, nullptr, PeerId()) { +: ContentMemento(peer, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index 9a070889aa..298ef5f3e1 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -8,10 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/saved/info_saved_sublists_widget.h" // #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "dialogs/dialogs_inner_widget.h" -#include "history/view/history_view_sublist_section.h" +#include "history/view/history_view_chat_section.h" #include "info/media/info_media_buttons.h" #include "info/profile/info_profile_icon.h" #include "info/info_controller.h" @@ -28,7 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Saved { SublistsMemento::SublistsMemento(not_null<Main::Session*> session) -: ContentMemento(session->user(), nullptr, PeerId()) { +: ContentMemento(session->user(), nullptr, nullptr, PeerId()) { } Section SublistsMemento::section() const { @@ -63,10 +64,14 @@ SublistsWidget::SublistsWidget( _list->chosenRow() | rpl::start_with_next([=](Dialogs::ChosenRow row) { if (const auto sublist = row.key.sublist()) { using namespace Window; + using namespace HistoryView; auto params = SectionShow(SectionShow::Way::Forward); params.dropSameFromStack = true; controller->showSection( - std::make_shared<HistoryView::SublistMemento>(sublist), + std::make_shared<ChatMemento>(ChatViewId{ + .history = sublist->owningHistory(), + .sublist = sublist, + }), params); } }, _list->lifetime()); @@ -108,6 +113,7 @@ void SublistsWidget::setupOtherTypes() { controller(), peer, MsgId(), // topicRootId + PeerId(), // monoforumPeerId nullptr, // migrated buttonType, tracker); diff --git a/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp b/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp index 3646b5b683..003166ff98 100644 --- a/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp +++ b/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp @@ -438,7 +438,7 @@ std::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() { } Memento::Memento(not_null<PeerData*> peer) -: ContentMemento(peer, nullptr, PeerId()) { +: ContentMemento(peer, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index 5412b5886b..f02f2f07c6 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -957,4 +957,3 @@ void InnerWidget::showFinished() { } } // namespace Info::Statistics - diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 33a131d110..a956b763ea 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_statistics.h" #include "boxes/peer_list_controllers.h" #include "boxes/peer_list_widgets.h" +#include "info/channel_statistics/earn/earn_icons.h" +#include "info/channel_statistics/earn/earn_format.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "core/ui_integration.h" // TextContext #include "data/data_channel.h" @@ -32,6 +34,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/toggle_arrow.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" @@ -39,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "styles/style_boxes.h" #include "styles/style_color_indices.h" +#include "styles/style_channel_earn.h" #include "styles/style_credits.h" #include "styles/style_dialogs.h" // dialogsStoriesFull. #include "styles/style_layers.h" // boxRowPadding. @@ -46,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" #include "styles/style_statistics.h" #include "styles/style_window.h" +#include "styles/style_chat.h" namespace Info::Statistics { namespace { @@ -744,6 +750,29 @@ rpl::producer<int> BoostsController::totalBoostsValue() const { return _totalBoosts.value(); } +struct CreditsRowDescriptionData { + uint64 rowId = 0; + uint64 bareGiftStickerId = 0; +}; + +[[nodiscard]] QString SerializeCreditsRowDescriptionData( + const CreditsRowDescriptionData &data) { + return QString("%1;%2").arg(data.rowId).arg(data.bareGiftStickerId); +} + +[[nodiscard]] CreditsRowDescriptionData DeserializeCreditsRowDescriptionData( + const QString &str) { + auto data = CreditsRowDescriptionData(); + const auto parts = str.split(';'); + if (parts.size() >= 1) { + data.rowId = parts[0].toULongLong(); + } + if (parts.size() >= 2) { + data.bareGiftStickerId = parts[1].toULongLong(); + } + return data; +} + class CreditsRow final : public PeerListRow { public: struct Descriptor final { @@ -758,6 +787,8 @@ public: CreditsRow(not_null<PeerData*> peer, const Descriptor &descriptor); CreditsRow(const Descriptor &descriptor); + void init(); + [[nodiscard]] const Data::CreditsHistoryEntry &entry() const; [[nodiscard]] const Data::SubscriptionEntry &subscription() const; [[nodiscard]] QString generateName() override; @@ -789,12 +820,11 @@ public: const style::PeerListItem &st) const override; private: - void init(); - const not_null<Main::Session*> _session; const Data::CreditsHistoryEntry _entry; const Data::SubscriptionEntry _subscription; const Ui::Text::MarkedContext _context; + const int _rowHeight; PaintRoundImageCallback _paintUserpicCallback; @@ -803,6 +833,7 @@ private: Ui::Text::String _description; Ui::Text::String _rightText; + Ui::Text::String _rightMinorText; std::shared_ptr<Ui::DynamicImage> _descriptionThumbnail; QImage _descriptionThumbnailCache; @@ -835,7 +866,6 @@ CreditsRow::CreditsRow( st::boostsListBox.item, _subscription.subscription.credits); } - init(); } CreditsRow::CreditsRow(const Descriptor &descriptor) @@ -845,7 +875,6 @@ CreditsRow::CreditsRow(const Descriptor &descriptor) , _subscription(descriptor.subscription) , _context(descriptor.context) , _rowHeight(descriptor.rowHeight) { - init(); } void CreditsRow::init() { @@ -885,6 +914,8 @@ void CreditsRow::init() { : (_entry.peerType == Data::CreditsHistoryEntry::PeerType::PremiumBot) ? tr::lng_credits_box_history_entry_via_premium_bot(tr::now) + : (_entry.peerType == Data::CreditsHistoryEntry::PeerType::Fragment) + ? tr::lng_credits_box_history_entry_fragment(tr::now) : (_entry.gift && isSpecial) ? tr::lng_credits_box_history_entry_anonymous(tr::now) : (_name == name) @@ -903,6 +934,19 @@ void CreditsRow::init() { langDayOfMonthFull(_subscription.until.date()))); _description.setText(st::defaultTextStyle, _subscription.title); } + if (_entry.bareGiftStickerId) { + _description.setMarkedText( + st::defaultTextStyle, + Ui::Text::SingleCustomEmoji( + SerializeCreditsRowDescriptionData({ + PeerListRow::id(), + _entry.bareGiftStickerId, + })) + .append(' ') + .append(description), + kMarkupTextOptions, + _context); + } const auto descriptionPhotoId = (!_entry.subscriptionUntil.isNull()) ? _entry.photoId : _subscription.photoId; @@ -920,26 +964,45 @@ void CreditsRow::init() { } }); } - auto &manager = _session->data().customEmojiManager(); if (_entry) { constexpr auto kMinus = QChar(0x2212); + const auto isCurrency = _entry.credits.ton(); _rightText.setMarkedText( - st::semiboldTextStyle, + isCurrency + ? st::channelEarnHistoryMajorLabel.style + : st::creditsHistoryRowRightStyle, TextWithEntities() .append(_entry.in ? QChar('+') : kMinus) - .append(Lang::FormatStarsAmountDecimal(_entry.credits.abs())) + .append(isCurrency + ? Info::ChannelEarn::MajorPart(_entry.credits) + : Lang::FormatCreditsAmountDecimal(_entry.credits.abs())) .append(QChar(' ')) - .append(manager.creditsEmoji()), + .append(isCurrency + ? TextWithEntities() + : Ui::MakeCreditsIconEntity()), kMarkupTextOptions, _context); + if (isCurrency) { + _rightMinorText.setMarkedText( + st::channelEarnHistoryMinorLabel.style, + TextWithEntities() + .append(Info::ChannelEarn::MinorPart(_entry.credits)) + .append(QChar(' ')) + .append( + Ui::Text::SingleCustomEmoji(_entry.in + ? u"ton:in"_q + : u"ton:out"_q)), + kMarkupTextOptions, + _context); + } } if (!_paintUserpicCallback) { - _paintUserpicCallback = _entry.stargift + _paintUserpicCallback = /*_entry.stargift ? Ui::GenerateGiftStickerUserpicCallback( _session, _entry.bareGiftStickerId, _context.repaint) - : !isSpecial + : */!isSpecial ? PeerListRow::generatePaintUserpicCallback(false) : Ui::GenerateCreditsPaintUserpicCallback(_entry); } @@ -986,7 +1049,9 @@ QSize CreditsRow::rightActionSize() const { return QSize(maxWidth + st::boxRowPadding.right(), _rowHeight); } else if (_subscription || _entry) { return QSize( - _rightText.maxWidth() + st::boxRowPadding.right() / 2, + _rightText.maxWidth() + + _rightMinorText.maxWidth() + + st::boxRowPadding.right() / 2, _rowHeight); } else if (!_entry && !_subscription) { return QSize(); @@ -1009,7 +1074,6 @@ void CreditsRow::rightActionPaint( int outerWidth, bool selected, bool actionSelected) { - const auto &font = _rightText.style()->font; const auto rightSkip = st::boxRowPadding.right(); if (_rightLabel) { return _rightLabel->draw(p, x, y, _rowHeight); @@ -1040,16 +1104,21 @@ void CreditsRow::rightActionPaint( p.drawTextRight(rightSkip, y - statusFont->height / 2, outerWidth, t); return; } - y += _rowHeight / 2; p.setPen(_entry.pending ? st::creditsStroke : _entry.in ? st::boxTextFgGood : st::menuIconAttentionColor); + const auto xMinor = outerWidth - _rightMinorText.maxWidth() - rightSkip; + _rightMinorText.draw(p, Ui::Text::PaintContext{ + .position = QPoint(xMinor, y + st::creditsHistoryRowRightMinorTop), + .outerWidth = outerWidth, + .availableWidth = outerWidth, + }); _rightText.draw(p, Ui::Text::PaintContext{ .position = QPoint( - outerWidth - _rightText.maxWidth() - rightSkip, - y - font->height / 2), + xMinor - _rightText.maxWidth(), + y + st::creditsHistoryRowRightTop), .outerWidth = outerWidth, .availableWidth = outerWidth, }); @@ -1078,7 +1147,7 @@ void CreditsRow::paintStatusText( available -= thumbnailSpace; } _description.draw(p, { - .position = QPoint(x, y - _description.minHeight()), + .position = QPoint(x, y - st::creditsHistoryRowDescriptionSkip), .outerWidth = outer, .availableWidth = available, .elisionLines = 1, @@ -1118,6 +1187,8 @@ private: Data::CreditsStatusSlice::OffsetToken _apiToken; Ui::Text::MarkedContext _context; + base::flat_map<PeerListRowId, not_null<PeerListRow*>> _rowsById; + rpl::variable<bool> _allLoaded = false; bool _requesting = false; @@ -1129,7 +1200,46 @@ CreditsController::CreditsController(CreditsDescriptor d) , _entryClickedCallback(std::move(d.entryClickedCallback)) , _api(d.peer, d.in, d.out) , _firstSlice(std::move(d.firstSlice)) -, _context(Core::TextContext({ .session = _session })) { +, _context([&]() -> Ui::Text::MarkedContext { + const auto height = st::creditsHistoryRowRightStyle.font->height + - st::lineWidth; + auto customEmojiFactory = [=]( + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr<Ui::Text::CustomEmoji> { + if (data == Ui::kCreditsCurrency) { + return std::make_unique<Ui::Text::ShiftedEmoji>( + Ui::MakeCreditsIconEmoji(height, 1), + QPoint(-st::lineWidth, st::lineWidth)); + } + if (data.startsWith(u"ton"_q)) { + const auto in = data.split(u":"_q)[1].startsWith(u"in"_q); + return std::make_unique<Ui::Text::ShiftedEmoji>( + std::make_unique<Ui::Text::StaticCustomEmoji>( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + in + ? st::boxTextFgGood->c + : st::menuIconAttentionColor->c), + data.toString()), + QPoint(0, st::lineWidth)); + } + const auto desc = DeserializeCreditsRowDescriptionData( + data.toString()); + if (!desc.rowId || !desc.bareGiftStickerId) { + return nullptr; + } + const auto it = _rowsById.find(desc.rowId); + if (it != _rowsById.end()) { + const auto row = it->second; + return _session->data().customEmojiManager().create( + desc.bareGiftStickerId, + [=]{ delegate()->peerListUpdateRow(row); }); + } + return nullptr; + }; + return { .customEmojiFactory = std::move(customEmojiFactory) }; +}()) { PeerListController::setStyleOverrides(&st::creditsHistoryEntriesList); } @@ -1180,15 +1290,19 @@ void CreditsController::applySlice(const Data::CreditsStatusSlice &slice) { delegate()->peerListUpdateRow(row); }, }; + auto owned = std::unique_ptr<CreditsRow>(nullptr); if (i.bareActorId) { const auto peer = session().data().peer(PeerId(i.bareActorId)); - return std::make_unique<CreditsRow>(peer, descriptor); + owned = std::make_unique<CreditsRow>(peer, descriptor); } else if (const auto peerId = PeerId(i.barePeerId + s.barePeerId)) { const auto peer = session().data().peer(peerId); - return std::make_unique<CreditsRow>(peer, descriptor); + owned = std::make_unique<CreditsRow>(peer, descriptor); } else { - return std::make_unique<CreditsRow>(descriptor); + owned = std::make_unique<CreditsRow>(descriptor); } + _rowsById.emplace(owned->id(), owned.get()); + owned->init(); + return owned; }; auto giftPacksRequested = false; diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index d0ab12cb50..4848fd3355 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -135,8 +135,10 @@ void InnerWidget::createProfileTop() { const auto peer = key.storiesPeer(); startTop(); - Profile::AddCover(_top, _controller, peer, nullptr); - Profile::AddDetails(_top, _controller, peer, nullptr, { v::null }); + + using namespace Profile; + AddCover(_top, _controller, peer, nullptr, nullptr); + AddDetails(_top, _controller, peer, nullptr, nullptr, { v::null }); auto tracker = Ui::MultiSlideTracker(); const auto dividerWrap = _top->add( diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 992335f1a7..c15f13cc0a 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -363,6 +363,7 @@ WebViewContext ResolveContext( if (const auto thread = state.key.thread()) { context.action = Api::SendAction(thread); context.action->replyTo = state.currentReplyTo; + context.action->options.suggest = state.currentSuggest; } else { context.action = Api::SendAction(bot->owner().history(bot)); } @@ -377,6 +378,7 @@ WebViewContext ResolveContext( .key = (topic ? Key{ topic } : Key{ history }), .section = (topic ? Section::Replies : Section::History), .currentReplyTo = context.action->replyTo, + .currentSuggest = context.action->options.suggest, }; } return context; @@ -1468,7 +1470,8 @@ bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) { if (Core::InternalPassportLink(local)) { return true; } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive) - && !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) { + && !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive) + && !local.startsWith(u"ton://"_q, Qt::CaseInsensitive)) { return false; } const auto bot = _bot; @@ -1916,8 +1919,8 @@ void WebViewInstance::botSendPreparedMessage( const auto checked = state->sendPayment.check( uiShow(), strong->peer(), + options, 1, - options.starsApproved, withPaymentApproved); if (!checked) { return; @@ -2551,8 +2554,8 @@ void ChooseAndSendLocation( const auto checked = state->sendPayment.check( strong, action.history->peer, + action.options, 1, - action.options.starsApproved, withPaymentApproved); if (!checked) { return; @@ -2613,7 +2616,7 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu( } if (peer->canCreatePolls()) { ++minimal; - raw->addAction(tr::lng_polls_create(tr::now), [=] { + raw->addAction(tr::lng_polls_menu_item(tr::now), [=] { const auto action = actionFactory(); const auto source = action.options.scheduled ? Api::SendType::Scheduled @@ -2623,17 +2626,37 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu( ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); - const auto replyTo = action.replyTo; Window::PeerMenuCreatePoll( controller, peer, - replyTo, + action.replyTo, + action.options.suggest, flag, flag, source, { sendMenuType }); }, &st::menuIconCreatePoll); } + if (peer->canCreateTodoLists()) { + ++minimal; + raw->addAction(tr::lng_todo_menu_item(tr::now), [=] { + const auto action = actionFactory(); + const auto source = action.options.scheduled + ? Api::SendType::Scheduled + : Api::SendType::Normal; + const auto sendMenuType = (action.replyTo.topicRootId + || action.history->peer->starsPerMessageChecked()) + ? SendMenu::Type::SilentOnly + : SendMenu::Type::Scheduled; + Window::PeerMenuCreateTodoList( + controller, + peer, + action.replyTo, + action.options.suggest, + source, + { sendMenuType }); + }, &st::menuIconCreateTodoList); + } const auto session = &controller->session(); const auto locationType = ChatRestriction::SendOther; const auto config = ResolveMapsConfig(session); diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 0b5c01cd39..b79c168013 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -347,9 +347,9 @@ bool Result::onChoose(Layout::ItemBase *layout) { Media::View::OpenRequest Result::openRequest() { using namespace Media::View; if (_document) { - return OpenRequest(nullptr, _document, nullptr, MsgId()); + return OpenRequest(nullptr, _document, nullptr, MsgId(), PeerId()); } else if (_photo) { - return OpenRequest(nullptr, _photo, nullptr, MsgId()); + return OpenRequest(nullptr, _photo, nullptr, MsgId(), PeerId()); } return {}; } diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index b4fbc4e844..09a7e2e383 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -894,6 +894,7 @@ void Instance::show( : nullptr; const auto item = (HistoryItem*)nullptr; const auto topicRootId = MsgId(0); + const auto monoforumPeerId = PeerId(0); if (event.context.startsWith("-photo")) { const auto id = event.context.mid(6).toULongLong(); const auto photo = _shownSession->data().photo(id); @@ -902,7 +903,8 @@ void Instance::show( controller, photo, item, - topicRootId + topicRootId, + monoforumPeerId }); } } else if (event.context.startsWith("-video")) { @@ -913,7 +915,8 @@ void Instance::show( controller, video, item, - topicRootId + topicRootId, + monoforumPeerId }); } } diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index d11cb1039f..eb6f938bf8 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/lang_tag.h" -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "lang/lang_keys.h" #include "ui/text/text.h" #include "base/qt/qt_common_adapters.h" @@ -952,18 +952,18 @@ QString FormatExactCountDecimal(float64 number) { return QLocale().toString(number, 'f', QLocale::FloatingPointShortest); } -ShortenedCount FormatStarsAmountToShort(StarsAmount amount) { +ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) { const auto attempt = FormatCountToShort(amount.whole()); return attempt.shortened ? attempt : ShortenedCount{ - .string = FormatStarsAmountDecimal(amount), + .string = FormatCreditsAmountDecimal(amount), }; } -QString FormatStarsAmountDecimal(StarsAmount amount) { +QString FormatCreditsAmountDecimal(CreditsAmount amount) { return FormatExactCountDecimal(amount.value()); } -QString FormatStarsAmountRounded(StarsAmount amount) { +QString FormatCreditsAmountRounded(CreditsAmount amount) { const auto value = amount.value(); return FormatExactCountDecimal(base::SafeRound(value * 100.) / 100.); } diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 1012d27714..40f3ef9972 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -class StarsAmount; +class CreditsAmount; enum lngtag_count : int; @@ -29,9 +29,10 @@ struct ShortenedCount { [[nodiscard]] ShortenedCount FormatCountToShort(int64 number); [[nodiscard]] QString FormatCountDecimal(int64 number); [[nodiscard]] QString FormatExactCountDecimal(float64 number); -[[nodiscard]] ShortenedCount FormatStarsAmountToShort(StarsAmount amount); -[[nodiscard]] QString FormatStarsAmountDecimal(StarsAmount amount); -[[nodiscard]] QString FormatStarsAmountRounded(StarsAmount amount); +[[nodiscard]] ShortenedCount FormatCreditsAmountToShort( + CreditsAmount amount); +[[nodiscard]] QString FormatCreditsAmountDecimal(CreditsAmount amount); +[[nodiscard]] QString FormatCreditsAmountRounded(CreditsAmount amount); struct PluralResult { int keyShift = 0; diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index f5ace98778..b65c104555 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -89,10 +89,18 @@ int AppConfig::starrefCommissionMax() const { return get<int>(u"starref_max_commission_permille"_q, 900); } +int AppConfig::starsWithdrawMax() const { + return get<int>(u"stars_revenue_withdrawal_max"_q, 100); +} + float64 AppConfig::starsWithdrawRate() const { return get<float64>(u"stars_usd_withdraw_rate_x1000"_q, 1300) / 1000.; } +float64 AppConfig::currencyWithdrawRate() const { + return get<float64>(u"ton_usd_rate"_q, 1); +} + bool AppConfig::paidMessagesAvailable() const { return get<bool>(u"stars_paid_messages_available"_q, false); } @@ -105,6 +113,10 @@ int AppConfig::paidMessageCommission() const { return get<int>(u"stars_paid_message_commission_permille"_q, 850); } +int AppConfig::paidMessageChannelStarsDefault() const { + return get<int>(u"stars_paid_messages_channel_amount_default"_q, 10); +} + int AppConfig::pinnedGiftsLimit() const { return get<int>(u"stargifts_pinned_to_top_limit"_q, 6); } @@ -140,6 +152,62 @@ int AppConfig::giftResaleReceiveThousandths() const { return get<int>(u"stars_stargift_resale_commission_permille"_q, 800); } +int AppConfig::pollOptionsLimit() const { + return get<int>(u"poll_answers_max"_q, 12); +} + +int AppConfig::todoListItemsLimit() const { + return get<int>( + u"todo_items_max"_q, + _account->mtp().isTestMode() ? 10 : 30); +} + +int AppConfig::todoListTitleLimit() const { + return get<int>(u"todo_title_length_max"_q, 32); +} + +int AppConfig::todoListItemTextLimit() const { + return get<int>(u"todo_item_length_max"_q, 64); +} + +int AppConfig::suggestedPostCommissionStars() const { + return get<int>(u"stars_suggested_post_commission_permille"_q, 850); +} + +int AppConfig::suggestedPostCommissionTon() const { + return get<int>(u"ton_suggested_post_commission_permille"_q, 850); +} + +int AppConfig::suggestedPostStarsMin() const { + return get<int>(u"stars_suggested_post_amount_min"_q, 5); +} + +int AppConfig::suggestedPostStarsMax() const { + return get<int>(u"stars_suggested_post_amount_max"_q, 100'000); +} + +int64 AppConfig::suggestedPostNanoTonMin() const { + return get<int64>(u"ton_suggested_post_amount_min"_q, 10'000'000LL); +} + +int64 AppConfig::suggestedPostNanoTonMax() const { + return get<int64>( + u"ton_suggested_post_amount_max"_q, + 10'000'000'000'000LL); +} + +int AppConfig::suggestedPostDelayMin() const { + return get<int>(u"stars_suggested_post_future_min"_q, 300); +} + +int AppConfig::suggestedPostDelayMax() const { + return get<int>(u"appConfig.stars_suggested_post_future_max"_q, 2678400); +} + +TimeId AppConfig::suggestedPostAgeMin() const { + return get<int>(u"stars_suggested_post_age_min"_q, 86400); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index dbf26522cf..a2ea65f426 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -31,6 +31,8 @@ public: return getDouble(key, fallback); } else if constexpr (std::is_same_v<Type, int>) { return int(base::SafeRound(getDouble(key, double(fallback)))); + } else if constexpr (std::is_same_v<Type, int64>) { + return int64(base::SafeRound(getDouble(key, double(fallback)))); } else if constexpr (std::is_same_v<Type, QString>) { return getString(key, fallback); } else if constexpr (std::is_same_v<Type, std::vector<QString>>) { @@ -67,10 +69,13 @@ public: [[nodiscard]] int starrefCommissionMin() const; [[nodiscard]] int starrefCommissionMax() const; + [[nodiscard]] int starsWithdrawMax() const; [[nodiscard]] float64 starsWithdrawRate() const; + [[nodiscard]] float64 currencyWithdrawRate() const; [[nodiscard]] bool paidMessagesAvailable() const; [[nodiscard]] int paidMessageStarsMax() const; [[nodiscard]] int paidMessageCommission() const; + [[nodiscard]] int paidMessageChannelStarsDefault() const; [[nodiscard]] int pinnedGiftsLimit() const; @@ -82,6 +87,21 @@ public: [[nodiscard]] int giftResalePriceMin() const; [[nodiscard]] int giftResaleReceiveThousandths() const; + [[nodiscard]] int pollOptionsLimit() const; + [[nodiscard]] int todoListItemsLimit() const; + [[nodiscard]] int todoListTitleLimit() const; + [[nodiscard]] int todoListItemTextLimit() const; + + [[nodiscard]] int suggestedPostCommissionStars() const; + [[nodiscard]] int suggestedPostCommissionTon() const; + [[nodiscard]] int suggestedPostStarsMin() const; + [[nodiscard]] int suggestedPostStarsMax() const; + [[nodiscard]] int64 suggestedPostNanoTonMin() const; + [[nodiscard]] int64 suggestedPostNanoTonMax() const; + [[nodiscard]] int suggestedPostDelayMin() const; + [[nodiscard]] int suggestedPostDelayMax() const; + [[nodiscard]] TimeId suggestedPostAgeMin() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index b9987430ff..499f938559 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/location_pickers.h" #include "data/components/promo_suggestions.h" #include "data/components/recent_peers.h" +#include "data/components/recent_shared_media_gifts.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" #include "data/components/top_peers.h" @@ -152,6 +153,7 @@ Session::Session( , _sendAsPeers(std::make_unique<SendAsPeers>(this)) , _attachWebView(std::make_unique<InlineBots::AttachWebView>(this)) , _recentPeers(std::make_unique<Data::RecentPeers>(this)) +, _recentSharedGifts(std::make_unique<Data::RecentSharedMediaGifts>(this)) , _scheduledMessages(std::make_unique<Data::ScheduledMessages>(this)) , _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this)) , _topPeers(std::make_unique<Data::TopPeers>(this, Data::TopPeerType::Chat)) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 6d5eaf38f7..d671d46e08 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -33,6 +33,7 @@ namespace Data { class Session; class Changes; class RecentPeers; +class RecentSharedMediaGifts; class ScheduledMessages; class SponsoredMessages; class TopPeers; @@ -133,6 +134,9 @@ public: [[nodiscard]] Data::RecentPeers &recentPeers() const { return *_recentPeers; } + [[nodiscard]] Data::RecentSharedMediaGifts &recentSharedGifts() const { + return *_recentSharedGifts; + } [[nodiscard]] Data::SponsoredMessages &sponsoredMessages() const { return *_sponsoredMessages; } @@ -287,6 +291,7 @@ private: const std::unique_ptr<SendAsPeers> _sendAsPeers; const std::unique_ptr<InlineBots::AttachWebView> _attachWebView; const std::unique_ptr<Data::RecentPeers> _recentPeers; + const std::unique_ptr<Data::RecentSharedMediaGifts> _recentSharedGifts; const std::unique_ptr<Data::ScheduledMessages> _scheduledMessages; const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages; const std::unique_ptr<Data::TopPeers> _topPeers; diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 7d13cde46c..7f562e3e48 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -39,11 +39,12 @@ QByteArray SessionSettings::serialize() const { + Serialize::bytearraySize(autoDownload) + sizeof(qint32) * 11 + (_mutePeriods.size() * sizeof(quint64)) - + sizeof(qint32) * 2 - + _hiddenPinnedMessages.size() * (sizeof(quint64) * 3) - + sizeof(qint32) + + sizeof(qint32) * 3 + _groupEmojiSectionHidden.size() * sizeof(quint64) - + sizeof(qint32) * 2; + + sizeof(qint32) * 3 + + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4) + + sizeof(qint32) + + _verticalSubsectionTabs.size() * sizeof(quint64); auto result = QByteArray(); result.reserve(size); @@ -68,32 +69,37 @@ QByteArray SessionSettings::serialize() const { << qint32(_archiveInMainMenu.current() ? 1 : 0) << qint32(_skipArchiveInSearch.current() ? 1 : 0) << qint32(0) // old _mediaLastPlaybackPosition.size()); - << qint32(0) // very old _hiddenPinnedMessages.size()); + << qint32(0) // very very old _hiddenPinnedMessages.size()); << qint32(_dialogsFiltersEnabled ? 1 : 0) << qint32(_supportAllSilent ? 1 : 0) << qint32(_photoEditorHintShowsCount) - << qint32(0) // old _hiddenPinnedMessages.size()); + << qint32(0) // very old _hiddenPinnedMessages.size()); << qint32(_mutePeriods.size()); for (const auto &period : _mutePeriods) { stream << quint64(period); } stream << qint32(0) // old _skipPremiumStickersSet - << qint32(_hiddenPinnedMessages.size()); - for (const auto &[key, value] : _hiddenPinnedMessages) { - stream - << SerializePeerId(key.peerId) - << qint64(key.topicRootId.bare) - << qint64(value.bare); - } - stream + << qint32(0) // old _hiddenPinnedMessages.size()); << qint32(_groupEmojiSectionHidden.size()); for (const auto &peerId : _groupEmojiSectionHidden) { stream << SerializePeerId(peerId); } stream << qint32(_lastNonPremiumLimitDownload) - << qint32(_lastNonPremiumLimitUpload); + << qint32(_lastNonPremiumLimitUpload) + << qint32(_hiddenPinnedMessages.size()); + for (const auto &[key, value] : _hiddenPinnedMessages) { + stream + << SerializePeerId(key.peerId) + << qint64(key.topicRootId.bare) + << SerializePeerId(key.monoforumPeerId) + << qint64(value.bare); + } + stream << qint32(_verticalSubsectionTabs.size()); + for (const auto &peerId : _verticalSubsectionTabs) { + stream << SerializePeerId(peerId); + } } Ensures(result.size() == size); @@ -153,6 +159,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { std::vector<int> appDictionariesEnabled; qint32 appAutoDownloadDictionaries = app.autoDownloadDictionaries() ? 1 : 0; base::flat_map<ThreadId, MsgId> hiddenPinnedMessages; + base::flat_set<PeerId> verticalSubsectionTabs; qint32 dialogsFiltersEnabled = _dialogsFiltersEnabled ? 1 : 0; qint32 supportAllSilent = _supportAllSilent ? 1 : 0; qint32 photoEditorHintShowsCount = _photoEditorHintShowsCount; @@ -401,6 +408,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { auto count = qint32(0); stream >> count; if (stream.status() == QDataStream::Ok) { + // Legacy. for (auto i = 0; i != count; ++i) { auto keyPeerId = quint64(); auto keyTopicRootId = qint64(); @@ -438,6 +446,49 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { >> lastNonPremiumLimitDownload >> lastNonPremiumLimitUpload; } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto keyPeerId = quint64(); + auto keyTopicRootId = qint64(); + auto keyMonoforumPeerId = quint64(); + auto value = qint64(); + stream + >> keyPeerId + >> keyTopicRootId + >> keyMonoforumPeerId + >> value; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()")); + return; + } + hiddenPinnedMessages.emplace(ThreadId{ + DeserializePeerId(keyPeerId), + keyTopicRootId, + DeserializePeerId(keyMonoforumPeerId), + }, value); + } + } + } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto peerId = quint64(); + stream >> peerId; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()")); + return; + } + verticalSubsectionTabs.emplace(DeserializePeerId(peerId)); + } + } + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for SessionSettings::addFromSerialized()")); @@ -484,6 +535,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { _mutePeriods = std::move(mutePeriods); _lastNonPremiumLimitDownload = lastNonPremiumLimitDownload; _lastNonPremiumLimitUpload = lastNonPremiumLimitUpload; + _verticalSubsectionTabs = std::move(verticalSubsectionTabs); if (version < 2) { app.setLastSeenWarningSeen(appLastSeenWarningSeen == 1); @@ -595,16 +647,22 @@ rpl::producer<bool> SessionSettings::skipArchiveInSearchChanges() const { MsgId SessionSettings::hiddenPinnedMessageId( PeerId peerId, - MsgId topicRootId) const { - const auto i = _hiddenPinnedMessages.find({ peerId, topicRootId }); + MsgId topicRootId, + PeerId monoforumPeerId) const { + const auto i = _hiddenPinnedMessages.find({ + peerId, + topicRootId, + monoforumPeerId, + }); return (i != end(_hiddenPinnedMessages)) ? i->second : 0; } void SessionSettings::setHiddenPinnedMessageId( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId msgId) { - const auto id = ThreadId{ peerId, topicRootId }; + const auto id = ThreadId{ peerId, topicRootId, monoforumPeerId }; if (msgId) { _hiddenPinnedMessages[id] = msgId; } else { @@ -612,6 +670,20 @@ void SessionSettings::setHiddenPinnedMessageId( } } +bool SessionSettings::verticalSubsectionTabs(PeerId peerId) const { + return _verticalSubsectionTabs.contains(peerId); +} + +void SessionSettings::setVerticalSubsectionTabs( + PeerId peerId, + bool vertical) { + if (vertical) { + _verticalSubsectionTabs.emplace(peerId); + } else { + _verticalSubsectionTabs.remove(peerId); + } +} + bool SessionSettings::photoEditorHintShown() const { return _photoEditorHintShowsCount < kPhotoEditorHintMaxShowsCount; } diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index c171968e2c..ee6218e69a 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -110,12 +110,17 @@ public: [[nodiscard]] MsgId hiddenPinnedMessageId( PeerId peerId, - MsgId topicRootId = 0) const; + MsgId topicRootId = 0, + PeerId monoforumPeerId = 0) const; void setHiddenPinnedMessageId( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId msgId); + [[nodiscard]] bool verticalSubsectionTabs(PeerId peerId) const; + void setVerticalSubsectionTabs(PeerId peerId, bool vertical); + [[nodiscard]] bool dialogsFiltersEnabled() const { return _dialogsFiltersEnabled; } @@ -149,6 +154,7 @@ private: struct ThreadId { PeerId peerId; MsgId topicRootId; + PeerId monoforumPeerId; friend inline constexpr auto operator<=>( ThreadId, @@ -164,6 +170,7 @@ private: rpl::variable<bool> _archiveInMainMenu = false; rpl::variable<bool> _skipArchiveInSearch = false; base::flat_map<ThreadId, MsgId> _hiddenPinnedMessages; + base::flat_set<PeerId> _verticalSubsectionTabs; bool _dialogsFiltersEnabled = false; int _photoEditorHintShowsCount = 0; std::vector<TimeId> _mutePeriods; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 60d6cc5299..bcec968478 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_game.h" #include "data/data_peer_values.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_folder.h" @@ -54,8 +55,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_widget.h" #include "history/history_item_helpers.h" // GetErrorForSending. #include "history/view/media/history_view_media.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_service_message.h" -#include "history/view/history_view_sublist_section.h" #include "lang/lang_keys.h" #include "lang/lang_cloud_manager.h" #include "inline_bots/inline_bot_layout_item.h" @@ -226,6 +227,8 @@ StackItemSection::StackItemSection( rpl::producer<> StackItemSection::sectionRemoveRequests() const { if (const auto topic = _memento->topicForRemoveRequests()) { return rpl::merge(_memento->removeRequests(), topic->destroyed()); + } else if (const auto sublist = _memento->sublistForRemoveRequests()) { + return rpl::merge(_memento->removeRequests(), sublist->destroyed()); } return _memento->removeRequests(); } @@ -560,6 +563,7 @@ bool MainWidget::setForwardDraft( const auto history = thread->owningHistory(); const auto items = session().data().idsToItems(draft.ids); const auto topicRootId = thread->topicRootId(); + const auto monoforumPeerId = thread->monoforumPeerId(); const auto error = GetErrorForSending( history->peer, { @@ -574,7 +578,7 @@ bool MainWidget::setForwardDraft( return false; } - history->setForwardDraft(topicRootId, std::move(draft)); + history->setForwardDraft(topicRootId, monoforumPeerId, std::move(draft)); _controller->showThread( thread, ShowAtUnreadMsgId, @@ -601,12 +605,17 @@ bool MainWidget::shareUrl( }; const auto history = thread->owningHistory(); const auto topicRootId = thread->topicRootId(); + const auto monoforumPeerId = thread->monoforumPeerId(); history->setLocalDraft(std::make_unique<Data::Draft>( textWithTags, - FullReplyTo{ .topicRootId = topicRootId }, + FullReplyTo{ + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }, + SuggestPostOptions(), cursor, Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( thread, Data::EntryUpdate::Flag::LocalDraftSet); @@ -649,6 +658,17 @@ bool MainWidget::filesOrForwardDrop( clearHider(_hider); } return true; + } else if (const auto history = thread->asHistory() + ; history && history->peer->monoforum()) { + Window::ShowDropMediaBox( + _controller, + Core::ShareMimeMediaData(data), + history->peer->monoforum()); + if (_hider) { + _hider->startHide(); + clearHider(_hider); + } + return true; } if (data->hasFormat(u"application/x-td-forward"_q)) { auto draft = Data::ForwardDraft{ @@ -782,8 +802,12 @@ void MainWidget::searchMessages( } } else { if (const auto sublist = inChat.sublist()) { + using namespace HistoryView; controller()->showSection( - std::make_shared<HistoryView::SublistMemento>(sublist)); + std::make_shared<ChatMemento>(ChatViewId{ + .history = sublist->owningHistory(), + .sublist = sublist, + })); } else if (!tags.empty()) { inChat = controller()->session().data().history( controller()->session().user()); @@ -1501,7 +1525,7 @@ void MainWidget::showHistory( : Window::SlideDirection::FromRight, animationParams); } else { - _history->show(); + _history->showFast(); crl::on_main(this, [=] { _controller->widget()->setInnerFocus(); }); @@ -1521,6 +1545,8 @@ void MainWidget::showHistory( } floatPlayerCheckVisibility(); + + controller()->dropSubsectionTabs(); } void MainWidget::showMessage( @@ -1549,6 +1575,12 @@ void MainWidget::showMessage( if (params.activation != anim::activation::background) { _controller->window().activate(); } + } else if (const auto sublist = item->savedSublist() + ; sublist && sublist->parentChat()) { + _controller->showSublist(sublist, item->id, params); + if (params.activation != anim::activation::background) { + _controller->window().activate(); + } } else { // showPeerHistory may be redirected to different window, // so we don't call activate() on current controller's window. @@ -2016,6 +2048,8 @@ bool MainWidget::showBackFromStack(const SectionShow ¶ms) { }); return (_dialogs != nullptr); } + session().api().saveCurrentDraftToCloud(); + auto item = std::move(_stack.back()); _stack.pop_back(); if (const auto currentHistoryPeer = _history->peer()) { @@ -2343,6 +2377,9 @@ void MainWidget::updateControlsGeometry() { (thread->asTopic() ? std::make_shared<Info::Memento>( thread->asTopic()) + : thread->asSublist() + ? std::make_shared<Info::Memento>( + thread->asSublist()) : Info::Memento::Default( thread->asHistory()->peer)), params.withThirdColumn()); @@ -2606,14 +2643,17 @@ auto MainWidget::thirdSectionForCurrentMainSection( return std::move(_thirdSectionFromStack); } else if (const auto topic = key.topic()) { return std::make_shared<Info::Memento>(topic); + } else if (const auto sublist = key.sublist() + ; sublist && sublist->parentChat()) { + return std::make_shared<Info::Memento>(sublist); } else if (const auto peer = key.peer()) { return std::make_shared<Info::Memento>( peer, Info::Memento::DefaultSection(peer)); - } else if (key.sublist()) { + } else if (const auto sublist = key.sublist()) { return std::make_shared<Info::Memento>( - session().user(), - Info::Memento::DefaultSection(session().user())); + sublist->owningHistory()->peer, + Info::Memento::DefaultSection(sublist->owningHistory()->peer)); } Unexpected("Key in MainWidget::thirdSectionForCurrentMainSection()."); } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 48ec64735b..fc9281f9fe 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -32,6 +32,7 @@ class Thread; class WallPaper; struct ForwardDraft; class Forum; +class SavedMessages; struct ReportInput; } // namespace Data diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index de1932b7aa..c73c34e5ef 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -85,6 +85,7 @@ struct Instance::ShuffleData { std::vector<UniversalMsgId> playedIds; History *history = nullptr; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; History *migrated = nullptr; bool scheduled = false; int indexInPlayedIds = 0; @@ -247,6 +248,7 @@ void Instance::setHistory( if (history) { data->history = history->migrateToOrMe(); data->topicRootId = 0; + data->monoforumPeerId = 0; data->migrated = data->history->migrateFrom(); setSession(data, &history->session()); } else { @@ -349,6 +351,7 @@ bool Instance::validPlaylist(not_null<const Data*> data) const { const auto inSameDomain = [](const Key &a, const Key &b) { return (a.peerId == b.peerId) && (a.topicRootId == b.topicRootId) + && (a.monoforumPeerId == b.monoforumPeerId) && (a.migratedPeerId == b.migratedPeerId); }; const auto countDistanceInData = [&](const Key &a, const Key &b) { @@ -422,6 +425,7 @@ auto Instance::playlistKey(not_null<const Data*> data) const (item->isScheduled() ? SparseIdsMergedSlice::kScheduledTopicId : data->topicRootId), + data->monoforumPeerId, data->migrated ? data->migrated->peer->id : 0, universalId); } @@ -479,6 +483,7 @@ auto Instance::playlistOtherKey(not_null<const Data*> data) const return SliceKey( data->history->peer->id, data->topicRootId, + data->monoforumPeerId, data->migrated ? data->migrated->peer->id : 0, (data->playlistSlice->skippedBefore() == 0 ? ServerMaxMsgId - 1 @@ -905,6 +910,7 @@ void Instance::validateShuffleData(not_null<Data*> data) { && (key->topicRootId == SparseIdsMergedSlice::kScheduledTopicId); if (raw->history != data->history || raw->topicRootId != data->topicRootId + || raw->monoforumPeerId != data->monoforumPeerId || raw->migrated != data->migrated || raw->scheduled != scheduled) { raw->history = data->history; @@ -962,6 +968,7 @@ void Instance::validateShuffleData(not_null<Data*> data) { SliceKey( raw->history->peer->id, raw->topicRootId, + raw->monoforumPeerId, raw->migrated ? raw->migrated->peer->id : 0, last), data->overview), diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h index ebe7d3d4cb..baf0599869 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.h +++ b/Telegram/SourceFiles/media/player/media_player_instance.h @@ -194,6 +194,7 @@ private: rpl::event_stream<> playlistChanges; History *history = nullptr; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; History *migrated = nullptr; Main::Session *session = nullptr; bool isPlaying = false; diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 1ba70c7fd5..715ee529b4 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -81,11 +81,9 @@ namespace { rpl::single(0) ) | rpl::map([=](TimeId left) { return starsPerMessage - ? tr::lng_message_paid_ph( - lt_amount, - tr::lng_prize_credits_amount( - lt_count, - rpl::single(starsPerMessage * 1.))) + ? tr::lng_message_stars_ph( + lt_count, + rpl::single(starsPerMessage * 1.)) : left ? tr::lng_stealth_mode_countdown( lt_left, @@ -257,7 +255,7 @@ bool ReplyArea::send( }; const auto checked = checkSendPayment( request.messagesCount, - message.action.options.starsApproved, + message.action.options, withPaymentApproved); if (!checked) { return false; @@ -273,7 +271,7 @@ bool ReplyArea::send( bool ReplyArea::checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn<void(int)> withPaymentApproved) { const auto st1 = ::Settings::DarkCreditsEntryBoxStyle(); const auto st2 = st1.shareBox.get(); @@ -282,8 +280,8 @@ bool ReplyArea::checkSendPayment( && _sendPayment.check( _controller->uiShow(), _data.peer, + options, messagesCount, - starsApproved, std::move(withPaymentApproved), { .label = st3 ? st3->chooseDateTimeArgs.labelStyle : nullptr, @@ -292,6 +290,8 @@ bool ReplyArea::checkSendPayment( } void ReplyArea::sendVoice(const VoiceToSend &data) { + auto action = prepareSendAction(data.options); + const auto withPaymentApproved = [=](int approved) { auto copy = data; copy.options.starsApproved = approved; @@ -299,13 +299,12 @@ void ReplyArea::sendVoice(const VoiceToSend &data) { }; const auto checked = checkSendPayment( 1, - data.options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - auto action = prepareSendAction(data.options); session().api().sendVoiceMessage( data.bytes, data.waveform, @@ -341,7 +340,7 @@ bool ReplyArea::sendExistingDocument( }; const auto checked = checkSendPayment( 1, - messageToSend.action.options.starsApproved, + messageToSend.action.options, withPaymentApproved); if (!checked) { return false; @@ -373,6 +372,8 @@ bool ReplyArea::sendExistingPhoto( } else if (showSlowmodeError()) { return false; } + const auto action = prepareSendAction(options); + const auto withPaymentApproved = [=](int approved) { auto copy = options; copy.starsApproved = approved; @@ -380,15 +381,13 @@ bool ReplyArea::sendExistingPhoto( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return false; } - Api::SendExistingPhoto( - Api::MessageToSend(prepareSendAction(options)), - photo); + Api::SendExistingPhoto(Api::MessageToSend(action), photo); _controls->cancelReplyMessage(); finishSending(); @@ -411,6 +410,9 @@ void ReplyArea::sendInlineResult( not_null<UserData*> bot, Api::SendOptions options, std::optional<MsgId> localMessageId) { + auto action = prepareSendAction(options); + action.generateLocal = true; + const auto withPaymentApproved = [=](int approved) { auto copy = options; copy.starsApproved = approved; @@ -418,14 +420,12 @@ void ReplyArea::sendInlineResult( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - auto action = prepareSendAction(options); - action.generateLocal = true; session().api().sendInlineResult( bot, result.get(), @@ -677,6 +677,11 @@ void ReplyArea::sendingFilesConfirmed( void ReplyArea::sendingFilesConfirmed( std::shared_ptr<Ui::PreparedBundle> bundle, Api::SendOptions options) { + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + const auto withPaymentApproved = [=](int approved) { auto copy = options; copy.starsApproved = approved; @@ -684,16 +689,12 @@ void ReplyArea::sendingFilesConfirmed( }; const auto checked = checkSendPayment( bundle->totalCount, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - const auto compress = bundle->way.sendImagesAsPhotos(); - const auto type = compress ? SendMediaType::Photo : SendMediaType::File; - auto action = prepareSendAction(options); - action.clearDraft = false; if (bundle->sendComment) { auto message = Api::MessageToSend(action); message.textWithTags = base::take(bundle->caption); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index bb7fe15f09..0eaf427ec3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -96,7 +96,7 @@ private: [[nodiscard]] bool checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn<void(int)> withPaymentApproved); void uploadFile(const QByteArray &fileContent, SendMediaType type); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 80ec8e75ba..7419a6715e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -135,6 +135,9 @@ namespace Media::Stories { if (options.effectId) { sendFlags |= SendFlag::f_effect; } + if (options.suggest) { + sendFlags |= SendFlag::f_suggested_post; + } if (options.invertCaption) { sendFlags |= SendFlag::f_invert_media; } @@ -170,7 +173,8 @@ namespace Media::Stories { 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) { diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index 76cfd7748a..f512ca45b3 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -30,11 +30,13 @@ public: Window::SessionController *controller, not_null<PhotoData*> photo, HistoryItem *item, - MsgId topicRootId) + MsgId topicRootId, + PeerId monoforumPeerId) : _controller(controller) , _photo(photo) , _item(item) - , _topicRootId(topicRootId) { + , _topicRootId(topicRootId) + , _monoforumPeerId(monoforumPeerId) { } OpenRequest( Window::SessionController *controller, @@ -50,12 +52,14 @@ public: not_null<DocumentData*> document, HistoryItem *item, MsgId topicRootId, + PeerId monoforumPeerId, bool continueStreaming = false, crl::time startTime = 0) : _controller(controller) , _document(document) , _item(item) , _topicRootId(topicRootId) + , _monoforumPeerId(monoforumPeerId) , _continueStreaming(continueStreaming) , _startTime(startTime) { } @@ -92,6 +96,9 @@ public: [[nodiscard]] MsgId topicRootId() const { return _topicRootId; } + [[nodiscard]] PeerId monoforumPeerId() const { + return _monoforumPeerId; + } [[nodiscard]] DocumentData *document() const { return _document; @@ -129,6 +136,7 @@ private: PeerData *_peer = nullptr; HistoryItem *_item = nullptr; MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; std::optional<Data::CloudTheme> _cloudTheme = std::nullopt; bool _continueStreaming = false; crl::time _startTime = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 41c7a7e2b5..afd69eb966 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -370,6 +370,7 @@ struct OverlayWidget::PipWrap { struct OverlayWidget::ItemContext { not_null<HistoryItem*> item; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; }; struct OverlayWidget::StoriesContext { @@ -2684,7 +2685,8 @@ void OverlayWidget::handleDocumentClick() { findWindow(), _document, _message, - _topicRootId); + _topicRootId, + _monoforumPeerId); if (_document && _document->loading() && !_radial.animating()) { _radial.start(_documentMedia->progress()); } @@ -2930,13 +2932,22 @@ void OverlayWidget::showMediaOverview() { const auto topic = _topicRootId ? _history->peer->forumTopicFor(_topicRootId) : nullptr; + const auto sublist = _monoforumPeerId + ? _history->peer->monoforumSublistFor(_monoforumPeerId) + : nullptr; if (_topicRootId && !topic) { return; + } else if (_monoforumPeerId && !sublist) { + return; } window->showSection(_topicRootId ? std::make_shared<Info::Memento>( topic, Info::Section(*overviewType)) + : _monoforumPeerId + ? std::make_shared<Info::Memento>( + sublist, + Info::Section(*overviewType)) : std::make_shared<Info::Memento>( _history->peer, Info::Section(*overviewType))); @@ -3026,6 +3037,7 @@ auto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> { return SharedMediaKey{ _history->peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId _migrated ? _migrated->peer->id : 0, SharedMediaType::ChatPhoto, _photo @@ -3041,6 +3053,7 @@ auto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> { (isScheduled ? SparseIdsMergedSlice::kScheduledTopicId : _topicRootId), + (isScheduled ? PeerId() : _monoforumPeerId), _migrated ? _migrated->peer->id : 0, type, (_message->history() == _history @@ -4624,6 +4637,7 @@ void OverlayWidget::switchToPip() { const auto document = _document; const auto messageId = _message ? _message->fullId() : FullMsgId(); const auto topicRootId = _topicRootId; + const auto monoforumPeerId = _monoforumPeerId; const auto closeAndContinue = [=] { _showAsPip = false; show(OpenRequest( @@ -4631,6 +4645,7 @@ void OverlayWidget::switchToPip() { document, document->owner().message(messageId), topicRootId, + monoforumPeerId, true)); }; _showAsPip = true; @@ -5714,9 +5729,9 @@ OverlayWidget::Entity OverlayWidget::entityForCollage(int index) const { return { v::null, nullptr }; } if (const auto document = std::get_if<DocumentData*>(&items[index])) { - return { *document, _message, _topicRootId }; + return { *document, _message, _topicRootId, _monoforumPeerId }; } else if (const auto photo = std::get_if<PhotoData*>(&items[index])) { - return { *photo, _message, _topicRootId }; + return { *photo, _message, _topicRootId, _monoforumPeerId }; } return { v::null, nullptr }; } @@ -5727,12 +5742,12 @@ OverlayWidget::Entity OverlayWidget::entityForItemId(const FullMsgId &itemId) co if (const auto item = _session->data().message(itemId)) { if (const auto media = item->media()) { if (const auto photo = media->photo()) { - return { photo, item, _topicRootId }; + return { photo, item, _topicRootId, _monoforumPeerId }; } else if (const auto document = media->document()) { - return { document, item, _topicRootId }; + return { document, item, _topicRootId, _monoforumPeerId }; } } - return { v::null, item, _topicRootId }; + return { v::null, item, _topicRootId, _monoforumPeerId }; } return { v::null, nullptr }; } @@ -5759,6 +5774,9 @@ void OverlayWidget::setContext( _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); + _monoforumPeerId = _peer->amMonoforumAdmin() + ? item->monoforumPeerId + : PeerId(); setStoriesPeer(nullptr); } else if (const auto peer = std::get_if<not_null<PeerData*>>(&context)) { _peer = *peer; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index fb8efce90e..8787be47e4 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -170,6 +170,7 @@ private: not_null<DocumentData*>> data; HistoryItem *item = nullptr; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; }; enum class SavePhotoVideo { None, @@ -674,6 +675,7 @@ private: History *_migrated = nullptr; History *_history = nullptr; // if conversation photos or files overview MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; PeerData *_peer = nullptr; UserData *_user = nullptr; // if user profile photos overview diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.cpp b/Telegram/SourceFiles/menu/menu_item_download_files.cpp index 2f6be600bd..57b84210f4 100644 --- a/Telegram/SourceFiles/menu/menu_item_download_files.cpp +++ b/Telegram/SourceFiles/menu/menu_item_download_files.cpp @@ -243,15 +243,19 @@ void AddDownloadFilesAction( void AddDownloadFilesAction( not_null<Ui::PopupMenu*> menu, not_null<Window::SessionController*> window, - const std::map<HistoryItem*, TextSelection, std::less<>> &items, + const base::flat_map<HistoryItem*, TextSelection, std::less<>> &items, not_null<HistoryInner*> list) { if (items.empty()) { return; } + auto sortedItems = ranges::views::all(items) + | ranges::views::keys + | ranges::to<std::vector>(); + ranges::sort(sortedItems, {}, &HistoryItem::fullId); auto docs = Documents(); auto photos = Photos(); - for (const auto &pair : items) { - if (!Added(pair.first, docs, photos)) { + for (const auto &item : sortedItems) { + if (!Added(item, docs, photos)) { return; } } diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.h b/Telegram/SourceFiles/menu/menu_item_download_files.h index 91e270b716..896d66fe3a 100644 --- a/Telegram/SourceFiles/menu/menu_item_download_files.h +++ b/Telegram/SourceFiles/menu/menu_item_download_files.h @@ -35,7 +35,7 @@ void AddDownloadFilesAction( not_null<Ui::PopupMenu*> menu, not_null<Window::SessionController*> window, // From the legacy history inner widget. - const std::map<HistoryItem*, TextSelection, std::less<>> &items, + const base::flat_map<HistoryItem*, TextSelection, std::less<>> &items, not_null<HistoryInner*> list); } // namespace Menu diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 57920d2f12..fa35fdf1bd 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_message_reactions.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "main/main_session.h" #include "apiwrap.h" @@ -929,13 +930,16 @@ void SetupUnreadReactionsMenu( return; } const auto topic = thread->asTopic(); + const auto sublist = thread->asSublist(); const auto peer = thread->peer(); const auto rootId = topic ? topic->rootId() : 0; using Flag = MTPmessages_ReadReactions::Flag; peer->session().api().request(MTPmessages_ReadReactions( - MTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)), + MTP_flags((rootId ? Flag::f_top_msg_id : Flag(0)) + | (sublist ? Flag::f_saved_peer_id : Flag(0))), peer->input, - MTP_int(rootId) + MTP_int(rootId), + sublist ? sublist->sublistPeer()->input : MTPInputPeer() )).done([=](const MTPmessages_AffectedHistory &result) { const auto offset = peer->session().api().applyAffectedHistory( peer, @@ -944,7 +948,9 @@ void SetupUnreadReactionsMenu( resend(weakThread, done, resend); } else { done(); - peer->owner().history(peer)->clearUnreadReactionsFor(rootId); + peer->owner().history(peer)->clearUnreadReactionsFor( + rootId, + sublist); } }).fail(done).send(); }; diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 6d09b676ca..6f092df424 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/135.0.0.0 Safari/537.36"); + "Chrome/137.0.0.0 Safari/537.36"); return kResult; } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 9c2351b24e..61aff7c1f2 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -46,6 +46,7 @@ inputMediaDice#e66fbf7b emoticon:string = InputMedia; inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia; inputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector<InputMedia> payload:flags.0?string = InputMedia; +inputMediaTodo#9fc55fde todo:TodoList = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; @@ -99,11 +100,11 @@ userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#7482147e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector<Username> stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long = Chat; +channel#fe685355 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector<Username> stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#52d6806b flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; +channelFull#e07429de flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -116,8 +117,8 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message; -messageService#d3d28540 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; +message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message; +messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; @@ -136,6 +137,7 @@ messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int sto messageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector<long> countries_iso2:flags.1?Vector<string> prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia; messageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector<long> months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia; messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector<MessageExtendedMedia> = MessageMedia; +messageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector<TodoCompletion> = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction; @@ -186,8 +188,14 @@ messageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long trans messageActionStarGift#4717e8a4 flags:# name_hidden:flags.0?true saved:flags.2?true converted:flags.3?true upgraded:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true gift:StarGift message:flags.1?TextWithEntities convert_stars:flags.4?long upgrade_msg_id:flags.5?int upgrade_stars:flags.8?long from_id:flags.11?Peer peer:flags.12?Peer saved_id:flags.12?long = MessageAction; messageActionStarGiftUnique#2e3ae60e flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long resale_stars:flags.8?long can_transfer_at:flags.9?int can_resell_at:flags.10?int = MessageAction; messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction; -messageActionPaidMessagesPrice#bcd71419 stars:long = MessageAction; +messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction; messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction; +messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction; +messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction; +messageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction; +messageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction; +messageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction; +messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -332,7 +340,7 @@ updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update; -updateDraftMessage#1b49ec6d flags:# peer:Peer top_msg_id:flags.0?int draft:DraftMessage = Update; +updateDraftMessage#edfc111e flags:# peer:Peer top_msg_id:flags.0?int saved_peer_id:flags.1?Peer draft:DraftMessage = Update; updateReadFeaturedStickers#571d2742 = Update; updateRecentStickers#9a422c20 = Update; updateConfig#a229dd06 = Update; @@ -348,10 +356,10 @@ updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; updateLangPackTooLong#46560264 lang_code:string = Update; updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; -updateChannelReadMessagesContents#ea29055d flags:# channel_id:long top_msg_id:flags.0?int messages:Vector<int> = Update; +updateChannelReadMessagesContents#25f324f7 flags:# channel_id:long top_msg_id:flags.0?int saved_peer_id:flags.1?Peer messages:Vector<int> = Update; updateContactsReset#7084a7be = Update; updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; -updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; +updateDialogUnreadMark#b658f23e flags:# unread:flags.0?true peer:DialogPeer saved_peer_id:flags.1?Peer = Update; updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; updateFolderPeers#19360dc0 folder_peers:Vector<FolderPeer> pts:int pts_count:int = Update; @@ -385,7 +393,7 @@ updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJ updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector<BotCommand> = Update; updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector<long> = Update; updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; -updateMessageReactions#5e1b3cb8 flags:# peer:Peer msg_id:int top_msg_id:flags.0?int reactions:MessageReactions = Update; +updateMessageReactions#1e297bfa flags:# peer:Peer msg_id:int top_msg_id:flags.0?int saved_peer_id:flags.1?Peer reactions:MessageReactions = Update; updateAttachMenuBots#17b7a20b = Update; updateWebViewResultSent#1592b79d query_id:long = Update; updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update; @@ -425,7 +433,6 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector<int> qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; -updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; updateStarsBalance#4e80a379 balance:StarsAmount = Update; updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update; updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; @@ -433,6 +440,9 @@ updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Updat updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update; updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector<bytes> next_offset:int = Update; +updateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateMonoForumNoPaidException#9f812b08 flags:# exception:flags.0?true channel_id:long saved_peer_id:Peer = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -642,6 +652,7 @@ inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet; inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet; inputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet; +inputStickerSetTonGifts#1cf671a0 = InputStickerSet; stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true creator:flags.11?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector<PhotoSize> thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; @@ -820,7 +831,7 @@ contacts.topPeers#70b772a8 categories:Vector<TopPeerCategoryPeers> chats:Vector< contacts.topPeersDisabled#b52c939d = contacts.TopPeers; draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; -draftMessage#2d65321f flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long = DraftMessage; +draftMessage#96eaa5eb flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers; @@ -1200,7 +1211,7 @@ chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights; +chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true = ChatAdminRights; chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; @@ -1405,9 +1416,9 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; -sponsoredMessage#4d93a990 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; +sponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage; -messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages; +messages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages; messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages; searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod; @@ -1638,8 +1649,9 @@ stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count stories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = stories.StoryViews; -inputReplyToMessage#22c0f6d5 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector<MessageEntity> quote_offset:flags.4?int = InputReplyTo; +inputReplyToMessage#b07038b0 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector<MessageEntity> quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; +inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; @@ -1712,6 +1724,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; +monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; @@ -1816,23 +1829,11 @@ channels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options: channels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult; channels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult; -stats.broadcastRevenueStats#5407e297 top_hours_graph:StatsGraph revenue_graph:StatsGraph balances:BroadcastRevenueBalances usd_rate:double = stats.BroadcastRevenueStats; - -stats.broadcastRevenueWithdrawalUrl#ec659737 url:string = stats.BroadcastRevenueWithdrawalUrl; - -broadcastRevenueTransactionProceeds#557e2cc4 amount:long from_date:int to_date:int = BroadcastRevenueTransaction; -broadcastRevenueTransactionWithdrawal#5a590978 flags:# pending:flags.0?true failed:flags.2?true amount:long date:int provider:string transaction_date:flags.1?int transaction_url:flags.1?string = BroadcastRevenueTransaction; -broadcastRevenueTransactionRefund#42d30d2e amount:long date:int provider:string = BroadcastRevenueTransaction; - -stats.broadcastRevenueTransactions#87158466 count:int transactions:Vector<BroadcastRevenueTransaction> = stats.BroadcastRevenueTransactions; - reactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom; reactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom; reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings; -broadcastRevenueBalances#c3ff71e7 flags:# withdrawal_enabled:flags.0?true current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances; - availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect; messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects; @@ -1851,7 +1852,7 @@ starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#a39fd94a flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int = StarsTransaction; +starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus; @@ -1863,7 +1864,7 @@ geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:f starsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus; -payments.starsRevenueStats#c92bb73b revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats; +payments.starsRevenueStats#6c207376 flags:# top_hours_graph:flags.0?StatsGraph revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats; payments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl; @@ -1916,6 +1917,7 @@ payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector<Connected payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector<StarRefProgram> users:Vector<User> next_offset:flags.0?string = payments.SuggestedStarRefBots; starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount; +starsTonAmount#74aee3e0 amount:long = StarsAmount; messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers; messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector<Document> = messages.FoundStickers; @@ -1979,6 +1981,14 @@ stories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount pendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion; +todoItem#cba9a52f id:int title:TextWithEntities = TodoItem; + +todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList; + +todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; + +suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2130,8 +2140,8 @@ account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings; account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings; account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; -account.addNoPaidMessagesException#6f688aa7 flags:# refund_charged:flags.0?true user_id:InputUser = Bool; -account.getPaidMessagesRevenue#f1266f38 user_id:InputUser = account.PaidMessagesRevenue; +account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue; +account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -2175,9 +2185,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#fbf2340a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; -messages.sendMedia#a550cd78 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; -messages.forwardMessages#bb9fa475 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates; +messages.sendMessage#fe05dc9a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; +messages.sendMedia#ac55d9c1 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; +messages.forwardMessages#978928ca flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#fc78af9b peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult; @@ -2227,7 +2237,7 @@ messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_me messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs; -messages.saveDraft#d372c5ce flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long = Bool; +messages.saveDraft#54ae308e flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost = Bool; messages.getAllDrafts#6a3f8d65 = Updates; messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool; @@ -2259,8 +2269,8 @@ messages.sendMultiMedia#1bf89d74 flags:# silent:flags.5?true background:flags.6? messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector<MessageRange>; -messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; -messages.getDialogUnreadMarks#22e24e22 = Vector<DialogPeer>; +messages.markDialogUnread#8c5006f8 flags:# unread:flags.0?true parent_peer:flags.1?InputPeer peer:InputDialogPeer = Bool; +messages.getDialogUnreadMarks#21202222 flags:# parent_peer:flags.0?InputPeer = Vector<DialogPeer>; messages.clearAllDrafts#7e58ee9c = Bool; messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector<bytes> = Updates; @@ -2290,7 +2300,7 @@ messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messag messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; -messages.unpinAllMessages#ee22b9a8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.unpinAllMessages#62dd747 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory; messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages; messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed; @@ -2321,8 +2331,8 @@ messages.setChatAvailableReactions#864b2581 flags:# peer:InputPeer available_rea messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool; messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector<int> text:flags.1?Vector<TextWithEntities> to_lang:string = messages.TranslatedText; -messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.getUnreadReactions#bd7f90ac flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.readReactions#9ec44f93 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory; messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; @@ -2354,9 +2364,9 @@ messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; messages.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = WebViewResult; messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; -messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; -messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; +messages.getSavedDialogs#1e91fc99 flags:# exclude_pinned:flags.0?true parent_peer:flags.1?InputPeer offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; +messages.getSavedHistory#998ab009 flags:# parent_peer:flags.0?InputPeer peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.deleteSavedHistory#4dc5085f flags:# parent_peer:flags.0?InputPeer peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector<InputDialogPeer> = Bool; @@ -2386,11 +2396,16 @@ messages.getPaidReactionPrivacy#472455aa = Updates; messages.viewSponsoredMessage#269e3643 random_id:bytes = Bool; messages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool; messages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult; -messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages; +messages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages; messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector<InlineQueryPeerType> = messages.BotPreparedInlineMessage; messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool; +messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector<InputPeer> = messages.SavedDialogs; +messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; +messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates; +messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates; +messages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2479,7 +2494,7 @@ channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool; channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool; channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool; -channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates; +channels.toggleForum#3ff75734 channel:InputChannel enabled:Bool tabs:Bool = Updates; channels.createForumTopic#f40c0224 flags:# channel:InputChannel title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates; channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; channels.getForumTopicsByID#b0831eb9 channel:InputChannel topics:Vector<int> = messages.ForumTopics; @@ -2498,8 +2513,9 @@ channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; -channels.updatePaidMessagesPrice#fc84653f channel:InputChannel send_paid_messages_stars:long = Updates; +channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; +channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2548,14 +2564,14 @@ payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; payments.getStarsTopupOptions#c00ec7d3 = Vector<StarsTopupOption>; -payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; -payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; +payments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus; +payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; -payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer = payments.StarsRevenueStats; -payments.getStarsRevenueWithdrawalUrl#13bbe8b3 peer:InputPeer stars:long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl; +payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true ton:flags.1?true peer:InputPeer = payments.StarsRevenueStats; +payments.getStarsRevenueWithdrawalUrl#2433dc92 flags:# ton:flags.0?true peer:InputPeer amount:flags.1?long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl; payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl; -payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus; +payments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus; payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>; payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus; payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool; @@ -2648,9 +2664,6 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; -stats.getBroadcastRevenueStats#f788ee19 flags:# dark:flags.0?true peer:InputPeer = stats.BroadcastRevenueStats; -stats.getBroadcastRevenueWithdrawalUrl#9df4faad peer:InputPeer password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl; -stats.getBroadcastRevenueTransactions#70990b6d peer:InputPeer offset:int limit:int = stats.BroadcastRevenueTransactions; chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite; chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; @@ -2707,4 +2720,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 203 +// LAYER 206 diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index 489e2666e5..ff0273d1d6 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -181,7 +181,7 @@ private: }; -struct Info : public RuntimeComponent<Info, LayoutItemBase> { +struct Info : RuntimeComponent<Info, LayoutItemBase> { int top = 0; }; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index a17b2dcfd4..20bebbef6e 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -631,7 +631,7 @@ void Form::processReceipt(const MTPDpayments_paymentReceiptStars &data) { ImageLocation()) : nullptr, .peerId = peerFromUser(data.vbot_id().v), - .credits = StarsAmount(data.vtotal_amount().v), + .credits = CreditsAmount(data.vtotal_amount().v), .date = data.vdate().v, }; _updates.fire(CreditsReceiptReady{ .data = receiptData }); diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 2e199e3598..598f9a23d8 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -211,7 +211,7 @@ struct CreditsReceiptData { QString description; PhotoData *photo = nullptr; PeerId peerId = PeerId(0); - StarsAmount credits; + CreditsAmount credits; TimeId date = 0; }; diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index b51fa0bb02..4af87b93d9 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -251,6 +251,7 @@ void ShowPaidReactionDetails( .chosen = chosen, .max = max, .top = std::move(top), + .session = &channel->session(), .channel = channel->name(), .submit = std::move(submitText), .balanceValue = session->credits().balanceValue(), diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 1786d250ce..67ad2c7cc0 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -33,7 +33,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { [[nodiscard]] not_null<Ui::RpWidget*> AddBalanceWidget( not_null<Ui::RpWidget*> parent, - rpl::producer<StarsAmount> balanceValue, + not_null<Main::Session*> session, + rpl::producer<CreditsAmount> balanceValue, bool rightAlign, rpl::producer<float64> opacityValue = nullptr); } // namespace Settings @@ -572,6 +573,7 @@ void PaidReactionsBox( { const auto balance = Settings::AddBalanceWidget( content, + args.session, std::move(args.balanceValue), false); rpl::combine( diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 1468d53baf..bf52952ac3 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -13,6 +13,10 @@ namespace style { struct RoundCheckbox; } // namespace style +namespace Main { +class Session; +} // namespace Main + namespace Ui { class BoxContent; @@ -39,9 +43,10 @@ struct PaidReactionBoxArgs { std::vector<PaidReactionTop> top; + not_null<Main::Session*> session; QString channel; Fn<rpl::producer<TextWithContext>(rpl::producer<int> amount)> submit; - rpl::producer<StarsAmount> balanceValue; + rpl::producer<CreditsAmount> balanceValue; Fn<void(int, uint64)> send; }; diff --git a/Telegram/SourceFiles/platform/linux/integration_linux.cpp b/Telegram/SourceFiles/platform/linux/integration_linux.cpp index e3e331cd7b..f36bfb6326 100644 --- a/Telegram/SourceFiles/platform/linux/integration_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/integration_linux.cpp @@ -22,6 +22,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include <gio/gio.hpp> #include <xdpinhibit/xdpinhibit.hpp> +#ifdef __GLIBC__ +#include <malloc.h> +#endif // __GLIBC__ + namespace Platform { namespace { @@ -166,6 +170,14 @@ LinuxIntegration::LinuxIntegration() g_warning("Qt is running without GLib event loop integration, " "expect various functionality to not to work."); } + +#ifdef __GLIBC__ + mallopt(M_ARENA_MAX, 1); + QObject::connect( + QCoreApplication::eventDispatcher(), + &QAbstractEventDispatcher::aboutToBlock, + [] { malloc_trim(0); }); +#endif // __GLIBC__ } void LinuxIntegration::init() { diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index 6f94340789..0abf779029 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -431,7 +431,7 @@ void MainWindow::createGlobalMenu() { u"AyuGram"_q), [=] { ensureWindowShown(); - controller().show(Box<AboutBox>(sessionController())); + controller().show(Box(AboutBox)); }); about->setMenuRole(QAction::AboutQtRole); diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index d49fecb814..f73fceab05 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/sandbox.h" #include "core/core_settings.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" +#include "data/data_peer.h" #include "history/history.h" #include "history/history_item.h" #include "main/main_session.h" @@ -156,6 +158,7 @@ public: void clearAll(); void clearFromItem(not_null<HistoryItem*> item); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void clearNotification(NotificationId id); @@ -366,7 +369,10 @@ Manager::Private::Private(not_null<Manager*> manager) .contextId = ContextId{ .sessionId = dict.lookup_value("session").get_uint64(), .peerId = PeerId(dict.lookup_value("peer").get_uint64()), - .topicRootId = dict.lookup_value("topic").get_int64(), + .topicRootId = MsgId( + dict.lookup_value("topic").get_int64()), + .monoforumPeerId = PeerId(dict.lookup_value( + "monoforumpeer").get_uint64()), }, .msgId = dict.lookup_value("msgid").get_int64(), }; @@ -531,6 +537,7 @@ void Manager::Private::showNotification( .sessionId = peer->session().uniqueId(), .peerId = peer->id, .topicRootId = info.topicRootId, + .monoforumPeerId = info.monoforumPeerId, }; const auto notificationId = NotificationId{ .contextId = key, @@ -591,6 +598,10 @@ void Manager::Private::showNotification( GLib::Variant::new_string("topic"), GLib::Variant::new_variant( GLib::Variant::new_int64(info.topicRootId.bare))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("monoforumpeer"), + GLib::Variant::new_variant( + GLib::Variant::new_uint64(info.monoforumPeerId.value))), GLib::Variant::new_dict_entry( GLib::Variant::new_string("msgid"), GLib::Variant::new_variant( @@ -809,6 +820,7 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) { .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), + .monoforumPeerId = item->sublistPeerId(), }); if (i != _notifications.cend() && i->second.remove(item->id) @@ -825,6 +837,15 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) { }); } +void Manager::Private::clearFromSublist( + not_null<Data::SavedSublist*> sublist) { + _notifications.remove(ContextId{ + .sessionId = sublist->session().uniqueId(), + .peerId = sublist->owningHistory()->peer->id, + .monoforumPeerId = sublist->sublistPeer()->id, + }); +} + void Manager::Private::clearFromHistory(not_null<History*> history) { const auto sessionId = history->session().uniqueId(); const auto peerId = history->peer->id; @@ -889,6 +910,10 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { _private->clearFromTopic(topic); } +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + _private->clearFromSublist(sublist); +} + void Manager::doClearFromHistory(not_null<History*> history) { _private->clearFromHistory(history); } diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h index 8ab17f55b7..d2e740750f 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h @@ -24,6 +24,7 @@ protected: void doClearAllFast() override; void doClearFromItem(not_null<HistoryItem*> item) override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; bool doSkipToast() const override; diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 76bea01808..aff3834b82 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -769,7 +769,7 @@ bool OpenSystemSettings(SystemSettingsType type) { } void NewVersionLaunched(int oldVersion) { - if (oldVersion <= 4001001 && cAutoStart()) { + if (oldVersion <= 5014003 && cAutoStart()) { AutostartToggle(true); } } diff --git a/Telegram/SourceFiles/platform/mac/main_window_mac.mm b/Telegram/SourceFiles/platform/mac/main_window_mac.mm index 78355f5f34..0d11bf889d 100644 --- a/Telegram/SourceFiles/platform/mac/main_window_mac.mm +++ b/Telegram/SourceFiles/platform/mac/main_window_mac.mm @@ -369,7 +369,7 @@ void MainWindow::createGlobalMenu() { { auto callback = [=] { ensureWindowShown(); - controller().show(Box<AboutBox>(sessionController())); + controller().show(Box(AboutBox)); }; main->addAction( tr::lng_mac_menu_about_telegram( @@ -529,6 +529,18 @@ void MainWindow::createGlobalMenu() { )->setShortcutContext(Qt::WidgetShortcut); QMenu *window = psMainMenu.addMenu(tr::lng_mac_menu_window(tr::now)); + + window->addAction( + tr::lng_mac_menu_fullscreen(tr::now), + this, + [=] { + NSWindow *nsWindow = [reinterpret_cast<NSView*>(winId()) window]; + [nsWindow toggleFullScreen:nsWindow]; + }, + QKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_F) + )->setShortcutContext(Qt::WidgetShortcut); + window->addSeparator(); + psContacts = window->addAction(tr::lng_mac_menu_contacts(tr::now)); connect(psContacts, &QAction::triggered, psContacts, crl::guard(this, [=] { Expects(sessionController() != nullptr && !controller().locked()); diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h index 2ffc5d6cb6..6af0292384 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h @@ -25,6 +25,7 @@ protected: void doClearAllFast() override; void doClearFromItem(not_null<HistoryItem*> item) override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; QString accountNameSeparator() override; diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm index 3be39ef841..b92cd90f55 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm @@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/mac/base_utilities_mac.h" #include "base/random.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" +#include "data/data_peer.h" #include "history/history.h" #include "history/history_item.h" #include "ui/empty_userpic.h" @@ -131,6 +133,12 @@ using Manager = Platform::Notifications::Manager; return; } const auto notificationTopicRootId = [topicObject longLongValue]; + NSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@"monoforumpeer"]; + if (!monoforumPeerObject) { + LOG(("App Error: A notification with unknown monoforum peer was received")); + return; + } + const auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue]; NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"]; const auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL; @@ -140,6 +148,7 @@ using Manager = Platform::Notifications::Manager; .sessionId = notificationSessionId, .peerId = PeerId(notificationPeerId), .topicRootId = MsgId(notificationTopicRootId), + .monoforumPeerId = PeerId(notificationMonoforumPeerId), }, .msgId = notificationMsgId, }; @@ -210,6 +219,7 @@ public: void clearAll(); void clearFromItem(not_null<HistoryItem*> item); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void updateDelegate(); @@ -237,6 +247,9 @@ private: struct ClearFromTopic { ContextId contextId; }; + struct ClearFromSublist { + ContextId contextId; + }; struct ClearFromHistory { ContextId partialContextId; }; @@ -250,6 +263,7 @@ private: using ClearTask = std::variant< ClearFromItem, ClearFromTopic, + ClearFromSublist, ClearFromHistory, ClearFromSession, ClearAll, @@ -311,6 +325,8 @@ void Manager::Private::showNotification( @"peer", [NSNumber numberWithLongLong:info.topicRootId.bare], @"topic", + [NSNumber numberWithUnsignedLongLong:info.monoforumPeerId.value], + @"monoforumpeer", [NSNumber numberWithLongLong:info.itemId.bare], @"msgid", [NSNumber numberWithUnsignedLongLong:_managerId], @@ -351,6 +367,7 @@ void Manager::Private::clearingThreadLoop() { auto clearAll = false; auto clearFromItems = base::flat_set<NotificationId>(); auto clearFromTopics = base::flat_set<ContextId>(); + auto clearFromSublists = base::flat_set<ContextId>(); auto clearFromHistories = base::flat_set<ContextId>(); auto clearFromSessions = base::flat_set<uint64>(); { @@ -368,6 +385,8 @@ void Manager::Private::clearingThreadLoop() { clearFromItems.emplace(value.id); }, [&](const ClearFromTopic &value) { clearFromTopics.emplace(value.contextId); + }, [&](const ClearFromSublist &value) { + clearFromSublists.emplace(value.contextId); }, [&](const ClearFromHistory &value) { clearFromHistories.emplace(value.partialContextId); }, [&](const ClearFromSession &value) { @@ -395,21 +414,35 @@ void Manager::Private::clearingThreadLoop() { return true; } const auto notificationTopicRootId = [topicObject longLongValue]; + NSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@"monoforumpeer"]; + if (!monoforumPeerObject) { + return true; + } + const auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue]; NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"]; const auto msgId = msgObject ? [msgObject longLongValue] : 0LL; const auto partialContextId = ContextId{ .sessionId = notificationSessionId, .peerId = PeerId(notificationPeerId), }; - const auto contextId = ContextId{ + const auto contextId = notificationTopicRootId + ? ContextId{ .sessionId = notificationSessionId, .peerId = PeerId(notificationPeerId), .topicRootId = MsgId(notificationTopicRootId), - }; + } + : notificationMonoforumPeerId + ? ContextId{ + .sessionId = notificationSessionId, + .peerId = PeerId(notificationPeerId), + .monoforumPeerId = PeerId(notificationMonoforumPeerId), + } + : partialContextId; const auto id = NotificationId{ contextId, MsgId(msgId) }; return clearFromSessions.contains(notificationSessionId) || clearFromHistories.contains(partialContextId) || clearFromTopics.contains(contextId) + || clearFromSublists.contains(contextId) || (msgId && clearFromItems.contains(id)); }; @@ -450,6 +483,7 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) { .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), + .monoforumPeerId = item->sublistPeerId(), }, item->id }); } @@ -461,6 +495,15 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) { } }); } +void Manager::Private::clearFromSublist( + not_null<Data::SavedSublist*> sublist) { + putClearTask(ClearFromSublist{ ContextId{ + .sessionId = sublist->session().uniqueId(), + .peerId = sublist->owningHistory()->peer->id, + .monoforumPeerId = sublist->sublistPeer()->id, + } }); +} + void Manager::Private::clearFromHistory(not_null<History*> history) { putClearTask(ClearFromHistory{ ContextId{ .sessionId = history->session().uniqueId(), @@ -511,6 +554,10 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { _private->clearFromTopic(topic); } +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + _private->clearFromSublist(sublist); +} + void Manager::doClearFromHistory(not_null<History*> history) { _private->clearFromHistory(history); } diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index 9a4f028af4..d132ed4f32 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/win/windows_dlls.h" #include "platform/win/specific_win.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" +#include "data/data_peer.h" #include "history/history.h" #include "history/history_item.h" #include "core/application.h" @@ -433,6 +435,7 @@ public: void clearAll(); void clearFromItem(not_null<HistoryItem*> item); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void beforeNotificationActivated(NotificationId id); @@ -508,6 +511,7 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) { .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), + .monoforumPeerId = item->sublistPeerId(), }); if (i == _notifications.cend()) { return; @@ -544,6 +548,27 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) { } } +void Manager::Private::clearFromSublist( + not_null<Data::SavedSublist*> sublist) { + if (!_notifier) { + return; + } + + const auto i = _notifications.find(ContextId{ + .sessionId = sublist->session().uniqueId(), + .peerId = sublist->owningHistory()->peer->id, + .monoforumPeerId = sublist->sublistPeer()->id, + }); + if (i != _notifications.cend()) { + const auto temp = base::take(i->second); + _notifications.erase(i); + + for (const auto &[msgId, notification] : temp) { + tryHide(notification); + } + } +} + void Manager::Private::clearFromHistory(not_null<History*> history) { if (!_notifier) { return; @@ -626,7 +651,9 @@ void Manager::Private::handleActivation(const ToastActivation &activation) { .contextId = ContextId{ .sessionId = parsed.value("session").toULongLong(), .peerId = PeerId(parsed.value("peer").toULongLong()), - .topicRootId = MsgId(parsed.value("topic").toLongLong()) + .topicRootId = MsgId(parsed.value("topic").toLongLong()), + .monoforumPeerId = PeerId( + parsed.value("monoforumpeer").toULongLong()), }, .msgId = MsgId(parsed.value("msg").toLongLong()), }; @@ -694,16 +721,18 @@ bool Manager::Private::showNotificationInTryCatch( .sessionId = peer->session().uniqueId(), .peerId = peer->id, .topicRootId = info.topicRootId, + .monoforumPeerId = info.monoforumPeerId, }; const auto notificationId = NotificationId{ .contextId = key, .msgId = info.itemId, }; - const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&msg=%5"_q + const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&monoforumpeer=%5&msg=%6"_q .arg(GetCurrentProcessId()) .arg(key.sessionId) .arg(key.peerId.value) .arg(info.topicRootId.bare) + .arg(info.monoforumPeerId.value) .arg(info.itemId.bare); const auto modern = Platform::IsWindows10OrGreater(); @@ -897,6 +926,10 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { _private->clearFromTopic(topic); } +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + _private->clearFromSublist(sublist); +} + void Manager::doClearFromHistory(not_null<History*> history) { _private->clearFromHistory(history); } diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.h b/Telegram/SourceFiles/platform/win/notifications_manager_win.h index 5e99c760a2..7f9a6ce8ef 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.h +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.h @@ -31,6 +31,7 @@ protected: void doClearAllFast() override; void doClearFromItem(not_null<HistoryItem*> item) override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; void onBeforeNotificationActivated(NotificationId id) override; diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index e2e23fe305..ce93f3d09a 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" +#include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" @@ -373,7 +374,7 @@ ShortcutMessages::ShortcutMessages( this, &controller->session(), static_cast<ListDelegate*>(this)); - _inner->overrideIsChatWide(false); + _inner->overrideChatMode(ElementChatMode::Default); _scroll->sizeValue() | rpl::filter([](QSize size) { return !size.isEmpty(); @@ -399,6 +400,8 @@ ShortcutMessages::ShortcutMessages( _composeControls->editMessage( fullId, _inner->getSelectedTextRange(item)); + } else if (media->todolist()) { + Window::PeerMenuEditTodoList(_controller, item); } } }, _inner->lifetime()); @@ -629,6 +632,7 @@ void ShortcutMessages::setupComposeControls() { .key = Dialogs::Key{ _history }, .section = Dialogs::EntryState::Section::ShortcutMessages, .currentReplyTo = replyTo(), + .currentSuggest = SuggestPostOptions(), }; _composeControls->setCurrentDialogsEntryState(state); diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h index 586e58a9dd..a87c1aaed5 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h @@ -34,6 +34,7 @@ struct StepData { QString email; int unconfirmedEmailLengthCode; bool setOnlyRecoveryEmail = false; + bool suggestionValidate = false; struct ProcessRecover { bool setNewPassword = false; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp index 0064dcbda7..7719fc2ddd 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp @@ -12,20 +12,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/unixtime.h" #include "core/core_cloud_password.h" +#include "data/components/promo_suggestions.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" +#include "main/main_session.h" #include "settings/cloud_password/settings_cloud_password_common.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" #include "settings/cloud_password/settings_cloud_password_hint.h" #include "settings/cloud_password/settings_cloud_password_manage.h" #include "settings/cloud_password/settings_cloud_password_step.h" +#include "settings/cloud_password/settings_cloud_password_validate_icon.h" +#include "ui/boxes/boost_box.h" // Ui::StartFireworks. #include "ui/boxes/confirm_box.h" +#include "ui/rect.h" #include "ui/text/format_values.h" +#include "ui/ui_utility.h" +#include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/password_input.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" -#include "ui/vertical_list.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" @@ -53,6 +59,10 @@ RecreateResetPassword: – Continue to RecreateResetHint. – Clear password and Back to Settings. – Back to Settings. + +ValidatePassword: +- Submit to show good validate. +- Back to Main Settings. */ namespace Settings { @@ -72,9 +82,7 @@ Icon CreateInteractiveLottieIcon( const auto raw = object.data(); const auto width = descriptor.sizeOverride.width(); - raw->resize(QRect( - QPoint(), - descriptor.sizeOverride).marginsAdded(padding).size()); + raw->resize((Rect(descriptor.sizeOverride) + padding).size()); auto owned = Lottie::MakeIcon(std::move(descriptor)); const auto icon = owned.get(); @@ -118,7 +126,10 @@ public: using TypedAbstractStep::TypedAbstractStep; [[nodiscard]] rpl::producer<QString> title() override; + [[nodiscard]] QPointer<Ui::RpWidget> createPinnedToTop( + not_null<QWidget*> parent) override; void setupContent(); + void setupValidateGood(); protected: [[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override; @@ -130,6 +141,8 @@ private: not_null<Ui::FlatLabel*> info, Fn<void()> recoverCallback); + QWidget *_parent = nullptr; + rpl::variable<std::vector<Type>> _removesFromStack; rpl::lifetime _requestLifetime; @@ -143,12 +156,58 @@ rpl::producer<QString> Input::title() { return tr::lng_settings_cloud_password_password_title(); } +QPointer<Ui::RpWidget> Input::createPinnedToTop( + not_null<QWidget*> parent) { + _parent = parent; + return nullptr; +} + +void Input::setupValidateGood() { + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + + if (_parent) { + Ui::StartFireworks(_parent); + } + + if (auto owned = CreateValidateGoodIcon(&controller()->session())) { + owned->setParent(content); + content->add( + object_ptr<Ui::CenterWrap<>>( + content, + std::move(owned)), + QMargins(0, st::lineWidth * 75, 0, 0)); + } + + SetupHeader( + content, + QString(), + rpl::never<>(), + tr::lng_settings_suggestion_password_step_finish_title(), + tr::lng_settings_suggestion_password_step_finish_about()); + + const auto button = AddDoneButton(content, tr::lng_share_done()); + button->setClickedCallback([=] { + showBack(); + }); + + Ui::ToggleChildrenVisibility(this, true); + Ui::ResizeFitChild(this, content); + content->resizeToWidth(width()); + Ui::SendPendingMoveResizeEvents(content); +} + void Input::setupContent() { + if (QWidget::children().count() > 0) { + return; + } + const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); auto currentStepData = stepData(); const auto currentStepDataPassword = base::take(currentStepData.password); const auto currentStepProcessRecover = base::take( currentStepData.processRecover); + const auto currentStepValidate = base::take( + currentStepData.suggestionValidate); setStepData(currentStepData); const auto currentState = cloudPassword().stateCurrent(); @@ -167,11 +226,10 @@ void Input::setupContent() { const auto icon = CreateInteractiveLottieIcon( content, { - .name = u"cloud_password/password_input"_q, - .sizeOverride = { - st::settingsCloudPasswordIconSize, - st::settingsCloudPasswordIconSize - }, + .name = currentStepValidate + ? u"cloud_password/validate"_q + : u"cloud_password/password_input"_q, + .sizeOverride = Size(st::settingsCloudPasswordIconSize), }, st::settingLocalPasscodeIconPadding); @@ -179,12 +237,16 @@ void Input::setupContent() { content, QString(), rpl::never<>(), - isCheck + currentStepValidate + ? tr::lng_settings_suggestion_password_step_input_title() + : isCheck ? tr::lng_settings_cloud_password_check_subtitle() : hasPassword ? tr::lng_settings_cloud_password_manage_password_change() : tr::lng_settings_cloud_password_password_subtitle(), - isCheck + currentStepValidate + ? tr::lng_settings_suggestion_password_step_input_about() + : isCheck ? tr::lng_settings_cloud_password_manage_about1() : tr::lng_cloud_password_about()); @@ -340,7 +402,9 @@ void Input::setupContent() { Ui::AddSkip(content); } - if (!newInput->text().isEmpty()) { + if (currentStepValidate) { + icon.icon->animate(icon.update, 0, icon.icon->framesCount() - 1); + } else if (!newInput->text().isEmpty()) { icon.icon->jumpTo(icon.icon->framesCount() / 2, icon.update); } @@ -376,10 +440,18 @@ void Input::setupContent() { } } - auto data = stepData(); - data.currentPassword = pass; - setStepData(std::move(data)); - showOther(CloudPasswordManageId()); + if (currentStepValidate) { + controller()->session().promoSuggestions().dismiss( + Data::PromoSuggestions::SugValidatePassword()); + setupValidateGood(); + delete content; + } else { + auto data = stepData(); + data.currentPassword = pass; + setStepData(std::move(data)); + showOther(CloudPasswordManageId()); + } + }); }; @@ -412,17 +484,19 @@ void Input::setupContent() { } }); - base::qt_signal_producer( - newInput.get(), - &QLineEdit::textChanged // Covers Undo. - ) | rpl::map([=] { - return newInput->text().isEmpty(); - }) | rpl::distinct_until_changed( - ) | rpl::start_with_next([=](bool empty) { - const auto from = icon.icon->frameIndex(); - const auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1); - icon.icon->animate(icon.update, from, to); - }, content->lifetime()); + if (!currentStepValidate) { + base::qt_signal_producer( + newInput.get(), + &QLineEdit::textChanged // Covers Undo. + ) | rpl::map([=] { + return newInput->text().isEmpty(); + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool empty) { + const auto from = icon.icon->frameIndex(); + const auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1); + icon.icon->animate(icon.update, from, to); + }, content->lifetime()); + } const auto submit = [=] { if (!reenterInput || reenterInput->hasFocus()) { @@ -437,7 +511,7 @@ void Input::setupContent() { QObject::connect(reenterInput, &MaskedInputField::submitted, submit); } - setFocusCallback([=] { + setFocusCallback(crl::guard(content, [=] { if (isCheck || newInput->text().isEmpty()) { newInput->setFocus(); } else if (reenterInput->text().isEmpty()) { @@ -445,7 +519,7 @@ void Input::setupContent() { } else { newInput->setFocus(); } - }); + })); Ui::ResizeFitChild(this, content); } @@ -586,10 +660,33 @@ void Input::setupRecoverButton( }); } +class SuggestionInput : public Input { +public: + SuggestionInput( + QWidget *parent, + not_null<Window::SessionController*> controller) + : Input(parent, controller) + , _stepData(StepData{ .suggestionValidate = true }) { + setStepDataReference(_stepData); + } + + [[nodiscard]] static Type Id() { + return SectionFactory<SuggestionInput>::Instance(); + } + +private: + std::any _stepData; + +}; + } // namespace CloudPassword Type CloudPasswordInputId() { return CloudPassword::Input::Id(); } +Type CloudPasswordSuggestionInputId() { + return CloudPassword::SuggestionInput::Id(); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h index 1103047df4..0c1ae4f89c 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { Type CloudPasswordInputId(); +Type CloudPasswordSuggestionInputId(); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp new file mode 100644 index 0000000000..7ea244e8ff --- /dev/null +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp @@ -0,0 +1,74 @@ +/* +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 "settings/cloud_password/settings_cloud_password_validate_icon.h" + +#include "apiwrap.h" +#include "base/object_ptr.h" +#include "chat_helpers/stickers_emoji_pack.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" +#include "main/main_session.h" +#include "ui/rect.h" +#include "ui/rp_widget.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +[[nodiscard]] DocumentData *EmojiValidateGood( + not_null<Main::Session*> session) { + auto emoji = TextWithEntities{ + .text = (QString(QChar(0xD83D)) + QChar(0xDC4D)), + }; + if (const auto e = Ui::Emoji::Find(emoji.text)) { + const auto sticker = session->emojiStickersPack().stickerForEmoji(e); + return sticker.document; + } + return nullptr; +} + +} // namespace + +object_ptr<Ui::RpWidget> CreateValidateGoodIcon( + not_null<Main::Session*> session) { + const auto document = EmojiValidateGood(session); + if (!document) { + return nullptr; + } + + auto owned = object_ptr<Ui::RpWidget>((QWidget*)nullptr); + const auto widget = owned.data(); + + struct State { + std::unique_ptr<Ui::Text::CustomEmoji> emoji; + }; + const auto state = widget->lifetime().make_state<State>(); + const auto size = st::settingsCloudPasswordIconSize; + state->emoji = std::make_unique<Ui::Text::LimitedLoopsEmoji>( + session->data().customEmojiManager().create( + document, + [=] { widget->update(); }, + Data::CustomEmojiManager::SizeTag::Normal, + size), + 1, + true); + widget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(widget); + state->emoji->paint(p, Ui::Text::CustomEmojiPaintContext{ + .textColor = st::windowFg->c, + .now = crl::now(), + }); + }, widget->lifetime()); + const auto padding = st::settingLocalPasscodeIconPadding; + widget->resize((Rect(Size(size)) + padding).size()); + + return owned; +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h new file mode 100644 index 0000000000..54280d7360 --- /dev/null +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h @@ -0,0 +1,27 @@ +/* +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 + +template <typename Object> +class object_ptr; + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Settings { + +[[nodiscard]] object_ptr<Ui::RpWidget> CreateValidateGoodIcon( + not_null<Main::Session*> session); + +} // namespace Settings + diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index f28f04e777..b153dbe371 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -103,6 +103,7 @@ settingsPremiumIconLastSeen: icon {{ "settings/premium/lastseen", settingsIconFg settingsPremiumIconPrivacy: icon {{ "settings/premium/privacy", settingsIconFg }}; settingsPremiumIconBusiness: icon {{ "settings/premium/market", settingsIconFg }}; settingsPremiumIconEffects: icon {{ "settings/premium/effects", settingsIconFg }}; +settingsPremiumIconChecklist: icon {{ "settings/premium/checklist", settingsIconFg }}; settingsStoriesIconOrder: icon {{ "settings/premium/stories_order", premiumButtonBg1 }}; settingsStoriesIconStealth: icon {{ "menu/stealth", premiumButtonBg1 }}; @@ -696,3 +697,11 @@ settingsGiftIconEmoji: IconEmoji { icon: icon{{ "settings/mini_gift", windowFg }}; padding: margins(1px, 2px, 1px, 0px); } + +settingsCreditsButtonBuyIcon: icon {{ "settings/add", windowBgActive, point(7px, 0px) }}; + +settingsCreditsButton: SettingsButton(settingsButton) { + padding: margins(62px, 8px, 22px, 8px); +} +settingsButtonIconGift: icon {{ "settings/gift", menuIconColor }}; +settingsButtonIconEarn: icon {{ "settings/earn", menuIconColor }}; diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index 465a70735c..d7f6139220 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -123,7 +123,11 @@ not_null<Button*> AddButtonWithIcon( const style::SettingsButton &st, IconDescriptor &&descriptor) { return container->add( - CreateButtonWithIcon(container, std::move(text), st, std::move(descriptor))); + CreateButtonWithIcon( + container, + std::move(text), + st, + std::move(descriptor))); } void CreateRightLabel( @@ -242,7 +246,8 @@ void AddDividerTextWithLottie( LottieIcon CreateLottieIcon( not_null<QWidget*> parent, Lottie::IconDescriptor &&descriptor, - style::margins padding) { + style::margins padding, + Fn<QColor()> colorOverride) { Expects(!descriptor.frame); // I'm not sure it considers limitFps. descriptor.limitFps = true; @@ -262,7 +267,9 @@ LottieIcon CreateLottieIcon( const auto looped = raw->lifetime().make_state<bool>(true); const auto start = [=] { - icon->animate([=] { raw->update(); }, 0, icon->framesCount() - 1); + icon->animate([=] { + raw->update(); + }, 0, icon->framesCount() - 1); }; const auto animate = [=](anim::repeat repeat) { *looped = (repeat == anim::repeat::loop); @@ -272,7 +279,9 @@ LottieIcon CreateLottieIcon( ) | rpl::start_with_next([=] { auto p = QPainter(raw); const auto left = (raw->width() - width) / 2; - icon->paint(p, left, padding.top()); + icon->paint(p, left, padding.top(), colorOverride + ? colorOverride() + : std::optional<QColor>()); if (!icon->animating() && icon->frameIndex() > 0 && *looped) { start(); } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 2c2c31b64b..6a9e214951 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -204,7 +204,8 @@ struct LottieIcon { [[nodiscard]] LottieIcon CreateLottieIcon( not_null<QWidget*> parent, Lottie::IconDescriptor &&descriptor, - style::margins padding = {}); + style::margins padding = {}, + Fn<QColor()> colorOverride = nullptr); struct SliderWithLabel { object_ptr<Ui::RpWidget> widget; diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 0e132d82f3..1ed8c28c51 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -12,15 +12,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/star_gift_box.h" #include "boxes/gift_credits_box.h" #include "boxes/gift_premium_box.h" +#include "chat_helpers/stickers_gift_box_pack.h" #include "core/click_handler_types.h" #include "data/components/credits.h" #include "data/data_file_origin.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" +#include "info/bot/earn/info_bot_earn_widget.h" #include "info/bot/starref/info_bot_starref_common.h" #include "info/bot/starref/info_bot_starref_join_widget.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. +#include "info/channel_statistics/earn/earn_format.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "info/info_memento.h" @@ -34,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_top_bar.h" #include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/text_utilities.h" @@ -44,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" +#include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_giveaway.h" @@ -52,6 +58,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_premium.h" #include "styles/style_settings.h" #include "styles/style_statistics.h" +#include "styles/style_menu_icons.h" +#include "styles/style_channel_earn.h" namespace Settings { namespace { @@ -60,7 +68,8 @@ class Credits : public Section<Credits> { public: Credits( QWidget *parent, - not_null<Window::SessionController*> controller); + not_null<Window::SessionController*> controller, + CreditsType type); [[nodiscard]] rpl::producer<QString> title() override; @@ -79,8 +88,8 @@ private: void setupContent(); void setupHistory(not_null<Ui::VerticalLayout*> container); void setupSubscriptions(not_null<Ui::VerticalLayout*> container); - void setupStarRefPromo(not_null<Ui::VerticalLayout*> container); const not_null<Window::SessionController*> _controller; + const CreditsType _creditsType; QWidget *_parent = nullptr; @@ -101,11 +110,18 @@ private: Credits::Credits( QWidget *parent, - not_null<Window::SessionController*> controller) + not_null<Window::SessionController*> controller, + CreditsType type) : Section(parent) , _controller(controller) +, _creditsType(type) , _star(Ui::GenerateStars(st::creditsTopupButton.height, 1)) -, _balanceStar(Ui::GenerateStars(st::creditsBalanceStarHeight, 1)) { +, _balanceStar((_creditsType == CreditsType::Ton) + ? Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::currencyFg->c) + : Ui::GenerateStars(st::creditsBalanceStarHeight, 1)) { + _controller->session().giftBoxStickersPacks().tonLoad(); setupContent(); _controller->session().premiumPossibleValue( @@ -117,6 +133,9 @@ Credits::Credits( } rpl::producer<QString> Credits::title() { + if (_creditsType == CreditsType::Ton) { + return tr::lng_credits_currency_summary_title(); + } return tr::lng_premium_summary_title(); } @@ -211,25 +230,6 @@ void Credits::setupSubscriptions(not_null<Ui::VerticalLayout*> container) { } } -void Credits::setupStarRefPromo(not_null<Ui::VerticalLayout*> container) { - const auto self = _controller->session().user(); - if (!Info::BotStarRef::Join::Allowed(self)) { - return; - } - Ui::AddSkip(container); - const auto button = Info::BotStarRef::AddViewListButton( - container, - tr::lng_credits_summary_earn_title(), - tr::lng_credits_summary_earn_about(), - true); - button->setClickedCallback([=] { - _controller->showSection(Info::BotStarRef::Join::Make(self)); - }); - Ui::AddSkip(container); - Ui::AddDivider(container); - Ui::AddSkip(container); -} - void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) { const auto history = container->add( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( @@ -238,7 +238,7 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) { const auto content = history->entity(); const auto self = _controller->session().user(); - Ui::AddSkip(content); + Ui::AddSkip(content, st::lineWidth * 6); const auto fill = [=]( not_null<PeerData*> premiumBot, @@ -270,9 +270,26 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) { inner, object_ptr<Ui::CustomWidthSlider>( inner, - st::defaultTabsSlider)), - st::boxRowPadding); + st::creditsHistoryTabsSlider)), + st::creditsHistoryTabsSliderPadding); slider->toggle(!hasOneTab, anim::type::instant); + if (!hasOneTab) { + const auto shadow = Ui::CreateChild<Ui::RpWidget>(inner); + shadow->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(shadow); + p.fillRect(shadow->rect(), st::shadowFg); + }, shadow->lifetime()); + slider->geometryValue( + ) | rpl::start_with_next([=](const QRect &r) { + shadow->setGeometry( + inner->x(), + rect::bottom(slider) - st::lineWidth, + inner->width(), + st::lineWidth); + shadow->show(); + shadow->raise(); + }, shadow->lifetime()); + } slider->entity()->addSection(fullTabText); if (hasIn) { @@ -283,12 +300,12 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) { } { - const auto &st = st::defaultTabsSlider; + const auto &st = st::creditsHistoryTabsSlider; slider->entity()->setNaturalWidth(0 + st.labelStyle.font->width(fullTabText) + (hasIn ? st.labelStyle.font->width(inTabText) : 0) + (hasOut ? st.labelStyle.font->width(outTabText) : 0) - + rect::m::sum::h(st::boxRowPadding)); + + rect::m::sum::h(st::creditsHistoryTabsSliderPadding)); } const auto fullWrap = inner->add( @@ -367,9 +384,10 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) { const auto apiLifetime = content->lifetime().make_state<rpl::lifetime>(); { using Api = Api::CreditsHistory; - const auto apiFull = apiLifetime->make_state<Api>(self, true, true); - const auto apiIn = apiLifetime->make_state<Api>(self, true, false); - const auto apiOut = apiLifetime->make_state<Api>(self, false, true); + const auto c = (_creditsType == CreditsType::Ton); + const auto apiFull = apiLifetime->make_state<Api>(self, true, true, c); + const auto apiIn = apiLifetime->make_state<Api>(self, true, false, c); + const auto apiOut = apiLifetime->make_state<Api>(self, false, true, c); apiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) { apiIn->request({}, [=](Data::CreditsStatusSlice inSlice) { apiOut->request({}, [=](Data::CreditsStatusSlice outSlice) { @@ -387,128 +405,199 @@ void Credits::setupHistory(not_null<Ui::VerticalLayout*> container) { void Credits::setupContent() { const auto content = Ui::CreateChild<Ui::VerticalLayout>(this); + const auto isCurrency = _creditsType == CreditsType::Ton; const auto paid = [=] { if (_parent) { Ui::StartFireworks(_parent); } }; - Ui::AddSkip(content); - Ui::AddSkip(content); - const auto balanceLine = content->add( - object_ptr<Ui::CenterWrap<>>( - content, - object_ptr<Ui::RpWidget>(content)))->entity(); - const auto balanceIcon = CreateSingleStarWidget( - balanceLine, - st::creditsSettingsBigBalance.style.font->height); - const auto balanceAmount = Ui::CreateChild<Ui::FlatLabel>( - balanceLine, - _controller->session().credits().balanceValue( - ) | rpl::map(Lang::FormatStarsAmountDecimal), - st::creditsSettingsBigBalance); - balanceAmount->sizeValue() | rpl::start_with_next([=] { - balanceLine->resize( - balanceIcon->width() - + st::creditsSettingsBigBalanceSkip - + balanceAmount->textMaxWidth(), - balanceIcon->height()); - }, balanceLine->lifetime()); - balanceLine->widthValue() | rpl::start_with_next([=] { - balanceAmount->moveToRight(0, 0); - }, balanceLine->lifetime()); - Ui::AddSkip(content); - content->add( - object_ptr<Ui::CenterWrap<>>( - content, - object_ptr<Ui::FlatLabel>( - content, - tr::lng_credits_balance_me(), - st::infoTopBar.subtitle))); - Ui::AddSkip(content); - Ui::AddSkip(content); - Ui::AddSkip(content); struct State final { BuyStarsHandler buyStars; }; const auto state = content->lifetime().make_state<State>(); - const auto button = content->add( - object_ptr<Ui::RoundButton>( - content, + { + const auto button = content->add( + object_ptr<Ui::CenterWrap<Ui::RoundButton>>( + content, + object_ptr<Ui::RoundButton>( + content, + nullptr, + st::creditsSettingsBigBalanceButton)), + st::boxRowPadding)->entity(); + button->setContext([&]() -> Ui::Text::MarkedContext { + auto customEmojiFactory = [=](const auto &...) { + const auto &icon = st::settingsIconAdd; + auto image = QImage( + (icon.size() + QSize(st::lineWidth * 4, 0)) + * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + const auto r = Rect(icon.size()) - Margins(st::lineWidth * 2); + image.setDevicePixelRatio(style::DevicePixelRatio()); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::activeButtonFg); + p.drawEllipse(r); + icon.paintInCenter(p, r, st::windowBgActive->c); + } + return std::make_unique<Ui::Text::StaticCustomEmoji>( + std::move(image), + u"topup_button"_q); + }; + return { .customEmojiFactory = std::move(customEmojiFactory) }; + }()); + button->setText( rpl::conditional( state->buyStars.loadingValue(), - rpl::single(QString()), - tr::lng_credits_buy_button()), - st::creditsSettingsBigBalanceButton), - st::boxRowPadding); - button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - const auto show = _controller->uiShow(); - button->setClickedCallback(state->buyStars.handler(show, paid)); - { - using namespace Info::Statistics; - const auto loadingAnimation = InfiniteRadialAnimationWidget( - button, - button->height() / 2); - AddChildToWidgetCenter(button, loadingAnimation); - loadingAnimation->showOn(state->buyStars.loadingValue()); - } - const auto paddings = rect::m::sum::h(st::boxRowPadding); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != (content->width() - paddings)); - }) | rpl::start_with_next([=] { - button->resizeToWidth(content->width() - paddings); - }, button->lifetime()); - - Ui::AddSkip(content); - - { - const auto &giftSt = st::creditsSettingsBigBalanceButtonGift; - const auto giftDelay = giftSt.ripple.hideDuration * 2; - const auto fakeLoading - = content->lifetime().make_state<rpl::variable<bool>>(false); - const auto gift = content->add( - object_ptr<Ui::RoundButton>( - content, - rpl::conditional( - fakeLoading->value(), - rpl::single(QString()), - tr::lng_credits_gift_button()), - giftSt), - st::boxRowPadding); - gift->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - gift->setClickedCallback([=, controller = _controller] { - if (fakeLoading->current()) { - return; - } - *fakeLoading = true; - base::call_delayed(giftDelay, crl::guard(gift, [=] { - *fakeLoading = false; - Ui::ShowGiftCreditsBox(controller, paid); - })); - }); + rpl::single(TextWithEntities()), + isCurrency + ? tr::lng_credits_currency_summary_in_button( + Ui::Text::WithEntities) + : tr::lng_credits_topup_button( + lt_emoji, + rpl::single(Ui::Text::SingleCustomEmoji(u"+"_q)), + Ui::Text::WithEntities))); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + const auto show = _controller->uiShow(); + if (isCurrency) { + const auto url = tr::lng_suggest_low_ton_fragment_url(tr::now); + button->setClickedCallback([=] { UrlClickHandler::Open(url); }); + } else { + button->setClickedCallback(state->buyStars.handler(show, paid)); + } { using namespace Info::Statistics; const auto loadingAnimation = InfiniteRadialAnimationWidget( - gift, - gift->height() / 2, - &st::editStickerSetNameLoading); - AddChildToWidgetCenter(gift, loadingAnimation); - loadingAnimation->showOn(fakeLoading->value()); + button, + button->height() / 2); + AddChildToWidgetCenter(button, loadingAnimation); + loadingAnimation->showOn(state->buyStars.loadingValue()); } - gift->widthValue() | rpl::filter([=] { - return (gift->widthNoMargins() != (content->width() - paddings)); - }) | rpl::start_with_next([=] { - gift->resizeToWidth(content->width() - paddings); - }, gift->lifetime()); } Ui::AddSkip(content); Ui::AddSkip(content); - Ui::AddDivider(content); + Ui::AddSkip(content, st::lineWidth); - setupStarRefPromo(content); - setupSubscriptions(content); + const auto &textSt = st::creditsPremiumCover.about; + auto context = [&]() -> Ui::Text::MarkedContext { + const auto height = textSt.style.font->height; + auto customEmojiFactory = [=](const auto &...) { + return std::make_unique<Ui::Text::ShiftedEmoji>( + isCurrency + ? std::make_unique<Ui::Text::StaticCustomEmoji>( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::currencyFg->c), + u"currency_icon:%1"_q.arg(height)) + : Ui::MakeCreditsIconEmoji(height, 1), + isCurrency + ? QPoint(0, st::lineWidth * 2) + : QPoint(-st::lineWidth, st::lineWidth)); + }; + return { .customEmojiFactory = std::move(customEmojiFactory) }; + }(); + content->add( + object_ptr<Ui::CenterWrap<>>( + content, + object_ptr<Ui::FlatLabel>( + content, + tr::lng_credits_balance_me_count( + lt_emoji, + rpl::single(Ui::MakeCreditsIconEntity()), + lt_amount, + (isCurrency + ? _controller->session().credits().tonBalanceValue() + : _controller->session().credits().balanceValue() + ) | rpl::map( + Lang::FormatCreditsAmountDecimal + ) | rpl::map(Ui::Text::Bold), + Ui::Text::WithEntities), + textSt, + st::defaultPopupMenu, + std::move(context)))); + if (isCurrency) { + const auto rate = _controller->session().credits().usdRate(); + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<>>( + content, + object_ptr<Ui::CenterWrap<>>( + content, + object_ptr<Ui::FlatLabel>( + content, + _controller->session().credits().tonBalanceValue( + ) | rpl::map([=](CreditsAmount value) { + using namespace Info::ChannelEarn; + return value ? ToUsd(value, rate, 3) : QString(); + }), + st::channelEarnOverviewSubMinorLabel)))); + wrap->toggleOn(_controller->session().credits().tonBalanceValue( + ) | rpl::map(rpl::mappers::_1 > CreditsAmount(0))); + wrap->finishAnimating(); + } + Ui::AddSkip(content, st::lineWidth); + Ui::AddSkip(content, st::lineWidth); + Ui::AddSkip(content); + + Ui::AddSkip(content); + if (isCurrency) { + Ui::AddDividerText( + content, + tr::lng_credits_currency_summary_in_subtitle()); + } else { + Ui::AddDivider(content); + } + Ui::AddSkip(content, st::lineWidth * 4); + + const auto controller = _controller->parentController(); + const auto self = _controller->session().user(); + if (!isCurrency) { + const auto wrap = content->add( + object_ptr<Ui::SlideWrap<Ui::AbstractButton>>( + content, + CreateButtonWithIcon( + content, + tr::lng_credits_stats_button(), + st::settingsCreditsButton, + { &st::menuIconStats }))); + wrap->entity()->setClickedCallback([=] { + controller->showSection(Info::BotEarn::Make(self)); + }); + wrap->toggleOn(_controller->session().credits().loadedValue( + ) | rpl::map([=] { + return _controller->session().credits().statsEnabled(); + })); + } + if (!isCurrency) { + AddButtonWithIcon( + content, + tr::lng_credits_gift_button(), + st::settingsCreditsButton, + { &st::settingsButtonIconGift })->setClickedCallback([=] { + Ui::ShowGiftCreditsBox(controller, paid); + }); + } + + if (!isCurrency && Info::BotStarRef::Join::Allowed(self)) { + AddButtonWithIcon( + content, + tr::lng_credits_earn_button(), + st::settingsCreditsButton, + { &st::settingsButtonIconEarn })->setClickedCallback([=] { + controller->showSection(Info::BotStarRef::Join::Make(self)); + }); + } + + if (!isCurrency) { + Ui::AddSkip(content, st::lineWidth * 4); + Ui::AddDivider(content); + + setupSubscriptions(content); + } setupHistory(content); Ui::ResizeFitChild(this, content); @@ -517,6 +606,7 @@ void Credits::setupContent() { QPointer<Ui::RpWidget> Credits::createPinnedToTop( not_null<QWidget*> parent) { _parent = parent; + const auto isCurrency = _creditsType == CreditsType::Ton; const auto content = [&]() -> Ui::Premium::TopBarAbstract* { const auto weak = base::make_weak(_controller); @@ -531,9 +621,12 @@ QPointer<Ui::RpWidget> Credits::createPinnedToTop( st::creditsPremiumCover, Ui::Premium::TopBarDescriptor{ .clickContextOther = clickContextOther, - .title = tr::lng_credits_summary_title(), - .about = tr::lng_credits_summary_about( - TextWithEntities::Simple), + .logo = isCurrency ? u"diamond"_q : QString(), + .title = title(), + .about = (isCurrency + ? tr::lng_credits_currency_summary_about + : tr::lng_credits_summary_about)( + TextWithEntities::Simple), .light = true, .gradientStops = Ui::Premium::CreditsIconGradientStops(), }); @@ -564,7 +657,10 @@ QPointer<Ui::RpWidget> Credits::createPinnedToTop( { const auto balance = AddBalanceWidget( content, - _controller->session().credits().balanceValue(), + &_controller->session(), + isCurrency + ? _controller->session().credits().tonBalanceValue() + : _controller->session().credits().balanceValue(), true, content->heightValue() | rpl::map([=](int height) { const auto ratio = float64(height - content->minimumHeight()) @@ -633,6 +729,9 @@ void Credits::showFinished() { _showFinished.fire({}); } +class Currency { +}; + } // namespace template <> @@ -643,7 +742,27 @@ struct SectionFactory<Credits> : AbstractSectionFactory { not_null<Ui::ScrollArea*> scroll, rpl::producer<Container> containerValue ) const final override { - return object_ptr<Credits>(parent, controller); + return object_ptr<Credits>(parent, controller, CreditsType::Stars); + } + bool hasCustomTopBar() const final override { + return true; + } + + [[nodiscard]] static const std::shared_ptr<SectionFactory> &Instance() { + static const auto result = std::make_shared<SectionFactory>(); + return result; + } +}; + +template <> +struct SectionFactory<Currency> : AbstractSectionFactory { + object_ptr<AbstractSection> create( + not_null<QWidget*> parent, + not_null<Window::SessionController*> controller, + not_null<Ui::ScrollArea*> scroll, + rpl::producer<Container> containerValue + ) const final override { + return object_ptr<Credits>(parent, controller, CreditsType::Ton); } bool hasCustomTopBar() const final override { return true; @@ -659,6 +778,10 @@ Type CreditsId() { return Credits::Id(); } +Type CurrencyId() { + return SectionFactory<Currency>::Instance(); +} + BuyStarsHandler::BuyStarsHandler() = default; BuyStarsHandler::~BuyStarsHandler() = default; @@ -675,7 +798,7 @@ Fn<void()> BuyStarsHandler::handler( const auto options = _api ? _api->options() : Data::CreditTopupOptions(); - const auto amount = StarsAmount(); + const auto amount = CreditsAmount(); const auto weak = Ui::MakeWeak(box); FillCreditOptions(show, inner, self, amount, [=] { if (const auto strong = weak.data()) { diff --git a/Telegram/SourceFiles/settings/settings_credits.h b/Telegram/SourceFiles/settings/settings_credits.h index 27692cd3d4..63ebae985f 100644 --- a/Telegram/SourceFiles/settings/settings_credits.h +++ b/Telegram/SourceFiles/settings/settings_credits.h @@ -20,6 +20,7 @@ class SessionShow; namespace Settings { [[nodiscard]] Type CreditsId(); +[[nodiscard]] Type CurrencyId(); class BuyStarsHandler final : public base::has_weak_ptr { public: diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index dcbe5c3705..15d889fea8 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -43,6 +43,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/bot/starref/info_bot_starref_common.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "info/channel_statistics/earn/info_channel_earn_widget.h" // Info::ChannelEarn::Make. +#include "info/channel_statistics/earn/earn_format.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "info/peer_gifts/info_peer_gifts_common.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" @@ -141,13 +143,13 @@ class Balance final public: using Ui::RpWidget::RpWidget; - void setBalance(StarsAmount balance) { + void setBalance(CreditsAmount balance) { _balance = balance; - _tooltip = Lang::FormatStarsAmountDecimal(balance); + _tooltip = Lang::FormatCreditsAmountDecimal(balance); } void enterEventHook(QEnterEvent *e) override { - if (_balance >= StarsAmount(10'000)) { + if (_balance >= CreditsAmount(10'000)) { Ui::Tooltip::Show(1000, this); } } @@ -170,7 +172,7 @@ public: private: QString _tooltip; - StarsAmount _balance; + CreditsAmount _balance; }; @@ -441,7 +443,7 @@ void AddMiniStars( const auto stars = widget->lifetime().make_state<ColoredMiniStars>( widget, false, - Ui::Premium::MiniStars::Type::BiStars); + Ui::Premium::MiniStarsType::BiStars); stars->setColorOverride(Ui::Premium::CreditsIconGradientStops()); widget->resize(boxWidth - photoSize, photoSize * heightRatio); content->sizeValue( @@ -506,7 +508,7 @@ void FillCreditOptions( std::shared_ptr<Main::SessionShow> show, not_null<Ui::VerticalLayout*> container, not_null<PeerData*> peer, - StarsAmount minimumCredits, + CreditsAmount minimumCredits, Fn<void()> paid, rpl::producer<QString> subtitle, std::vector<Data::CreditTopupOption> preloadedTopupOptions) { @@ -552,12 +554,12 @@ void FillCreditOptions( - int(singleStarWidth * 1.5); const auto buttonHeight = st.height + rect::m::sum::v(st.padding); const auto minCredits = (!options.empty() - && (minimumCredits > StarsAmount(options.back().credits))) - ? StarsAmount() + && (minimumCredits > CreditsAmount(options.back().credits))) + ? CreditsAmount() : minimumCredits; for (auto i = 0; i < options.size(); i++) { const auto &option = options[i]; - if (StarsAmount(option.credits) < minCredits) { + if (CreditsAmount(option.credits) < minCredits) { continue; } const auto button = [&] { @@ -684,19 +686,17 @@ void FillCreditOptions( not_null<Ui::RpWidget*> AddBalanceWidget( not_null<Ui::RpWidget*> parent, - rpl::producer<StarsAmount> balanceValue, + not_null<Main::Session*> session, + rpl::producer<CreditsAmount> balanceValue, bool rightAlign, rpl::producer<float64> opacityValue) { struct State final { - QImage star; float64 opacity = 1.0; Ui::Text::String label; Ui::Text::String count; }; const auto balance = Ui::CreateChild<Balance>(parent); const auto state = balance->lifetime().make_state<State>(); - state->star = QImage(Ui::GenerateStars(st::creditsBalanceStarHeight, 1)); - const auto starSize = state->star.size() / style::DevicePixelRatio(); state->label = Ui::Text::String( st::defaultTextStyle, tr::lng_credits_summary_balance(tr::now)); @@ -708,22 +708,40 @@ not_null<Ui::RpWidget*> AddBalanceWidget( state->opacity = value; }, balance->lifetime()); } - const auto diffBetweenStarAndCount = state->count.style()->font->spacew; const auto resize = [=] { balance->resize( - std::max( - state->label.maxWidth(), - state->count.maxWidth() - + starSize.width() - + diffBetweenStarAndCount), - state->label.style()->font->height + starSize.height()); + std::max(state->label.maxWidth(), state->count.maxWidth()), + (state->label.style()->font->height + + state->count.style()->font->height)); }; std::move( balanceValue - ) | rpl::start_with_next([=](StarsAmount value) { - state->count.setText( + ) | rpl::start_with_next([=](CreditsAmount value) { + auto text = TextWithEntities(); + const auto manager = &session->data().customEmojiManager(); + if (value.ton()) { + text.append(Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::currencyFg->c), + st::channelEarnCurrencyLearnMargins, + false)) + ).append(' ').append(Lang::FormatCreditsAmountDecimal(value)); + } else { + text.append( + manager->creditsEmoji() + ).append(' ').append( + Lang::FormatCreditsAmountToShort(value).string); + } + state->count.setMarkedText( st::semiboldTextStyle, - Lang::FormatStarsAmountToShort(value).string); + text, + kMarkupTextOptions, + Core::TextContext({ + .session = session, + .repaint = [=] { balance->update(); }, + })); balance->setBalance(value); resize(); }, balance->lifetime()); @@ -742,22 +760,10 @@ not_null<Ui::RpWidget*> AddBalanceWidget( }); state->count.draw(p, { .position = QPoint( - (rightAlign - ? (balance->width() - state->count.maxWidth()) - : (starSize.width() + diffBetweenStarAndCount)), - state->label.minHeight() - + (starSize.height() - state->count.minHeight()) / 2), + rightAlign ? (balance->width() - state->count.maxWidth()) : 0, + state->label.minHeight()), .availableWidth = balance->width(), }); - p.drawImage( - (rightAlign - ? (balance->width() - - state->count.maxWidth() - - starSize.width() - - diffBetweenStarAndCount) - : 0), - state->label.minHeight(), - state->star); }, balance->lifetime()); return balance; } @@ -1157,7 +1163,8 @@ void GenericCreditsEntryBox( && giftChannel->canTransferGifts(); const auto starGiftCanManage = isStarGift && !creditsHistoryStarGift - && (e.in || giftToChannelCanManage); + && (e.in || giftToChannelCanManage) + && !e.fromGiftSlug; const auto starGiftCanTransfer = isStarGift && !creditsHistoryStarGift && (e.in || giftToChannelCanTransfer); @@ -1304,14 +1311,22 @@ void GenericCreditsEntryBox( auto &packs = session->giftBoxStickersPacks(); const auto document = starGiftSticker ? starGiftSticker - : packs.lookup(e.premiumMonthsForStars - ? e.premiumMonthsForStars - : packs.monthsForStars(e.credits.whole())); + : e.credits.ton() + ? packs.tonLookup(e.credits.whole()) + : packs.lookup( + e.premiumMonthsForStars + ? e.premiumMonthsForStars + : packs.monthsForStars(e.credits.whole())); if (document && document->sticker()) { + const auto origin = starGiftSticker + ? starGiftSticker->stickerOrGifOrigin() + : e.credits.ton() + ? packs.tonOrigin() + : packs.origin(); state->sticker = document; state->media = document->createMediaView(); - state->media->thumbnailWanted(packs.origin()); - state->media->automaticLoad(packs.origin(), nullptr); + state->media->thumbnailWanted(origin); + state->media->automaticLoad(origin, nullptr); rpl::single() | rpl::then( session->downloaderTaskFinished() ) | rpl::filter([=] { @@ -1423,6 +1438,7 @@ void GenericCreditsEntryBox( constexpr auto kMinus = QChar(0x2212); auto &lifetime = content->lifetime(); const auto text = lifetime.make_state<Ui::Text::String>(); + auto minorText = (Ui::Text::String*)(nullptr); const auto roundedText = e.refunded ? tr::lng_channel_earn_history_return(tr::now) : e.pending @@ -1460,14 +1476,14 @@ void GenericCreditsEntryBox( Ui::Text::WithEntities), kMarkupTextOptions, context); - } else { + } else if (e.credits.stars()) { auto t = TextWithEntities() .append((e.in && (creditsHistoryStarGift || !isStarGift)) - ? u"+"_q + ? QChar('+') : (e.gift && !creditsHistoryStarGift) - ? QString() - : QString(kMinus)) - .append(Lang::FormatStarsAmountDecimal(e.credits.abs())) + ? QChar() + : kMinus) + .append(Lang::FormatCreditsAmountDecimal(e.credits.abs())) .append(QChar(' ')) .append(owner->customEmojiManager().creditsEmoji()); text->setMarkedText( @@ -1475,6 +1491,35 @@ void GenericCreditsEntryBox( std::move(t), kMarkupTextOptions, context); + } else if (e.credits.ton()) { + auto t = TextWithEntities() + .append((e.in ? QChar('+') : kMinus)) + .append(Info::ChannelEarn::MajorPart(e.credits.abs())); + text->setMarkedText( + st::channelEarnHistoryMajorLabel.style, + std::move(t), + kMarkupTextOptions, + context); + + auto minor = TextWithEntities() + .append(Info::ChannelEarn::MinorPart(e.credits.abs())) + .append(QChar(' ')) + .append( + Ui::Text::SingleCustomEmoji( + owner->customEmojiManager().registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + (e.in + ? st::boxTextFgGood->c + : st::menuIconAttentionColor->c)), + QMargins(0, st::lineWidth * 1, 0, 0), + false))); + minorText = lifetime.make_state<Ui::Text::String>(); + minorText->setMarkedText( + st::channelEarnHistoryMinorLabel.style, + std::move(minor), + kMarkupTextOptions, + context); } const auto font = text->style()->font; const auto roundedFont = st::defaultTextStyle.font; @@ -1484,7 +1529,9 @@ void GenericCreditsEntryBox( + roundedSkip + roundedFont->height : 0; - const auto fullWidth = text->maxWidth() + roundedWidth; + const auto fullWidth = text->maxWidth() + + roundedWidth + + (minorText ? minorText->maxWidth() : 0); amount->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(amount); @@ -1500,13 +1547,21 @@ void GenericCreditsEntryBox( ? st::windowBoldFg : st::menuIconAttentionColor); const auto x = (amount->width() - fullWidth) / 2; + const auto y = (amount->height() - font->height) / 2; text->draw(p, Ui::Text::PaintContext{ - .position = QPoint( - x, - (amount->height() - font->height) / 2), + .position = QPoint(x, y), .outerWidth = amount->width(), .availableWidth = amount->width(), }); + if (minorText) { + minorText->draw(p, Ui::Text::PaintContext{ + .position = QPoint( + x + text->maxWidth(), + y + st::lineWidth * 2), + .outerWidth = amount->width(), + .availableWidth = amount->width(), + }); + } if (rounded) { const auto roundedLeft = fullWidth @@ -1614,7 +1669,7 @@ void GenericCreditsEntryBox( about->setTextColorOverride(st::menuIconAttentionColor->c); } } else if (isStarGift) { - } else if (e.gift || isPrize) { + } else if ((e.gift || isPrize) && e.credits.stars()) { Ui::AddSkip(content); auto link = tr::lng_credits_box_history_entry_gift_about_link( lt_emoji, @@ -1799,7 +1854,7 @@ void GenericCreditsEntryBox( Ui::AddSkip(content); - if (!isStarGift) { + if (!isStarGift && e.credits.stars()) { box->addRow(object_ptr<Ui::CenterWrap<>>( box, object_ptr<Ui::FlatLabel>( @@ -2068,7 +2123,7 @@ void GiftedCreditsBox( ? tr::lng_credits_box_history_entry_gift_name : tr::lng_credits_box_history_entry_gift_sent)(tr::now), .date = base::unixtime::parse(date), - .credits = StarsAmount(count), + .credits = CreditsAmount(count), .bareMsgId = uint64(), .barePeerId = (anonymous ? uint64() : peer->id.value), .peerType = (anonymous ? PeerType::Fragment : PeerType::Peer), @@ -2091,7 +2146,7 @@ void CreditsPrizeBox( .title = QString(), .description = TextWithEntities(), .date = base::unixtime::parse(date), - .credits = StarsAmount(data.count), + .credits = CreditsAmount(data.count), .barePeerId = data.channel ? data.channel->id.value : 0, @@ -2114,7 +2169,7 @@ void GlobalStarGiftBox( box, show, Data::CreditsHistoryEntry{ - .credits = StarsAmount(data.stars), + .credits = CreditsAmount(data.stars), .bareGiftStickerId = data.document->id, .bareGiftOwnerId = ownerId, .bareGiftResaleRecipientId = ((resaleRecipientId != selfId) @@ -2141,7 +2196,7 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( return { .description = data.message, .date = base::unixtime::parse(data.date), - .credits = StarsAmount(data.info.stars), + .credits = CreditsAmount(data.info.stars), .bareMsgId = uint64(data.manageId.userMessageId().bare), .barePeerId = data.fromId.value, .bareGiftStickerId = data.info.document->id, @@ -2220,7 +2275,7 @@ void StarGiftViewBox( .id = data.slug, .description = data.message, .date = base::unixtime::parse(item->date()), - .credits = StarsAmount(data.count), + .credits = CreditsAmount(data.count), .bareMsgId = uint64(item->id.bare), .barePeerId = fromId.value, .bareGiftStickerId = data.document ? data.document->id : 0, @@ -2271,7 +2326,7 @@ void ShowRefundInfoBox( auto info = Data::CreditsHistoryEntry(); info.id = refund->transactionId; info.date = base::unixtime::parse(item->date()); - info.credits = StarsAmount(refund->amount); + info.credits = CreditsAmount(refund->amount); info.barePeerId = refund->peer->id.value; info.peerType = Data::CreditsHistoryEntry::PeerType::Peer; info.refunded = true; @@ -2369,7 +2424,7 @@ void SmallBalanceBox( Fn<void()> paid) { Expects(show->session().credits().loaded()); - auto credits = StarsAmount(wholeCredits); + auto credits = CreditsAmount(wholeCredits); box->setWidth(st::boxWideWidth); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); @@ -2395,11 +2450,15 @@ void SmallBalanceBox( return value.recipientId ? owner->peer(value.recipientId)->shortName() : QString(); + }, [&](SmallBalanceForSuggest value) { + return value.recipientId + ? owner->peer(value.recipientId)->shortName() + : QString(); }); auto needed = show->session().credits().balanceValue( - ) | rpl::map([=](StarsAmount balance) { - return (balance < credits) ? (credits - balance) : StarsAmount(); + ) | rpl::map([=](CreditsAmount balance) { + return (balance < credits) ? (credits - balance) : CreditsAmount(); }); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { return box->setPinnedToTopContent(object_ptr<Ui::Premium::TopBar>( @@ -2411,8 +2470,8 @@ void SmallBalanceBox( rpl::duplicate( needed ) | rpl::filter( - rpl::mappers::_1 > StarsAmount(0) - ) | rpl::map([](StarsAmount amount) { + rpl::mappers::_1 > CreditsAmount(0) + ) | rpl::map([](CreditsAmount amount) { return amount.value(); })), .about = (v::is<SmallBalanceSubscription>(source) @@ -2441,6 +2500,11 @@ void SmallBalanceBox( lt_user, rpl::single(Ui::Text::Bold(name)), Ui::Text::RichLangValue)) + : v::is<SmallBalanceForSuggest>(source) + ? tr::lng_credits_small_balance_for_suggest( + lt_channel, + rpl::single(Ui::Text::Bold(name)), + Ui::Text::RichLangValue) : name.isEmpty() ? tr::lng_credits_small_balance_fallback( Ui::Text::RichLangValue) @@ -2479,6 +2543,7 @@ void SmallBalanceBox( { const auto balance = AddBalanceWidget( content, + &show->session(), show->session().credits().balanceValue(), true); show->session().credits().load(true); @@ -2506,7 +2571,7 @@ void AddWithdrawalWidget( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, rpl::producer<QString> secondButtonUrl, - rpl::producer<StarsAmount> availableBalanceValue, + rpl::producer<CreditsAmount> availableBalanceValue, rpl::producer<QDateTime> dateValue, bool withdrawalEnabled, rpl::producer<QString> usdValue) { @@ -2521,8 +2586,8 @@ void AddWithdrawalWidget( labels, rpl::duplicate( availableBalanceValue - ) | rpl::map([](StarsAmount v) { - return Lang::FormatStarsAmountDecimal(v); + ) | rpl::map([](CreditsAmount v) { + return Lang::FormatCreditsAmountDecimal(v); }), st::channelEarnBalanceMajorLabel); const auto icon = Ui::CreateSingleStarWidget( @@ -2554,9 +2619,15 @@ void AddWithdrawalWidget( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( container, object_ptr<Ui::VerticalLayout>(container))); + const auto starsWithdrawMax = CreditsAmount( + controller->session().appConfig().starsWithdrawMax()); const auto input = Ui::AddInputFieldForCredits( withdrawalWrap->entity(), - rpl::duplicate(availableBalanceValue)); + rpl::duplicate( + availableBalanceValue + ) | rpl::map([=](CreditsAmount amount) { + return (amount > starsWithdrawMax) ? starsWithdrawMax : amount; + })); Ui::AddSkip(withdrawalWrap->entity()); Ui::AddSkip(withdrawalWrap->entity()); @@ -2621,7 +2692,7 @@ void AddWithdrawalWidget( st::settingsPremiumIconStar, { 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, true)); - using Balance = rpl::variable<StarsAmount>; + using Balance = rpl::variable<CreditsAmount>; const auto currentBalance = input->lifetime().make_state<Balance>( rpl::duplicate(availableBalanceValue)); const auto process = [=] { @@ -2789,17 +2860,19 @@ void AddWithdrawalWidget( const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); auto about = Ui::CreateLabelWithCustomEmoji( container, - tr::lng_bot_earn_learn_credits_out_about( - lt_link, - tr::lng_channel_earn_about_link( - lt_emoji, - rpl::single(arrow), - Ui::Text::RichLangValue - ) | rpl::map([](TextWithEntities text) { - return Ui::Text::Link( - std::move(text), - tr::lng_bot_earn_balance_about_url(tr::now)); - }), + (peer->isSelf() + ? tr::lng_self_earn_learn_credits_out_about + : tr::lng_bot_earn_learn_credits_out_about)( + lt_link, + tr::lng_channel_earn_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + tr::lng_bot_earn_balance_about_url(tr::now)); + }), Ui::Text::RichLangValue), Core::TextContext({ .session = session }), st::boxDividerLabel); @@ -2831,7 +2904,7 @@ void MaybeRequestBalanceIncrease( state->lifetime.destroy(); const auto balance = session->credits().balance(); - if (StarsAmount(credits) <= balance) { + if (CreditsAmount(credits) <= balance) { if (const auto onstack = done) { onstack(SmallBalanceResult::Already); } diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index a1bf02a4bd..9789278fac 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -72,14 +72,15 @@ void FillCreditOptions( std::shared_ptr<Main::SessionShow> show, not_null<Ui::VerticalLayout*> container, not_null<PeerData*> peer, - StarsAmount minCredits, + CreditsAmount minCredits, Fn<void()> paid, rpl::producer<QString> subtitle, std::vector<Data::CreditTopupOption> preloadedTopupOptions); [[nodiscard]] not_null<Ui::RpWidget*> AddBalanceWidget( not_null<Ui::RpWidget*> parent, - rpl::producer<StarsAmount> balanceValue, + not_null<Main::Session*> session, + rpl::producer<CreditsAmount> balanceValue, bool rightAlign, rpl::producer<float64> opacityValue = nullptr); @@ -88,7 +89,7 @@ void AddWithdrawalWidget( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, rpl::producer<QString> secondButtonUrl, - rpl::producer<StarsAmount> availableBalanceValue, + rpl::producer<CreditsAmount> availableBalanceValue, rpl::producer<QDateTime> dateValue, bool withdrawalEnabled, rpl::producer<QString> usdValue); @@ -233,13 +234,17 @@ struct SmallBalanceStarGift { struct SmallBalanceForMessage { PeerId recipientId; }; +struct SmallBalanceForSuggest { + PeerId recipientId; +}; struct SmallBalanceSource : std::variant< SmallBalanceBot, SmallBalanceReaction, SmallBalanceSubscription, SmallBalanceDeepLink, SmallBalanceStarGift, - SmallBalanceForMessage> { + SmallBalanceForMessage, + SmallBalanceForSuggest> { using variant::variant; }; diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 7b2162da8d..ef7252be56 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_credits.h" #include "core/application.h" #include "core/click_handler_types.h" +#include "settings/cloud_password/settings_cloud_password_input.h" #include "settings/settings_advanced.h" #include "settings/settings_business.h" #include "settings/settings_calls.h" @@ -42,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/new_badges.h" #include "ui/rect.h" #include "ui/vertical_list.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_emoji_status_panel.h" #include "data/components/credits.h" @@ -71,6 +73,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "base/call_delayed.h" #include "base/platform/base_platform_info.h" +#include "styles/style_chat.h" #include "styles/style_settings.h" #include "styles/style_info.h" #include "styles/style_layers.h" // boxLabel @@ -496,7 +499,7 @@ void SetupValidatePhoneNumberSuggestion( yes->setClickedCallback([=] { controller->session().promoSuggestions().dismiss( kSugValidatePhone.utf8()); - mainWrap->toggle(false, anim::type::normal); + mainWrap->toggle(false, anim::type::normal); }); const auto no = Ui::CreateChild<Ui::RoundButton>( wrap, @@ -551,6 +554,77 @@ void SetupValidatePhoneNumberSuggestion( Ui::AddSkip(content); } +void SetupValidatePasswordSuggestion( + not_null<Window::SessionController*> controller, + not_null<Ui::VerticalLayout*> container, + Fn<void(Type)> showOther) { + if (!controller->session().promoSuggestions().current( + Data::PromoSuggestions::SugValidatePassword()) + || controller->session().promoSuggestions().current( + kSugValidatePhone.utf8())) { + return; + } + const auto mainWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto content = mainWrap->entity(); + Ui::AddSubsectionTitle( + content, + tr::lng_settings_suggestion_password_title(), + QMargins( + st::boxRowPadding.left() + - st::defaultSubsectionTitlePadding.left(), + 0, + 0, + 0)); + content->add( + object_ptr<Ui::FlatLabel>( + content, + tr::lng_settings_suggestion_password_about(), + st::boxLabel), + st::boxRowPadding); + + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto wrap = content->add( + object_ptr<Ui::FixedHeightWidget>( + content, + st::inviteLinkButton.height), + st::inviteLinkButtonsPadding); + const auto yes = Ui::CreateChild<Ui::RoundButton>( + wrap, + tr::lng_settings_suggestion_password_yes(), + st::inviteLinkButton); + yes->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + yes->setClickedCallback([=] { + controller->session().promoSuggestions().dismiss( + Data::PromoSuggestions::SugValidatePassword()); + mainWrap->toggle(false, anim::type::normal); + }); + const auto no = Ui::CreateChild<Ui::RoundButton>( + wrap, + tr::lng_settings_suggestion_password_no(), + st::inviteLinkButton); + no->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + no->setClickedCallback([=] { + showOther(Settings::CloudPasswordSuggestionInputId()); + }); + + wrap->widthValue() | rpl::start_with_next([=](int width) { + const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2; + yes->setFullWidth(buttonWidth); + no->setFullWidth(buttonWidth); + yes->moveToLeft(0, 0, width); + no->moveToRight(0, 0, width); + }, wrap->lifetime()); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddDivider(content); + Ui::AddSkip(content); +} + void SetupSections( not_null<Window::SessionController*> controller, not_null<Ui::VerticalLayout*> container, @@ -561,6 +635,10 @@ void SetupSections( controller, container, showOther); + SetupValidatePasswordSuggestion( + controller, + container, + showOther); const auto addSection = [&]( rpl::producer<QString> label, @@ -700,9 +778,9 @@ void SetupPremium( container, tr::lng_settings_credits(), controller->session().credits().balanceValue( - ) | rpl::map([=](StarsAmount c) { + ) | rpl::map([=](CreditsAmount c) { return c - ? Lang::FormatStarsAmountToShort(c).string + ? Lang::FormatCreditsAmountToShort(c).string : QString(); }), st::settingsButton), @@ -713,6 +791,50 @@ void SetupPremium( showOther(CreditsId()); }); } + { + const auto wrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + wrap->toggleOn( + controller->session().credits().tonBalanceValue( + ) | rpl::map([](CreditsAmount c) -> bool { return !c.empty(); })); + wrap->finishAnimating(); + controller->session().credits().tonLoad(); + const auto button = AddButtonWithLabel( + wrap->entity(), + tr::lng_settings_currency(), + controller->session().credits().tonBalanceValue( + ) | rpl::map([=](CreditsAmount c) { + return c + ? Lang::FormatCreditsAmountToShort(c).string + : QString(); + }), + st::settingsButton); + button->addClickHandler([=] { + controller->setPremiumRef("settings"); + showOther(CurrencyId()); + }); + + const auto badge = Ui::CreateChild<Ui::RpWidget>(button.get()); + const auto image = Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::menuIconColor->c); + + badge->resize(Size(st::tonFieldIconSize)); + badge->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(badge); + p.drawImage(0, 0, image); + }, badge->lifetime()); + + button->sizeValue() | rpl::start_with_next([=](const QSize &s) { + badge->moveToLeft( + button->st().iconLeft + + (st::menuIconShop.width() - badge->width()) / 2, + (s.height() - badge->height()) / 2); + }, badge->lifetime()); + } const auto button = AddButtonWithIcon( container, tr::lng_business_title(), diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 9775fb7ed0..45ae6993eb 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -390,6 +390,15 @@ using Order = std::vector<QString>; PremiumFeature::Effects, }, }, + { + u"todo"_q, + Entry{ + &st::settingsPremiumIconChecklist, + tr::lng_premium_summary_subtitle_todo_lists(), + tr::lng_premium_summary_about_todo_lists(), + PremiumFeature::TodoLists, + }, + }, }; } @@ -1618,6 +1627,8 @@ std::vector<PremiumFeature> PremiumFeaturesOrder( return PremiumFeature::Wallpapers; } else if (s == u"effects"_q) { return PremiumFeature::Effects; + } else if (s == u"todo"_q) { + return PremiumFeature::TodoLists; } return PremiumFeature::kCount; }) | ranges::views::filter([](PremiumFeature type) { diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index c64c2c5ccc..5ce93e02bb 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -204,7 +204,8 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPlong(), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong() // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost() ).match([&](const MTPDmessage &data) { return history->makeMessage( history->nextNonHistoryEntryId(), diff --git a/Telegram/SourceFiles/settings/settings_websites.cpp b/Telegram/SourceFiles/settings/settings_websites.cpp index 836f20ab4d..7f495998f0 100644 --- a/Telegram/SourceFiles/settings/settings_websites.cpp +++ b/Telegram/SourceFiles/settings/settings_websites.cpp @@ -119,7 +119,7 @@ void InfoBox( data.bot, st::websiteBigUserpic)), st::sessionBigCoverPadding)->entity(); - userpic->forceForumShape(true); + userpic->overrideShape(Ui::PeerUserpicShape::Forum); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); const auto nameWrap = box->addRow( @@ -224,25 +224,11 @@ PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) { const auto peer = _data.bot; auto userpic = _userpic = peer->createUserpicView(); return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { - const auto ratio = style::DevicePixelRatio(); - if (const auto cloud = peer->userpicCloudImage(userpic)) { - Ui::ValidateUserpicCache( - userpic, - cloud, - nullptr, - size * ratio, - true); - p.drawImage(QRect(x, y, size, size), userpic.cached); - } else { - if (_emptyUserpic.isNull()) { - _emptyUserpic = PeerData::GenerateUserpicImage( - peer, - _userpic, - size * ratio, - size * ratio * Ui::ForumUserpicRadiusMultiplier()); - } - p.drawImage(QRect(x, y, size, size), _emptyUserpic); - } + peer->paintUserpic(p, _userpic, { + .position = QPoint(x, y), + .size = size, + .shape = Ui::PeerUserpicShape::Forum, + }); }; } diff --git a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp index 4f9471c8ce..6241901804 100644 --- a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp +++ b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp @@ -22,7 +22,7 @@ constexpr auto kStep = 5.; } [[nodiscard]] QString Format(ChartValue absoluteValue) { - constexpr auto kTooMuch = ChartValue(10'000); + static constexpr auto kTooMuch = ChartValue(10'000); return (absoluteValue >= kTooMuch) ? Lang::FormatCountToShort(absoluteValue).string : QString::number(absoluteValue); diff --git a/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp b/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp index 3555dca48b..10a77ade05 100644 --- a/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp +++ b/Telegram/SourceFiles/statistics/statistics_data_deserialize.cpp @@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "statistics/statistics_data_deserialize.h" #include "base/debug_log.h" -#include "data/data_channel_earn.h" // kEarnMultiplier. #include "data/data_statistics_chart.h" #include "statistics/statistics_types.h" #include "ui/text/format_values.h" // kCreditsCurrency. @@ -76,10 +75,7 @@ Data::StatisticalChart StatisticalChartFromJSON(const QByteArray &json) { line.isHiddenOnStart = ranges::contains(hiddenLines, columnId); line.y.resize(length); for (auto i = 0; i < length; i++) { - using Currency = Data::StatisticalCurrency; - const auto multiplier = (result.currency == Currency::Credits) - ? Data::kEarnMultiplier - : 1; + const auto multiplier = 1; const auto value = ChartValue( base::SafeRound(array.at(i + 1).toDouble())) * multiplier; diff --git a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp index 8a94d10844..cdc8dc29f2 100644 --- a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp +++ b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "statistics/view/chart_rulers_view.h" -#include "data/data_channel_earn.h" // Data::kEarnMultiplier. #include "info/channel_statistics/earn/earn_format.h" #include "lang/lang_keys.h" #include "statistics/chart_lines_filter_controller.h" @@ -20,8 +19,11 @@ namespace Statistic { namespace { [[nodiscard]] QString FormatF(float64 absoluteValue) { - constexpr auto kTooMuch = int(10'000); - return (absoluteValue >= kTooMuch) + static constexpr auto kTooMuch = int(10'000); + static constexpr auto kTooSmall = 1e-9; + return (std::abs(absoluteValue) <= kTooSmall) + ? u"0"_q + : (absoluteValue >= kTooMuch) ? Lang::FormatCountToShort(absoluteValue).string : QString::number(absoluteValue); } @@ -39,12 +41,21 @@ void ChartRulersView::setChartData( || chartData.currencyRate; if (chartData.currencyRate) { _currencyIcon = ChartCurrencyIcon(chartData, {}); - _leftCustomCaption = [=](float64 value) { - return FormatF(value / float64(Data::kEarnMultiplier)); - }; - _rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) { - return Info::ChannelEarn::ToUsd(v, rate, 0); - }; + if (chartData.currency == Data::StatisticalCurrency::Ton) { + _leftCustomCaption = [=](float64 value) { + return FormatF(value / float64(kOneStarInNano)); + }; + _rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) { + return Info::ChannelEarn::ToUsd(v / float64(kOneStarInNano), rate, 0); + }; + } else { + _leftCustomCaption = [=](float64 value) { + return FormatF(value); + }; + _rightCustomCaption = [=, rate = chartData.currencyRate](float64 v) { + return Info::ChannelEarn::ToUsd(v, rate, 0); + }; + } _rightPen = QPen(st::windowSubTextFg); } if (_isDouble && (chartData.lines.size() == 2)) { diff --git a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp index 44f2496baa..97704994aa 100644 --- a/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp +++ b/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "statistics/widgets/point_details_widget.h" -#include "data/data_channel_earn.h" // Data::kEarnMultiplier. #include "info/channel_statistics/earn/earn_format.h" #include "lang/lang_keys.h" #include "statistics/statistics_common.h" @@ -179,7 +178,7 @@ PointDetailsWidget::PointDetailsWidget( const auto maxValueTextWidth = [&] { if (hasUsdLine) { auto maxValueWidth = 0; - const auto multiplier = float64(Data::kEarnMultiplier); + const auto multiplier = float64(kOneStarInNano); for (const auto &value : _chartData.lines.front().y) { const auto valueText = Ui::Text::String( _textStyle, @@ -187,7 +186,7 @@ PointDetailsWidget::PointDetailsWidget( const auto usdText = Ui::Text::String( _textStyle, Info::ChannelEarn::ToUsd( - value, + value / multiplier, _chartData.currencyRate, 0)); const auto width = std::max( @@ -325,7 +324,7 @@ void PointDetailsWidget::setXIndex(int xIndex) { nullptr, { float64(xIndex), float64(xIndex) }).parts : std::vector<PiePartData::Part>(); - const auto multiplier = float64(Data::kEarnMultiplier); + const auto multiplier = float64(kOneStarInNano); const auto isCredits = _chartData.currency == Data::StatisticalCurrency::Credits; for (auto i = 0; i < _chartData.lines.size(); i++) { @@ -362,7 +361,7 @@ void PointDetailsWidget::setXIndex(int xIndex) { textLine.value.setText( _textStyle, Info::ChannelEarn::ToUsd( - dataLine.y[xIndex], + dataLine.y[xIndex] / multiplier, _chartData.currencyRate, 0)); } _lines.push_back(std::move(textLine)); diff --git a/Telegram/SourceFiles/stdafx.h b/Telegram/SourceFiles/stdafx.h index 977126555b..6647362881 100644 --- a/Telegram/SourceFiles/stdafx.h +++ b/Telegram/SourceFiles/stdafx.h @@ -134,7 +134,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/palette.h" #include "styles/style_basic.h" -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "core/utils.h" #include "logs.h" #include "config.h" diff --git a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp index d9421433e6..ebef828a98 100644 --- a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp +++ b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp @@ -1084,6 +1084,7 @@ bool ReadSetting( context.sessionSettings().setHiddenPinnedMessageId( DeserializePeerId(i.key()), MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId MsgId(i.value())); } context.legacyRead = true; diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 08d2c98c2d..642f1f56fd 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -405,12 +405,15 @@ void SendingAlbum::removeItem(not_null<HistoryItem*> item) { Assert(i != end(items)); items.erase(i); if (moveCaption) { - const auto caption = item->originalText(); + auto caption = item->originalText(); const auto firstId = items.front().msgId; if (const auto first = item->history()->owner().message(firstId)) { // We don't need to finishEdition() here, because the whole // album will be rebuilt after one item was removed from it. - first->setText(caption); + auto firstCaption = first->originalText(); + first->setText(firstCaption.text.isEmpty() + ? std::move(caption) + : firstCaption.append('\n').append(std::move(caption))); refreshMediaCaption(first); } } diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 339840de23..e39e6ba018 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -67,6 +67,7 @@ constexpr auto kMultiDraftCursorsTagOld = quint64(0xFFFF'FFFF'FFFF'FF02ULL); constexpr auto kMultiDraftTag = quint64(0xFFFF'FFFF'FFFF'FF03ULL); constexpr auto kMultiDraftCursorsTag = quint64(0xFFFF'FFFF'FFFF'FF04ULL); constexpr auto kRichDraftsTag = quint64(0xFFFF'FFFF'FFFF'FF05ULL); +constexpr auto kDraftsTag2 = quint64(0xFFFF'FFFF'FFFF'FF06ULL); enum { // Local Storage Keys lskUserMap = 0x00, @@ -135,6 +136,33 @@ auto EmptyMessageDraftSources() return cWorkingDir() + u"tdata/tdld/"_q; } +[[nodiscard]] std::pair<quint64, quint64> SerializeSuggest( + SuggestPostOptions options) { + return { + ((quint64(options.exists) << 63) + | (quint64(quint32(options.date)))), + ((quint64(options.ton) << 63) + | (quint64(options.priceWhole) << 32) + | (quint64(options.priceNano))), + }; +} + +[[nodiscard]] SuggestPostOptions DeserializeSuggest( + std::pair<quint64, quint64> suggest) { + const auto exists = (suggest.first >> 63) ? 1 : 0; + const auto date = TimeId(uint32(suggest.first & 0xFFFF'FFFFULL)); + const auto ton = (suggest.second >> 63) ? 1 : 0; + const auto priceWhole = uint32((suggest.second >> 32) & 0x7FFF'FFFFULL); + const auto priceNano = uint32(suggest.second & 0xFFFF'FFFFULL); + return { + .exists = uint32(exists), + .priceWhole = priceWhole, + .priceNano = priceNano, + .ton = uint32(ton), + .date = date, + }; +} + } // namespace Account::Account(not_null<Main::Account*> owner, const QString &dataName) @@ -1176,7 +1204,9 @@ void EnumerateDrafts( } else if (key.isLocal() && (!supportMode || key.topicRootId())) { const auto i = map.find( - Data::DraftKey::Cloud(key.topicRootId())); + Data::DraftKey::Cloud( + key.topicRootId(), + key.monoforumPeerId())); const auto cloud = (i != end(map)) ? i->second.get() : nullptr; if (Data::DraftsAreEqual(draft.get(), cloud)) { continue; @@ -1185,6 +1215,7 @@ void EnumerateDrafts( callback( key, draft->reply, + draft->suggest, draft->textWithTags, draft->webpage, draft->cursor); @@ -1198,6 +1229,7 @@ void EnumerateDrafts( callback( key, draft.reply, + draft.suggest, draft.textWithTags, draft.webpage, cursor); @@ -1263,6 +1295,7 @@ void Account::writeDrafts(not_null<History*> history) { const auto sizeCallback = [&]( auto&&, // key const FullReplyTo &reply, + SuggestPostOptions suggest, const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor @@ -1270,6 +1303,7 @@ void Account::writeDrafts(not_null<History*> history) { + Serialize::stringSize(text.text) + TextUtilities::SerializeTagsSize(text.tags) + sizeof(qint64) + sizeof(qint64) // messageId + + (sizeof(quint64) * 2) // suggest + Serialize::stringSize(webpage.url) + sizeof(qint32) // webpage.forceLargeMedia + sizeof(qint32) // webpage.forceSmallMedia @@ -1285,22 +1319,26 @@ void Account::writeDrafts(not_null<History*> history) { EncryptedDescriptor data(size); data.stream - << quint64(kRichDraftsTag) + << quint64(kDraftsTag2) << SerializePeerId(peerId) << quint32(count); const auto writeCallback = [&]( const Data::DraftKey &key, const FullReplyTo &reply, + SuggestPostOptions suggest, const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor + const auto serialized = SerializeSuggest(suggest); data.stream << key.serialize() << text.text << TextUtilities::SerializeTags(text.tags) << qint64(reply.messageId.peer.value) << qint64(reply.messageId.msg.bare) + << serialized.first + << serialized.second << webpage.url << qint32(webpage.forceLargeMedia ? 1 : 0) << qint32(webpage.forceSmallMedia ? 1 : 0) @@ -1357,6 +1395,7 @@ void Account::writeDraftCursors(not_null<History*> history) { const auto writeCallback = [&]( const Data::DraftKey &key, auto&&, // reply + auto&&, // suggest auto&&, // text auto&&, // webpage const MessageCursor &cursor) { // cursor @@ -1426,7 +1465,7 @@ void Account::readDraftCursors(PeerId peerId, Data::HistoryDrafts &map) { ? Data::DraftKey::FromSerialized(keyValue) : keysOld ? Data::DraftKey::FromSerializedOld(keyValueOld) - : Data::DraftKey::Local(0); + : Data::DraftKey::Local(MsgId(), PeerId()); qint32 position = 0, anchor = 0, scroll = Ui::kQFixedMax; draft.stream >> position >> anchor >> scroll; if (const auto i = map.find(key); i != end(map)) { @@ -1453,13 +1492,14 @@ void Account::readDraftCursorsLegacy( return; } - if (const auto i = map.find(Data::DraftKey::Local({})); i != end(map)) { + if (const auto i = map.find(Data::DraftKey::Local(MsgId(), PeerId())) + ; i != end(map)) { i->second->cursor = MessageCursor( localPosition, localAnchor, localScroll); } - if (const auto i = map.find(Data::DraftKey::LocalEdit({})) + if (const auto i = map.find(Data::DraftKey::LocalEdit(MsgId(), PeerId())) ; i != end(map)) { i->second->cursor = MessageCursor( editPosition, @@ -1472,7 +1512,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) { const auto guard = gsl::finally([&] { if (const auto migrated = history->migrateFrom()) { readDraftsWithCursors(migrated); - migrated->clearLocalEditDraft({}); + migrated->clearLocalEditDraft(MsgId(), PeerId()); history->takeLocalDraft(migrated); } }); @@ -1516,12 +1556,14 @@ void Account::readDraftsWithCursors(not_null<History*> history) { } auto map = Data::HistoryDrafts(); const auto keysOld = (tag == kMultiDraftTagOld); - const auto rich = (tag == kRichDraftsTag); + const auto withSuggest = (tag == kDraftsTag2); + const auto rich = (tag == kRichDraftsTag) || withSuggest; for (auto i = 0; i != count; ++i) { TextWithTags text; QByteArray textTagsSerialized; qint64 keyValue = 0; qint64 messageIdPeer = 0, messageIdMsg = 0; + std::pair<quint64, quint64> suggestSerialized; qint32 keyValueOld = 0; QString webpageUrl; qint32 webpageForceLargeMedia = 0; @@ -1555,7 +1597,13 @@ void Account::readDraftsWithCursors(not_null<History*> history) { >> text.text >> textTagsSerialized >> messageIdPeer - >> messageIdMsg + >> messageIdMsg; + if (withSuggest) { + draft.stream + >> suggestSerialized.first + >> suggestSerialized.second; + } + draft.stream >> webpageUrl >> webpageForceLargeMedia >> webpageForceSmallMedia @@ -1578,6 +1626,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) { MsgId(messageIdMsg)), .topicRootId = key.topicRootId(), }, + DeserializeSuggest(suggestSerialized), MessageCursor(), Data::WebPageDraft{ .url = webpageUrl, @@ -1643,13 +1692,15 @@ void Account::readDraftsWithCursorsLegacy( editData.text.size()); const auto topicRootId = MsgId(); + const auto monoforumPeerId = PeerId(); auto map = base::flat_map<Data::DraftKey, std::unique_ptr<Data::Draft>>(); if (!msgData.text.isEmpty() || msgReplyTo) { map.emplace( - Data::DraftKey::Local(topicRootId), + Data::DraftKey::Local(topicRootId, monoforumPeerId), std::make_unique<Data::Draft>( msgData, FullReplyTo{ FullMsgId(peerId, MsgId(msgReplyTo)) }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft{ .removed = (msgPreviewCancelled == 1), @@ -1657,10 +1708,11 @@ void Account::readDraftsWithCursorsLegacy( } if (editMsgId) { map.emplace( - Data::DraftKey::LocalEdit(topicRootId), + Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId), std::make_unique<Data::Draft>( editData, FullReplyTo{ FullMsgId(peerId, editMsgId) }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft{ .removed = (editPreviewCancelled == 1), diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index c096039941..4003e10de5 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -53,6 +53,7 @@ enum class StartResult : uchar; struct MessageDraft { FullReplyTo reply; + SuggestPostOptions suggest; TextWithTags textWithTags; Data::WebPageDraft webpage; }; diff --git a/Telegram/SourceFiles/storage/storage_shared_media.cpp b/Telegram/SourceFiles/storage/storage_shared_media.cpp index 0326ad3566..89ce8f6d3b 100644 --- a/Telegram/SourceFiles/storage/storage_shared_media.cpp +++ b/Telegram/SourceFiles/storage/storage_shared_media.cpp @@ -27,6 +27,7 @@ auto SharedMedia::enforceLists(Key key) return SharedMediaSliceUpdate( key.peerId, key.topicRootId, + key.monoforumPeerId, type, update); }) | rpl::start_to_stream(_sliceUpdated, _lifetime); @@ -50,10 +51,20 @@ void SharedMedia::add(SharedMediaAddNew &&query) { if (topicIt != end(_lists)) { addByIt(topicIt); } + const auto monoforumPeerIt = query.monoforumPeerId + ? _lists.find({ query.peerId, MsgId(), query.monoforumPeerId }) + : end(_lists); + if (monoforumPeerIt != end(_lists)) { + addByIt(monoforumPeerIt); + } } void SharedMedia::add(SharedMediaAddExisting &&query) { - auto peerIt = enforceLists({ query.peerId, query.topicRootId }); + auto peerIt = enforceLists({ + query.peerId, + query.topicRootId, + query.monoforumPeerId, + }); for (auto index = 0; index != kSharedMediaTypeCount; ++index) { auto type = static_cast<SharedMediaType>(index); if (query.types.test(type)) { @@ -67,7 +78,11 @@ void SharedMedia::add(SharedMediaAddExisting &&query) { void SharedMedia::add(SharedMediaAddSlice &&query) { Expects(IsValidSharedMediaType(query.type)); - auto peerIt = enforceLists({ query.peerId, query.topicRootId }); + auto peerIt = enforceLists({ + query.peerId, + query.topicRootId, + query.monoforumPeerId, + }); auto index = static_cast<int>(query.type); peerIt->second[index].addSlice( std::move(query.messageIds), @@ -90,11 +105,17 @@ void SharedMedia::remove(SharedMediaRemoveOne &&query) { } void SharedMedia::remove(SharedMediaRemoveAll &&query) { - auto peerIt = _lists.lower_bound({ query.peerId, query.topicRootId }); + auto peerIt = _lists.lower_bound({ + query.peerId, + query.topicRootId, + query.monoforumPeerId, + }); while (peerIt != end(_lists) && peerIt->first.peerId == query.peerId && (!query.topicRootId - || peerIt->first.topicRootId == query.topicRootId)) { + || peerIt->first.topicRootId == query.topicRootId) + && (!query.monoforumPeerId + || peerIt->first.monoforumPeerId == query.monoforumPeerId)) { for (auto index = 0; index != kSharedMediaTypeCount; ++index) { auto type = static_cast<SharedMediaType>(index); if (query.types.test(type)) { @@ -118,13 +139,17 @@ void SharedMedia::invalidate(SharedMediaInvalidateBottom &&query) { } void SharedMedia::unload(SharedMediaUnloadThread &&query) { - _lists.erase({ query.peerId, query.topicRootId }); + _lists.erase({ query.peerId, query.topicRootId, query.monoforumPeerId }); } rpl::producer<SharedMediaResult> SharedMedia::query(SharedMediaQuery &&query) const { Expects(IsValidSharedMediaType(query.key.type)); - auto peerIt = _lists.find({ query.key.peerId, query.key.topicRootId }); + auto peerIt = _lists.find({ + query.key.peerId, + query.key.topicRootId, + query.key.monoforumPeerId, + }); if (peerIt != _lists.end()) { auto index = static_cast<int>(query.key.type); return peerIt->second[index].query(SparseIdsListQuery( @@ -141,7 +166,11 @@ rpl::producer<SharedMediaResult> SharedMedia::query(SharedMediaQuery &&query) co SharedMediaResult SharedMedia::snapshot(const SharedMediaQuery &query) const { Expects(IsValidSharedMediaType(query.key.type)); - auto peerIt = _lists.find({ query.key.peerId, query.key.topicRootId }); + auto peerIt = _lists.find({ + query.key.peerId, + query.key.topicRootId, + query.key.monoforumPeerId, + }); if (peerIt != _lists.end()) { auto index = static_cast<int>(query.key.type); return peerIt->second[index].snapshot(SparseIdsListQuery( @@ -155,7 +184,11 @@ SharedMediaResult SharedMedia::snapshot(const SharedMediaQuery &query) const { bool SharedMedia::empty(const SharedMediaKey &key) const { Expects(IsValidSharedMediaType(key.type)); - auto peerIt = _lists.find({ key.peerId, key.topicRootId }); + auto peerIt = _lists.find({ + key.peerId, + key.topicRootId, + key.monoforumPeerId, + }); if (peerIt != _lists.end()) { auto index = static_cast<int>(key.type); return peerIt->second[index].empty(); diff --git a/Telegram/SourceFiles/storage/storage_shared_media.h b/Telegram/SourceFiles/storage/storage_shared_media.h index 697bd284c3..09f5cce406 100644 --- a/Telegram/SourceFiles/storage/storage_shared_media.h +++ b/Telegram/SourceFiles/storage/storage_shared_media.h @@ -42,16 +42,19 @@ struct SharedMediaAddNew { SharedMediaAddNew( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaTypesMask types, MsgId messageId) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , messageId(messageId) , types(types) { } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; MsgId messageId = 0; SharedMediaTypesMask types; @@ -61,11 +64,13 @@ struct SharedMediaAddExisting { SharedMediaAddExisting( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaTypesMask types, MsgId messageId, MsgRange noSkipRange) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , messageId(messageId) , noSkipRange(noSkipRange) , types(types) { @@ -73,6 +78,7 @@ struct SharedMediaAddExisting { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; MsgId messageId = 0; MsgRange noSkipRange; SharedMediaTypesMask types; @@ -83,12 +89,14 @@ struct SharedMediaAddSlice { SharedMediaAddSlice( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, std::vector<MsgId> &&messageIds, MsgRange noSkipRange, std::optional<int> count = std::nullopt) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , messageIds(std::move(messageIds)) , noSkipRange(noSkipRange) , type(type) @@ -97,6 +105,7 @@ struct SharedMediaAddSlice { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; std::vector<MsgId> messageIds; MsgRange noSkipRange; SharedMediaType type = SharedMediaType::kCount; @@ -135,9 +144,18 @@ struct SharedMediaRemoveAll { , topicRootId(topicRootId) , types(types) { } + SharedMediaRemoveAll( + PeerId peerId, + PeerId monoforumPeerId, + SharedMediaTypesMask types = SharedMediaTypesMask::All()) + : peerId(peerId) + , monoforumPeerId(monoforumPeerId) + , types(types) { + } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaTypesMask types; }; @@ -154,10 +172,12 @@ struct SharedMediaKey { SharedMediaKey( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, MsgId messageId) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , type(type) , messageId(messageId) { } @@ -168,6 +188,7 @@ struct SharedMediaKey { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaType type = SharedMediaType::kCount; MsgId messageId = 0; @@ -195,16 +216,19 @@ struct SharedMediaSliceUpdate { SharedMediaSliceUpdate( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, const SparseIdsSliceUpdate &data) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , type(type) , data(data) { } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaType type = SharedMediaType::kCount; SparseIdsSliceUpdate data; }; @@ -212,13 +236,16 @@ struct SharedMediaSliceUpdate { struct SharedMediaUnloadThread { SharedMediaUnloadThread( PeerId peerId, - MsgId topicRootId) + MsgId topicRootId, + PeerId monoforumPeerId) : peerId(peerId) - , topicRootId(topicRootId) { + , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) { } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; }; class SharedMedia { @@ -245,6 +272,7 @@ private: struct Key { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; friend inline constexpr auto operator<=>(Key, Key) = default; }; diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 892bda6311..1119d19e2c 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -54,6 +54,7 @@ constexpr auto kOccupyFor = TimeId(60); constexpr auto kReoccupyEach = 30 * crl::time(1000); constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4; constexpr auto kTopicRootId = MsgId(0); +constexpr auto kMonoforumPeerId = PeerId(0); class EditInfoBox : public Ui::BoxContent { public: @@ -165,6 +166,7 @@ Data::Draft OccupiedDraft(const QString &normalizedName) { + ";n:" + normalizedName }, FullReplyTo(), + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft() }; @@ -183,7 +185,7 @@ uint32 ParseOccupationTag(History *history) { if (!TrackHistoryOccupation(history)) { return 0; } - const auto draft = history->cloudDraft(kTopicRootId); + const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId); if (!draft) { return 0; } @@ -209,7 +211,7 @@ QString ParseOccupationName(History *history) { if (!TrackHistoryOccupation(history)) { return QString(); } - const auto draft = history->cloudDraft(kTopicRootId); + const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId); if (!draft) { return QString(); } @@ -235,7 +237,7 @@ TimeId OccupiedBySomeoneTill(History *history) { if (!TrackHistoryOccupation(history)) { return 0; } - const auto draft = history->cloudDraft(kTopicRootId); + const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId); if (!draft) { return 0; } @@ -353,7 +355,7 @@ void Helper::updateOccupiedHistory( not_null<Window::SessionController*> controller, History *history) { if (isOccupiedByMe(_occupiedHistory)) { - _occupiedHistory->clearCloudDraft(kTopicRootId); + _occupiedHistory->clearCloudDraft(kTopicRootId, kMonoforumPeerId); _session->api().saveDraftToCloudDelayed(_occupiedHistory); } _occupiedHistory = history; @@ -377,7 +379,10 @@ void Helper::occupyInDraft() { && !isOccupiedBySomeone(_occupiedHistory) && !_supportName.isEmpty()) { const auto draft = OccupiedDraft(_supportNameNormalized); - _occupiedHistory->createCloudDraft(kTopicRootId, &draft); + _occupiedHistory->createCloudDraft( + kTopicRootId, + kMonoforumPeerId, + &draft); _session->api().saveDraftToCloudDelayed(_occupiedHistory); _reoccupyTimer.callEach(kReoccupyEach); } @@ -386,7 +391,10 @@ void Helper::occupyInDraft() { void Helper::reoccupy() { if (isOccupiedByMe(_occupiedHistory)) { const auto draft = OccupiedDraft(_supportNameNormalized); - _occupiedHistory->createCloudDraft(kTopicRootId, &draft); + _occupiedHistory->createCloudDraft( + kTopicRootId, + kMonoforumPeerId, + &draft); _session->api().saveDraftToCloudDelayed(_occupiedHistory); } } diff --git a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp index 1155258ac1..36508cb35b 100644 --- a/Telegram/SourceFiles/ui/boxes/calendar_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/calendar_box.cpp @@ -27,6 +27,11 @@ constexpr auto kDaysInWeek = 7; constexpr auto kTooltipDelay = crl::time(350); constexpr auto kJumpDelay = 2 * crl::time(350); +// QDate -> 0..6 +[[nodiscard]] int DayOfWeekIndex(const QDate &date, int firstDayOfWeek) { + return (kDaysInWeek + date.dayOfWeek() - firstDayOfWeek) % kDaysInWeek; +} + } // namespace class CalendarBox::Context { @@ -77,8 +82,8 @@ public: [[nodiscard]] rpl::producer<QDate> monthValue() const { return _month.value(); } - [[nodiscard]] int firstDayShift() const { - return _firstDayShift; + [[nodiscard]] int firstDayOfWeek() const { + return _firstDayOfWeek; } [[nodiscard]] QDate dateFromIndex(int index) const; @@ -102,14 +107,17 @@ private: }; void applyMonth(const QDate &month, bool forced = false); - static int DaysShiftForMonth(QDate month, QDate min, int firstDayShift); + static int DaysShiftForMonth( + const QDate &month, + QDate min, + int firstDayOfWeek); static int RowsCountForMonth( - QDate month, + const QDate &month, QDate min, QDate max, - int firstDayShift); + int firstDayOfWeek); - const int _firstDayShift = 0; + const int _firstDayOfWeek = 0; bool _allowsSelection = false; rpl::variable<QDate> _month; @@ -134,8 +142,7 @@ private: }; CalendarBox::Context::Context(QDate month, QDate highlighted) -: _firstDayShift(static_cast<int>(QLocale().firstDayOfWeek()) - - static_cast<int>(Qt::Monday)) +: _firstDayOfWeek(static_cast<int>(QLocale().firstDayOfWeek())) // 1..7 , _highlighted(highlighted) { showMonth(month); } @@ -169,8 +176,8 @@ bool CalendarBox::Context::showsMonthOf(QDate date) const { void CalendarBox::Context::applyMonth(const QDate &month, bool forced) { const auto was = _month.current(); _daysCount = month.daysInMonth(); - _daysShift = DaysShiftForMonth(month, _min, _firstDayShift); - _rowsCount = RowsCountForMonth(month, _min, _max, _firstDayShift); + _daysShift = DaysShiftForMonth(month, _min, _firstDayOfWeek); + _rowsCount = RowsCountForMonth(month, _min, _max, _firstDayOfWeek); _highlightedIndex = month.daysTo(_highlighted); _minDayIndex = _min.isNull() ? INT_MIN : month.daysTo(_min); _maxDayIndex = _max.isNull() ? INT_MAX : month.daysTo(_max); @@ -210,36 +217,38 @@ void CalendarBox::Context::skipMonth(int skip) { } int CalendarBox::Context::DaysShiftForMonth( - QDate month, + const QDate &month, QDate min, - int firstDayShift) { + int firstDayOfWeek) { Expects(!month.isNull()); constexpr auto kMaxRows = 6; const auto inMonthIndex = month.day() - 1; - const auto inWeekIndex = month.dayOfWeek() - 1; + const auto inWeekIndex = DayOfWeekIndex(month, firstDayOfWeek); const auto from = ((kMaxRows * kDaysInWeek) + inWeekIndex - inMonthIndex) % kDaysInWeek; if (min.isNull()) { min = month.addYears(-1); } else if (min >= month) { - return from - firstDayShift; + return from; } if (min.day() != 1) { min = QDate(min.year(), min.month(), 1); } - const auto add = min.daysTo(month) - inWeekIndex + (min.dayOfWeek() - 1); - return from + add - firstDayShift; + const auto add = min.daysTo(month) + - inWeekIndex + + DayOfWeekIndex(min, firstDayOfWeek); + return from + add; } int CalendarBox::Context::RowsCountForMonth( - QDate month, + const QDate &month, QDate min, QDate max, - int firstDayShift) { + int firstDayOfWeek) { Expects(!month.isNull()); - const auto daysShift = DaysShiftForMonth(month, min, firstDayShift); + const auto daysShift = DaysShiftForMonth(month, min, firstDayOfWeek); const auto daysCount = month.daysInMonth(); const auto cellsCount = daysShift + daysCount; auto result = (cellsCount / kDaysInWeek); @@ -256,7 +265,7 @@ int CalendarBox::Context::RowsCountForMonth( max = QDate(max.year(), max.month(), 1); } max = max.addMonths(1); - max = max.addDays(1 - max.dayOfWeek()); + max = max.addDays(-DayOfWeekIndex(max, firstDayOfWeek)); const auto cellsFull = daysShift + (month.day() - 1) + month.daysTo(max); return cellsFull / kDaysInWeek; } @@ -549,8 +558,6 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { index += fromRow * kDaysInWeek; const auto innerSkipLeft = (_st.cellSize.width() - _st.cellInner) / 2; const auto innerSkipTop = (_st.cellSize.height() - _st.cellInner) / 2; - const auto fromCol = _context->firstDayShift(); - const auto toCol = fromCol + kDaysInWeek; for (auto row = fromRow; row != tillRow; ++row, y += rowHeight) { auto x = rowsLeft(); const auto fromIndex = index; @@ -576,7 +583,7 @@ void CalendarBox::Inner::paintRows(QPainter &p, QRect clip) { (_st.cellInner / 2.) + st::lineWidth); p.setBrush(Qt::NoBrush); } - for (auto col = fromCol; col != toCol; ++col, ++index, x += _st.cellSize.width()) { + for (auto col = 0; col != kDaysInWeek; ++col, ++index, x += _st.cellSize.width()) { const auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.cellSize.height()); const auto selected = (index >= selectedMin) && (index <= selectedMax); const auto grayedOut = !selected && (index < 0 || index >= daysCount); @@ -843,14 +850,14 @@ void CalendarBox::Title::paintDayNames(Painter &p, QRect clip) { if (!myrtlrect(x, y, _st.cellSize.width() * kDaysInWeek, _st.daysHeight).intersects(clip)) { return; } - const auto from = _context->firstDayShift(); + const auto from = _context->firstDayOfWeek(); const auto to = from + kDaysInWeek; for (auto i = from; i != to; ++i, x += _st.cellSize.width()) { auto rect = myrtlrect(x, y, _st.cellSize.width(), _st.daysHeight); if (!rect.intersects(clip)) { continue; } - p.drawText(rect, langDayOfWeek((i % 7) + 1), style::al_top); + p.drawText(rect, langDayOfWeek(((i - 1) % 7) + 1), style::al_top); } } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 23e630236c..604c931019 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -49,6 +49,7 @@ msgReplyBarSize: size(2px, 36px); msgReplyBarSkip: 10px; msgServicePadding: margins(12px, 3px, 12px, 4px); msgServiceMargin: margins(10px, 10px, 10px, 2px); +monoforumBarUserpicSkip: 2px; msgDateSpace: 12px; msgDateDelta: point(2px, 5px); @@ -656,6 +657,9 @@ historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgS historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }}; historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }}; +historyChecklistTaskPadding: margins(32px, 12px, 0px, 12px); +historyChecklistCheckedTop: 4px; + historyViewButtonHeight: 48px; historyViewButtonMargins: margins(10px, 5px, 10px, 10px); historyViewButtonTextStyle: semiboldTextStyle; @@ -1051,9 +1055,32 @@ chatSimilarName: TextStyle(defaultTextStyle) { chatSimilarWidthMax: 424px; chatSimilarSkip: 12px; +chatSuggestWidth: 236px; +chatSuggestInfoWidth: 272px; +chatSuggestInfoTitleMargin: margins(16px, 16px, 16px, 6px); +chatSuggestInfoMiddleMargin: margins(16px, 4px, 16px, 4px); +chatSuggestInfoLastMargin: margins(16px, 4px, 16px, 16px); +chatSuggestTableMiddleMargin: margins(8px, 4px, 8px, 4px); +chatSuggestTableLastMargin: margins(8px, 4px, 8px, 16px); +chatSuggestInfoFullMargin: margins(16px, 16px, 16px, 16px); +chatSuggestAcceptIcon: IconEmoji { + icon: icon{{ "chat/paid_approve", windowFg }}; + padding: margins(0px, -2px, 0px, 0px); +} +chatSuggestDeclineIcon: IconEmoji { + icon: icon{{ "chat/paid_decline", windowFg }}; + padding: margins(0px, -2px, 0px, 0px); +} +chatSuggestChangeIcon: IconEmoji { + icon: icon{{ "chat/paid_edit", windowFg }}; + padding: margins(0px, -2px, 0px, 0px); +} + premiumRequiredWidth: 186px; premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; premiumRequiredCircle: 60px; +directMessagesIcon: icon{{ "chat/large_messages", msgServiceFg }}; +starsPerMessageWidth: 226px; repliesEmptyIcon: icon{{ "chat/large_quickreply", msgServiceFg }}; greetingEmptyIcon: icon{{ "chat/large_greeting", msgServiceFg }}; @@ -1252,3 +1279,135 @@ newPeerUserpicsPadding: margins(0px, 3px, 0px, 0px); newPeerWidth: 320px; swipeBackSize: 150px; + +chatTabsToggle: IconButton(defaultIconButton) { + width: 64px; + height: 36px; + icon: icon {{ "top_bar_profile-flip_horizontal", menuIconFg }}; + iconOver: icon {{ "top_bar_profile-flip_horizontal", menuIconFgOver }}; + ripple: emptyRippleAnimation; +} +chatTabsToggleActive: icon {{ "top_bar_profile-flip_horizontal", windowActiveTextFg }}; +chatTabsScroll: ScrollArea(defaultScrollArea) { + barHidden: true; +} +chatTabsSlider: SettingsSlider(defaultSettingsSlider) { + padding: 0px; + height: 36px; + barTop: 33px; + barSkip: 0px; + barStroke: 6px; + barRadius: 2px; + barFg: transparent; + barSnapToLabel: true; + strictSkip: 18px; + labelTop: 9px; + labelStyle: semiboldTextStyle; + labelFg: windowSubTextFg; + labelFgActive: lightButtonFg; + rippleBottomSkip: 1px; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + ripple: defaultRippleAnimation; +} + +ChatTabsOutline { + radius: pixels; + stroke: pixels; + fg: color; + skip: pixels; +} + +ChatTabsVertical { + barStroke: pixels; + barRadius: pixels; + barFg: color; + nameStyle: TextStyle; + nameWidth: pixels; + nameTop: pixels; + nameFg: color; + nameFgActive: color; + userpicTop: pixels; + userpicSize: pixels; + baseHeight: pixels; + width: pixels; + ripple: RippleAnimation; + rippleBg: color; + rippleBgActive: color; + duration: int; +} + +chatTabsVertical: ChatTabsVertical { + barStroke: 8px; + barRadius: 4px; + barFg: sliderBgActive; + nameStyle: TextStyle(defaultTextStyle) { + font: font(10px); + } + nameWidth: 54px; + nameTop: 42px; + nameFg: windowSubTextFg; + nameFgActive: lightButtonFg; + userpicTop: 8px; + userpicSize: 28px; + baseHeight: 50px; + width: 64px; + ripple: defaultRippleAnimation; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + duration: 150; +} + +chatTabsOutlineHorizontal: ChatTabsOutline { + stroke: 8px; + radius: 4px; + fg: sliderBgActive; + skip: 8px; +} + +chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) { +} + +suggestPriceTonIconMargins: margins(0px, 2px, 0px, 0px); +suggestPriceBox: Box(defaultBox) { + buttonPadding: margins(22px, 22px, 22px, 22px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + style: semiboldTextStyle; + } + shadowIgnoreTopSkip: true; +} +suggestPriceEstimate: FlatLabel(defaultFlatLabel) { + textFg: windowSubTextFg; +} +suggestPriceEstimateTop: 12px; +tonInput: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 7px, 0px, 7px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(0px, 0px, 0px, 0px); + placeholderScale: 0.; + placeholderFont: boxTextFont; + + heightMin: 34px; + heightMax: 100px; +} +starsFieldIconPosition: point(0px, 10px); +tonFieldIconSize: 16px; +tonFieldIconPosition: point(2px, 9px); + +suggestBarTonIconSize: 14px; +suggestBarTonIconMargins: margins(0px, 3px, 0px, 0px); + +lowTonIconPadding: margins(12px, 20px, 12px, 0px); +lowTonTitlePadding: margins(0px, 12px, 0px, 12px); +lowTonTextPadding: margins(0px, 0px, 0px, 8px); +lowTonText: FlatLabel(defaultFlatLabel) { + minWidth: 100px; + align: align(top); +} diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index 3c904cd6be..36a03c66ee 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ffmpeg/ffmpeg_bytes_io_wrap.h" #include "ffmpeg/ffmpeg_utility.h" #include "media/audio/media_audio_capture.h" +#include "ui/controls/round_video_recorder_data.h" #include "ui/image/image_prepare.h" #include "ui/arc_angles.h" #include "ui/dynamic_image.h" @@ -38,6 +39,13 @@ constexpr auto kMinithumbsInRow = 16; constexpr auto kFadeDuration = crl::time(150); constexpr auto kSkipFrames = 8; constexpr auto kMinScale = 0.7; +constexpr auto &kPlainLogoFrames = RoundVideoData::kLogoFrames; +constexpr auto kLogoSize = RoundVideoData::kLogoSize; +constexpr auto kLogoXShift = -10; +constexpr auto kLogoYShift = 10; +constexpr auto kOverlayOpacity = 0.1; +constexpr auto kOverlayOpaque = 1. - kOverlayOpacity; +constexpr auto kOverlayUVOpaque = 128 * kOverlayOpaque; using namespace FFmpeg; @@ -49,6 +57,115 @@ using namespace FFmpeg; return inner * style::DevicePixelRatio(); } +[[nodiscard]] QImage CircularTextImage( + const QString &text, + int width, + int height, + int radius, + float64 startAngle = 0.0, + float64 endAngle = 360.0, + const QColor &textColor = Qt::black, + const QColor &bgColor = Qt::white, + const QFont &font = QFont(), + bool reverseDirection = false) { + auto image = QImage(width, height, QImage::Format_ARGB32); + image.fill(bgColor); + + auto painter = QPainter(&image); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(textColor); + painter.setFont(font); + + auto center = QPoint(width / 2, height / 2); + painter.translate(center); + + if (endAngle < startAngle) { + std::swap(startAngle, endAngle); + } + + const auto startRad = float64(startAngle - 90) * M_PI / 180.0; + const auto endRad = float64(endAngle - 90) * M_PI / 180.0; + const auto angleRange = float64(endRad) - float64(startRad); + + const auto &metrics = QFontMetrics(font); + + for (auto i = 0; i < text.length(); ++i) { + const auto ratio = (text.length() <= 1) + ? 0.5 + : reverseDirection + ? 1.0 - static_cast<float64>(i) / (text.length() - 1) + : static_cast<float64>(i) / (text.length() - 1); + + const auto angle = startRad + ratio * angleRange; + + const auto x = radius * std::cos(angle); + const auto y = radius * std::sin(angle); + + const auto degrees = (angle * 180.0 / M_PI) - 90; + painter.save(); + painter.translate(x, y); + painter.rotate(degrees); + const auto offset = (i == text.length() - 1) ? 2. : 0.; + painter.drawText( + -metrics.horizontalAdvance(text[i]) / 2 + offset, + metrics.ascent() / 2, + QString(text[i])); + painter.restore(); + } + + return image; +} + +using PrecomputedLogo = std::array<std::array<float, kLogoSize>, kLogoSize>; +[[nodiscard]] const std::vector<PrecomputedLogo> &PrecomputedLogos() { + static std::vector<PrecomputedLogo> precomputedLogos; + + if (!precomputedLogos.empty()) { + return precomputedLogos; + } + constexpr auto kAntialiasRadius = 0.4; + precomputedLogos.resize(kPlainLogoFrames.size()); + const auto antialiasFactor = 1.0 + / (2. * kAntialiasRadius * kAntialiasRadius); + + for (auto index = size_t(0); index < kPlainLogoFrames.size(); ++index) { + uint8_t logoFrame[kLogoSize][kLogoSize] = {{ 0 }}; + RoundVideoData::DecompressLogoRLEFrame( + kPlainLogoFrames[index], + logoFrame); + + for (auto y = 0; y < kLogoSize; ++y) { + for (auto x = 0; x < kLogoSize; ++x) { + auto blendedValue = 0.; + auto weightSum = 0.; + + const auto minY = std::max(0, y - 1); + const auto maxY = std::min(kLogoSize - 1, y + 1); + const auto minX = std::max(0, x - 1); + const auto maxX = std::min(kLogoSize - 1, x + 1); + + for (auto sampleY = minY; sampleY <= maxY; ++sampleY) { + const auto dy = sampleY - y; + for (auto sampleX = minX; sampleX <= maxX; ++sampleX) { + const auto dx = sampleX - x; + const auto distanceSq = dx * dx + dy * dy; + const auto weight + = std::exp(-distanceSq * antialiasFactor); + + blendedValue += logoFrame[sampleY][sampleX] * weight; + weightSum += weight; + } + } + + precomputedLogos[index][y][x] = (weightSum > 0) + ? (blendedValue / weightSum) + : 0; + } + } + } + return precomputedLogos; +} + } // namespace class RoundVideoRecorder::Private final { @@ -77,6 +194,7 @@ private: void initEncoding(); void initCircleMask(); + void initCircularTextImage(); void initMinithumbsCanvas(); void maybeSaveMinithumb( not_null<AVFrame*> frame, @@ -102,7 +220,7 @@ private: void updateResultDuration(int64 pts, AVRational timeBase); void mirrorYUV420P(not_null<AVFrame*> frame); - void cutCircleFromYUV420P(not_null<AVFrame*> frame); + void drawLogoOnYUV420P(not_null<AVFrame*> frame); [[nodiscard]] RoundVideoResult appendToPrevious(RoundVideoResult video); [[nodiscard]] static FormatPointer OpenInputContext( @@ -158,6 +276,9 @@ private: ReadBytesWrap _forConcat1, _forConcat2; + uint8_t _logoFrameCounter = 0; + QImage _circularTextImage; + std::vector<bool> _circleMask; // Always nice to use vector<bool>! :D base::ConcurrentTimer _timeoutTimer; @@ -178,6 +299,7 @@ RoundVideoRecorder::Private::Private( , _timeoutTimer(_weak, [=] { timeout(); }) { initEncoding(); initCircleMask(); + initCircularTextImage(); initMinithumbsCanvas(); _timeoutTimer.callOnce(kInitTimeout); @@ -673,7 +795,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame( _videoFrame->linesize); mirrorYUV420P(_videoFrame.get()); - cutCircleFromYUV420P(_videoFrame.get()); + drawLogoOnYUV420P(_videoFrame.get()); _videoFrame->pts = mcstimestamp - _videoFirstTimestamp; maybeSaveMinithumb(_videoFrame.get(), frame, crop); @@ -747,6 +869,23 @@ void RoundVideoRecorder::Private::initCircleMask() { } } +void RoundVideoRecorder::Private::initCircularTextImage() { + constexpr auto kCircularTextRadius = kSide / 2 + 17; + constexpr auto kCircularTextStartAngle = 125; + constexpr auto kCircularTextEndAngle = 145; + _circularTextImage = CircularTextImage( + u"Telegram"_q.toUpper(), + kSide, + kSide, + kCircularTextRadius, + kCircularTextStartAngle, + kCircularTextEndAngle, + Qt::white, + Qt::transparent, + st::roundVideoFont, + true); +} + void RoundVideoRecorder::Private::initMinithumbsCanvas() { const auto width = kMinithumbsInRow * _minithumbSize; const auto seconds = (kMaxDuration + 999) / 1000; @@ -772,45 +911,74 @@ void RoundVideoRecorder::Private::mirrorYUV420P(not_null<AVFrame*> frame) { } } -void RoundVideoRecorder::Private::cutCircleFromYUV420P( +void RoundVideoRecorder::Private::drawLogoOnYUV420P( not_null<AVFrame*> frame) { const auto width = frame->width; const auto height = frame->height; - auto yMaskIndex = 0; + const auto logoBottom = height - kLogoSize + kLogoYShift; + const auto logoStartX = kLogoXShift; + const auto logoEndX = logoStartX + kLogoSize; + const auto logoStartY = logoBottom; + const auto logoEndY = logoBottom + kLogoSize; + + const auto ¤tLogo = PrecomputedLogos()[_logoFrameCounter]; + _logoFrameCounter = (_logoFrameCounter + 1) % kPlainLogoFrames.size(); + auto yData = frame->data[0]; const auto ySkip = frame->linesize[0] - width; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + + const auto uvWidth = width / 2; + auto uData = frame->data[1]; + auto vData = frame->data[2]; + const auto uvSkip = frame->linesize[1] - uvWidth; + auto yMaskIndex = 0; + + for (auto y = 0; y < height; ++y) { + for (auto x = 0; x < width; ++x) { if (_circleMask[yMaskIndex]) { - *yData = 255; + *yData = static_cast<uint8_t>(*yData * kOverlayOpacity + + 16 * kOverlayOpaque); } + + if ((x >= logoStartX && x < logoEndX) + && (y >= logoStartY && y < logoEndY)) { + const auto logoX = x - kLogoXShift; + const auto logoY = y - logoBottom; + + const auto blendedValue = currentLogo[logoX][logoY]; + if (blendedValue > 0) { + const auto logoFactor = blendedValue / 255.0f; + *yData = static_cast<uint8_t>(*yData * (1 - logoFactor) + + 255 * logoFactor); + } + } + + const auto textAlpha = qAlpha(_circularTextImage.pixel(x, y)) + / 255.; + *yData = std::min( + *yData + static_cast<uint8_t>(textAlpha * 100), + 255); + ++yData; ++yMaskIndex; } yData += ySkip; - } - const auto whalf = width / 2; - const auto hhalf = height / 2; - - auto uvMaskIndex = 0; - auto uData = frame->data[1]; - auto vData = frame->data[2]; - const auto uSkip = frame->linesize[1] - whalf; - for (auto y = 0; y != hhalf; ++y) { - for (auto x = 0; x != whalf; ++x) { - if (_circleMask[uvMaskIndex]) { - *uData = 128; - *vData = 128; + if (y % 2 == 0) { + for (auto x = 0; x < uvWidth; ++x) { + if (_circleMask[(y * width) + (x * 2)]) { + *uData = static_cast<uint8_t>(*uData * kOverlayOpacity + + kOverlayUVOpaque); + *vData = static_cast<uint8_t>(*vData * kOverlayOpacity + + kOverlayUVOpaque); + } + ++uData; + ++vData; } - ++uData; - ++vData; - uvMaskIndex += 2; + uData += uvSkip; + vData += uvSkip; } - uData += uSkip; - vData += uSkip; - uvMaskIndex += width; } } diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder_data.h b/Telegram/SourceFiles/ui/controls/round_video_recorder_data.h new file mode 100644 index 0000000000..8a5496f7e6 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder_data.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 + +namespace RoundVideoData { + +constexpr auto kLogoSize = 80; + +struct LogoRLENode final { + uint16_t count; + uint8_t value; +}; +using LogoRLEFrame = std::vector<LogoRLENode>; + +void DecompressLogoRLEFrame( + const std::vector<LogoRLENode> &rleFrame, + uint8_t outFrame[kLogoSize][kLogoSize]) { + auto pos = size_t(0); + for (const auto &node : rleFrame) { + for (auto i = uint16_t(0); i < node.count; ++i) { + if (pos >= kLogoSize * kLogoSize) { + break; + } + const auto y = int(pos / kLogoSize); + const auto x = int(pos % kLogoSize); + outFrame[y][x] = node.value; + pos++; + } + } +} + +const auto kLogoFrames = std::array<LogoRLEFrame, 27>{ { + + LogoRLEFrame{ { 997, 0 }, { 1, 9 }, { 1, 4 }, { 77, 0 }, { 1, 1 }, { 1, 27 }, { 78, 0 }, { 1, 32 }, { 1, 11 }, { 77, 0 }, { 1, 14 }, { 1, 44 }, { 77, 0 }, { 1, 2 }, { 1, 54 }, { 1, 17 }, { 77, 0 }, { 1, 33 }, { 1, 47 }, { 77, 0 }, { 1, 9 }, { 1, 61 }, { 1, 13 }, { 77, 0 }, { 1, 45 }, { 1, 40 }, { 77, 0 }, { 1, 19 }, { 1, 60 }, { 1, 6 }, { 76, 0 }, { 1, 2 }, { 1, 54 }, { 1, 31 }, { 77, 0 }, { 1, 27 }, { 1, 54 }, { 1, 2 }, { 76, 0 }, { 1, 1 }, { 1, 55 }, { 1, 15 }, { 9, 0 }, { 1, 53 }, { 1, 196 }, { 1, 65 }, { 65, 0 }, { 1, 24 }, { 1, 35 }, { 9, 0 }, { 1, 32 }, { 1, 238 }, { 1, 255 }, { 1, 115 }, { 65, 0 }, { 1, 41 }, { 1, 2 }, { 8, 0 }, { 1, 4 }, { 1, 199 }, { 2, 255 }, { 1, 121 }, { 20, 0 }, { 1, 2 }, { 1, 4 }, { 42, 0 }, { 1, 16 }, { 1, 9 }, { 9, 0 }, { 1, 135 }, { 3, 255 }, { 1, 98 }, { 19, 0 }, { 2, 19 }, { 43, 0 }, { 1, 7 }, { 9, 0 }, { 1, 68 }, { 1, 253 }, { 3, 255 }, { 1, 65 }, { 17, 0 }, { 1, 8 }, { 1, 43 }, { 1, 20 }, { 53, 0 }, { 1, 22 }, { 1, 231 }, { 3, 255 }, { 1, 217 }, { 1, 3 }, { 16, 0 }, { 1, 30 }, { 1, 56 }, { 1, 15 }, { 53, 0 }, { 1, 1 }, { 1, 183 }, { 3, 255 }, { 1, 250 }, { 1, 67 }, { 15, 0 }, { 1, 8 }, { 1, 48 }, { 1, 52 }, { 1, 10 }, { 54, 0 }, { 1, 115 }, { 4, 255 }, { 1, 91 }, { 15, 0 }, { 1, 23 }, { 1, 59 }, { 1, 39 }, { 1, 2 }, { 54, 0 }, { 1, 52 }, { 1, 250 }, { 3, 255 }, { 1, 114 }, { 14, 0 }, { 1, 4 }, { 1, 41 }, { 1, 60 }, { 1, 22 }, { 55, 0 }, { 1, 13 }, { 1, 219 }, { 3, 255 }, { 1, 139 }, { 14, 0 }, { 1, 10 }, { 1, 54 }, { 1, 51 }, { 1, 9 }, { 56, 0 }, { 1, 164 }, { 3, 255 }, { 1, 162 }, { 1, 1 }, { 1, 0 }, { 1, 102 }, { 1, 222 }, { 1, 236 }, { 1, 85 }, { 8, 0 }, { 1, 15 }, { 1, 54 }, { 1, 27 }, { 57, 0 }, { 1, 95 }, { 3, 255 }, { 1, 183 }, { 1, 5 }, { 1, 2 }, { 1, 152 }, { 3, 255 }, { 1, 164 }, { 7, 0 }, { 1, 22 }, { 1, 36 }, { 1, 4 }, { 57, 0 }, { 1, 38 }, { 1, 243 }, { 2, 255 }, { 1, 201 }, { 1, 11 }, { 1, 7 }, { 1, 172 }, { 4, 255 }, { 1, 188 }, { 6, 0 }, { 1, 16 }, { 1, 12 }, { 58, 0 }, { 1, 6 }, { 1, 206 }, { 2, 255 }, { 1, 217 }, { 1, 20 }, { 1, 14 }, { 1, 190 }, { 5, 255 }, { 1, 213 }, { 5, 0 }, { 1, 1 }, { 60, 0 }, { 1, 144 }, { 2, 255 }, { 1, 230 }, { 1, 32 }, { 1, 23 }, { 1, 207 }, { 6, 255 }, { 1, 238 }, { 65, 0 }, { 1, 76 }, { 2, 255 }, { 1, 240 }, { 1, 46 }, { 1, 35 }, { 1, 220 }, { 8, 255 }, { 1, 8 }, { 63, 0 }, { 1, 26 }, { 1, 235 }, { 1, 255 }, { 1, 248 }, { 1, 64 }, { 1, 48 }, { 1, 232 }, { 9, 255 }, { 1, 31 }, { 62, 0 }, { 1, 2 }, { 1, 191 }, { 1, 255 }, { 1, 253 }, { 1, 83 }, { 1, 64 }, { 1, 241 }, { 10, 255 }, { 1, 56 }, { 62, 0 }, { 1, 124 }, { 2, 255 }, { 1, 106 }, { 1, 83 }, { 1, 248 }, { 11, 255 }, { 1, 81 }, { 61, 0 }, { 1, 59 }, { 1, 252 }, { 1, 255 }, { 1, 130 }, { 1, 103 }, { 1, 253 }, { 12, 255 }, { 1, 105 }, { 60, 0 }, { 1, 17 }, { 1, 225 }, { 1, 255 }, { 1, 226 }, { 1, 143 }, { 14, 255 }, { 1, 103 }, { 60, 0 }, { 1, 173 }, { 17, 255 }, { 1, 223 }, { 1, 12 }, { 59, 0 }, { 1, 105 }, { 17, 255 }, { 1, 206 }, { 1, 30 }, { 59, 0 }, { 1, 44 }, { 1, 247 }, { 15, 255 }, { 1, 204 }, { 1, 95 }, { 1, 4 }, { 59, 0 }, { 1, 9 }, { 1, 213 }, { 12, 255 }, { 1, 252 }, { 1, 191 }, { 1, 110 }, { 1, 30 }, { 62, 0 }, { 1, 154 }, { 10, 255 }, { 1, 247 }, { 1, 178 }, { 1, 97 }, { 1, 20 }, { 64, 0 }, { 1, 85 }, { 8, 255 }, { 1, 240 }, { 1, 165 }, { 1, 84 }, { 1, 12 }, { 66, 0 }, { 1, 32 }, { 1, 240 }, { 5, 255 }, { 1, 232 }, { 1, 153 }, { 1, 71 }, { 1, 6 }, { 68, 0 }, { 1, 1 }, { 1, 196 }, { 3, 255 }, { 1, 221 }, { 1, 140 }, { 1, 59 }, { 1, 2 }, { 71, 0 }, { 1, 49 }, { 1, 253 }, { 1, 207 }, { 1, 127 }, { 1, 46 }, { 76, 0 }, { 1, 6 }, { 508, 0 }, { 1, 6 }, { 1, 1 }, { 76, 0 }, { 1, 7 }, { 1, 29 }, { 1, 9 }, { 75, 0 }, { 1, 6 }, { 1, 36 }, { 1, 41 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 31 }, { 1, 59 }, { 1, 29 }, { 75, 0 }, { 1, 17 }, { 1, 53 }, { 1, 51 }, { 1, 13 }, { 74, 0 }, { 1, 6 }, { 1, 42 }, { 1, 60 }, { 1, 29 }, { 75, 0 }, { 1, 25 }, { 1, 60 }, { 1, 47 }, { 1, 9 }, { 74, 0 }, { 1, 3 }, { 1, 41 }, { 1, 52 }, { 1, 21 }, { 75, 0 }, { 1, 12 }, { 1, 43 }, { 1, 21 }, { 76, 0 }, { 1, 15 }, { 1, 18 }, { 77, 0 }, { 1, 1 }, { 843, 0 } }, + LogoRLEFrame{ { 600, 0 }, { 1, 1 }, { 79, 0 }, { 1, 7 }, { 78, 0 }, { 1, 14 }, { 1, 1 }, { 77, 0 }, { 1, 7 }, { 1, 16 }, { 77, 0 }, { 1, 1 }, { 1, 26 }, { 1, 3 }, { 77, 0 }, { 1, 18 }, { 1, 19 }, { 77, 0 }, { 1, 6 }, { 1, 29 }, { 1, 3 }, { 77, 0 }, { 1, 24 }, { 1, 16 }, { 77, 0 }, { 1, 12 }, { 1, 28 }, { 1, 1 }, { 76, 0 }, { 1, 2 }, { 1, 28 }, { 1, 12 }, { 77, 0 }, { 1, 17 }, { 1, 25 }, { 77, 0 }, { 1, 3 }, { 1, 29 }, { 1, 7 }, { 77, 0 }, { 1, 16 }, { 1, 17 }, { 77, 0 }, { 1, 1 }, { 1, 24 }, { 1, 1 }, { 77, 0 }, { 1, 13 }, { 1, 5 }, { 77, 0 }, { 1, 1 }, { 1, 8 }, { 78, 0 }, { 1, 1 }, { 90, 0 }, { 1, 14 }, { 1, 167 }, { 1, 127 }, { 76, 0 }, { 1, 2 }, { 1, 189 }, { 1, 255 }, { 1, 196 }, { 76, 0 }, { 1, 124 }, { 2, 255 }, { 1, 204 }, { 75, 0 }, { 1, 58 }, { 1, 252 }, { 2, 255 }, { 1, 185 }, { 74, 0 }, { 1, 16 }, { 1, 224 }, { 3, 255 }, { 1, 155 }, { 74, 0 }, { 1, 171 }, { 4, 255 }, { 1, 64 }, { 73, 0 }, { 1, 101 }, { 4, 255 }, { 1, 167 }, { 73, 0 }, { 1, 42 }, { 1, 245 }, { 3, 255 }, { 1, 192 }, { 1, 7 }, { 72, 0 }, { 1, 8 }, { 1, 210 }, { 3, 255 }, { 1, 209 }, { 1, 15 }, { 57, 0 }, { 1, 5 }, { 15, 0 }, { 1, 148 }, { 3, 255 }, { 1, 223 }, { 1, 25 }, { 57, 0 }, { 1, 14 }, { 1, 1 }, { 14, 0 }, { 1, 80 }, { 3, 255 }, { 1, 235 }, { 1, 38 }, { 1, 0 }, { 1, 26 }, { 1, 159 }, { 1, 214 }, { 1, 142 }, { 52, 0 }, { 1, 4 }, { 1, 20 }, { 14, 0 }, { 1, 28 }, { 1, 237 }, { 2, 255 }, { 1, 245 }, { 1, 54 }, { 1, 0 }, { 1, 48 }, { 1, 231 }, { 3, 255 }, { 1, 7 }, { 51, 0 }, { 1, 29 }, { 1, 5 }, { 13, 0 }, { 1, 3 }, { 1, 193 }, { 2, 255 }, { 1, 251 }, { 1, 74 }, { 1, 0 }, { 1, 64 }, { 1, 241 }, { 4, 255 }, { 1, 32 }, { 50, 0 }, { 1, 14 }, { 1, 30 }, { 14, 0 }, { 1, 126 }, { 3, 255 }, { 1, 95 }, { 1, 0 }, { 1, 83 }, { 1, 248 }, { 5, 255 }, { 1, 59 }, { 49, 0 }, { 1, 1 }, { 1, 37 }, { 1, 12 }, { 13, 0 }, { 1, 60 }, { 1, 252 }, { 2, 255 }, { 1, 119 }, { 1, 0 }, { 1, 105 }, { 1, 253 }, { 6, 255 }, { 1, 85 }, { 49, 0 }, { 1, 21 }, { 1, 32 }, { 13, 0 }, { 1, 17 }, { 1, 225 }, { 2, 255 }, { 1, 144 }, { 1, 0 }, { 1, 128 }, { 8, 255 }, { 1, 111 }, { 48, 0 }, { 1, 3 }, { 1, 40 }, { 1, 11 }, { 13, 0 }, { 1, 173 }, { 2, 255 }, { 1, 167 }, { 1, 4 }, { 1, 152 }, { 9, 255 }, { 1, 138 }, { 48, 0 }, { 1, 25 }, { 1, 31 }, { 13, 0 }, { 1, 104 }, { 2, 255 }, { 1, 188 }, { 1, 14 }, { 1, 174 }, { 10, 255 }, { 1, 164 }, { 47, 0 }, { 1, 6 }, { 1, 41 }, { 1, 9 }, { 12, 0 }, { 1, 43 }, { 1, 246 }, { 1, 255 }, { 1, 206 }, { 1, 29 }, { 1, 192 }, { 11, 255 }, { 1, 191 }, { 47, 0 }, { 1, 28 }, { 1, 29 }, { 12, 0 }, { 1, 9 }, { 1, 211 }, { 1, 255 }, { 1, 221 }, { 1, 49 }, { 1, 209 }, { 12, 255 }, { 1, 218 }, { 46, 0 }, { 1, 5 }, { 1, 40 }, { 1, 5 }, { 12, 0 }, { 1, 151 }, { 2, 255 }, { 1, 125 }, { 1, 223 }, { 13, 255 }, { 1, 223 }, { 46, 0 }, { 1, 22 }, { 1, 20 }, { 12, 0 }, { 1, 82 }, { 18, 255 }, { 1, 121 }, { 45, 0 }, { 1, 1 }, { 1, 32 }, { 12, 0 }, { 1, 30 }, { 1, 238 }, { 17, 255 }, { 1, 139 }, { 1, 2 }, { 45, 0 }, { 1, 15 }, { 1, 8 }, { 11, 0 }, { 1, 3 }, { 1, 195 }, { 15, 255 }, { 1, 253 }, { 1, 186 }, { 1, 63 }, { 47, 0 }, { 1, 11 }, { 12, 0 }, { 1, 128 }, { 13, 255 }, { 1, 247 }, { 1, 179 }, { 1, 99 }, { 1, 22 }, { 48, 0 }, { 1, 1 }, { 12, 0 }, { 1, 62 }, { 1, 252 }, { 10, 255 }, { 1, 238 }, { 1, 163 }, { 1, 83 }, { 1, 12 }, { 63, 0 }, { 1, 18 }, { 1, 227 }, { 8, 255 }, { 1, 226 }, { 1, 146 }, { 1, 66 }, { 1, 4 }, { 66, 0 }, { 1, 175 }, { 6, 255 }, { 1, 209 }, { 1, 130 }, { 1, 50 }, { 69, 0 }, { 1, 100 }, { 3, 255 }, { 1, 252 }, { 1, 193 }, { 1, 113 }, { 1, 34 }, { 72, 0 }, { 1, 216 }, { 1, 246 }, { 1, 176 }, { 1, 97 }, { 1, 21 }, { 75, 0 }, { 1, 17 }, { 1, 4 }, { 349, 0 }, { 1, 18 }, { 1, 13 }, { 76, 0 }, { 1, 19 }, { 1, 42 }, { 1, 11 }, { 75, 0 }, { 1, 15 }, { 1, 49 }, { 1, 43 }, { 1, 3 }, { 74, 0 }, { 1, 5 }, { 1, 40 }, { 1, 59 }, { 1, 27 }, { 75, 0 }, { 1, 26 }, { 1, 59 }, { 1, 45 }, { 1, 7 }, { 74, 0 }, { 1, 12 }, { 1, 50 }, { 1, 57 }, { 1, 22 }, { 75, 0 }, { 1, 27 }, { 1, 60 }, { 1, 38 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 42 }, { 1, 38 }, { 1, 8 }, { 75, 0 }, { 1, 12 }, { 1, 29 }, { 1, 7 }, { 76, 0 }, { 1, 2 }, { 1, 5 }, { 999, 0 } }, + LogoRLEFrame{ { 2042, 0 }, { 1, 2 }, { 1, 144 }, { 1, 220 }, { 1, 25 }, { 1, 4 }, { 75, 0 }, { 1, 132 }, { 2, 255 }, { 1, 64 }, { 75, 0 }, { 1, 62 }, { 1, 253 }, { 2, 255 }, { 1, 55 }, { 74, 0 }, { 1, 16 }, { 1, 225 }, { 3, 255 }, { 1, 43 }, { 74, 0 }, { 1, 169 }, { 4, 255 }, { 1, 19 }, { 55, 0 }, { 2, 5 }, { 16, 0 }, { 1, 96 }, { 4, 255 }, { 1, 201 }, { 56, 0 }, { 1, 26 }, { 16, 0 }, { 1, 35 }, { 1, 243 }, { 3, 255 }, { 1, 252 }, { 1, 66 }, { 55, 0 }, { 1, 20 }, { 1, 21 }, { 15, 0 }, { 1, 4 }, { 1, 201 }, { 4, 255 }, { 1, 105 }, { 55, 0 }, { 1, 3 }, { 1, 50 }, { 1, 1 }, { 15, 0 }, { 1, 132 }, { 4, 255 }, { 1, 134 }, { 56, 0 }, { 1, 37 }, { 1, 32 }, { 15, 0 }, { 1, 62 }, { 1, 253 }, { 3, 255 }, { 1, 160 }, { 56, 0 }, { 1, 12 }, { 1, 60 }, { 1, 6 }, { 14, 0 }, { 1, 16 }, { 1, 225 }, { 3, 255 }, { 1, 183 }, { 1, 4 }, { 1, 0 }, { 1, 28 }, { 1, 132 }, { 1, 143 }, { 1, 31 }, { 51, 0 }, { 1, 46 }, { 1, 34 }, { 15, 0 }, { 1, 169 }, { 3, 255 }, { 1, 202 }, { 1, 11 }, { 1, 0 }, { 1, 73 }, { 1, 243 }, { 2, 255 }, { 1, 165 }, { 50, 0 }, { 1, 17 }, { 1, 59 }, { 1, 5 }, { 14, 0 }, { 1, 95 }, { 3, 255 }, { 1, 219 }, { 1, 21 }, { 1, 0 }, { 1, 94 }, { 1, 251 }, { 3, 255 }, { 1, 197 }, { 50, 0 }, { 1, 51 }, { 1, 32 }, { 14, 0 }, { 1, 35 }, { 1, 242 }, { 2, 255 }, { 1, 232 }, { 1, 34 }, { 1, 0 }, { 1, 118 }, { 5, 255 }, { 1, 229 }, { 49, 0 }, { 1, 24 }, { 1, 58 }, { 1, 3 }, { 13, 0 }, { 1, 4 }, { 1, 200 }, { 2, 255 }, { 1, 243 }, { 1, 51 }, { 1, 1 }, { 1, 143 }, { 7, 255 }, { 1, 7 }, { 48, 0 }, { 1, 55 }, { 1, 29 }, { 14, 0 }, { 1, 132 }, { 2, 255 }, { 1, 250 }, { 1, 70 }, { 1, 6 }, { 1, 167 }, { 8, 255 }, { 1, 38 }, { 47, 0 }, { 1, 20 }, { 1, 52 }, { 1, 1 }, { 13, 0 }, { 1, 61 }, { 1, 253 }, { 2, 255 }, { 1, 93 }, { 1, 13 }, { 1, 187 }, { 9, 255 }, { 1, 70 }, { 47, 0 }, { 1, 47 }, { 1, 15 }, { 13, 0 }, { 1, 16 }, { 1, 225 }, { 2, 255 }, { 1, 118 }, { 1, 23 }, { 1, 205 }, { 10, 255 }, { 1, 102 }, { 46, 0 }, { 1, 11 }, { 1, 37 }, { 14, 0 }, { 1, 168 }, { 2, 255 }, { 1, 145 }, { 1, 36 }, { 1, 221 }, { 11, 255 }, { 1, 134 }, { 46, 0 }, { 1, 29 }, { 1, 2 }, { 13, 0 }, { 1, 95 }, { 2, 255 }, { 1, 170 }, { 1, 53 }, { 1, 233 }, { 12, 255 }, { 1, 166 }, { 45, 0 }, { 1, 3 }, { 1, 10 }, { 13, 0 }, { 1, 35 }, { 1, 242 }, { 1, 255 }, { 1, 226 }, { 1, 89 }, { 1, 243 }, { 13, 255 }, { 1, 187 }, { 59, 0 }, { 1, 4 }, { 1, 200 }, { 18, 255 }, { 1, 136 }, { 59, 0 }, { 1, 131 }, { 18, 255 }, { 1, 190 }, { 1, 9 }, { 58, 0 }, { 1, 61 }, { 1, 253 }, { 16, 255 }, { 1, 237 }, { 1, 125 }, { 1, 4 }, { 58, 0 }, { 1, 16 }, { 1, 225 }, { 14, 255 }, { 1, 235 }, { 1, 161 }, { 1, 84 }, { 1, 11 }, { 60, 0 }, { 1, 168 }, { 12, 255 }, { 1, 213 }, { 1, 136 }, { 1, 59 }, { 1, 2 }, { 62, 0 }, { 1, 94 }, { 9, 255 }, { 1, 250 }, { 1, 188 }, { 1, 111 }, { 1, 34 }, { 65, 0 }, { 1, 34 }, { 1, 242 }, { 6, 255 }, { 1, 236 }, { 1, 162 }, { 1, 85 }, { 1, 14 }, { 67, 0 }, { 1, 1 }, { 1, 198 }, { 4, 255 }, { 1, 214 }, { 1, 137 }, { 1, 60 }, { 1, 3 }, { 70, 0 }, { 1, 67 }, { 1, 255 }, { 1, 251 }, { 1, 189 }, { 1, 112 }, { 1, 35 }, { 74, 0 }, { 1, 9 }, { 1, 57 }, { 1, 14 }, { 111, 0 }, { 1, 6 }, { 1, 11 }, { 76, 0 }, { 1, 5 }, { 1, 32 }, { 1, 20 }, { 75, 0 }, { 1, 4 }, { 1, 33 }, { 1, 51 }, { 1, 11 }, { 75, 0 }, { 1, 24 }, { 1, 57 }, { 1, 42 }, { 1, 3 }, { 74, 0 }, { 1, 11 }, { 1, 48 }, { 1, 56 }, { 1, 20 }, { 74, 0 }, { 1, 3 }, { 1, 35 }, { 1, 61 }, { 1, 38 }, { 1, 3 }, { 74, 0 }, { 1, 14 }, { 1, 55 }, { 1, 53 }, { 1, 15 }, { 75, 0 }, { 1, 28 }, { 1, 52 }, { 1, 24 }, { 75, 0 }, { 1, 4 }, { 1, 34 }, { 1, 23 }, { 76, 0 }, { 1, 4 }, { 1, 15 }, { 1156, 0 } }, + LogoRLEFrame{ { 1807, 0 }, { 2, 3 }, { 77, 0 }, { 1, 5 }, { 1, 34 }, { 74, 0 }, { 1, 15 }, { 1, 88 }, { 1, 9 }, { 1, 7 }, { 1, 52 }, { 1, 10 }, { 73, 0 }, { 1, 14 }, { 1, 210 }, { 1, 255 }, { 1, 94 }, { 1, 51 }, { 1, 33 }, { 57, 0 }, { 1, 2 }, { 16, 0 }, { 1, 160 }, { 2, 255 }, { 1, 151 }, { 1, 47 }, { 57, 0 }, { 1, 13 }, { 1, 1 }, { 15, 0 }, { 1, 80 }, { 3, 255 }, { 1, 178 }, { 1, 5 }, { 56, 0 }, { 1, 2 }, { 1, 27 }, { 15, 0 }, { 1, 21 }, { 1, 233 }, { 3, 255 }, { 1, 157 }, { 57, 0 }, { 1, 35 }, { 1, 9 }, { 15, 0 }, { 1, 175 }, { 4, 255 }, { 1, 143 }, { 56, 0 }, { 1, 13 }, { 1, 45 }, { 15, 0 }, { 1, 94 }, { 5, 255 }, { 1, 73 }, { 55, 0 }, { 1, 1 }, { 1, 52 }, { 1, 20 }, { 14, 0 }, { 1, 29 }, { 1, 240 }, { 4, 255 }, { 1, 199 }, { 1, 1 }, { 55, 0 }, { 1, 26 }, { 1, 52 }, { 14, 0 }, { 1, 1 }, { 1, 189 }, { 4, 255 }, { 1, 227 }, { 1, 24 }, { 55, 0 }, { 1, 3 }, { 1, 57 }, { 1, 20 }, { 14, 0 }, { 1, 109 }, { 4, 255 }, { 1, 241 }, { 1, 46 }, { 56, 0 }, { 1, 32 }, { 1, 50 }, { 14, 0 }, { 1, 39 }, { 1, 246 }, { 3, 255 }, { 1, 250 }, { 1, 68 }, { 56, 0 }, { 1, 6 }, { 1, 60 }, { 1, 18 }, { 13, 0 }, { 1, 3 }, { 1, 201 }, { 4, 255 }, { 1, 93 }, { 2, 0 }, { 1, 24 }, { 1, 97 }, { 1, 81 }, { 1, 1 }, { 51, 0 }, { 1, 38 }, { 1, 48 }, { 14, 0 }, { 1, 124 }, { 4, 255 }, { 1, 122 }, { 2, 0 }, { 1, 83 }, { 1, 240 }, { 2, 255 }, { 1, 113 }, { 50, 0 }, { 1, 7 }, { 1, 61 }, { 1, 14 }, { 13, 0 }, { 1, 50 }, { 1, 250 }, { 3, 255 }, { 1, 153 }, { 2, 0 }, { 1, 109 }, { 1, 253 }, { 3, 255 }, { 1, 164 }, { 50, 0 }, { 1, 33 }, { 1, 37 }, { 13, 0 }, { 1, 7 }, { 1, 212 }, { 3, 255 }, { 1, 180 }, { 1, 3 }, { 1, 1 }, { 1, 136 }, { 5, 255 }, { 1, 206 }, { 49, 0 }, { 1, 2 }, { 1, 52 }, { 1, 4 }, { 13, 0 }, { 1, 139 }, { 3, 255 }, { 1, 203 }, { 1, 11 }, { 1, 5 }, { 1, 163 }, { 6, 255 }, { 1, 247 }, { 1, 2 }, { 48, 0 }, { 1, 24 }, { 1, 20 }, { 13, 0 }, { 1, 61 }, { 1, 253 }, { 2, 255 }, { 1, 221 }, { 1, 22 }, { 1, 12 }, { 1, 186 }, { 8, 255 }, { 1, 35 }, { 48, 0 }, { 1, 27 }, { 13, 0 }, { 1, 12 }, { 1, 222 }, { 2, 255 }, { 1, 236 }, { 1, 38 }, { 1, 23 }, { 1, 205 }, { 9, 255 }, { 1, 78 }, { 47, 0 }, { 1, 6 }, { 1, 2 }, { 13, 0 }, { 1, 154 }, { 2, 255 }, { 1, 247 }, { 1, 58 }, { 1, 37 }, { 1, 222 }, { 10, 255 }, { 1, 120 }, { 61, 0 }, { 1, 74 }, { 2, 255 }, { 1, 253 }, { 1, 82 }, { 1, 55 }, { 1, 235 }, { 11, 255 }, { 1, 162 }, { 60, 0 }, { 1, 19 }, { 1, 230 }, { 2, 255 }, { 1, 110 }, { 1, 75 }, { 1, 245 }, { 12, 255 }, { 1, 205 }, { 60, 0 }, { 1, 169 }, { 2, 255 }, { 1, 180 }, { 1, 103 }, { 1, 251 }, { 13, 255 }, { 1, 242 }, { 59, 0 }, { 1, 89 }, { 19, 255 }, { 1, 232 }, { 58, 0 }, { 1, 26 }, { 1, 238 }, { 19, 255 }, { 1, 87 }, { 58, 0 }, { 1, 183 }, { 18, 255 }, { 1, 233 }, { 1, 78 }, { 58, 0 }, { 1, 104 }, { 16, 255 }, { 1, 243 }, { 1, 176 }, { 1, 98 }, { 1, 8 }, { 58, 0 }, { 1, 35 }, { 1, 244 }, { 13, 255 }, { 1, 209 }, { 1, 136 }, { 1, 64 }, { 1, 6 }, { 60, 0 }, { 1, 2 }, { 1, 196 }, { 10, 255 }, { 1, 238 }, { 1, 169 }, { 1, 97 }, { 1, 26 }, { 64, 0 }, { 1, 119 }, { 7, 255 }, { 1, 253 }, { 1, 201 }, { 1, 129 }, { 1, 57 }, { 1, 3 }, { 66, 0 }, { 1, 39 }, { 1, 249 }, { 4, 255 }, { 1, 232 }, { 1, 161 }, { 1, 89 }, { 1, 20 }, { 70, 0 }, { 1, 139 }, { 1, 255 }, { 1, 251 }, { 1, 193 }, { 1, 121 }, { 1, 49 }, { 1, 1 }, { 31, 0 }, { 1, 7 }, { 41, 0 }, { 1, 23 }, { 1, 62 }, { 1, 14 }, { 33, 0 }, { 1, 18 }, { 1, 24 }, { 1, 1 }, { 75, 0 }, { 1, 16 }, { 1, 46 }, { 1, 22 }, { 75, 0 }, { 1, 10 }, { 1, 46 }, { 1, 53 }, { 1, 11 }, { 74, 0 }, { 1, 2 }, { 1, 33 }, { 1, 60 }, { 1, 35 }, { 1, 2 }, { 74, 0 }, { 1, 19 }, { 1, 55 }, { 1, 51 }, { 1, 13 }, { 74, 0 }, { 1, 5 }, { 1, 43 }, { 1, 61 }, { 1, 30 }, { 1, 1 }, { 74, 0 }, { 1, 15 }, { 1, 55 }, { 1, 40 }, { 1, 9 }, { 75, 0 }, { 1, 29 }, { 1, 38 }, { 1, 9 }, { 75, 0 }, { 1, 4 }, { 1, 22 }, { 1, 8 }, { 77, 0 }, { 1, 2 }, { 1234, 0 } }, + LogoRLEFrame{ { 1649, 0 }, { 1, 23 }, { 1, 2 }, { 77, 0 }, { 1, 33 }, { 1, 24 }, { 77, 0 }, { 1, 33 }, { 1, 48 }, { 56, 0 }, { 2, 2 }, { 15, 0 }, { 1, 17 }, { 3, 0 }, { 1, 24 }, { 1, 59 }, { 1, 9 }, { 56, 0 }, { 1, 18 }, { 15, 0 }, { 1, 152 }, { 1, 255 }, { 1, 98 }, { 1, 0 }, { 1, 15 }, { 1, 61 }, { 1, 19 }, { 56, 0 }, { 1, 12 }, { 1, 20 }, { 14, 0 }, { 1, 98 }, { 2, 255 }, { 1, 169 }, { 1, 5 }, { 1, 58 }, { 1, 32 }, { 57, 0 }, { 1, 45 }, { 1, 1 }, { 13, 0 }, { 1, 24 }, { 1, 239 }, { 2, 255 }, { 1, 216 }, { 1, 43 }, { 1, 39 }, { 57, 0 }, { 1, 29 }, { 1, 33 }, { 14, 0 }, { 1, 173 }, { 3, 255 }, { 1, 250 }, { 1, 36 }, { 57, 0 }, { 1, 8 }, { 1, 59 }, { 1, 8 }, { 13, 0 }, { 1, 82 }, { 5, 255 }, { 1, 2 }, { 57, 0 }, { 1, 41 }, { 1, 39 }, { 13, 0 }, { 1, 16 }, { 1, 230 }, { 5, 255 }, { 1, 7 }, { 56, 0 }, { 1, 12 }, { 1, 61 }, { 1, 7 }, { 13, 0 }, { 1, 156 }, { 5, 255 }, { 1, 198 }, { 57, 0 }, { 1, 47 }, { 1, 36 }, { 13, 0 }, { 1, 66 }, { 6, 255 }, { 1, 85 }, { 56, 0 }, { 1, 18 }, { 1, 60 }, { 1, 6 }, { 12, 0 }, { 1, 10 }, { 1, 221 }, { 5, 255 }, { 1, 152 }, { 57, 0 }, { 1, 52 }, { 1, 34 }, { 13, 0 }, { 1, 140 }, { 5, 255 }, { 1, 192 }, { 1, 6 }, { 56, 0 }, { 1, 19 }, { 1, 57 }, { 1, 4 }, { 12, 0 }, { 1, 52 }, { 1, 252 }, { 4, 255 }, { 1, 216 }, { 1, 17 }, { 57, 0 }, { 1, 46 }, { 1, 22 }, { 12, 0 }, { 1, 5 }, { 1, 209 }, { 4, 255 }, { 1, 235 }, { 1, 35 }, { 3, 0 }, { 1, 23 }, { 1, 7 }, { 52, 0 }, { 1, 10 }, { 1, 46 }, { 13, 0 }, { 1, 123 }, { 4, 255 }, { 1, 247 }, { 1, 58 }, { 2, 0 }, { 1, 48 }, { 1, 206 }, { 1, 255 }, { 1, 246 }, { 1, 77 }, { 51, 0 }, { 1, 33 }, { 1, 7 }, { 12, 0 }, { 1, 40 }, { 1, 248 }, { 4, 255 }, { 1, 87 }, { 2, 0 }, { 1, 73 }, { 1, 244 }, { 3, 255 }, { 1, 173 }, { 50, 0 }, { 1, 4 }, { 1, 18 }, { 12, 0 }, { 1, 1 }, { 1, 196 }, { 4, 255 }, { 1, 121 }, { 2, 0 }, { 1, 100 }, { 1, 251 }, { 4, 255 }, { 1, 233 }, { 50, 0 }, { 1, 5 }, { 13, 0 }, { 1, 107 }, { 4, 255 }, { 1, 157 }, { 2, 0 }, { 1, 131 }, { 7, 255 }, { 1, 36 }, { 62, 0 }, { 1, 29 }, { 1, 242 }, { 3, 255 }, { 1, 188 }, { 2, 5 }, { 1, 160 }, { 8, 255 }, { 1, 95 }, { 62, 0 }, { 1, 181 }, { 3, 255 }, { 1, 213 }, { 1, 15 }, { 1, 14 }, { 1, 187 }, { 9, 255 }, { 1, 154 }, { 61, 0 }, { 1, 90 }, { 3, 255 }, { 1, 232 }, { 1, 32 }, { 1, 27 }, { 1, 209 }, { 10, 255 }, { 1, 214 }, { 60, 0 }, { 1, 20 }, { 1, 235 }, { 2, 255 }, { 1, 246 }, { 1, 54 }, { 1, 44 }, { 1, 227 }, { 12, 255 }, { 1, 18 }, { 59, 0 }, { 1, 165 }, { 3, 255 }, { 1, 82 }, { 1, 65 }, { 1, 240 }, { 13, 255 }, { 1, 76 }, { 58, 0 }, { 1, 74 }, { 3, 255 }, { 1, 166 }, { 1, 94 }, { 1, 249 }, { 14, 255 }, { 1, 134 }, { 57, 0 }, { 1, 13 }, { 1, 226 }, { 20, 255 }, { 1, 165 }, { 57, 0 }, { 1, 148 }, { 21, 255 }, { 1, 89 }, { 56, 0 }, { 1, 60 }, { 21, 255 }, { 1, 137 }, { 56, 0 }, { 1, 7 }, { 1, 215 }, { 18, 255 }, { 1, 252 }, { 1, 185 }, { 1, 68 }, { 57, 0 }, { 1, 132 }, { 15, 255 }, { 1, 253 }, { 1, 205 }, { 1, 140 }, { 1, 75 }, { 1, 14 }, { 58, 0 }, { 1, 46 }, { 1, 250 }, { 12, 255 }, { 1, 209 }, { 1, 144 }, { 1, 79 }, { 1, 17 }, { 61, 0 }, { 1, 3 }, { 1, 203 }, { 9, 255 }, { 1, 212 }, { 1, 148 }, { 1, 83 }, { 1, 20 }, { 26, 0 }, { 1, 1 }, { 38, 0 }, { 1, 105 }, { 6, 255 }, { 1, 216 }, { 1, 151 }, { 1, 86 }, { 1, 22 }, { 28, 0 }, { 1, 5 }, { 1, 21 }, { 1, 4 }, { 38, 0 }, { 1, 183 }, { 2, 255 }, { 1, 220 }, { 1, 155 }, { 1, 90 }, { 1, 26 }, { 30, 0 }, { 1, 3 }, { 1, 32 }, { 1, 33 }, { 1, 1 }, { 39, 0 }, { 1, 33 }, { 1, 74 }, { 1, 29 }, { 32, 0 }, { 1, 1 }, { 1, 29 }, { 1, 56 }, { 1, 22 }, { 75, 0 }, { 1, 17 }, { 1, 53 }, { 1, 50 }, { 1, 10 }, { 74, 0 }, { 1, 6 }, { 1, 42 }, { 1, 60 }, { 1, 28 }, { 75, 0 }, { 1, 28 }, { 1, 59 }, { 1, 45 }, { 1, 8 }, { 74, 0 }, { 1, 6 }, { 1, 46 }, { 1, 55 }, { 1, 22 }, { 75, 0 }, { 1, 16 }, { 1, 49 }, { 1, 24 }, { 1, 1 }, { 75, 0 }, { 2, 23 }, { 77, 0 }, { 1, 8 }, { 1391, 0 } }, + LogoRLEFrame{ { 1411, 0 }, { 1, 8 }, { 1, 2 }, { 77, 0 }, { 1, 11 }, { 1, 33 }, { 77, 0 }, { 1, 13 }, { 1, 55 }, { 1, 5 }, { 55, 0 }, { 1, 6 }, { 20, 0 }, { 1, 7 }, { 1, 57 }, { 1, 24 }, { 56, 0 }, { 1, 21 }, { 19, 0 }, { 1, 3 }, { 1, 51 }, { 1, 39 }, { 56, 0 }, { 1, 27 }, { 1, 9 }, { 19, 0 }, { 1, 44 }, { 1, 50 }, { 1, 1 }, { 55, 0 }, { 1, 7 }, { 1, 43 }, { 15, 0 }, { 1, 8 }, { 3, 0 }, { 1, 26 }, { 1, 57 }, { 1, 7 }, { 56, 0 }, { 1, 45 }, { 1, 20 }, { 13, 0 }, { 1, 15 }, { 1, 200 }, { 1, 249 }, { 1, 54 }, { 1, 0 }, { 1, 7 }, { 1, 52 }, { 1, 9 }, { 56, 0 }, { 1, 21 }, { 1, 55 }, { 14, 0 }, { 1, 158 }, { 2, 255 }, { 1, 150 }, { 1, 0 }, { 1, 36 }, { 1, 7 }, { 56, 0 }, { 1, 1 }, { 1, 54 }, { 1, 24 }, { 13, 0 }, { 1, 55 }, { 3, 255 }, { 1, 216 }, { 1, 13 }, { 1, 5 }, { 57, 0 }, { 1, 27 }, { 1, 53 }, { 13, 0 }, { 1, 2 }, { 1, 203 }, { 4, 255 }, { 1, 25 }, { 57, 0 }, { 1, 3 }, { 1, 58 }, { 1, 22 }, { 13, 0 }, { 1, 100 }, { 5, 255 }, { 1, 60 }, { 57, 0 }, { 1, 33 }, { 1, 51 }, { 13, 0 }, { 1, 17 }, { 1, 235 }, { 5, 255 }, { 1, 88 }, { 56, 0 }, { 1, 6 }, { 1, 60 }, { 1, 19 }, { 13, 0 }, { 1, 147 }, { 6, 255 }, { 1, 109 }, { 56, 0 }, { 1, 32 }, { 1, 45 }, { 13, 0 }, { 1, 46 }, { 1, 252 }, { 6, 255 }, { 1, 54 }, { 55, 0 }, { 1, 1 }, { 1, 55 }, { 1, 8 }, { 13, 0 }, { 1, 193 }, { 6, 255 }, { 1, 211 }, { 1, 1 }, { 55, 0 }, { 1, 23 }, { 1, 29 }, { 13, 0 }, { 1, 90 }, { 6, 255 }, { 1, 251 }, { 1, 50 }, { 56, 0 }, { 1, 36 }, { 13, 0 }, { 1, 12 }, { 1, 229 }, { 6, 255 }, { 1, 106 }, { 56, 0 }, { 1, 11 }, { 1, 7 }, { 13, 0 }, { 1, 137 }, { 6, 255 }, { 1, 148 }, { 57, 0 }, { 1, 2 }, { 13, 0 }, { 1, 38 }, { 1, 249 }, { 5, 255 }, { 1, 187 }, { 1, 4 }, { 71, 0 }, { 1, 183 }, { 5, 255 }, { 1, 217 }, { 1, 16 }, { 2, 0 }, { 1, 4 }, { 1, 109 }, { 1, 172 }, { 1, 145 }, { 1, 26 }, { 64, 0 }, { 1, 79 }, { 5, 255 }, { 1, 238 }, { 1, 37 }, { 2, 0 }, { 1, 22 }, { 1, 199 }, { 3, 255 }, { 1, 194 }, { 63, 0 }, { 1, 8 }, { 1, 222 }, { 4, 255 }, { 1, 251 }, { 1, 67 }, { 2, 0 }, { 1, 41 }, { 1, 222 }, { 4, 255 }, { 1, 253 }, { 1, 22 }, { 62, 0 }, { 1, 126 }, { 5, 255 }, { 1, 106 }, { 2, 0 }, { 1, 67 }, { 1, 240 }, { 6, 255 }, { 1, 100 }, { 61, 0 }, { 1, 31 }, { 1, 246 }, { 4, 255 }, { 1, 148 }, { 2, 0 }, { 1, 99 }, { 1, 250 }, { 7, 255 }, { 1, 181 }, { 61, 0 }, { 1, 173 }, { 4, 255 }, { 1, 187 }, { 1, 4 }, { 1, 1 }, { 1, 136 }, { 9, 255 }, { 1, 249 }, { 1, 13 }, { 59, 0 }, { 1, 69 }, { 4, 255 }, { 1, 217 }, { 1, 16 }, { 1, 9 }, { 1, 171 }, { 11, 255 }, { 1, 87 }, { 58, 0 }, { 1, 5 }, { 1, 215 }, { 3, 255 }, { 1, 238 }, { 1, 37 }, { 1, 22 }, { 1, 200 }, { 12, 255 }, { 1, 168 }, { 58, 0 }, { 1, 115 }, { 3, 255 }, { 1, 251 }, { 1, 67 }, { 1, 42 }, { 1, 223 }, { 13, 255 }, { 1, 242 }, { 1, 7 }, { 56, 0 }, { 1, 24 }, { 1, 242 }, { 3, 255 }, { 1, 173 }, { 1, 72 }, { 1, 240 }, { 15, 255 }, { 1, 74 }, { 56, 0 }, { 1, 162 }, { 22, 255 }, { 1, 144 }, { 55, 0 }, { 1, 59 }, { 23, 255 }, { 1, 164 }, { 54, 0 }, { 1, 2 }, { 1, 206 }, { 22, 255 }, { 1, 251 }, { 1, 54 }, { 54, 0 }, { 1, 105 }, { 22, 255 }, { 1, 236 }, { 1, 69 }, { 54, 0 }, { 1, 19 }, { 1, 237 }, { 19, 255 }, { 1, 224 }, { 1, 168 }, { 1, 98 }, { 1, 9 }, { 16, 0 }, { 1, 12 }, { 1, 4 }, { 37, 0 }, { 1, 151 }, { 15, 255 }, { 1, 243 }, { 1, 190 }, { 1, 134 }, { 1, 79 }, { 1, 24 }, { 18, 0 }, { 1, 15 }, { 1, 34 }, { 1, 7 }, { 37, 0 }, { 1, 49 }, { 1, 253 }, { 10, 255 }, { 1, 253 }, { 1, 211 }, { 1, 155 }, { 1, 100 }, { 1, 45 }, { 1, 2 }, { 20, 0 }, { 1, 13 }, { 1, 46 }, { 1, 37 }, { 1, 1 }, { 38, 0 }, { 1, 179 }, { 7, 255 }, { 1, 232 }, { 1, 177 }, { 1, 121 }, { 1, 66 }, { 1, 13 }, { 4, 0 }, { 1, 2 }, { 1, 8 }, { 17, 0 }, { 1, 5 }, { 1, 39 }, { 1, 59 }, { 1, 23 }, { 40, 0 }, { 1, 223 }, { 2, 255 }, { 1, 248 }, { 1, 198 }, { 1, 142 }, { 1, 87 }, { 1, 32 }, { 8, 0 }, { 1, 9 }, { 1, 20 }, { 17, 0 }, { 1, 26 }, { 1, 59 }, { 1, 43 }, { 1, 6 }, { 41, 0 }, { 1, 39 }, { 1, 88 }, { 1, 53 }, { 1, 6 }, { 11, 0 }, { 1, 21 }, { 1, 25 }, { 16, 0 }, { 1, 13 }, { 1, 50 }, { 1, 56 }, { 1, 20 }, { 56, 0 }, { 1, 5 }, { 1, 32 }, { 1, 26 }, { 16, 0 }, { 1, 32 }, { 1, 62 }, { 1, 38 }, { 1, 3 }, { 56, 0 }, { 1, 10 }, { 1, 38 }, { 1, 24 }, { 15, 0 }, { 1, 6 }, { 1, 47 }, { 1, 41 }, { 1, 11 }, { 57, 0 }, { 1, 17 }, { 1, 40 }, { 1, 18 }, { 15, 0 }, { 1, 17 }, { 1, 35 }, { 1, 10 }, { 58, 0 }, { 1, 24 }, { 1, 40 }, { 1, 12 }, { 15, 0 }, { 1, 11 }, { 1, 9 }, { 58, 0 }, { 1, 1 }, { 1, 31 }, { 1, 37 }, { 1, 8 }, { 75, 0 }, { 1, 2 }, { 1, 32 }, { 1, 31 }, { 1, 4 }, { 75, 0 }, { 1, 2 }, { 1, 31 }, { 1, 20 }, { 76, 0 }, { 1, 2 }, { 1, 25 }, { 1, 9 }, { 76, 0 }, { 1, 1 }, { 1, 11 }, { 1171, 0 } }, + LogoRLEFrame{ { 1174, 0 }, { 1, 1 }, { 78, 0 }, { 1, 29 }, { 77, 0 }, { 1, 1 }, { 1, 43 }, { 1, 17 }, { 55, 0 }, { 1, 11 }, { 21, 0 }, { 1, 43 }, { 1, 42 }, { 55, 0 }, { 1, 7 }, { 1, 18 }, { 20, 0 }, { 1, 33 }, { 1, 54 }, { 1, 4 }, { 55, 0 }, { 1, 38 }, { 1, 1 }, { 19, 0 }, { 1, 24 }, { 1, 60 }, { 1, 12 }, { 55, 0 }, { 1, 21 }, { 1, 33 }, { 19, 0 }, { 1, 11 }, { 1, 61 }, { 1, 23 }, { 55, 0 }, { 1, 4 }, { 1, 57 }, { 1, 8 }, { 19, 0 }, { 1, 50 }, { 1, 29 }, { 56, 0 }, { 1, 36 }, { 1, 42 }, { 19, 0 }, { 1, 29 }, { 1, 25 }, { 56, 0 }, { 1, 9 }, { 1, 61 }, { 1, 10 }, { 13, 0 }, { 1, 44 }, { 1, 19 }, { 3, 0 }, { 1, 9 }, { 1, 20 }, { 57, 0 }, { 1, 42 }, { 1, 40 }, { 13, 0 }, { 1, 125 }, { 1, 255 }, { 1, 232 }, { 1, 19 }, { 2, 0 }, { 1, 5 }, { 57, 0 }, { 1, 14 }, { 1, 61 }, { 1, 9 }, { 12, 0 }, { 1, 38 }, { 1, 251 }, { 2, 255 }, { 1, 107 }, { 60, 0 }, { 1, 48 }, { 1, 38 }, { 13, 0 }, { 1, 170 }, { 3, 255 }, { 1, 191 }, { 59, 0 }, { 1, 17 }, { 1, 60 }, { 1, 7 }, { 12, 0 }, { 1, 51 }, { 4, 255 }, { 1, 252 }, { 1, 22 }, { 58, 0 }, { 1, 45 }, { 1, 30 }, { 13, 0 }, { 1, 186 }, { 5, 255 }, { 1, 85 }, { 57, 0 }, { 1, 9 }, { 1, 52 }, { 1, 1 }, { 12, 0 }, { 1, 66 }, { 6, 255 }, { 1, 132 }, { 57, 0 }, { 1, 35 }, { 1, 14 }, { 13, 0 }, { 1, 202 }, { 6, 255 }, { 1, 180 }, { 56, 0 }, { 1, 3 }, { 1, 29 }, { 13, 0 }, { 1, 83 }, { 7, 255 }, { 1, 210 }, { 56, 0 }, { 1, 14 }, { 13, 0 }, { 1, 3 }, { 1, 216 }, { 7, 255 }, { 1, 167 }, { 70, 0 }, { 1, 99 }, { 8, 255 }, { 1, 86 }, { 69, 0 }, { 1, 8 }, { 1, 227 }, { 7, 255 }, { 1, 192 }, { 70, 0 }, { 1, 116 }, { 7, 255 }, { 1, 235 }, { 1, 31 }, { 69, 0 }, { 1, 15 }, { 1, 237 }, { 6, 255 }, { 1, 251 }, { 1, 66 }, { 70, 0 }, { 1, 132 }, { 7, 255 }, { 1, 112 }, { 70, 0 }, { 1, 24 }, { 1, 244 }, { 6, 255 }, { 1, 163 }, { 4, 0 }, { 1, 7 }, { 1, 45 }, { 1, 27 }, { 64, 0 }, { 1, 149 }, { 6, 255 }, { 1, 205 }, { 1, 9 }, { 3, 0 }, { 1, 81 }, { 1, 231 }, { 2, 255 }, { 1, 173 }, { 1, 2 }, { 61, 0 }, { 1, 35 }, { 1, 250 }, { 5, 255 }, { 1, 234 }, { 1, 30 }, { 2, 0 }, { 1, 1 }, { 1, 129 }, { 5, 255 }, { 1, 76 }, { 61, 0 }, { 1, 165 }, { 5, 255 }, { 1, 251 }, { 1, 64 }, { 2, 0 }, { 1, 11 }, { 1, 173 }, { 6, 255 }, { 1, 184 }, { 60, 0 }, { 1, 47 }, { 6, 255 }, { 1, 110 }, { 2, 0 }, { 1, 31 }, { 1, 208 }, { 8, 255 }, { 1, 37 }, { 59, 0 }, { 1, 181 }, { 5, 255 }, { 1, 161 }, { 2, 0 }, { 1, 60 }, { 1, 234 }, { 9, 255 }, { 1, 143 }, { 58, 0 }, { 1, 62 }, { 5, 255 }, { 1, 204 }, { 1, 8 }, { 1, 0 }, { 1, 99 }, { 1, 249 }, { 10, 255 }, { 1, 240 }, { 1, 11 }, { 57, 0 }, { 1, 198 }, { 4, 255 }, { 1, 233 }, { 1, 29 }, { 1, 3 }, { 1, 145 }, { 13, 255 }, { 1, 102 }, { 56, 0 }, { 1, 78 }, { 4, 255 }, { 1, 251 }, { 1, 63 }, { 1, 17 }, { 1, 186 }, { 14, 255 }, { 1, 210 }, { 55, 0 }, { 1, 2 }, { 1, 212 }, { 4, 255 }, { 1, 209 }, { 1, 49 }, { 1, 218 }, { 16, 255 }, { 1, 61 }, { 54, 0 }, { 1, 95 }, { 24, 255 }, { 1, 169 }, { 53, 0 }, { 1, 7 }, { 1, 224 }, { 24, 255 }, { 1, 250 }, { 1, 12 }, { 14, 0 }, { 1, 3 }, { 1, 2 }, { 36, 0 }, { 1, 111 }, { 25, 255 }, { 1, 237 }, { 1, 8 }, { 12, 0 }, { 1, 3 }, { 1, 26 }, { 1, 13 }, { 36, 0 }, { 1, 13 }, { 1, 234 }, { 25, 255 }, { 1, 109 }, { 11, 0 }, { 1, 2 }, { 1, 29 }, { 1, 45 }, { 1, 7 }, { 37, 0 }, { 1, 128 }, { 24, 255 }, { 1, 222 }, { 1, 92 }, { 11, 0 }, { 1, 24 }, { 1, 57 }, { 1, 37 }, { 1, 1 }, { 37, 0 }, { 1, 21 }, { 1, 243 }, { 19, 255 }, { 1, 240 }, { 1, 195 }, { 1, 150 }, { 1, 105 }, { 1, 57 }, { 1, 1 }, { 10, 0 }, { 1, 11 }, { 1, 48 }, { 1, 55 }, { 1, 18 }, { 39, 0 }, { 1, 144 }, { 14, 255 }, { 1, 250 }, { 1, 211 }, { 1, 183 }, { 1, 139 }, { 1, 74 }, { 1, 29 }, { 14, 0 }, { 1, 3 }, { 1, 35 }, { 1, 61 }, { 1, 36 }, { 1, 3 }, { 39, 0 }, { 1, 2 }, { 1, 239 }, { 9, 255 }, { 1, 225 }, { 1, 180 }, { 1, 134 }, { 1, 89 }, { 1, 44 }, { 1, 23 }, { 1, 59 }, { 1, 31 }, { 16, 0 }, { 1, 18 }, { 1, 56 }, { 1, 52 }, { 1, 14 }, { 41, 0 }, { 1, 3 }, { 1, 231 }, { 3, 255 }, { 1, 240 }, { 1, 195 }, { 1, 149 }, { 1, 104 }, { 1, 59 }, { 1, 15 }, { 4, 0 }, { 1, 31 }, { 1, 61 }, { 1, 22 }, { 15, 0 }, { 1, 1 }, { 1, 34 }, { 1, 56 }, { 1, 27 }, { 1, 1 }, { 43, 0 }, { 1, 30 }, { 1, 92 }, { 1, 74 }, { 1, 29 }, { 8, 0 }, { 1, 2 }, { 1, 42 }, { 1, 58 }, { 1, 15 }, { 15, 0 }, { 1, 7 }, { 1, 42 }, { 1, 26 }, { 1, 1 }, { 56, 0 }, { 1, 2 }, { 1, 47 }, { 1, 52 }, { 1, 9 }, { 15, 0 }, { 1, 12 }, { 1, 21 }, { 1, 1 }, { 57, 0 }, { 1, 2 }, { 1, 47 }, { 1, 38 }, { 1, 2 }, { 16, 0 }, { 1, 1 }, { 58, 0 }, { 1, 3 }, { 1, 44 }, { 1, 21 }, { 76, 0 }, { 1, 3 }, { 1, 27 }, { 1, 5 }, { 77, 0 }, { 1, 4 }, { 1407, 0 } }, + LogoRLEFrame{ { 1015, 0 }, { 1, 6 }, { 1, 1 }, { 54, 0 }, { 1, 1 }, { 22, 0 }, { 1, 9 }, { 1, 14 }, { 55, 0 }, { 1, 14 }, { 21, 0 }, { 1, 10 }, { 1, 26 }, { 1, 1 }, { 54, 0 }, { 1, 19 }, { 1, 9 }, { 20, 0 }, { 1, 7 }, { 1, 29 }, { 1, 8 }, { 54, 0 }, { 1, 3 }, { 1, 39 }, { 20, 0 }, { 1, 3 }, { 1, 28 }, { 1, 14 }, { 55, 0 }, { 1, 38 }, { 1, 20 }, { 19, 0 }, { 1, 1 }, { 1, 25 }, { 1, 21 }, { 55, 0 }, { 1, 15 }, { 1, 56 }, { 20, 0 }, { 1, 17 }, { 1, 24 }, { 1, 1 }, { 55, 0 }, { 1, 50 }, { 1, 28 }, { 19, 0 }, { 1, 6 }, { 1, 23 }, { 1, 1 }, { 55, 0 }, { 1, 22 }, { 1, 56 }, { 1, 2 }, { 19, 0 }, { 1, 17 }, { 1, 1 }, { 55, 0 }, { 1, 1 }, { 1, 55 }, { 1, 26 }, { 19, 0 }, { 1, 6 }, { 57, 0 }, { 1, 29 }, { 1, 55 }, { 1, 1 }, { 76, 0 }, { 1, 4 }, { 1, 58 }, { 1, 23 }, { 11, 0 }, { 1, 53 }, { 1, 218 }, { 1, 196 }, { 1, 20 }, { 62, 0 }, { 1, 31 }, { 1, 51 }, { 11, 0 }, { 1, 2 }, { 1, 217 }, { 2, 255 }, { 1, 155 }, { 61, 0 }, { 1, 1 }, { 1, 56 }, { 1, 14 }, { 11, 0 }, { 1, 87 }, { 3, 255 }, { 1, 243 }, { 1, 12 }, { 60, 0 }, { 1, 22 }, { 1, 37 }, { 12, 0 }, { 1, 207 }, { 4, 255 }, { 1, 99 }, { 60, 0 }, { 1, 42 }, { 1, 4 }, { 11, 0 }, { 1, 73 }, { 5, 255 }, { 1, 198 }, { 59, 0 }, { 1, 13 }, { 1, 15 }, { 12, 0 }, { 1, 194 }, { 6, 255 }, { 1, 20 }, { 58, 0 }, { 1, 10 }, { 12, 0 }, { 1, 59 }, { 7, 255 }, { 1, 82 }, { 71, 0 }, { 1, 180 }, { 7, 255 }, { 1, 146 }, { 70, 0 }, { 1, 47 }, { 8, 255 }, { 1, 206 }, { 70, 0 }, { 1, 167 }, { 8, 255 }, { 1, 198 }, { 69, 0 }, { 1, 35 }, { 1, 252 }, { 8, 255 }, { 1, 162 }, { 69, 0 }, { 1, 153 }, { 9, 255 }, { 1, 53 }, { 68, 0 }, { 1, 25 }, { 1, 248 }, { 8, 255 }, { 1, 167 }, { 69, 0 }, { 1, 139 }, { 8, 255 }, { 1, 218 }, { 1, 14 }, { 68, 0 }, { 1, 17 }, { 1, 243 }, { 7, 255 }, { 1, 246 }, { 1, 46 }, { 69, 0 }, { 1, 126 }, { 8, 255 }, { 1, 95 }, { 69, 0 }, { 1, 11 }, { 1, 236 }, { 7, 255 }, { 1, 154 }, { 5, 0 }, { 1, 20 }, { 1, 50 }, { 1, 15 }, { 62, 0 }, { 1, 112 }, { 7, 255 }, { 1, 204 }, { 1, 7 }, { 3, 0 }, { 1, 8 }, { 1, 144 }, { 1, 252 }, { 1, 255 }, { 1, 253 }, { 1, 148 }, { 60, 0 }, { 1, 6 }, { 1, 227 }, { 6, 255 }, { 1, 238 }, { 1, 33 }, { 3, 0 }, { 1, 31 }, { 1, 204 }, { 5, 255 }, { 1, 83 }, { 59, 0 }, { 1, 98 }, { 7, 255 }, { 1, 76 }, { 3, 0 }, { 1, 69 }, { 1, 236 }, { 6, 255 }, { 1, 215 }, { 1, 3 }, { 57, 0 }, { 1, 2 }, { 1, 217 }, { 6, 255 }, { 1, 133 }, { 2, 0 }, { 1, 1 }, { 1, 121 }, { 1, 253 }, { 8, 255 }, { 1, 97 }, { 57, 0 }, { 1, 85 }, { 6, 255 }, { 1, 189 }, { 1, 3 }, { 1, 0 }, { 1, 14 }, { 1, 175 }, { 10, 255 }, { 1, 225 }, { 1, 7 }, { 56, 0 }, { 1, 205 }, { 5, 255 }, { 1, 229 }, { 1, 22 }, { 1, 0 }, { 1, 42 }, { 1, 217 }, { 12, 255 }, { 1, 111 }, { 55, 0 }, { 1, 71 }, { 5, 255 }, { 1, 251 }, { 1, 59 }, { 1, 0 }, { 1, 86 }, { 1, 243 }, { 13, 255 }, { 1, 234 }, { 1, 12 }, { 54, 0 }, { 1, 192 }, { 5, 255 }, { 1, 150 }, { 1, 4 }, { 1, 141 }, { 16, 255 }, { 1, 125 }, { 53, 0 }, { 1, 58 }, { 6, 255 }, { 1, 228 }, { 1, 202 }, { 17, 255 }, { 1, 241 }, { 1, 19 }, { 52, 0 }, { 1, 179 }, { 26, 255 }, { 1, 140 }, { 51, 0 }, { 1, 45 }, { 27, 255 }, { 1, 245 }, { 1, 13 }, { 50, 0 }, { 1, 165 }, { 28, 255 }, { 1, 25 }, { 49, 0 }, { 1, 34 }, { 1, 252 }, { 27, 255 }, { 1, 179 }, { 50, 0 }, { 1, 151 }, { 27, 255 }, { 1, 170 }, { 1, 11 }, { 49, 0 }, { 1, 24 }, { 1, 248 }, { 21, 255 }, { 1, 248 }, { 1, 214 }, { 1, 178 }, { 1, 142 }, { 1, 105 }, { 1, 31 }, { 51, 0 }, { 1, 111 }, { 15, 255 }, { 1, 248 }, { 1, 219 }, { 1, 176 }, { 1, 139 }, { 1, 103 }, { 1, 67 }, { 1, 31 }, { 1, 2 }, { 56, 0 }, { 1, 127 }, { 8, 255 }, { 1, 245 }, { 1, 209 }, { 1, 173 }, { 1, 137 }, { 1, 101 }, { 1, 66 }, { 1, 69 }, { 1, 29 }, { 63, 0 }, { 1, 24 }, { 1, 184 }, { 1, 227 }, { 1, 207 }, { 1, 171 }, { 1, 134 }, { 1, 98 }, { 1, 62 }, { 1, 26 }, { 4, 0 }, { 1, 3 }, { 1, 36 }, { 1, 12 }, { 76, 0 }, { 1, 1 }, { 1, 15 }, { 1722, 0 } }, + LogoRLEFrame{ { 833, 0 }, { 1, 1 }, { 78, 0 }, { 1, 1 }, { 1, 7 }, { 78, 0 }, { 1, 15 }, { 78, 0 }, { 1, 7 }, { 1, 16 }, { 78, 0 }, { 1, 25 }, { 1, 3 }, { 77, 0 }, { 1, 15 }, { 1, 22 }, { 77, 0 }, { 1, 3 }, { 1, 29 }, { 1, 6 }, { 77, 0 }, { 1, 18 }, { 1, 21 }, { 77, 0 }, { 1, 5 }, { 1, 30 }, { 1, 5 }, { 77, 0 }, { 1, 21 }, { 1, 20 }, { 77, 0 }, { 1, 7 }, { 1, 30 }, { 1, 4 }, { 77, 0 }, { 1, 22 }, { 1, 18 }, { 77, 0 }, { 1, 4 }, { 1, 27 }, { 1, 1 }, { 9, 0 }, { 1, 104 }, { 1, 242 }, { 1, 191 }, { 1, 15 }, { 64, 0 }, { 1, 17 }, { 1, 10 }, { 9, 0 }, { 1, 23 }, { 1, 249 }, { 2, 255 }, { 1, 156 }, { 63, 0 }, { 1, 1 }, { 1, 19 }, { 10, 0 }, { 1, 126 }, { 3, 255 }, { 1, 248 }, { 1, 22 }, { 62, 0 }, { 1, 9 }, { 1, 2 }, { 9, 0 }, { 1, 4 }, { 1, 229 }, { 4, 255 }, { 1, 125 }, { 62, 0 }, { 1, 3 }, { 10, 0 }, { 1, 86 }, { 5, 255 }, { 1, 230 }, { 1, 5 }, { 72, 0 }, { 1, 194 }, { 6, 255 }, { 1, 82 }, { 71, 0 }, { 1, 47 }, { 7, 255 }, { 1, 158 }, { 71, 0 }, { 1, 155 }, { 7, 255 }, { 1, 233 }, { 1, 1 }, { 69, 0 }, { 1, 17 }, { 1, 246 }, { 8, 255 }, { 1, 53 }, { 69, 0 }, { 1, 115 }, { 9, 255 }, { 1, 117 }, { 68, 0 }, { 1, 2 }, { 1, 221 }, { 9, 255 }, { 1, 114 }, { 68, 0 }, { 1, 76 }, { 10, 255 }, { 1, 90 }, { 68, 0 }, { 1, 184 }, { 9, 255 }, { 1, 236 }, { 1, 9 }, { 67, 0 }, { 1, 38 }, { 10, 255 }, { 1, 119 }, { 68, 0 }, { 1, 145 }, { 9, 255 }, { 1, 190 }, { 1, 2 }, { 51, 0 }, { 1, 7 }, { 1, 4 }, { 14, 0 }, { 1, 12 }, { 1, 241 }, { 8, 255 }, { 1, 234 }, { 1, 25 }, { 52, 0 }, { 1, 27 }, { 15, 0 }, { 1, 105 }, { 9, 255 }, { 1, 72 }, { 52, 0 }, { 1, 12 }, { 1, 29 }, { 15, 0 }, { 1, 213 }, { 8, 255 }, { 1, 138 }, { 53, 0 }, { 1, 31 }, { 1, 18 }, { 14, 0 }, { 1, 66 }, { 8, 255 }, { 1, 200 }, { 1, 5 }, { 4, 0 }, { 1, 27 }, { 1, 158 }, { 1, 209 }, { 1, 192 }, { 1, 113 }, { 1, 5 }, { 42, 0 }, { 1, 6 }, { 1, 41 }, { 1, 3 }, { 14, 0 }, { 1, 174 }, { 7, 255 }, { 1, 240 }, { 1, 32 }, { 4, 0 }, { 1, 80 }, { 1, 239 }, { 4, 255 }, { 1, 163 }, { 42, 0 }, { 1, 23 }, { 1, 29 }, { 14, 0 }, { 1, 30 }, { 1, 252 }, { 7, 255 }, { 1, 84 }, { 3, 0 }, { 1, 5 }, { 1, 142 }, { 7, 255 }, { 1, 68 }, { 40, 0 }, { 1, 1 }, { 1, 39 }, { 1, 14 }, { 14, 0 }, { 1, 134 }, { 7, 255 }, { 1, 150 }, { 3, 0 }, { 1, 31 }, { 1, 200 }, { 8, 255 }, { 1, 219 }, { 1, 8 }, { 2, 0 }, { 1, 4 }, { 36, 0 }, { 1, 10 }, { 1, 37 }, { 1, 1 }, { 13, 0 }, { 1, 8 }, { 1, 235 }, { 6, 255 }, { 1, 209 }, { 1, 8 }, { 2, 0 }, { 1, 77 }, { 1, 238 }, { 10, 255 }, { 1, 131 }, { 1, 8 }, { 1, 25 }, { 37, 0 }, { 1, 22 }, { 1, 15 }, { 14, 0 }, { 1, 95 }, { 6, 255 }, { 1, 244 }, { 1, 40 }, { 1, 0 }, { 1, 4 }, { 1, 139 }, { 12, 255 }, { 1, 250 }, { 1, 72 }, { 38, 0 }, { 1, 26 }, { 15, 0 }, { 1, 204 }, { 6, 255 }, { 1, 96 }, { 1, 0 }, { 1, 29 }, { 1, 197 }, { 14, 255 }, { 1, 195 }, { 1, 1 }, { 36, 0 }, { 1, 3 }, { 1, 11 }, { 14, 0 }, { 1, 56 }, { 6, 255 }, { 1, 228 }, { 1, 0 }, { 1, 74 }, { 1, 236 }, { 16, 255 }, { 1, 99 }, { 36, 0 }, { 1, 4 }, { 15, 0 }, { 1, 164 }, { 7, 255 }, { 1, 196 }, { 18, 255 }, { 1, 238 }, { 1, 22 }, { 50, 0 }, { 1, 23 }, { 1, 249 }, { 27, 255 }, { 1, 163 }, { 50, 0 }, { 1, 124 }, { 29, 255 }, { 1, 61 }, { 48, 0 }, { 1, 4 }, { 1, 228 }, { 29, 255 }, { 1, 154 }, { 48, 0 }, { 1, 85 }, { 30, 255 }, { 1, 112 }, { 48, 0 }, { 1, 193 }, { 29, 255 }, { 1, 204 }, { 1, 17 }, { 47, 0 }, { 1, 45 }, { 27, 255 }, { 1, 247 }, { 1, 205 }, { 1, 119 }, { 1, 11 }, { 48, 0 }, { 1, 107 }, { 19, 255 }, { 1, 231 }, { 1, 201 }, { 1, 171 }, { 1, 141 }, { 1, 111 }, { 1, 81 }, { 1, 51 }, { 1, 21 }, { 52, 0 }, { 1, 84 }, { 10, 255 }, { 1, 245 }, { 1, 215 }, { 1, 185 }, { 1, 155 }, { 1, 125 }, { 1, 95 }, { 1, 65 }, { 1, 35 }, { 1, 7 }, { 60, 0 }, { 1, 3 }, { 1, 140 }, { 1, 224 }, { 1, 228 }, { 1, 198 }, { 1, 168 }, { 1, 138 }, { 1, 108 }, { 1, 78 }, { 1, 48 }, { 1, 19 }, { 1805, 0 } }, + LogoRLEFrame{ { 1717, 0 }, { 1, 74 }, { 1, 216 }, { 1, 178 }, { 1, 22 }, { 75, 0 }, { 1, 6 }, { 1, 234 }, { 2, 255 }, { 1, 190 }, { 75, 0 }, { 1, 84 }, { 4, 255 }, { 1, 62 }, { 74, 0 }, { 1, 182 }, { 4, 255 }, { 1, 182 }, { 73, 0 }, { 1, 28 }, { 1, 253 }, { 5, 255 }, { 1, 48 }, { 72, 0 }, { 1, 123 }, { 6, 255 }, { 1, 165 }, { 71, 0 }, { 1, 1 }, { 1, 220 }, { 6, 255 }, { 1, 247 }, { 1, 12 }, { 70, 0 }, { 1, 63 }, { 8, 255 }, { 1, 88 }, { 70, 0 }, { 1, 161 }, { 8, 255 }, { 1, 174 }, { 69, 0 }, { 1, 14 }, { 1, 245 }, { 8, 255 }, { 1, 247 }, { 1, 12 }, { 68, 0 }, { 1, 101 }, { 10, 255 }, { 1, 58 }, { 68, 0 }, { 1, 200 }, { 10, 255 }, { 1, 58 }, { 52, 0 }, { 1, 17 }, { 14, 0 }, { 1, 42 }, { 11, 255 }, { 1, 33 }, { 51, 0 }, { 1, 8 }, { 1, 31 }, { 14, 0 }, { 1, 140 }, { 10, 255 }, { 1, 191 }, { 52, 0 }, { 1, 42 }, { 1, 17 }, { 13, 0 }, { 1, 5 }, { 1, 233 }, { 10, 255 }, { 1, 68 }, { 51, 0 }, { 1, 9 }, { 1, 61 }, { 1, 3 }, { 13, 0 }, { 1, 80 }, { 10, 255 }, { 1, 142 }, { 52, 0 }, { 1, 34 }, { 1, 42 }, { 14, 0 }, { 1, 178 }, { 9, 255 }, { 1, 209 }, { 1, 7 }, { 51, 0 }, { 1, 1 }, { 1, 58 }, { 1, 19 }, { 13, 0 }, { 1, 25 }, { 1, 252 }, { 8, 255 }, { 1, 247 }, { 1, 43 }, { 52, 0 }, { 1, 22 }, { 1, 58 }, { 1, 1 }, { 13, 0 }, { 1, 118 }, { 9, 255 }, { 1, 108 }, { 53, 0 }, { 1, 41 }, { 1, 33 }, { 14, 0 }, { 1, 216 }, { 8, 255 }, { 1, 182 }, { 5, 0 }, { 1, 2 }, { 1, 102 }, { 1, 171 }, { 1, 174 }, { 1, 118 }, { 1, 13 }, { 7, 0 }, { 1, 4 }, { 1, 15 }, { 34, 0 }, { 1, 53 }, { 1, 3 }, { 13, 0 }, { 1, 59 }, { 8, 255 }, { 1, 233 }, { 1, 23 }, { 4, 0 }, { 1, 29 }, { 1, 194 }, { 4, 255 }, { 1, 226 }, { 1, 20 }, { 5, 0 }, { 1, 15 }, { 1, 32 }, { 34, 0 }, { 1, 13 }, { 1, 28 }, { 14, 0 }, { 1, 157 }, { 8, 255 }, { 1, 75 }, { 4, 0 }, { 1, 80 }, { 1, 237 }, { 6, 255 }, { 1, 175 }, { 3, 0 }, { 1, 1 }, { 1, 34 }, { 1, 39 }, { 35, 0 }, { 1, 22 }, { 1, 1 }, { 13, 0 }, { 1, 12 }, { 1, 243 }, { 7, 255 }, { 1, 148 }, { 3, 0 }, { 1, 8 }, { 1, 148 }, { 9, 255 }, { 1, 99 }, { 1, 0 }, { 1, 8 }, { 1, 50 }, { 1, 40 }, { 36, 0 }, { 1, 7 }, { 14, 0 }, { 1, 97 }, { 7, 255 }, { 1, 214 }, { 1, 9 }, { 2, 0 }, { 1, 40 }, { 1, 209 }, { 10, 255 }, { 1, 244 }, { 1, 48 }, { 1, 58 }, { 1, 35 }, { 52, 0 }, { 1, 195 }, { 6, 255 }, { 1, 249 }, { 1, 48 }, { 2, 0 }, { 1, 98 }, { 1, 245 }, { 12, 255 }, { 1, 212 }, { 1, 29 }, { 52, 0 }, { 1, 38 }, { 7, 255 }, { 1, 117 }, { 1, 0 }, { 1, 14 }, { 1, 167 }, { 15, 255 }, { 1, 128 }, { 52, 0 }, { 1, 135 }, { 7, 255 }, { 1, 58 }, { 1, 54 }, { 1, 221 }, { 16, 255 }, { 1, 252 }, { 1, 56 }, { 50, 0 }, { 1, 3 }, { 1, 230 }, { 7, 255 }, { 1, 242 }, { 1, 252 }, { 18, 255 }, { 1, 220 }, { 1, 12 }, { 49, 0 }, { 1, 76 }, { 29, 255 }, { 1, 156 }, { 49, 0 }, { 1, 174 }, { 30, 255 }, { 1, 75 }, { 47, 0 }, { 1, 22 }, { 1, 250 }, { 30, 255 }, { 1, 189 }, { 47, 0 }, { 1, 114 }, { 31, 255 }, { 1, 180 }, { 47, 0 }, { 1, 212 }, { 30, 255 }, { 1, 250 }, { 1, 73 }, { 46, 0 }, { 1, 48 }, { 29, 255 }, { 1, 252 }, { 1, 189 }, { 1, 67 }, { 47, 0 }, { 1, 82 }, { 20, 255 }, { 1, 251 }, { 1, 226 }, { 1, 200 }, { 1, 174 }, { 1, 147 }, { 1, 121 }, { 1, 95 }, { 1, 68 }, { 1, 42 }, { 1, 14 }, { 49, 0 }, { 1, 24 }, { 1, 248 }, { 10, 255 }, { 1, 233 }, { 1, 207 }, { 1, 180 }, { 1, 154 }, { 1, 128 }, { 1, 101 }, { 1, 75 }, { 1, 49 }, { 1, 23 }, { 1, 2 }, { 59, 0 }, { 1, 62 }, { 1, 187 }, { 1, 208 }, { 1, 187 }, { 1, 161 }, { 1, 135 }, { 1, 108 }, { 1, 82 }, { 1, 56 }, { 1, 29 }, { 1, 5 }, { 1804, 0 } }, + LogoRLEFrame{ { 1717, 0 }, { 1, 16 }, { 1, 160 }, { 1, 162 }, { 1, 39 }, { 76, 0 }, { 1, 154 }, { 2, 255 }, { 1, 226 }, { 1, 19 }, { 74, 0 }, { 1, 9 }, { 1, 242 }, { 3, 255 }, { 1, 134 }, { 74, 0 }, { 1, 86 }, { 4, 255 }, { 1, 242 }, { 1, 18 }, { 73, 0 }, { 1, 177 }, { 5, 255 }, { 1, 132 }, { 72, 0 }, { 1, 18 }, { 1, 249 }, { 5, 255 }, { 1, 241 }, { 1, 17 }, { 71, 0 }, { 1, 102 }, { 7, 255 }, { 1, 115 }, { 71, 0 }, { 1, 193 }, { 7, 255 }, { 1, 206 }, { 54, 0 }, { 1, 2 }, { 1, 12 }, { 14, 0 }, { 1, 29 }, { 9, 255 }, { 1, 43 }, { 53, 0 }, { 1, 31 }, { 1, 6 }, { 14, 0 }, { 1, 118 }, { 9, 255 }, { 1, 134 }, { 52, 0 }, { 1, 6 }, { 1, 52 }, { 15, 0 }, { 1, 209 }, { 9, 255 }, { 1, 222 }, { 52, 0 }, { 1, 34 }, { 1, 39 }, { 14, 0 }, { 1, 44 }, { 10, 255 }, { 1, 250 }, { 51, 0 }, { 1, 1 }, { 1, 57 }, { 1, 17 }, { 14, 0 }, { 1, 135 }, { 11, 255 }, { 1, 3 }, { 50, 0 }, { 1, 22 }, { 1, 56 }, { 14, 0 }, { 1, 1 }, { 1, 224 }, { 10, 255 }, { 1, 205 }, { 51, 0 }, { 1, 47 }, { 1, 34 }, { 14, 0 }, { 1, 60 }, { 11, 255 }, { 1, 113 }, { 50, 0 }, { 1, 4 }, { 1, 61 }, { 1, 9 }, { 14, 0 }, { 1, 151 }, { 10, 255 }, { 1, 215 }, { 1, 8 }, { 19, 0 }, { 1, 5 }, { 30, 0 }, { 1, 21 }, { 1, 37 }, { 14, 0 }, { 1, 5 }, { 1, 236 }, { 9, 255 }, { 1, 251 }, { 1, 52 }, { 18, 0 }, { 1, 9 }, { 1, 27 }, { 31, 0 }, { 1, 35 }, { 1, 5 }, { 14, 0 }, { 1, 76 }, { 10, 255 }, { 1, 127 }, { 18, 0 }, { 1, 24 }, { 1, 37 }, { 32, 0 }, { 1, 24 }, { 15, 0 }, { 1, 167 }, { 9, 255 }, { 1, 203 }, { 1, 4 }, { 16, 0 }, { 1, 4 }, { 1, 43 }, { 1, 40 }, { 32, 0 }, { 1, 4 }, { 1, 2 }, { 14, 0 }, { 1, 12 }, { 1, 245 }, { 8, 255 }, { 1, 246 }, { 1, 40 }, { 16, 0 }, { 1, 11 }, { 1, 55 }, { 1, 39 }, { 49, 0 }, { 1, 93 }, { 9, 255 }, { 1, 110 }, { 5, 0 }, { 1, 11 }, { 1, 140 }, { 1, 216 }, { 1, 227 }, { 1, 181 }, { 1, 69 }, { 5, 0 }, { 1, 20 }, { 1, 59 }, { 1, 31 }, { 50, 0 }, { 1, 183 }, { 8, 255 }, { 1, 189 }, { 1, 1 }, { 4, 0 }, { 1, 52 }, { 1, 218 }, { 5, 255 }, { 1, 96 }, { 3, 0 }, { 1, 31 }, { 1, 60 }, { 1, 22 }, { 50, 0 }, { 1, 22 }, { 1, 251 }, { 7, 255 }, { 1, 240 }, { 1, 30 }, { 3, 0 }, { 1, 1 }, { 1, 117 }, { 1, 250 }, { 6, 255 }, { 1, 246 }, { 1, 45 }, { 1, 2 }, { 1, 42 }, { 1, 58 }, { 1, 14 }, { 51, 0 }, { 1, 109 }, { 8, 255 }, { 1, 94 }, { 3, 0 }, { 1, 25 }, { 1, 187 }, { 9, 255 }, { 1, 217 }, { 1, 57 }, { 1, 51 }, { 1, 8 }, { 52, 0 }, { 1, 199 }, { 7, 255 }, { 1, 174 }, { 3, 0 }, { 1, 76 }, { 1, 235 }, { 11, 255 }, { 1, 180 }, { 53, 0 }, { 1, 35 }, { 7, 255 }, { 1, 233 }, { 1, 21 }, { 1, 0 }, { 1, 8 }, { 1, 147 }, { 14, 255 }, { 1, 107 }, { 52, 0 }, { 1, 125 }, { 7, 255 }, { 1, 99 }, { 1, 0 }, { 1, 43 }, { 1, 210 }, { 15, 255 }, { 1, 249 }, { 1, 53 }, { 51, 0 }, { 1, 215 }, { 7, 255 }, { 1, 159 }, { 1, 112 }, { 1, 246 }, { 17, 255 }, { 1, 224 }, { 1, 17 }, { 49, 0 }, { 1, 50 }, { 29, 255 }, { 1, 179 }, { 1, 1 }, { 48, 0 }, { 1, 141 }, { 30, 255 }, { 1, 119 }, { 47, 0 }, { 1, 2 }, { 1, 229 }, { 30, 255 }, { 1, 252 }, { 1, 45 }, { 46, 0 }, { 1, 67 }, { 32, 255 }, { 1, 139 }, { 46, 0 }, { 1, 157 }, { 32, 255 }, { 1, 108 }, { 45, 0 }, { 1, 7 }, { 1, 240 }, { 31, 255 }, { 1, 212 }, { 1, 21 }, { 45, 0 }, { 1, 58 }, { 29, 255 }, { 1, 244 }, { 1, 208 }, { 1, 127 }, { 1, 17 }, { 46, 0 }, { 1, 57 }, { 19, 255 }, { 1, 238 }, { 1, 213 }, { 1, 188 }, { 1, 163 }, { 1, 138 }, { 1, 113 }, { 1, 88 }, { 1, 63 }, { 1, 38 }, { 1, 13 }, { 51, 0 }, { 1, 191 }, { 7, 255 }, { 1, 253 }, { 1, 232 }, { 1, 207 }, { 1, 182 }, { 1, 157 }, { 1, 132 }, { 1, 107 }, { 1, 82 }, { 1, 57 }, { 1, 32 }, { 1, 8 }, { 61, 0 }, { 1, 6 }, { 1, 103 }, { 1, 139 }, { 1, 126 }, { 1, 101 }, { 1, 76 }, { 1, 51 }, { 1, 26 }, { 1, 3 }, { 1725, 0 } }, + LogoRLEFrame{ { 1718, 0 }, { 1, 88 }, { 1, 155 }, { 1, 71 }, { 76, 0 }, { 1, 47 }, { 1, 252 }, { 2, 255 }, { 1, 85 }, { 75, 0 }, { 1, 140 }, { 3, 255 }, { 1, 230 }, { 1, 9 }, { 74, 0 }, { 1, 225 }, { 4, 255 }, { 1, 115 }, { 57, 0 }, { 1, 14 }, { 15, 0 }, { 1, 55 }, { 5, 255 }, { 1, 234 }, { 1, 12 }, { 55, 0 }, { 1, 1 }, { 1, 35 }, { 15, 0 }, { 1, 141 }, { 6, 255 }, { 1, 121 }, { 55, 0 }, { 1, 28 }, { 1, 29 }, { 14, 0 }, { 1, 1 }, { 1, 226 }, { 6, 255 }, { 1, 236 }, { 1, 7 }, { 53, 0 }, { 1, 1 }, { 1, 57 }, { 1, 14 }, { 14, 0 }, { 1, 56 }, { 8, 255 }, { 1, 84 }, { 53, 0 }, { 1, 21 }, { 1, 54 }, { 15, 0 }, { 1, 142 }, { 8, 255 }, { 1, 181 }, { 53, 0 }, { 1, 47 }, { 1, 32 }, { 14, 0 }, { 1, 1 }, { 1, 227 }, { 8, 255 }, { 1, 252 }, { 1, 25 }, { 51, 0 }, { 1, 9 }, { 1, 62 }, { 1, 9 }, { 14, 0 }, { 1, 57 }, { 10, 255 }, { 1, 118 }, { 51, 0 }, { 1, 29 }, { 1, 46 }, { 15, 0 }, { 1, 143 }, { 10, 255 }, { 1, 170 }, { 51, 0 }, { 1, 46 }, { 1, 13 }, { 14, 0 }, { 1, 1 }, { 1, 227 }, { 10, 255 }, { 1, 185 }, { 21, 0 }, { 1, 4 }, { 1, 17 }, { 27, 0 }, { 1, 2 }, { 1, 40 }, { 15, 0 }, { 1, 58 }, { 11, 255 }, { 1, 167 }, { 20, 0 }, { 1, 16 }, { 1, 33 }, { 28, 0 }, { 1, 17 }, { 1, 8 }, { 15, 0 }, { 1, 144 }, { 11, 255 }, { 1, 84 }, { 18, 0 }, { 1, 1 }, { 1, 34 }, { 1, 40 }, { 29, 0 }, { 1, 9 }, { 15, 0 }, { 1, 1 }, { 1, 228 }, { 10, 255 }, { 1, 219 }, { 1, 8 }, { 17, 0 }, { 1, 8 }, { 1, 51 }, { 1, 40 }, { 46, 0 }, { 1, 59 }, { 10, 255 }, { 1, 253 }, { 1, 60 }, { 17, 0 }, { 1, 16 }, { 1, 58 }, { 1, 35 }, { 47, 0 }, { 1, 145 }, { 10, 255 }, { 1, 141 }, { 17, 0 }, { 1, 27 }, { 1, 61 }, { 1, 26 }, { 47, 0 }, { 1, 1 }, { 1, 229 }, { 9, 255 }, { 1, 217 }, { 1, 9 }, { 15, 0 }, { 1, 1 }, { 1, 38 }, { 1, 59 }, { 1, 18 }, { 48, 0 }, { 1, 60 }, { 9, 255 }, { 1, 253 }, { 1, 57 }, { 15, 0 }, { 1, 2 }, { 1, 47 }, { 1, 55 }, { 1, 11 }, { 49, 0 }, { 1, 146 }, { 9, 255 }, { 1, 137 }, { 6, 0 }, { 1, 42 }, { 1, 108 }, { 1, 118 }, { 1, 73 }, { 1, 3 }, { 4, 0 }, { 1, 2 }, { 1, 47 }, { 1, 44 }, { 1, 4 }, { 49, 0 }, { 1, 2 }, { 1, 229 }, { 8, 255 }, { 1, 214 }, { 1, 7 }, { 4, 0 }, { 1, 4 }, { 1, 129 }, { 1, 251 }, { 3, 255 }, { 1, 215 }, { 1, 35 }, { 2, 0 }, { 1, 3 }, { 1, 46 }, { 1, 27 }, { 51, 0 }, { 1, 61 }, { 8, 255 }, { 1, 252 }, { 1, 53 }, { 4, 0 }, { 1, 33 }, { 1, 198 }, { 6, 255 }, { 1, 211 }, { 1, 12 }, { 1, 3 }, { 1, 34 }, { 1, 10 }, { 52, 0 }, { 1, 147 }, { 8, 255 }, { 1, 133 }, { 4, 0 }, { 1, 90 }, { 1, 241 }, { 8, 255 }, { 1, 172 }, { 1, 10 }, { 53, 0 }, { 1, 2 }, { 1, 230 }, { 7, 255 }, { 1, 211 }, { 1, 6 }, { 2, 0 }, { 1, 13 }, { 1, 162 }, { 11, 255 }, { 1, 121 }, { 53, 0 }, { 1, 62 }, { 7, 255 }, { 1, 251 }, { 1, 50 }, { 2, 0 }, { 1, 54 }, { 1, 220 }, { 12, 255 }, { 1, 253 }, { 1, 73 }, { 52, 0 }, { 1, 148 }, { 7, 255 }, { 1, 133 }, { 1, 0 }, { 1, 2 }, { 1, 121 }, { 1, 251 }, { 14, 255 }, { 1, 239 }, { 1, 36 }, { 50, 0 }, { 1, 2 }, { 1, 231 }, { 7, 255 }, { 1, 100 }, { 1, 28 }, { 1, 191 }, { 17, 255 }, { 1, 212 }, { 1, 12 }, { 49, 0 }, { 1, 63 }, { 8, 255 }, { 1, 249 }, { 1, 245 }, { 19, 255 }, { 1, 172 }, { 1, 1 }, { 48, 0 }, { 1, 149 }, { 30, 255 }, { 1, 122 }, { 47, 0 }, { 1, 2 }, { 1, 232 }, { 30, 255 }, { 1, 253 }, { 1, 68 }, { 46, 0 }, { 1, 64 }, { 32, 255 }, { 1, 205 }, { 46, 0 }, { 1, 150 }, { 32, 255 }, { 1, 229 }, { 45, 0 }, { 1, 3 }, { 1, 232 }, { 32, 255 }, { 1, 150 }, { 45, 0 }, { 1, 58 }, { 31, 255 }, { 1, 248 }, { 1, 154 }, { 1, 5 }, { 45, 0 }, { 1, 77 }, { 23, 255 }, { 1, 249 }, { 1, 224 }, { 1, 199 }, { 1, 174 }, { 1, 149 }, { 1, 124 }, { 1, 99 }, { 1, 72 }, { 1, 13 }, { 47, 0 }, { 1, 18 }, { 1, 241 }, { 12, 255 }, { 1, 244 }, { 1, 219 }, { 1, 194 }, { 1, 169 }, { 1, 144 }, { 1, 118 }, { 1, 93 }, { 1, 68 }, { 1, 43 }, { 1, 18 }, { 57, 0 }, { 1, 63 }, { 1, 213 }, { 1, 250 }, { 1, 239 }, { 1, 214 }, { 1, 188 }, { 1, 163 }, { 1, 138 }, { 1, 113 }, { 1, 88 }, { 1, 63 }, { 1, 38 }, { 1, 13 }, { 490, 0 }, { 1, 1 }, { 1, 20 }, { 1, 4 }, { 75, 0 }, { 1, 1 }, { 1, 24 }, { 1, 45 }, { 1, 8 }, { 75, 0 }, { 1, 15 }, { 1, 53 }, { 1, 46 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 39 }, { 1, 60 }, { 1, 26 }, { 75, 0 }, { 1, 17 }, { 1, 57 }, { 1, 47 }, { 1, 8 }, { 75, 0 }, { 1, 28 }, { 1, 50 }, { 1, 21 }, { 75, 0 }, { 1, 1 }, { 1, 24 }, { 1, 17 }, { 77, 0 }, { 1, 2 }, { 678, 0 } }, + LogoRLEFrame{ { 1637, 0 }, { 1, 4 }, { 1, 151 }, { 1, 183 }, { 1, 74 }, { 61, 0 }, { 1, 12 }, { 14, 0 }, { 1, 111 }, { 2, 255 }, { 1, 251 }, { 1, 68 }, { 59, 0 }, { 1, 17 }, { 1, 18 }, { 14, 0 }, { 1, 199 }, { 3, 255 }, { 1, 212 }, { 1, 2 }, { 58, 0 }, { 1, 51 }, { 1, 4 }, { 13, 0 }, { 1, 27 }, { 5, 255 }, { 1, 92 }, { 57, 0 }, { 1, 21 }, { 1, 51 }, { 14, 0 }, { 1, 109 }, { 5, 255 }, { 1, 221 }, { 1, 5 }, { 56, 0 }, { 1, 47 }, { 1, 30 }, { 14, 0 }, { 1, 191 }, { 6, 255 }, { 1, 104 }, { 55, 0 }, { 1, 9 }, { 1, 62 }, { 1, 7 }, { 13, 0 }, { 1, 21 }, { 1, 252 }, { 6, 255 }, { 1, 224 }, { 1, 2 }, { 54, 0 }, { 1, 34 }, { 1, 46 }, { 14, 0 }, { 1, 100 }, { 8, 255 }, { 1, 70 }, { 54, 0 }, { 1, 55 }, { 1, 21 }, { 14, 0 }, { 1, 183 }, { 8, 255 }, { 1, 169 }, { 53, 0 }, { 1, 9 }, { 1, 50 }, { 14, 0 }, { 1, 16 }, { 1, 250 }, { 8, 255 }, { 1, 249 }, { 1, 19 }, { 52, 0 }, { 1, 26 }, { 1, 17 }, { 14, 0 }, { 1, 92 }, { 10, 255 }, { 1, 111 }, { 52, 0 }, { 1, 27 }, { 15, 0 }, { 1, 175 }, { 10, 255 }, { 1, 162 }, { 52, 0 }, { 1, 8 }, { 14, 0 }, { 1, 11 }, { 1, 246 }, { 10, 255 }, { 1, 180 }, { 67, 0 }, { 1, 84 }, { 11, 255 }, { 1, 161 }, { 67, 0 }, { 1, 167 }, { 11, 255 }, { 1, 79 }, { 66, 0 }, { 1, 7 }, { 1, 242 }, { 10, 255 }, { 1, 215 }, { 1, 7 }, { 66, 0 }, { 1, 76 }, { 10, 255 }, { 1, 253 }, { 1, 57 }, { 67, 0 }, { 1, 158 }, { 10, 255 }, { 1, 141 }, { 67, 0 }, { 1, 4 }, { 1, 237 }, { 9, 255 }, { 1, 219 }, { 1, 9 }, { 67, 0 }, { 1, 68 }, { 10, 255 }, { 1, 61 }, { 68, 0 }, { 1, 150 }, { 9, 255 }, { 1, 147 }, { 6, 0 }, { 1, 36 }, { 1, 110 }, { 1, 128 }, { 1, 88 }, { 1, 10 }, { 57, 0 }, { 1, 2 }, { 1, 231 }, { 8, 255 }, { 1, 222 }, { 1, 11 }, { 4, 0 }, { 1, 1 }, { 1, 112 }, { 1, 247 }, { 3, 255 }, { 1, 233 }, { 1, 64 }, { 56, 0 }, { 1, 59 }, { 9, 255 }, { 1, 66 }, { 4, 0 }, { 1, 22 }, { 1, 183 }, { 6, 255 }, { 1, 237 }, { 1, 34 }, { 55, 0 }, { 1, 142 }, { 8, 255 }, { 1, 152 }, { 4, 0 }, { 1, 72 }, { 1, 232 }, { 8, 255 }, { 1, 212 }, { 1, 13 }, { 54, 0 }, { 1, 224 }, { 7, 255 }, { 1, 226 }, { 1, 14 }, { 2, 0 }, { 1, 6 }, { 1, 142 }, { 11, 255 }, { 1, 177 }, { 1, 1 }, { 52, 0 }, { 1, 51 }, { 8, 255 }, { 1, 72 }, { 2, 0 }, { 1, 40 }, { 1, 207 }, { 13, 255 }, { 1, 132 }, { 52, 0 }, { 1, 134 }, { 7, 255 }, { 1, 162 }, { 2, 0 }, { 1, 101 }, { 1, 245 }, { 15, 255 }, { 1, 88 }, { 51, 0 }, { 1, 217 }, { 7, 255 }, { 1, 137 }, { 1, 17 }, { 1, 172 }, { 17, 255 }, { 1, 246 }, { 1, 51 }, { 49, 0 }, { 1, 43 }, { 9, 255 }, { 1, 242 }, { 19, 255 }, { 1, 227 }, { 1, 24 }, { 48, 0 }, { 1, 126 }, { 30, 255 }, { 1, 198 }, { 1, 7 }, { 47, 0 }, { 1, 208 }, { 31, 255 }, { 1, 151 }, { 46, 0 }, { 1, 35 }, { 33, 255 }, { 1, 34 }, { 45, 0 }, { 1, 118 }, { 33, 255 }, { 1, 50 }, { 45, 0 }, { 1, 200 }, { 32, 255 }, { 1, 219 }, { 1, 2 }, { 44, 0 }, { 1, 22 }, { 31, 255 }, { 1, 252 }, { 1, 184 }, { 1, 25 }, { 45, 0 }, { 1, 39 }, { 23, 255 }, { 1, 247 }, { 1, 221 }, { 1, 195 }, { 1, 169 }, { 1, 143 }, { 1, 117 }, { 1, 91 }, { 1, 66 }, { 1, 20 }, { 47, 0 }, { 1, 4 }, { 1, 219 }, { 12, 255 }, { 1, 250 }, { 1, 225 }, { 1, 199 }, { 1, 173 }, { 1, 147 }, { 1, 121 }, { 1, 95 }, { 1, 69 }, { 1, 43 }, { 1, 17 }, { 57, 0 }, { 1, 45 }, { 1, 206 }, { 1, 253 }, { 1, 252 }, { 1, 228 }, { 1, 202 }, { 1, 176 }, { 1, 150 }, { 1, 125 }, { 1, 99 }, { 1, 73 }, { 1, 47 }, { 1, 21 }, { 1, 1 }, { 68, 0 }, { 1, 5 }, { 1, 2 }, { 343, 0 }, { 1, 6 }, { 77, 0 }, { 1, 13 }, { 1, 35 }, { 1, 7 }, { 75, 0 }, { 1, 10 }, { 1, 44 }, { 1, 46 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 31 }, { 1, 60 }, { 1, 32 }, { 75, 0 }, { 1, 15 }, { 1, 53 }, { 1, 52 }, { 1, 12 }, { 75, 0 }, { 1, 28 }, { 1, 58 }, { 1, 31 }, { 1, 1 }, { 74, 0 }, { 1, 1 }, { 1, 34 }, { 1, 30 }, { 1, 3 }, { 76, 0 }, { 1, 13 }, { 1, 2 }, { 834, 0 } }, + LogoRLEFrame{ { 1383, 0 }, { 1, 8 }, { 1, 2 }, { 78, 0 }, { 1, 35 }, { 78, 0 }, { 1, 15 }, { 1, 41 }, { 13, 0 }, { 1, 16 }, { 1, 183 }, { 1, 198 }, { 1, 75 }, { 61, 0 }, { 1, 46 }, { 1, 26 }, { 13, 0 }, { 1, 142 }, { 2, 255 }, { 1, 250 }, { 1, 59 }, { 59, 0 }, { 1, 9 }, { 1, 61 }, { 1, 5 }, { 13, 0 }, { 1, 226 }, { 3, 255 }, { 1, 200 }, { 59, 0 }, { 1, 34 }, { 1, 44 }, { 13, 0 }, { 1, 53 }, { 5, 255 }, { 1, 80 }, { 57, 0 }, { 1, 1 }, { 1, 58 }, { 1, 21 }, { 13, 0 }, { 1, 135 }, { 5, 255 }, { 1, 213 }, { 1, 3 }, { 56, 0 }, { 1, 17 }, { 1, 57 }, { 1, 1 }, { 13, 0 }, { 1, 216 }, { 6, 255 }, { 1, 95 }, { 56, 0 }, { 1, 35 }, { 1, 27 }, { 13, 0 }, { 1, 42 }, { 7, 255 }, { 1, 216 }, { 56, 0 }, { 1, 43 }, { 1, 1 }, { 13, 0 }, { 1, 124 }, { 8, 255 }, { 1, 60 }, { 54, 0 }, { 1, 6 }, { 1, 21 }, { 14, 0 }, { 1, 205 }, { 8, 255 }, { 1, 161 }, { 54, 0 }, { 1, 10 }, { 14, 0 }, { 1, 32 }, { 9, 255 }, { 1, 246 }, { 1, 16 }, { 68, 0 }, { 1, 113 }, { 10, 255 }, { 1, 105 }, { 68, 0 }, { 1, 194 }, { 10, 255 }, { 1, 147 }, { 67, 0 }, { 1, 23 }, { 1, 253 }, { 10, 255 }, { 1, 165 }, { 67, 0 }, { 1, 102 }, { 11, 255 }, { 1, 135 }, { 67, 0 }, { 1, 183 }, { 11, 255 }, { 1, 54 }, { 66, 0 }, { 1, 15 }, { 1, 250 }, { 10, 255 }, { 1, 188 }, { 67, 0 }, { 1, 91 }, { 10, 255 }, { 1, 244 }, { 1, 33 }, { 67, 0 }, { 1, 173 }, { 10, 255 }, { 1, 109 }, { 67, 0 }, { 1, 9 }, { 1, 245 }, { 9, 255 }, { 1, 196 }, { 1, 2 }, { 67, 0 }, { 1, 80 }, { 9, 255 }, { 1, 247 }, { 1, 39 }, { 68, 0 }, { 1, 162 }, { 9, 255 }, { 1, 119 }, { 6, 0 }, { 1, 68 }, { 1, 147 }, { 1, 163 }, { 1, 123 }, { 1, 28 }, { 57, 0 }, { 1, 5 }, { 1, 238 }, { 8, 255 }, { 1, 204 }, { 1, 3 }, { 4, 0 }, { 1, 9 }, { 1, 152 }, { 4, 255 }, { 1, 248 }, { 1, 83 }, { 56, 0 }, { 1, 69 }, { 8, 255 }, { 1, 250 }, { 1, 46 }, { 4, 0 }, { 1, 45 }, { 1, 213 }, { 6, 255 }, { 1, 245 }, { 1, 49 }, { 55, 0 }, { 1, 151 }, { 8, 255 }, { 1, 129 }, { 4, 0 }, { 1, 105 }, { 1, 247 }, { 8, 255 }, { 1, 226 }, { 1, 23 }, { 53, 0 }, { 1, 1 }, { 1, 230 }, { 7, 255 }, { 1, 211 }, { 1, 6 }, { 2, 0 }, { 1, 18 }, { 1, 175 }, { 11, 255 }, { 1, 198 }, { 1, 7 }, { 52, 0 }, { 1, 58 }, { 7, 255 }, { 1, 252 }, { 1, 53 }, { 2, 0 }, { 1, 62 }, { 1, 226 }, { 13, 255 }, { 1, 160 }, { 52, 0 }, { 1, 140 }, { 7, 255 }, { 1, 146 }, { 1, 0 }, { 1, 3 }, { 1, 128 }, { 1, 252 }, { 15, 255 }, { 1, 116 }, { 51, 0 }, { 1, 221 }, { 7, 255 }, { 1, 144 }, { 1, 29 }, { 1, 194 }, { 17, 255 }, { 1, 253 }, { 1, 75 }, { 49, 0 }, { 1, 47 }, { 9, 255 }, { 1, 252 }, { 19, 255 }, { 1, 241 }, { 1, 42 }, { 48, 0 }, { 1, 129 }, { 30, 255 }, { 1, 220 }, { 1, 18 }, { 47, 0 }, { 1, 211 }, { 31, 255 }, { 1, 180 }, { 46, 0 }, { 1, 37 }, { 33, 255 }, { 1, 44 }, { 45, 0 }, { 1, 118 }, { 33, 255 }, { 1, 36 }, { 45, 0 }, { 1, 200 }, { 32, 255 }, { 1, 188 }, { 45, 0 }, { 1, 20 }, { 31, 255 }, { 1, 227 }, { 1, 138 }, { 1, 8 }, { 45, 0 }, { 1, 34 }, { 22, 255 }, { 1, 239 }, { 1, 212 }, { 1, 185 }, { 1, 158 }, { 1, 131 }, { 1, 104 }, { 1, 77 }, { 1, 50 }, { 1, 23 }, { 1, 1 }, { 47, 0 }, { 1, 2 }, { 1, 211 }, { 11, 255 }, { 1, 250 }, { 1, 225 }, { 1, 198 }, { 1, 171 }, { 1, 144 }, { 1, 117 }, { 1, 91 }, { 1, 64 }, { 1, 37 }, { 1, 10 }, { 58, 0 }, { 1, 36 }, { 1, 190 }, { 1, 243 }, { 1, 239 }, { 1, 212 }, { 1, 185 }, { 1, 158 }, { 1, 131 }, { 1, 104 }, { 1, 77 }, { 1, 50 }, { 1, 23 }, { 1, 1 }, { 415, 0 }, { 1, 5 }, { 1, 23 }, { 1, 2 }, { 75, 0 }, { 1, 4 }, { 1, 33 }, { 1, 42 }, { 1, 3 }, { 75, 0 }, { 1, 25 }, { 1, 58 }, { 1, 35 }, { 75, 0 }, { 1, 10 }, { 1, 48 }, { 1, 55 }, { 1, 16 }, { 75, 0 }, { 1, 27 }, { 1, 61 }, { 1, 38 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 38 }, { 1, 42 }, { 1, 12 }, { 75, 0 }, { 1, 4 }, { 1, 25 }, { 1, 9 }, { 77, 0 }, { 1, 1 }, { 913, 0 } }, + LogoRLEFrame{ { 1478, 0 }, { 1, 38 }, { 1, 100 }, { 1, 33 }, { 76, 0 }, { 1, 17 }, { 1, 233 }, { 1, 255 }, { 1, 246 }, { 1, 62 }, { 75, 0 }, { 1, 102 }, { 3, 255 }, { 1, 223 }, { 1, 6 }, { 74, 0 }, { 1, 184 }, { 4, 255 }, { 1, 107 }, { 73, 0 }, { 1, 16 }, { 1, 250 }, { 4, 255 }, { 1, 231 }, { 1, 10 }, { 72, 0 }, { 1, 94 }, { 6, 255 }, { 1, 121 }, { 72, 0 }, { 1, 176 }, { 6, 255 }, { 1, 239 }, { 1, 11 }, { 70, 0 }, { 1, 12 }, { 1, 247 }, { 7, 255 }, { 1, 96 }, { 70, 0 }, { 1, 86 }, { 8, 255 }, { 1, 196 }, { 70, 0 }, { 1, 169 }, { 9, 255 }, { 1, 40 }, { 68, 0 }, { 1, 8 }, { 1, 243 }, { 9, 255 }, { 1, 138 }, { 68, 0 }, { 1, 78 }, { 10, 255 }, { 1, 206 }, { 68, 0 }, { 1, 161 }, { 10, 255 }, { 1, 223 }, { 67, 0 }, { 1, 5 }, { 1, 238 }, { 10, 255 }, { 1, 218 }, { 67, 0 }, { 1, 70 }, { 11, 255 }, { 1, 142 }, { 67, 0 }, { 1, 153 }, { 10, 255 }, { 1, 251 }, { 1, 46 }, { 66, 0 }, { 1, 2 }, { 1, 233 }, { 10, 255 }, { 1, 134 }, { 67, 0 }, { 1, 62 }, { 10, 255 }, { 1, 215 }, { 1, 7 }, { 67, 0 }, { 1, 145 }, { 9, 255 }, { 1, 253 }, { 1, 57 }, { 67, 0 }, { 1, 1 }, { 1, 226 }, { 9, 255 }, { 1, 142 }, { 68, 0 }, { 1, 54 }, { 9, 255 }, { 1, 221 }, { 1, 10 }, { 5, 0 }, { 1, 10 }, { 1, 76 }, { 1, 103 }, { 1, 70 }, { 1, 7 }, { 58, 0 }, { 1, 137 }, { 9, 255 }, { 1, 65 }, { 5, 0 }, { 1, 55 }, { 1, 218 }, { 3, 255 }, { 1, 228 }, { 1, 70 }, { 57, 0 }, { 1, 219 }, { 8, 255 }, { 1, 151 }, { 4, 0 }, { 1, 1 }, { 1, 118 }, { 1, 251 }, { 5, 255 }, { 1, 241 }, { 1, 41 }, { 55, 0 }, { 1, 46 }, { 8, 255 }, { 1, 226 }, { 1, 13 }, { 3, 0 }, { 1, 22 }, { 1, 184 }, { 8, 255 }, { 1, 219 }, { 1, 17 }, { 54, 0 }, { 1, 129 }, { 8, 255 }, { 1, 72 }, { 3, 0 }, { 1, 68 }, { 1, 231 }, { 10, 255 }, { 1, 186 }, { 1, 3 }, { 46, 0 }, { 1, 5 }, { 6, 0 }, { 1, 211 }, { 7, 255 }, { 1, 160 }, { 2, 0 }, { 1, 4 }, { 1, 133 }, { 1, 253 }, { 12, 255 }, { 1, 142 }, { 45, 0 }, { 1, 1 }, { 1, 7 }, { 5, 0 }, { 1, 38 }, { 7, 255 }, { 1, 231 }, { 1, 17 }, { 1, 0 }, { 1, 30 }, { 1, 197 }, { 15, 255 }, { 1, 96 }, { 44, 0 }, { 1, 12 }, { 1, 1 }, { 5, 0 }, { 1, 121 }, { 7, 255 }, { 1, 165 }, { 1, 0 }, { 1, 82 }, { 1, 238 }, { 16, 255 }, { 1, 248 }, { 1, 57 }, { 42, 0 }, { 1, 4 }, { 1, 14 }, { 6, 0 }, { 1, 204 }, { 7, 255 }, { 1, 250 }, { 1, 187 }, { 19, 255 }, { 1, 231 }, { 1, 28 }, { 41, 0 }, { 1, 16 }, { 1, 7 }, { 5, 0 }, { 1, 31 }, { 30, 255 }, { 1, 203 }, { 1, 9 }, { 39, 0 }, { 1, 6 }, { 1, 18 }, { 6, 0 }, { 1, 113 }, { 31, 255 }, { 1, 159 }, { 39, 0 }, { 1, 17 }, { 1, 9 }, { 6, 0 }, { 1, 196 }, { 32, 255 }, { 1, 44 }, { 37, 0 }, { 1, 7 }, { 1, 19 }, { 6, 0 }, { 1, 24 }, { 1, 253 }, { 32, 255 }, { 1, 66 }, { 37, 0 }, { 1, 17 }, { 1, 10 }, { 6, 0 }, { 1, 105 }, { 32, 255 }, { 1, 232 }, { 1, 9 }, { 36, 0 }, { 1, 7 }, { 1, 19 }, { 1, 1 }, { 6, 0 }, { 1, 187 }, { 31, 255 }, { 1, 203 }, { 1, 37 }, { 37, 0 }, { 1, 16 }, { 1, 10 }, { 7, 0 }, { 1, 226 }, { 23, 255 }, { 1, 251 }, { 1, 224 }, { 1, 196 }, { 1, 168 }, { 1, 140 }, { 1, 112 }, { 1, 84 }, { 1, 36 }, { 38, 0 }, { 1, 4 }, { 1, 18 }, { 8, 0 }, { 1, 194 }, { 14, 255 }, { 1, 248 }, { 1, 220 }, { 1, 192 }, { 1, 164 }, { 1, 136 }, { 1, 108 }, { 1, 80 }, { 1, 52 }, { 1, 24 }, { 1, 2 }, { 45, 0 }, { 1, 12 }, { 1, 5 }, { 8, 0 }, { 1, 50 }, { 1, 230 }, { 4, 255 }, { 1, 245 }, { 1, 216 }, { 1, 188 }, { 1, 160 }, { 1, 132 }, { 1, 104 }, { 1, 76 }, { 1, 48 }, { 1, 20 }, { 55, 0 }, { 1, 12 }, { 10, 0 }, { 1, 15 }, { 1, 64 }, { 1, 72 }, { 1, 44 }, { 1, 16 }, { 63, 0 }, { 1, 6 }, { 1, 2 }, { 78, 0 }, { 1, 3 }, { 122, 0 }, { 1, 8 }, { 77, 0 }, { 1, 22 }, { 1, 35 }, { 1, 3 }, { 75, 0 }, { 1, 17 }, { 1, 52 }, { 1, 36 }, { 75, 0 }, { 1, 5 }, { 1, 41 }, { 1, 58 }, { 1, 22 }, { 75, 0 }, { 1, 24 }, { 1, 58 }, { 1, 43 }, { 1, 6 }, { 74, 0 }, { 1, 1 }, { 1, 38 }, { 1, 53 }, { 1, 22 }, { 75, 0 }, { 1, 5 }, { 1, 36 }, { 1, 21 }, { 77, 0 }, { 1, 11 }, { 1070, 0 } }, + LogoRLEFrame{ { 1479, 0 }, { 1, 41 }, { 1, 68 }, { 1, 5 }, { 76, 0 }, { 1, 52 }, { 1, 251 }, { 1, 255 }, { 1, 199 }, { 1, 11 }, { 75, 0 }, { 1, 164 }, { 3, 255 }, { 1, 137 }, { 74, 0 }, { 1, 8 }, { 1, 242 }, { 3, 255 }, { 1, 245 }, { 1, 24 }, { 73, 0 }, { 1, 80 }, { 5, 255 }, { 1, 145 }, { 73, 0 }, { 1, 166 }, { 5, 255 }, { 1, 248 }, { 1, 29 }, { 71, 0 }, { 1, 8 }, { 1, 243 }, { 6, 255 }, { 1, 147 }, { 71, 0 }, { 1, 81 }, { 7, 255 }, { 1, 239 }, { 1, 8 }, { 70, 0 }, { 1, 167 }, { 8, 255 }, { 1, 87 }, { 69, 0 }, { 1, 9 }, { 1, 244 }, { 8, 255 }, { 1, 184 }, { 69, 0 }, { 1, 83 }, { 9, 255 }, { 1, 253 }, { 1, 28 }, { 68, 0 }, { 1, 169 }, { 10, 255 }, { 1, 82 }, { 67, 0 }, { 1, 10 }, { 1, 244 }, { 10, 255 }, { 1, 95 }, { 67, 0 }, { 1, 84 }, { 11, 255 }, { 1, 77 }, { 67, 0 }, { 1, 170 }, { 10, 255 }, { 1, 240 }, { 1, 7 }, { 66, 0 }, { 1, 11 }, { 1, 245 }, { 10, 255 }, { 1, 133 }, { 67, 0 }, { 1, 86 }, { 10, 255 }, { 1, 212 }, { 1, 7 }, { 67, 0 }, { 1, 172 }, { 9, 255 }, { 1, 252 }, { 1, 53 }, { 67, 0 }, { 1, 12 }, { 1, 246 }, { 9, 255 }, { 1, 135 }, { 62, 0 }, { 1, 2 }, { 5, 0 }, { 1, 88 }, { 9, 255 }, { 1, 214 }, { 1, 7 }, { 61, 0 }, { 1, 5 }, { 1, 9 }, { 5, 0 }, { 1, 173 }, { 8, 255 }, { 1, 252 }, { 1, 55 }, { 5, 0 }, { 1, 11 }, { 1, 117 }, { 1, 162 }, { 1, 144 }, { 1, 77 }, { 52, 0 }, { 1, 29 }, { 5, 0 }, { 1, 12 }, { 1, 247 }, { 8, 255 }, { 1, 137 }, { 5, 0 }, { 1, 54 }, { 1, 221 }, { 4, 255 }, { 1, 182 }, { 1, 3 }, { 49, 0 }, { 1, 13 }, { 1, 29 }, { 5, 0 }, { 1, 89 }, { 8, 255 }, { 1, 215 }, { 1, 8 }, { 4, 0 }, { 1, 113 }, { 1, 250 }, { 6, 255 }, { 1, 137 }, { 49, 0 }, { 1, 49 }, { 1, 7 }, { 5, 0 }, { 1, 175 }, { 7, 255 }, { 1, 253 }, { 1, 56 }, { 3, 0 }, { 1, 18 }, { 1, 178 }, { 9, 255 }, { 1, 84 }, { 47, 0 }, { 1, 24 }, { 1, 46 }, { 5, 0 }, { 1, 13 }, { 1, 247 }, { 7, 255 }, { 1, 139 }, { 3, 0 }, { 1, 58 }, { 1, 225 }, { 10, 255 }, { 1, 243 }, { 1, 42 }, { 45, 0 }, { 1, 1 }, { 1, 56 }, { 1, 19 }, { 5, 0 }, { 1, 91 }, { 7, 255 }, { 1, 216 }, { 1, 8 }, { 1, 0 }, { 1, 1 }, { 1, 118 }, { 1, 251 }, { 12, 255 }, { 1, 217 }, { 1, 14 }, { 44, 0 }, { 1, 26 }, { 1, 52 }, { 6, 0 }, { 1, 177 }, { 6, 255 }, { 1, 253 }, { 1, 57 }, { 1, 0 }, { 1, 20 }, { 1, 182 }, { 15, 255 }, { 1, 176 }, { 1, 1 }, { 42, 0 }, { 1, 1 }, { 1, 56 }, { 1, 23 }, { 5, 0 }, { 1, 14 }, { 1, 248 }, { 6, 255 }, { 1, 243 }, { 1, 8 }, { 1, 62 }, { 1, 228 }, { 17, 255 }, { 1, 123 }, { 42, 0 }, { 1, 27 }, { 1, 55 }, { 6, 0 }, { 1, 92 }, { 8, 255 }, { 1, 222 }, { 1, 252 }, { 18, 255 }, { 1, 253 }, { 1, 72 }, { 40, 0 }, { 1, 2 }, { 1, 56 }, { 1, 26 }, { 6, 0 }, { 1, 178 }, { 29, 255 }, { 1, 238 }, { 1, 34 }, { 39, 0 }, { 1, 25 }, { 1, 55 }, { 1, 1 }, { 5, 0 }, { 1, 15 }, { 1, 249 }, { 30, 255 }, { 1, 203 }, { 1, 2 }, { 38, 0 }, { 1, 49 }, { 1, 19 }, { 6, 0 }, { 1, 94 }, { 32, 255 }, { 1, 64 }, { 37, 0 }, { 1, 11 }, { 1, 43 }, { 7, 0 }, { 1, 180 }, { 32, 255 }, { 1, 49 }, { 37, 0 }, { 1, 33 }, { 1, 7 }, { 6, 0 }, { 1, 16 }, { 1, 249 }, { 31, 255 }, { 1, 193 }, { 1, 1 }, { 36, 0 }, { 1, 1 }, { 1, 24 }, { 7, 0 }, { 1, 94 }, { 30, 255 }, { 1, 226 }, { 1, 137 }, { 1, 9 }, { 37, 0 }, { 1, 10 }, { 8, 0 }, { 1, 129 }, { 21, 255 }, { 1, 251 }, { 1, 225 }, { 1, 196 }, { 1, 167 }, { 1, 138 }, { 1, 109 }, { 1, 80 }, { 1, 51 }, { 1, 22 }, { 49, 0 }, { 1, 87 }, { 12, 255 }, { 1, 253 }, { 1, 230 }, { 1, 201 }, { 1, 172 }, { 1, 143 }, { 1, 114 }, { 1, 85 }, { 1, 56 }, { 1, 27 }, { 1, 2 }, { 57, 0 }, { 1, 1 }, { 1, 151 }, { 3, 255 }, { 1, 235 }, { 1, 206 }, { 1, 177 }, { 1, 148 }, { 1, 119 }, { 1, 90 }, { 1, 61 }, { 1, 32 }, { 1, 5 }, { 68, 0 }, { 1, 14 }, { 1, 32 }, { 1, 9 }, { 188, 0 }, { 1, 11 }, { 1, 23 }, { 1, 1 }, { 75, 0 }, { 1, 10 }, { 1, 42 }, { 1, 35 }, { 75, 0 }, { 1, 2 }, { 1, 34 }, { 1, 59 }, { 1, 25 }, { 75, 0 }, { 1, 18 }, { 1, 55 }, { 1, 48 }, { 1, 9 }, { 74, 0 }, { 1, 1 }, { 1, 37 }, { 1, 60 }, { 1, 28 }, { 75, 0 }, { 1, 5 }, { 1, 44 }, { 1, 33 }, { 1, 5 }, { 75, 0 }, { 1, 7 }, { 1, 22 }, { 1, 3 }, { 1226, 0 } }, + LogoRLEFrame{ { 1479, 0 }, { 1, 67 }, { 1, 135 }, { 1, 53 }, { 76, 0 }, { 1, 37 }, { 1, 247 }, { 1, 255 }, { 1, 248 }, { 1, 59 }, { 75, 0 }, { 1, 135 }, { 3, 255 }, { 1, 204 }, { 74, 0 }, { 1, 1 }, { 1, 224 }, { 4, 255 }, { 1, 75 }, { 73, 0 }, { 1, 61 }, { 5, 255 }, { 1, 203 }, { 73, 0 }, { 1, 152 }, { 6, 255 }, { 1, 74 }, { 71, 0 }, { 1, 6 }, { 1, 237 }, { 6, 255 }, { 1, 184 }, { 71, 0 }, { 1, 79 }, { 7, 255 }, { 1, 252 }, { 1, 23 }, { 70, 0 }, { 1, 170 }, { 8, 255 }, { 1, 110 }, { 69, 0 }, { 1, 14 }, { 1, 247 }, { 8, 255 }, { 1, 202 }, { 69, 0 }, { 1, 96 }, { 10, 255 }, { 1, 27 }, { 68, 0 }, { 1, 187 }, { 10, 255 }, { 1, 41 }, { 67, 0 }, { 1, 25 }, { 1, 253 }, { 10, 255 }, { 1, 40 }, { 67, 0 }, { 1, 114 }, { 10, 255 }, { 1, 221 }, { 62, 0 }, { 1, 2 }, { 5, 0 }, { 1, 205 }, { 10, 255 }, { 1, 121 }, { 62, 0 }, { 1, 15 }, { 4, 0 }, { 1, 40 }, { 10, 255 }, { 1, 210 }, { 1, 6 }, { 61, 0 }, { 1, 13 }, { 1, 15 }, { 4, 0 }, { 1, 131 }, { 9, 255 }, { 1, 250 }, { 1, 48 }, { 62, 0 }, { 1, 42 }, { 5, 0 }, { 1, 221 }, { 9, 255 }, { 1, 125 }, { 62, 0 }, { 1, 25 }, { 1, 32 }, { 4, 0 }, { 1, 57 }, { 9, 255 }, { 1, 203 }, { 1, 4 }, { 61, 0 }, { 1, 4 }, { 1, 57 }, { 1, 9 }, { 4, 0 }, { 1, 148 }, { 8, 255 }, { 1, 248 }, { 1, 42 }, { 5, 0 }, { 1, 18 }, { 1, 85 }, { 1, 104 }, { 1, 63 }, { 1, 1 }, { 52, 0 }, { 1, 32 }, { 1, 45 }, { 4, 0 }, { 1, 4 }, { 1, 235 }, { 8, 255 }, { 1, 116 }, { 5, 0 }, { 1, 76 }, { 1, 232 }, { 3, 255 }, { 1, 208 }, { 1, 27 }, { 50, 0 }, { 1, 4 }, { 1, 59 }, { 1, 15 }, { 4, 0 }, { 1, 75 }, { 8, 255 }, { 1, 196 }, { 1, 2 }, { 3, 0 }, { 1, 4 }, { 1, 137 }, { 6, 255 }, { 1, 194 }, { 1, 3 }, { 49, 0 }, { 1, 32 }, { 1, 48 }, { 5, 0 }, { 1, 166 }, { 7, 255 }, { 1, 245 }, { 1, 36 }, { 3, 0 }, { 1, 26 }, { 1, 194 }, { 8, 255 }, { 1, 131 }, { 48, 0 }, { 1, 4 }, { 1, 59 }, { 1, 18 }, { 4, 0 }, { 1, 12 }, { 1, 245 }, { 7, 255 }, { 1, 107 }, { 3, 0 }, { 1, 69 }, { 1, 234 }, { 9, 255 }, { 1, 253 }, { 1, 68 }, { 47, 0 }, { 1, 33 }, { 1, 51 }, { 5, 0 }, { 1, 92 }, { 7, 255 }, { 1, 188 }, { 1, 1 }, { 1, 0 }, { 1, 2 }, { 1, 128 }, { 1, 253 }, { 11, 255 }, { 1, 232 }, { 1, 24 }, { 45, 0 }, { 1, 2 }, { 1, 59 }, { 1, 20 }, { 5, 0 }, { 1, 183 }, { 6, 255 }, { 1, 241 }, { 1, 30 }, { 1, 0 }, { 1, 22 }, { 1, 187 }, { 14, 255 }, { 1, 188 }, { 1, 2 }, { 44, 0 }, { 1, 24 }, { 1, 44 }, { 5, 0 }, { 1, 22 }, { 1, 252 }, { 6, 255 }, { 1, 130 }, { 1, 0 }, { 1, 61 }, { 1, 229 }, { 16, 255 }, { 1, 124 }, { 44, 0 }, { 1, 46 }, { 1, 8 }, { 5, 0 }, { 1, 110 }, { 7, 255 }, { 1, 207 }, { 1, 134 }, { 1, 252 }, { 17, 255 }, { 1, 252 }, { 1, 63 }, { 42, 0 }, { 1, 10 }, { 1, 30 }, { 6, 0 }, { 1, 201 }, { 28, 255 }, { 1, 229 }, { 1, 21 }, { 41, 0 }, { 1, 24 }, { 1, 1 }, { 5, 0 }, { 1, 36 }, { 30, 255 }, { 1, 180 }, { 40, 0 }, { 1, 1 }, { 1, 10 }, { 6, 0 }, { 1, 127 }, { 31, 255 }, { 1, 56 }, { 47, 0 }, { 1, 218 }, { 31, 255 }, { 1, 69 }, { 46, 0 }, { 1, 54 }, { 31, 255 }, { 1, 226 }, { 1, 7 }, { 46, 0 }, { 1, 145 }, { 30, 255 }, { 1, 190 }, { 1, 29 }, { 47, 0 }, { 1, 216 }, { 23, 255 }, { 1, 232 }, { 1, 202 }, { 1, 171 }, { 1, 140 }, { 1, 109 }, { 1, 79 }, { 1, 28 }, { 49, 0 }, { 1, 220 }, { 14, 255 }, { 1, 251 }, { 1, 223 }, { 1, 192 }, { 1, 161 }, { 1, 130 }, { 1, 99 }, { 1, 69 }, { 1, 38 }, { 1, 8 }, { 56, 0 }, { 1, 104 }, { 6, 255 }, { 1, 244 }, { 1, 213 }, { 1, 182 }, { 1, 151 }, { 1, 120 }, { 1, 90 }, { 1, 59 }, { 1, 28 }, { 1, 2 }, { 65, 0 }, { 1, 61 }, { 1, 116 }, { 1, 111 }, { 1, 80 }, { 1, 49 }, { 1, 18 }, { 109, 0 }, { 1, 3 }, { 1, 9 }, { 76, 0 }, { 1, 3 }, { 2, 31 }, { 76, 0 }, { 1, 26 }, { 1, 57 }, { 1, 26 }, { 75, 0 }, { 1, 12 }, { 1, 49 }, { 1, 53 }, { 1, 14 }, { 74, 0 }, { 1, 1 }, { 1, 34 }, { 1, 61 }, { 1, 34 }, { 1, 1 }, { 74, 0 }, { 1, 5 }, { 1, 47 }, { 1, 46 }, { 1, 14 }, { 75, 0 }, { 1, 11 }, { 1, 35 }, { 1, 13 }, { 76, 0 }, { 1, 1 }, { 1, 7 }, { 1305, 0 } }, + LogoRLEFrame{ { 1400, 0 }, { 1, 1 }, { 77, 0 }, { 1, 1 }, { 1, 167 }, { 1, 246 }, { 1, 150 }, { 1, 1 }, { 75, 0 }, { 1, 82 }, { 3, 255 }, { 1, 102 }, { 75, 0 }, { 1, 182 }, { 3, 255 }, { 1, 222 }, { 1, 3 }, { 73, 0 }, { 1, 27 }, { 1, 252 }, { 4, 255 }, { 1, 89 }, { 73, 0 }, { 1, 123 }, { 5, 255 }, { 1, 209 }, { 72, 0 }, { 1, 1 }, { 1, 221 }, { 6, 255 }, { 1, 65 }, { 71, 0 }, { 1, 65 }, { 7, 255 }, { 1, 151 }, { 71, 0 }, { 1, 164 }, { 7, 255 }, { 1, 233 }, { 1, 3 }, { 69, 0 }, { 1, 16 }, { 1, 247 }, { 8, 255 }, { 1, 65 }, { 65, 0 }, { 1, 2 }, { 3, 0 }, { 1, 106 }, { 9, 255 }, { 1, 142 }, { 64, 0 }, { 1, 11 }, { 1, 3 }, { 3, 0 }, { 1, 205 }, { 9, 255 }, { 1, 152 }, { 64, 0 }, { 1, 28 }, { 3, 0 }, { 1, 48 }, { 10, 255 }, { 1, 143 }, { 63, 0 }, { 1, 25 }, { 1, 18 }, { 3, 0 }, { 1, 147 }, { 10, 255 }, { 1, 61 }, { 62, 0 }, { 1, 4 }, { 1, 52 }, { 3, 0 }, { 1, 7 }, { 1, 238 }, { 9, 255 }, { 1, 206 }, { 63, 0 }, { 1, 36 }, { 1, 35 }, { 3, 0 }, { 1, 89 }, { 9, 255 }, { 1, 249 }, { 1, 46 }, { 62, 0 }, { 1, 8 }, { 1, 61 }, { 1, 9 }, { 3, 0 }, { 1, 188 }, { 9, 255 }, { 1, 116 }, { 63, 0 }, { 1, 38 }, { 1, 41 }, { 3, 0 }, { 1, 32 }, { 9, 255 }, { 1, 192 }, { 1, 2 }, { 62, 0 }, { 1, 8 }, { 1, 61 }, { 1, 11 }, { 3, 0 }, { 1, 129 }, { 8, 255 }, { 1, 240 }, { 1, 30 }, { 63, 0 }, { 1, 39 }, { 1, 44 }, { 3, 0 }, { 1, 2 }, { 1, 226 }, { 8, 255 }, { 1, 92 }, { 5, 0 }, { 1, 48 }, { 1, 144 }, { 1, 166 }, { 1, 125 }, { 1, 23 }, { 53, 0 }, { 1, 8 }, { 1, 61 }, { 1, 15 }, { 3, 0 }, { 1, 71 }, { 8, 255 }, { 1, 169 }, { 4, 0 }, { 1, 1 }, { 1, 119 }, { 1, 251 }, { 3, 255 }, { 1, 241 }, { 1, 38 }, { 52, 0 }, { 1, 36 }, { 1, 46 }, { 4, 0 }, { 1, 170 }, { 7, 255 }, { 1, 228 }, { 1, 17 }, { 3, 0 }, { 1, 15 }, { 1, 176 }, { 6, 255 }, { 1, 200 }, { 1, 3 }, { 50, 0 }, { 1, 2 }, { 1, 58 }, { 1, 11 }, { 3, 0 }, { 1, 20 }, { 1, 249 }, { 7, 255 }, { 1, 68 }, { 3, 0 }, { 1, 45 }, { 1, 219 }, { 8, 255 }, { 1, 121 }, { 50, 0 }, { 1, 23 }, { 1, 37 }, { 4, 0 }, { 1, 112 }, { 7, 255 }, { 1, 144 }, { 3, 0 }, { 1, 92 }, { 1, 245 }, { 9, 255 }, { 1, 249 }, { 1, 47 }, { 49, 0 }, { 1, 43 }, { 1, 5 }, { 4, 0 }, { 1, 211 }, { 6, 255 }, { 1, 213 }, { 1, 8 }, { 1, 0 }, { 1, 6 }, { 1, 149 }, { 12, 255 }, { 1, 209 }, { 1, 6 }, { 47, 0 }, { 1, 9 }, { 1, 21 }, { 4, 0 }, { 1, 54 }, { 6, 255 }, { 1, 250 }, { 1, 49 }, { 1, 0 }, { 1, 29 }, { 1, 200 }, { 14, 255 }, { 1, 133 }, { 47, 0 }, { 1, 14 }, { 5, 0 }, { 1, 153 }, { 6, 255 }, { 1, 144 }, { 1, 0 }, { 1, 68 }, { 1, 235 }, { 15, 255 }, { 1, 252 }, { 1, 56 }, { 51, 0 }, { 1, 10 }, { 1, 241 }, { 6, 255 }, { 1, 204 }, { 1, 136 }, { 1, 253 }, { 17, 255 }, { 1, 217 }, { 1, 9 }, { 50, 0 }, { 1, 95 }, { 28, 255 }, { 1, 145 }, { 50, 0 }, { 1, 194 }, { 29, 255 }, { 1, 57 }, { 48, 0 }, { 1, 37 }, { 30, 255 }, { 1, 156 }, { 48, 0 }, { 1, 136 }, { 30, 255 }, { 1, 124 }, { 47, 0 }, { 1, 3 }, { 1, 230 }, { 29, 255 }, { 1, 221 }, { 1, 27 }, { 47, 0 }, { 1, 77 }, { 28, 255 }, { 1, 231 }, { 1, 143 }, { 1, 24 }, { 48, 0 }, { 1, 161 }, { 20, 255 }, { 1, 251 }, { 1, 220 }, { 1, 186 }, { 1, 153 }, { 1, 119 }, { 1, 85 }, { 1, 51 }, { 1, 18 }, { 51, 0 }, { 1, 180 }, { 13, 255 }, { 1, 234 }, { 1, 200 }, { 1, 167 }, { 1, 133 }, { 1, 99 }, { 1, 65 }, { 1, 32 }, { 1, 3 }, { 58, 0 }, { 1, 79 }, { 1, 252 }, { 4, 255 }, { 1, 247 }, { 1, 214 }, { 1, 181 }, { 1, 147 }, { 1, 113 }, { 1, 79 }, { 1, 46 }, { 1, 13 }, { 67, 0 }, { 1, 48 }, { 1, 102 }, { 1, 93 }, { 1, 59 }, { 1, 26 }, { 1, 1 }, { 112, 0 }, { 1, 19 }, { 1, 22 }, { 76, 0 }, { 1, 18 }, { 1, 49 }, { 1, 25 }, { 75, 0 }, { 1, 7 }, { 1, 43 }, { 1, 56 }, { 1, 16 }, { 75, 0 }, { 1, 27 }, { 1, 59 }, { 1, 40 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 47 }, { 1, 56 }, { 1, 19 }, { 56, 0 }, { 1, 1 }, { 1, 7 }, { 1, 1 }, { 16, 0 }, { 1, 11 }, { 1, 46 }, { 1, 25 }, { 1, 1 }, { 56, 0 }, { 1, 8 }, { 1, 12 }, { 17, 0 }, { 1, 9 }, { 1, 19 }, { 57, 0 }, { 1, 5 }, { 1, 17 }, { 1, 11 }, { 76, 0 }, { 1, 11 }, { 1, 19 }, { 1, 7 }, { 75, 0 }, { 1, 4 }, { 1, 17 }, { 1, 16 }, { 1, 3 }, { 75, 0 }, { 1, 10 }, { 1, 20 }, { 1, 11 }, { 75, 0 }, { 1, 2 }, { 1, 16 }, { 1, 18 }, { 1, 5 }, { 75, 0 }, { 1, 4 }, { 1, 18 }, { 1, 10 }, { 1, 1 }, { 75, 0 }, { 1, 7 }, { 1, 12 }, { 1, 2 }, { 76, 0 }, { 1, 5 }, { 1, 4 }, { 852, 0 } }, + LogoRLEFrame{ { 1479, 0 }, { 1, 2 }, { 1, 131 }, { 1, 159 }, { 1, 33 }, { 76, 0 }, { 1, 115 }, { 2, 255 }, { 1, 209 }, { 1, 1 }, { 74, 0 }, { 1, 4 }, { 1, 227 }, { 3, 255 }, { 1, 75 }, { 74, 0 }, { 1, 84 }, { 4, 255 }, { 1, 186 }, { 69, 0 }, { 1, 2 }, { 4, 0 }, { 1, 193 }, { 5, 255 }, { 1, 43 }, { 68, 0 }, { 1, 14 }, { 3, 0 }, { 1, 46 }, { 6, 255 }, { 1, 143 }, { 67, 0 }, { 1, 23 }, { 1, 5 }, { 3, 0 }, { 1, 155 }, { 6, 255 }, { 1, 219 }, { 66, 0 }, { 1, 3 }, { 1, 39 }, { 3, 0 }, { 1, 18 }, { 1, 246 }, { 7, 255 }, { 1, 38 }, { 65, 0 }, { 1, 36 }, { 1, 21 }, { 3, 0 }, { 1, 118 }, { 8, 255 }, { 1, 113 }, { 64, 0 }, { 1, 11 }, { 1, 58 }, { 1, 2 }, { 2, 0 }, { 1, 2 }, { 1, 224 }, { 8, 255 }, { 1, 161 }, { 64, 0 }, { 1, 44 }, { 1, 34 }, { 3, 0 }, { 1, 80 }, { 9, 255 }, { 1, 146 }, { 63, 0 }, { 1, 12 }, { 1, 61 }, { 1, 6 }, { 3, 0 }, { 1, 189 }, { 9, 255 }, { 1, 100 }, { 63, 0 }, { 1, 44 }, { 1, 37 }, { 3, 0 }, { 1, 42 }, { 9, 255 }, { 1, 234 }, { 1, 8 }, { 62, 0 }, { 1, 13 }, { 1, 61 }, { 1, 8 }, { 3, 0 }, { 1, 151 }, { 9, 255 }, { 1, 100 }, { 63, 0 }, { 1, 45 }, { 1, 40 }, { 3, 0 }, { 1, 15 }, { 1, 244 }, { 8, 255 }, { 1, 170 }, { 63, 0 }, { 1, 11 }, { 1, 62 }, { 1, 10 }, { 3, 0 }, { 1, 113 }, { 8, 255 }, { 1, 225 }, { 1, 16 }, { 63, 0 }, { 2, 36 }, { 3, 0 }, { 1, 1 }, { 1, 220 }, { 7, 255 }, { 1, 252 }, { 1, 59 }, { 63, 0 }, { 1, 1 }, { 1, 54 }, { 1, 4 }, { 3, 0 }, { 1, 75 }, { 8, 255 }, { 1, 127 }, { 5, 0 }, { 1, 54 }, { 1, 111 }, { 1, 96 }, { 1, 26 }, { 55, 0 }, { 1, 22 }, { 1, 26 }, { 4, 0 }, { 1, 184 }, { 7, 255 }, { 1, 194 }, { 1, 3 }, { 3, 0 }, { 1, 7 }, { 1, 153 }, { 3, 255 }, { 1, 244 }, { 1, 73 }, { 54, 0 }, { 1, 31 }, { 4, 0 }, { 1, 38 }, { 7, 255 }, { 1, 238 }, { 1, 29 }, { 3, 0 }, { 1, 27 }, { 1, 201 }, { 5, 255 }, { 1, 224 }, { 1, 10 }, { 52, 0 }, { 1, 5 }, { 1, 8 }, { 4, 0 }, { 1, 146 }, { 7, 255 }, { 1, 83 }, { 3, 0 }, { 1, 59 }, { 1, 231 }, { 7, 255 }, { 1, 131 }, { 57, 0 }, { 1, 13 }, { 1, 242 }, { 6, 255 }, { 1, 154 }, { 3, 0 }, { 1, 103 }, { 1, 250 }, { 8, 255 }, { 1, 248 }, { 1, 37 }, { 56, 0 }, { 1, 108 }, { 6, 255 }, { 1, 214 }, { 1, 10 }, { 1, 0 }, { 1, 6 }, { 1, 153 }, { 11, 255 }, { 1, 184 }, { 55, 0 }, { 1, 1 }, { 1, 216 }, { 5, 255 }, { 1, 248 }, { 1, 47 }, { 1, 0 }, { 1, 24 }, { 1, 197 }, { 13, 255 }, { 1, 80 }, { 54, 0 }, { 1, 70 }, { 6, 255 }, { 1, 110 }, { 1, 0 }, { 1, 55 }, { 1, 229 }, { 14, 255 }, { 1, 224 }, { 1, 10 }, { 53, 0 }, { 1, 179 }, { 5, 255 }, { 1, 250 }, { 1, 6 }, { 1, 99 }, { 1, 248 }, { 16, 255 }, { 1, 131 }, { 52, 0 }, { 1, 34 }, { 1, 253 }, { 6, 255 }, { 1, 228 }, { 18, 255 }, { 1, 248 }, { 1, 37 }, { 51, 0 }, { 1, 141 }, { 27, 255 }, { 1, 181 }, { 50, 0 }, { 1, 11 }, { 1, 239 }, { 28, 255 }, { 1, 33 }, { 49, 0 }, { 1, 103 }, { 28, 255 }, { 1, 249 }, { 1, 18 }, { 49, 0 }, { 1, 212 }, { 28, 255 }, { 1, 142 }, { 49, 0 }, { 1, 66 }, { 27, 255 }, { 1, 221 }, { 1, 110 }, { 50, 0 }, { 1, 175 }, { 21, 255 }, { 1, 231 }, { 1, 192 }, { 1, 154 }, { 1, 116 }, { 1, 77 }, { 1, 39 }, { 1, 1 }, { 50, 0 }, { 1, 2 }, { 1, 249 }, { 14, 255 }, { 1, 243 }, { 1, 205 }, { 1, 167 }, { 1, 128 }, { 1, 90 }, { 1, 51 }, { 1, 14 }, { 57, 0 }, { 1, 7 }, { 1, 241 }, { 7, 255 }, { 1, 251 }, { 1, 218 }, { 1, 179 }, { 1, 141 }, { 1, 102 }, { 1, 64 }, { 1, 26 }, { 65, 0 }, { 1, 83 }, { 1, 215 }, { 1, 225 }, { 1, 192 }, { 1, 153 }, { 1, 115 }, { 1, 77 }, { 1, 38 }, { 1, 5 }, { 33, 0 }, { 1, 4 }, { 1, 3 }, { 76, 0 }, { 1, 4 }, { 1, 18 }, { 1, 11 }, { 75, 0 }, { 1, 1 }, { 1, 17 }, { 1, 28 }, { 1, 8 }, { 56, 0 }, { 1, 11 }, { 1, 1 }, { 17, 0 }, { 1, 9 }, { 1, 27 }, { 1, 22 }, { 1, 3 }, { 55, 0 }, { 1, 10 }, { 1, 31 }, { 1, 5 }, { 16, 0 }, { 1, 2 }, { 1, 21 }, { 1, 29 }, { 1, 12 }, { 55, 0 }, { 1, 3 }, { 1, 35 }, { 1, 41 }, { 1, 2 }, { 16, 0 }, { 1, 5 }, { 1, 25 }, { 1, 19 }, { 1, 3 }, { 55, 0 }, { 1, 20 }, { 1, 57 }, { 1, 34 }, { 17, 0 }, { 1, 8 }, { 1, 15 }, { 1, 3 }, { 55, 0 }, { 1, 4 }, { 1, 41 }, { 1, 58 }, { 1, 20 }, { 17, 0 }, { 1, 1 }, { 1, 2 }, { 56, 0 }, { 1, 18 }, { 1, 56 }, { 1, 47 }, { 1, 7 }, { 74, 0 }, { 1, 2 }, { 1, 38 }, { 1, 61 }, { 1, 30 }, { 75, 0 }, { 1, 6 }, { 2, 50 }, { 1, 13 }, { 75, 0 }, { 1, 13 }, { 1, 49 }, { 1, 23 }, { 76, 0 }, { 1, 18 }, { 1, 29 }, { 1, 2 }, { 76, 0 }, { 1, 8 }, { 1, 6 }, { 1088, 0 } }, + LogoRLEFrame{ { 1396, 0 }, { 1, 1 }, { 78, 0 }, { 1, 14 }, { 78, 0 }, { 1, 3 }, { 1, 25 }, { 5, 0 }, { 1, 82 }, { 1, 174 }, { 1, 71 }, { 70, 0 }, { 1, 35 }, { 1, 7 }, { 4, 0 }, { 1, 43 }, { 1, 250 }, { 1, 255 }, { 1, 243 }, { 1, 18 }, { 68, 0 }, { 1, 11 }, { 1, 46 }, { 5, 0 }, { 1, 164 }, { 3, 255 }, { 1, 109 }, { 68, 0 }, { 1, 47 }, { 1, 24 }, { 4, 0 }, { 1, 33 }, { 1, 252 }, { 3, 255 }, { 1, 209 }, { 67, 0 }, { 1, 18 }, { 1, 57 }, { 1, 2 }, { 4, 0 }, { 1, 151 }, { 5, 255 }, { 1, 51 }, { 66, 0 }, { 1, 49 }, { 1, 30 }, { 4, 0 }, { 1, 24 }, { 1, 248 }, { 5, 255 }, { 1, 124 }, { 65, 0 }, { 1, 18 }, { 1, 59 }, { 1, 3 }, { 4, 0 }, { 1, 138 }, { 6, 255 }, { 1, 187 }, { 65, 0 }, { 1, 49 }, { 1, 33 }, { 4, 0 }, { 1, 16 }, { 1, 242 }, { 6, 255 }, { 1, 245 }, { 1, 5 }, { 63, 0 }, { 1, 18 }, { 1, 60 }, { 1, 5 }, { 4, 0 }, { 1, 124 }, { 8, 255 }, { 1, 40 }, { 63, 0 }, { 1, 48 }, { 1, 34 }, { 4, 0 }, { 1, 10 }, { 1, 235 }, { 8, 255 }, { 1, 15 }, { 62, 0 }, { 1, 9 }, { 1, 56 }, { 1, 3 }, { 4, 0 }, { 1, 111 }, { 8, 255 }, { 1, 214 }, { 63, 0 }, { 1, 34 }, { 1, 20 }, { 4, 0 }, { 1, 5 }, { 1, 226 }, { 8, 255 }, { 1, 85 }, { 62, 0 }, { 1, 1 }, { 1, 39 }, { 5, 0 }, { 1, 98 }, { 8, 255 }, { 1, 182 }, { 1, 1 }, { 62, 0 }, { 1, 17 }, { 1, 8 }, { 4, 0 }, { 1, 2 }, { 1, 217 }, { 7, 255 }, { 1, 227 }, { 1, 20 }, { 63, 0 }, { 1, 11 }, { 5, 0 }, { 1, 84 }, { 7, 255 }, { 1, 251 }, { 1, 60 }, { 70, 0 }, { 1, 205 }, { 7, 255 }, { 1, 119 }, { 70, 0 }, { 1, 71 }, { 7, 255 }, { 1, 182 }, { 1, 1 }, { 3, 0 }, { 1, 22 }, { 1, 153 }, { 1, 209 }, { 1, 187 }, { 1, 82 }, { 62, 0 }, { 1, 192 }, { 6, 255 }, { 1, 227 }, { 1, 20 }, { 3, 0 }, { 1, 56 }, { 1, 232 }, { 4, 255 }, { 1, 58 }, { 60, 0 }, { 1, 58 }, { 6, 255 }, { 1, 251 }, { 1, 60 }, { 3, 0 }, { 1, 91 }, { 1, 248 }, { 5, 255 }, { 1, 187 }, { 60, 0 }, { 1, 179 }, { 6, 255 }, { 1, 119 }, { 2, 0 }, { 1, 1 }, { 1, 133 }, { 8, 255 }, { 1, 62 }, { 58, 0 }, { 1, 46 }, { 6, 255 }, { 1, 182 }, { 1, 1 }, { 1, 0 }, { 1, 11 }, { 1, 174 }, { 9, 255 }, { 1, 192 }, { 58, 0 }, { 1, 166 }, { 5, 255 }, { 1, 227 }, { 1, 20 }, { 1, 0 }, { 1, 28 }, { 1, 207 }, { 11, 255 }, { 1, 65 }, { 56, 0 }, { 1, 34 }, { 1, 252 }, { 4, 255 }, { 1, 251 }, { 1, 59 }, { 1, 0 }, { 1, 55 }, { 1, 231 }, { 12, 255 }, { 1, 195 }, { 56, 0 }, { 1, 152 }, { 5, 255 }, { 1, 119 }, { 1, 0 }, { 1, 89 }, { 1, 247 }, { 14, 255 }, { 1, 68 }, { 54, 0 }, { 1, 25 }, { 1, 248 }, { 4, 255 }, { 1, 227 }, { 1, 2 }, { 1, 131 }, { 16, 255 }, { 1, 198 }, { 54, 0 }, { 1, 139 }, { 6, 255 }, { 1, 216 }, { 18, 255 }, { 1, 71 }, { 52, 0 }, { 1, 17 }, { 1, 243 }, { 25, 255 }, { 1, 196 }, { 52, 0 }, { 1, 126 }, { 26, 255 }, { 1, 239 }, { 51, 0 }, { 1, 11 }, { 1, 236 }, { 26, 255 }, { 1, 153 }, { 51, 0 }, { 1, 112 }, { 26, 255 }, { 1, 183 }, { 1, 11 }, { 50, 0 }, { 1, 6 }, { 1, 228 }, { 22, 255 }, { 1, 246 }, { 1, 203 }, { 1, 155 }, { 1, 65 }, { 1, 1 }, { 51, 0 }, { 1, 99 }, { 17, 255 }, { 1, 253 }, { 1, 218 }, { 1, 173 }, { 1, 128 }, { 1, 83 }, { 1, 37 }, { 1, 2 }, { 55, 0 }, { 1, 202 }, { 12, 255 }, { 1, 233 }, { 1, 188 }, { 1, 142 }, { 1, 97 }, { 1, 52 }, { 1, 10 }, { 61, 0 }, { 1, 232 }, { 6, 255 }, { 1, 246 }, { 1, 202 }, { 1, 157 }, { 1, 112 }, { 1, 67 }, { 1, 22 }, { 10, 0 }, { 1, 1 }, { 56, 0 }, { 1, 94 }, { 1, 219 }, { 1, 214 }, { 1, 172 }, { 1, 127 }, { 1, 82 }, { 1, 37 }, { 1, 2 }, { 13, 0 }, { 1, 1 }, { 1, 20 }, { 1, 6 }, { 76, 0 }, { 1, 18 }, { 1, 39 }, { 1, 5 }, { 75, 0 }, { 1, 9 }, { 1, 44 }, { 1, 42 }, { 1, 2 }, { 75, 0 }, { 1, 27 }, { 1, 60 }, { 1, 32 }, { 75, 0 }, { 1, 7 }, { 1, 46 }, { 1, 56 }, { 1, 16 }, { 75, 0 }, { 1, 24 }, { 1, 59 }, { 1, 43 }, { 1, 4 }, { 74, 0 }, { 1, 2 }, { 1, 42 }, { 1, 60 }, { 1, 25 }, { 75, 0 }, { 1, 6 }, { 1, 50 }, { 1, 41 }, { 1, 8 }, { 75, 0 }, { 1, 13 }, { 1, 43 }, { 1, 14 }, { 76, 0 }, { 1, 14 }, { 1, 19 }, { 77, 0 }, { 2, 1 }, { 1324, 0 } }, + LogoRLEFrame{ { 998, 0 }, { 1, 2 }, { 78, 0 }, { 1, 3 }, { 1, 10 }, { 78, 0 }, { 1, 28 }, { 78, 0 }, { 1, 11 }, { 1, 32 }, { 78, 0 }, { 1, 47 }, { 1, 10 }, { 77, 0 }, { 1, 21 }, { 1, 49 }, { 78, 0 }, { 1, 54 }, { 1, 22 }, { 77, 0 }, { 1, 23 }, { 1, 54 }, { 78, 0 }, { 1, 54 }, { 1, 26 }, { 6, 0 }, { 1, 21 }, { 1, 145 }, { 1, 100 }, { 68, 0 }, { 1, 24 }, { 1, 57 }, { 1, 1 }, { 6, 0 }, { 1, 192 }, { 2, 255 }, { 1, 46 }, { 66, 0 }, { 1, 1 }, { 1, 54 }, { 1, 29 }, { 6, 0 }, { 1, 79 }, { 3, 255 }, { 1, 133 }, { 66, 0 }, { 1, 22 }, { 1, 57 }, { 1, 3 }, { 5, 0 }, { 1, 2 }, { 1, 211 }, { 3, 255 }, { 1, 217 }, { 66, 0 }, { 1, 47 }, { 1, 22 }, { 6, 0 }, { 1, 93 }, { 5, 255 }, { 1, 38 }, { 64, 0 }, { 1, 8 }, { 1, 46 }, { 6, 0 }, { 1, 5 }, { 1, 222 }, { 5, 255 }, { 1, 87 }, { 64, 0 }, { 1, 31 }, { 1, 9 }, { 6, 0 }, { 1, 107 }, { 6, 255 }, { 1, 133 }, { 64, 0 }, { 1, 25 }, { 6, 0 }, { 1, 10 }, { 1, 231 }, { 6, 255 }, { 1, 174 }, { 63, 0 }, { 1, 9 }, { 1, 1 }, { 6, 0 }, { 1, 120 }, { 7, 255 }, { 1, 142 }, { 70, 0 }, { 1, 16 }, { 1, 238 }, { 7, 255 }, { 1, 74 }, { 70, 0 }, { 1, 134 }, { 7, 255 }, { 1, 185 }, { 70, 0 }, { 1, 24 }, { 1, 245 }, { 6, 255 }, { 1, 237 }, { 1, 32 }, { 70, 0 }, { 1, 148 }, { 6, 255 }, { 1, 253 }, { 1, 73 }, { 70, 0 }, { 1, 32 }, { 1, 249 }, { 6, 255 }, { 1, 127 }, { 71, 0 }, { 1, 161 }, { 6, 255 }, { 1, 182 }, { 1, 1 }, { 3, 0 }, { 1, 15 }, { 1, 54 }, { 1, 23 }, { 64, 0 }, { 1, 43 }, { 1, 253 }, { 5, 255 }, { 1, 222 }, { 1, 17 }, { 3, 0 }, { 1, 108 }, { 1, 245 }, { 1, 255 }, { 1, 253 }, { 1, 122 }, { 63, 0 }, { 1, 175 }, { 5, 255 }, { 1, 247 }, { 1, 49 }, { 2, 0 }, { 1, 3 }, { 1, 151 }, { 4, 255 }, { 1, 246 }, { 1, 17 }, { 61, 0 }, { 1, 55 }, { 6, 255 }, { 1, 96 }, { 2, 0 }, { 1, 12 }, { 1, 182 }, { 6, 255 }, { 1, 112 }, { 61, 0 }, { 1, 189 }, { 5, 255 }, { 1, 152 }, { 2, 0 }, { 1, 27 }, { 1, 207 }, { 7, 255 }, { 1, 216 }, { 60, 0 }, { 1, 67 }, { 5, 255 }, { 1, 202 }, { 1, 7 }, { 1, 0 }, { 1, 46 }, { 1, 227 }, { 9, 255 }, { 1, 65 }, { 59, 0 }, { 1, 201 }, { 4, 255 }, { 1, 235 }, { 1, 30 }, { 1, 0 }, { 1, 71 }, { 1, 242 }, { 10, 255 }, { 1, 169 }, { 58, 0 }, { 1, 81 }, { 4, 255 }, { 1, 253 }, { 1, 69 }, { 1, 0 }, { 1, 101 }, { 1, 251 }, { 11, 255 }, { 1, 250 }, { 1, 23 }, { 56, 0 }, { 1, 2 }, { 1, 213 }, { 4, 255 }, { 1, 122 }, { 1, 1 }, { 1, 135 }, { 14, 255 }, { 1, 122 }, { 56, 0 }, { 1, 95 }, { 4, 255 }, { 1, 199 }, { 1, 9 }, { 1, 168 }, { 15, 255 }, { 1, 224 }, { 1, 2 }, { 54, 0 }, { 1, 6 }, { 1, 223 }, { 4, 255 }, { 1, 239 }, { 1, 212 }, { 17, 255 }, { 1, 75 }, { 54, 0 }, { 1, 108 }, { 24, 255 }, { 1, 158 }, { 53, 0 }, { 1, 11 }, { 1, 232 }, { 24, 255 }, { 1, 113 }, { 53, 0 }, { 1, 122 }, { 24, 255 }, { 1, 200 }, { 1, 11 }, { 52, 0 }, { 1, 17 }, { 1, 239 }, { 22, 255 }, { 1, 237 }, { 1, 137 }, { 1, 9 }, { 53, 0 }, { 1, 136 }, { 19, 255 }, { 1, 216 }, { 1, 163 }, { 1, 110 }, { 1, 56 }, { 1, 7 }, { 1, 0 }, { 1, 8 }, { 1, 2 }, { 51, 0 }, { 1, 25 }, { 1, 245 }, { 14, 255 }, { 1, 227 }, { 1, 174 }, { 1, 120 }, { 1, 67 }, { 1, 15 }, { 4, 0 }, { 1, 4 }, { 1, 29 }, { 1, 9 }, { 52, 0 }, { 1, 144 }, { 10, 255 }, { 1, 237 }, { 1, 184 }, { 1, 131 }, { 1, 78 }, { 1, 25 }, { 8, 0 }, { 1, 27 }, { 1, 45 }, { 1, 5 }, { 53, 0 }, { 1, 215 }, { 5, 255 }, { 1, 245 }, { 1, 195 }, { 1, 142 }, { 1, 88 }, { 1, 35 }, { 11, 0 }, { 1, 13 }, { 1, 51 }, { 1, 43 }, { 1, 2 }, { 54, 0 }, { 1, 117 }, { 1, 227 }, { 1, 205 }, { 1, 152 }, { 1, 99 }, { 1, 46 }, { 1, 3 }, { 13, 0 }, { 1, 1 }, { 1, 32 }, { 1, 60 }, { 1, 29 }, { 75, 0 }, { 1, 11 }, { 1, 50 }, { 1, 53 }, { 1, 13 }, { 75, 0 }, { 1, 29 }, { 1, 61 }, { 1, 38 }, { 1, 2 }, { 74, 0 }, { 1, 2 }, { 1, 42 }, { 1, 57 }, { 1, 20 }, { 75, 0 }, { 1, 7 }, { 1, 50 }, { 1, 36 }, { 1, 3 }, { 75, 0 }, { 1, 14 }, { 1, 39 }, { 1, 12 }, { 76, 0 }, { 1, 7 }, { 1, 13 }, { 1639, 0 } }, + LogoRLEFrame{ { 680, 0 }, { 1, 7 }, { 78, 0 }, { 1, 5 }, { 1, 8 }, { 78, 0 }, { 1, 20 }, { 78, 0 }, { 1, 10 }, { 1, 17 }, { 77, 0 }, { 1, 1 }, { 1, 27 }, { 1, 6 }, { 77, 0 }, { 1, 14 }, { 1, 23 }, { 77, 0 }, { 1, 1 }, { 1, 28 }, { 1, 9 }, { 77, 0 }, { 1, 14 }, { 1, 25 }, { 77, 0 }, { 1, 1 }, { 1, 28 }, { 1, 10 }, { 77, 0 }, { 1, 15 }, { 1, 26 }, { 78, 0 }, { 1, 28 }, { 1, 11 }, { 77, 0 }, { 1, 10 }, { 1, 23 }, { 78, 0 }, { 1, 21 }, { 1, 5 }, { 7, 0 }, { 1, 85 }, { 1, 118 }, { 1, 1 }, { 67, 0 }, { 1, 3 }, { 1, 16 }, { 7, 0 }, { 1, 93 }, { 2, 255 }, { 1, 79 }, { 67, 0 }, { 1, 11 }, { 1, 1 }, { 6, 0 }, { 1, 16 }, { 1, 235 }, { 2, 255 }, { 1, 149 }, { 67, 0 }, { 1, 5 }, { 7, 0 }, { 1, 144 }, { 3, 255 }, { 1, 216 }, { 74, 0 }, { 1, 41 }, { 1, 251 }, { 4, 255 }, { 1, 10 }, { 73, 0 }, { 1, 185 }, { 5, 255 }, { 1, 38 }, { 72, 0 }, { 1, 78 }, { 6, 255 }, { 1, 67 }, { 71, 0 }, { 1, 6 }, { 1, 219 }, { 6, 255 }, { 1, 37 }, { 71, 0 }, { 1, 119 }, { 6, 255 }, { 1, 212 }, { 71, 0 }, { 1, 24 }, { 1, 242 }, { 5, 255 }, { 1, 253 }, { 1, 60 }, { 71, 0 }, { 1, 160 }, { 6, 255 }, { 1, 129 }, { 71, 0 }, { 1, 54 }, { 6, 255 }, { 1, 177 }, { 1, 1 }, { 70, 0 }, { 1, 1 }, { 1, 200 }, { 5, 255 }, { 1, 214 }, { 1, 13 }, { 71, 0 }, { 1, 93 }, { 5, 255 }, { 1, 239 }, { 1, 37 }, { 3, 0 }, { 1, 77 }, { 1, 130 }, { 1, 91 }, { 1, 2 }, { 64, 0 }, { 1, 12 }, { 1, 229 }, { 4, 255 }, { 1, 253 }, { 1, 72 }, { 2, 0 }, { 1, 11 }, { 1, 177 }, { 3, 255 }, { 1, 131 }, { 64, 0 }, { 1, 134 }, { 5, 255 }, { 1, 118 }, { 2, 0 }, { 1, 22 }, { 1, 203 }, { 4, 255 }, { 1, 217 }, { 63, 0 }, { 1, 34 }, { 1, 248 }, { 4, 255 }, { 1, 167 }, { 2, 0 }, { 1, 36 }, { 1, 220 }, { 6, 255 }, { 1, 42 }, { 62, 0 }, { 1, 175 }, { 4, 255 }, { 1, 206 }, { 1, 9 }, { 1, 0 }, { 1, 53 }, { 1, 234 }, { 7, 255 }, { 1, 123 }, { 51, 0 }, { 1, 4 }, { 9, 0 }, { 1, 68 }, { 4, 255 }, { 1, 234 }, { 1, 30 }, { 1, 0 }, { 1, 74 }, { 1, 244 }, { 8, 255 }, { 1, 204 }, { 50, 0 }, { 1, 1 }, { 1, 9 }, { 8, 0 }, { 1, 4 }, { 1, 212 }, { 3, 255 }, { 1, 250 }, { 1, 62 }, { 1, 0 }, { 1, 98 }, { 1, 251 }, { 10, 255 }, { 1, 30 }, { 49, 0 }, { 1, 13 }, { 1, 2 }, { 8, 0 }, { 1, 109 }, { 4, 255 }, { 1, 106 }, { 1, 0 }, { 1, 125 }, { 12, 255 }, { 1, 110 }, { 48, 0 }, { 1, 7 }, { 1, 13 }, { 8, 0 }, { 1, 19 }, { 1, 238 }, { 3, 255 }, { 1, 155 }, { 1, 3 }, { 1, 153 }, { 13, 255 }, { 1, 191 }, { 47, 0 }, { 1, 2 }, { 1, 19 }, { 1, 4 }, { 8, 0 }, { 1, 150 }, { 3, 255 }, { 1, 204 }, { 1, 16 }, { 1, 177 }, { 14, 255 }, { 1, 252 }, { 1, 20 }, { 46, 0 }, { 2, 13 }, { 8, 0 }, { 1, 46 }, { 1, 252 }, { 3, 255 }, { 1, 213 }, { 1, 206 }, { 16, 255 }, { 1, 89 }, { 45, 0 }, { 1, 4 }, { 1, 20 }, { 1, 2 }, { 8, 0 }, { 1, 190 }, { 22, 255 }, { 1, 116 }, { 45, 0 }, { 1, 17 }, { 1, 11 }, { 8, 0 }, { 1, 83 }, { 22, 255 }, { 1, 243 }, { 1, 24 }, { 44, 0 }, { 1, 8 }, { 1, 19 }, { 1, 1 }, { 7, 0 }, { 1, 8 }, { 1, 223 }, { 21, 255 }, { 1, 231 }, { 1, 53 }, { 5, 0 }, { 1, 16 }, { 1, 8 }, { 37, 0 }, { 1, 1 }, { 1, 19 }, { 1, 8 }, { 8, 0 }, { 1, 124 }, { 19, 255 }, { 1, 247 }, { 1, 191 }, { 1, 108 }, { 1, 11 }, { 4, 0 }, { 1, 10 }, { 1, 38 }, { 1, 10 }, { 38, 0 }, { 1, 10 }, { 1, 16 }, { 8, 0 }, { 1, 28 }, { 1, 245 }, { 15, 255 }, { 1, 241 }, { 1, 182 }, { 1, 120 }, { 1, 59 }, { 1, 6 }, { 5, 0 }, { 1, 3 }, { 1, 35 }, { 1, 49 }, { 1, 6 }, { 38, 0 }, { 1, 1 }, { 1, 18 }, { 1, 3 }, { 8, 0 }, { 1, 165 }, { 12, 255 }, { 1, 234 }, { 1, 173 }, { 1, 111 }, { 1, 50 }, { 1, 3 }, { 8, 0 }, { 1, 17 }, { 1, 56 }, { 1, 42 }, { 1, 2 }, { 39, 0 }, { 2, 9 }, { 8, 0 }, { 1, 58 }, { 9, 255 }, { 1, 226 }, { 1, 164 }, { 1, 102 }, { 1, 40 }, { 11, 0 }, { 1, 2 }, { 1, 37 }, { 1, 60 }, { 1, 25 }, { 41, 0 }, { 1, 12 }, { 9, 0 }, { 1, 168 }, { 5, 255 }, { 1, 216 }, { 1, 155 }, { 1, 93 }, { 1, 31 }, { 14, 0 }, { 1, 15 }, { 1, 54 }, { 1, 50 }, { 1, 9 }, { 41, 0 }, { 1, 5 }, { 1, 1 }, { 9, 0 }, { 1, 133 }, { 1, 243 }, { 1, 207 }, { 1, 146 }, { 1, 84 }, { 1, 23 }, { 17, 0 }, { 1, 32 }, { 1, 62 }, { 1, 34 }, { 1, 1 }, { 42, 0 }, { 1, 1 }, { 31, 0 }, { 1, 2 }, { 1, 43 }, { 1, 49 }, { 1, 14 }, { 75, 0 }, { 1, 7 }, { 1, 43 }, { 1, 22 }, { 76, 0 }, { 1, 10 }, { 1, 24 }, { 1, 1 }, { 77, 0 }, { 1, 3 }, { 1875, 0 } }, + LogoRLEFrame{ { 1643, 0 }, { 1, 62 }, { 1, 116 }, { 1, 1 }, { 76, 0 }, { 1, 66 }, { 1, 251 }, { 1, 255 }, { 1, 64 }, { 75, 0 }, { 1, 10 }, { 1, 223 }, { 2, 255 }, { 1, 116 }, { 75, 0 }, { 1, 138 }, { 3, 255 }, { 1, 165 }, { 74, 0 }, { 1, 47 }, { 1, 251 }, { 3, 255 }, { 1, 184 }, { 73, 0 }, { 1, 2 }, { 1, 200 }, { 4, 255 }, { 1, 195 }, { 73, 0 }, { 1, 107 }, { 5, 255 }, { 1, 180 }, { 72, 0 }, { 1, 26 }, { 1, 241 }, { 5, 255 }, { 1, 93 }, { 72, 0 }, { 1, 173 }, { 5, 255 }, { 1, 194 }, { 1, 3 }, { 71, 0 }, { 1, 77 }, { 5, 255 }, { 1, 231 }, { 1, 26 }, { 71, 0 }, { 1, 12 }, { 1, 226 }, { 4, 255 }, { 1, 247 }, { 1, 54 }, { 72, 0 }, { 1, 143 }, { 5, 255 }, { 1, 89 }, { 72, 0 }, { 1, 50 }, { 1, 252 }, { 4, 255 }, { 1, 129 }, { 2, 0 }, { 1, 3 }, { 1, 102 }, { 1, 161 }, { 1, 117 }, { 1, 5 }, { 56, 0 }, { 1, 5 }, { 8, 0 }, { 1, 3 }, { 1, 204 }, { 4, 255 }, { 1, 170 }, { 1, 1 }, { 1, 0 }, { 1, 17 }, { 1, 194 }, { 3, 255 }, { 1, 100 }, { 55, 0 }, { 1, 4 }, { 1, 16 }, { 8, 0 }, { 1, 112 }, { 4, 255 }, { 1, 204 }, { 1, 9 }, { 1, 0 }, { 1, 27 }, { 1, 212 }, { 4, 255 }, { 1, 161 }, { 55, 0 }, { 1, 34 }, { 8, 0 }, { 1, 29 }, { 1, 243 }, { 3, 255 }, { 1, 229 }, { 1, 26 }, { 1, 0 }, { 1, 40 }, { 1, 225 }, { 5, 255 }, { 1, 223 }, { 54, 0 }, { 1, 22 }, { 1, 28 }, { 8, 0 }, { 1, 178 }, { 3, 255 }, { 1, 246 }, { 1, 52 }, { 1, 0 }, { 1, 56 }, { 1, 236 }, { 7, 255 }, { 1, 29 }, { 52, 0 }, { 1, 7 }, { 1, 55 }, { 1, 3 }, { 7, 0 }, { 1, 82 }, { 4, 255 }, { 1, 85 }, { 1, 0 }, { 1, 74 }, { 1, 245 }, { 8, 255 }, { 1, 90 }, { 52, 0 }, { 1, 45 }, { 1, 34 }, { 7, 0 }, { 1, 14 }, { 1, 229 }, { 3, 255 }, { 1, 125 }, { 1, 0 }, { 1, 94 }, { 1, 251 }, { 9, 255 }, { 1, 152 }, { 51, 0 }, { 1, 20 }, { 1, 58 }, { 1, 4 }, { 7, 0 }, { 1, 148 }, { 3, 255 }, { 1, 167 }, { 1, 0 }, { 1, 117 }, { 11, 255 }, { 1, 213 }, { 50, 0 }, { 1, 2 }, { 1, 55 }, { 1, 26 }, { 7, 0 }, { 1, 55 }, { 1, 253 }, { 2, 255 }, { 1, 201 }, { 1, 10 }, { 1, 141 }, { 13, 255 }, { 1, 20 }, { 49, 0 }, { 1, 31 }, { 1, 53 }, { 7, 0 }, { 1, 4 }, { 1, 208 }, { 2, 255 }, { 1, 227 }, { 1, 30 }, { 1, 164 }, { 14, 255 }, { 1, 80 }, { 48, 0 }, { 1, 8 }, { 1, 61 }, { 1, 18 }, { 7, 0 }, { 1, 117 }, { 3, 255 }, { 1, 177 }, { 1, 187 }, { 15, 255 }, { 1, 129 }, { 11, 0 }, { 1, 4 }, { 1, 3 }, { 35, 0 }, { 1, 42 }, { 1, 46 }, { 7, 0 }, { 1, 32 }, { 1, 245 }, { 20, 255 }, { 1, 116 }, { 9, 0 }, { 1, 1 }, { 1, 25 }, { 1, 14 }, { 35, 0 }, { 1, 10 }, { 1, 59 }, { 1, 8 }, { 7, 0 }, { 1, 183 }, { 20, 255 }, { 1, 212 }, { 1, 9 }, { 8, 0 }, { 1, 18 }, { 1, 46 }, { 1, 11 }, { 36, 0 }, { 1, 40 }, { 1, 26 }, { 7, 0 }, { 1, 87 }, { 20, 255 }, { 1, 171 }, { 1, 15 }, { 7, 0 }, { 1, 6 }, { 1, 44 }, { 1, 50 }, { 1, 6 }, { 36, 0 }, { 1, 7 }, { 1, 45 }, { 7, 0 }, { 1, 16 }, { 1, 231 }, { 16, 255 }, { 1, 251 }, { 1, 195 }, { 1, 126 }, { 1, 40 }, { 8, 0 }, { 1, 23 }, { 1, 58 }, { 1, 38 }, { 1, 2 }, { 37, 0 }, { 1, 30 }, { 1, 5 }, { 7, 0 }, { 1, 153 }, { 14, 255 }, { 1, 217 }, { 1, 147 }, { 1, 78 }, { 1, 14 }, { 9, 0 }, { 1, 5 }, { 1, 43 }, { 1, 58 }, { 1, 20 }, { 38, 0 }, { 1, 4 }, { 1, 12 }, { 7, 0 }, { 1, 59 }, { 11, 255 }, { 1, 236 }, { 1, 168 }, { 1, 99 }, { 1, 30 }, { 12, 0 }, { 1, 19 }, { 1, 57 }, { 1, 47 }, { 1, 7 }, { 39, 0 }, { 1, 1 }, { 7, 0 }, { 1, 5 }, { 1, 212 }, { 7, 255 }, { 1, 249 }, { 1, 190 }, { 1, 120 }, { 1, 51 }, { 1, 2 }, { 14, 0 }, { 1, 33 }, { 1, 60 }, { 1, 29 }, { 49, 0 }, { 1, 106 }, { 5, 255 }, { 1, 211 }, { 1, 141 }, { 1, 72 }, { 1, 10 }, { 16, 0 }, { 1, 2 }, { 1, 43 }, { 1, 39 }, { 1, 6 }, { 50, 0 }, { 1, 156 }, { 1, 255 }, { 1, 231 }, { 1, 163 }, { 1, 93 }, { 1, 25 }, { 19, 0 }, { 1, 7 }, { 1, 36 }, { 1, 12 }, { 52, 0 }, { 1, 4 }, { 1, 24 }, { 22, 0 }, { 1, 5 }, { 1, 14 }, { 2190, 0 } }, + LogoRLEFrame{ { 1724, 0 }, { 1, 99 }, { 1, 81 }, { 77, 0 }, { 1, 129 }, { 1, 255 }, { 1, 228 }, { 76, 0 }, { 1, 57 }, { 1, 253 }, { 2, 255 }, { 1, 9 }, { 74, 0 }, { 1, 8 }, { 1, 216 }, { 3, 255 }, { 1, 35 }, { 74, 0 }, { 1, 140 }, { 4, 255 }, { 1, 31 }, { 73, 0 }, { 1, 58 }, { 1, 253 }, { 4, 255 }, { 1, 23 }, { 72, 0 }, { 1, 9 }, { 1, 217 }, { 4, 255 }, { 1, 201 }, { 73, 0 }, { 1, 141 }, { 5, 255 }, { 1, 72 }, { 61, 0 }, { 2, 5 }, { 9, 0 }, { 1, 58 }, { 1, 253 }, { 4, 255 }, { 1, 123 }, { 62, 0 }, { 1, 25 }, { 9, 0 }, { 1, 9 }, { 1, 217 }, { 4, 255 }, { 1, 162 }, { 62, 0 }, { 1, 24 }, { 1, 16 }, { 9, 0 }, { 1, 142 }, { 4, 255 }, { 1, 192 }, { 1, 6 }, { 61, 0 }, { 1, 8 }, { 1, 47 }, { 9, 0 }, { 1, 59 }, { 1, 253 }, { 3, 255 }, { 1, 216 }, { 1, 17 }, { 2, 0 }, { 1, 78 }, { 1, 131 }, { 1, 73 }, { 57, 0 }, { 1, 47 }, { 1, 23 }, { 8, 0 }, { 1, 9 }, { 1, 218 }, { 3, 255 }, { 1, 235 }, { 1, 35 }, { 1, 0 }, { 1, 10 }, { 1, 178 }, { 3, 255 }, { 1, 39 }, { 55, 0 }, { 1, 25 }, { 1, 53 }, { 1, 1 }, { 8, 0 }, { 1, 143 }, { 3, 255 }, { 1, 248 }, { 1, 58 }, { 1, 0 }, { 1, 18 }, { 1, 199 }, { 4, 255 }, { 1, 87 }, { 54, 0 }, { 1, 4 }, { 1, 58 }, { 1, 19 }, { 8, 0 }, { 1, 60 }, { 1, 253 }, { 3, 255 }, { 1, 88 }, { 1, 0 }, { 1, 28 }, { 1, 213 }, { 5, 255 }, { 1, 133 }, { 54, 0 }, { 1, 37 }, { 1, 47 }, { 8, 0 }, { 1, 10 }, { 1, 219 }, { 3, 255 }, { 1, 121 }, { 1, 0 }, { 1, 40 }, { 1, 225 }, { 6, 255 }, { 1, 180 }, { 1, 0 }, { 1, 18 }, { 1, 5 }, { 50, 0 }, { 1, 12 }, { 1, 61 }, { 1, 12 }, { 8, 0 }, { 1, 144 }, { 3, 255 }, { 1, 157 }, { 1, 0 }, { 1, 54 }, { 1, 235 }, { 7, 255 }, { 1, 227 }, { 1, 38 }, { 1, 6 }, { 51, 0 }, { 1, 49 }, { 1, 39 }, { 8, 0 }, { 1, 61 }, { 3, 255 }, { 1, 188 }, { 1, 5 }, { 1, 69 }, { 1, 243 }, { 9, 255 }, { 1, 20 }, { 51, 0 }, { 1, 21 }, { 1, 59 }, { 1, 6 }, { 7, 0 }, { 1, 10 }, { 1, 220 }, { 2, 255 }, { 1, 214 }, { 1, 16 }, { 1, 87 }, { 1, 249 }, { 10, 255 }, { 1, 63 }, { 16, 0 }, { 1, 11 }, { 1, 10 }, { 33, 0 }, { 1, 50 }, { 1, 23 }, { 8, 0 }, { 1, 145 }, { 2, 255 }, { 1, 233 }, { 1, 32 }, { 1, 107 }, { 1, 253 }, { 11, 255 }, { 1, 110 }, { 14, 0 }, { 1, 4 }, { 1, 34 }, { 1, 17 }, { 33, 0 }, { 1, 17 }, { 1, 44 }, { 8, 0 }, { 1, 62 }, { 2, 255 }, { 1, 246 }, { 1, 55 }, { 1, 128 }, { 13, 255 }, { 1, 157 }, { 13, 0 }, { 1, 27 }, { 1, 51 }, { 1, 11 }, { 34, 0 }, { 1, 42 }, { 1, 6 }, { 7, 0 }, { 1, 11 }, { 1, 221 }, { 2, 255 }, { 1, 124 }, { 1, 150 }, { 14, 255 }, { 1, 193 }, { 11, 0 }, { 1, 10 }, { 1, 49 }, { 1, 50 }, { 1, 6 }, { 34, 0 }, { 1, 13 }, { 1, 15 }, { 8, 0 }, { 1, 147 }, { 19, 255 }, { 1, 171 }, { 10, 0 }, { 1, 28 }, { 1, 60 }, { 1, 34 }, { 1, 1 }, { 35, 0 }, { 1, 11 }, { 8, 0 }, { 1, 63 }, { 19, 255 }, { 1, 232 }, { 1, 33 }, { 8, 0 }, { 1, 8 }, { 1, 47 }, { 1, 56 }, { 1, 16 }, { 45, 0 }, { 1, 11 }, { 1, 221 }, { 18, 255 }, { 1, 189 }, { 1, 29 }, { 8, 0 }, { 1, 22 }, { 1, 59 }, { 1, 43 }, { 1, 4 }, { 46, 0 }, { 1, 148 }, { 16, 255 }, { 1, 214 }, { 1, 139 }, { 1, 53 }, { 9, 0 }, { 1, 33 }, { 1, 55 }, { 1, 22 }, { 47, 0 }, { 1, 64 }, { 13, 255 }, { 1, 247 }, { 1, 182 }, { 1, 107 }, { 1, 33 }, { 10, 0 }, { 1, 2 }, { 1, 40 }, { 1, 30 }, { 1, 2 }, { 47, 0 }, { 1, 11 }, { 1, 222 }, { 10, 255 }, { 1, 226 }, { 1, 151 }, { 1, 76 }, { 1, 10 }, { 12, 0 }, { 1, 7 }, { 1, 26 }, { 1, 5 }, { 49, 0 }, { 1, 149 }, { 7, 255 }, { 1, 252 }, { 1, 195 }, { 1, 120 }, { 1, 45 }, { 16, 0 }, { 1, 5 }, { 50, 0 }, { 1, 61 }, { 5, 255 }, { 1, 236 }, { 1, 163 }, { 1, 89 }, { 1, 18 }, { 70, 0 }, { 1, 173 }, { 2, 255 }, { 1, 207 }, { 1, 132 }, { 1, 57 }, { 1, 2 }, { 73, 0 }, { 1, 55 }, { 1, 86 }, { 1, 27 }, { 2213, 0 } }, + LogoRLEFrame{ { 1804, 0 }, { 1, 34 }, { 1, 136 }, { 1, 20 }, { 76, 0 }, { 1, 26 }, { 1, 229 }, { 1, 255 }, { 1, 94 }, { 63, 0 }, { 1, 1 }, { 11, 0 }, { 1, 1 }, { 1, 185 }, { 2, 255 }, { 1, 116 }, { 63, 0 }, { 1, 14 }, { 11, 0 }, { 1, 108 }, { 3, 255 }, { 1, 124 }, { 62, 0 }, { 1, 23 }, { 1, 6 }, { 10, 0 }, { 1, 40 }, { 1, 246 }, { 3, 255 }, { 1, 104 }, { 61, 0 }, { 1, 8 }, { 1, 36 }, { 10, 0 }, { 1, 5 }, { 1, 204 }, { 4, 255 }, { 1, 67 }, { 61, 0 }, { 1, 48 }, { 1, 11 }, { 10, 0 }, { 1, 132 }, { 4, 255 }, { 1, 209 }, { 61, 0 }, { 1, 29 }, { 1, 46 }, { 10, 0 }, { 1, 58 }, { 1, 252 }, { 3, 255 }, { 1, 241 }, { 1, 44 }, { 60, 0 }, { 1, 7 }, { 1, 60 }, { 1, 13 }, { 9, 0 }, { 1, 12 }, { 1, 221 }, { 3, 255 }, { 1, 251 }, { 1, 71 }, { 61, 0 }, { 1, 42 }, { 1, 41 }, { 10, 0 }, { 1, 157 }, { 4, 255 }, { 1, 98 }, { 61, 0 }, { 1, 17 }, { 1, 60 }, { 1, 7 }, { 9, 0 }, { 1, 79 }, { 4, 255 }, { 1, 128 }, { 2, 0 }, { 1, 6 }, { 1, 38 }, { 1, 6 }, { 56, 0 }, { 1, 1 }, { 1, 53 }, { 1, 32 }, { 9, 0 }, { 1, 23 }, { 1, 234 }, { 3, 255 }, { 1, 158 }, { 2, 0 }, { 1, 85 }, { 1, 235 }, { 1, 255 }, { 1, 221 }, { 1, 4 }, { 5, 0 }, { 1, 9 }, { 1, 3 }, { 48, 0 }, { 1, 28 }, { 1, 57 }, { 1, 3 }, { 9, 0 }, { 1, 180 }, { 3, 255 }, { 1, 185 }, { 1, 4 }, { 1, 0 }, { 1, 114 }, { 4, 255 }, { 1, 35 }, { 3, 0 }, { 1, 4 }, { 1, 30 }, { 1, 10 }, { 48, 0 }, { 1, 2 }, { 1, 58 }, { 1, 22 }, { 9, 0 }, { 1, 104 }, { 3, 255 }, { 1, 207 }, { 1, 13 }, { 1, 0 }, { 1, 136 }, { 5, 255 }, { 1, 71 }, { 2, 0 }, { 1, 23 }, { 1, 47 }, { 1, 7 }, { 17, 0 }, { 1, 1 }, { 1, 2 }, { 30, 0 }, { 1, 27 }, { 1, 43 }, { 9, 0 }, { 1, 37 }, { 1, 245 }, { 2, 255 }, { 1, 225 }, { 1, 25 }, { 1, 3 }, { 1, 157 }, { 6, 255 }, { 1, 107 }, { 1, 8 }, { 1, 47 }, { 1, 48 }, { 1, 4 }, { 17, 0 }, { 2, 9 }, { 30, 0 }, { 1, 1 }, { 1, 51 }, { 1, 5 }, { 8, 0 }, { 1, 4 }, { 1, 201 }, { 2, 255 }, { 1, 239 }, { 1, 42 }, { 1, 8 }, { 1, 176 }, { 7, 255 }, { 1, 152 }, { 1, 59 }, { 1, 36 }, { 1, 1 }, { 16, 0 }, { 1, 5 }, { 1, 21 }, { 1, 9 }, { 31, 0 }, { 1, 24 }, { 1, 18 }, { 9, 0 }, { 1, 128 }, { 2, 255 }, { 1, 249 }, { 1, 64 }, { 1, 15 }, { 1, 193 }, { 8, 255 }, { 1, 195 }, { 1, 20 }, { 16, 0 }, { 1, 1 }, { 1, 17 }, { 1, 27 }, { 1, 5 }, { 32, 0 }, { 1, 23 }, { 9, 0 }, { 1, 55 }, { 1, 251 }, { 2, 255 }, { 1, 89 }, { 1, 24 }, { 1, 208 }, { 9, 255 }, { 1, 215 }, { 16, 0 }, { 1, 7 }, { 1, 26 }, { 1, 23 }, { 1, 3 }, { 32, 0 }, { 1, 4 }, { 9, 0 }, { 1, 11 }, { 1, 218 }, { 2, 255 }, { 1, 118 }, { 1, 35 }, { 1, 221 }, { 10, 255 }, { 1, 248 }, { 1, 2 }, { 14, 0 }, { 1, 16 }, { 1, 30 }, { 1, 14 }, { 44, 0 }, { 1, 152 }, { 2, 255 }, { 1, 149 }, { 1, 48 }, { 1, 232 }, { 12, 255 }, { 1, 29 }, { 12, 0 }, { 1, 6 }, { 1, 25 }, { 1, 26 }, { 1, 6 }, { 44, 0 }, { 1, 76 }, { 2, 255 }, { 1, 178 }, { 1, 66 }, { 1, 241 }, { 13, 255 }, { 1, 62 }, { 11, 0 }, { 1, 11 }, { 1, 30 }, { 1, 19 }, { 1, 1 }, { 44, 0 }, { 1, 21 }, { 1, 232 }, { 2, 255 }, { 1, 185 }, { 1, 248 }, { 14, 255 }, { 1, 59 }, { 10, 0 }, { 1, 16 }, { 1, 23 }, { 1, 6 }, { 46, 0 }, { 1, 176 }, { 18, 255 }, { 1, 184 }, { 9, 0 }, { 1, 1 }, { 1, 17 }, { 1, 10 }, { 47, 0 }, { 1, 99 }, { 18, 255 }, { 1, 168 }, { 1, 10 }, { 8, 0 }, { 1, 1 }, { 1, 8 }, { 48, 0 }, { 1, 34 }, { 1, 243 }, { 15, 255 }, { 1, 242 }, { 1, 169 }, { 1, 59 }, { 59, 0 }, { 1, 3 }, { 1, 198 }, { 13, 255 }, { 1, 227 }, { 1, 149 }, { 1, 71 }, { 1, 6 }, { 61, 0 }, { 1, 123 }, { 11, 255 }, { 1, 206 }, { 1, 128 }, { 1, 50 }, { 64, 0 }, { 1, 51 }, { 1, 250 }, { 7, 255 }, { 1, 250 }, { 1, 185 }, { 1, 107 }, { 1, 30 }, { 66, 0 }, { 1, 9 }, { 1, 215 }, { 5, 255 }, { 1, 238 }, { 1, 164 }, { 1, 86 }, { 1, 14 }, { 69, 0 }, { 1, 131 }, { 3, 255 }, { 1, 222 }, { 1, 143 }, { 1, 65 }, { 1, 4 }, { 72, 0 }, { 1, 155 }, { 1, 191 }, { 1, 122 }, { 1, 44 }, { 2212, 0 } }, + LogoRLEFrame{ { 1633, 0 }, { 1, 4 }, { 78, 0 }, { 1, 18 }, { 78, 0 }, { 1, 9 }, { 1, 25 }, { 78, 0 }, { 1, 45 }, { 1, 3 }, { 10, 0 }, { 1, 9 }, { 1, 171 }, { 1, 197 }, { 64, 0 }, { 1, 31 }, { 1, 34 }, { 11, 0 }, { 1, 167 }, { 1, 255 }, { 1, 248 }, { 63, 0 }, { 1, 11 }, { 1, 60 }, { 1, 7 }, { 10, 0 }, { 1, 96 }, { 3, 255 }, { 1, 3 }, { 62, 0 }, { 1, 47 }, { 1, 34 }, { 10, 0 }, { 1, 37 }, { 1, 243 }, { 2, 255 }, { 1, 240 }, { 62, 0 }, { 1, 22 }, { 1, 57 }, { 1, 3 }, { 9, 0 }, { 1, 5 }, { 1, 203 }, { 3, 255 }, { 1, 211 }, { 61, 0 }, { 1, 3 }, { 1, 56 }, { 1, 26 }, { 10, 0 }, { 1, 136 }, { 4, 255 }, { 1, 115 }, { 61, 0 }, { 1, 34 }, { 1, 52 }, { 10, 0 }, { 1, 66 }, { 1, 253 }, { 3, 255 }, { 1, 208 }, { 1, 9 }, { 60, 0 }, { 1, 9 }, { 1, 61 }, { 1, 18 }, { 9, 0 }, { 1, 19 }, { 1, 229 }, { 3, 255 }, { 1, 225 }, { 1, 26 }, { 13, 0 }, { 1, 2 }, { 47, 0 }, { 1, 38 }, { 1, 41 }, { 10, 0 }, { 1, 176 }, { 3, 255 }, { 1, 238 }, { 1, 41 }, { 13, 0 }, { 1, 19 }, { 1, 12 }, { 46, 0 }, { 1, 6 }, { 1, 55 }, { 1, 4 }, { 9, 0 }, { 1, 104 }, { 3, 255 }, { 1, 247 }, { 1, 59 }, { 12, 0 }, { 1, 10 }, { 1, 41 }, { 1, 13 }, { 47, 0 }, { 1, 34 }, { 1, 19 }, { 9, 0 }, { 1, 42 }, { 1, 246 }, { 2, 255 }, { 1, 253 }, { 1, 80 }, { 1, 0 }, { 1, 10 }, { 1, 136 }, { 1, 221 }, { 1, 194 }, { 1, 24 }, { 5, 0 }, { 1, 2 }, { 1, 33 }, { 1, 52 }, { 1, 8 }, { 47, 0 }, { 1, 4 }, { 1, 32 }, { 9, 0 }, { 1, 7 }, { 1, 209 }, { 3, 255 }, { 1, 104 }, { 1, 0 }, { 1, 19 }, { 1, 200 }, { 3, 255 }, { 1, 85 }, { 4, 0 }, { 1, 12 }, { 1, 52 }, { 1, 47 }, { 1, 5 }, { 48, 0 }, { 1, 17 }, { 1, 1 }, { 9, 0 }, { 1, 144 }, { 3, 255 }, { 1, 131 }, { 1, 0 }, { 1, 29 }, { 1, 214 }, { 4, 255 }, { 1, 113 }, { 3, 0 }, { 1, 29 }, { 1, 60 }, { 1, 32 }, { 50, 0 }, { 1, 2 }, { 9, 0 }, { 1, 74 }, { 3, 255 }, { 1, 157 }, { 1, 0 }, { 1, 41 }, { 1, 226 }, { 5, 255 }, { 1, 142 }, { 1, 0 }, { 1, 7 }, { 1, 47 }, { 1, 57 }, { 1, 16 }, { 60, 0 }, { 1, 23 }, { 1, 233 }, { 2, 255 }, { 1, 181 }, { 1, 4 }, { 1, 55 }, { 1, 236 }, { 6, 255 }, { 1, 171 }, { 1, 16 }, { 1, 58 }, { 1, 46 }, { 1, 5 }, { 60, 0 }, { 1, 1 }, { 1, 184 }, { 2, 255 }, { 1, 201 }, { 1, 10 }, { 1, 71 }, { 1, 244 }, { 7, 255 }, { 1, 204 }, { 1, 56 }, { 1, 24 }, { 62, 0 }, { 1, 113 }, { 2, 255 }, { 1, 218 }, { 1, 20 }, { 1, 90 }, { 1, 250 }, { 8, 255 }, { 1, 231 }, { 1, 3 }, { 62, 0 }, { 1, 48 }, { 1, 249 }, { 1, 255 }, { 1, 232 }, { 1, 33 }, { 1, 110 }, { 10, 255 }, { 1, 253 }, { 1, 4 }, { 61, 0 }, { 1, 10 }, { 1, 215 }, { 1, 255 }, { 1, 243 }, { 1, 50 }, { 1, 132 }, { 12, 255 }, { 1, 29 }, { 61, 0 }, { 1, 153 }, { 1, 255 }, { 1, 250 }, { 1, 72 }, { 1, 154 }, { 13, 255 }, { 1, 58 }, { 60, 0 }, { 1, 82 }, { 2, 255 }, { 2, 175 }, { 14, 255 }, { 1, 64 }, { 59, 0 }, { 1, 28 }, { 1, 237 }, { 17, 255 }, { 1, 209 }, { 1, 5 }, { 58, 0 }, { 1, 2 }, { 1, 191 }, { 17, 255 }, { 1, 204 }, { 1, 28 }, { 59, 0 }, { 1, 121 }, { 16, 255 }, { 1, 219 }, { 1, 106 }, { 1, 7 }, { 59, 0 }, { 1, 54 }, { 1, 251 }, { 13, 255 }, { 1, 205 }, { 1, 125 }, { 1, 45 }, { 61, 0 }, { 1, 13 }, { 1, 220 }, { 10, 255 }, { 1, 252 }, { 1, 190 }, { 1, 110 }, { 1, 30 }, { 64, 0 }, { 1, 162 }, { 8, 255 }, { 1, 246 }, { 1, 175 }, { 1, 95 }, { 1, 19 }, { 66, 0 }, { 1, 90 }, { 6, 255 }, { 1, 237 }, { 1, 160 }, { 1, 80 }, { 1, 10 }, { 68, 0 }, { 1, 27 }, { 1, 241 }, { 3, 255 }, { 1, 225 }, { 1, 145 }, { 1, 65 }, { 1, 4 }, { 71, 0 }, { 1, 126 }, { 1, 255 }, { 1, 211 }, { 1, 130 }, { 1, 50 }, { 75, 0 }, { 1, 7 }, { 1, 19 }, { 2135, 0 } }, + LogoRLEFrame{ { 1315, 0 }, { 1, 8 }, { 78, 0 }, { 1, 10 }, { 1, 13 }, { 77, 0 }, { 1, 1 }, { 1, 38 }, { 78, 0 }, { 1, 32 }, { 1, 22 }, { 77, 0 }, { 1, 13 }, { 1, 55 }, { 1, 1 }, { 77, 0 }, { 1, 52 }, { 1, 27 }, { 77, 0 }, { 1, 28 }, { 1, 53 }, { 1, 1 }, { 76, 0 }, { 1, 5 }, { 1, 59 }, { 1, 19 }, { 9, 0 }, { 1, 4 }, { 1, 141 }, { 1, 147 }, { 65, 0 }, { 1, 39 }, { 1, 47 }, { 10, 0 }, { 1, 153 }, { 1, 255 }, { 1, 224 }, { 64, 0 }, { 1, 14 }, { 1, 62 }, { 1, 12 }, { 9, 0 }, { 1, 86 }, { 2, 255 }, { 1, 232 }, { 64, 0 }, { 1, 48 }, { 1, 38 }, { 9, 0 }, { 1, 32 }, { 1, 240 }, { 2, 255 }, { 1, 212 }, { 63, 0 }, { 1, 15 }, { 1, 56 }, { 1, 3 }, { 8, 0 }, { 1, 4 }, { 1, 199 }, { 3, 255 }, { 1, 179 }, { 63, 0 }, { 1, 45 }, { 1, 17 }, { 9, 0 }, { 1, 135 }, { 4, 255 }, { 1, 83 }, { 16, 0 }, { 2, 9 }, { 44, 0 }, { 1, 12 }, { 1, 37 }, { 9, 0 }, { 1, 67 }, { 1, 253 }, { 3, 255 }, { 1, 182 }, { 15, 0 }, { 1, 2 }, { 1, 31 }, { 1, 18 }, { 45, 0 }, { 1, 29 }, { 1, 1 }, { 8, 0 }, { 1, 21 }, { 1, 230 }, { 3, 255 }, { 1, 201 }, { 1, 11 }, { 14, 0 }, { 1, 20 }, { 1, 51 }, { 1, 14 }, { 45, 0 }, { 1, 5 }, { 1, 7 }, { 8, 0 }, { 1, 1 }, { 1, 182 }, { 3, 255 }, { 1, 217 }, { 1, 20 }, { 13, 0 }, { 1, 4 }, { 1, 43 }, { 1, 55 }, { 1, 10 }, { 56, 0 }, { 1, 114 }, { 3, 255 }, { 1, 230 }, { 1, 32 }, { 13, 0 }, { 1, 17 }, { 1, 56 }, { 1, 43 }, { 1, 4 }, { 56, 0 }, { 1, 51 }, { 1, 249 }, { 2, 255 }, { 1, 241 }, { 1, 47 }, { 1, 0 }, { 1, 27 }, { 1, 167 }, { 1, 230 }, { 1, 169 }, { 1, 2 }, { 6, 0 }, { 1, 1 }, { 1, 35 }, { 1, 61 }, { 1, 27 }, { 57, 0 }, { 1, 12 }, { 1, 218 }, { 2, 255 }, { 1, 248 }, { 1, 64 }, { 1, 0 }, { 1, 46 }, { 1, 230 }, { 3, 255 }, { 1, 23 }, { 5, 0 }, { 1, 10 }, { 1, 52 }, { 1, 54 }, { 1, 12 }, { 58, 0 }, { 1, 162 }, { 2, 255 }, { 1, 253 }, { 1, 85 }, { 1, 0 }, { 1, 62 }, { 1, 240 }, { 4, 255 }, { 1, 48 }, { 4, 0 }, { 1, 16 }, { 1, 58 }, { 1, 38 }, { 1, 3 }, { 58, 0 }, { 1, 93 }, { 3, 255 }, { 1, 108 }, { 1, 0 }, { 1, 79 }, { 1, 247 }, { 5, 255 }, { 1, 73 }, { 3, 0 }, { 1, 24 }, { 1, 47 }, { 1, 12 }, { 59, 0 }, { 1, 36 }, { 1, 243 }, { 2, 255 }, { 1, 133 }, { 1, 0 }, { 1, 100 }, { 1, 252 }, { 6, 255 }, { 1, 98 }, { 2, 0 }, { 1, 26 }, { 1, 22 }, { 60, 0 }, { 1, 6 }, { 1, 204 }, { 2, 255 }, { 1, 158 }, { 1, 0 }, { 1, 123 }, { 8, 255 }, { 1, 123 }, { 1, 0 }, { 1, 11 }, { 1, 2 }, { 61, 0 }, { 1, 141 }, { 2, 255 }, { 1, 180 }, { 1, 6 }, { 1, 146 }, { 9, 255 }, { 1, 148 }, { 63, 0 }, { 1, 73 }, { 2, 255 }, { 1, 199 }, { 1, 16 }, { 1, 167 }, { 10, 255 }, { 1, 174 }, { 62, 0 }, { 1, 24 }, { 1, 234 }, { 1, 255 }, { 1, 215 }, { 1, 31 }, { 1, 186 }, { 11, 255 }, { 1, 199 }, { 61, 0 }, { 1, 1 }, { 1, 187 }, { 1, 255 }, { 1, 229 }, { 1, 52 }, { 1, 203 }, { 12, 255 }, { 1, 223 }, { 61, 0 }, { 1, 120 }, { 2, 255 }, { 1, 138 }, { 1, 217 }, { 13, 255 }, { 1, 225 }, { 60, 0 }, { 1, 56 }, { 1, 251 }, { 17, 255 }, { 1, 111 }, { 59, 0 }, { 1, 15 }, { 1, 222 }, { 16, 255 }, { 1, 253 }, { 1, 122 }, { 60, 0 }, { 1, 168 }, { 15, 255 }, { 1, 247 }, { 1, 168 }, { 1, 47 }, { 60, 0 }, { 1, 99 }, { 13, 255 }, { 1, 240 }, { 1, 164 }, { 1, 83 }, { 1, 11 }, { 61, 0 }, { 1, 40 }, { 1, 245 }, { 10, 255 }, { 1, 230 }, { 1, 151 }, { 1, 70 }, { 1, 5 }, { 63, 0 }, { 1, 8 }, { 1, 209 }, { 8, 255 }, { 1, 219 }, { 1, 138 }, { 1, 57 }, { 1, 1 }, { 66, 0 }, { 1, 147 }, { 6, 255 }, { 1, 206 }, { 1, 125 }, { 1, 44 }, { 69, 0 }, { 1, 73 }, { 3, 255 }, { 1, 252 }, { 1, 193 }, { 1, 112 }, { 1, 32 }, { 72, 0 }, { 1, 190 }, { 1, 247 }, { 1, 180 }, { 1, 99 }, { 1, 21 }, { 75, 0 }, { 1, 13 }, { 1, 5 }, { 664, 0 }, { 1, 7 }, { 1, 1 }, { 76, 0 }, { 1, 11 }, { 1, 16 }, { 1, 1 }, { 75, 0 }, { 1, 10 }, { 1, 25 }, { 1, 14 }, { 75, 0 }, { 1, 5 }, { 1, 23 }, { 1, 27 }, { 1, 7 }, { 74, 0 }, { 1, 1 }, { 1, 16 }, { 1, 30 }, { 1, 18 }, { 1, 1 }, { 74, 0 }, { 1, 9 }, { 1, 27 }, { 1, 25 }, { 1, 7 }, { 74, 0 }, { 1, 1 }, { 1, 19 }, { 1, 30 }, { 1, 15 }, { 75, 0 }, { 1, 5 }, { 1, 25 }, { 1, 17 }, { 1, 3 }, { 75, 0 }, { 1, 11 }, { 1, 16 }, { 1, 3 }, { 76, 0 }, { 1, 6 }, { 1, 2 }, { 764, 0 } }, + +} }; + +} // namespace RoundVideoData diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp new file mode 100644 index 0000000000..a057463507 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -0,0 +1,604 @@ +/* +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 "ui/controls/subsection_tabs_slider.h" + +#include "dialogs/dialogs_three_state_icon.h" +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/scroll_area.h" +#include "ui/dynamic_image.h" +#include "ui/unread_badge_paint.h" +#include "styles/style_chat.h" +#include "styles/style_dialogs.h" +#include "styles/style_filter_icons.h" + +namespace Ui { +namespace { + +constexpr auto kMaxNameLines = 3; + +class VerticalButton final : public SubsectionButton { +public: + VerticalButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data); + +private: + void paintEvent(QPaintEvent *e) override; + + void dataUpdatedHook() override; + + void updateSize(); + + const style::ChatTabsVertical &_st; + Text::String _text; + bool _subscribed = false; + +}; + +class HorizontalButton final : public SubsectionButton { +public: + HorizontalButton( + not_null<QWidget*> parent, + const style::SettingsSlider &st, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data); + +private: + void paintEvent(QPaintEvent *e) override; + + void dataUpdatedHook() override; + void updateSize(); + + const style::SettingsSlider &_st; + Text::String _text; + +}; + +VerticalButton::VerticalButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data) +: SubsectionButton(parent, delegate, std::move(data)) +, _st(st::chatTabsVertical) +, _text(_st.nameStyle, _data.text, kDefaultTextOptions, _st.nameWidth) { + updateSize(); +} + +void VerticalButton::dataUpdatedHook() { + _text.setMarkedText(_st.nameStyle, _data.text, kDefaultTextOptions); + updateSize(); +} + +void VerticalButton::updateSize() { + resize(_st.width, _st.baseHeight + std::min( + _st.nameStyle.font->height * kMaxNameLines, + _text.countHeight(_st.nameWidth, true))); +} + +void VerticalButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto active = _delegate->buttonActive(this); + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + paintRipple(p, QPoint(0, 0), &color); + + if (!_subscribed) { + _subscribed = true; + _data.userpic->subscribeToUpdates([=] { update(); }); + } + const auto &image = _data.userpic->image(_st.userpicSize); + const auto userpicLeft = (width() - _st.userpicSize) / 2; + p.drawImage(userpicLeft, _st.userpicTop, image); + p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active)); + + const auto textLeft = (width() - _st.nameWidth) / 2; + _text.draw(p, { + .position = QPoint(textLeft, _st.nameTop), + .outerWidth = width(), + .availableWidth = _st.nameWidth, + .align = style::al_top, + .paused = _delegate->buttonPaused(), + .elisionLines = kMaxNameLines, + }); + + const auto &state = _data.badges; + const auto top = _st.userpicTop / 2; + auto right = width() - textLeft; + UnreadBadgeStyle st; + if (state.unread) { + st.muted = state.unreadMuted; + const auto counter = (state.unreadCounter <= 0) + ? QString() + : ((state.mention || state.reaction) + && (state.unreadCounter > 999)) + ? (u"99+"_q) + : (state.unreadCounter > 999999) + ? (u"99999+"_q) + : QString::number(state.unreadCounter); + const auto badge = PaintUnreadBadge(p, counter, right, top, st); + right -= badge.width() + st.padding; + } + if (state.mention || state.reaction) { + UnreadBadgeStyle st; + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.muted = state.mention + ? state.mentionMuted + : state.reactionMuted; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = PaintUnreadBadge(p, counter, right, top, st); + (state.mention + ? st::dialogsUnreadMention.icon + : st::dialogsUnreadReaction.icon).paintInCenter(p, badge); + right -= badge.width() + st.padding + st::dialogsUnreadPadding; + } +} + +HorizontalButton::HorizontalButton( + not_null<QWidget*> parent, + const style::SettingsSlider &st, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data) +: SubsectionButton(parent, delegate, std::move(data)) +, _st(st) { + dataUpdatedHook(); +} + +void HorizontalButton::updateSize() { + auto width = _st.strictSkip + _text.maxWidth(); + + const auto &state = _data.badges; + UnreadBadgeStyle st; + if (state.unread) { + const auto counter = (state.unreadCounter <= 0) + ? QString() + : QString::number(state.unreadCounter); + const auto badge = CountUnreadBadgeSize(counter, st); + width += badge.width() + st.padding; + } + if (state.mention || state.reaction) { + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = CountUnreadBadgeSize(counter, st); + width += badge.width() + st.padding + st::dialogsUnreadPadding; + } + resize(width, _st.height); +} + +void HorizontalButton::dataUpdatedHook() { + auto context = _delegate->buttonContext(); + context.repaint = [=] { update(); }; + _text.setMarkedText( + _st.labelStyle, + _data.text, + kDefaultTextOptions, + context); + updateSize(); +} + +void HorizontalButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto active = _delegate->buttonActive(this); + + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + paintRipple(p, QPoint(0, 0), &color); + + p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active)); + _text.draw(p, { + .position = QPoint(_st.strictSkip / 2, _st.labelTop), + .outerWidth = width(), + .availableWidth = _text.maxWidth(), + .paused = _delegate->buttonPaused(), + }); + + auto right = width() - _st.strictSkip + (_st.strictSkip / 2); + UnreadBadgeStyle st; + const auto &state = _data.badges; + const auto badgeTop = (height() - st.size) / 2; + if (state.unread) { + st.muted = state.unreadMuted; + const auto counter = (state.unreadCounter <= 0) + ? QString() + : QString::number(state.unreadCounter); + const auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st); + right -= badge.width() + st.padding; + } + if (state.mention || state.reaction) { + UnreadBadgeStyle st; + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.muted = state.mention + ? state.mentionMuted + : state.reactionMuted; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st); + (state.mention + ? st::dialogsUnreadMention.icon + : st::dialogsUnreadReaction.icon).paintInCenter(p, badge); + right -= badge.width() + st.padding + st::dialogsUnreadPadding; + } +} + +} // namespace + +SubsectionButton::SubsectionButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data) +: RippleButton(parent, st::defaultRippleAnimationBgOver) +, _delegate(delegate) +, _data(std::move(data)) { +} + +SubsectionButton::~SubsectionButton() = default; + +void SubsectionButton::setData(SubsectionTab &&data) { + _data = std::move(data); + dataUpdatedHook(); + update(); +} + +DynamicImage *SubsectionButton::userpic() const { + return _data.userpic.get(); +} + +void SubsectionButton::setActiveShown(float64 activeShown) { + if (_activeShown != activeShown) { + _activeShown = activeShown; + update(); + } +} + +void SubsectionButton::contextMenuEvent(QContextMenuEvent *e) { + _delegate->buttonContextMenu(this, e); +} + +SubsectionSlider::SubsectionSlider(not_null<QWidget*> parent, bool vertical) +: RpWidget(parent) +, _vertical(vertical) +, _barSt(vertical + ? st::chatTabsOutlineVertical + : st::chatTabsOutlineHorizontal) +, _bar(CreateChild<RpWidget>(this)) +, _barRect(_barSt.radius, _barSt.fg) { + setupBar(); +} + +SubsectionSlider::~SubsectionSlider() = default; + +void SubsectionSlider::setupBar() { + _bar->setAttribute(Qt::WA_TransparentForMouseEvents); + sizeValue() | rpl::start_with_next([=](QSize size) { + const auto thickness = _barSt.stroke - (_barSt.stroke / 2); + _bar->setGeometry( + 0, + _vertical ? 0 : (size.height() - thickness), + _vertical ? thickness : size.width(), + _vertical ? size.height() : thickness); + }, _bar->lifetime()); + _bar->paintRequest() | rpl::start_with_next([=](QRect clip) { + const auto start = -_barSt.stroke / 2; + const auto currentRange = getCurrentActiveRange(); + const auto from = currentRange.from + _barSt.skip; + const auto size = currentRange.size - 2 * _barSt.skip; + if (size <= 0) { + return; + } + const auto rect = myrtlrect( + _vertical ? start : from, + _vertical ? from : 0, + _vertical ? _barSt.stroke : size, + _vertical ? size : _barSt.stroke); + if (rect.intersects(clip)) { + auto p = QPainter(_bar); + _barRect.paint(p, rect); + } + }, _bar->lifetime()); +} + +void SubsectionSlider::setSections( + SubsectionTabs sections, + Fn<bool()> paused) { + Expects(!sections.tabs.empty()); + + _context = sections.context; + _paused = std::move(paused); + _fixedCount = sections.fixed; + _pinnedCount = sections.pinned; + _reorderAllowed = sections.reorder; + + auto old = base::take(_tabs); + _tabs.reserve(sections.tabs.size()); + + auto size = 0; + for (auto &data : sections.tabs) { + const auto i = data.userpic + ? ranges::find( + old, + data.userpic.get(), + &SubsectionButton::userpic) + : old.empty() + ? end(old) + : (end(old) - 1); + if (i != end(old)) { + _tabs.push_back(std::move(*i)); + old.erase(i); + _tabs.back()->setData(std::move(data)); + } else { + _tabs.push_back(makeButton(std::move(data))); + _tabs.back()->show(); + } + _tabs.back()->move(_vertical ? 0 : size, _vertical ? size : 0); + + const auto index = int(_tabs.size()) - 1; + _tabs.back()->setClickedCallback([=] { + activate(index); + }); + size += _vertical ? _tabs.back()->height() : _tabs.back()->width(); + } + + if (!_tabs.empty()) { + resize( + _vertical ? _tabs.front()->width() : size, + _vertical ? size : _tabs.front()->height()); + } + + _bar->raise(); +} + +void SubsectionSlider::activate(int index) { + if (_active == index) { + return; + } + const auto old = _active; + const auto was = getFinalActiveRange(); + _active = index; + const auto now = getFinalActiveRange(); + const auto callback = [=] { + _bar->update(); + for (auto i = std::min(old, index); i != std::max(old, index); ++i) { + if (i >= 0 && i < int(_tabs.size())) { + _tabs[i]->update(); + } + } + }; + const auto weak = MakeWeak(_bar); + _sectionActivated.fire_copy(index); + if (weak) { + const auto duration = st::chatTabsSlider.duration; + _activeFrom.start(callback, was.from, now.from, duration); + _activeSize.start(callback, was.size, now.size, duration); + _requestShown.fire_copy({ now.from, now.from + now.size }); + } +} + +void SubsectionSlider::setActiveSectionFast(int active) { + Expects(active < int(_tabs.size())); + + if (_active == active) { + return; + } + _active = active; + _activeFrom.stop(); + _activeSize.stop(); + const auto now = getFinalActiveRange(); + _requestShown.fire({ now.from, now.from + now.size }); + _bar->update(); +} + +rpl::producer<ScrollToRequest> SubsectionSlider::requestShown() const { + return _requestShown.events(); +} + +int SubsectionSlider::sectionsCount() const { + return int(_tabs.size()); +} + +rpl::producer<int> SubsectionSlider::sectionActivated() const { + return _sectionActivated.events(); +} + +rpl::producer<int> SubsectionSlider::sectionContextMenu() const { + return _sectionContextMenu.events(); +} + +int SubsectionSlider::lookupSectionPosition(int index) const { + Expects(index >= 0 && index < _tabs.size()); + + return _vertical ? _tabs[index]->y() : _tabs[index]->x(); +} + +void SubsectionSlider::paintEvent(QPaintEvent *e) { +} + +int SubsectionSlider::lookupSectionIndex(QPoint position) const { + Expects(!_tabs.empty()); + + const auto count = sectionsCount(); + if (_vertical) { + for (auto i = 0; i != count; ++i) { + const auto tab = _tabs[i].get(); + if (position.y() < tab->y() + tab->height()) { + return i; + } + } + } else { + for (auto i = 0; i != count; ++i) { + const auto tab = _tabs[i].get(); + if (position.x() < tab->x() + tab->width()) { + return i; + } + } + } + return count - 1; +} + +SubsectionSlider::Range SubsectionSlider::getFinalActiveRange() const { + if (_active < 0 || _active >= _tabs.size()) { + return {}; + } + const auto tab = _tabs[_active].get(); + return Range{ + .from = _vertical ? tab->y() : tab->x(), + .size = _vertical ? tab->height() : tab->width(), + }; +} + +SubsectionSlider::Range SubsectionSlider::getCurrentActiveRange() const { + const auto finalRange = getFinalActiveRange(); + return { + .from = int(base::SafeRound(_activeFrom.value(finalRange.from))), + .size = int(base::SafeRound(_activeSize.value(finalRange.size))), + }; +} + +bool SubsectionSlider::buttonPaused() { + return _paused && _paused(); +} + +float64 SubsectionSlider::buttonActive(not_null<SubsectionButton*> button) { + const auto currentRange = getCurrentActiveRange(); + const auto from = _vertical ? button->y() : button->x(); + const auto size = _vertical ? button->height() : button->width(); + const auto checkSize = std::min(size, currentRange.size); + return (checkSize > 0) + ? (1. - (std::abs(currentRange.from - from) / float64(checkSize))) + : 0.; +} + +void SubsectionSlider::buttonContextMenu( + not_null<SubsectionButton*> button, + not_null<QContextMenuEvent*> e) { + const auto i = ranges::find( + _tabs, + button.get(), + &std::unique_ptr<SubsectionButton>::get); + Assert(i != end(_tabs)); + + _sectionContextMenu.fire(int(i - begin(_tabs))); + e->accept(); +} + +Text::MarkedContext SubsectionSlider::buttonContext() { + return _context; +} + +not_null<SubsectionButton*> SubsectionSlider::buttonAt(int index) { + Expects(index >= 0 && index < _tabs.size()); + + return _tabs[index].get(); +} + +VerticalSlider::VerticalSlider(not_null<QWidget*> parent) +: SubsectionSlider(parent, true) { +} + +VerticalSlider::~VerticalSlider() = default; + +std::unique_ptr<SubsectionButton> VerticalSlider::makeButton( + SubsectionTab &&data) { + return std::make_unique<VerticalButton>( + this, + static_cast<SubsectionButtonDelegate*>(this), + std::move(data)); +} + +HorizontalSlider::HorizontalSlider(not_null<QWidget*> parent) +: SubsectionSlider(parent, false) +, _st(st::chatTabsSlider) { +} + +HorizontalSlider::~HorizontalSlider() = default; + +std::unique_ptr<SubsectionButton> HorizontalSlider::makeButton( + SubsectionTab &&data) { + return std::make_unique<HorizontalButton>( + this, + _st, + static_cast<SubsectionButtonDelegate*>(this), + std::move(data)); +} + +std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail( + Fn<QColor()> textColor) { + class Image final : public DynamicImage { + public: + Image(Fn<QColor()> textColor) : _textColor(std::move(textColor)) { + Expects(_textColor != nullptr); + } + + std::shared_ptr<DynamicImage> clone() { + return std::make_shared<Image>(_textColor); + } + + QImage image(int size) { + const auto ratio = style::DevicePixelRatio(); + const auto full = size * ratio; + const auto color = _textColor(); + if (_cache.size() != QSize(full, full)) { + _cache = QImage( + QSize(full, full), + QImage::Format_ARGB32_Premultiplied); + _cache.fill(Qt::TransparentMode); + } else if (_color == color) { + return _cache; + } + _color = color; + if (_mask.isNull()) { + _mask = st::foldersAll.instance(QColor(255, 255, 255)); + } + const auto position = ratio * QPoint( + (size - (_mask.width() / ratio)) / 2, + (size - (_mask.height() / ratio)) / 2); + if (_mask.width() <= full && _mask.height() <= full) { + style::colorizeImage(_mask, color, &_cache, QRect(), position); + } else { + _cache = style::colorizeImage(_mask, color).scaled( + full, + full, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + _cache.setDevicePixelRatio(ratio); + } + return _cache; + } + void subscribeToUpdates(Fn<void()> callback) { + if (!callback) { + _cache = QImage(); + _mask = QImage(); + } + } + + private: + Fn<QColor()> _textColor; + QImage _mask; + QImage _cache; + QColor _color; + + }; + return std::make_shared<Image>(std::move(textColor)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h new file mode 100644 index 0000000000..2c82684ef9 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -0,0 +1,174 @@ +/* +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 "dialogs/dialogs_common.h" +#include "ui/round_rect.h" +#include "ui/rp_widget.h" +#include "ui/widgets/buttons.h" + +namespace style { +struct ChatTabsVertical; +struct ChatTabsOutline; +} // namespace style + +namespace Ui { + +class DynamicImage; +class RippleAnimation; +class SubsectionButton; +struct ScrollToRequest; + +struct SubsectionTab { + TextWithEntities text; + std::shared_ptr<DynamicImage> userpic; + Dialogs::BadgesState badges; +}; + +struct SubsectionTabs { + std::vector<SubsectionTab> tabs; + Text::MarkedContext context; + int fixed = 0; + int pinned = 0; + bool reorder = false; +}; + +class SubsectionButtonDelegate { +public: + virtual bool buttonPaused() = 0; + virtual float64 buttonActive(not_null<SubsectionButton*> button) = 0; + virtual Text::MarkedContext buttonContext() = 0; + virtual void buttonContextMenu( + not_null<SubsectionButton*> button, + not_null<QContextMenuEvent*> e) = 0; +}; + +class SubsectionButton : public RippleButton { +public: + SubsectionButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data); + ~SubsectionButton(); + + void setData(SubsectionTab &&data); + [[nodiscard]] DynamicImage *userpic() const; + + void setActiveShown(float64 activeShown); + +protected: + virtual void dataUpdatedHook() = 0; + + void contextMenuEvent(QContextMenuEvent *e) override; + + const not_null<SubsectionButtonDelegate*> _delegate; + SubsectionTab _data; + float64 _activeShown = 0.; + +}; + +class SubsectionSlider + : public RpWidget + , public SubsectionButtonDelegate { +public: + ~SubsectionSlider(); + + void setSections( + SubsectionTabs sections, + Fn<bool()> paused); + void setActiveSectionFast(int active); + + [[nodiscard]] int sectionsCount() const; + [[nodiscard]] rpl::producer<int> sectionActivated() const; + [[nodiscard]] rpl::producer<int> sectionContextMenu() const; + [[nodiscard]] int lookupSectionPosition(int index) const; + + bool buttonPaused() override; + float64 buttonActive(not_null<SubsectionButton*> button) override; + void buttonContextMenu( + not_null<SubsectionButton*> button, + not_null<QContextMenuEvent*> e) override; + Text::MarkedContext buttonContext() override; + [[nodiscard]] not_null<SubsectionButton*> buttonAt(int index); + + [[nodiscard]] rpl::producer<ScrollToRequest> requestShown() const; + +protected: + struct Range { + int from = 0; + int size = 0; + }; + + SubsectionSlider(not_null<QWidget*> parent, bool vertical); + void setupBar(); + + void paintEvent(QPaintEvent *e) override; + + [[nodiscard]] int lookupSectionIndex(QPoint position) const; + [[nodiscard]] Range getFinalActiveRange() const; + [[nodiscard]] Range getCurrentActiveRange() const; + void activate(int index); + + [[nodiscard]] virtual std::unique_ptr<SubsectionButton> makeButton( + SubsectionTab &&data) = 0; + + const bool _vertical = false; + + const style::ChatTabsOutline &_barSt; + RpWidget *_bar = nullptr; + RoundRect _barRect; + + std::vector<std::unique_ptr<SubsectionButton>> _tabs; + int _active = -1; + int _pressed = -1; + Animations::Simple _activeFrom; + Animations::Simple _activeSize; + + //int _buttonIndexHint = 0; + + Text::MarkedContext _context; + int _fixedCount = 0; + int _pinnedCount = 0; + bool _reorderAllowed = false; + + rpl::event_stream<int> _sectionActivated; + rpl::event_stream<int> _sectionContextMenu; + Fn<bool()> _paused; + + rpl::event_stream<ScrollToRequest> _requestShown; + +}; + +class VerticalSlider final : public SubsectionSlider { +public: + explicit VerticalSlider(not_null<QWidget*> parent); + ~VerticalSlider(); + +private: + std::unique_ptr<SubsectionButton> makeButton( + SubsectionTab &&data) override; + +}; + +class HorizontalSlider final : public SubsectionSlider { +public: + explicit HorizontalSlider(not_null<QWidget*> parent); + ~HorizontalSlider(); + +private: + std::unique_ptr<SubsectionButton> makeButton( + SubsectionTab &&data) override; + + const style::SettingsSlider &_st; + +}; + +[[nodiscard]] std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail( + Fn<QColor()> textColor); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/ton_common.cpp b/Telegram/SourceFiles/ui/controls/ton_common.cpp new file mode 100644 index 0000000000..4f66b4b786 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/ton_common.cpp @@ -0,0 +1,240 @@ +/* +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 "ui/controls/ton_common.h" + +#include "base/qthelp_url.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/ui_utility.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +//#include "styles/style_wallet.h" + +#include <QtCore/QLocale> + +namespace Ui { +namespace { + +constexpr auto kOneTon = kNanosInOne; +constexpr auto kNanoDigits = 9; + +struct FixedAmount { + QString text; + int position = 0; +}; + +std::optional<int64> ParseAmountTons(const QString &trimmed) { + auto ok = false; + const auto grams = int64(trimmed.toLongLong(&ok)); + return (ok + && (grams <= std::numeric_limits<int64>::max() / kOneTon) + && (grams >= std::numeric_limits<int64>::min() / kOneTon)) + ? std::make_optional(grams * kOneTon) + : std::nullopt; +} + +std::optional<int64> ParseAmountNano(QString trimmed) { + while (trimmed.size() < kNanoDigits) { + trimmed.append('0'); + } + auto zeros = 0; + for (const auto ch : trimmed) { + if (ch == '0') { + ++zeros; + } else { + break; + } + } + if (zeros == trimmed.size()) { + return 0; + } else if (trimmed.size() > kNanoDigits) { + return std::nullopt; + } + auto ok = false; + const auto value = trimmed.mid(zeros).toLongLong(&ok); + return (ok && value > 0 && value < kOneTon) + ? std::make_optional(value) + : std::nullopt; +} + +[[nodiscard]] FixedAmount FixTonAmountInput( + const QString &was, + const QString &text, + int position) { + constexpr auto kMaxDigitsCount = 9; + const auto separator = FormatTonAmount(1).separator; + + auto result = FixedAmount{ text, position }; + if (text.isEmpty()) { + return result; + } else if (text.startsWith('.') + || text.startsWith(',') + || text.startsWith(separator)) { + result.text.prepend('0'); + ++result.position; + } + auto separatorFound = false; + auto digitsCount = 0; + for (auto i = 0; i != result.text.size();) { + const auto ch = result.text[i]; + const auto atSeparator = QStringView(result.text).mid(i).startsWith(separator); + if (ch >= '0' && ch <= '9' && digitsCount < kMaxDigitsCount) { + ++i; + ++digitsCount; + continue; + } else if (!separatorFound + && (atSeparator || ch == '.' || ch == ',')) { + separatorFound = true; + if (!atSeparator) { + result.text.replace(i, 1, separator); + } + digitsCount = 0; + i += separator.size(); + continue; + } + result.text.remove(i, 1); + if (result.position > i) { + --result.position; + } + } + if (result.text == "0" && result.position > 0) { + if (was.startsWith('0')) { + result.text = QString(); + result.position = 0; + } else { + result.text += separator; + result.position += separator.size(); + } + } + return result; +} + +} // namespace + +FormattedTonAmount FormatTonAmount(int64 amount, TonFormatFlags flags) { + auto result = FormattedTonAmount(); + const auto grams = amount / kOneTon; + const auto preciseNanos = std::abs(amount) % kOneTon; + auto roundedNanos = preciseNanos; + if (flags & TonFormatFlag::Rounded) { + if (std::abs(grams) >= 1'000'000 && (roundedNanos % 1'000'000)) { + roundedNanos -= (roundedNanos % 1'000'000); + } else if (std::abs(grams) >= 1'000 && (roundedNanos % 1'000)) { + roundedNanos -= (roundedNanos % 1'000); + } + } + const auto precise = (roundedNanos == preciseNanos); + auto nanos = preciseNanos; + auto zeros = 0; + while (zeros < kNanoDigits && nanos % 10 == 0) { + nanos /= 10; + ++zeros; + } + const auto system = QLocale::system(); + const auto locale = (flags & TonFormatFlag::Simple) + ? QLocale::c() + : system; + const auto separator = system.decimalPoint(); + + result.wholeString = locale.toString(grams); + if ((flags & TonFormatFlag::Signed) && amount > 0) { + result.wholeString = locale.positiveSign() + result.wholeString; + } else if (amount < 0 && grams == 0) { + result.wholeString = locale.negativeSign() + result.wholeString; + } + result.full = result.wholeString; + if (zeros < kNanoDigits) { + result.separator = separator; + result.nanoString = QString("%1" + ).arg(nanos, kNanoDigits - zeros, 10, QChar('0')); + if (!precise) { + const auto nanoLength = (std::abs(grams) >= 1'000'000) + ? 3 + : (std::abs(grams) >= 1'000) + ? 6 + : 9; + result.nanoString = result.nanoString.mid(0, nanoLength); + } + result.full += separator + result.nanoString; + } + return result; +} + +std::optional<int64> ParseTonAmountString(const QString &amount) { + const auto trimmed = amount.trimmed(); + const auto separator = QString(QLocale::system().decimalPoint()); + const auto index1 = trimmed.indexOf('.'); + const auto index2 = trimmed.indexOf(','); + const auto index3 = (separator == "." || separator == ",") + ? -1 + : trimmed.indexOf(separator); + const auto found = (index1 >= 0 ? 1 : 0) + + (index2 >= 0 ? 1 : 0) + + (index3 >= 0 ? 1 : 0); + if (found > 1) { + return std::nullopt; + } + const auto index = (index1 >= 0) + ? index1 + : (index2 >= 0) + ? index2 + : index3; + const auto used = (index1 >= 0) + ? "." + : (index2 >= 0) + ? "," + : separator; + const auto grams = ParseAmountTons(trimmed.mid(0, index)); + const auto nano = ParseAmountNano(trimmed.mid(index + used.size())); + if (index < 0 || index == trimmed.size() - used.size()) { + return grams; + } else if (index == 0) { + return nano; + } else if (!nano || !grams) { + return std::nullopt; + } + return *grams + (*grams < 0 ? (-*nano) : (*nano)); +} + +QString TonAmountSeparator() { + return FormatTonAmount(1).separator; +} + +not_null<Ui::InputField*> CreateTonAmountInput( + not_null<QWidget*> parent, + rpl::producer<QString> placeholder, + int64 amount) { + const auto result = Ui::CreateChild<Ui::InputField>( + parent.get(), + st::editTagField, + Ui::InputField::Mode::SingleLine, + std::move(placeholder), + (amount > 0 + ? FormatTonAmount(amount, TonFormatFlag::Simple).full + : QString())); + const auto lastAmountValue = std::make_shared<QString>(); + result->changes() | rpl::start_with_next([=] { + Ui::PostponeCall(result, [=] { + const auto position = result->textCursor().position(); + const auto now = result->getLastText(); + const auto fixed = FixTonAmountInput( + *lastAmountValue, + now, + position); + *lastAmountValue = fixed.text; + if (fixed.text == now) { + return; + } + result->setText(fixed.text); + result->setFocusFast(); + result->setCursorPosition(fixed.position); + }); + }, result->lifetime()); + return result; +} + +} // namespace Wallet diff --git a/Telegram/SourceFiles/ui/controls/ton_common.h b/Telegram/SourceFiles/ui/controls/ton_common.h new file mode 100644 index 0000000000..189a793d8b --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/ton_common.h @@ -0,0 +1,46 @@ +/* +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/flags.h" + +namespace Ui { + +class InputField; + +inline constexpr auto kNanosInOne = 1'000'000'000LL; + +struct FormattedTonAmount { + QString wholeString; + QString separator; + QString nanoString; + QString full; +}; + +enum class TonFormatFlag { + Signed = 0x01, + Rounded = 0x02, + Simple = 0x04, +}; +constexpr bool is_flag_type(TonFormatFlag) { return true; }; +using TonFormatFlags = base::flags<TonFormatFlag>; + +[[nodiscard]] FormattedTonAmount FormatTonAmount( + int64 amount, + TonFormatFlags flags = TonFormatFlags()); +[[nodiscard]] std::optional<int64> ParseTonAmountString( + const QString &amount); + +[[nodiscard]] QString TonAmountSeparator(); + +[[nodiscard]] not_null<Ui::InputField*> CreateTonAmountInput( + not_null<QWidget*> parent, + rpl::producer<QString> placeholder, + int64 amount = 0); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 39faf9ca80..ed382472df 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -160,12 +160,12 @@ UserpicButton::UserpicButton( not_null<Window::Controller*> window, Role role, const style::UserpicButton &st, - bool forceForumShape) + PeerUserpicShape shape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(window->sessionController()) , _window(window) -, _forceForumShape(forceForumShape) +, _shape(shape) , _role(role) { Expects(_role == Role::ChangePhoto || _role == Role::ChoosePhoto); @@ -180,12 +180,14 @@ UserpicButton::UserpicButton( not_null<PeerData*> peer, Role role, Source source, - const style::UserpicButton &st) + const style::UserpicButton &st, + PeerUserpicShape shape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(controller) , _window(&controller->window()) , _peer(peer) +, _shape(shape) , _role(role) , _source(source) { if (_source == Source::Custom) { @@ -200,10 +202,12 @@ UserpicButton::UserpicButton( UserpicButton::UserpicButton( QWidget *parent, not_null<PeerData*> peer, - const style::UserpicButton &st) + const style::UserpicButton &st, + PeerUserpicShape shape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _peer(peer) +, _shape(shape) , _role(Role::Custom) , _source(Source::PeerPhoto) { Expects(_role != Role::OpenPhoto); @@ -403,7 +407,7 @@ void UserpicButton::choosePhotoLocally() { CameraBox, _window, _peer, - _forceForumShape, + (_shape == PeerUserpicShape::Forum), callback(ChosenType::Set))); }, &st::menuIconPhotoSet); } @@ -644,7 +648,8 @@ void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) { auto size = QSize{ _st.photoSize, _st.photoSize }; const auto ratio = style::DevicePixelRatio(); request.outer = request.resize = size * ratio; - if (useForumShape()) { + if (_shape == PeerUserpicShape::Monoforum) { + } else if (useForumShape()) { const auto radius = int(_st.photoSize * Ui::ForumUserpicRadiusMultiplier()); if (_roundingCorners[0].width() != radius * ratio) { @@ -657,7 +662,24 @@ void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) { } request.mask = _ellipseMask; } - p.drawImage(QRect(photoPosition, size), _streamed->frame(request)); + auto frame = _streamed->frame(request); + + if (_shape == PeerUserpicShape::Monoforum) { + if (_monoforumMask.isNull()) { + _monoforumMask = MonoforumShapeMask(request.resize); + } + constexpr auto format = QImage::Format_ARGB32_Premultiplied; + if (frame.format() != format) { + frame = std::move(frame).convertToFormat(format); + } + auto q = QPainter(&frame); + q.setCompositionMode(QPainter::CompositionMode_DestinationIn); + q.drawImage( + QRect(QPoint(), frame.size() / frame.devicePixelRatio()), + _monoforumMask); + q.end(); + } + p.drawImage(QRect(photoPosition, size), frame); if (!paused) { _streamed->markFrameShown(); } @@ -888,7 +910,8 @@ void UserpicButton::processNewPeerPhoto() { } bool UserpicButton::useForumShape() const { - return _forceForumShape || (_peer && _peer->isForum()); + return (_shape == PeerUserpicShape::Forum) + || (_peer && _peer->isForum() && _shape == PeerUserpicShape::Auto); } void UserpicButton::grabOldUserpic() { @@ -940,8 +963,8 @@ void UserpicButton::switchChangePhotoOverlay( } } -void UserpicButton::forceForumShape(bool force) { - _forceForumShape = force; +void UserpicButton::overrideShape(PeerUserpicShape shape) { + _shape = shape; prepare(); } @@ -1077,28 +1100,11 @@ void UserpicButton::prepareUserpicPixmap() { _userpic = CreateSquarePixmap(size, [&](Painter &p) { if (_userpicHasImage) { if (_showPeerUserpic) { - if (useForumShape()) { - const auto ratio = style::DevicePixelRatio(); - if (const auto cloud = _peer->userpicCloudImage(_userpicView)) { - Ui::ValidateUserpicCache( - _userpicView, - cloud, - nullptr, - size * ratio, - true); - p.drawImage(QRect(0, 0, size, size), _userpicView.cached); - } else { - const auto empty = PeerData::GenerateUserpicImage( - _peer, - _userpicView, - size * ratio, - (size * ratio) - * Ui::ForumUserpicRadiusMultiplier()); - p.drawImage(QRect(0, 0, size, size), empty); - } - } else { - _peer->paintUserpic(p, _userpicView, 0, 0, size); - } + _peer->paintUserpic(p, _userpicView, { + .position = QPoint(), + .size = size, + .shape = _shape, + }); } else if (_nonPersonalView) { using Size = Data::PhotoSize; if (const auto full = _nonPersonalView->image(Size::Large)) { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index 07abeec1a2..f126fac07a 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -62,18 +62,20 @@ public: not_null<::Window::Controller*> window, Role role, const style::UserpicButton &st, - bool forceForumShape = false); + PeerUserpicShape shape = PeerUserpicShape::Auto); UserpicButton( QWidget *parent, not_null<::Window::SessionController*> controller, not_null<PeerData*> peer, Role role, Source source, - const style::UserpicButton &st); + const style::UserpicButton &st, + PeerUserpicShape shape = PeerUserpicShape::Auto); UserpicButton( QWidget *parent, not_null<PeerData*> peer, // Role::Custom, Source::PeerPhoto - const style::UserpicButton &st); + const style::UserpicButton &st, + PeerUserpicShape shape = PeerUserpicShape::Auto); ~UserpicButton(); enum class ChosenType { @@ -94,7 +96,7 @@ public: bool enabled, Fn<void(ChosenImage)> chosen); void showSavedMessagesOnSelf(bool enabled); - void forceForumShape(bool force); + void overrideShape(PeerUserpicShape shape); // Role::ChoosePhoto or Role::ChangePhoto [[nodiscard]] rpl::producer<ChosenImage> chosenImages() const { @@ -161,8 +163,9 @@ private: ::Window::SessionController *_controller = nullptr; ::Window::Controller *_window = nullptr; PeerData *_peer = nullptr; - bool _forceForumShape = false; + PeerUserpicShape _shape = PeerUserpicShape::Auto; PeerUserpicView _userpicView; + QImage _monoforumMask; std::shared_ptr<Data::PhotoMedia> _nonPersonalView; Role _role = Role::ChangePhoto; bool _notShownYet = true; diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp index f6538867a9..32f164776f 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp @@ -196,7 +196,11 @@ private: class EmojiThumbnail final : public DynamicImage { public: - EmojiThumbnail(not_null<Data::Session*> owner, const QString &data); + EmojiThumbnail( + not_null<Data::Session*> owner, + const QString &data, + Fn<bool()> paused, + Fn<QColor()> textColor); std::shared_ptr<DynamicImage> clone() override; @@ -207,6 +211,8 @@ private: const not_null<Data::Session*> _owner; const QString _data; std::unique_ptr<Ui::Text::CustomEmoji> _emoji; + Fn<bool()> _paused; + Fn<QColor()> _textColor; QImage _frame; }; @@ -244,22 +250,13 @@ QImage PeerUserpic::image(int size) { auto p = Painter(&_frame); auto &view = _subscribed->view; - if (!_forceRound) { - _peer->paintUserpic(p, view, 0, 0, size); - } else if (const auto cloud = _peer->userpicCloudImage(view)) { - const auto full = size * style::DevicePixelRatio(); - Ui::ValidateUserpicCache(view, cloud, nullptr, full, false); - p.drawImage(QRect(0, 0, size, size), view.cached); - } else { - const auto full = size * style::DevicePixelRatio(); - const auto r = full / 2.; - const auto empty = PeerData::GenerateUserpicImage( - _peer, - view, - full, - r); - p.drawImage(QRect(0, 0, size, size), empty); - } + _peer->paintUserpic(p, view, { + .position = QPoint(), + .size = size, + .shape = (_forceRound + ? Ui::PeerUserpicShape::Circle + : Ui::PeerUserpicShape::Auto), + }); } return _frame; } @@ -581,9 +578,13 @@ void IconThumbnail::subscribeToUpdates(Fn<void()> callback) { EmojiThumbnail::EmojiThumbnail( not_null<Data::Session*> owner, - const QString &data) + const QString &data, + Fn<bool()> paused, + Fn<QColor()> textColor) : _owner(owner) -, _data(data) { +, _data(data) +, _paused(std::move(paused)) +, _textColor(std::move(textColor)) { } void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) { @@ -598,7 +599,11 @@ void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) { } std::shared_ptr<DynamicImage> EmojiThumbnail::clone() { - return std::make_shared<EmojiThumbnail>(_owner, _data); + return std::make_shared<EmojiThumbnail>( + _owner, + _data, + _paused, + _textColor); } QImage EmojiThumbnail::image(int size) { @@ -614,12 +619,16 @@ QImage EmojiThumbnail::image(int size) { } _frame.fill(Qt::transparent); + const auto esize = Text::AdjustCustomEmojiSize( + Emoji::GetSizeLarge() / style::DevicePixelRatio()); + const auto eskip = (size - esize) / 2; + auto p = Painter(&_frame); _emoji->paint(p, { - .textColor = st::windowBoldFg->c, + .textColor = _textColor ? _textColor() : st::windowBoldFg->c, .now = crl::now(), - .position = QPoint(0, 0), - .paused = false, + .position = QPoint(eskip, eskip), + .paused = _paused && _paused(), }); p.end(); @@ -665,8 +674,14 @@ std::shared_ptr<DynamicImage> MakeIconThumbnail(const style::icon &icon) { std::shared_ptr<DynamicImage> MakeEmojiThumbnail( not_null<Data::Session*> owner, - const QString &data) { - return std::make_shared<EmojiThumbnail>(owner, data); + const QString &data, + Fn<bool()> paused, + Fn<QColor()> textColor) { + return std::make_shared<EmojiThumbnail>( + owner, + data, + std::move(paused), + std::move(textColor)); } std::shared_ptr<DynamicImage> MakePhotoThumbnail( diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.h b/Telegram/SourceFiles/ui/dynamic_thumbnails.h index 08ae74052a..e58c300fab 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.h +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.h @@ -33,7 +33,9 @@ class DynamicImage; const style::icon &icon); [[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail( not_null<Data::Session*> owner, - const QString &data); + const QString &data, + Fn<bool()> paused = nullptr, + Fn<QColor()> textColor = nullptr); [[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnail( not_null<PhotoData*> photo, FullMsgId fullId); diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index b06ef54a64..3c865fead6 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -18,8 +18,9 @@ creditsSettingsBigBalance: FlatLabel(defaultFlatLabel) { } creditsSettingsBigBalanceSkip: 4px; creditsSettingsBigBalanceButton: RoundButton(defaultActiveButton) { - height: 42px; - textTop: 12px; + width: 240px; + height: 40px; + textTop: 11px; style: semiboldTextStyle; } creditsSettingsBigBalanceButtonGift: RoundButton(defaultLightButton) { @@ -29,9 +30,13 @@ creditsSettingsBigBalanceButtonGift: RoundButton(defaultLightButton) { } creditsPremiumCover: PremiumCover(defaultPremiumCover) { + starTopSkip: 39px; + titleFont: font(15px semibold); about: FlatLabel(userPremiumCoverAbout) { textFg: boxTitleFg; } + aboutMaxWidth: 236px; + additionalShadowForDarkThemes: false; } creditsLowBalancePremiumCover: PremiumCover(creditsPremiumCover) { starSize: size(64px, 62px); @@ -214,16 +219,12 @@ giftBoxLockMargins: margins(-2px, 1px, 0px, 0px); giftBoxPinIcon: icon {{ "dialogs/dialogs_pinned", premiumButtonFg }}; creditsHistoryEntriesList: PeerList(defaultPeerList) { - padding: margins( - 0px, - 7px, - 0px, - 7px); + padding: margins(0px, 7px, 0px, 7px); item: PeerListItem(defaultPeerListItem) { - height: 66px; - photoPosition: point(18px, 6px); + height: 72px; + photoPosition: point(18px, 7px); namePosition: point(70px, 6px); - statusPosition: point(70px, 43px); + statusPosition: point(70px, 46px); photoSize: 42px; } } @@ -297,3 +298,18 @@ giftTooManyPinnedBox: Box(giftBox) { giftTooManyPinnedChoose: FlatLabel(giftBoxAbout) { textFg: windowSubTextFg; } + +creditsHistoryTabsSlider: SettingsSlider(defaultTabsSlider) { + height: 39px; + labelTop: 7px; + barTop: 36px; + barSkip: 0px; + rippleBottomSkip: 0px; +} +creditsHistoryTabsSliderPadding: margins(14px, 0px, 24px, 0px); +creditsHistoryRowDescriptionSkip: 20px; +creditsHistoryRowRightTop: 16px; +creditsHistoryRowRightMinorTop: 18px; +creditsHistoryRowRightStyle: TextStyle(defaultTextStyle) { + font: font(fsize); +} diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp index d7822ee730..18e5c01eee 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp @@ -26,7 +26,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/empty_userpic.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/text/format_values.h" #include "ui/text/text_custom_emoji.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/fields/number_input.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" @@ -164,21 +166,25 @@ not_null<RpWidget*> CreateSingleStarWidget( not_null<MaskedInputField*> AddInputFieldForCredits( not_null<VerticalLayout*> container, - rpl::producer<StarsAmount> value) { + rpl::producer<CreditsAmount> value) { const auto &st = st::botEarnInputField; const auto inputContainer = container->add( CreateSkipWidget(container, st.heightMin)); - const auto currentValue = rpl::variable<StarsAmount>( + const auto currentValue = rpl::variable<CreditsAmount>( rpl::duplicate(value)); const auto input = CreateChild<NumberInput>( inputContainer, st, - tr::lng_bot_earn_out_ph(), + tr::lng_bot_earn_out_ph_max( + lt_amount, + currentValue.value() | rpl::map([](CreditsAmount amount) { + return QString::number(amount.whole()); + })), QString::number(currentValue.current().whole()), currentValue.current().whole()); rpl::duplicate( value - ) | rpl::start_with_next([=](StarsAmount v) { + ) | rpl::start_with_next([=](CreditsAmount v) { input->changeLimit(v.whole()); input->setText(QString::number(v.whole())); }, input->lifetime()); @@ -691,4 +697,18 @@ std::unique_ptr<Ui::Text::CustomEmoji> MakeCreditsIconEmoji( u"credits_icon:%1:%2"_q.arg(height).arg(count)); } +Ui::Text::MarkedContext MakeCreditsIconContext(int height, int count) { + auto customEmojiFactory = [=]( + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr<Ui::Text::CustomEmoji> { + return MakeCreditsIconEmoji(height, count); + }; + return { .customEmojiFactory = std::move(customEmojiFactory) }; +} + +TextWithEntities MakeCreditsIconEntity() { + return Ui::Text::SingleCustomEmoji(Ui::kCreditsCurrency); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.h b/Telegram/SourceFiles/ui/effects/credits_graphics.h index 022c7cac80..08496856d1 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.h +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.h @@ -21,6 +21,7 @@ class Session; namespace Ui::Text { class CustomEmoji; +struct MarkedContext; } // namespace Ui::Text namespace Ui { @@ -44,7 +45,7 @@ using PaintRoundImageCallback = Fn<void( [[nodiscard]] not_null<Ui::MaskedInputField*> AddInputFieldForCredits( not_null<Ui::VerticalLayout*> container, - rpl::producer<StarsAmount> value); + rpl::producer<CreditsAmount> value); PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( const Data::CreditsHistoryEntry &entry); @@ -86,8 +87,12 @@ Fn<void(QPainter &)> PaintOutlinedColoredCreditsIconCallback( [[nodiscard]] QImage CreditsWhiteDoubledIcon(int size, float64 outlineRatio); -std::unique_ptr<Ui::Text::CustomEmoji> MakeCreditsIconEmoji( +[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> MakeCreditsIconEmoji( int height, int count); +[[nodiscard]] Ui::Text::MarkedContext MakeCreditsIconContext( + int height, + int count); +[[nodiscard]] TextWithEntities MakeCreditsIconEntity(); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 38022321e8..86d54300bd 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -33,6 +33,7 @@ PremiumCover { titlePadding: margins; titleFont: font; about: FlatLabel; + aboutMaxWidth: pixels; additionalShadowForDarkThemes: bool; } ComposePremiumRequired { @@ -64,6 +65,7 @@ defaultPremiumCover: PremiumCover { textFg: premiumButtonFg; minWidth: 190px; } + aboutMaxWidth: 0px; additionalShadowForDarkThemes: true; } userPremiumCoverAbout: FlatLabel(boxDividerLabel) { diff --git a/Telegram/SourceFiles/ui/effects/premium_stars.cpp b/Telegram/SourceFiles/ui/effects/premium_stars.cpp index 23d8ad355e..7323500ea1 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_stars.cpp @@ -14,35 +14,46 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace Premium { +namespace { +using Type = MiniStarsType; constexpr auto kDeformationMax = 0.1; +} // namespace + MiniStars::MiniStars( Fn<void(const QRect &r)> updateCallback, bool opaque, Type type) -: _availableAngles((type != Type::SlowStars) -? std::vector<Interval>{ - Interval{ -10, 40 }, - Interval{ 180 + 10 - 40, 40 }, - Interval{ 180 + 15, 50 }, - Interval{ -15 - 50, 50 }, -} -: std::vector<Interval>{ Interval{ -90, 180 }, Interval{ 90, 180 } }) -, _lifeLength((type != Type::SlowStars) +: _availableAngles((type != Type::SlowStars && type != Type::SlowDiamondStars) + ? std::vector<Interval>{ + Interval{ -10, 40 }, + Interval{ 180 + 10 - 40, 40 }, + Interval{ 180 + 15, 50 }, + Interval{ -15 - 50, 50 }, + } + : std::vector<Interval>{ Interval{ -90, 180 }, Interval{ 90, 180 } }) +, _lifeLength((type != Type::SlowStars && type != Type::SlowDiamondStars) ? Interval{ 150 / 5, 200 / 5 } : Interval{ 150 * 2, 200 * 2 }) -, _deathTime((type != Type::SlowStars) +, _deathTime((type != Type::SlowStars && type != Type::SlowDiamondStars) ? Interval{ 1500, 2000 } : Interval{ 1500 * 2, 2000 * 2 }) -, _size({ 5, 10 }) +, _size((type != Type::SlowStars) + ? Interval{ 5, 10 } + : Interval{ 2, 4 }) , _alpha({ opaque ? 100 : 40, opaque ? 100 : 60 }) , _sinFactor({ 10, 190 }) , _spritesCount({ 0, ((type == Type::MonoStars) ? 1 : 2) }) -, _appearProgressTill((type != Type::SlowStars) ? 0.2 : 0.01) +, _appearProgressTill((type != Type::SlowStars + && type != Type::SlowDiamondStars) + ? 0.2 + : 0.01) , _disappearProgressAfter(0.8) , _distanceProgressStart(0.5) -, _sprite(u":/gui/icons/settings/starmini.svg"_q) +, _sprite((type == Type::DiamondStars || type == Type::SlowDiamondStars) + ? u":/gui/icons/settings/starmini.svg"_q + : u":/gui/icons/settings/star.svg"_q) , _animation([=](crl::time now) { if (now > _nextBirthTime && !_paused) { createStar(now); diff --git a/Telegram/SourceFiles/ui/effects/premium_stars.h b/Telegram/SourceFiles/ui/effects/premium_stars.h index 0adbd21fe8..ade27a409f 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars.h +++ b/Telegram/SourceFiles/ui/effects/premium_stars.h @@ -14,18 +14,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace Premium { +enum class MiniStarsType { + MonoStars, + BiStars, + SlowStars, + DiamondStars, + SlowDiamondStars, +}; + class MiniStars final { public: - enum class Type { - MonoStars, - BiStars, - SlowStars, - }; - MiniStars( Fn<void(const QRect &r)> updateCallback, bool opaque = false, - Type type = Type::MonoStars); + MiniStarsType type = MiniStarsType::MonoStars); void paint(QPainter &p, const QRectF &rect); void setPaused(bool paused); diff --git a/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp b/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp index 7b7157b504..017e1b154d 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_stars_colored.cpp @@ -226,7 +226,7 @@ bool CollectibleEmoji::readyInDefaultState() { ColoredMiniStars::ColoredMiniStars( not_null<Ui::RpWidget*> parent, bool optimizeUpdate, - MiniStars::Type type) + MiniStarsType type) : _ministars( optimizeUpdate ? Fn<void(const QRect &)>([=](const QRect &r) { @@ -239,7 +239,7 @@ ColoredMiniStars::ColoredMiniStars( ColoredMiniStars::ColoredMiniStars( Fn<void(const QRect &)> update, - MiniStars::Type type) + MiniStarsType type) : _ministars(update, true, type) { } diff --git a/Telegram/SourceFiles/ui/effects/premium_stars_colored.h b/Telegram/SourceFiles/ui/effects/premium_stars_colored.h index 0e53869e7a..42fff3cefd 100644 --- a/Telegram/SourceFiles/ui/effects/premium_stars_colored.h +++ b/Telegram/SourceFiles/ui/effects/premium_stars_colored.h @@ -25,8 +25,8 @@ public: ColoredMiniStars( not_null<Ui::RpWidget*> parent, bool optimizeUpdate, - MiniStars::Type type = MiniStars::Type::MonoStars); - ColoredMiniStars(Fn<void(const QRect &)> update, MiniStars::Type type); + MiniStarsType type = MiniStarsType::MonoStars); + ColoredMiniStars(Fn<void(const QRect &)> update, MiniStarsType type); void setSize(const QSize &size); void setPosition(QPoint position); diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp index 4d538ce374..9cfb5e35b9 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.cpp @@ -7,14 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/effects/premium_top_bar.h" +#include "lottie/lottie_icon.h" #include "ui/color_contrast.h" #include "ui/painter.h" #include "ui/effects/premium_graphics.h" #include "ui/widgets/labels.h" #include "ui/wrap/fade_wrap.h" +#include "ui/rect.h" #include "styles/style_layers.h" #include "styles/style_settings.h" #include "styles/style_premium.h" +#include "styles/style_boxes.h" namespace Ui::Premium { namespace { @@ -107,8 +110,14 @@ TopBar::TopBar( , _logo(descriptor.logo) , _titleFont(st.titleFont) , _titlePadding(st.titlePadding) +, _aboutMaxWidth(st.aboutMaxWidth) , _about(this, std::move(descriptor.about), st.about) -, _ministars(this, descriptor.optimizeMinistars, MiniStars::Type::BiStars) { +, _ministars( + this, + descriptor.optimizeMinistars, + (_logo == u"diamond"_q) + ? MiniStarsType::DiamondStars + : MiniStarsType::BiStars) { std::move( descriptor.title ) | rpl::start_with_next([=](QString text) { @@ -131,7 +140,7 @@ TopBar::TopBar( rpl::single() | rpl::then( style::PaletteChanged() - ) | rpl::start_with_next([=] { + ) | rpl::start_with_next([=, starSize = st.starSize] { TopBarAbstract::computeIsDark(); if (_logo == u"dollar"_q) { @@ -141,6 +150,17 @@ TopBar::TopBar( } else if (_logo == u"affiliate"_q) { _dollar = ScaleTo(QImage(u":/gui/art/affiliate_logo.png"_q)); _ministars.setColorOverride(descriptor.gradientStops); + } else if (_logo == u"diamond"_q) { + _lottie = Lottie::MakeIcon({ + .name = u"diamond"_q, + .sizeOverride = starSize, + }); + _lottie->animate( + [=] { update(_starRect.toRect() + Margins(st::lineWidth)); }, + 0, + _lottie->framesCount() - 1); + _ministars.setColorOverride( + QGradientStops{{ 0, st::windowActiveTextFg->c }}); } else if (!_light && !TopBarAbstract::isDark()) { _star.load(Svg()); _ministars.setColorOverride( @@ -212,8 +232,11 @@ void TopBar::resizeEvent(QResizeEvent *e) { const auto aboutTop = titleTop + titlePathRect.height() + _titlePadding.bottom(); - _about->resizeToWidth(availableWidth); - _about->moveToLeft(padding.left(), aboutTop); + _about->resizeToWidth(_aboutMaxWidth ? _aboutMaxWidth : availableWidth); + _about->moveToLeft( + padding.left() + + (_aboutMaxWidth ? (availableWidth - _about->width()) / 2 : 0), + aboutTop); _about->setOpacity(_progress.body); RpWidget::resizeEvent(e); @@ -243,8 +266,23 @@ void TopBar::paintEvent(QPaintEvent *e) { if (_progress.top) { _ministars.paint(p); } + if (_lottie) { + _lottie->paint( + p, + _starRect.left() + + (_starRect.width() - _lottie->width()) / 2 + - st::lineWidth * 6, + _starRect.top()); + if (!_lottie->animating() && _lottie->frameIndex() > 0) { + _lottie->animate( + [=] { update(_starRect.toRect() + Margins(st::lineWidth)); }, + 0, + _lottie->framesCount() - 1); + } + } p.resetTransform(); + if (!_dollar.isNull()) { auto hq = PainterHighQualityEnabler(p); p.drawImage(_starRect, _dollar); diff --git a/Telegram/SourceFiles/ui/effects/premium_top_bar.h b/Telegram/SourceFiles/ui/effects/premium_top_bar.h index 1945270252..838c9fe0b4 100644 --- a/Telegram/SourceFiles/ui/effects/premium_top_bar.h +++ b/Telegram/SourceFiles/ui/effects/premium_top_bar.h @@ -23,6 +23,10 @@ namespace Ui { class FlatLabel; } // namespace Ui +namespace Lottie { +class Icon; +} // namespace Lottie + namespace Ui::Premium { class TopBarAbstract : public RpWidget { @@ -92,10 +96,12 @@ private: const QString _logo; const style::font &_titleFont; const style::margins &_titlePadding; + const int _aboutMaxWidth = 0; object_ptr<FlatLabel> _about; ColoredMiniStars _ministars; QSvgRenderer _star; QImage _dollar; + std::unique_ptr<Lottie::Icon> _lottie; struct { float64 top = 0.; diff --git a/Telegram/SourceFiles/ui/empty_userpic.cpp b/Telegram/SourceFiles/ui/empty_userpic.cpp index ea38baa05d..e731062b67 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.cpp +++ b/Telegram/SourceFiles/ui/empty_userpic.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" // style::IconButton #include "styles/style_info.h" // st::topBarCall +#include <QtCore/QMutex> #include <QtSvg/QSvgRenderer> namespace Ui { @@ -366,6 +367,17 @@ void EmptyUserpic::paintSquare( }); } +void EmptyUserpic::paintMonoforum( + QPainter &p, + int x, + int y, + int outerWidth, + int size) const { + paint(p, x, y, outerWidth, size, [&] { + PaintMonoforumShape(p, QRect(x, y, size, size)); + }); +} + void EmptyUserpic::PaintSavedMessages( QPainter &p, int x, @@ -649,4 +661,86 @@ void EmptyUserpic::fillString(const QString &name) { EmptyUserpic::~EmptyUserpic() = default; +void PaintMonoforumShape(QPainter &p, QRect rect) { + p.drawEllipse(rect); + + auto path = QPainterPath(); + path.moveTo( + rect.x() + rect.width() * 0.5, + rect.y() + rect.height() * 0.5); + path.arcTo( + QRectF( + rect.x() - rect.width() * 0.5, + rect.y(), + rect.width(), + rect.height()), + 0, + -90); + path.arcTo( + QRectF( + rect.x() - rect.width() * 0.25, + rect.y() - rect.height() * 2, + rect.width() * 0.5, + rect.height() * 3), + -90, + 45); + path.lineTo( + rect.x() + rect.width() * 0.5, + rect.y() + rect.height() * 0.5); + p.drawPath(path); +} + +QImage MonoforumShapeMask(QSize size) { + auto result = QImage(size, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + + QPainter p(&result); + PainterHighQualityEnabler hq(p); + p.setBrush(Qt::white); + p.setPen(Qt::NoPen); + + PaintMonoforumShape(p, QRect(QPoint(), size)); + + p.end(); + + return result; +} + +const QImage &MonoforumShapeMaskCached(QSize size) { + const auto key = (uint64(uint32(size.width())) << 32) + | uint64(uint32(size.height())); + + static auto Masks = base::flat_map<uint64, QImage>(); + static auto Mutex = QMutex(); + auto lock = QMutexLocker(&Mutex); + const auto i = Masks.find(key); + if (i != end(Masks)) { + return i->second; + } + lock.unlock(); + + auto mask = MonoforumShapeMask(size); + + lock.relock(); + return Masks.emplace(key, std::move(mask)).first->second; +} + +QImage ApplyMonoforumShape(QImage image) { + const auto size = image.size(); + auto mask = MonoforumShapeMaskCached(size); + + constexpr auto format = QImage::Format_ARGB32_Premultiplied; + if (image.format() != format) { + image = std::move(image).convertToFormat(format); + } + auto p = QPainter(&image); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.drawImage( + QRect(QPoint(), image.size() / image.devicePixelRatio()), + mask); + p.end(); + + return image; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/empty_userpic.h b/Telegram/SourceFiles/ui/empty_userpic.h index fba1fd88ad..6e2e903cec 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.h +++ b/Telegram/SourceFiles/ui/empty_userpic.h @@ -46,6 +46,12 @@ public: int y, int outerWidth, int size) const; + void paintMonoforum( + QPainter &p, + int x, + int y, + int outerWidth, + int size) const; [[nodiscard]] QPixmap generate(int size); [[nodiscard]] std::pair<uint64, uint64> uniqueKey() const; @@ -147,4 +153,9 @@ private: }; +void PaintMonoforumShape(QPainter &p, QRect rect); +[[nodiscard]] QImage MonoforumShapeMask(QSize size); +[[nodiscard]] const QImage &MonoforumShapeMaskCached(QSize size); +[[nodiscard]] QImage ApplyMonoforumShape(QImage image); + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 48c2b0d8fa..9c3233f0e2 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -57,6 +57,7 @@ menuIconStats: icon {{ "menu/stats", menuIconColor }}; menuIconBoosts: icon {{ "menu/boosts", menuIconColor }}; menuIconEarn: icon {{ "menu/earn", menuIconColor }}; menuIconCreatePoll: icon {{ "menu/create_poll", menuIconColor }}; +menuIconCreateTodoList: icon {{ "menu/select", menuIconColor }}; menuIconQrCode: icon {{ "menu/qr_code", menuIconColor }}; menuIconExpand: icon {{ "menu/expand", menuIconColor }}; menuIconCollapse: icon {{ "menu/collapse", menuIconColor }}; @@ -134,7 +135,6 @@ menuIconTopics: icon {{ "menu/topics", menuIconColor }}; menuIconGroupReactions: icon {{ "menu/group_reactions", menuIconColor }}; menuIconLinks: icon {{ "menu/links_profile", menuIconColor }}; menuIconGroupLog: icon {{ "menu/group_log", menuIconColor }}; -menuIconGroupCreate: icon {{ "menu/groups_create", menuIconColor }}; menuIconSigned: icon {{ "menu/signed", menuIconColor }}; menuIconAntispam: icon {{ "menu/antispam", menuIconColor }}; menuIconChatDiscuss: icon {{ "menu/chat_discuss", menuIconColor }}; @@ -199,6 +199,7 @@ menuBlueIconColorNames: icon{{ "settings/premium/features/feature_color_names", menuBlueIconWallpaper: icon{{ "settings/premium/features/feature_wallpaper", lightButtonFg }}; menuBlueIconEmojiStatus: icon{{ "settings/premium/features/feature_status", lightButtonFg }}; menuBlueIconEmojiPack: icon{{ "settings/premium/features/feature_emoji_pack", lightButtonFg }}; +menuBlueIconGroupCreate: icon {{ "menu/groups_create", lightButtonFg }}; mediaMenuIconStickers: icon {{ "menu/stickers", mediaviewMenuFg }}; mediaMenuIconCancel: icon {{ "menu/cancel", mediaviewMenuFg }}; @@ -231,9 +232,10 @@ menuIconRestoreAttention: icon {{ "menu/restore", menuIconAttentionColor }}; menuIconTagRemoveAttention: icon {{ "menu/tag_remove", menuIconAttentionColor }}; menuIconCancelAttention: icon {{ "menu/cancel", menuIconAttentionColor }}; menuIconBlockAttention: icon {{ "menu/block", menuIconAttentionColor }}; +menuIconRemoveAttention: icon {{ "menu/remove", menuIconAttentionColor }}; menuIconBlockSettings: icon {{ "menu/block", windowBgActive }}; -menuIconInviteSettings: icon {{ "menu/invite", windowBgActive }}; +menuIconInviteSettings: icon {{ "menu/invite", lightButtonFg }}; playerSpeedSlow: icon {{ "player/speed/audiospeed_menu_0.5", menuIconColor }}; playerSpeedSlowActive: icon {{ "player/speed/audiospeed_menu_0.5", mediaPlayerActiveFg }}; diff --git a/Telegram/SourceFiles/ui/ui_pch.h b/Telegram/SourceFiles/ui/ui_pch.h index 10bd37c13e..eb9a86a713 100644 --- a/Telegram/SourceFiles/ui/ui_pch.h +++ b/Telegram/SourceFiles/ui/ui_pch.h @@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flat_map.h" #include "base/flat_set.h" -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "ui/arc_angles.h" #include "ui/text/text.h" diff --git a/Telegram/SourceFiles/ui/unread_badge.cpp b/Telegram/SourceFiles/ui/unread_badge.cpp index b2f84d658e..ad44eca691 100644 --- a/Telegram/SourceFiles/ui/unread_badge.cpp +++ b/Telegram/SourceFiles/ui/unread_badge.cpp @@ -75,10 +75,17 @@ void UnreadBadge::paintEvent(QPaintEvent *e) { unreadSt); } -QSize ScamBadgeSize(bool fake) { - const auto phrase = fake - ? tr::lng_fake_badge(tr::now) - : tr::lng_scam_badge(tr::now); +QString TextBadgeText(TextBadgeType type) { + switch (type) { + case TextBadgeType::Fake: return tr::lng_fake_badge(tr::now); + case TextBadgeType::Scam: return tr::lng_scam_badge(tr::now); + case TextBadgeType::Direct: return tr::lng_direct_badge(tr::now); + } + Unexpected("Type in TextBadgeText."); +} + +QSize TextBadgeSize(TextBadgeType type) { + const auto phrase = TextBadgeText(type); const auto phraseWidth = st::dialogsScamFont->width(phrase); const auto width = st::dialogsScamPadding.left() + phraseWidth @@ -89,7 +96,7 @@ QSize ScamBadgeSize(bool fake) { return { width, height }; } -void DrawScamFakeBadge( +void DrawTextBadge( Painter &p, QRect rect, int outerWidth, @@ -111,16 +118,14 @@ void DrawScamFakeBadge( phraseWidth); } -void DrawScamBadge( - bool fake, +void DrawTextBadge( + TextBadgeType type, Painter &p, QRect rect, int outerWidth, const style::color &color) { - const auto phrase = fake - ? tr::lng_fake_badge(tr::now) - : tr::lng_scam_badge(tr::now); - DrawScamFakeBadge( + const auto phrase = TextBadgeText(type); + DrawTextBadge( p, rect, outerWidth, @@ -137,8 +142,9 @@ int PeerBadge::drawGetWidth(Painter &p, Descriptor &&descriptor) { Expects(descriptor.customEmojiRepaint != nullptr); const auto peer = descriptor.peer; - if (descriptor.scam && (peer->isScam() || peer->isFake())) { - return drawScamOrFake(p, descriptor); + if ((descriptor.scam && (peer->isScam() || peer->isFake())) + || (descriptor.direct && peer->isMonoforum())) { + return drawTextBadge(p, descriptor); } const auto verifyCheck = descriptor.verified && peer->isVerified(); const auto premiumMark = descriptor.premium @@ -212,10 +218,16 @@ int PeerBadge::drawGetWidth(Painter &p, Descriptor &&descriptor) { return 0; } -int PeerBadge::drawScamOrFake(Painter &p, const Descriptor &descriptor) { - const auto phrase = descriptor.peer->isScam() - ? tr::lng_scam_badge(tr::now) - : tr::lng_fake_badge(tr::now); +int PeerBadge::drawTextBadge(Painter &p, const Descriptor &descriptor) { + const auto type = [&] { + if (descriptor.peer->isScam()) { + return TextBadgeType::Scam; + } else if (descriptor.peer->isFake()) { + return TextBadgeType::Fake; + } + return TextBadgeType::Direct; + }(); + const auto phrase = TextBadgeText(type); const auto phraseWidth = st::dialogsScamFont->width(phrase); const auto width = st::dialogsScamPadding.left() + phraseWidth @@ -232,11 +244,13 @@ int PeerBadge::drawScamOrFake(Painter &p, const Descriptor &descriptor) { rectForName.y() + (rectForName.height() - height) / 2, width, height); - DrawScamFakeBadge( + DrawTextBadge( p, rect, descriptor.outerWidth, - *descriptor.scam, + *((type == TextBadgeType::Direct) + ? descriptor.direct + : descriptor.scam), phrase, phraseWidth); return st::dialogsScamSkip + width; diff --git a/Telegram/SourceFiles/ui/unread_badge.h b/Telegram/SourceFiles/ui/unread_badge.h index a160fd63ca..aaa98cacf5 100644 --- a/Telegram/SourceFiles/ui/unread_badge.h +++ b/Telegram/SourceFiles/ui/unread_badge.h @@ -60,6 +60,7 @@ public: const style::icon *exteraSupporter = nullptr; const style::icon *premium = nullptr; const style::color *scam = nullptr; + const style::color *direct = nullptr; const style::color *premiumFg = nullptr; Fn<void()> customEmojiRepaint; crl::time now = 0; @@ -86,7 +87,7 @@ private: struct EmojiStatus; struct BotVerifiedData; - int drawScamOrFake(Painter &p, const Descriptor &descriptor); + int drawTextBadge(Painter &p, const Descriptor &descriptor); int drawVerifyCheck(Painter &p, const Descriptor &descriptor); int drawPremiumEmojiStatus(Painter &p, const Descriptor &descriptor); int drawPremiumStar(Painter &p, const Descriptor &descriptor); @@ -99,9 +100,15 @@ private: }; -QSize ScamBadgeSize(bool fake); -void DrawScamBadge( - bool fake, +enum class TextBadgeType : uchar { + Scam, + Fake, + Direct, +}; + +QSize TextBadgeSize(TextBadgeType type); +void DrawTextBadge( + TextBadgeType, Painter &p, QRect rect, int outerWidth, diff --git a/Telegram/SourceFiles/ui/userpic_view.cpp b/Telegram/SourceFiles/ui/userpic_view.cpp index f14ac76e80..ff5f336d42 100644 --- a/Telegram/SourceFiles/ui/userpic_view.cpp +++ b/Telegram/SourceFiles/ui/userpic_view.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/userpic_view.h" #include "ui/empty_userpic.h" +#include "ui/painter.h" #include "ui/image/image_prepare.h" namespace Ui { @@ -25,14 +26,14 @@ void ValidateUserpicCache( const QImage *cloud, const EmptyUserpic *empty, int size, - bool forum) { + PeerUserpicShape shape) { Expects(cloud != nullptr || empty != nullptr); const auto full = QSize(size, size); const auto version = style::PaletteVersion(); - const auto forumValue = forum ? 1 : 0; + const auto shapeValue = static_cast<uint32>(shape) & 3; const auto regenerate = (view.cached.size() != QSize(size, size)) - || (view.forum != forumValue) + || (view.shape != shapeValue) || (cloud && !view.empty.null()) || (empty && empty != view.empty.get()) || (empty && view.paletteVersion != version); @@ -40,7 +41,7 @@ void ValidateUserpicCache( return; } view.empty = empty; - view.forum = forumValue; + view.shape = shapeValue; view.paletteVersion = version; if (cloud) { @@ -48,7 +49,9 @@ void ValidateUserpicCache( full, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - if (forum) { + if (shape == PeerUserpicShape::Monoforum) { + view.cached = Ui::ApplyMonoforumShape(std::move(view.cached)); + } else if (shape == PeerUserpicShape::Forum) { view.cached = Images::Round( std::move(view.cached), Images::CornersMask(size @@ -64,7 +67,9 @@ void ValidateUserpicCache( view.cached.fill(Qt::transparent); auto p = QPainter(&view.cached); - if (forum) { + if (shape == PeerUserpicShape::Monoforum) { + empty->paintMonoforum(p, 0, 0, size, size); + } else if (shape == PeerUserpicShape::Forum) { empty->paintRounded( p, 0, diff --git a/Telegram/SourceFiles/ui/userpic_view.h b/Telegram/SourceFiles/ui/userpic_view.h index 6e50127d74..0bd9cbcedc 100644 --- a/Telegram/SourceFiles/ui/userpic_view.h +++ b/Telegram/SourceFiles/ui/userpic_view.h @@ -17,6 +17,13 @@ class EmptyUserpic; [[nodiscard]] float64 ForumUserpicRadiusMultiplier(); +enum class PeerUserpicShape : uint8 { + Auto, + Circle, + Forum, + Monoforum, +}; + struct PeerUserpicView { [[nodiscard]] bool null() const { return cached.isNull() && !cloud && empty.null(); @@ -25,8 +32,8 @@ struct PeerUserpicView { QImage cached; std::shared_ptr<QImage> cloud; base::weak_ptr<const EmptyUserpic> empty; - uint32 paletteVersion : 31 = 0; - uint32 forum : 1 = 0; + uint32 paletteVersion : 30 = 0; + uint32 shape : 2 = 0; }; [[nodiscard]] bool PeerUserpicLoading(const PeerUserpicView &view); @@ -36,6 +43,6 @@ void ValidateUserpicCache( const QImage *cloud, const EmptyUserpic *empty, int size, - bool forum); + PeerUserpicShape shape); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp index d55da801d3..3c80be1720 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp @@ -50,6 +50,7 @@ void DiscreteSlider::setActiveSectionFast(int index) { void DiscreteSlider::finishAnimating() { _a_left.stop(); + _a_width.stop(); update(); _callbackAfterMs = 0; if (_timerId >= 0) { @@ -64,10 +65,24 @@ void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) { } } +int DiscreteSlider::sectionsCount() const { + return int(_sections.size()); +} + +int DiscreteSlider::lookupSectionLeft(int index) const { + Expects(index >= 0 && index < _sections.size()); + + return _sections[index].left; +} + void DiscreteSlider::setSelectOnPress(bool selectOnPress) { _selectOnPress = selectOnPress; } +bool DiscreteSlider::paused() const { + return _paused && _paused(); +} + std::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() { return _sections; } @@ -97,7 +112,8 @@ void DiscreteSlider::setSections(const std::vector<QString> &labels) { void DiscreteSlider::setSections( const std::vector<TextWithEntities> &labels, - Text::MarkedContext context) { + Text::MarkedContext context, + Fn<bool()> paused) { Assert(!labels.empty()); context.repaint = [this] { update(); }; @@ -106,6 +122,7 @@ void DiscreteSlider::setSections( for (const auto &label : labels) { _sections.push_back(Section(label, getLabelStyle(), context)); } + _paused = std::move(paused); refresh(); } @@ -122,7 +139,9 @@ void DiscreteSlider::refresh() { } DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const { - const auto raw = _sections.empty() ? nullptr : &_sections[_selected]; + const auto raw = (_sections.empty() || _selected < 0) + ? nullptr + : &_sections[_selected]; if (!raw) { return { 0, 0 }; } @@ -193,7 +212,7 @@ void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) { } void DiscreteSlider::setSelectedSection(int index) { - if (index < 0 || index >= _sections.size()) { + if (index >= int(_sections.size())) { return; } @@ -414,9 +433,10 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { : section.width; const auto activeLeft = section.left + (section.width - activeWidth) / 2; + const auto divider = std::max(std::min(activeWidth, range.width), 1); const auto active = 1. - std::clamp( - std::abs(range.left - activeLeft) / float64(range.width), + std::abs(range.left - activeLeft) / float64(divider), 0., 1.); if (section.ripple) { @@ -467,6 +487,7 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { .position = QPoint(labelLeft, _st.labelTop), .outerWidth = width(), .availableWidth = section.label.maxWidth(), + .paused = paused(), }); } return true; diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h index 4e9476bb02..6c7e6405bb 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h @@ -37,7 +37,8 @@ public: void setSections(const std::vector<QString> &labels); void setSections( const std::vector<TextWithEntities> &labels, - Text::MarkedContext context = {}); + Text::MarkedContext context = {}, + Fn<bool()> paused = nullptr); int activeSection() const { return _activeIndex; } @@ -51,6 +52,9 @@ public: return _sectionActivated.events(); } + [[nodiscard]] int sectionsCount() const; + [[nodiscard]] int lookupSectionLeft(int index) const; + protected: void timerEvent(QTimerEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -98,7 +102,9 @@ protected: void setSelectOnPress(bool selectOnPress); - std::vector<Section> §ionsRef(); + [[nodiscard]] std::vector<Section> §ionsRef(); + + [[nodiscard]] bool paused() const; private: void activateCallback(); @@ -109,6 +115,7 @@ private: void setSelectedSection(int index); std::vector<Section> _sections; + Fn<bool()> _paused; int _activeIndex = 0; bool _selectOnPress = true; bool _snapToLabel = false; diff --git a/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp b/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp index fa43f4b920..a033a17b64 100644 --- a/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp +++ b/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp @@ -151,8 +151,8 @@ void AddExpandablePeerList( using namespace Info::Profile; auto name = controller->data.bold - ? NameValue(peer) | rpl::map(Ui::Text::Bold) - : NameValue(peer) | rpl::map(Ui::Text::WithEntities); + ? NameValue(peer) | Ui::Text::ToBold() + : NameValue(peer) | Ui::Text::ToWithEntities(); const auto userpic = Ui::CreateChild<Ui::UserpicButton>(line, peer, st); const auto checkbox = Ui::CreateChild<Ui::Checkbox>( diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index 05920c8e3d..75f4c02e36 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/platform/ui_platform_window.h" #include "platform/platform_window_title.h" #include "history/history.h" +#include "info/media/info_media_widget.h" // SharedMediaTitle. #include "window/window_separate_id.h" #include "window/window_session_controller.h" #include "window/window_lock_widgets.h" @@ -91,42 +92,24 @@ base::options::toggle OptionDisableTouchbar({ .restartRequired = true, }); -[[nodiscard]] QString TitleFromSeparateId( +[[nodiscard]] QString TitleFromSeparateSharedMedia( const Core::WindowTitleContent &settings, const SeparateId &id) { - if (id.sharedMedia == SeparateSharedMediaType::None - || !id.sharedMediaPeer()) { + if (id.type != SeparateType::SharedMedia) { return QString(); } - const auto result = (id.sharedMedia == SeparateSharedMediaType::Photos) - ? tr::lng_media_type_photos(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Videos) - ? tr::lng_media_type_videos(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Files) - ? tr::lng_media_type_files(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Audio) - ? tr::lng_media_type_songs(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Links) - ? tr::lng_media_type_links(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::GIF) - ? tr::lng_media_type_gifs(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Voices) - ? tr::lng_media_type_audios(tr::now) - : QString(); - + const auto type = id.sharedMediaType; + const auto result = Info::Media::SharedMediaTitle(type)(tr::now); if (settings.hideChatName) { return result; } - const auto peer = id.sharedMediaPeer(); - const auto topicRootId = id.sharedMediaTopicRootId(); - const auto topic = topicRootId - ? peer->forumTopicFor(topicRootId) - : nullptr; + const auto thread = id.thread; + const auto topic = thread->asTopic(); const auto name = topic ? topic->title() - : peer->isSelf() + : thread->peer()->isSelf() ? tr::lng_saved_messages(tr::now) - : peer->name(); + : thread->peer()->name(); const auto wrapped = st::wrap_rtl(name); return name + u" @ "_q + result; } @@ -900,11 +883,11 @@ void MainWindow::updateTitle() { && Core::App().domain().accountsAuthedCount() > 1) ? st::wrap_rtl(session->authedName()) : QString(); - const auto separateIdTitle = session - ? TitleFromSeparateId(settings, session->windowId()) + const auto separateSharedMediaTitle = session + ? TitleFromSeparateSharedMedia(settings, session->windowId()) : QString(); - if (!separateIdTitle.isEmpty()) { - setTitle(separateIdTitle); + if (!separateSharedMediaTitle.isEmpty()) { + setTitle(separateSharedMediaTitle); return; } const auto key = (session && !settings.hideChatName) diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 04bd0d31b3..2400cab77f 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -16,11 +16,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_config.h" #include "history/history.h" #include "history/history_item_components.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "lang/lang_keys.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document_media.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" @@ -359,6 +360,15 @@ void System::registerThread(not_null<Data::Thread*> thread) { clearFromTopic(topic); }, i->second); } + } else if (const auto sublist = thread->asSublist()) { + const auto &[i, ok] = _watchedSublists.emplace( + sublist, + rpl::lifetime()); + if (ok) { + sublist->destroyed() | rpl::start_with_next([=] { + clearFromSublist(sublist); + }, i->second); + } } } @@ -436,6 +446,7 @@ void System::clearAll() { _waiters.clear(); _settingWaiters.clear(); _watchedTopics.clear(); + _watchedSublists.clear(); } void System::clearFromTopic(not_null<Data::ForumTopic*> topic) { @@ -455,6 +466,23 @@ void System::clearFromTopic(not_null<Data::ForumTopic*> topic) { showNext(); } +void System::clearFromSublist(not_null<Data::SavedSublist*> sublist) { + if (_manager) { + _manager->clearFromSublist(sublist); + } + + sublist->clearNotifications(); + _whenMaps.remove(sublist); + _whenAlerts.remove(sublist); + _waiters.remove(sublist); + _settingWaiters.remove(sublist); + + _watchedSublists.remove(sublist); + + _waitTimer.cancel(); + showNext(); +} + void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) { for (auto i = _whenMaps.begin(); i != _whenMaps.end();) { const auto thread = i->first; @@ -470,6 +498,8 @@ void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) { _settingWaiters.remove(thread); if (const auto topic = thread->asTopic()) { _watchedTopics.remove(topic); + } else if (const auto sublist = thread->asSublist()) { + _watchedSublists.remove(sublist); } } const auto clearFrom = [&](auto &map) { @@ -478,6 +508,8 @@ void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) { if (predicate(thread)) { if (const auto topic = thread->asTopic()) { _watchedTopics.remove(topic); + } else if (const auto sublist = thread->asSublist()) { + _watchedSublists.remove(sublist); } i = map.erase(i); } else { @@ -527,6 +559,15 @@ void System::clearIncomingFromTopic(not_null<Data::ForumTopic*> topic) { _whenAlerts.remove(topic); } +void System::clearIncomingFromSublist( + not_null<Data::SavedSublist*> sublist) { + if (_manager) { + _manager->clearFromSublist(sublist); + } + sublist->clearIncomingNotifications(); + _whenAlerts.remove(sublist); +} + void System::clearFromItem(not_null<HistoryItem*> item) { if (_manager) { _manager->clearFromItem(item); @@ -543,6 +584,7 @@ void System::clearAllFast() { _waiters.clear(); _settingWaiters.clear(); _watchedTopics.clear(); + _watchedSublists.clear(); } void System::checkDelayed() { @@ -1124,10 +1166,14 @@ void Manager::notificationActivated( history->peer, id.msgId); const auto topic = item ? item->topic() : nullptr; + const auto sublist = item ? item->savedSublist() : nullptr; if (!options.draft.text.isEmpty()) { const auto topicRootId = topic ? topic->rootId() : id.contextId.topicRootId; + const auto monoforumPeerId = (sublist && sublist->parentChat()) + ? sublist->sublistPeer()->id + : id.contextId.monoforumPeerId; const auto replyToId = (id.msgId > 0 && !history->peer->isUser() && id.msgId != topicRootId) @@ -1139,7 +1185,9 @@ void Manager::notificationActivated( FullReplyTo{ .messageId = replyToId, .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), MessageCursor{ length, length, @@ -1177,18 +1225,23 @@ Window::SessionController *Manager::openNotificationMessage( && item->isRegular() && (item->out() || (item->mentionsMe() && !history->peer->isUser())); const auto topic = item ? item->topic() : nullptr; + const auto sublist = item ? item->savedSublist() : nullptr; const auto guard = gsl::finally([&] { if (topic) { system()->clearFromTopic(topic); + } else if (sublist && sublist->parentChat()) { + system()->clearFromSublist(sublist); } else { system()->clearFromHistory(history); } }); - const auto separateId = topic - ? Window::SeparateId(Window::SeparateType::Forum, history) - : Window::SeparateId(history->peer); + const auto separateId = !topic + ? Window::SeparateId(history->peer) + : history->peer->asChannel()->useSubsectionTabs() + ? Window::SeparateId(Window::SeparateType::Chat, topic) + : Window::SeparateId(Window::SeparateType::Forum, history); const auto separate = Core::App().separateWindowFor(separateId); const auto itemId = openExactlyMessage ? messageId : ShowAtUnreadMsgId; if (openSeparated && !separate && !topic) { @@ -1231,12 +1284,9 @@ Window::SessionController *Manager::openNotificationMessage( if (window) { window->widget()->showFromTray(); if (topic) { - window->showSection( - std::make_shared<HistoryView::RepliesMemento>( - history, - topic->rootId(), - itemId), - SectionShow::Way::Forward); + window->showTopic(topic, itemId, SectionShow::Way::Forward); + } else if (sublist) { + window->showSublist(sublist, itemId, SectionShow::Way::Forward); } else { window->showPeerHistory( history->peer->id, @@ -1264,6 +1314,10 @@ void Manager::notificationReplied( const auto topicRootId = topic ? topic->rootId() : id.contextId.topicRootId; + const auto sublist = item ? item->savedSublist() : nullptr; + const auto monoforumPeerId = (sublist && sublist->parentChat()) + ? sublist->sublistPeer()->id + : id.contextId.monoforumPeerId; auto message = Api::MessageToSend(Api::SendAction(history)); message.textWithTags = reply; @@ -1276,6 +1330,7 @@ void Manager::notificationReplied( message.action.replyTo = { .messageId = { replyToId ? history->peer->id : 0, replyToId }, .topicRootId = topic ? topic->rootId() : 0, + .monoforumPeerId = monoforumPeerId, }; message.action.clearDraft = false; history->session().api().sendMessage(std::move(message)); @@ -1301,16 +1356,21 @@ void NativeManager::doShowNotification(NotificationFields &&fields) { && !reactionFrom && (item->out() || peer->isSelf()) && item->isFromScheduled(); - const auto topicWithChat = [&] { + const auto subWithChat = [&] { const auto name = peer->name(); const auto topic = item->topic(); - return topic ? (topic->title() + u" ("_q + name + ')') : name; + const auto sublist = item->savedSublist(); + return topic + ? (topic->title() + u" ("_q + name + ')') + : (sublist && sublist->parentChat()) + ? (sublist->sublistPeer()->shortName() + u" ("_q + name + ')') + : name; }; const auto title = options.hideNameAndPhoto ? AppName.utf16() : (scheduled && peer->isSelf()) ? tr::lng_notification_reminder(tr::now) - : topicWithChat(); + : subWithChat(); const auto fullTitle = addTargetAccountName(title, &peer->session()); const auto subtitle = reactionFrom ? (reactionFrom != peer ? reactionFrom->name() : QString()) @@ -1349,6 +1409,9 @@ void NativeManager::doShowNotification(NotificationFields &&fields) { doShowNativeNotification({ .peer = item->history()->peer, .topicRootId = item->topicRootId(), + .monoforumPeerId = (item->history()->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId()), .itemId = item->id, .title = scheduled ? WrapFromScheduled(fullTitle) : fullTitle, .subtitle = subtitle, diff --git a/Telegram/SourceFiles/window/notifications_manager.h b/Telegram/SourceFiles/window/notifications_manager.h index 9caa1dd264..b91afcfca0 100644 --- a/Telegram/SourceFiles/window/notifications_manager.h +++ b/Telegram/SourceFiles/window/notifications_manager.h @@ -17,6 +17,7 @@ class History; namespace Data { class Session; class ForumTopic; +class SavedSublist; class Thread; struct ItemNotification; enum class ItemNotificationType; @@ -109,8 +110,10 @@ public: void checkDelayed(); void schedule(Data::ItemNotification notification); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearIncomingFromTopic(not_null<Data::ForumTopic*> topic); + void clearIncomingFromSublist(not_null<Data::SavedSublist*> sublist); void clearIncomingFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void clearFromItem(not_null<HistoryItem*> item); @@ -221,6 +224,9 @@ private: base::flat_map< not_null<Data::ForumTopic*>, rpl::lifetime> _watchedTopics; + base::flat_map< + not_null<Data::SavedSublist*>, + rpl::lifetime> _watchedSublists; int _lastForwardedCount = 0; uint64 _lastHistorySessionId = 0; @@ -237,6 +243,7 @@ public: uint64 sessionId = 0; PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; friend inline auto operator<=>( const ContextId&, @@ -279,6 +286,9 @@ public: void clearFromTopic(not_null<Data::ForumTopic*> topic) { doClearFromTopic(topic); } + void clearFromSublist(not_null<Data::SavedSublist*> sublist) { + doClearFromSublist(sublist); + } void clearFromHistory(not_null<History*> history) { doClearFromHistory(history); } @@ -341,6 +351,8 @@ protected: virtual void doClearAllFast() = 0; virtual void doClearFromItem(not_null<HistoryItem*> item) = 0; virtual void doClearFromTopic(not_null<Data::ForumTopic*> topic) = 0; + virtual void doClearFromSublist( + not_null<Data::SavedSublist*> sublist) = 0; virtual void doClearFromHistory(not_null<History*> history) = 0; virtual void doClearFromSession(not_null<Main::Session*> session) = 0; [[nodiscard]] virtual bool doSkipToast() const = 0; @@ -377,6 +389,7 @@ public: struct NotificationInfo { not_null<PeerData*> peer; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; MsgId itemId = 0; QString title; QString subtitle; @@ -426,6 +439,8 @@ protected: } void doClearFromTopic(not_null<Data::ForumTopic*> topic) override { } + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override { + } void doClearFromHistory(not_null<History*> history) override { } void doClearFromSession(not_null<Main::Session*> session) override { diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 3486a28d65..32afd3f565 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/ui_utility.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_forum_topic.h" #include "data/stickers/data_custom_emoji.h" @@ -256,6 +257,7 @@ void Manager::showNextFromQueue() { this, queued.history, queued.topicRootId, + queued.monoforumPeerId, queued.peer, queued.author, queued.item, @@ -403,7 +405,25 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { } } for (const auto ¬ification : _notifications) { - if (notification->unlinkHistory(history, topicRootId)) { + if (notification->unlinkHistory(history, topicRootId, PeerId())) { + _positionsOutdated = true; + } + } + showNextFromQueue(); +} + +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + const auto history = sublist->owningHistory(); + const auto sublistPeerId = sublist->sublistPeer()->id; + for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) { + if (i->history == history && i->monoforumPeerId == sublistPeerId) { + i = _queuedNotifications.erase(i); + } else { + ++i; + } + } + for (const auto ¬ification : _notifications) { + if (notification->unlinkHistory(history, MsgId(), sublistPeerId)) { _positionsOutdated = true; } } @@ -638,6 +658,7 @@ Notification::Notification( not_null<Manager*> manager, not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, not_null<PeerData*> peer, const QString &author, HistoryItem *item, @@ -653,7 +674,9 @@ Notification::Notification( , _history(history) , _topic(history->peer->forumTopicFor(topicRootId)) , _topicRootId(topicRootId) -, _userpicView(_peer->createUserpicView()) +, _sublist(history->peer->monoforumSublistFor(monoforumPeerId)) +, _monoforumPeerId(monoforumPeerId) +, _userpicView(_peer->userpicPaintingPeer()->createUserpicView()) , _author(author) , _reaction(reaction) , _item(item) @@ -1173,10 +1196,14 @@ void Notification::changeHeight(int newHeight) { manager()->changeNotificationHeight(this, newHeight); } -bool Notification::unlinkHistory(History *history, MsgId topicRootId) { +bool Notification::unlinkHistory( + History *history, + MsgId topicRootId, + PeerId monoforumPeerId) { const auto unlink = _history && (history == _history || !history) - && (topicRootId == _topicRootId || !topicRootId); + && (topicRootId == _topicRootId || !topicRootId) + && (monoforumPeerId == _monoforumPeerId || !monoforumPeerId); if (unlink) { hideFast(); _history = nullptr; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index 7c81282355..219ce8d83a 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -70,6 +70,7 @@ private: void doClearAll() override; void doClearAllFast() override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; void doClearFromItem(not_null<HistoryItem*> item) override; @@ -111,6 +112,7 @@ private: not_null<History*> history; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; not_null<PeerData*> peer; Data::ReactionId reaction; QString author; @@ -203,6 +205,7 @@ public: not_null<Manager*> manager, not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, not_null<PeerData*> peer, const QString &author, HistoryItem *item, @@ -231,7 +234,10 @@ public: // Called only by Manager. bool unlinkItem(HistoryItem *del); - bool unlinkHistory(History *history = nullptr, MsgId topicRootId = 0); + bool unlinkHistory( + History *history = nullptr, + MsgId topicRootId = 0, + PeerId monoforumPeerId = 0); bool unlinkSession(not_null<Main::Session*> session); bool checkLastInput( bool hasReplyingNotifications, @@ -285,6 +291,8 @@ private: History *_history = nullptr; Data::ForumTopic *_topic = nullptr; MsgId _topicRootId = 0; + Data::SavedSublist *_sublist = nullptr; + PeerId _monoforumPeerId = 0; Ui::PeerUserpicView _userpicView; QString _author; Data::ReactionId _reaction; diff --git a/Telegram/SourceFiles/window/section_memento.h b/Telegram/SourceFiles/window/section_memento.h index e794d024eb..d139928a99 100644 --- a/Telegram/SourceFiles/window/section_memento.h +++ b/Telegram/SourceFiles/window/section_memento.h @@ -13,6 +13,7 @@ class LayerWidget; namespace Data { class ForumTopic; +class SavedSublist; } // namespace Data namespace Window { @@ -41,6 +42,10 @@ public: [[nodiscard]] virtual Data::ForumTopic *topicForRemoveRequests() const { return nullptr; } + [[nodiscard]] virtual auto sublistForRemoveRequests() const + -> Data::SavedSublist* { + return nullptr; + } [[nodiscard]] virtual rpl::producer<> removeRequests() const { return rpl::never<>(); } diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index a2d4cdfd35..2cd0b0f8e2 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -283,6 +283,7 @@ void SectionWidget::setGeometryWithTopMoved( void SectionWidget::showAnimated( SlideDirection direction, const SectionSlideParams ¶ms) { + validateSubsectionTabs(); if (_showAnimation) { return; } @@ -313,6 +314,7 @@ std::shared_ptr<SectionMemento> SectionWidget::createMemento() { } void SectionWidget::showFast() { + validateSubsectionTabs(); show(); showFinished(); } diff --git a/Telegram/SourceFiles/window/section_widget.h b/Telegram/SourceFiles/window/section_widget.h index 24761f0971..b1d6f41cbc 100644 --- a/Telegram/SourceFiles/window/section_widget.h +++ b/Telegram/SourceFiles/window/section_widget.h @@ -194,6 +194,9 @@ public: return nullptr; } + virtual void validateSubsectionTabs() { + } + static void PaintBackground( not_null<SessionController*> controller, not_null<Ui::ChatTheme*> theme, diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 361951d477..e9cb5b015f 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -402,7 +402,7 @@ MainMenu::MainMenu( _version->setLink( 2, std::make_shared<LambdaClickHandler>([=] { - controller->show(Box<AboutBox>(controller)); + controller->show(Box(AboutBox)); })); rpl::combine( diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 23cac3636d..8dbfca0fb7 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/moderate_messages_box.h" #include "boxes/choose_filter_box.h" #include "boxes/create_poll_box.h" +#include "boxes/edit_todo_list_box.h" #include "boxes/pin_messages_box.h" #include "boxes/premium_limits_box.h" #include "boxes/report_messages_box.h" @@ -50,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/delayed_activation.h" #include "ui/vertical_list.h" #include "ui/ui_utility.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "menu/menu_mute.h" @@ -59,10 +61,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_blocked_peers.h" #include "api/api_chat_filters.h" #include "api/api_polls.h" +#include "api/api_todo_lists.h" #include "api/api_updates.h" #include "mtproto/mtproto_config.h" #include "history/history.h" #include "history/history_item_helpers.h" // GetErrorForSending. +#include "history/history_item_components.h" #include "history/view/history_view_context_menu.h" #include "window/window_separate_id.h" #include "window/window_session_controller.h" @@ -88,6 +92,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_user.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" @@ -95,6 +100,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "export/export_manager.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/premium_preview_box.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -150,7 +156,8 @@ void ShareBotGame( MTPInputPeer(), // send_as MTPInputQuickReplyShortcut(), MTPlong(), - MTPlong() + MTPlong(), + MTPSuggestedPost() ), [=](const MTPUpdates &, const MTP::Response &) { }, [=](const MTP::Error &error, const MTP::Response &) { history->session().api().sendMessageFail(error, history->peer); @@ -294,9 +301,11 @@ private: void addManageTopic(); void addManageChat(); void addCreatePoll(); + void addCreateTodoList(); void addThemeEdit(); void addBlockUser(); void addViewDiscussion(); + void addDirectMessages(); void addToggleTopicClosed(); void addExportChat(); void addTranslate(); @@ -318,6 +327,8 @@ private: void addViewStatistics(); void addBoostChat(); + [[nodiscard]] bool skipCreateActions() const; + not_null<SessionController*> _controller; Dialogs::EntryState _request; Data::Thread *_thread = nullptr; @@ -440,7 +451,7 @@ void TogglePinnedThread( : MTPmessages_ToggleSavedDialogPin::Flag(0); owner->session().api().request(MTPmessages_ToggleSavedDialogPin( MTP_flags(flags), - MTP_inputDialogPeer(sublist->peer()->input) + MTP_inputDialogPeer(sublist->sublistPeer()->input) )).done([=] { owner->notifyPinnedDialogsOrderUpdated(); if (onToggled) { @@ -499,6 +510,12 @@ void Filler::addToggleTopicClosed() { void Filler::addTogglePin() { if ((!_sublist && !_peer) || (_topic && !_topic->canTogglePinned())) { return; + } else if (_request.section == Section::SubsectionTabsMenu + && !_sublist + && !_topic) { + return; + } else if (_sublist && !_peer->isSelf()) { + return; } const auto controller = _controller; const auto filterId = _request.filterId; @@ -526,7 +543,10 @@ void Filler::addTogglePin() { } void Filler::addToggleMuteSubmenu(bool addSeparator) { - if (!_thread || _thread->peer()->isSelf()) { + if (!_thread + || _thread->peer()->isSelf() + || _thread->asSublist() + || (_thread->asHistory() && _thread->asHistory()->isForum())) { return; } PeerMenuAddMuteSubmenuAction(_controller, _thread, _addAction); @@ -550,16 +570,18 @@ void Filler::addSupportInfo() { } void Filler::addInfo() { - if (_peer - && (_peer->isSelf() - || _peer->isRepliesChat() - || _peer->isVerifyCodes())) { + const auto sublist = _thread ? _thread->asSublist() : nullptr; + const auto infoPeer = sublist ? sublist->sublistPeer().get() : _peer; + if (infoPeer + && (infoPeer->isSelf() + || infoPeer->isRepliesChat() + || infoPeer->isVerifyCodes())) { return; } else if (!_thread) { return; } else if (_controller->adaptive().isThreeColumn()) { const auto thread = _controller->activeChatCurrent().thread(); - if (thread && thread == _thread) { + if (thread && !thread->asSublist() && thread == _thread) { if (Core::App().settings().thirdSectionInfoEnabled() || Core::App().settings().tabbedReplacedWithInfo()) { return; @@ -570,16 +592,16 @@ void Filler::addInfo() { const auto weak = base::make_weak(_thread); const auto text = _thread->asTopic() ? tr::lng_context_view_topic(tr::now) - : (_peer->isChat() || _peer->isMegagroup()) + : (infoPeer->isChat() || infoPeer->isMegagroup()) ? tr::lng_context_view_group(tr::now) - : _peer->isUser() + : infoPeer->isUser() ? tr::lng_context_view_profile(tr::now) : tr::lng_context_view_channel(tr::now); _addAction(text, [=] { if (const auto strong = weak.get()) { controller->showPeerInfo(strong); } - }, _peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); + }, infoPeer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); } void Filler::addStoryArchive() { @@ -606,6 +628,10 @@ void Filler::addToggleFolder() { || !history->owner().chatsFilters().has() || !history->inChatList()) { return; + } else if (_request.section == Section::SubsectionTabsMenu + && !_sublist + && !_topic) { + return; } _addAction(PeerMenuCallback::Args{ .text = tr::lng_filters_menu_add(tr::now), @@ -620,12 +646,9 @@ void Filler::addToggleFolder() { void Filler::addToggleUnreadMark() { const auto peer = _peer; - const auto history = _request.key.history(); - if (!_thread) { - return; - } const auto unread = IsUnreadThread(_thread); - if ((_thread->asTopic() || peer->isForum()) && !unread) { + const auto history = _request.key.history(); + if (!_thread || !_thread->canToggleUnread(unread)) { return; } const auto weak = base::make_weak(_thread); @@ -639,6 +662,8 @@ void Filler::addToggleUnreadMark() { } if (unread) { MarkAsReadThread(thread); + } else if (const auto sublist = thread->asSublist()) { + peer->owner().histories().changeSublistUnreadMark(sublist, true); } else if (history) { peer->owner().histories().changeDialogUnreadMark(history, true); } @@ -660,10 +685,9 @@ void Filler::addNewWindow() { _addAction(tr::lng_context_new_window(tr::now), [=] { Ui::PreventDelayedActivation(); if (const auto sublist = weak.get()) { - const auto peer = sublist->peer(); controller->showInNewWindow(SeparateId( SeparateType::SavedSublist, - peer->owner().history(peer))); + sublist)); } }, &st::menuIconNewWindow); AddSeparatorAndShiftUp(_addAction); @@ -684,7 +708,9 @@ void Filler::addNewWindow() { _addAction(tr::lng_context_new_window(tr::now), [=] { Ui::PreventDelayedActivation(); if (const auto strong = weak.get()) { - const auto forum = !strong->asTopic() && peer->isForum(); + const auto forum = !strong->asTopic() + && peer->isForum() + && !peer->asChannel()->useSubsectionTabs(); controller->showInNewWindow(SeparateId( forum ? SeparateType::Forum : SeparateType::Chat, strong)); @@ -694,7 +720,9 @@ void Filler::addNewWindow() { } void Filler::addToggleArchive() { - if (!_peer || _topic) { + if (!_peer + || _topic + || _request.section == Section::SubsectionTabsMenu) { return; } const auto peer = _peer; @@ -726,7 +754,7 @@ void Filler::addToggleArchive() { } void Filler::addClearHistory() { - if (_topic) { + if (_topic || _peer->isMonoforum()) { return; } const auto channel = _peer->asChannel(); @@ -746,14 +774,16 @@ void Filler::addClearHistory() { } void Filler::addDeleteChat() { - if (_topic || _peer->isChannel()) { + if (_topic || (!_sublist && _peer->isChannel())) { return; } _addAction({ - .text = (_peer->isUser() + .text = ((_peer->isUser() || _sublist) ? tr::lng_profile_delete_conversation(tr::now) : tr::lng_profile_clear_and_exit(tr::now)), - .handler = DeleteAndLeaveHandler(_controller, _peer), + .handler = (_sublist + ? DeleteSublistHandler(_controller, _sublist) + : DeleteAndLeaveHandler(_controller, _peer)), .icon = &st::menuIconDeleteAttention, .isAttention = true, }); @@ -761,7 +791,7 @@ void Filler::addDeleteChat() { void Filler::addLeaveChat() { const auto channel = _peer->asChannel(); - if (_topic || !channel || !channel->amIn()) { + if (_topic || _sublist || !channel || !channel->amIn()) { return; } _addAction({ @@ -860,6 +890,23 @@ void Filler::addViewDiscussion() { }, &st::menuIconDiscussion); } +void Filler::addDirectMessages() { + const auto channel = _peer->asBroadcast(); + if (!channel) { + return; + } + const auto monoforum = channel->broadcastMonoforum(); + if (!monoforum || !monoforum->amMonoforumAdmin()) { + return; + } + const auto navigation = _controller; + _addAction(tr::lng_profile_direct_messages(tr::now), [=] { + navigation->showPeerHistory( + monoforum, + Window::SectionShow::Way::Forward); + }, &st::menuIconChatDiscuss); +} + void Filler::addExportChat() { if (_thread->asTopic() || !_peer->canExportChatHistory()) { return; @@ -1077,6 +1124,9 @@ void Filler::addManageChat() { void Filler::addBoostChat() { if (const auto channel = _peer->asChannel()) { + if (channel->isMonoforum()) { + return; + } const auto text = channel->isMegagroup() ? tr::lng_boost_group_button(tr::now) : tr::lng_boost_channel_button(tr::now); @@ -1091,6 +1141,9 @@ void Filler::addBoostChat() { void Filler::addViewStatistics() { if (const auto channel = _peer->asChannel()) { + if (channel->isMonoforum()) { + return; + } const auto controller = _controller; const auto weak = base::make_weak(_thread); const auto peer = _peer; @@ -1126,7 +1179,7 @@ void Filler::addViewStatistics() { } } -void Filler::addCreatePoll() { +bool Filler::skipCreateActions() const { const auto isJoinChannel = [&] { if (_request.section != Section::Replies) { if (const auto c = _peer->asChannel(); c && !c->amIn()) { @@ -1151,10 +1204,13 @@ void Filler::addCreatePoll() { const auto isBlocked = [&] { return _peer && _peer->isUser() && _peer->asUser()->isBlocked(); }(); - if (isBlocked || isJoinChannel || isBotStart) { + return isBlocked || isJoinChannel || isBotStart; +} + +void Filler::addCreatePoll() { + if (skipCreateActions()) { return; } - const auto can = _topic ? Data::CanSend(_topic, ChatRestriction::SendPolls) : _peer->canCreatePolls(); @@ -1174,11 +1230,13 @@ void Filler::addCreatePoll() { : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); const auto replyTo = _request.currentReplyTo; + const auto suggest = _request.currentSuggest; auto callback = [=] { PeerMenuCreatePoll( controller, peer, replyTo, + suggest, flag, flag, source, @@ -1190,6 +1248,45 @@ void Filler::addCreatePoll() { &st::menuIconCreatePoll); } +void Filler::addCreateTodoList() { + if (skipCreateActions()) { + return; + } + const auto can = _topic + ? (_peer->session().premium() + && Data::CanSend(_topic, ChatRestriction::SendPolls)) + : _peer->canCreateTodoLists(); + if (!can) { + return; + } + const auto peer = _peer; + const auto controller = _controller; + const auto source = (_request.section == Section::Scheduled) + ? Api::SendType::Scheduled + : Api::SendType::Normal; + const auto sendMenuType = (_request.section == Section::Scheduled) + ? SendMenu::Type::Disabled + : (_request.section == Section::Replies + || _peer->starsPerMessageChecked()) + ? SendMenu::Type::SilentOnly + : SendMenu::Type::Scheduled; + const auto replyTo = _request.currentReplyTo; + const auto suggest = _request.currentSuggest; + auto callback = [=] { + PeerMenuCreateTodoList( + controller, + peer, + replyTo, + suggest, + source, + { sendMenuType }); + }; + _addAction( + tr::lng_todo_create(tr::now), + std::move(callback), + &st::menuIconCreateTodoList); +} + void Filler::addThemeEdit() { if (_peer->isVerifyCodes() || _peer->isRepliesChat()) { return; @@ -1209,7 +1306,7 @@ void Filler::addThemeEdit() { } void Filler::addTTLSubmenu(bool addSeparator) { - if (_thread->asTopic()) { + if (_thread->asTopic() || !_peer || _peer->isMonoforum()) { return; // #TODO later forum } const auto validator = TTLMenu::TTLValidator( @@ -1258,7 +1355,7 @@ void Filler::addSendGift() { void Filler::fill() { if (_folder) { fillArchiveActions(); - } else if (_sublist) { + } else if (_sublist && _peer->isSelf()) { fillSavedSublistActions(); } else switch (_request.section) { case Section::ChatsList: fillChatsListActions(); break; @@ -1266,7 +1363,8 @@ void Filler::fill() { case Section::Profile: fillProfileActions(); break; case Section::Replies: fillRepliesActions(); break; case Section::Scheduled: fillScheduledActions(); break; - case Section::ContextMenu: fillContextMenuActions(); break; + case Section::ContextMenu: + case Section::SubsectionTabsMenu: fillContextMenuActions(); break; default: Unexpected("_request.section in Filler::fill."); } } @@ -1335,6 +1433,7 @@ void Filler::addViewAsMessages() { void Filler::addViewAsTopics() { if (!_peer || !_peer->isForum() + || (_peer->asChannel()->flags() & ChannelDataFlag::ForumTabs) || !_controller->adaptive().isOneColumn()) { return; } @@ -1433,6 +1532,7 @@ void Filler::fillContextMenuActions() { void Filler::fillHistoryActions() { addToggleMuteSubmenu(true); + addCreateTopic(); addInfo(); AyuUi::AddJumpToBeginningAction(_peer, _thread, _controller, _addAction); AyuUi::AddOpenChannelAction(_peer, _controller, _addAction); @@ -1442,8 +1542,10 @@ void Filler::fillHistoryActions() { addSupportInfo(); addBoostChat(); addCreatePoll(); + addCreateTodoList(); addThemeEdit(); addViewDiscussion(); + addDirectMessages(); addExportChat(); addTranslate(); addReport(); @@ -1471,6 +1573,7 @@ void Filler::fillProfileActions() { addToggleTopicClosed(); AyuUi::AddOpenChannelAction(_peer, _controller, _addAction); addViewDiscussion(); + addDirectMessages(); addExportChat(); addToggleFolder(); addBlockUser(); @@ -1488,6 +1591,7 @@ void Filler::fillRepliesActions() { } addBoostChat(); addCreatePoll(); + addCreateTodoList(); addToggleTopicClosed(); addDeleteTopic(); AyuUi::AddDeletedMessagesActions(_peer, _thread, _controller, _addAction); @@ -1495,6 +1599,7 @@ void Filler::fillRepliesActions() { void Filler::fillScheduledActions() { addCreatePoll(); + addCreateTodoList(); } void Filler::fillArchiveActions() { @@ -1707,6 +1812,10 @@ void PeerMenuShareContactBox( state->share = nullptr; return; } + + auto action = Api::SendAction(strong, options); + action.clearDraft = false; + const auto withPaymentApproved = [=](int stars) { if (const auto onstack = state->share) { auto copy = options; @@ -1717,8 +1826,8 @@ void PeerMenuShareContactBox( const auto checked = state->sendPayment.check( navigation, peer, + action.options, 1, - options.starsApproved, withPaymentApproved); if (!checked) { return; @@ -1727,8 +1836,6 @@ void PeerMenuShareContactBox( strong, ShowAtTheEndMsgId, Window::SectionShow::Way::ClearStack); - auto action = Api::SendAction(strong, options); - action.clearDraft = false; strong->session().api().shareContact(user, action); state->share = nullptr; }; @@ -1765,6 +1872,7 @@ void PeerMenuCreatePoll( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo, + SuggestPostOptions suggest, PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, @@ -1795,6 +1903,12 @@ void PeerMenuCreatePoll( const auto weak = QPointer<CreatePollBox>(box); const auto state = box->lifetime().make_state<State>(); state->create = [=](const CreatePollBox::Result &result) { + auto action = Api::SendAction( + peer->owner().history(peer), + result.options); + action.replyTo = replyTo; + action.options.suggest = suggest; + const auto withPaymentApproved = crl::guard(weak, [=](int stars) { if (const auto onstack = state->create) { auto copy = result; @@ -1805,18 +1919,17 @@ void PeerMenuCreatePoll( const auto checked = state->sendPayment.check( controller, peer, + action.options, 1, - result.options.starsApproved, withPaymentApproved); if (!checked || std::exchange(state->lock, true)) { return; } - auto action = Api::SendAction( - peer->owner().history(peer), - result.options); - action.replyTo = replyTo; - const auto topicRootId = replyTo.topicRootId; - if (const auto local = action.history->localDraft(topicRootId)) { + + const auto local = action.history->localDraft( + replyTo.topicRootId, + replyTo.monoforumPeerId); + if (local) { action.clearDraft = local->textWithTags.text.isEmpty(); } else { action.clearDraft = false; @@ -1835,6 +1948,183 @@ void PeerMenuCreatePoll( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +void PeerMenuTodoWantsPremium(TodoWantsPremium type) { + const auto window = Core::App().activeWindow(); + if (!window) { + return; + } + const auto filter = [=](const auto &...) { + if (const auto controller = window->sessionController()) { + ShowPremiumPreviewBox(controller, PremiumFeature::TodoLists); + window->activate(); + } + return false; + }; + const auto link = Ui::Text::Link( + Ui::Text::Semibold(tr::lng_todo_premium_link(tr::now))); + const auto text = [&] { + switch (type) { + case TodoWantsPremium::Create: return tr::lng_todo_create_premium; + case TodoWantsPremium::Add: return tr::lng_todo_add_premium; + case TodoWantsPremium::Mark: return tr::lng_todo_mark_premium; + } + Unexpected("Type in PeerMenuTodoWantsPremium."); + }(); + constexpr auto kToastDuration = crl::time(4000); + window->uiShow()->showToast(Ui::Toast::Config{ + .text = text( + tr::now, + lt_link, + link, + Ui::Text::WithEntities), + .filter = filter, + .duration = kToastDuration, + }); +} + +void PeerMenuCreateTodoList( + not_null<Window::SessionController*> controller, + not_null<PeerData*> peer, + FullReplyTo replyTo, + SuggestPostOptions suggest, + Api::SendType sendType, + SendMenu::Details sendMenuDetails) { + if (!peer->session().premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Create); + return; + } + auto starsRequired = peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::FullInfo + | Data::PeerUpdate::Flag::StarsPerMessage + ) | rpl::map([=] { + return peer->starsPerMessageChecked(); + }); + auto box = Box<EditTodoListBox>( + controller, + std::move(starsRequired), + sendType, + sendMenuDetails); + struct State { + Fn<void(const EditTodoListBox::Result &)> create; + SendPaymentHelper sendPayment; + bool lock = false; + }; + const auto weak = QPointer<EditTodoListBox>(box); + const auto state = box->lifetime().make_state<State>(); + state->create = [=](const EditTodoListBox::Result &result) { + const auto withPaymentApproved = crl::guard(weak, [=](int stars) { + if (const auto onstack = state->create) { + auto copy = result; + copy.options.starsApproved = stars; + onstack(copy); + } + }); + auto action = Api::SendAction( + peer->owner().history(peer), + result.options); + action.replyTo = replyTo; + action.options.suggest = suggest; + + const auto checked = state->sendPayment.check( + controller, + peer, + action.options, + 1, + withPaymentApproved); + if (!checked || std::exchange(state->lock, true)) { + return; + } + + const auto local = action.history->localDraft( + replyTo.topicRootId, + replyTo.monoforumPeerId); + if (local) { + action.clearDraft = local->textWithTags.text.isEmpty(); + } else { + action.clearDraft = false; + } + const auto api = &peer->session().api(); + api->todoLists().create(result.todolist, action, crl::guard(weak, [=] { + state->create = nullptr; + weak->closeBox(); + }), crl::guard(weak, [=](const QString &error) { + state->lock = false; + weak->submitFailed(error); + })); + }; + box->submitRequests( + ) | rpl::start_with_next(state->create, box->lifetime()); + controller->show(std::move(box), Ui::LayerOption::CloseOther); +} + +void PeerMenuEditTodoList( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto media = item->media(); + const auto todolist = media ? media->todolist() : nullptr; + if (!todolist) { + return; + } else if (!item->history()->session().premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Add); + return; + } + auto box = Box<EditTodoListBox>(controller, item); + const auto weak = QPointer<EditTodoListBox>(box); + box->submitRequests( + ) | rpl::start_with_next([=](const EditTodoListBox::Result &result) { + const auto api = &item->history()->session().api(); + api->todoLists().edit( + item, + result.todolist, + result.options, + crl::guard(weak, [=] { weak->closeBox(); }), + crl::guard(weak, [=](const QString &error) { + weak->submitFailed(error); + })); + }, box->lifetime()); + controller->show(std::move(box), Ui::LayerOption::CloseOther); +} + +bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item) { + const auto media = item ? item->media() : nullptr; + const auto todolist = media ? media->todolist() : nullptr; + const auto appConfig = &item->history()->session().appConfig(); + return item->isRegular() + && !item->Has<HistoryMessageForwarded>() + && todolist + && (todolist->items.size() < appConfig->todoListItemsLimit()) + && (item->out() || todolist->othersCanAppend()); +} + +void PeerMenuAddTodoListTasks( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto session = &item->history()->session(); + if (!session->premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Add); + return; + } + const auto media = item->media(); + const auto todolist = media ? media->todolist() : nullptr; + if (!todolist) { + return; + } + auto box = Box<AddTodoListTasksBox>(controller, item); + const auto raw = box.data(); + box->submitRequests( + ) | rpl::start_with_next([=](const AddTodoListTasksBox::Result &result) { + const auto show = raw->uiShow(); + raw->closeBox(); + session->api().todoLists().add( + item, + result.items, + [] {}, + [=](const QString &error) { show->showToast(error); }); + }, box->lifetime()); + controller->show(std::move(box), Ui::LayerOption::CloseOther); +} + void PeerMenuBlockUserBox( not_null<Ui::GenericBox*> box, not_null<Window::Controller*> window, @@ -2465,7 +2755,7 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox( return true; } const auto id = SeparateId( - (peer->isForum() + ((peer->isForum() && !peer->asChannel()->useSubsectionTabs()) ? SeparateType::Forum : SeparateType::Chat), thread); @@ -2876,6 +3166,46 @@ QPointer<Ui::BoxContent> ShowDropMediaBox( return weak->data(); } +QPointer<Ui::BoxContent> ShowDropMediaBox( + not_null<Window::SessionNavigation*> navigation, + std::shared_ptr<QMimeData> data, + not_null<Data::SavedMessages*> monoforum, + FnMut<void()> &&successCallback) { + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + auto chosen = [ + data = std::move(data), + callback = std::move(successCallback), + weak, + navigation + ](not_null<Data::SavedSublist*> sublist) mutable { + const auto content = navigation->parentController()->content(); + if (!content->filesOrForwardDrop(sublist, data.get())) { + return; + } else if (const auto strong = *weak) { + strong->closeBox(); + } + if (callback) { + callback(); + } + }; + auto initBox = [=](not_null<PeerListBox*> box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + monoforum->destroyed( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }; + *weak = navigation->parentController()->show(Box<PeerListBox>( + std::make_unique<ChooseSublistBoxController>( + monoforum, + std::move(chosen)), + std::move(initBox))); + return weak->data(); +} + QPointer<Ui::BoxContent> ShowSendNowMessagesBox( not_null<Window::SessionNavigation*> navigation, not_null<History*> history, @@ -2999,14 +3329,18 @@ void HidePinnedBar( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Fn<void()> onHidden) { const auto callback = crl::guard(navigation, [=](Fn<void()> &&close) { close(); auto &session = peer->session(); - const auto migrated = topicRootId ? nullptr : peer->migrateFrom(); + const auto migrated = (topicRootId || monoforumPeerId) + ? nullptr + : peer->migrateFrom(); const auto top = Data::ResolveTopPinnedId( peer, topicRootId, + monoforumPeerId, migrated); const auto universal = !top ? MsgId(0) @@ -3017,6 +3351,7 @@ void HidePinnedBar( session.settings().setHiddenPinnedMessageId( peer->id, topicRootId, + monoforumPeerId, universal); session.saveSettingsDelayed(); if (onHidden) { @@ -3049,18 +3384,22 @@ void UnpinAllMessages( const auto sendRequest = [=](auto self) -> void { const auto history = strong->owningHistory(); const auto topicRootId = strong->topicRootId(); + const auto sublist = strong->asSublist(); + const auto monoforumPeerId = strong->monoforumPeerId(); using Flag = MTPmessages_UnpinAllMessages::Flag; api->request(MTPmessages_UnpinAllMessages( - MTP_flags(topicRootId ? Flag::f_top_msg_id : Flag()), + MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag()) + | (sublist ? Flag::f_saved_peer_id : Flag())), history->peer->input, - MTP_int(topicRootId.bare) + MTP_int(topicRootId.bare), + sublist ? sublist->sublistPeer()->input : MTPInputPeer() )).done([=](const MTPmessages_AffectedHistory &result) { const auto peer = history->peer; const auto offset = api->applyAffectedHistory(peer, result); if (offset > 0) { self(self); } else { - history->unpinMessagesFor(topicRootId); + history->unpinMessagesFor(topicRootId, monoforumPeerId); } }).send(); }; @@ -3204,6 +3543,19 @@ Fn<void()> DeleteAndLeaveHandler( }; } +Fn<void()> DeleteSublistHandler( + not_null<Window::SessionController*> controller, + not_null<Data::SavedSublist*> sublist) { + const auto weak = base::make_weak(sublist.get()); + return [=] { + if (const auto strong = weak.get()) { + if (!controller->showFrozenError()) { + controller->show(Box(DeleteSublistBox, strong)); + } + } + }; +} + void FillDialogsEntryMenu( not_null<SessionController*> controller, Dialogs::EntryState request, @@ -3317,8 +3669,7 @@ void MarkAsReadThread(not_null<Data::Thread*> thread) { if (!IsUnreadThread(thread)) { return; } else if (const auto forum = thread->asForum()) { - forum->enumerateTopics([]( - not_null<Data::ForumTopic*> topic) { + forum->enumerateTopics([](not_null<Data::ForumTopic*> topic) { MarkAsReadThread(topic); }); } else if (const auto history = thread->asHistory()) { @@ -3328,6 +3679,8 @@ void MarkAsReadThread(not_null<Data::Thread*> thread) { } } else if (const auto topic = thread->asTopic()) { topic->readTillEnd(); + } else if (const auto sublist = thread->asSublist()) { + sublist->readTillEnd(); } } diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index a125c3c55d..f01801423f 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -32,6 +32,8 @@ class Folder; class Session; struct ForwardDraft; class ForumTopic; +class SavedMessages; +class SavedSublist; class Thread; } // namespace Data @@ -105,10 +107,31 @@ void PeerMenuCreatePoll( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo = FullReplyTo(), + SuggestPostOptions suggest = SuggestPostOptions(), PollData::Flags chosen = PollData::Flags(), PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +enum class TodoWantsPremium { + Create, + Add, + Mark, +}; +void PeerMenuTodoWantsPremium(TodoWantsPremium type); +void PeerMenuCreateTodoList( + not_null<Window::SessionController*> controller, + not_null<PeerData*> peer, + FullReplyTo replyTo = FullReplyTo(), + SuggestPostOptions suggest = SuggestPostOptions(), + Api::SendType sendType = Api::SendType::Normal, + SendMenu::Details sendMenuDetails = SendMenu::Details()); +void PeerMenuEditTodoList( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item); +[[nodiscard]] bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item); +void PeerMenuAddTodoListTasks( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item); void PeerMenuDeleteTopicWithConfirmation( not_null<Window::SessionNavigation*> navigation, not_null<Data::ForumTopic*> topic); @@ -146,6 +169,9 @@ Fn<void()> ClearHistoryHandler( Fn<void()> DeleteAndLeaveHandler( not_null<Window::SessionController*> controller, not_null<PeerData*> peer); +Fn<void()> DeleteSublistHandler( + not_null<Window::SessionController*> controller, + not_null<Data::SavedSublist*> sublist); object_ptr<Ui::BoxContent> PrepareChooseRecipientBox( not_null<Main::Session*> session, @@ -188,6 +214,11 @@ QPointer<Ui::BoxContent> ShowDropMediaBox( std::shared_ptr<QMimeData> data, not_null<Data::Forum*> forum, FnMut<void()> &&successCallback = nullptr); +QPointer<Ui::BoxContent> ShowDropMediaBox( + not_null<Window::SessionNavigation*> navigation, + std::shared_ptr<QMimeData> data, + not_null<Data::SavedMessages*> monoforum, + FnMut<void()> &&successCallback = nullptr); QPointer<Ui::BoxContent> ShowSendNowMessagesBox( not_null<Window::SessionNavigation*> navigation, @@ -208,6 +239,7 @@ void HidePinnedBar( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Fn<void()> onHidden); void UnpinAllMessages( not_null<Window::SessionNavigation*> navigation, diff --git a/Telegram/SourceFiles/window/window_separate_id.cpp b/Telegram/SourceFiles/window/window_separate_id.cpp index 2b88968962..f6ed9155fd 100644 --- a/Telegram/SourceFiles/window/window_separate_id.cpp +++ b/Telegram/SourceFiles/window/window_separate_id.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "window/window_separate_id.h" +#include "data/data_channel.h" #include "data/data_folder.h" #include "data/data_peer.h" #include "data/data_saved_messages.h" @@ -44,12 +45,13 @@ SeparateId::SeparateId(not_null<PeerData*> peer) : SeparateId(SeparateType::Chat, peer->owner().history(peer)) { } -SeparateId::SeparateId(SeparateSharedMedia data) +SeparateId::SeparateId( + not_null<Data::Thread*> thread, + Storage::SharedMediaType sharedMediaType) : type(SeparateType::SharedMedia) -, sharedMedia(data.type) -, account(&data.peer->session().account()) -, sharedMediaDataPeer(data.peer) -, sharedMediaDataTopicRootId(data.topicRootId) { +, sharedMediaType(sharedMediaType) +, account(&thread->session().account()) +, thread(thread) { } bool SeparateId::primary() const { @@ -71,9 +73,9 @@ Data::Folder *SeparateId::folder() const { } Data::SavedSublist *SeparateId::sublist() const { - return (type == SeparateType::SavedSublist) - ? thread->owner().savedMessages().sublist(thread->peer()).get() - : nullptr; + return (type != SeparateType::SavedSublist) + ? nullptr + : thread->asSublist(); } bool SeparateId::hasChatsList() const { @@ -82,16 +84,4 @@ bool SeparateId::hasChatsList() const { || (type == SeparateType::Forum); } -PeerData *SeparateId::sharedMediaPeer() const { - return (type == SeparateType::SharedMedia) - ? sharedMediaDataPeer - : nullptr; -} - -MsgId SeparateId::sharedMediaTopicRootId() const { - return (type == SeparateType::SharedMedia) - ? sharedMediaDataTopicRootId - : MsgId(); -} - } // namespace Window diff --git a/Telegram/SourceFiles/window/window_separate_id.h b/Telegram/SourceFiles/window/window_separate_id.h index 81f417d4fb..dbc79397a5 100644 --- a/Telegram/SourceFiles/window/window_separate_id.h +++ b/Telegram/SourceFiles/window/window_separate_id.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +class ChannelData; class PeerData; namespace Data { @@ -21,6 +22,10 @@ class Account; class Session; } // namespace Main +namespace Storage { +enum class SharedMediaType : signed char; +} // namespace Storage + namespace Window { enum class SeparateType { @@ -32,21 +37,9 @@ enum class SeparateType { SharedMedia, }; -enum class SeparateSharedMediaType { - None, - Photos, - Videos, - Files, - Audio, - Links, - Voices, - GIF, -}; - struct SeparateSharedMedia { - SeparateSharedMediaType type = SeparateSharedMediaType::None; - not_null<PeerData*> peer; - MsgId topicRootId = MsgId(); + not_null<Data::Thread*> thread; + Storage::SharedMediaType type = {}; }; struct SeparateId { @@ -56,15 +49,14 @@ struct SeparateId { SeparateId(SeparateType type, not_null<Data::Thread*> thread); SeparateId(not_null<Data::Thread*> thread); SeparateId(not_null<PeerData*> peer); - SeparateId(SeparateSharedMedia data); + SeparateId( + not_null<Data::Thread*> thread, + Storage::SharedMediaType sharedMediaType); SeparateType type = SeparateType::Primary; - SeparateSharedMediaType sharedMedia = SeparateSharedMediaType::None; + Storage::SharedMediaType sharedMediaType = {}; Main::Account *account = nullptr; Data::Thread *thread = nullptr; // For types except Main and Archive. - PeerData *sharedMediaDataPeer = nullptr; - MsgId sharedMediaDataTopicRootId = MsgId(); - [[nodiscard]] bool valid() const { return account != nullptr; } @@ -77,8 +69,6 @@ struct SeparateId { [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] Data::Folder *folder() const; [[nodiscard]] Data::SavedSublist *sublist() const; - [[nodiscard]] PeerData *sharedMediaPeer() const; - [[nodiscard]] MsgId sharedMediaTopicRootId() const; [[nodiscard]] bool hasChatsList() const; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 6f9d6b7df5..c849a249f9 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -25,17 +25,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/view/reactions/history_view_reactions.h" //#include "history/view/reactions/history_view_reactions_button.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_scheduled_section.h" -#include "history/view/history_view_sublist_section.h" +#include "history/view/history_view_subsection_tabs.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document_resolver.h" #include "data/data_download_manager.h" #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_file_origin.h" +#include "data/data_flags.h" #include "data/data_folder.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -588,7 +590,8 @@ void SessionNavigation::showPeerByLinkResolved( if (const auto forum = peer->forum()) { if (controller->windowId().hasChatsList() && !controller->adaptive().isOneColumn() - && controller->shownForum().current() != forum) { + && controller->shownForum().current() != forum + && !forum->channel()->useSubsectionTabs()) { controller->showForum(forum); } } @@ -1139,9 +1142,12 @@ void SessionNavigation::showRepliesForMessage( if (const auto topic = history->peer->forumTopicFor(rootId)) { auto replies = topic->replies(); if (replies->unreadCountKnown()) { - auto memento = std::make_shared<HistoryView::RepliesMemento>( - history, - rootId, + using namespace HistoryView; + auto memento = std::make_shared<ChatMemento>( + ChatViewId{ + .history = history, + .repliesRootId = rootId, + }, commentId, params.highlightPart, params.highlightPartOffsetHint); @@ -1155,7 +1161,7 @@ void SessionNavigation::showRepliesForMessage( && _showingRepliesRootId == rootId) { return; } else if (!history->peer->asChannel()) { - // HistoryView::RepliesWidget right now handles only channels. + // HistoryView::ChatWidget replies right now handles only channels. return; } _api.request(base::take(_showingRepliesRequestId)).cancel(); @@ -1210,14 +1216,16 @@ void SessionNavigation::showRepliesForMessage( } } if (deleted || item) { + using namespace HistoryView; auto memento = item - ? std::make_shared<HistoryView::RepliesMemento>( + ? std::make_shared<ChatMemento>( + ChatMemento::Comments(), item, commentId) - : std::make_shared<HistoryView::RepliesMemento>( - history, - rootId, - commentId); + : std::make_shared<ChatMemento>(ChatViewId{ + .history = history, + .repliesRootId = rootId, + }, commentId); memento->setReadInformation( data.vread_inbox_max_id().value_or_empty(), data.vunread_count().v, @@ -1253,12 +1261,30 @@ void SessionNavigation::showTopic( params); } +void SessionNavigation::showSublist( + not_null<Data::SavedSublist*> sublist, + MsgId itemId, + const SectionShow ¶ms) { + using namespace HistoryView; + auto memento = std::make_shared<ChatMemento>( + ChatViewId{ + .history = sublist->owningHistory(), + .sublist = sublist, + }, + itemId, + params.highlightPart, + params.highlightPartOffsetHint); + showSection(std::move(memento), params); +} + void SessionNavigation::showThread( not_null<Data::Thread*> thread, MsgId itemId, const SectionShow ¶ms) { if (const auto topic = thread->asTopic()) { showTopic(topic, itemId, params); + } else if (const auto sublist = thread->asSublist()) { + showSublist(sublist, itemId, params); } else { showPeerHistory(thread->asHistory(), params, itemId); } @@ -1283,6 +1309,9 @@ void SessionNavigation::showPeerInfo( const SectionShow ¶ms) { if (const auto topic = thread->asTopic()) { showSection(std::make_shared<Info::Memento>(topic), params); + } else if (const auto sublist = thread->asSublist() + ; sublist && sublist->parentChat()) { + showPeerInfo(sublist->sublistPeer()->id, params); } else { showPeerInfo(thread->peer()->id, params); } @@ -1325,42 +1354,24 @@ void SessionNavigation::showByInitialId( showThread(id.thread, msgId, instant); break; case SeparateType::SharedMedia: { - Assert(id.sharedMedia != SeparateSharedMediaType::None); clearSectionStack(instant); - const auto type = (id.sharedMedia == SeparateSharedMediaType::Photos) - ? Storage::SharedMediaType::Photo - : (id.sharedMedia == SeparateSharedMediaType::Videos) - ? Storage::SharedMediaType::Video - : (id.sharedMedia == SeparateSharedMediaType::Files) - ? Storage::SharedMediaType::File - : (id.sharedMedia == SeparateSharedMediaType::Audio) - ? Storage::SharedMediaType::MusicFile - : (id.sharedMedia == SeparateSharedMediaType::Links) - ? Storage::SharedMediaType::Link - : (id.sharedMedia == SeparateSharedMediaType::Voices) - ? Storage::SharedMediaType::RoundVoiceFile - : (id.sharedMedia == SeparateSharedMediaType::GIF) - ? Storage::SharedMediaType::GIF - : Storage::SharedMediaType::Photo; - const auto topicRootId = id.sharedMediaTopicRootId(); - const auto peer = id.sharedMediaPeer(); - const auto topic = topicRootId - ? peer->forumTopicFor(topicRootId) - : nullptr; - if (topicRootId && !topic) { - break; - } + const auto type = id.sharedMediaType; + const auto topic = id.thread->asTopic(); showSection( - topicRootId + (topic ? std::make_shared<Info::Memento>(topic, type) - : std::make_shared<Info::Memento>(peer, type), + : std::make_shared<Info::Memento>(id.thread->peer(), type)), instant); parent->widget()->setMaximumWidth(st::maxWidthSharedMediaWindow); break; } case SeparateType::SavedSublist: + using namespace HistoryView; showSection( - std::make_shared<HistoryView::SublistMemento>(id.sublist()), + std::make_shared<ChatMemento>(ChatViewId{ + .history = id.sublist()->owningHistory(), + .sublist = id.sublist(), + }), instant); break; } @@ -1873,8 +1884,16 @@ bool SessionController::showForumInDifferentWindow( void SessionController::showForum( not_null<Data::Forum*> forum, const SectionShow ¶ms) { + const auto forced = params.forceTopicsList; if (showForumInDifferentWindow(forum, params)) { return; + } else if (!forced && forum->channel()->useSubsectionTabs()) { + if (const auto active = forum->activeSubsectionThread()) { + showThread(active, ShowAtUnreadMsgId, params); + } else { + showPeerHistory(forum->channel(), params); + } + return; } _shownForumLifetime.destroy(); if (_shownForum.current() != forum) { @@ -1889,10 +1908,11 @@ void SessionController::showForum( if (_shownForum.current() != forum) { return; } - forum->destroyed( - ) | rpl::start_with_next([=, history = forum->history()] { + const auto history = forum->history(); + const auto closeAndShowHistory = [=](bool showOnlyIfEmpty) { const auto now = activeChatCurrent().owningHistory(); - const auto showHistory = !now || (now == history); + const auto showHistory = !now + || (!showOnlyIfEmpty && (now == history)); const auto weak = base::make_weak(this); closeForum(); if (weak && showHistory) { @@ -1902,8 +1922,27 @@ void SessionController::showForum( anim::activation::background, }); } - }, _shownForumLifetime); + }; content()->showForum(forum, params); + if (_shownForum.current() != forum) { + return; + } + + forum->destroyed( + ) | rpl::start_with_next([=] { + closeAndShowHistory(false); + }, _shownForumLifetime); + if (!forced) { + using FlagChange = Data::Flags<ChannelDataFlags>::Change; + forum->channel()->flagsValue( + ) | rpl::start_with_next([=](FlagChange change) { + if (change.diff & ChannelDataFlag::ForumTabs) { + if (HistoryView::SubsectionTabs::UsedFor(history)) { + closeAndShowHistory(true); + } + } + }, _shownForumLifetime); + } } void SessionController::closeForum() { @@ -1973,9 +2012,9 @@ void SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) { Data::PeerFlagValue( channel, ChannelData::Flag::Forum - ) | rpl::filter( - rpl::mappers::_1 - ) | rpl::start_with_next([=] { + ) | rpl::filter([=](bool forum) { + return forum && !channel->useSubsectionTabs(); + }) | rpl::start_with_next([=] { clearSectionStack( { anim::type::normal, anim::activation::background }); showForum(channel->forum(), @@ -2083,9 +2122,13 @@ bool SessionController::switchInlineQuery( && to.currentReplyTo.quote.empty()) { to.currentReplyTo.messageId.msg = MsgId(); } + if (!history->suggestDraftAllowed()) { + to.currentSuggest = SuggestPostOptions(); + } auto draft = std::make_unique<Data::Draft>( textWithTags, to.currentReplyTo, + to.currentSuggest, cursor, Data::WebPageDraft()); @@ -2098,8 +2141,9 @@ bool SessionController::switchInlineQuery( params); } else { const auto topicRootId = to.currentReplyTo.topicRootId; + const auto monoforumPeerId = to.currentReplyTo.monoforumPeerId; history->setLocalDraft(std::move(draft)); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); if (to.section == Section::Replies) { const auto commentId = MsgId(); showRepliesForMessage(history, topicRootId, commentId, params); @@ -2907,8 +2951,12 @@ void SessionController::openPhoto( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } - _window->openInMediaView( - Media::View::OpenRequest(this, photo, item, message.topicRootId)); + _window->openInMediaView(Media::View::OpenRequest( + this, + photo, + item, + message.topicRootId, + message.monoforumPeerId)); } void SessionController::openPhoto( @@ -2943,11 +2991,17 @@ void SessionController::openDocument( document, item, message.topicRootId, + message.monoforumPeerId, false, usedTimestamp)); return; } - Data::ResolveDocument(this, document, item, message.topicRootId); + Data::ResolveDocument( + this, + document, + item, + message.topicRootId, + message.monoforumPeerId); } bool SessionController::openSharedStory(HistoryItem *item) { @@ -3385,8 +3439,37 @@ std::shared_ptr<ChatHelpers::Show> SessionController::uiShow() { return _cachedShow; } +void SessionController::saveSubsectionTabs( + std::unique_ptr<HistoryView::SubsectionTabs> tabs) { + _savedSubsectionTabsLifetime.destroy(); + _savedSubsectionTabs = std::move(tabs); + _savedSubsectionTabs->extractToParent(widget()); + _savedSubsectionTabs->removeRequests() | rpl::start_with_next([=] { + _savedSubsectionTabs = nullptr; + }, _savedSubsectionTabsLifetime); +} + +auto SessionController::restoreSubsectionTabsFor( + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread) +-> std::unique_ptr<HistoryView::SubsectionTabs> { + if (!_savedSubsectionTabs) { + return nullptr; + } else if (_savedSubsectionTabs->switchTo(thread, parent)) { + _savedSubsectionTabsLifetime.destroy(); + return base::take(_savedSubsectionTabs); + } + return nullptr; +} + +void SessionController::dropSubsectionTabs() { + _savedSubsectionTabsLifetime.destroy(); + base::take(_savedSubsectionTabs); +} + SessionController::~SessionController() { resetFakeUnreadWhileOpened(); + dropSubsectionTabs(); } bool CheckAndJumpToNearChatsFilter( diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 040739589d..9af6714b60 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -26,6 +26,7 @@ enum class WindowLayout; namespace Data { struct StoriesContext; +class SavedMessages; enum class StorySourcesList : uchar; } // namespace Data @@ -73,9 +74,14 @@ enum class CloudThemeType; class Thread; class Forum; class ForumTopic; +class SavedSublist; class WallPaper; } // namespace Data +namespace HistoryView { +class SubsectionTabs; +} // namespace HistoryView + namespace HistoryView::Reactions { class CachedIconFactory; } // namespace HistoryView::Reactions @@ -165,6 +171,7 @@ struct SectionShow { bool thirdColumn = false; bool childColumn = false; bool forbidLayer = false; + bool forceTopicsList = false; bool reapplyLocalDraft = false; bool dropSameFromStack = false; Origin origin; @@ -200,6 +207,10 @@ public: not_null<Data::ForumTopic*> topic, MsgId itemId = 0, const SectionShow ¶ms = SectionShow()); + void showSublist( + not_null<Data::SavedSublist*> sublist, + MsgId itemId = 0, + const SectionShow ¶ms = SectionShow()); void showThread( not_null<Data::Thread*> thread, MsgId itemId = 0, @@ -513,6 +524,7 @@ public: struct MessageContext { FullMsgId id; MsgId topicRootId; + PeerId monoforumPeerId; }; void openPhoto( not_null<PhotoData*> photo, @@ -647,6 +659,14 @@ public: [[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() override; + void saveSubsectionTabs( + std::unique_ptr<HistoryView::SubsectionTabs> tabs); + [[nodiscard]] auto restoreSubsectionTabsFor( + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread) + -> std::unique_ptr<HistoryView::SubsectionTabs>; + void dropSubsectionTabs(); + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } @@ -760,6 +780,8 @@ private: base::has_weak_ptr _storyOpenGuard; QString _premiumRef; + std::unique_ptr<HistoryView::SubsectionTabs> _savedSubsectionTabs; + rpl::lifetime _savedSubsectionTabsLifetime; rpl::lifetime _lifetime; diff --git a/Telegram/ThirdParty/jemalloc b/Telegram/ThirdParty/jemalloc deleted file mode 160000 index 54eaed1d8b..0000000000 --- a/Telegram/ThirdParty/jemalloc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 54eaed1d8b56b1aa528be3bdd1877e59c56fa90c diff --git a/Telegram/ThirdParty/libtgvoip b/Telegram/ThirdParty/libtgvoip deleted file mode 160000 index 2d25928604..0000000000 --- a/Telegram/ThirdParty/libtgvoip +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2d2592860478e60d972b96e67ee034b8a71bb57a diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 2afc6b1e62..99fb373035 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,175 +1,217 @@ -{%- set GIT = "https://github.com" -%} -{%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} -{%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git config -f .gitmodules submodule.m4.shallow true && git submodule init && git submodule update" -%} -{%- set TOOLSET = "gcc-toolset-12" -%} -{%- set QT = "6.9.0" -%} -{%- set QT_TAG = "v" ~ QT -%} -{%- set CFLAGS_DEBUG = "$CFLAGS -O0 -fno-lto -U_FORTIFY_SOURCE" -%} -{%- set LibrariesPath = "/usr/src/Libraries" -%} - # syntax=docker/dockerfile:1 FROM rockylinux:8 AS builder -ENV LANG C.UTF-8 -ENV PATH /opt/rh/{{ TOOLSET }}/root/usr/bin:$PATH -ENV LIBRARY_PATH /opt/rh/{{ TOOLSET }}/root/usr/lib64:/opt/rh/{{ TOOLSET }}/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib -ENV LD_LIBRARY_PATH $LIBRARY_PATH -ENV PKG_CONFIG_PATH /opt/rh/{{ TOOLSET }}/root/usr/lib64/pkgconfig:/opt/rh/{{ TOOLSET }}/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig +ENV LANG=C.UTF-8 +ENV TOOLSET=gcc-toolset-14 +ENV PATH=/opt/rh/$TOOLSET/root/usr/bin:$PATH +ENV LIBRARY_PATH=/opt/rh/$TOOLSET/root/usr/lib64:/opt/rh/$TOOLSET/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib +ENV LD_LIBRARY_PATH=$LIBRARY_PATH +ENV PKG_CONFIG_PATH=/opt/rh/$TOOLSET/root/usr/lib64/pkgconfig:/opt/rh/$TOOLSET/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ python3.11-pip python3.11-devel gperf flex bison clang clang-tools-extra \ - lld nasm yasm file which perl-open perl-XML-Parser perl-IPC-Cmd \ - xorg-x11-util-macros {{ TOOLSET }}-gcc {{ TOOLSET }}-gcc-c++ \ - {{ TOOLSET }}-binutils {{ TOOLSET }}-gdb {{ TOOLSET }}-libasan-devel \ - libffi-devel fontconfig-devel freetype-devel libX11-devel wayland-devel \ - alsa-lib-devel pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel \ - mesa-libgbm-devel libdrm-devel vulkan-devel libva-devel libvdpau-devel \ - glib2-devel at-spi2-core-devel gtk3-devel boost1.78-devel fmt-devel \ + lld nasm yasm file which wget perl-open perl-XML-Parser perl-IPC-Cmd \ + xorg-x11-util-macros $TOOLSET-gcc $TOOLSET-gcc-c++ $TOOLSET-binutils \ + $TOOLSET-gdb $TOOLSET-libasan-devel libffi-devel fontconfig-devel \ + freetype-devel libX11-devel wayland-devel alsa-lib-devel \ + pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel mesa-libgbm-devel \ + libdrm-devel vulkan-devel libva-devel libvdpau-devel libselinux-devel \ + libmount-devel systemd-devel glib2-devel gobject-introspection-devel \ + at-spi2-core-devel gtk3-devel boost1.78-devel \ && dnf clean all RUN alternatives --set python3 /usr/bin/python3.11 RUN python3 -m pip install meson ninja +RUN cat <<EOF > /usr/local/bin/pkg-config && chmod +x /usr/local/bin/pkg-config +#!/bin/sh +for i in "\$@"; do + [ "\$i" = "--version" ] && exec /usr/bin/pkg-config "\$i" +done +exec /usr/bin/pkg-config --static "\$@" +EOF +RUN sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/")/ -O0 {% if LTO %}-fno-lto -fno-use-linker-plugin -fuse-ld=lld{% endif %}")/' /usr/share/cmake/Modules/Compiler/GNU.cmake +RUN sed -i 's/NO_DEFAULT_PATH//g; s/PKG_CONFIG_ALLOW_SYSTEM_LIBS/PKG_CONFIG_IS_DUMB/g' /usr/share/cmake/Modules/FindPkgConfig.cmake +RUN sed -i 's/set(OpenGL_GL_PREFERENCE "")/set(OpenGL_GL_PREFERENCE "LEGACY")/' /usr/share/cmake/Modules/FindOpenGL.cmake RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc -RUN echo set debuginfod enabled on > /opt/rh/{{ TOOLSET }}/root/etc/gdbinit.d/00-debuginfod.gdb +RUN sed -i 's/-lharfbuzz//' /usr/lib64/pkgconfig/harfbuzz.pc +RUN sed -i 's/-lpng16//' /usr/lib64/pkgconfig/libpng16.pc +RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user -WORKDIR {{ LibrariesPath }} -ENV AR gcc-ar -ENV RANLIB gcc-ranlib -ENV NM gcc-nm -ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -ENV CXXFLAGS $CFLAGS +WORKDIR /usr/src +ENV AR=gcc-ar +ENV RANLIB=gcc-ranlib +ENV NM=gcc-nm +ENV CFLAGS='{% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' +ENV CXXFLAGS=$CFLAGS +ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %} -static-libstdc++ -static-libgcc -static-libasan -pthread -ldl -Wl,--as-needed -Wl,-z,muldefs' + +ENV CMAKE_GENERATOR=Ninja +ENV CMAKE_BUILD_TYPE=None +ENV CMAKE_BUILD_PARALLEL_LEVEL='{{ JOBS }}' + +RUN git init Implib.so \ + && cd Implib.so \ + && git remote add origin https://github.com/yugr/Implib.so.git \ + && git fetch --depth=1 origin ecf7bb51a92a0fb16834c5b698570ab25f9f1d21 \ + && git reset --hard FETCH_HEAD \ + && mkdir build \ + && cd build \ + && implib() { \ + LIBFILE=$(basename $1); \ + LIBNAME=$(basename $1 .so); \ + ../implib-gen.py -q $1; \ + gcc $CFLAGS -c -o $LIBFILE.tramp.o $LIBFILE.tramp.S; \ + gcc $CFLAGS -c -o $LIBFILE.init.o $LIBFILE.init.c; \ + ar rcs /usr/local/lib64/$LIBNAME.a $LIBFILE.tramp.o $LIBFILE.init.o; \ + } \ + && implib /usr/lib64/libgtk-3.so \ + && implib /usr/lib64/libgdk-3.so \ + && implib /usr/lib64/libgdk_pixbuf-2.0.so \ + && implib /usr/lib64/libpango-1.0.so \ + && implib /usr/lib64/libvdpau.so \ + && implib /usr/lib64/libva-x11.so \ + && implib /usr/lib64/libva-drm.so \ + && implib /usr/lib64/libva.so \ + && implib /usr/lib64/libEGL.so \ + && implib /usr/lib64/libGL.so \ + && implib /usr/lib64/libdrm.so \ + && implib /usr/lib64/libwayland-egl.so \ + && implib /usr/lib64/libwayland-cursor.so \ + && implib /usr/lib64/libwayland-client.so \ + && implib /usr/lib64/libwayland-server.so \ + && implib /usr/lib64/libX11-xcb.so \ + && implib /usr/lib64/libxcb.so \ + && cd ../.. \ + && rm -rf Implib.so FROM builder AS patches RUN git init patches \ && cd patches \ - && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin 7119a74e3f9b782f3cc29bf52fc78f2e8b0ca352 \ + && git remote add origin https://github.com/desktop-app/patches.git \ + && git fetch --depth=1 origin 1ffcb17817a2cab167061d530703842395291e69 \ && git reset --hard FETCH_HEAD \ && rm -rf .git FROM builder AS zlib -RUN git clone -b v1.3.1 --depth=1 {{ GIT }}/madler/zlib.git \ +RUN git clone -b v1.3.1 --depth=1 https://github.com/madler/zlib.git \ && cd zlib \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ - -DZLIB_BUILD_EXAMPLES=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/zlib-cache" cmake --install build \ + && cmake -B build . -DZLIB_BUILD_EXAMPLES=OFF \ + && cmake --build build \ + && export DESTDIR=/usr/src/zlib-cache \ + && cmake --install build \ + && rm $DESTDIR/usr/local/lib/libz.so* \ && cd .. \ && rm -rf zlib FROM builder AS xz -RUN git clone -b v5.8.1 --depth=1 {{ GIT }}/tukaani-project/xz.git \ +RUN git clone -b v5.8.1 --depth=1 https://github.com/tukaani-project/xz.git \ && cd xz \ - && cmake -GNinja -B build . -DCMAKE_BUILD_TYPE=None \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/xz-cache" cmake --install build \ + && cmake -B build . \ + && cmake --build build \ + && DESTDIR=/usr/src/xz-cache cmake --install build \ && cd .. \ && rm -rf xz FROM builder AS protobuf -RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules {{ GIT }}/protocolbuffers/protobuf.git \ +RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules https://github.com/protocolbuffers/protobuf.git \ && cd protobuf \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -Dprotobuf_BUILD_TESTS=OFF \ -Dprotobuf_BUILD_PROTOBUF_BINARIES=ON \ -Dprotobuf_BUILD_LIBPROTOC=ON \ -Dprotobuf_WITH_ZLIB=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/protobuf-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/protobuf-cache cmake --install build \ && cd .. \ && rm -rf protobuf FROM builder AS lcms2 -RUN git clone -b lcms2.15 --depth=1 {{ GIT }}/mm2/Little-CMS.git \ +RUN git clone -b lcms2.15 --depth=1 https://github.com/mm2/Little-CMS.git \ && cd Little-CMS \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/lcms2-cache" meson install -C build \ + && DESTDIR=/usr/src/lcms2-cache meson install -C build \ && cd .. \ && rm -rf Little-CMS FROM builder AS brotli -RUN git clone -b v1.1.0 --depth=1 {{ GIT }}/google/brotli.git \ +RUN git clone -b v1.1.0 --depth=1 https://github.com/google/brotli.git \ && cd brotli \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DBROTLI_DISABLE_TESTS=ON \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/brotli-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/brotli-cache cmake --install build \ && cd .. \ && rm -rf brotli FROM builder AS highway -RUN git clone -b 1.0.7 --depth=1 {{ GIT }}/google/highway.git \ +RUN git clone -b 1.0.7 --depth=1 https://github.com/google/highway.git \ && cd highway \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_TESTING=OFF \ -DHWY_ENABLE_CONTRIB=OFF \ -DHWY_ENABLE_EXAMPLES=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/highway-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/highway-cache cmake --install build \ && cd .. \ && rm -rf highway FROM builder AS opus -RUN git clone -b v1.5.2 --depth=1 {{ GIT }}/xiph/opus.git \ +RUN git clone -b v1.5.2 --depth=1 https://github.com/xiph/opus.git \ && cd opus \ - && cmake -GNinja -B build . -DCMAKE_BUILD_TYPE=None \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/opus-cache" cmake --install build \ + && cmake -B build . \ + && cmake --build build \ + && DESTDIR=/usr/src/opus-cache cmake --install build \ && cd .. \ && rm -rf opus FROM builder AS dav1d -RUN git clone -b 1.4.1 --depth=1 {{ GIT }}/videolan/dav1d.git \ +RUN git clone -b 1.5.1 --depth=1 https://github.com/videolan/dav1d.git \ && cd dav1d \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ -Denable_tools=false \ -Denable_tests=false \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/dav1d-cache" meson install -C build \ + && DESTDIR=/usr/src/dav1d-cache meson install -C build \ && cd .. \ && rm -rf dav1d FROM builder AS openh264 -RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \ +RUN git clone -b v2.6.0 --depth=1 https://github.com/cisco/openh264.git \ && cd openh264 \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/openh264-cache" meson install -C build \ + && DESTDIR=/usr/src/openh264-cache meson install -C build \ && cd .. \ && rm -rf openh264 -FROM builder AS libde265 -RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \ +FROM builder AS de265 +RUN git clone -b v1.0.16 --depth=1 https://github.com/strukturag/libde265.git \ && cd libde265 \ - && cmake -GNinja . \ + && cmake -B build . \ -DCMAKE_BUILD_TYPE=None \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_DECODER=OFF \ -DENABLE_SDL=OFF \ - && cmake --build . --parallel \ - && DESTDIR="{{ LibrariesPath }}/libde265-cache" cmake --install . \ + && cmake --build build \ + && DESTDIR=/usr/src/de265-cache cmake --install build \ && cd .. \ && rm -rf libde265 -FROM builder AS libvpx +FROM builder AS vpx RUN git init libvpx \ && cd libvpx \ - && git remote add origin {{ GIT }}/webmproject/libvpx.git \ + && git remote add origin https://github.com/webmproject/libvpx.git \ && git fetch --depth=1 origin 12f3a2ac603e8f10742105519e0cd03c3b8f71dd \ && git reset --hard FETCH_HEAD \ && CFLAGS="$CFLAGS -fno-lto" CXXFLAGS="$CXXFLAGS -fno-lto" ./configure \ @@ -182,15 +224,14 @@ RUN git init libvpx \ --enable-webm-io \ --size-limit=4096x4096 \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libvpx-cache" install \ + && make DESTDIR=/usr/src/vpx-cache install \ && cd .. \ && rm -rf libvpx -FROM builder AS libwebp -RUN git clone -b chrome-m116-5845 --depth=1 {{ GIT }}/webmproject/libwebp.git \ +FROM builder AS webp +RUN git clone -b v1.5.0 --depth=1 https://github.com/webmproject/libwebp.git \ && cd libwebp \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DWEBP_BUILD_ANIM_UTILS=OFF \ -DWEBP_BUILD_CWEBP=OFF \ -DWEBP_BUILD_DWEBP=OFF \ @@ -200,59 +241,59 @@ RUN git clone -b chrome-m116-5845 --depth=1 {{ GIT }}/webmproject/libwebp.git \ -DWEBP_BUILD_WEBPMUX=OFF \ -DWEBP_BUILD_WEBPINFO=OFF \ -DWEBP_BUILD_EXTRAS=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/libwebp-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/webp-cache cmake --install build \ && cd .. \ && rm -rf libwebp -FROM builder AS libavif -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / +FROM builder AS avif +COPY --link --from=dav1d /usr/src/dav1d-cache / -RUN git clone -b v1.0.4 --depth=1 {{ GIT }}/AOMediaCodec/libavif.git \ +RUN git clone -b v1.3.0 --depth=1 https://github.com/AOMediaCodec/libavif.git \ && cd libavif \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ - -DAVIF_CODEC_DAV1D=ON \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/libavif-cache" cmake --install build \ + -DAVIF_CODEC_DAV1D=SYSTEM \ + -DAVIF_LIBYUV=OFF \ + && cmake --build build \ + && DESTDIR=/usr/src/avif-cache cmake --install build \ && cd .. \ && rm -rf libavif -FROM builder AS libheif -COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / +FROM builder AS heif +COPY --link --from=de265 /usr/src/de265-cache / -RUN git clone -b v1.18.2 --depth=1 {{ GIT }}/strukturag/libheif.git \ +RUN git clone -b v1.19.8 --depth=1 https://github.com/strukturag/libheif.git \ && cd libheif \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ -DENABLE_PLUGIN_LOADING=OFF \ -DWITH_X265=OFF \ -DWITH_AOM_DECODER=OFF \ -DWITH_AOM_ENCODER=OFF \ + -DWITH_OpenH264_DECODER=OFF \ -DWITH_RAV1E=OFF \ -DWITH_RAV1E_PLUGIN=OFF \ -DWITH_SvtEnc=OFF \ -DWITH_SvtEnc_PLUGIN=OFF \ -DWITH_DAV1D=OFF \ + -DWITH_LIBSHARPYUV=OFF \ -DWITH_EXAMPLES=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/libheif-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/heif-cache cmake --install build \ && cd .. \ && rm -rf libheif -FROM builder AS libjxl -COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / -COPY --link --from=highway {{ LibrariesPath }}/highway-cache / +FROM builder AS jxl +COPY --link --from=lcms2 /usr/src/lcms2-cache / +COPY --link --from=brotli /usr/src/brotli-cache / +COPY --link --from=highway /usr/src/highway-cache / -RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ +RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ && cd libjxl \ && git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ -DJPEGXL_ENABLE_DEVTOOLS=OFF \ @@ -266,220 +307,233 @@ RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ -DJPEGXL_ENABLE_SJPEG=OFF \ -DJPEGXL_ENABLE_OPENEXR=OFF \ -DJPEGXL_ENABLE_SKCMS=OFF \ - && cmake --build build --parallel \ - && export DESTDIR="{{ LibrariesPath }}/libjxl-cache" \ + && cmake --build build \ + && export DESTDIR=/usr/src/jxl-cache \ && cmake --install build \ + && sed -i 's/-lstdc++//' $DESTDIR/usr/local/lib64/pkgconfig/libjxl*.pc \ + && rm $DESTDIR/usr/local/lib64/libjpeg.so* \ && cp build/lib/libjpegli-static.a $DESTDIR/usr/local/lib64/libjpeg.a \ - && ar rcs $DESTDIR/usr/local/lib64/libjpeg.a build/lib/CMakeFiles/jpegli-libjpeg-obj.dir/jpegli/libjpeg_wrapper.cc.o \ + && mkdir build/hwy \ + && ar --output=build/hwy x /usr/local/lib64/libhwy.a \ + && ar rcs $DESTDIR/usr/local/lib64/libjpeg.a build/lib/CMakeFiles/jpegli-libjpeg-obj.dir/jpegli/libjpeg_wrapper.cc.o build/hwy/* \ && cd .. \ && rm -rf libjxl FROM builder AS rnnoise -RUN git clone -b master --depth=1 {{ GIT }}/desktop-app/rnnoise.git \ +RUN git clone -b v0.2 --depth=1 https://github.com/xiph/rnnoise.git \ && cd rnnoise \ - && cmake -GNinja -B build . -DCMAKE_BUILD_TYPE=None \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/rnnoise-cache" cmake --install build \ + && ./autogen.sh \ + && ./configure --enable-static --disable-shared \ + && make -j$(nproc) \ + && make DESTDIR=/usr/src/rnnoise-cache install \ && cd .. \ && rm -rf rnnoise FROM builder AS xcb-proto -RUN git clone -b xcb-proto-1.16.0 --depth=1 {{ GIT_FREEDESKTOP }}/xcbproto.git \ +RUN git clone -b xcb-proto-1.16.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/xcbproto.git \ && cd xcbproto \ && ./autogen.sh \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-proto-cache" install \ + && make DESTDIR=/usr/src/xcb-proto-cache install \ && cd .. \ && rm -rf xcbproto FROM builder AS xcb -COPY --link --from=xcb-proto {{ LibrariesPath }}/xcb-proto-cache / +COPY --link --from=xcb-proto /usr/src/xcb-proto-cache / -RUN git clone -b libxcb-1.16 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb.git \ +RUN git clone -b libxcb-1.16 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcb.git \ && cd libxcb \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-cache" install \ + && export DESTDIR=/usr/src/xcb-cache \ + && make install \ + && rm $DESTDIR/usr/local/lib/{libxcb.{,l}a,pkgconfig/xcb.pc} \ && cd .. \ && rm -rf libxcb FROM builder AS xcb-wm -RUN git clone -b xcb-util-wm-0.4.2 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-wm.git \ +RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-wm.git \ && cd libxcb-wm \ - && {{ GIT_UPDATE_M4 }} \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-wm-cache" install \ + && make DESTDIR=/usr/src/xcb-wm-cache install \ && cd .. \ && rm -rf libxcb-wm FROM builder AS xcb-util -RUN git clone -b xcb-util-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-util.git \ +RUN git clone -b xcb-util-0.4.1-gitlab --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-util.git \ && cd libxcb-util \ - && {{ GIT_UPDATE_M4 }} \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-util-cache" install \ + && make DESTDIR=/usr/src/xcb-util-cache install \ && cd .. \ && rm -rf libxcb-util FROM builder AS xcb-image -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / -RUN git clone -b xcb-util-image-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-image.git \ +RUN git clone -b xcb-util-image-0.4.1-gitlab --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \ && cd libxcb-image \ - && {{ GIT_UPDATE_M4 }} \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-image-cache" install \ + && make DESTDIR=/usr/src/xcb-image-cache install \ && cd .. \ && rm -rf libxcb-image FROM builder AS xcb-keysyms -RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \ +RUN git init libxcb-keysyms \ && cd libxcb-keysyms \ - && {{ GIT_UPDATE_M4 }} \ - && ./autogen.sh --enable-static \ + && git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ + && git fetch --depth=1 origin ef5cb393d27511ba511c68a54f8ff7b9aab4a384 \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init --recursive --depth=1 \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-keysyms-cache" install \ + && make DESTDIR=/usr/src/xcb-keysyms-cache install \ && cd .. \ && rm -rf libxcb-keysyms FROM builder AS xcb-render-util -RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \ +RUN git init libxcb-render-util \ && cd libxcb-render-util \ - && {{ GIT_UPDATE_M4 }} \ - && ./autogen.sh --enable-static \ + && git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-render-util.git \ + && git fetch --depth=1 origin 5ad9853d6ddcac394d42dd2d4e34436b5db9da39 \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init --recursive --depth=1 \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-render-util-cache" install \ + && make DESTDIR=/usr/src/xcb-render-util-cache install \ && cd .. \ && rm -rf libxcb-render-util FROM builder AS xcb-cursor -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / -COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / +COPY --link --from=xcb-image /usr/src/xcb-image-cache / +COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / -RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \ +RUN git init libxcb-cursor \ && cd libxcb-cursor \ - && {{ GIT_UPDATE_M4 }} \ - && ./autogen.sh --enable-static \ + && git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \ + && git fetch --depth=1 origin 4929f6051658ba5424b41703a1fb63f9db896065 \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init --recursive --depth=1 \ + && ./autogen.sh --enable-static --disable-shared --with-cursorpath='~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps' \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-cursor-cache" install \ + && make DESTDIR=/usr/src/xcb-cursor-cache install \ && cd .. \ && rm -rf libxcb-cursor -FROM builder AS libXext -RUN git clone -b libXext-1.3.5 --depth=1 {{ GIT_FREEDESKTOP }}/libxext.git \ +FROM builder AS xext +RUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxext.git \ && cd libxext \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXext-cache" install \ + && make DESTDIR=/usr/src/xext-cache install \ && cd .. \ && rm -rf libxext -FROM builder AS libXtst -RUN git clone -b libXtst-1.2.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxtst.git \ +FROM builder AS xtst +RUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxtst.git \ && cd libxtst \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXtst-cache" install \ + && make DESTDIR=/usr/src/xtst-cache install \ && cd .. \ && rm -rf libxtst -FROM builder AS libXfixes -RUN git clone -b libXfixes-5.0.3 --depth=1 {{ GIT_FREEDESKTOP }}/libxfixes.git \ +FROM builder AS xfixes +RUN git clone -b libXfixes-5.0.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxfixes.git \ && cd libxfixes \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXfixes-cache" install \ + && make DESTDIR=/usr/src/xfixes-cache install \ && cd .. \ && rm -rf libxfixes -FROM builder AS libXv -COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / +FROM builder AS xv +COPY --link --from=xext /usr/src/xext-cache / -RUN git clone -b libXv-1.0.12 --depth=1 {{ GIT_FREEDESKTOP }}/libxv.git \ +RUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxv.git \ && cd libxv \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXv-cache" install \ + && make DESTDIR=/usr/src/xv-cache install \ && cd .. \ && rm -rf libxv -FROM builder AS libXrandr -RUN git clone -b libXrandr-1.5.3 --depth=1 {{ GIT_FREEDESKTOP }}/libxrandr.git \ +FROM builder AS xrandr +RUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrandr.git \ && cd libxrandr \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXrandr-cache" install \ + && make DESTDIR=/usr/src/xrandr-cache install \ && cd .. \ && rm -rf libxrandr -FROM builder AS libXrender -RUN git clone -b libXrender-0.9.11 --depth=1 {{ GIT_FREEDESKTOP }}/libxrender.git \ +FROM builder AS xrender +RUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrender.git \ && cd libxrender \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXrender-cache" install \ + && make DESTDIR=/usr/src/xrender-cache install \ && cd .. \ && rm -rf libxrender -FROM builder AS libXdamage -RUN git clone -b libXdamage-1.1.6 --depth=1 {{ GIT_FREEDESKTOP }}/libxdamage.git \ +FROM builder AS xdamage +RUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxdamage.git \ && cd libxdamage \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXdamage-cache" install \ + && make DESTDIR=/usr/src/xdamage-cache install \ && cd .. \ && rm -rf libxdamage -FROM builder AS libXcomposite -RUN git clone -b libXcomposite-0.4.6 --depth=1 {{ GIT_FREEDESKTOP }}/libxcomposite.git \ +FROM builder AS xcomposite +RUN git clone -b libXcomposite-0.4.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcomposite.git \ && cd libxcomposite \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXcomposite-cache" install \ + && make DESTDIR=/usr/src/xcomposite-cache install \ && cd .. \ && rm -rf libxcomposite FROM builder AS wayland -RUN git clone -b 1.19.0 --depth=1 {{ GIT_FREEDESKTOP }}/wayland.git \ +RUN git clone -b 1.19.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/wayland.git \ && cd wayland \ && sed -i "/subdir('tests')/d" meson.build \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ -Ddocumentation=false \ -Ddtd_validation=false \ -Dicon_directory=/usr/share/icons \ && meson compile -C build src/wayland-scanner \ - && mkdir -p "{{ LibrariesPath }}/wayland-cache/usr/local/bin" "{{ LibrariesPath }}/wayland-cache/usr/local/lib64/pkgconfig" \ - && cp build/src/wayland-scanner "{{ LibrariesPath }}/wayland-cache/usr/local/bin" \ - && sed 's@bindir=${prefix}/bin@bindir=${prefix}/local/bin@;s/1.21.0/1.19.0/' /usr/lib64/pkgconfig/wayland-scanner.pc > "{{ LibrariesPath }}/wayland-cache/usr/local/lib64/pkgconfig/wayland-scanner.pc" \ + && mkdir -p "/usr/src/wayland-cache/usr/local/bin" "/usr/src/wayland-cache/usr/local/lib64/pkgconfig" \ + && cp build/src/wayland-scanner "/usr/src/wayland-cache/usr/local/bin" \ + && sed 's@bindir=${prefix}/bin@bindir=${prefix}/local/bin@;s/1.21.0/1.19.0/' /usr/lib64/pkgconfig/wayland-scanner.pc > "/usr/src/wayland-cache/usr/local/lib64/pkgconfig/wayland-scanner.pc" \ && cd .. \ && rm -rf wayland FROM builder AS nv-codec-headers -RUN git clone -b n12.1.14.0 --depth=1 {{ GIT }}/FFmpeg/nv-codec-headers.git \ - && DESTDIR="{{ LibrariesPath }}/nv-codec-headers-cache" make -C nv-codec-headers install \ +RUN git clone -b n12.1.14.0 --depth=1 https://github.com/FFmpeg/nv-codec-headers.git \ + && DESTDIR=/usr/src/nv-codec-headers-cache make -C nv-codec-headers install \ && rm -rf nv-codec-headers FROM builder AS ffmpeg -COPY --link --from=opus {{ LibrariesPath }}/opus-cache / -COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / -COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / -COPY --link --from=libXv {{ LibrariesPath }}/libXv-cache / -COPY --link --from=nv-codec-headers {{ LibrariesPath }}/nv-codec-headers-cache / +COPY --link --from=opus /usr/src/opus-cache / +COPY --link --from=openh264 /usr/src/openh264-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / +COPY --link --from=vpx /usr/src/vpx-cache / +COPY --link --from=xext /usr/src/xext-cache / +COPY --link --from=xv /usr/src/xv-cache / +COPY --link --from=nv-codec-headers /usr/src/nv-codec-headers-cache / -RUN git clone -b n6.1.1 --depth=1 {{ GIT }}/FFmpeg/FFmpeg.git \ +RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ && cd FFmpeg \ && ./configure \ --extra-cflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ --extra-cxxflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ - --pkg-config-flags=--static \ + --extra-ldflags="-lstdc++" \ --disable-debug \ --disable-programs \ --disable-doc \ @@ -613,12 +667,12 @@ RUN git clone -b n6.1.1 --depth=1 {{ GIT }}/FFmpeg/FFmpeg.git \ --enable-muxer=opus \ --enable-muxer=wav \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/ffmpeg-cache" install \ + && make DESTDIR=/usr/src/ffmpeg-cache install \ && cd .. \ && rm -rf ffmpeg FROM builder AS pipewire -RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ +RUN git clone -b 0.3.62 --depth=1 https://github.com/PipeWire/pipewire.git \ && cd pipewire \ && meson build \ --buildtype=plain \ @@ -627,46 +681,46 @@ RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ -Dsession-managers=media-session \ -Dspa-plugins=disabled \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/pipewire-cache" meson install -C build \ + && DESTDIR=/usr/src/pipewire-cache meson install -C build \ && cd .. \ && rm -rf pipewire FROM builder AS openal -COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / +COPY --link --from=pipewire /usr/src/pipewire-cache / -RUN git clone -b 1.24.1 --depth=1 {{ GIT }}/kcat/openal-soft.git \ +RUN git clone -b 1.24.3 --depth=1 https://github.com/kcat/openal-soft.git \ && cd openal-soft \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DLIBTYPE:STRING=STATIC \ -DALSOFT_EXAMPLES=OFF \ -DALSOFT_UTILS=OFF \ -DALSOFT_INSTALL_CONFIG=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/openal-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/openal-cache cmake --install build \ && cd .. \ && rm -rf openal-soft FROM builder AS openssl -RUN git clone -b openssl-3.2.1 --depth=1 {{ GIT }}/openssl/openssl.git \ +RUN git clone -b openssl-3.2.1 --depth=1 https://github.com/openssl/openssl.git \ && cd openssl \ && ./config \ --openssldir=/etc/ssl \ + no-shared \ no-tests \ no-dso \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/openssl-cache" install_sw \ + && make DESTDIR=/usr/src/openssl-cache install_sw \ && cd .. \ && rm -rf openssl FROM builder AS xkbcommon -COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / +COPY --link --from=xcb /usr/src/xcb-cache / -RUN git clone -b xkbcommon-1.6.0 --depth=1 {{ GIT }}/xkbcommon/libxkbcommon.git \ +RUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbcommon.git \ && cd libxkbcommon \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ -Denable-docs=false \ -Denable-wayland=false \ -Denable-xkbregistry=false \ @@ -674,71 +728,50 @@ RUN git clone -b xkbcommon-1.6.0 --depth=1 {{ GIT }}/xkbcommon/libxkbcommon.git -Dxkb-config-extra-path=/etc/xkb \ -Dx-locale-root=/usr/share/X11/locale \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/xkbcommon-cache" meson install -C build \ + && DESTDIR=/usr/src/xkbcommon-cache meson install -C build \ && cd .. \ && rm -rf libxkbcommon -FROM builder AS glib -RUN git clone -b 2.78.1 --depth=1 {{ GIT }}/GNOME/glib.git \ - && cd glib \ - && meson build \ - --buildtype=plain \ - --default-library=both \ - -Dtests=false \ - -Dmm-common:use-network=true \ - && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/glib-cache" meson install -C build \ - && cd .. \ - && rm -rf glib - -FROM builder AS gobject-introspection -COPY --link --from=glib {{ LibrariesPath }}/glib-cache / - -RUN git clone -b 1.78.1 --depth=1 {{ GIT }}/GNOME/gobject-introspection.git \ - && cd gobject-introspection \ - && meson build --buildtype=plain \ - && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/gobject-introspection-cache" meson install -C build \ - && cd .. \ - && rm -rf gobject-introspection - FROM patches AS qt -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=libwebp {{ LibrariesPath }}/libwebp-cache / -COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / -COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / -COPY --link --from=xcb-wm {{ LibrariesPath }}/xcb-wm-cache / -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / -COPY --link --from=xcb-keysyms {{ LibrariesPath }}/xcb-keysyms-cache / -COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -COPY --link --from=xcb-cursor {{ LibrariesPath }}/xcb-cursor-cache / -COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=lcms2 /usr/src/lcms2-cache / +COPY --link --from=webp /usr/src/webp-cache / +COPY --link --from=jxl /usr/src/jxl-cache / +COPY --link --from=xcb /usr/src/xcb-cache / +COPY --link --from=xcb-wm /usr/src/xcb-wm-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / +COPY --link --from=xcb-image /usr/src/xcb-image-cache / +COPY --link --from=xcb-keysyms /usr/src/xcb-keysyms-cache / +COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / +COPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache / +COPY --link --from=wayland /usr/src/wayland-cache / +COPY --link --from=openssl /usr/src/openssl-cache / +COPY --link --from=xkbcommon /usr/src/xkbcommon-cache / -RUN git clone -b {{ QT_TAG }} --depth=1 {{ GIT }}/qt/qt5.git \ +ENV QT=6.9.1 +RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ && cd qt5 \ && git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \ && cd qtbase \ - && find ../../patches/qtbase_{{ QT }} -type f -print0 | sort -z | xargs -r0 git apply \ + && find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -r0 git apply \ && cd ../qtwayland \ - && find ../../patches/qtwayland_{{ QT }} -type f -print0 | sort -z | xargs -r0 git apply \ + && find ../../patches/qtwayland_$QT -type f -print0 | sort -z | xargs -r0 git apply \ && cd .. \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ -DBUILD_SHARED_LIBS=OFF \ -DQT_GENERATE_SBOM=OFF \ + -DQT_QPA_PLATFORMS="wayland;xcb" \ -DINPUT_libpng=qt \ -DINPUT_harfbuzz=qt \ -DINPUT_pcre=qt \ -DFEATURE_icu=OFF \ -DFEATURE_xcb_sm=OFF \ + -DFEATURE_eglfs=OFF \ -DINPUT_dbus=runtime \ -DINPUT_openssl=linked \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/qt-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/qt-cache cmake --install build \ && cd .. \ && rm -rf qt5 @@ -748,138 +781,118 @@ RUN git clone -b v2024.02.16 --depth=1 https://chromium.googlesource.com/breakpa && git clone -b v2024.02.01 --depth=1 https://chromium.googlesource.com/linux-syscall-support.git src/third_party/lss \ && CFLAGS="$CFLAGS -fno-lto" CXXFLAGS="$CXXFLAGS -fno-lto" ./configure \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/breakpad-cache" install \ + && make DESTDIR=/usr/src/breakpad-cache install \ && cd .. \ && rm -rf breakpad FROM builder AS webrtc -COPY --link --from=opus {{ LibrariesPath }}/opus-cache / -COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / -COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / -COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=libXtst {{ LibrariesPath }}/libXtst-cache / -COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=opus /usr/src/opus-cache / +COPY --link --from=openh264 /usr/src/openh264-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / +COPY --link --from=vpx /usr/src/vpx-cache / +COPY --link --from=jxl /usr/src/jxl-cache / +COPY --link --from=ffmpeg /usr/src/ffmpeg-cache / +COPY --link --from=openssl /usr/src/openssl-cache / +COPY --link --from=xext /usr/src/xext-cache / +COPY --link --from=xfixes /usr/src/xfixes-cache / +COPY --link --from=xtst /usr/src/xtst-cache / +COPY --link --from=xrandr /usr/src/xrandr-cache / +COPY --link --from=xrender /usr/src/xrender-cache / +COPY --link --from=xdamage /usr/src/xdamage-cache / +COPY --link --from=xcomposite /usr/src/xcomposite-cache / +COPY --link --from=pipewire /usr/src/pipewire-cache / # Shallow clone on a specific commit. RUN git init tg_owt \ && cd tg_owt \ - && git remote add origin {{ GIT }}/desktop-app/tg_owt.git \ - && git fetch --depth=1 origin c4192e8e2e10ccb72704daa79fa108becfa57b01 \ + && git remote add origin https://github.com/desktop-app/tg_owt.git \ + && git fetch --depth=1 origin 62321fd7128ab2650b459d4195781af8185e46b5 \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ - && rm -rf .git \ - && env -u CFLAGS -u CXXFLAGS cmake -G"Ninja Multi-Config" -B out . \ - -DCMAKE_C_FLAGS_RELEASE="$CFLAGS" \ - -DCMAKE_C_FLAGS_DEBUG="{{ CFLAGS_DEBUG }}" \ - -DCMAKE_CXX_FLAGS_RELEASE="$CXXFLAGS" \ - -DCMAKE_CXX_FLAGS_DEBUG="{{ CFLAGS_DEBUG }}" \ - -DTG_OWT_SPECIAL_TARGET=linux \ - -DTG_OWT_LIBJPEG_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_OPENSSL_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_OPUS_INCLUDE_PATH=/usr/local/include/opus \ - -DTG_OWT_LIBVPX_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_OPENH264_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_FFMPEG_INCLUDE_PATH=/usr/local/include - -WORKDIR tg_owt - -FROM webrtc AS webrtc_release -RUN cmake --build out --config Release --parallel \ - && find out -mindepth 1 -maxdepth 1 ! -name Release -exec rm -rf {} \; - -{%- if DEBUG %} - -FROM webrtc AS webrtc_debug -RUN cmake --build out --config Debug --parallel \ - && find out -mindepth 1 -maxdepth 1 ! -name Debug -exec rm -rf {} \; -{%- endif %} + && cmake -B build . -DTG_OWT_DLOPEN_PIPEWIRE=ON \ + && cmake --build build \ + && DESTDIR=/usr/src/webrtc-cache cmake --install build \ + && cd .. \ + && rm -rf tg_owt FROM builder AS ada -RUN git clone -b v3.2.2 --depth=1 {{ GIT }}/ada-url/ada.git \ +RUN git clone -b v3.2.4 --depth=1 https://github.com/ada-url/ada.git \ && cd ada \ - && cmake -GNinja -B build . \ - -D CMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -D ADA_TESTING=OFF \ -D ADA_TOOLS=OFF \ -D ADA_INCLUDE_URL_PATTERN=OFF \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/ada-cache" cmake --install build \ + && cmake --build build \ + && DESTDIR=/usr/src/ada-cache cmake --install build \ && cd .. \ && rm -rf ada FROM builder AS tde2e -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=openssl /usr/src/openssl-cache / # Shallow clone on a specific commit. RUN git init tde2e \ && cd tde2e \ - && git remote add origin {{ GIT }}/tdlib/td.git \ + && git remote add origin https://github.com/tdlib/td.git \ && git fetch --depth=1 origin 51743dfd01dff6179e2d8f7095729caa4e2222e9 \ && git reset --hard FETCH_HEAD \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=NONE \ - -DTD_E2E_ONLY=ON \ - && cmake --build build --parallel \ - && DESTDIR="{{ LibrariesPath }}/tde2e-cache" cmake --install build \ + && cmake -B build . -DTD_E2E_ONLY=ON \ + && cmake --build build \ + && DESTDIR=/usr/src/tde2e-cache cmake --install build \ && cd .. \ && rm -rf tde2e FROM builder -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=xz {{ LibrariesPath }}/xz-cache / -COPY --link --from=protobuf {{ LibrariesPath }}/protobuf-cache / -COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / -COPY --link --from=highway {{ LibrariesPath }}/highway-cache / -COPY --link --from=opus {{ LibrariesPath }}/opus-cache / -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / -COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / -COPY --link --from=libwebp {{ LibrariesPath }}/libwebp-cache / -COPY --link --from=libavif {{ LibrariesPath }}/libavif-cache / -COPY --link --from=libheif {{ LibrariesPath }}/libheif-cache / -COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / -COPY --link --from=rnnoise {{ LibrariesPath }}/rnnoise-cache / -COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / -COPY --link --from=xcb-wm {{ LibrariesPath }}/xcb-wm-cache / -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / -COPY --link --from=xcb-keysyms {{ LibrariesPath }}/xcb-keysyms-cache / -COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -COPY --link --from=xcb-cursor {{ LibrariesPath }}/xcb-cursor-cache / -COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / -COPY --link --from=libXfixes {{ LibrariesPath }}/libXfixes-cache / -COPY --link --from=libXv {{ LibrariesPath }}/libXv-cache / -COPY --link --from=libXtst {{ LibrariesPath }}/libXtst-cache / -COPY --link --from=libXrandr {{ LibrariesPath }}/libXrandr-cache / -COPY --link --from=libXrender {{ LibrariesPath }}/libXrender-cache / -COPY --link --from=libXdamage {{ LibrariesPath }}/libXdamage-cache / -COPY --link --from=libXcomposite {{ LibrariesPath }}/libXcomposite-cache / -COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / -COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / -COPY --link --from=openal {{ LibrariesPath }}/openal-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -COPY --link --from=glib {{ LibrariesPath }}/glib-cache / -COPY --link --from=gobject-introspection {{ LibrariesPath }}/gobject-introspection-cache / -COPY --link --from=qt {{ LibrariesPath }}/qt-cache / -COPY --link --from=breakpad {{ LibrariesPath }}/breakpad-cache / -COPY --link --from=webrtc {{ LibrariesPath }}/tg_owt tg_owt -COPY --link --from=webrtc_release {{ LibrariesPath }}/tg_owt/out/Release tg_owt/out/Release -{%- if DEBUG %} -COPY --link --from=webrtc_debug {{ LibrariesPath }}/tg_owt/out/Debug tg_owt/out/Debug -{%- endif %} -COPY --link --from=ada {{ LibrariesPath }}/ada-cache / -COPY --link --from=tde2e {{ LibrariesPath }}/tde2e-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=xz /usr/src/xz-cache / +COPY --link --from=protobuf /usr/src/protobuf-cache / +COPY --link --from=lcms2 /usr/src/lcms2-cache / +COPY --link --from=brotli /usr/src/brotli-cache / +COPY --link --from=highway /usr/src/highway-cache / +COPY --link --from=opus /usr/src/opus-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / +COPY --link --from=openh264 /usr/src/openh264-cache / +COPY --link --from=de265 /usr/src/de265-cache / +COPY --link --from=vpx /usr/src/vpx-cache / +COPY --link --from=webp /usr/src/webp-cache / +COPY --link --from=avif /usr/src/avif-cache / +COPY --link --from=heif /usr/src/heif-cache / +COPY --link --from=jxl /usr/src/jxl-cache / +COPY --link --from=rnnoise /usr/src/rnnoise-cache / +COPY --link --from=xcb /usr/src/xcb-cache / +COPY --link --from=xcb-wm /usr/src/xcb-wm-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / +COPY --link --from=xcb-image /usr/src/xcb-image-cache / +COPY --link --from=xcb-keysyms /usr/src/xcb-keysyms-cache / +COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / +COPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache / +COPY --link --from=xext /usr/src/xext-cache / +COPY --link --from=xfixes /usr/src/xfixes-cache / +COPY --link --from=xv /usr/src/xv-cache / +COPY --link --from=xtst /usr/src/xtst-cache / +COPY --link --from=xrandr /usr/src/xrandr-cache / +COPY --link --from=xrender /usr/src/xrender-cache / +COPY --link --from=xdamage /usr/src/xdamage-cache / +COPY --link --from=xcomposite /usr/src/xcomposite-cache / +COPY --link --from=wayland /usr/src/wayland-cache / +COPY --link --from=ffmpeg /usr/src/ffmpeg-cache / +COPY --link --from=openal /usr/src/openal-cache / +COPY --link --from=openssl /usr/src/openssl-cache / +COPY --link --from=xkbcommon /usr/src/xkbcommon-cache / +COPY --link --from=qt /usr/src/qt-cache / +COPY --link --from=breakpad /usr/src/breakpad-cache / +COPY --link --from=webrtc /usr/src/webrtc-cache / +COPY --link --from=ada /usr/src/ada-cache / +COPY --link --from=tde2e /usr/src/tde2e-cache / -WORKDIR ../tdesktop -ENV QT {{ QT }} -ENV BOOST_INCLUDEDIR /usr/include/boost1.78 -ENV BOOST_LIBRARYDIR /usr/lib64/boost1.78 +COPY --link --from=patches /usr/src/patches patches +RUN patch -p1 -d /usr/lib64/gobject-introspection -i $PWD/patches/gobject-introspection.patch && rm -rf patches + +WORKDIR /usr/src/tdesktop +ENV BOOST_INCLUDEDIR=/usr/include/boost1.78 +ENV BOOST_LIBRARYDIR=/usr/lib64/boost1.78 USER user VOLUME [ "/usr/src/tdesktop" ] diff --git a/Telegram/build/docker/centos_env/build.sh b/Telegram/build/docker/centos_env/build.sh index e7a34e6aef..dfa475955f 100755 --- a/Telegram/build/docker/centos_env/build.sh +++ b/Telegram/build/docker/centos_env/build.sh @@ -3,4 +3,4 @@ set -e cd Telegram ./configure.sh "$@" -cmake --build ../out --config "${CONFIG:-Release}" --parallel +cmake --build ../out --config "${CONFIG:-Release}" diff --git a/Telegram/build/docker/centos_env/gen_dockerfile.py b/Telegram/build/docker/centos_env/gen_dockerfile.py index 997b784caf..96269eba6e 100755 --- a/Telegram/build/docker/centos_env/gen_dockerfile.py +++ b/Telegram/build/docker/centos_env/gen_dockerfile.py @@ -4,12 +4,15 @@ from os.path import dirname from jinja2 import Environment, FileSystemLoader def checkEnv(envName, defaultValue): - return bool(len(environ[envName])) if envName in environ else defaultValue + if isinstance(defaultValue, bool): + return bool(len(environ[envName])) if envName in environ else defaultValue + return environ[envName] if envName in environ else defaultValue def main(): print(Environment(loader=FileSystemLoader(dirname(__file__))).get_template("Dockerfile").render( DEBUG=checkEnv("DEBUG", True), LTO=checkEnv("LTO", True), + JOBS=checkEnv("JOBS", ""), )) if __name__ == '__main__': diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 42ab88f734..26ec22b16d 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -456,7 +456,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 7119a74e3f + git checkout 1ffcb17817a2cab167061d530703842395291e69 """) stage('msys64', """ @@ -749,7 +749,7 @@ win: # Somehow in x86 Debug build dav1d crashes on AV1 10bpc videos. stage('dav1d', """ - git clone -b 1.4.1 https://code.videolan.org/videolan/dav1d.git + git clone -b 1.5.1 https://code.videolan.org/videolan/dav1d.git cd dav1d win32: SET "TARGET=x86" @@ -817,7 +817,7 @@ mac: """) stage('openh264', """ - git clone -b v2.4.1 https://github.com/cisco/openh264.git + git clone -b v2.6.0 https://github.com/cisco/openh264.git cd openh264 win32: SET "TARGET=x86" @@ -878,7 +878,7 @@ mac: """) stage('libavif', """ - git clone -b v1.0.4 https://github.com/AOMediaCodec/libavif.git + git clone -b v1.3.0 https://github.com/AOMediaCodec/libavif.git cd libavif win: cmake . ^ @@ -888,7 +888,8 @@ win: -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^ -DBUILD_SHARED_LIBS=OFF ^ -DAVIF_ENABLE_WERROR=OFF ^ - -DAVIF_CODEC_DAV1D=ON + -DAVIF_CODEC_DAV1D=SYSTEM ^ + -DAVIF_LIBYUV=OFF cmake --build . --config Debug --parallel cmake --install . --config Debug release: @@ -901,16 +902,15 @@ mac: -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ -D BUILD_SHARED_LIBS=OFF \\ -D AVIF_ENABLE_WERROR=OFF \\ - -D AVIF_CODEC_DAV1D=ON \\ - -D CMAKE_DISABLE_FIND_PACKAGE_libsharpyuv=ON + -D AVIF_CODEC_DAV1D=SYSTEM \\ + -D AVIF_LIBYUV=OFF cmake --build . --config MinSizeRel $MAKE_THREADS_CNT cmake --install . --config MinSizeRel """) stage('libde265', """ - git clone -b v1.0.15 https://github.com/strukturag/libde265.git + git clone -b v1.0.16 https://github.com/strukturag/libde265.git cd libde265 - git cherry-pick 5c5af1e win: cmake . ^ -A %WIN32X64% ^ @@ -943,7 +943,7 @@ mac: """) stage('libwebp', """ - git clone -b v1.4.0 https://github.com/webmproject/libwebp.git + git clone -b v1.5.0 https://github.com/webmproject/libwebp.git cd libwebp win: nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all @@ -983,11 +983,13 @@ mac: """) stage('libheif', """ - git clone -b v1.18.2 https://github.com/strukturag/libheif.git + git clone -b v1.19.8 https://github.com/strukturag/libheif.git cd libheif win: %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' libheif/CMakeLists.txt %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' libheif/CMakeLists.txt + %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' heifio/CMakeLists.txt + %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' heifio/CMakeLists.txt cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -996,10 +998,15 @@ win: -DBUILD_TESTING=OFF ^ -DENABLE_PLUGIN_LOADING=OFF ^ -DWITH_LIBDE265=ON ^ + -DWITH_OpenH264_DECODER=OFF ^ -DWITH_SvtEnc=OFF ^ -DWITH_SvtEnc_PLUGIN=OFF ^ -DWITH_RAV1E=OFF ^ -DWITH_RAV1E_PLUGIN=OFF ^ + -DWITH_LIBSHARPYUV=OFF ^ + -DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE ^ + -DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE ^ + -DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE ^ -DWITH_EXAMPLES=OFF cmake --build . --config Debug --parallel cmake --install . --config Debug @@ -1017,14 +1024,17 @@ mac: -D WITH_AOM_ENCODER=OFF \\ -D WITH_AOM_DECODER=OFF \\ -D WITH_X265=OFF \\ + -D WITH_OpenH264_DECODER=OFF \\ -D WITH_SvtEnc=OFF \\ -D WITH_RAV1E=OFF \\ -D WITH_DAV1D=ON \\ -D WITH_LIBDE265=ON \\ -D LIBDE265_INCLUDE_DIR=$USED_PREFIX/include/ \\ -D LIBDE265_LIBRARY=$USED_PREFIX/lib/libde265.a \\ - -D LIBSHARPYUV_INCLUDE_DIR=$USED_PREFIX/include/webp/ \\ - -D LIBSHARPYUV_LIBRARY=$USED_PREFIX/lib/libsharpyuv.a \\ + -D WITH_LIBSHARPYUV=OFF \\ + -D CMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE \\ + -D CMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE \\ + -D CMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE \\ -D WITH_EXAMPLES=OFF cmake --build . --config MinSizeRel $MAKE_THREADS_CNT cmake --install . --config MinSizeRel @@ -1557,7 +1567,6 @@ release: depends:patches/qtbase_""" + qt + """/*.patch cd qtbase win: - git revert --no-edit 6ad56dce34 setlocal enabledelayedexpansion for /r %%i in (..\\..\\patches\\qtbase_%QT%\\*) do ( git apply %%i -v @@ -1746,7 +1755,7 @@ win: stage('tg_owt', """ git clone https://github.com/desktop-app/tg_owt.git cd tg_owt - git checkout c4192e8 + git checkout 62321fd git submodule update --init --recursive win: SET MOZJPEG_PATH=$LIBS_DIR/mozjpeg @@ -1865,7 +1874,7 @@ release: """) stage('ada', """ - git clone -b v3.2.2 https://github.com/ada-url/ada.git + git clone -b v3.2.4 https://github.com/ada-url/ada.git cd ada win: cmake -B out . ^ diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py index df7bb2a92c..e551364ce3 100644 --- a/Telegram/build/qt_version.py +++ b/Telegram/build/qt_version.py @@ -5,10 +5,9 @@ def resolve(arch): os.environ['QT'] = '6.2.12' elif sys.platform == 'win32': if arch == 'arm' or 'qt6' in sys.argv: - os.environ['QT'] = '6.9.0' - elif os.environ.get('QT') is None: - os.environ['QT'] = '5.15.15' - elif os.environ.get('QT') is None: - return False - print('Choosing Qt ' + os.environ.get('QT')) + print('Choosing Qt 6.') + os.environ['QT'] = '6.9.1' + else: + print('Choosing Qt 5.') + os.environ['QT'] = '5.15.17' return True diff --git a/Telegram/build/version b/Telegram/build/version index af8ef69b2c..ecab5bf25c 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5014003 -AppVersionStrMajor 5.14 -AppVersionStrSmall 5.14.3 -AppVersionStr 5.14.3 +AppVersion 5016002 +AppVersionStrMajor 5.16 +AppVersionStrSmall 5.16.2 +AppVersionStr 5.16.2 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.14.3 +AppVersionOriginal 5.16.2 diff --git a/Telegram/cmake/lib_tgcalls.cmake b/Telegram/cmake/lib_tgcalls.cmake index 9f59550bff..154169e3ca 100644 --- a/Telegram/cmake/lib_tgcalls.cmake +++ b/Telegram/cmake/lib_tgcalls.cmake @@ -280,26 +280,3 @@ PUBLIC PRIVATE ${tgcalls_loc} ) - -# add_library(lib_tgcalls_legacy STATIC) -# init_target(lib_tgcalls_legacy) - -# add_library(tdesktop::lib_tgcalls_legacy ALIAS lib_tgcalls_legacy) - -# nice_target_sources(lib_tgcalls_legacy ${tgcalls_loc} -# PRIVATE -# legacy/InstanceImplLegacy.cpp -# legacy/InstanceImplLegacy.h -# ) - -# target_include_directories(lib_tgcalls_legacy -# PRIVATE -# ${tgcalls_loc} -# ) - -# target_link_libraries(lib_tgcalls_legacy -# PRIVATE -# tdesktop::lib_tgcalls -# tdesktop::lib_tgvoip -# desktop-app::external_openssl -# ) diff --git a/Telegram/cmake/lib_tgvoip.cmake b/Telegram/cmake/lib_tgvoip.cmake deleted file mode 100644 index f6d80daca7..0000000000 --- a/Telegram/cmake/lib_tgvoip.cmake +++ /dev/null @@ -1,220 +0,0 @@ -# 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 - -add_library(lib_tgvoip INTERFACE IMPORTED GLOBAL) -add_library(tdesktop::lib_tgvoip ALIAS lib_tgvoip) - -if (DESKTOP_APP_USE_PACKAGED) - find_package(PkgConfig) - if (PkgConfig_FOUND) - pkg_check_modules(TDESKTOP_TGVOIP IMPORTED_TARGET tgvoip) - endif() - - if (TDESKTOP_TGVOIP_FOUND) - target_link_libraries(lib_tgvoip INTERFACE PkgConfig::TDESKTOP_TGVOIP) - return() - endif() -endif() - -include(CMakeDependentOption) - -add_library(lib_tgvoip_bundled STATIC) -init_target(lib_tgvoip_bundled) - -cmake_dependent_option(LIBTGVOIP_DISABLE_ALSA "Disable libtgvoip's ALSA backend." OFF LINUX ON) -cmake_dependent_option(LIBTGVOIP_DISABLE_PULSEAUDIO "Disable libtgvoip's PulseAudio backend." OFF LINUX ON) - -set(tgvoip_loc ${third_party_loc}/libtgvoip) - -nice_target_sources(lib_tgvoip_bundled ${tgvoip_loc} -PRIVATE - BlockingQueue.cpp - BlockingQueue.h - Buffers.cpp - Buffers.h - CongestionControl.cpp - CongestionControl.h - EchoCanceller.cpp - EchoCanceller.h - JitterBuffer.cpp - JitterBuffer.h - logging.cpp - logging.h - MediaStreamItf.cpp - MediaStreamItf.h - OpusDecoder.cpp - OpusDecoder.h - OpusEncoder.cpp - OpusEncoder.h - threading.h - VoIPController.cpp - VoIPGroupController.cpp - VoIPController.h - PrivateDefines.h - VoIPServerConfig.cpp - VoIPServerConfig.h - audio/AudioInput.cpp - audio/AudioInput.h - audio/AudioOutput.cpp - audio/AudioOutput.h - audio/Resampler.cpp - audio/Resampler.h - NetworkSocket.cpp - NetworkSocket.h - PacketReassembler.cpp - PacketReassembler.h - MessageThread.cpp - MessageThread.h - audio/AudioIO.cpp - audio/AudioIO.h - video/ScreamCongestionController.cpp - video/ScreamCongestionController.h - video/VideoSource.cpp - video/VideoSource.h - video/VideoRenderer.cpp - video/VideoRenderer.h - json11.cpp - json11.hpp - - # Windows - os/windows/NetworkSocketWinsock.cpp - os/windows/NetworkSocketWinsock.h - os/windows/AudioInputWave.cpp - os/windows/AudioInputWave.h - os/windows/AudioOutputWave.cpp - os/windows/AudioOutputWave.h - os/windows/AudioOutputWASAPI.cpp - os/windows/AudioOutputWASAPI.h - os/windows/AudioInputWASAPI.cpp - os/windows/AudioInputWASAPI.h - os/windows/MinGWSupport.h - os/windows/WindowsSpecific.cpp - os/windows/WindowsSpecific.h - - # macOS - os/darwin/AudioInputAudioUnit.cpp - os/darwin/AudioInputAudioUnit.h - os/darwin/AudioOutputAudioUnit.cpp - os/darwin/AudioOutputAudioUnit.h - os/darwin/AudioInputAudioUnitOSX.cpp - os/darwin/AudioInputAudioUnitOSX.h - os/darwin/AudioOutputAudioUnitOSX.cpp - os/darwin/AudioOutputAudioUnitOSX.h - os/darwin/AudioUnitIO.cpp - os/darwin/AudioUnitIO.h - os/darwin/DarwinSpecific.mm - os/darwin/DarwinSpecific.h - - # Linux - os/linux/AudioInputALSA.cpp - os/linux/AudioInputALSA.h - os/linux/AudioOutputALSA.cpp - os/linux/AudioOutputALSA.h - os/linux/AudioOutputPulse.cpp - os/linux/AudioOutputPulse.h - os/linux/AudioInputPulse.cpp - os/linux/AudioInputPulse.h - os/linux/AudioPulse.cpp - os/linux/AudioPulse.h - - # POSIX - os/posix/NetworkSocketPosix.cpp - os/posix/NetworkSocketPosix.h -) - -target_compile_definitions(lib_tgvoip_bundled -PRIVATE - TGVOIP_USE_DESKTOP_DSP -) - -target_include_directories(lib_tgvoip_bundled -PUBLIC - ${tgvoip_loc} -) -target_link_libraries(lib_tgvoip_bundled -PRIVATE - desktop-app::external_webrtc - desktop-app::external_opus -) - -if (APPLE) - target_compile_definitions(lib_tgvoip_bundled - PUBLIC - TARGET_OS_OSX - TARGET_OSX - ) - if (build_macstore) - target_compile_definitions(lib_tgvoip_bundled - PUBLIC - TGVOIP_NO_OSX_PRIVATE_API - ) - endif() -elseif (LINUX) - if (NOT LIBTGVOIP_DISABLE_ALSA) - find_package(ALSA REQUIRED) - target_include_directories(lib_tgvoip_bundled SYSTEM PRIVATE ${ALSA_INCLUDE_DIRS}) - else() - remove_target_sources(lib_tgvoip_bundled ${tgvoip_loc} - os/linux/AudioInputALSA.cpp - os/linux/AudioInputALSA.h - os/linux/AudioOutputALSA.cpp - os/linux/AudioOutputALSA.h - ) - - target_compile_definitions(lib_tgvoip_bundled PRIVATE WITHOUT_ALSA) - endif() - - if (NOT LIBTGVOIP_DISABLE_PULSEAUDIO) - find_package(PkgConfig REQUIRED) - pkg_check_modules(PULSE REQUIRED libpulse) - target_include_directories(lib_tgvoip_bundled SYSTEM PRIVATE ${PULSE_INCLUDE_DIRS}) - else() - remove_target_sources(lib_tgvoip_bundled ${tgvoip_loc} - os/linux/AudioOutputPulse.cpp - os/linux/AudioOutputPulse.h - os/linux/AudioInputPulse.cpp - os/linux/AudioInputPulse.h - os/linux/AudioPulse.cpp - os/linux/AudioPulse.h - ) - - target_compile_definitions(lib_tgvoip_bundled PRIVATE WITHOUT_PULSE) - endif() -endif() - -add_library(lib_tgvoip_bundled_options INTERFACE) - -if (MSVC) - target_compile_options(lib_tgvoip_bundled_options - INTERFACE - /wd4005 # 'identifier' : macro redefinition - /wd4068 # unknown pragma - /wd4996 # deprecated - /wd5055 # operator '>' deprecated between enumerations and floating-point types - ) -else() - target_compile_options_if_exists(lib_tgvoip_bundled_options - INTERFACE - -Wno-unqualified-std-cast-call - -Wno-unused-variable - -Wno-unknown-pragmas - -Wno-error=sequence-point - -Wno-error=unused-result - ) - if (CMAKE_SIZEOF_VOID_P EQUAL 4 AND CMAKE_SYSTEM_PROCESSOR MATCHES "i686.*|i386.*|x86.*") - target_compile_options(lib_tgvoip_bundled_options INTERFACE -msse2) - endif() -endif() - -target_link_libraries(lib_tgvoip_bundled -PRIVATE - lib_tgvoip_bundled_options -) - -target_link_libraries(lib_tgvoip -INTERFACE - lib_tgvoip_bundled -) diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index d510ea3bbf..15a5f7a71a 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -61,6 +61,8 @@ PRIVATE boxes/peers/edit_peer_history_visibility_box.cpp boxes/peers/edit_peer_history_visibility_box.h + boxes/peers/toggle_topics_box.cpp + boxes/peers/toggle_topics_box.h calls/group/ui/calls_group_recording_box.cpp calls/group/ui/calls_group_recording_box.h @@ -382,17 +384,22 @@ PRIVATE ui/controls/invite_link_label.h ui/controls/peer_list_dummy.cpp ui/controls/peer_list_dummy.h + ui/controls/round_video_recorder_data.h ui/controls/round_video_recorder.cpp ui/controls/round_video_recorder.h ui/controls/send_as_button.cpp ui/controls/send_as_button.h ui/controls/send_button.cpp ui/controls/send_button.h + ui/controls/subsection_tabs_slider.cpp + ui/controls/subsection_tabs_slider.h ui/controls/swipe_handler.cpp ui/controls/swipe_handler.h ui/controls/swipe_handler_data.h ui/controls/tabbed_search.cpp ui/controls/tabbed_search.h + ui/controls/ton_common.cpp + ui/controls/ton_common.h ui/controls/who_reacted_context_action.cpp ui/controls/who_reacted_context_action.h ui/controls/window_outdated_bar.cpp diff --git a/Telegram/lib_base b/Telegram/lib_base index b4f913beb8..402034cba6 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit b4f913beb8fba75046b3b6b329658624bf7e934d +Subproject commit 402034cba675220647c5e2041f38cf9d977d496e diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 4038a11f63..4fc3ac0ea5 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 4038a11f635311073f6d55786490920b043cb319 +Subproject commit 4fc3ac0ea52f271cc9b108481f83d56fd76ab0ed diff --git a/Telegram/lib_spellcheck b/Telegram/lib_spellcheck index 73dba37d0a..c8ded8b758 160000 --- a/Telegram/lib_spellcheck +++ b/Telegram/lib_spellcheck @@ -1 +1 @@ -Subproject commit 73dba37d0a7e0def17889091498a99d1057f961d +Subproject commit c8ded8b7585f8819780ea22a40c237625aec0c75 diff --git a/Telegram/lib_webview b/Telegram/lib_webview index b9f9e981c8..04c45d069f 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit b9f9e981c81a78120a023822d2aa908d38b6795f +Subproject commit 04c45d069fc0088740b9637bc5da414ee82be198 diff --git a/changelog.txt b/changelog.txt index f9fc66744e..91d27aedda 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,51 @@ +5.16.2 (04.07.25) + +- Fix crash in some checklists. +- Fix problems with some private chats message sending. +- Fix chats list preview for rejected/balance-low suggestions. + +5.16.1 (02.07.25) + +- Fix inline keyboard updating in bot messages. +- Fix possible crash in fast chat switching. +- Different minor fixes. + +5.16 (01.07.25) + +- Create private and group checklists. +- Suggest Posts in Channels. +- Monetizing via Suggested Posts. + +5.15.4 (12.06.25) + +- Fix updating messages in Saved Messages subchats. +- Fix possible issues with mouse cursor on Linux. + +5.15.3 (09.06.25) + +- Fix new contact top bar appearance. +- Remove change photo button for channel direct messages. + +5.15.2 (05.06.25) + +- Fix sending messages in new forum layout. +- Add statistics for user stars. + +5.15.1 (05.06.25) + +- Fix launch on Windows 7. +- Fix launch on older Linux distributions. +- Fix crash in group chat message right click. +- Fix unread counters in channel direct messages. +- Don't generate "User joined" message in channel direct messages. +- Fix some other glitches in new forums and channel direct messages. + +5.15 (04.06.25) + +- Send Direct Messages to Channels. +- Enable New Tab Layout for Topics. +- Create Polls with Up To 12 Options. + 5.14.3 (18.05.25) - Fix stale birthday suggestions removing. diff --git a/cmake b/cmake index 50c3edca14..b032f270b6 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 50c3edca148cee2bbb1ce41a7c19c9d0b20c5c48 +Subproject commit b032f270b622610ca3f42a83f37b3a183c9da0da diff --git a/docs/building-mac.md b/docs/building-mac.md index b66847dad2..2eafc93b30 100644 --- a/docs/building-mac.md +++ b/docs/building-mac.md @@ -4,6 +4,10 @@ Choose a folder for the future build, for example **/Users/user/TBuild**. It will be named ***BuildPath*** in the rest of this document. All commands will be launched from Terminal. +**Note about disk space:** The full build process will require approximately **55 GB** of free space. This includes: +- **~35 GB** for libraries (when building for both x64 and arm64 architectures) +- **~20 GB** for the compiled Telegram app (in the `out` folder) + ### Clone source code and prepare libraries Go to ***BuildPath*** and run