diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml new file mode 100644 index 000000000..5147f6cbe --- /dev/null +++ b/.github/workflows/mac_packaged.yml @@ -0,0 +1,158 @@ +name: MacOS Packaged. + +on: + push: + paths-ignore: + - 'docs/**' + - '**.md' + - 'changelog.txt' + - 'LEGAL' + - 'LICENSE' + - '.github/**' + - '!.github/workflows/mac_packaged.yml' + - 'lib/xdg/**' + - 'snap/**' + - 'Telegram/build/**' + - 'Telegram/Resources/uwp/**' + - 'Telegram/Resources/winrc/**' + - 'Telegram/SourceFiles/platform/win/**' + - 'Telegram/SourceFiles/platform/linux/**' + - 'Telegram/configure.bat' + pull_request: + paths-ignore: + - 'docs/**' + - '**.md' + - 'changelog.txt' + - 'LEGAL' + - 'LICENSE' + - '.github/**' + - '!.github/workflows/mac_packaged.yml' + - 'lib/xdg/**' + - 'snap/**' + - 'Telegram/build/**' + - 'Telegram/Resources/uwp/**' + - 'Telegram/Resources/winrc/**' + - 'Telegram/SourceFiles/platform/win/**' + - 'Telegram/SourceFiles/platform/linux/**' + - 'Telegram/configure.bat' + +jobs: + + macos: + name: MacOS + runs-on: macos-latest + + strategy: + matrix: + defines: + - "" + + env: + GIT: "https://github.com" + OPENALDIR: "/usr/local/opt/openal-soft" + UPLOAD_ARTIFACT: "false" + ONLY_CACHE: "false" + MANUAL_CACHING: "1" + AUTO_CACHING: "1" + + steps: + - name: Get repository name. + run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Clone. + uses: actions/checkout@v3.1.0 + with: + submodules: recursive + path: ${{ env.REPO_NAME }} + + - name: First set up. + run: | + brew install autoconf automake boost cmake ffmpeg openal-soft openssl opus ninja pkg-config python qt yasm xz + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + + xcodebuild -version > CACHE_KEY.txt + echo $MANUAL_CACHING >> CACHE_KEY.txt + echo "$GITHUB_WORKSPACE" >> CACHE_KEY.txt + if [ "$AUTO_CACHING" = "1" ]; then + thisFile=$REPO_NAME/.github/workflows/mac_packaged.yml + echo `md5 -q $thisFile` >> CACHE_KEY.txt + fi + echo "CACHE_KEY=`md5 -q CACHE_KEY.txt`" >> $GITHUB_ENV + + echo "LibrariesPath=`pwd`" >> $GITHUB_ENV + + curl -o tg_owt-version.json https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master + + - name: RNNoise. + run: | + cd $LibrariesPath + + git clone --depth=1 https://gitlab.xiph.org/xiph/rnnoise.git + cd rnnoise + ./autogen.sh + ./configure --disable-examples --disable-doc + make -j$(sysctl -n hw.logicalcpu) + make install + + - name: WebRTC cache. + id: cache-webrtc + uses: actions/cache@v3.0.11 + with: + path: ${{ env.LibrariesPath }}/tg_owt + key: ${{ runner.OS }}-webrtc-${{ env.CACHE_KEY }}-${{ hashFiles('**/tg_owt-version.json') }} + - name: WebRTC. + if: steps.cache-webrtc.outputs.cache-hit != 'true' + run: | + cd $LibrariesPath + + git clone --recursive --depth=1 $GIT/desktop-app/tg_owt.git + cd tg_owt + + cmake -B build . -GNinja -DCMAKE_BUILD_TYPE=Debug + cmake --build build --parallel + + - name: Telegram Desktop build. + if: env.ONLY_CACHE == 'false' + env: + tg_owt_DIR: ${{ env.LibrariesPath }}/tg_owt/build + run: | + cd $REPO_NAME + + DEFINE="" + if [ -n "${{ matrix.defines }}" ]; then + DEFINE="-D ${{ matrix.defines }}=ON" + echo Define from matrix: $DEFINE + echo "ARTIFACT_NAME=Telegram_${{ matrix.defines }}" >> $GITHUB_ENV + else + echo "ARTIFACT_NAME=Telegram" >> $GITHUB_ENV + fi + + cmake -Bbuild -GNinja . \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_FIND_FRAMEWORK=LAST \ + -DTDESKTOP_API_TEST=ON \ + -DDESKTOP_APP_USE_PACKAGED_LAZY=ON \ + $DEFINE + + cmake --build build --parallel + + cd build + macdeployqt Telegram.app + codesign --remove-signature Telegram.app + + mkdir dmgsrc + mv Telegram.app dmgsrc + hdiutil create -volname Telegram -srcfolder dmgsrc -ov -format UDZO Telegram.dmg + + - name: Move artifact. + if: env.UPLOAD_ARTIFACT == 'true' + run: | + cd $REPO_NAME/build + mkdir artifact + mv Telegram.dmg artifact/ + - uses: actions/upload-artifact@master + if: env.UPLOAD_ARTIFACT == 'true' + name: Upload artifact. + with: + name: ${{ env.ARTIFACT_NAME }} + path: ${{ env.REPO_NAME }}/build/artifact/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8457b36d8..4d82efbfe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,9 +59,9 @@ include(cmake/options.cmake) if (NOT DESKTOP_APP_USE_PACKAGED) if (WIN32) - set(qt_version 5.15.10) + set(qt_version 5.15.11) elseif (APPLE) - set(qt_version 6.2.5) + set(qt_version 6.2.6) endif() endif() include(cmake/external/qt/package.cmake) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f9f3de29c..0ec5a06b0 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -837,6 +837,8 @@ PRIVATE 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 history/view/history_view_requests_bar.h history/view/history_view_schedule_box.cpp @@ -891,6 +893,10 @@ PRIVATE history/history_view_highlight_manager.h history/history_widget.cpp history/history_widget.h + info/boosts/giveaway/giveaway_list_controllers.cpp + info/boosts/giveaway/giveaway_list_controllers.h + info/boosts/create_giveaway_box.cpp + info/boosts/create_giveaway_box.h info/boosts/info_boosts_inner_widget.cpp info/boosts/info_boosts_inner_widget.h info/boosts/info_boosts_widget.cpp @@ -1609,7 +1615,39 @@ elseif (APPLE) endif() set(icons_path ${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Images.xcassets) - target_add_resource(Telegram ${icons_path}) + if (CMAKE_GENERATOR STREQUAL Xcode) + target_add_resource(Telegram ${icons_path}) + else() + set(icon_path ${icons_path}/Icon.iconset) + find_program(ICONUTIL iconutil) + find_program(PNG2ICNS png2icns) + if (ICONUTIL) + add_custom_command( + OUTPUT Icon.icns + COMMAND ${ICONUTIL} + ARGS + --convert icns + --output Icon.icns + ${icon_path} + ) + elseif (PNG2ICNS) + add_custom_command( + OUTPUT Icon.icns + COMMAND ${PNG2ICNS} + ARGS + Icon.icns + ${icon_path}/icon_16x16.png + ${icon_path}/icon_32x32.png + ${icon_path}/icon_128x128.png + ${icon_path}/icon_256x256.png + ${icon_path}/icon_512x512.png + ) + endif() + if (ICONUTIL OR PNG2ICNS) + set_source_files_properties(Icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + target_add_resource(Telegram Icon.icns) + endif() + endif() set(lang_packs en @@ -1636,24 +1674,17 @@ elseif (APPLE) COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $/../Resources COMMAND cp ${CMAKE_BINARY_DIR}/lib_spellcheck.rcc $/../Resources ) - if (NOT build_macstore) + if (NOT build_macstore AND NOT DESKTOP_APP_DISABLE_CRASH_REPORTS) + if (DESKTOP_APP_MAC_ARCH STREQUAL "x86_64" OR DESKTOP_APP_MAC_ARCH STREQUAL "arm64") + set(crashpad_dir_part ".${DESKTOP_APP_MAC_ARCH}") + else() + set(crashpad_dir_part "") + endif() add_custom_command(TARGET Telegram PRE_LINK - COMMAND mkdir -p $/../Frameworks - COMMAND cp $ $/../Frameworks/ + COMMAND mkdir -p $/../Helpers + COMMAND cp ${libs_loc}/crashpad/out/$,Debug,Release>${crashpad_dir_part}/crashpad_handler $/../Helpers/ ) - if (NOT DESKTOP_APP_DISABLE_CRASH_REPORTS) - if (DESKTOP_APP_MAC_ARCH STREQUAL "x86_64" OR DESKTOP_APP_MAC_ARCH STREQUAL "arm64") - set(crashpad_dir_part ".${DESKTOP_APP_MAC_ARCH}") - else() - set(crashpad_dir_part "") - endif() - add_custom_command(TARGET Telegram - PRE_LINK - COMMAND mkdir -p $/../Helpers - COMMAND cp ${libs_loc}/crashpad/out/$,Debug,Release>${crashpad_dir_part}/crashpad_handler $/../Helpers/ - ) - endif() endif() else() target_link_libraries(Telegram @@ -1702,7 +1733,11 @@ if (build_macstore) COMMAND rm -rf $/../Frameworks/Breakpad.framework/Resources/Inspector ) else() - set(bundle_identifier "com.tdesktop.Telegram$<$:Debug>") + if (CMAKE_GENERATOR STREQUAL Xcode) + set(bundle_identifier "com.tdesktop.Telegram$<$:Debug>") + else() + set(bundle_identifier "com.tdesktop.Telegram") + endif() set(bundle_entitlements "Telegram.entitlements") if (LINUX AND DESKTOP_APP_USE_PACKAGED) set(output_name "ayugram-desktop") @@ -1711,6 +1746,12 @@ else() endif() endif() +if (CMAKE_GENERATOR STREQUAL Xcode) + set(bundle_identifier_plist "$(PRODUCT_BUNDLE_IDENTIFIER)") +else() + set(bundle_identifier_plist ${bundle_identifier}) +endif() + set_target_properties(Telegram PROPERTIES OUTPUT_NAME ${output_name} MACOSX_BUNDLE_GUI_IDENTIFIER ${bundle_identifier} @@ -1784,7 +1825,7 @@ endif() target_prepare_qrc(Telegram) -if ((NOT DESKTOP_APP_DISABLE_AUTOUPDATE OR APPLE) AND NOT build_macstore AND NOT build_winstore) +if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_winstore) add_executable(Updater WIN32) init_non_host_target(Updater) @@ -1822,6 +1863,12 @@ if ((NOT DESKTOP_APP_DISABLE_AUTOUPDATE OR APPLE) AND NOT build_macstore AND NOT else() target_link_options(Updater PRIVATE -municode) endif() + elseif (APPLE) + add_custom_command(TARGET Updater + PRE_LINK + COMMAND mkdir -p $/../Frameworks + COMMAND cp $ $/../Frameworks/ + ) endif() if (DESKTOP_APP_SPECIAL_TARGET) diff --git a/Telegram/Resources/icons/boosts/boost_mini2.png b/Telegram/Resources/icons/boosts/boost_mini2.png new file mode 100644 index 000000000..b4884ccc8 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_mini2.png differ diff --git a/Telegram/Resources/icons/boosts/boost_mini2@2x.png b/Telegram/Resources/icons/boosts/boost_mini2@2x.png new file mode 100644 index 000000000..9fb6e01e9 Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_mini2@2x.png differ diff --git a/Telegram/Resources/icons/boosts/boost_mini2@3x.png b/Telegram/Resources/icons/boosts/boost_mini2@3x.png new file mode 100644 index 000000000..16aff7dbe Binary files /dev/null and b/Telegram/Resources/icons/boosts/boost_mini2@3x.png differ diff --git a/Telegram/Resources/icons/boosts/filled_gift.png b/Telegram/Resources/icons/boosts/filled_gift.png new file mode 100644 index 000000000..123818f74 Binary files /dev/null and b/Telegram/Resources/icons/boosts/filled_gift.png differ diff --git a/Telegram/Resources/icons/boosts/filled_gift@2x.png b/Telegram/Resources/icons/boosts/filled_gift@2x.png new file mode 100644 index 000000000..13e5102ed Binary files /dev/null and b/Telegram/Resources/icons/boosts/filled_gift@2x.png differ diff --git a/Telegram/Resources/icons/boosts/filled_gift@3x.png b/Telegram/Resources/icons/boosts/filled_gift@3x.png new file mode 100644 index 000000000..7e3ad4e24 Binary files /dev/null and b/Telegram/Resources/icons/boosts/filled_gift@3x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_channel.png b/Telegram/Resources/icons/chat/reply_type_channel.png new file mode 100644 index 000000000..8aa9e90f8 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_channel.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_channel@2x.png b/Telegram/Resources/icons/chat/reply_type_channel@2x.png new file mode 100644 index 000000000..cb8bb8c2b Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_channel@2x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_channel@3x.png b/Telegram/Resources/icons/chat/reply_type_channel@3x.png new file mode 100644 index 000000000..e659c35ac Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_channel@3x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_group.png b/Telegram/Resources/icons/chat/reply_type_group.png new file mode 100644 index 000000000..b40ba4b81 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_group.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_group@2x.png b/Telegram/Resources/icons/chat/reply_type_group@2x.png new file mode 100644 index 000000000..8ad079fb3 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_group@2x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_group@3x.png b/Telegram/Resources/icons/chat/reply_type_group@3x.png new file mode 100644 index 000000000..11dd58c19 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_group@3x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_user.png b/Telegram/Resources/icons/chat/reply_type_user.png new file mode 100644 index 000000000..f2d6cf6fc Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_user.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_user@2x.png b/Telegram/Resources/icons/chat/reply_type_user@2x.png new file mode 100644 index 000000000..8ef54da95 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_user@2x.png differ diff --git a/Telegram/Resources/icons/chat/reply_type_user@3x.png b/Telegram/Resources/icons/chat/reply_type_user@3x.png new file mode 100644 index 000000000..67a947f36 Binary files /dev/null and b/Telegram/Resources/icons/chat/reply_type_user@3x.png differ diff --git a/Telegram/Resources/icons/tray_monochrome.svg b/Telegram/Resources/icons/tray_monochrome.svg new file mode 100644 index 000000000..b960a4afd --- /dev/null +++ b/Telegram/Resources/icons/tray_monochrome.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 64ad8af50..8c2ffe471 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2078,9 +2078,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_giveaway_award_subtitle" = "Select recipients >"; "lng_giveaway_award_chosen#one" = "{count} recipient >"; "lng_giveaway_award_chosen#other" = "{count} recipients >"; -"lng_giveaway_quantity_title" = "Quantity of prizes / boosts"; -"lng_giveaway_quantity#one" = "{count} Subscription / Boost"; -"lng_giveaway_quantity#other" = "{count} Subscriptions / Boosts"; +"lng_giveaway_quantity_title" = "Quantity of prizes"; +"lng_giveaway_quantity#one" = "{count} boost"; +"lng_giveaway_quantity#other" = "{count} boosts"; "lng_giveaway_quantity_about" = "Choose how many Premium subscriptions to give away and boosts to receive."; "lng_giveaway_channels_title" = "Channels included in the giveaway"; "lng_giveaway_channels_this#one" = "this channel will receive {count} boost"; @@ -2089,8 +2089,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_giveaway_channels_about" = "Choose the channels the users need to join to take part in the giveaway."; "lng_giveaway_users_title" = "Users eligible for the giveaway"; "lng_giveaway_users_all" = "All subscribers"; +"lng_giveaway_users_from_all_countries" = "from all countries"; +"lng_giveaway_users_from_one_country" = "from {country}"; +"lng_giveaway_users_from_countries#one" = "from {count} country"; +"lng_giveaway_users_from_countries#other" = "from {count} countries"; "lng_giveaway_users_new" = "Only new subscribers"; -"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to the newly joined subscribers."; +"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started or to users from specific countries."; "lng_giveaway_start" = "Start Giveaway"; "lng_giveaway_award" = "Gift Premium"; "lng_giveaway_date_title" = "Date when giveaway ends"; @@ -2100,14 +2104,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_giveaway_duration_title#one" = "Duration of Premium subscription"; "lng_giveaway_duration_title#other" = "Duration of Premium subscriptions"; "lng_giveaway_duration_price" = "{price} x {amount}"; -"lng_giveaway_duration_about" = "You can review the list of features and terms of use for Telegram Premium {link}."; -"lng_giveaway_duration_about_link" = "here"; "lng_giveaway_date_select" = "Select Date and Time"; "lng_giveaway_date_confirm" = "Confirm"; "lng_giveaway_channels_select#one" = "Select up to {count} channel"; "lng_giveaway_channels_select#other" = "Select up to {count} channels"; "lng_giveaway_recipients_save" = "Save Recipients"; "lng_giveaway_recipients_deselect" = "Deselect All"; +"lng_giveaway_maximum_countries_error#one" = "You can select maximum {count} country."; +"lng_giveaway_maximum_countries_error#other" = "You can select maximum {count} countries."; +"lng_giveaway_maximum_channels_error#one" = "You can select maximum {count} channel."; +"lng_giveaway_maximum_channels_error#other" = "You can select maximum {count} channels."; +"lng_giveaway_maximum_users_error#one" = "You can select maximum {count} user."; +"lng_giveaway_maximum_users_error#other" = "You can select maximum {count} users."; +"lng_giveaway_channels_confirm_title" = "Channel is Private"; +"lng_giveaway_channels_confirm_about" = "Are you sure you want to add a private channel? Users won't be able to join it without an invite link."; + +"lng_giveaway_created_title" = "Giveaway created"; +"lng_giveaway_created_body" = "Check your channels' {link} to see how this giveaway boosted your channel."; +"lng_giveaway_awarded_title" = "Premium subscriptions gifted"; +"lng_giveaway_awarded_body" = "Check your channels' {link} to see how gifts boosted your channel."; +"lng_giveaway_created_link" = "Statistics"; "lng_prize_title" = "Congratulations!"; "lng_prize_about" = "You won a prize in a giveaway organized by {channel}."; @@ -2143,10 +2159,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_prizes_end_when_finish" = "On {date}, Telegram automatically selected {winners}."; "lng_prizes_end_activated#one" = "**{count}** of the winners already used their gift link."; "lng_prizes_end_activated#other" = "**{count}** of the winners already used their gift links."; -"lng_prizes_winners_all_of_one#one" = "{count} random subscribers of {channel}."; -"lng_prizes_winners_all_of_one#other" = "{count} random subscribers of {channel}."; -"lng_prizes_winners_all_of_many#one" = "{count} random subscribers of {channel} and other listed channels."; -"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed channels."; +"lng_prizes_winners_all_of_one#one" = "{count} random subscribers of {channel}"; +"lng_prizes_winners_all_of_one#other" = "{count} random subscribers of {channel}"; +"lng_prizes_winners_all_of_many#one" = "{count} random subscribers of {channel} and other listed channels"; +"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed channels"; "lng_prizes_winners_new_of_one#one" = "{count} random user that joined {channel} after {start_date}"; "lng_prizes_winners_new_of_one#other" = "{count} random users that joined {channel} after {start_date}"; "lng_prizes_winners_new_of_many#one" = "{count} random user that joined {channel} and other listed channels after {start_date}"; @@ -4312,10 +4328,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boosts_list_title#one" = "{count} booster"; "lng_boosts_list_title#other" = "{count} boosters"; "lng_boosts_list_subtext" = "Your channel is currently boosted by these users."; -"lng_boosts_show_more" = "Show More Boosts"; +"lng_boosts_show_more_boosts#one" = "Show {count} More Boosts"; +"lng_boosts_show_more_boosts#other" = "Show {count} More Boosts"; +"lng_boosts_show_more_gifts#one" = "Show {count} More Boosts"; +"lng_boosts_show_more_gifts#other" = "Show {count} More Boosts"; "lng_boosts_list_status" = "boost expires on {date}"; "lng_boosts_link_title" = "Link for boosting"; "lng_boosts_link_subtext" = "Share this link with your subscribers to get more boosts."; +"lng_boosts_get_boosts" = "Get Boosts via Gifts"; +"lng_boosts_get_boosts_subtext" = "Get more boosts for your channel by gifting Telegram Premium to your subscribers."; +"lng_boosts_list_unclaimed" = "Unclaimed"; +"lng_boosts_list_pending" = "To be distributed"; +"lng_boosts_list_pending_about" = "The recipient will be selected when the giveaway ends."; +"lng_boosts_list_tab_gifts#one" = "{count} Gifts"; +"lng_boosts_list_tab_gifts#other" = "{count} Gifts"; // Wnd specific diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc index 788e0822e..ce814218c 100644 --- a/Telegram/Resources/qrc/telegram/telegram.qrc +++ b/Telegram/Resources/qrc/telegram/telegram.qrc @@ -28,6 +28,7 @@ ../../icons/settings/dino.svg ../../icons/settings/star.svg ../../icons/settings/starmini.svg + ../../icons/tray_monochrome.svg ../../art/topic_icons/blue.svg ../../art/topic_icons/yellow.svg ../../art/topic_icons/violet.svg diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index cc75e489e..522be5d7b 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.11.5.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 895e49214..2f61f1e73 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 4,11,3,0 - PRODUCTVERSION 4,11,3,0 + FILEVERSION 4,11,5,0 + PRODUCTVERSION 4,11,5,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "4.11.3.0" + VALUE "FileVersion", "4.11.5.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.11.3.0" + VALUE "ProductVersion", "4.11.5.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 6f0135c28..ff45afad7 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 4,11,3,0 - PRODUCTVERSION 4,11,3,0 + FILEVERSION 4,11,5,0 + PRODUCTVERSION 4,11,5,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "4.11.3.0" + VALUE "FileVersion", "4.11.5.0" VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.11.3.0" + VALUE "ProductVersion", "4.11.5.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 320f3f94a..629abc0d0 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -9,12 +9,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium_option.h" #include "api/api_text_entities.h" -#include "main/main_session.h" -#include "data/data_peer_values.h" -#include "data/data_document.h" -#include "data/data_session.h" -#include "data/data_peer.h" #include "apiwrap.h" +#include "base/random.h" +#include "data/data_document.h" +#include "data/data_peer.h" +#include "data/data_peer_values.h" +#include "data/data_session.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "payments/payments_form.h" +#include "ui/text/format_values.h" namespace Api { namespace { @@ -31,6 +36,24 @@ namespace { }; } +[[nodiscard]] Data::SubscriptionOptions GiftCodesFromTL( + const QVector &tlOptions) { + auto options = SubscriptionOptionsFromTL(tlOptions); + for (auto i = 0; i < options.size(); i++) { + const auto &tlOption = tlOptions[i].data(); + const auto perUserText = Ui::FillAmountAndCurrency( + tlOption.vamount().v / float64(tlOption.vusers().v), + qs(tlOption.vcurrency()), + false); + options[i].costPerMonth = perUserText + + ' ' + + QChar(0x00D7) + + ' ' + + QString::number(tlOption.vusers().v); + } + return options; +} + } // namespace Premium::Premium(not_null api) @@ -311,4 +334,139 @@ const Data::SubscriptionOptions &Premium::subscriptionOptions() const { return _subscriptionOptions; } +PremiumGiftCodeOptions::PremiumGiftCodeOptions(not_null peer) +: _peer(peer) +, _api(&peer->session().api().instance()) { +} + +rpl::producer PremiumGiftCodeOptions::request() { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto channel = _peer->asChannel(); + if (!channel) { + return lifetime; + } + + using TLOption = MTPPremiumGiftCodeOption; + _api.request(MTPpayments_GetPremiumGiftCodeOptions( + MTP_flags( + MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer), + _peer->input + )).done([=](const MTPVector &result) { + auto tlMapOptions = base::flat_map>(); + for (const auto &tlOption : result.v) { + const auto &data = tlOption.data(); + tlMapOptions[data.vusers().v].push_back(tlOption); + + const auto token = Token{ data.vusers().v, data.vmonths().v }; + _stores[token] = Store{ + .amount = data.vamount().v, + .product = qs(data.vstore_product().value_or_empty()), + .quantity = data.vstore_quantity().value_or_empty(), + }; + if (!ranges::contains(_availablePresets, data.vusers().v)) { + _availablePresets.push_back(data.vusers().v); + } + } + for (const auto &[amount, tlOptions] : tlMapOptions) { + if (amount == 1 && _optionsForOnePerson.currency.isEmpty()) { + _optionsForOnePerson.currency = qs( + tlOptions.front().data().vcurrency()); + for (const auto &option : tlOptions) { + _optionsForOnePerson.months.push_back( + option.data().vmonths().v); + _optionsForOnePerson.totalCosts.push_back( + option.data().vamount().v); + } + } + _subscriptionOptions[amount] = GiftCodesFromTL(tlOptions); + } + consumer.put_done(); + }).fail([=](const MTP::Error &error) { + consumer.put_error_copy(error.type()); + }).send(); + + return lifetime; + }; +} + +const std::vector &PremiumGiftCodeOptions::availablePresets() const { + return _availablePresets; +} + +Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice( + int users, + int monthsIndex) { + const auto randomId = base::RandomValue(); + const auto token = Token{ + users, + _optionsForOnePerson.months[monthsIndex], + }; + const auto &store = _stores[token]; + return Payments::InvoicePremiumGiftCode{ + .randomId = randomId, + .currency = _optionsForOnePerson.currency, + .amount = store.amount, + .storeProduct = store.product, + .storeQuantity = store.quantity, + .users = token.users, + .months = token.months, + }; +} + +Data::SubscriptionOptions PremiumGiftCodeOptions::options(int amount) { + const auto it = _subscriptionOptions.find(amount); + if (it != end(_subscriptionOptions)) { + return it->second; + } else { + auto tlOptions = QVector(); + for (auto i = 0; i < _optionsForOnePerson.months.size(); i++) { + tlOptions.push_back(MTP_premiumGiftCodeOption( + MTP_flags(MTPDpremiumGiftCodeOption::Flags(0)), + MTP_int(amount), + MTP_int(_optionsForOnePerson.months[i]), + MTPstring(), + MTPint(), + MTP_string(_optionsForOnePerson.currency), + MTP_long(_optionsForOnePerson.totalCosts[i] * amount))); + } + _subscriptionOptions[amount] = GiftCodesFromTL(tlOptions); + return _subscriptionOptions[amount]; + } +} + +int PremiumGiftCodeOptions::giveawayBoostsPerPremium() const { + constexpr auto kFallbackCount = 4; + return _peer->session().account().appConfig().get( + u"giveaway_boosts_per_premium"_q, + kFallbackCount); +} + +int PremiumGiftCodeOptions::giveawayCountriesMax() const { + constexpr auto kFallbackCount = 10; + return _peer->session().account().appConfig().get( + u"giveaway_countries_max"_q, + kFallbackCount); +} + +int PremiumGiftCodeOptions::giveawayAddPeersMax() const { + constexpr auto kFallbackCount = 10; + return _peer->session().account().appConfig().get( + u"giveaway_add_peers_max"_q, + kFallbackCount); +} + +int PremiumGiftCodeOptions::giveawayPeriodMax() const { + constexpr auto kFallbackCount = 3600 * 24 * 7; + return _peer->session().account().appConfig().get( + u"giveaway_period_max"_q, + kFallbackCount); +} + +bool PremiumGiftCodeOptions::giveawayGiftsPurchaseAvailable() const { + return _peer->session().account().appConfig().get( + u"giveaway_gifts_purchase_available"_q, + false); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 01ed83cc7..deb66df80 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -16,6 +16,10 @@ namespace Main { class Session; } // namespace Main +namespace Payments { +struct InvoicePremiumGiftCode; +} // namespace Payments + namespace Api { struct GiftCode { @@ -141,4 +145,51 @@ private: }; +class PremiumGiftCodeOptions final { +public: + PremiumGiftCodeOptions(not_null peer); + + [[nodiscard]] rpl::producer request(); + [[nodiscard]] Data::SubscriptionOptions options(int amount); + [[nodiscard]] const std::vector &availablePresets() const; + [[nodiscard]] Payments::InvoicePremiumGiftCode invoice( + int users, + int monthsIndex); + + [[nodiscard]] int giveawayBoostsPerPremium() const; + [[nodiscard]] int giveawayCountriesMax() const; + [[nodiscard]] int giveawayAddPeersMax() const; + [[nodiscard]] int giveawayPeriodMax() const; + [[nodiscard]] bool giveawayGiftsPurchaseAvailable() const; + +private: + struct Token final { + int users = 0; + int months = 0; + + friend inline constexpr auto operator<=>(Token, Token) = default; + + }; + struct Store final { + uint64 amount = 0; + QString product; + int quantity = 0; + }; + using Amount = int; + const not_null _peer; + base::flat_map _subscriptionOptions; + struct { + std::vector months; + std::vector totalCosts; + QString currency; + } _optionsForOnePerson; + + std::vector _availablePresets; + + base::flat_map _stores; + + MTP::Sender _api; + +}; + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium_option.h b/Telegram/SourceFiles/api/api_premium_option.h index c23eb7876..5758a8cb8 100644 --- a/Telegram/SourceFiles/api/api_premium_option.h +++ b/Telegram/SourceFiles/api/api_premium_option.h @@ -36,7 +36,10 @@ template result.reserve(tlOptions.size()); for (const auto &tlOption : tlOptions) { const auto &option = tlOption.data(); - const auto botUrl = qs(option.vbot_url()); + auto botUrl = QString(); + if constexpr (!std::is_same_v) { + botUrl = qs(option.vbot_url()); + } const auto months = option.vmonths().v; const auto amount = option.vamount().v; const auto currency = qs(option.vcurrency()); diff --git a/Telegram/SourceFiles/api/api_statistics.cpp b/Telegram/SourceFiles/api/api_statistics.cpp index c3286f667..3069843c1 100644 --- a/Telegram/SourceFiles/api/api_statistics.cpp +++ b/Telegram/SourceFiles/api/api_statistics.cpp @@ -533,8 +533,13 @@ rpl::producer Boosts::request() { }; _boostStatus.link = qs(data.vboost_url()); - requestBoosts({}, [=](Data::BoostsListSlice &&slice) { - _boostStatus.firstSlice = std::move(slice); + using namespace Data; + requestBoosts({ .gifts = false }, [=](BoostsListSlice &&slice) { + _boostStatus.firstSliceBoosts = std::move(slice); + requestBoosts({ .gifts = true }, [=](BoostsListSlice &&s) { + _boostStatus.firstSliceGifts = std::move(s); + consumer.put_done(); + }); consumer.put_done(); }); }).fail([=](const MTP::Error &error) { @@ -553,8 +558,11 @@ void Boosts::requestBoosts( } constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice); constexpr auto kTlLimit = tl::make_int(kLimit); + const auto gifts = token.gifts; _requestId = _api.request(MTPpremium_GetBoostsList( - MTP_flags(0), + gifts + ? MTP_flags(MTPpremium_GetBoostsList::Flag::f_gifts) + : MTP_flags(0), _peer->input, MTP_string(token.next), token.next.isEmpty() ? kTlFirstSlice : kTlLimit @@ -567,19 +575,41 @@ void Boosts::requestBoosts( auto list = std::vector(); list.reserve(data.vboosts().v.size()); for (const auto &boost : data.vboosts().v) { + const auto &data = boost.data(); + const auto path = data.vused_gift_slug() + ? (u"giftcode/"_q + qs(data.vused_gift_slug()->v)) + : QString(); + auto giftCodeLink = !path.isEmpty() + ? Data::GiftCodeLink{ + _peer->session().createInternalLink(path), + _peer->session().createInternalLinkFull(path), + qs(data.vused_gift_slug()->v), + } + : Data::GiftCodeLink(); list.push_back({ - boost.data().vuser_id().value_or_empty(), - QDateTime::fromSecsSinceEpoch(boost.data().vexpires().v), + data.is_gift(), + data.is_giveaway(), + data.is_unclaimed(), + qs(data.vid()), + data.vuser_id().value_or_empty(), + data.vgiveaway_msg_id() + ? FullMsgId{ _peer->id, data.vgiveaway_msg_id()->v } + : FullMsgId(), + QDateTime::fromSecsSinceEpoch(data.vdate().v), + data.vexpires().v, + std::move(giftCodeLink), + data.vmultiplier().value_or_empty(), }); } done(Data::BoostsListSlice{ .list = std::move(list), - .total = data.vcount().v, + .multipliedTotal = data.vcount().v, .allLoaded = (data.vcount().v == data.vboosts().v.size()), .token = Data::BoostsListSlice::OffsetToken{ - data.vnext_offset() + .next = data.vnext_offset() ? qs(*data.vnext_offset()) - : QString() + : QString(), + .gifts = gifts, }, }); }).fail([=] { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 93d289e76..c0d7a5bc2 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3614,7 +3614,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendAction(action); const auto clearCloudDraft = action.clearDraft; - const auto topicRootId = action.replyTo.topicRootId; + const auto replyTo = action.replyTo.messageId + ? peer->owner().message(action.replyTo.messageId) + : nullptr; + const auto topicRootId = action.replyTo.topicRootId + ? action.replyTo.topicRootId + : replyTo + ? replyTo->topicRootId() + : Data::ForumTopic::kGeneralId; const auto topic = peer->forumTopicFor(topicRootId); if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) || Api::SendDice(message)) { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index 557eefa9a..d731ead51 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -37,8 +37,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" #include "styles/style_settings.h" -#include - namespace { constexpr auto kMaxLinkTitleLength = 32; @@ -223,14 +221,6 @@ private: }; -[[nodiscard]] uint64 ComputeRowId(const QString &link) { - return XXH64(link.data(), link.size() * sizeof(ushort), 0); -} - -[[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) { - return ComputeRowId(data.url); -} - [[nodiscard]] Color ComputeColor(const InviteLinkData &link) { return Color::Permanent; } @@ -242,7 +232,7 @@ private: LinkRow::LinkRow( not_null delegate, const InviteLinkData &data) -: PeerListRow(ComputeRowId(data)) +: PeerListRow(UniqueRowIdFromString(data.url)) , _delegate(delegate) , _data(data) , _color(ComputeColor(data)) { diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 84ea6a429..9a87a58fc 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "base/weak_ptr.h" #include "boxes/peers/prepare_short_info_box.h" +#include "data/data_boosts.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_media_types.h" // Data::Giveaway @@ -31,7 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/premium_stars_colored.h" #include "ui/effects/premium_top_bar.h" #include "ui/layers/generic_box.h" -#include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/gradient_round_button.h" @@ -40,9 +40,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_peer_menu.h" // ShowChooseRecipientBox. #include "window/window_session_controller.h" #include "styles/style_boxes.h" -#include "styles/style_layers.h" -#include "styles/style_chat_helpers.h" +#include "styles/style_giveaway.h" #include "styles/style_info.h" +#include "styles/style_layers.h" #include "styles/style_premium.h" #include @@ -237,11 +237,7 @@ void GiftBox( }, box->lifetime()); } -struct GiftCodeLink { - QString text; - QString link; -}; -[[nodiscard]] GiftCodeLink MakeGiftCodeLink( +[[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink( not_null session, const QString &slug) { const auto path = u"giftcode/"_q + slug; @@ -693,7 +689,8 @@ void GiveawayInfoBox( text.append(' ').append(tr::lng_prizes_end_activated( tr::now, lt_count, - info.activatedCount)); + info.activatedCount, + Ui::Text::RichLangValue)); } if (!info.giftCode.isEmpty()) { text.append("\n\n"); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 3395a5850..abd4f4b32 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -35,6 +35,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_dialogs.h" #include "styles/style_widgets.h" +#include // XXH64. + +[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) { + return XXH64(d.data(), d.size() * sizeof(ushort), 0); +} + PaintRoundImageCallback PaintUserpicCallback( not_null peer, bool respectSavedMessagesChat) { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 6dfce4630..0f557da68 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -54,6 +54,8 @@ using PaintRoundImageCallback = Fn value, Fn repaint); + int width() override; QString entityData() override; void paint(QPainter &p, const Context &context) override; @@ -80,6 +81,10 @@ DefaultIconEmoji::DefaultIconEmoji( }, _lifetime); } +int DefaultIconEmoji::width() { + return st::emojiSize + 2 * st::emojiPadding; +} + QString DefaultIconEmoji::entityData() { return u"topic_icon:%1"_q.arg(_icon.colorId); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 45a4c0ea0..4d7254388 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -32,8 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" // st::settingsDividerLabelPadding #include "styles/style_menu_icons.h" -#include - namespace { enum class Color { @@ -112,12 +110,8 @@ private: }; -[[nodiscard]] uint64 ComputeRowId(const QString &link) { - return XXH64(link.data(), link.size() * sizeof(ushort), 0); -} - [[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) { - return ComputeRowId(data.link); + return UniqueRowIdFromString(data.link); } [[nodiscard]] float64 ComputeProgress( @@ -628,7 +622,8 @@ void LinksController::updateRow(const InviteLinkData &data, TimeId now) { } bool LinksController::removeRow(const QString &link) { - if (const auto row = delegate()->peerListFindRow(ComputeRowId(link))) { + const auto id = UniqueRowIdFromString(link); + if (const auto row = delegate()->peerListFindRow(id)) { delegate()->peerListRemoveRow(row); return true; } diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 18e5f1d37..1bd1c043f 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -432,6 +432,7 @@ void SendFilesBox::prepare() { preparePreview(); initPreview(); SetupShadowsToScrollContent(this, _scroll, _inner->heightValue()); + setCloseByOutsideClick(false); boxClosing() | rpl::start_with_next([=] { if (!_confirmed && _cancelledCallback) { @@ -1451,7 +1452,12 @@ void SendFilesBox::sendScheduled() { ? SendMenu::Type::ScheduledToUser : _sendMenuType; const auto callback = [=](Api::SendOptions options) { send(options); }; - _show->showBox(HistoryView::PrepareScheduleBox(this, type, callback)); + auto box = HistoryView::PrepareScheduleBox(this, type, callback); + const auto weak = Ui::MakeWeak(box.data()); + _show->showBox(std::move(box)); + if (const auto strong = weak.data()) { + strong->setCloseByOutsideClick(false); + } } void SendFilesBox::sendWhenOnline() { diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 8b121852e..776a48e00 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 = 4011003; -constexpr auto AppVersionStr = "4.11.3"; +constexpr auto AppVersion = 4011005; +constexpr auto AppVersionStr = "4.11.5"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/data_boosts.h b/Telegram/SourceFiles/data/data_boosts.h index 6ad253169..a62c70c92 100644 --- a/Telegram/SourceFiles/data/data_boosts.h +++ b/Telegram/SourceFiles/data/data_boosts.h @@ -19,24 +19,41 @@ struct BoostsOverview final { float64 premiumMemberPercentage = 0; }; +struct GiftCodeLink final { + QString text; + QString link; + QString slug; +}; + struct Boost final { + bool isGift = false; + bool isGiveaway = false; + bool isUnclaimed = false; + + QString id; UserId userId = UserId(0); - QDateTime expirationDate; + FullMsgId giveawayMessage; + QDateTime date; + crl::time expiresAt = 0; + GiftCodeLink giftCodeLink; + int multiplier = 0; }; struct BoostsListSlice final { struct OffsetToken final { QString next; + bool gifts = false; }; std::vector list; - int total = 0; + int multipliedTotal = 0; bool allLoaded = false; OffsetToken token; }; struct BoostStatus final { BoostsOverview overview; - BoostsListSlice firstSlice; + BoostsListSlice firstSliceBoosts; + BoostsListSlice firstSliceGifts; QString link; }; diff --git a/Telegram/SourceFiles/data/data_file_origin.cpp b/Telegram/SourceFiles/data/data_file_origin.cpp index d37663208..9cac2ec4b 100644 --- a/Telegram/SourceFiles/data/data_file_origin.cpp +++ b/Telegram/SourceFiles/data/data_file_origin.cpp @@ -86,9 +86,16 @@ struct FileReferenceAccumulator { }, [](const auto &data) { }); } + void push(const MTPMessageReplyHeader &data) { + data.match([&](const MTPDmessageReplyHeader &data) { + push(data.vreply_media()); + }, [](const MTPDmessageReplyStoryHeader &data) { + }); + } void push(const MTPMessage &data) { data.match([&](const MTPDmessage &data) { push(data.vmedia()); + push(data.vreply_to()); }, [&](const MTPDmessageService &data) { data.vaction().match( [&](const MTPDmessageActionChatEditPhoto &data) { @@ -99,6 +106,7 @@ struct FileReferenceAccumulator { push(data.vwallpaper()); }, [](const auto &data) { }); + push(data.vreply_to()); }, [](const MTPDmessageEmpty &data) { }); } diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 298dae217..2312f006f 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -54,14 +54,17 @@ MTPInputReplyTo ReplyToForMTP( } } else if (replyTo.messageId || replyTo.topicRootId) { const auto to = LookupReplyTo(history, replyTo.messageId); + const auto replyingToTopic = replyTo.topicRootId + ? history->peer->forumTopicFor(replyTo.topicRootId) + : nullptr; const auto replyingToTopicId = replyTo.topicRootId - ? replyTo.topicRootId - : Data::ForumTopic::kGeneralId; - const auto replyToTopicId = !to - ? replyingToTopicId - : to->topicRootId() + ? (replyingToTopic + ? replyingToTopic->rootId() + : Data::ForumTopic::kGeneralId) + : (to ? to->topicRootId() : Data::ForumTopic::kGeneralId); + const auto replyToTopicId = to ? to->topicRootId() - : Data::ForumTopic::kGeneralId; + : replyingToTopicId; const auto external = replyTo.messageId && (replyTo.messageId.peer != history->peer->id || replyingToTopicId != replyToTopicId); diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index cb0278a27..1132d75c4 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_shared_media.h" #include "storage/localstorage.h" #include "chat_helpers/stickers_dice_pack.h" // Stickers::DicePacks::IsSlot. +#include "chat_helpers/stickers_gift_box_pack.h" #include "data/data_session.h" #include "data/data_auto_download.h" #include "data/data_photo.h" @@ -369,6 +370,7 @@ Giveaway ComputeGiveawayData( .untilDate = data.vuntil_date().v, .quantity = data.vquantity().v, .months = data.vmonths().v, + .all = !data.is_only_new_subscribers(), }; result.channels.reserve(data.vchannels().v.size()); const auto owner = &item->history()->owner(); @@ -705,7 +707,7 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const { } } const auto type = tr::lng_in_dlg_photo(tr::now); - const auto caption = options.hideCaption + const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() : options.translated ? parent()->translatedText() @@ -951,7 +953,7 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const { } return tr::lng_in_dlg_file(tr::now); }(); - const auto caption = options.hideCaption + const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() : options.translated ? parent()->translatedText() @@ -1500,7 +1502,9 @@ bool MediaWebPage::replyPreviewLoaded() const { } ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const { - auto text = options.translated + auto text = options.ignoreMessageText + ? TextWithEntities() + : options.translated ? parent()->translatedText() : parent()->originalText(); if (text.empty()) { @@ -2038,28 +2042,23 @@ MediaStory::MediaStory( owner->registerStoryItem(storyId, parent); const auto stories = &owner->stories(); - if (const auto maybeStory = stories->lookup(storyId)) { - if (!_mention) { - parent->setText((*maybeStory)->caption()); - } - } else { - if (maybeStory.error() == NoStory::Unknown) { - stories->resolve(storyId, crl::guard(this, [=] { - if (const auto maybeStory = stories->lookup(storyId)) { - if (!_mention) { - parent->setText((*maybeStory)->caption()); - } - } else { - _expired = true; + const auto maybeStory = stories->lookup(storyId); + if (!maybeStory && maybeStory.error() == NoStory::Unknown) { + stories->resolve(storyId, crl::guard(this, [=] { + if (const auto maybeStory = stories->lookup(storyId)) { + if (!_mention && _viewMayExist) { + parent->setText((*maybeStory)->caption()); } - if (_mention) { - parent->updateStoryMentionText(); - } - parent->history()->owner().requestItemViewRefresh(parent); - })); - } else { - _expired = true; - } + } else { + _expired = true; + } + if (_mention) { + parent->updateStoryMentionText(); + } + parent->history()->owner().requestItemViewRefresh(parent); + })); + } else if (!maybeStory) { + _expired = true; } } @@ -2154,6 +2153,7 @@ std::unique_ptr MediaStory::createView( if (_mention) { return nullptr; } + _viewMayExist = true; return std::make_unique( message, realParent, @@ -2161,6 +2161,7 @@ std::unique_ptr MediaStory::createView( spoiler); } _expired = false; + _viewMayExist = true; const auto story = *maybeStory; if (_mention) { return std::make_unique( @@ -2189,6 +2190,7 @@ MediaGiveaway::MediaGiveaway( const Giveaway &data) : Media(parent) , _giveaway(data) { + parent->history()->session().giftBoxStickersPacks().load(); } std::unique_ptr MediaGiveaway::clone(not_null parent) { diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 9af199b4d..b62cf9825 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -623,6 +623,7 @@ public: private: const FullStoryId _storyId; const bool _mention = false; + bool _viewMayExist = false; bool _expired = false; }; diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 54b16fec6..123621b37 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -89,6 +89,18 @@ private: : FrameSizeFromTag(tag); } +[[nodiscard]] QString InternalPrefix() { + return u"internal:"_q; +} + +[[nodiscard]] QString InternalPadding(QMargins value) { + return value.isNull() ? QString() : QString(",%1,%2,%3,%4" + ).arg(value.left() + ).arg(value.top() + ).arg(value.right() + ).arg(value.bottom()); +} + } // namespace class CustomEmojiLoader final @@ -514,6 +526,9 @@ std::unique_ptr CustomEmojiManager::create( Fn update, SizeTag tag, int sizeOverride) { + if (data.startsWith(InternalPrefix())) { + return internal(data); + } const auto parsed = ParseCustomEmojiData(data); return parsed ? create(parsed, std::move(update), tag, sizeOverride) @@ -540,6 +555,26 @@ std::unique_ptr CustomEmojiManager::create( }); } +std::unique_ptr CustomEmojiManager::internal( + QStringView data) { + const auto v = data.mid(InternalPrefix().size()).split(','); + if (v.size() != 5 && v.size() != 1) { + return nullptr; + } + const auto index = v[0].toInt(); + Assert(index >= 0 && index < _internalEmoji.size()); + + auto &info = _internalEmoji[index]; + const auto padding = (v.size() == 5) + ? QMargins(v[1].toInt(), v[2].toInt(), v[3].toInt(), v[4].toInt()) + : QMargins(); + return std::make_unique( + data.toString(), + info.image, + padding, + info.textColor); +} + void CustomEmojiManager::resolve( QStringView data, not_null listener) { @@ -885,6 +920,41 @@ uint64 CustomEmojiManager::coloredSetId() const { return _coloredSetId; } +QString CustomEmojiManager::registerInternalEmoji( + QImage emoji, + QMargins padding, + bool textColor) { + _internalEmoji.push_back({ std::move(emoji), textColor }); + return InternalPrefix() + + QString::number(_internalEmoji.size() - 1) + + InternalPadding(padding); +} + +QString CustomEmojiManager::registerInternalEmoji( + const style::icon &icon, + QMargins padding, + bool textColor) { + const auto i = _iconEmoji.find(&icon); + if (i != end(_iconEmoji)) { + return i->second + InternalPadding(padding); + } + auto image = QImage( + icon.size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + image.setDevicePixelRatio(style::DevicePixelRatio()); + auto p = QPainter(&image); + icon.paint(p, 0, 0, icon.width()); + p.end(); + + const auto result = registerInternalEmoji( + std::move(image), + QMargins{}, + textColor); + _iconEmoji.emplace(&icon, result); + return result + InternalPadding(padding); +} + int FrameSizeFromTag(SizeTag tag) { const auto emoji = EmojiSizeFromTag(tag); const auto factor = style::DevicePixelRatio(); diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index 4c79f8870..947f8bfda 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -83,11 +83,24 @@ public: [[nodiscard]] Main::Session &session() const; [[nodiscard]] Session &owner() const; + [[nodiscard]] QString registerInternalEmoji( + QImage emoji, + QMargins padding = {}, + bool textColor = true); + [[nodiscard]] QString registerInternalEmoji( + const style::icon &icon, + QMargins padding = {}, + bool textColor = true); + [[nodiscard]] uint64 coloredSetId() const; private: static constexpr auto kSizeCount = int(SizeTag::kCount); + struct InternalEmojiData { + QImage image; + bool textColor = true; + }; struct RepaintBunch { crl::time when = 0; std::vector> instances; @@ -131,6 +144,8 @@ private: SizeTag tag, int sizeOverride, LoaderFactory factory); + [[nodiscard]] std::unique_ptr internal( + QStringView data); [[nodiscard]] static int SizeIndex(SizeTag tag); const not_null _owner; @@ -163,6 +178,9 @@ private: bool _repaintTimerScheduled = false; bool _requestSetsScheduled = false; + std::vector _internalEmoji; + base::flat_map, QString> _iconEmoji; + #if 0 // inject-to-on_main crl::time _repaintsLastAdded = 0; rpl::lifetime _repaintsLifetime; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 4b9c3fe87..e71c3cefd 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -420,7 +420,7 @@ void PaintRow( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionOneLine = true, + .elisionLines = 1, }); } else if (draft || (supportMode @@ -514,7 +514,7 @@ void PaintRow( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionOneLine = true, + .elisionLines = 1, }); } } else if (!item) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index f9fe16062..cd312fd85 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -141,7 +141,7 @@ void TopicsView::paint( .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), - .elisionOneLine = true, + .elisionLines = 1, }); const auto skip = skipBig ? context.st->topicsSkipBig diff --git a/Telegram/SourceFiles/editor/scene/scene_item_image.cpp b/Telegram/SourceFiles/editor/scene/scene_item_image.cpp index b932b11ad..b3cc544cf 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_image.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_image.cpp @@ -23,7 +23,13 @@ void ItemImage::paint( QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *w) { - p->drawPixmap(contentRect().toRect(), _pixmap); + const auto rect = contentRect(); + const auto pixmapSize = QSizeF(_pixmap.size() / style::DevicePixelRatio()) + .scaled(rect.size(), Qt::KeepAspectRatio); + const auto resultRect = QRectF(rect.topLeft(), pixmapSize).translated( + (rect.width() - pixmapSize.width()) / 2., + (rect.height() - pixmapSize.height()) / 2.); + p->drawPixmap(resultRect.toRect(), _pixmap); ItemBase::paint(p, option, w); } diff --git a/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp b/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp index a60f487e0..c678452e9 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp @@ -110,7 +110,13 @@ void ItemSticker::paint( QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *w) { - p->drawImage(contentRect().toRect(), _image); + const auto rect = contentRect(); + const auto imageSize = QSizeF(_image.size() / style::DevicePixelRatio()) + .scaled(rect.size(), Qt::KeepAspectRatio); + const auto resultRect = QRectF(rect.topLeft(), imageSize).translated( + (rect.width() - imageSize.width()) / 2., + (rect.height() - imageSize.height()) / 2.); + p->drawImage(resultRect, _image); ItemBase::paint(p, option, w); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 6059be072..ff643044f 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -265,7 +265,11 @@ public: return _widget ? _widget->elementAnimationsPaused() : false; } bool elementHideReply(not_null view) override { - return view->isTopicRootReply(); + if (!view->isTopicRootReply()) { + return false; + } + const auto reply = view->data()->Get(); + return reply && !reply->fields().manualQuote; } bool elementShownUnread(not_null view) override { return view->data()->unread(view->data()->history()); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 9199fa370..d08027dcd 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -472,7 +472,7 @@ HistoryItem::HistoryItem( config.reply.topicPost = (topicRootId != 0); if (const auto originalReply = original->Get()) { if (originalReply->external()) { - config.reply = originalReply->fields(); + config.reply = originalReply->fields().clone(this); if (!config.reply.externalPeerId) { config.reply.messageId = 0; } @@ -744,7 +744,7 @@ HistoryItem::HistoryItem( HistoryItem::~HistoryItem() { _media = nullptr; clearSavedMedia(); - if (auto reply = Get()) { + if (const auto reply = Get()) { reply->clearData(this); } clearDependencyMessage(); @@ -1695,7 +1695,6 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) { setForwardsCount(data.vforwards().value_or(-1)); if (const auto reply = data.vreply_to()) { reply->match([&](const MTPDmessageReplyHeader &data) { - // #TODO replies const auto replyToPeer = data.vreply_to_peer_id() ? peerFromMTP(*data.vreply_to_peer_id()) : PeerId(); @@ -2001,9 +2000,6 @@ void HistoryItem::setRealId(MsgId newId) { _history->owner().requestItemResize(this); if (const auto reply = Get()) { - if (reply->link()) { - reply->setLinkFrom(this); - } incrementReplyToTopCounter(); } } @@ -3189,7 +3185,7 @@ void HistoryItem::createComponents(CreateConfig &&config) { UpdateComponents(mask); if (const auto reply = Get()) { - reply->set(config.reply); + reply->set(std::move(config.reply)); if (!reply->updateData(this)) { if (const auto messageId = reply->messageId()) { RequestDependentMessageItem( @@ -3405,13 +3401,15 @@ void HistoryItem::createComponentsHelper( : (replyTo.messageId.peer == history()->peer->id) ? replyTo.messageId.msg : MsgId(); + const auto forum = _history->asForum(); + const auto topic = forum + ? forum->topicFor(replyTo.topicRootId) + : nullptr; if (!config.reply.externalPeerId - && to - && config.reply.topicPost - && replyTo.topicRootId != to->topicRootId()) { + && topic + && topic->rootId() != to->topicRootId()) { config.reply.externalPeerId = replyTo.messageId.peer; } - const auto forum = _history->asForum(); config.reply.topicPost = config.reply.externalPeerId ? (replyTo.topicRootId && (replyTo.topicRootId != Data::ForumTopic::kGeneralId)) @@ -3512,7 +3510,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { }); } if (const auto reply = data.vreply_to()) { - config.reply = ReplyFieldsFromMTP(history(), *reply); + config.reply = ReplyFieldsFromMTP(this, *reply); } config.viaBotId = data.vvia_bot_id().value_or_empty(); config.viewsCount = data.vviews().value_or(-1); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index c9f01a2c0..9abc250ad 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio.h" #include "media/player/media_player_instance.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 "data/data_user.h" @@ -56,127 +57,6 @@ namespace { const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; -void ValidateBackgroundEmoji( - DocumentId backgroundEmojiId, - not_null data, - not_null cache, - not_null quote, - not_null holder) { - if (data->firstFrameMask.isNull()) { - if (!cache->frames[0].isNull()) { - for (auto &frame : cache->frames) { - frame = QImage(); - } - } - const auto tag = Data::CustomEmojiSizeTag::Isolated; - if (!data->emoji) { - const auto owner = &holder->history()->owner(); - const auto repaint = crl::guard(holder, [=] { - holder->history()->owner().requestViewRepaint(holder); - }); - data->emoji = owner->customEmojiManager().create( - backgroundEmojiId, - repaint, - tag); - } - if (!data->emoji->ready()) { - return; - } - const auto size = Data::FrameSizeFromTag(tag); - data->firstFrameMask = QImage( - QSize(size, size), - QImage::Format_ARGB32_Premultiplied); - data->firstFrameMask.fill(Qt::transparent); - data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio()); - auto p = Painter(&data->firstFrameMask); - data->emoji->paint(p, { - .textColor = QColor(255, 255, 255), - .position = QPoint(0, 0), - .internal = { - .forceFirstFrame = true, - }, - }); - p.end(); - - data->emoji = nullptr; - } - if (!cache->frames[0].isNull() && cache->color == quote->icon) { - return; - } - cache->color = quote->icon; - const auto ratio = style::DevicePixelRatio(); - auto colorized = QImage( - data->firstFrameMask.size(), - QImage::Format_ARGB32_Premultiplied); - colorized.setDevicePixelRatio(ratio); - style::colorizeImage( - data->firstFrameMask, - cache->color, - &colorized, - QRect(), // src - QPoint(), // dst - true); // use alpha - const auto make = [&](int size) { - size = style::ConvertScale(size) * ratio; - auto result = colorized.scaled( - size, - size, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - result.setDevicePixelRatio(ratio); - return result; - }; - - constexpr auto kSize1 = 12; - constexpr auto kSize2 = 16; - constexpr auto kSize3 = 20; - cache->frames[0] = make(kSize1); - cache->frames[1] = make(kSize2); - cache->frames[2] = make(kSize3); -} - -void FillBackgroundEmoji( - Painter &p, - const QRect &rect, - bool quote, - const Ui::BackgroundEmojiCache &cache) { - p.setClipRect(rect); - - const auto &frames = cache.frames; - const auto right = rect.x() + rect.width(); - const auto paint = [&](int x, int y, int index, float64 opacity) { - y = style::ConvertScale(y); - if (y >= rect.height()) { - return; - } - p.setOpacity(opacity); - p.drawImage( - right - style::ConvertScale(x + (quote ? 12 : 0)), - rect.y() + y, - frames[index]); - }; - - paint(28, 4, 2, 0.32); - paint(51, 15, 1, 0.32); - paint(64, -2, 0, 0.28); - paint(87, 11, 1, 0.24); - paint(125, -2, 2, 0.16); - - paint(28, 31, 1, 0.24); - paint(72, 33, 2, 0.2); - - paint(46, 52, 1, 0.24); - paint(24, 55, 2, 0.18); - - if (quote) { - paint(4, 23, 1, 0.28); - paint(0, 48, 0, 0.24); - } - - p.setClipping(false); - p.setOpacity(1.); -} - } // namespace void HistoryMessageVia::create( @@ -390,15 +270,33 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const { } } +ReplyFields ReplyFields::clone(not_null parent) const { + return { + .quote = quote, + .externalMedia = (externalMedia + ? externalMedia->clone(parent) + : nullptr), + .externalSenderId = externalSenderId, + .externalSenderName = externalSenderName, + .externalPostAuthor = externalPostAuthor, + .externalPeerId = externalPeerId, + .messageId = messageId, + .topMessageId = topMessageId, + .storyId = storyId, + .topicPost = topicPost, + .manualQuote = manualQuote, + }; +} + ReplyFields ReplyFieldsFromMTP( - not_null history, + not_null item, const MTPMessageReplyHeader &reply) { return reply.match([&](const MTPDmessageReplyHeader &data) { auto result = ReplyFields(); if (const auto peer = data.vreply_to_peer_id()) { result.externalPeerId = peerFromMTP(*peer); } - const auto owner = &history->owner(); + const auto owner = &item->history()->owner(); if (const auto id = data.vreply_to_msg_id().value_or_empty()) { result.messageId = data.is_reply_to_scheduled() ? owner->scheduledMessages().localMessageId(id) @@ -417,6 +315,9 @@ ReplyFields ReplyFieldsFromMTP( result.externalSenderName = qs(data.vfrom_name().value_or_empty()); } + if (const auto media = data.vreply_media()) { + result.externalMedia = HistoryItem::CreateMedia(item, *media); + } result.quote = TextWithEntities{ qs(data.vquote_text().value_or_empty()), Api::EntitiesFromMTP( @@ -477,7 +378,7 @@ HistoryMessageReply &HistoryMessageReply::operator=( HistoryMessageReply::~HistoryMessageReply() { // clearData() should be called by holder. Expects(resolvedMessage.empty()); - Expects(originalVia == nullptr); + _fields.externalMedia = nullptr; } bool HistoryMessageReply::updateData( @@ -523,64 +424,31 @@ bool HistoryMessageReply::updateData( } } - const auto external = this->external(); - if (resolvedMessage + const auto asExternal = displayAsExternal(holder); + const auto nonEmptyQuote = !_fields.quote.empty() + && (asExternal || _fields.manualQuote); + _multiline = !_fields.storyId && (asExternal || nonEmptyQuote); + + const auto displaying = resolvedMessage || resolvedStory - || (external && (!_fields.messageId || force))) { - const auto repaint = [=] { holder->customEmojiRepaint(); }; - const auto context = Core::MarkedTextContext{ - .session = &holder->history()->session(), - .customEmojiRepaint = repaint, - }; - const auto text = !_fields.quote.empty() - ? _fields.quote - : resolvedMessage - ? resolvedMessage->inReplyText() - : resolvedStory - ? resolvedStory->inReplyText() - : TextWithEntities{ u"..."_q }; - _text.setMarkedText( - st::defaultTextStyle, - text, - Ui::DialogTextOptions(), - context); + || ((nonEmptyQuote || _fields.externalMedia) + && (!_fields.messageId || force)); + _displaying = displaying ? 1 : 0; - updateName(holder); - setLinkFrom(holder); - if (resolvedMessage - && !resolvedMessage->Has()) { - if (const auto bot = resolvedMessage->viaBot()) { - originalVia = std::make_unique(); - originalVia->create( - &holder->history()->owner(), - peerToUser(bot->id)); - } - } + const auto unavailable = !resolvedMessage + && !resolvedStory + && ((!_fields.storyId && !_fields.messageId) || force); + _unavailable = unavailable ? 1 : 0; - if (!resolvedMessage && !resolvedStory) { - _unavailable = 1; - } - - const auto media = resolvedMessage - ? resolvedMessage->media() - : nullptr; - if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { - spoiler = nullptr; - } else if (!spoiler) { - spoiler = std::make_unique(repaint); - } - } else if (force) { - if (_fields.messageId || _fields.storyId) { - _unavailable = 1; - } - spoiler = nullptr; - } if (force) { + if (!_displaying && (_fields.messageId || _fields.storyId)) { + _unavailable = 1; + } holder->history()->owner().requestItemResize(holder); } return resolvedMessage || resolvedStory - || (external && !_fields.messageId) + || (!_fields.messageId && !_fields.storyId && external()) || _unavailable; } @@ -610,39 +478,11 @@ void HistoryMessageReply::updateFields( } } -void HistoryMessageReply::setLinkFrom( - not_null holder) { - const auto externalPeerId = _fields.externalSenderId; - const auto external = externalPeerId - || !_fields.externalSenderName.isEmpty(); - const auto externalLink = [=](ClickContext context) { - const auto my = context.other.value(); - if (const auto controller = my.sessionWindow.get()) { - if (externalPeerId) { - controller->showPeerInfo( - controller->session().data().peer(externalPeerId)); - } - controller->showToast(tr::lng_reply_from_private_chat(tr::now)); - } - }; - _link = resolvedMessage - ? JumpToMessageClickHandler( - resolvedMessage.get(), - holder->fullId(), - _fields.manualQuote ? _fields.quote : TextWithEntities()) - : resolvedStory - ? JumpToStoryClickHandler(resolvedStory.get()) - : (external && !_fields.messageId) - ? std::make_shared(externalLink) - : nullptr; -} - void HistoryMessageReply::setTopMessageId(MsgId topMessageId) { _fields.topMessageId = topMessageId; } void HistoryMessageReply::clearData(not_null holder) { - originalVia = nullptr; if (resolvedMessage) { holder->history()->owner().unregisterDependentMessage( holder, @@ -655,9 +495,12 @@ void HistoryMessageReply::clearData(not_null holder) { resolvedStory.get()); resolvedStory = nullptr; } - _name.clear(); - _text.clear(); _unavailable = 1; + _displaying = 0; + if (_multiline) { + holder->history()->owner().requestItemResize(holder); + _multiline = 0; + } refreshReplyToMedia(); } @@ -667,145 +510,13 @@ bool HistoryMessageReply::external() const { || !_fields.externalSenderName.isEmpty(); } -PeerData *HistoryMessageReply::sender(not_null holder) const { - if (resolvedStory) { - return resolvedStory->peer(); - } else if (!resolvedMessage) { - if (!_externalSender && _fields.externalSenderId) { - _externalSender = holder->history()->owner().peer( - _fields.externalSenderId); - } - return _externalSender; - } else if (holder->Has()) { - // Forward of a reply. Show reply-to original sender. - const auto forwarded - = resolvedMessage->Get(); - if (forwarded) { - return forwarded->originalSender; - } - } - if (const auto from = resolvedMessage->displayFrom()) { - return from; - } - return resolvedMessage->author().get(); -} - -QString HistoryMessageReply::senderName( +bool HistoryMessageReply::displayAsExternal( not_null holder) const { - if (const auto peer = sender(holder)) { - return senderName(peer); - } else if (!resolvedMessage) { - return _fields.externalSenderName; - } else if (holder->Has()) { - // Forward of a reply. Show reply-to original sender. - const auto forwarded - = resolvedMessage->Get(); - if (forwarded) { - Assert(forwarded->hiddenSenderInfo != nullptr); - return forwarded->hiddenSenderInfo->name; - } - } - return QString(); -} - -QString HistoryMessageReply::senderName(not_null peer) const { - if (const auto user = originalVia ? peer->asUser() : nullptr) { - return user->firstName; - } - return peer->name(); -} - -bool HistoryMessageReply::isNameUpdated( - not_null holder) const { - if (const auto from = sender(holder)) { - if (_nameVersion < from->nameVersion()) { - updateName(holder, from); - return true; - } - } - return false; -} - -void HistoryMessageReply::updateName( - not_null holder, - std::optional resolvedSender) const { - const auto peer = resolvedSender.value_or(sender(holder)); - const auto name = peer ? senderName(peer) : senderName(holder); - const auto hasPreview = (resolvedStory - && resolvedStory->hasReplyPreview()) - || (resolvedMessage - && resolvedMessage->media() - && resolvedMessage->media()->hasReplyPreview()); - const auto textLeft = hasPreview - ? (st::messageQuoteStyle.outline - + st::historyReplyPreviewMargin.left() - + st::historyReplyPreview - + st::historyReplyPreviewMargin.right()) - : st::historyReplyPadding.left(); - if (!name.isEmpty()) { - _name.setText(st::fwdTextStyle, name, Ui::NameTextOptions()); - if (peer) { - _nameVersion = peer->nameVersion(); - } - const auto w = _name.maxWidth() - + (originalVia - ? (st::msgServiceFont->spacew + originalVia->maxWidth) - : 0) - + (_fields.quote.empty() - ? 0 - : st::messageTextStyle.blockquote.icon.width()); - _maxWidth = std::max( - w, - std::min(_text.maxWidth(), st::maxSignatureSize)) - + (_fields.storyId - ? (st::dialogsMiniReplyStory.skipText - + st::dialogsMiniReplyStory.icon.icon.width()) - : 0); - } else { - _maxWidth = st::msgDateFont->width(statePhrase()); - } - _maxWidth = textLeft - + _maxWidth - + st::historyReplyPadding.right(); - _minHeight = st::historyReplyPadding.top() - + st::msgServiceNameFont->height - + st::normalFont->height - + st::historyReplyPadding.bottom(); -} - -int HistoryMessageReply::resizeToWidth(int width) const { - const auto hasPreview = (resolvedStory - && resolvedStory->hasReplyPreview()) - || (resolvedMessage - && resolvedMessage->media() - && resolvedMessage->media()->hasReplyPreview()); - const auto textLeft = hasPreview - ? (st::messageQuoteStyle.outline - + st::historyReplyPreviewMargin.left() - + st::historyReplyPreview - + st::historyReplyPreviewMargin.right()) - : st::historyReplyPadding.left(); - if (originalVia) { - originalVia->resize(width - - textLeft - - st::historyReplyPadding.right() - - _name.maxWidth() - - st::msgServiceFont->spacew); - } - if (width >= _maxWidth) { - _height = _minHeight; - return height(); - } - _height = _minHeight; - return height(); -} - -int HistoryMessageReply::height() const { - return _height + st::historyReplyTop + st::historyReplyBottom; -} - -QMargins HistoryMessageReply::margins() const { - return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom); + // Don't display replies that could be local as external. + return external() + && (!resolvedMessage + || (holder->history() != resolvedMessage->history()) + || (holder->topicRootId() != resolvedMessage->topicRootId())); } void HistoryMessageReply::itemRemoved( @@ -826,224 +537,6 @@ void HistoryMessageReply::storyRemoved( } } -void HistoryMessageReply::paint( - Painter &p, - not_null holder, - const Ui::ChatPaintContext &context, - int x, - int y, - int w, - bool inBubble) const { - const auto st = context.st; - const auto stm = context.messageStyle(); - - y += st::historyReplyTop; - const auto rect = QRect(x, y, w, _height); - const auto hasQuote = _fields.manualQuote && !_fields.quote.empty(); - const auto selected = context.selected(); - const auto colorPeer = resolvedMessage - ? resolvedMessage->displayFrom() - : resolvedStory - ? resolvedStory->peer().get() - : _externalSender - ? _externalSender - : nullptr; - const auto backgroundEmojiId = colorPeer - ? colorPeer->backgroundEmojiId() - : DocumentId(); - const auto colorIndexPlusOne = colorPeer - ? (colorPeer->colorIndex() + 1) - : resolvedMessage - ? (resolvedMessage->hiddenSenderInfo()->colorIndex + 1) - : 0; - const auto useColorIndex = colorIndexPlusOne && !context.outbg; - const auto colorPattern = colorIndexPlusOne - ? st->colorPatternIndex(colorIndexPlusOne - 1) - : 0; - const auto cache = !inBubble - ? (hasQuote - ? st->serviceQuoteCache(colorPattern) - : st->serviceReplyCache(colorPattern)).get() - : useColorIndex - ? (hasQuote - ? st->coloredQuoteCache(selected, colorIndexPlusOne - 1) - : st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get() - : (hasQuote - ? stm->quoteCache[colorPattern] - : stm->replyCache[colorPattern]).get(); - const auto "eSt = hasQuote - ? st::messageTextStyle.blockquote - : st::messageQuoteStyle; - const auto backgroundEmoji = backgroundEmojiId - ? st->backgroundEmojiData(backgroundEmojiId).get() - : nullptr; - const auto backgroundEmojiCache = backgroundEmoji - ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( - selected, - context.outbg, - inBubble, - colorIndexPlusOne)] - : nullptr; - const auto rippleColor = cache->bg; - if (!inBubble) { - cache->bg = QColor(0, 0, 0, 0); - } - Ui::Text::ValidateQuotePaintCache(*cache, quoteSt); - Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt); - if (backgroundEmoji) { - ValidateBackgroundEmoji( - backgroundEmojiId, - backgroundEmoji, - backgroundEmojiCache, - cache, - holder); - if (!backgroundEmojiCache->frames[0].isNull()) { - FillBackgroundEmoji(p, rect, hasQuote, *backgroundEmojiCache); - } - } - if (!inBubble) { - cache->bg = rippleColor; - } - - if (ripple.animation) { - ripple.animation->paint(p, x, y, w, &rippleColor); - if (ripple.animation->empty()) { - ripple.animation.reset(); - } - } - - const auto withPreviewLeft = st::messageQuoteStyle.outline - + st::historyReplyPreviewMargin.left() - + st::historyReplyPreview - + st::historyReplyPreviewMargin.right(); - auto textLeft = st::historyReplyPadding.left(); - const auto pausedSpoiler = context.paused - || On(PowerSaving::kChatSpoiler); - if (w > textLeft) { - if (resolvedMessage || resolvedStory || !_text.isEmpty()) { - const auto media = resolvedMessage ? resolvedMessage->media() : nullptr; - auto hasPreview = (media && media->hasReplyPreview()) - || (resolvedStory && resolvedStory->hasReplyPreview()); - if (hasPreview && w <= withPreviewLeft) { - hasPreview = false; - } - if (hasPreview) { - textLeft = withPreviewLeft; - const auto image = media - ? media->replyPreview() - : resolvedStory->replyPreview(); - if (image) { - auto to = style::rtlrect( - x + st::historyReplyPreviewMargin.left(), - y + st::historyReplyPreviewMargin.top(), - st::historyReplyPreview, - st::historyReplyPreview, - w + 2 * x); - const auto preview = image->pixSingle( - image->size() / style::DevicePixelRatio(), - { - .colored = (context.selected() - ? &st->msgStickerOverlay() - : nullptr), - .options = Images::Option::RoundSmall, - .outer = to.size(), - }); - p.drawPixmap(to.x(), to.y(), preview); - if (spoiler) { - holder->clearCustomEmojiRepaint(); - Ui::FillSpoilerRect( - p, - to, - Ui::DefaultImageSpoiler().frame( - spoiler->index( - context.now, - pausedSpoiler))); - } - } - } - if (w > textLeft + st::historyReplyPadding.right()) { - w -= textLeft + st::historyReplyPadding.right(); - p.setPen(!inBubble - ? st->msgImgReplyBarColor()->c - : useColorIndex - ? FromNameFg(context, colorIndexPlusOne - 1) - : stm->msgServiceFg->c); - _name.drawLeftElided(p, x + textLeft, y + st::historyReplyPadding.top(), w, w + 2 * x + 2 * textLeft); - if (originalVia && w > _name.maxWidth() + st::msgServiceFont->spacew) { - p.setFont(st::msgServiceFont); - p.drawText(x + textLeft + _name.maxWidth() + st::msgServiceFont->spacew, y + st::historyReplyPadding.top() + st::msgServiceFont->ascent, originalVia->text); - } - - p.setPen(inBubble - ? stm->historyTextFg - : st->msgImgReplyBarColor()); - holder->prepareCustomEmojiPaint(p, context, _text); - auto replyToTextPosition = QPoint( - x + textLeft, - y + st::historyReplyPadding.top() + st::msgServiceNameFont->height); - auto replyToTextPalette = &(!inBubble - ? st->imgReplyTextPalette() - : useColorIndex - ? st->coloredTextPalette(selected, colorIndexPlusOne - 1) - : stm->replyTextPalette); - if (_fields.storyId) { - st::dialogsMiniReplyStory.icon.icon.paint( - p, - replyToTextPosition, - w + 2 * x + 2 * textLeft, - replyToTextPalette->linkFg->c); - replyToTextPosition += QPoint( - st::dialogsMiniReplyStory.skipText - + st::dialogsMiniReplyStory.icon.icon.width(), - 0); - } - auto owned = std::optional(); - auto copy = std::optional(); - if (inBubble && colorIndexPlusOne) { - copy.emplace(*replyToTextPalette); - owned.emplace(cache->icon); - copy->linkFg = owned->color(); - replyToTextPalette = &*copy; - } - _text.draw(p, { - .position = replyToTextPosition, - .availableWidth = w, - .palette = replyToTextPalette, - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = context.now, - .pausedEmoji = (context.paused - || On(PowerSaving::kEmojiChat)), - .pausedSpoiler = pausedSpoiler, - .elisionOneLine = true, - }); - p.setTextPalette(stm->textPalette); - } - } else { - p.setFont(st::msgDateFont); - p.setPen(cache->icon); - p.drawTextLeft( - x + textLeft, - (y + (_height - st::msgDateFont->height) / 2), - w + 2 * x + 2 * textLeft, - st::msgDateFont->elided( - statePhrase(), - w - textLeft - st::historyReplyPadding.right())); - } - } -} - -void HistoryMessageReply::unloadPersistentAnimation() { - _text.unloadPersistentAnimation(); -} - -QString HistoryMessageReply::statePhrase() const { - return ((_fields.messageId || _fields.storyId) && !_unavailable) - ? tr::lng_profile_loading(tr::now) - : _fields.storyId - ? tr::lng_deleted_story(tr::now) - : tr::lng_deleted_message(tr::now); -} - void HistoryMessageReply::refreshReplyToMedia() { replyToDocumentId = 0; replyToWebPageId = 0; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index c250e91fb..3d363225f 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -22,9 +22,12 @@ namespace Ui { struct ChatPaintContext; class ChatStyle; struct PeerUserpicView; -class SpoilerAnimation; } // namespace Ui +namespace Ui::Text { +struct GeometryDescriptor; +} // namespace Ui::Text + namespace Data { class Session; class Story; @@ -230,7 +233,10 @@ private: }; struct ReplyFields { + ReplyFields clone(not_null parent) const; + TextWithEntities quote; + std::unique_ptr externalMedia; PeerId externalSenderId = 0; QString externalSenderName; QString externalPostAuthor; @@ -243,7 +249,7 @@ struct ReplyFields { }; [[nodiscard]] ReplyFields ReplyFieldsFromMTP( - not_null history, + not_null item, const MTPMessageReplyHeader &reply); [[nodiscard]] FullReplyTo ReplyToFromMTP( @@ -260,8 +266,6 @@ struct HistoryMessageReply HistoryMessageReply &operator=(HistoryMessageReply &&other); ~HistoryMessageReply(); - static constexpr auto kBarAlpha = 230. / 255.; - void set(ReplyFields fields); void updateFields( @@ -275,16 +279,8 @@ struct HistoryMessageReply void clearData(not_null holder); [[nodiscard]] bool external() const; - [[nodiscard]] PeerData *sender(not_null holder) const; - [[nodiscard]] QString senderName(not_null holder) const; - [[nodiscard]] QString senderName(not_null peer) const; - [[nodiscard]] bool isNameUpdated(not_null holder) const; - void updateName( - not_null holder, - std::optional resolvedSender = std::nullopt) const; - [[nodiscard]] int resizeToWidth(int width) const; - [[nodiscard]] int height() const; - [[nodiscard]] QMargins margins() const; + [[nodiscard]] bool displayAsExternal( + not_null holder) const; void itemRemoved( not_null holder, not_null removed); @@ -292,17 +288,7 @@ struct HistoryMessageReply not_null holder, not_null removed); - void paint( - Painter &p, - not_null holder, - const Ui::ChatPaintContext &context, - int x, - int y, - int w, - bool inBubble) const; - void unloadPersistentAnimation(); - - [[nodiscard]] ReplyFields fields() const { + [[nodiscard]] const ReplyFields &fields() const { return _fields; } [[nodiscard]] PeerId externalPeerId() const { @@ -317,21 +303,22 @@ struct HistoryMessageReply [[nodiscard]] MsgId topMessageId() const { return _fields.topMessageId; } - [[nodiscard]] int maxWidth() const { - return _maxWidth; - } - [[nodiscard]] ClickHandlerPtr link() const { - return _link; - } [[nodiscard]] bool topicPost() const { return _fields.topicPost; } [[nodiscard]] bool manualQuote() const { return _fields.manualQuote; } - [[nodiscard]] QString statePhrase() const; + [[nodiscard]] bool unavailable() const { + return _unavailable; + } + [[nodiscard]] bool displaying() const { + return _displaying; + } + [[nodiscard]] bool multiline() const { + return _multiline; + } - void setLinkFrom(not_null holder); void setTopMessageId(MsgId topMessageId); void refreshReplyToMedia(); @@ -340,25 +327,12 @@ struct HistoryMessageReply WebPageId replyToWebPageId = 0; ReplyToMessagePointer resolvedMessage; ReplyToStoryPointer resolvedStory; - std::unique_ptr originalVia; - std::unique_ptr spoiler; - - struct { - mutable std::unique_ptr animation; - QPoint lastPoint; - } ripple; private: ReplyFields _fields; - ClickHandlerPtr _link; - mutable Ui::Text::String _name; - mutable Ui::Text::String _text; - mutable PeerData *_externalSender = nullptr; - mutable int _maxWidth = 0; - mutable int _minHeight = 0; - mutable int _height = 0; - mutable int _nameVersion = 0; uint8 _unavailable : 1 = 0; + uint8 _displaying : 1 = 0; + uint8 _multiline : 1 = 0; }; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index c646a9b40..3c1ad01e6 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5391,8 +5391,7 @@ bool HistoryWidget::confirmSendingFiles( })); Window::ActivateWindow(controller()); - const auto shown = controller()->show(std::move(box)); - shown->setCloseByOutsideClick(false); + controller()->show(std::move(box)); return true; } @@ -7972,7 +7971,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { .now = now, .pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = pausedSpoiler, - .elisionOneLine = true, + .elisionLines = 1, }); } else { p.setFont(st::msgDateFont); 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 21ce63e1a..540d4ca5f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -629,7 +629,7 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { .now = crl::now(), .pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat), .pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler), - .elisionOneLine = true, + .elisionLines = 1, }); } 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 65a1cd6de..850ecfe6b 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -669,6 +669,7 @@ void DraftOptionsBox( }); } + const auto weak = Ui::MakeWeak(box); Settings::AddButton( bottom, tr::lng_reply_show_in_chat(), @@ -676,6 +677,9 @@ void DraftOptionsBox( { &st::menuIconShowInChat } )->setClickedCallback([=] { highlight(resolveReply()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } }); Settings::AddButton( 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 98799149f..85dadd5d8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -396,7 +396,7 @@ void ForwardPanel::paint( .now = now, .pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = pausedSpoiler, - .elisionOneLine = true, + .elisionLines = 1, }); } diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 88251cf7a..9b5334a5c 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" +#include "history/view/history_view_reply.h" #include "history/view/history_view_spoiler_click_handler.h" #include "history/history.h" #include "history/history_item.h" @@ -500,7 +501,8 @@ Element::Element( | Flag::NeedsResize | (IsItemScheduledUntilOnline(data) ? Flag::ScheduledUntilOnline - : Flag())) + : Flag()) + | (countIsTopicRootReply() ? Flag::TopicRootReply : Flag())) , _context(delegate->elementContext()) { history()->owner().registerItemView(this); refreshMedia(replacing); @@ -1262,15 +1264,6 @@ QSize Element::countCurrentSize(int newWidth) { return performCountCurrentSize(newWidth); } -void Element::refreshIsTopicRootReply() { - const auto topicRootReply = countIsTopicRootReply(); - if (topicRootReply) { - _flags |= Flag::TopicRootReply; - } else { - _flags &= ~Flag::TopicRootReply; - } -} - bool Element::countIsTopicRootReply() const { const auto item = data(); if (!item->history()->isForum()) { @@ -1365,6 +1358,10 @@ bool Element::hasFromName() const { return false; } +bool Element::displayReply() const { + return Has(); +} + bool Element::displayFromName() const { return false; } @@ -1422,10 +1419,6 @@ TimeId Element::displayedEditDate() const { return TimeId(0); } -HistoryMessageReply *Element::displayedReply() const { - return nullptr; -} - bool Element::toggleSelectionByHandlerClick( const ClickHandlerPtr &handler) const { return false; @@ -1485,7 +1478,7 @@ void Element::unloadHeavyPart() { if (_flags & Flag::HeavyCustomEmoji) { _flags &= ~Flag::HeavyCustomEmoji; _text.unloadPersistentAnimation(); - if (const auto reply = data()->Get()) { + if (const auto reply = Get()) { reply->unloadPersistentAnimation(); } } diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index db3933b98..a5e7a86a4 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -49,6 +49,7 @@ enum class InfoDisplayType : char; struct StateRequest; struct TextState; class Media; +class Reply; enum class Context : char { History, @@ -433,6 +434,7 @@ public: [[nodiscard]] virtual bool hasFromPhoto() const; [[nodiscard]] virtual bool displayFromPhoto() const; [[nodiscard]] virtual bool hasFromName() const; + [[nodiscard]] bool displayReply() const; [[nodiscard]] virtual bool displayFromName() const; [[nodiscard]] virtual TopicButton *displayedTopicButton() const; [[nodiscard]] virtual bool displayForwardedFrom() const; @@ -456,7 +458,6 @@ public: std::optional pressPoint) const; [[nodiscard]] virtual TimeId displayedEditDate() const; [[nodiscard]] virtual bool hasVisibleText() const; - [[nodiscard]] virtual HistoryMessageReply *displayedReply() const; virtual void applyGroupAdminChanges( const base::flat_set &changes) { } @@ -564,7 +565,6 @@ protected: void clearSpecialOnlyEmoji(); void checkSpecialOnlyEmoji(); - void refreshIsTopicRootReply(); private: // This should be called only from previousInBlocksChanged() diff --git a/Telegram/SourceFiles/history/view/history_view_item_preview.h b/Telegram/SourceFiles/history/view/history_view_item_preview.h index f2304c49e..b29da9442 100644 --- a/Telegram/SourceFiles/history/view/history_view_item_preview.h +++ b/Telegram/SourceFiles/history/view/history_view_item_preview.h @@ -40,6 +40,7 @@ struct ToPreviewOptions { const std::vector *existing = nullptr; bool hideSender = false; bool hideCaption = false; + bool ignoreMessageText = false; bool generateImages = true; bool ignoreGroup = false; bool ignoreTopic = true; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index f5a6d655d..c92b05715 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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. +#include "history/view/history_view_reply.h" #include "history/view/history_view_view_button.h" // ViewButton. #include "history/history.h" #include "boxes/share_box.h" @@ -406,6 +407,7 @@ Message::Message( Element *replacing) : Element(delegate, data, replacing, Flag(0)) , _invertMedia(data->invertMedia() && !data->emptyText()) +, _hideReply(delegate->elementHideReply(this)) , _bottomInfo( &data->history()->owner().reactions(), BottomInfoDataFromMessage(this)) { @@ -597,6 +599,14 @@ auto Message::takeReactionAnimations() QSize Message::performCountOptimalSize() { const auto item = data(); + + const auto replyData = item->Get(); + if (replyData && !_hideReply) { + AddComponents(Reply::Bit()); + } else { + RemoveComponents(Reply::Bit()); + } + const auto markup = item->inlineReplyMarkup(); const auto reactionsKey = [&] { return embedReactionsInBottomInfo() @@ -606,7 +616,6 @@ QSize Message::performCountOptimalSize() { : 2; }; const auto oldKey = reactionsKey(); - refreshIsTopicRootReply(); validateText(); validateInlineKeyboard(markup); updateViewButtonExistence(); @@ -633,17 +642,19 @@ QSize Message::performCountOptimalSize() { if (_reactions) { _reactions->initDimensions(); } + + const auto reply = Get(); + if (reply) { + reply->update(this, replyData); + } + if (drawBubble()) { const auto forwarded = item->Get(); - const auto reply = displayedReply(); const auto via = item->Get(); const auto entry = logEntryOriginal(); if (forwarded) { forwarded->create(via); } - if (reply) { - reply->updateName(item); - } auto mediaDisplayed = false; if (media) { @@ -672,6 +683,10 @@ QSize Message::performCountOptimalSize() { std::min(st::msgMaxWidth, reactionsMaxWidth)); if (!mediaDisplayed || _viewButton) { minHeight += st::mediaInBubbleSkip; + } else if (!media->additionalInfoString().isEmpty()) { + // In round videos in a web page status text is painted + // in the bottom left corner, reactions should be below. + minHeight += st::msgDateFont->height; } if (maxWidth >= reactionsMaxWidth) { minHeight += _reactions->minHeight(); @@ -771,13 +786,9 @@ QSize Message::performCountOptimalSize() { accumulate_max(maxWidth, namew); } if (reply) { - auto replyw = st::msgPadding.left() + const auto replyw = st::msgPadding.left() + reply->maxWidth() + st::msgPadding.right(); - if (reply->originalVia) { - replyw += st::msgServiceFont->spacew - + reply->originalVia->maxWidth; - } accumulate_max(maxWidth, replyw); } if (entry) { @@ -1221,9 +1232,11 @@ void Message::draw(Painter &p, const PaintContext &context) const { p.restore(); } - if (const auto reply = displayedReply()) { - if (reply->isNameUpdated(data())) { - const_cast(this)->setPendingResize(); + if (const auto reply = Get()) { + if (const auto replyData = item->Get()) { + if (reply->isNameUpdated(this, replyData)) { + const_cast(this)->setPendingResize(); + } } } } @@ -1602,8 +1615,15 @@ void Message::paintReplyInfo( Painter &p, QRect &trect, const PaintContext &context) const { - if (const auto reply = displayedReply()) { - reply->paint(p, this, context, trect.x(), trect.y(), trect.width(), true); + if (const auto reply = Get()) { + reply->paint( + p, + this, + context, + trect.x(), + trect.y(), + trect.width(), + true); trect.setY(trect.y() + reply->height()); } } @@ -1757,7 +1777,7 @@ void Message::clickHandlerPressedChanged( toggleTopicButtonRipple(pressed); } else if (_viewButton) { _viewButton->checkLink(handler, pressed); - } else if (const auto reply = displayedReply() + } else if (const auto reply = Get() ; reply && (handler == reply->link())) { toggleReplyRipple(pressed); } @@ -1800,32 +1820,24 @@ void Message::toggleRightActionRipple(bool pressed) { } void Message::toggleReplyRipple(bool pressed) { - const auto reply = displayedReply(); + const auto reply = Get(); if (!reply) { return; } if (pressed) { - if (!reply->ripple.animation && !unwrapped()) { + if (!unwrapped()) { const auto &padding = st::msgPadding; const auto geometry = countGeometry(); - const auto item = data(); const auto margins = reply->margins(); const auto size = QSize( geometry.width() - padding.left() - padding.right(), reply->height() - margins.top() - margins.bottom()); - reply->ripple.animation = std::make_unique( - st::defaultRippleAnimation, - Ui::RippleAnimation::RoundRectMask( - size, - st::messageQuoteStyle.radius), - [=] { item->history()->owner().requestItemRepaint(item); }); + reply->createRippleAnimation(this, size); } - if (reply->ripple.animation) { - reply->ripple.animation->add(reply->ripple.lastPoint); - } - } else if (reply->ripple.animation) { - reply->ripple.animation->lastStop(); + reply->addRipple(); + } else { + reply->stopLastRipple(); } } @@ -2486,7 +2498,7 @@ bool Message::getStateReplyInfo( QPoint point, QRect &trect, not_null outResult) const { - if (const auto reply = displayedReply()) { + if (const auto reply = Get()) { const auto margins = reply->margins(); const auto height = reply->height(); if (point.y() >= trect.top() && point.y() < trect.top() + height) { @@ -2498,7 +2510,7 @@ bool Message::getStateReplyInfo( if (g.contains(point)) { if (const auto link = reply->link()) { outResult->link = reply->link(); - reply->ripple.lastPoint = point - g.topLeft(); + reply->saveRipplePoint(point - g.topLeft()); } } return true; @@ -2581,7 +2593,7 @@ void Message::updatePressed(QPoint point) { auto fwdheight = ((forwarded->text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; trect.setTop(trect.top() + fwdheight); } - if (const auto reply = item->Get()) { + if (const auto reply = Get()) { trect.setTop(trect.top() + reply->height()); } if (const auto via = item->Get()) { @@ -3127,13 +3139,6 @@ WebPage *Message::logEntryOriginal() const { return nullptr; } -HistoryMessageReply *Message::displayedReply() const { - if (const auto reply = data()->Get()) { - return delegate()->elementHideReply(this) ? nullptr : reply; - } - return nullptr; -} - bool Message::toggleSelectionByHandlerClick( const ClickHandlerPtr &handler) const { if (_comments && _comments->link == handler) { @@ -3584,7 +3589,7 @@ void Message::updateMediaInBubbleState() { return displayFromName() || displayedTopicButton() || displayForwardedFrom() - || displayedReply() + || Has() || item->Has(); }; auto entry = logEntryOriginal(); @@ -3709,7 +3714,7 @@ QRect Message::innerGeometry() const { + st::topicButtonSkip); } // Skip displayForwardedFrom() until there are no animations for it. - if (const auto reply = displayedReply()) { + if (const auto reply = Get()) { // See paintReplyInfo(). result.translate(0, reply->height()); } @@ -3876,7 +3881,7 @@ int Message::resizeContentGetHeight(int newWidth) { textWidth - 2 * st::msgDateDelta.x())); if (bubble) { - auto reply = displayedReply(); + auto reply = Get(); auto via = item->Get(); auto entry = logEntryOriginal(); @@ -3966,7 +3971,6 @@ int Message::resizeContentGetHeight(int newWidth) { newHeight += reply->resizeToWidth(contentWidth - st::msgPadding.left() - st::msgPadding.right()); - reply->ripple.animation = nullptr; } if (needInfoDisplay()) { newHeight += (bottomInfoHeight - st::msgDateFont->height); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index aeba7c1aa..2b9112c1c 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -137,7 +137,6 @@ public: [[nodiscard]] ClickHandlerPtr rightActionLink( std::optional pressPoint) const override; [[nodiscard]] TimeId displayedEditDate() const override; - [[nodiscard]] HistoryMessageReply *displayedReply() const override; [[nodiscard]] bool toggleSelectionByHandlerClick( const ClickHandlerPtr &handler) const override; [[nodiscard]] bool allowTextSelectionByHandler( @@ -308,8 +307,9 @@ private: mutable std::unique_ptr _fromNameStatus; Ui::Text::String _rightBadge; mutable int _fromNameVersion = 0; - uint32 _bubbleWidthLimit : 31 = 0; + uint32 _bubbleWidthLimit : 30 = 0; uint32 _invertMedia : 1 = 0; + uint32 _hideReply : 1 = 0; BottomInfo _bottomInfo; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index 0472e4eb1..017289a0b 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -597,7 +597,11 @@ void PinnedWidget::listUpdateDateLink( } bool PinnedWidget::listElementHideReply(not_null view) { - return (view->data()->replyToId() == _thread->topicRootId()); + if (const auto reply = view->data()->Get()) { + return !reply->fields().manualQuote + && (reply->messageId() == _thread->topicRootId()); + } + return false; } bool PinnedWidget::listElementShownUnread(not_null view) { diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index c652ab2e8..ac23678b8 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -972,8 +972,7 @@ bool RepliesWidget::confirmSendingFiles( insertTextOnCancel)); //ActivateWindow(controller()); - const auto shown = controller()->show(std::move(box)); - shown->setCloseByOutsideClick(false); + controller()->show(std::move(box)); return true; } @@ -2020,7 +2019,7 @@ bool RepliesWidget::showMessage( } const auto id = FullMsgId(_history->peer->id, messageId); const auto message = _history->owner().message(id); - if (!message || !message->inThread(_rootId)) { + if (!message || (!message->inThread(_rootId) && id.msg != _rootId)) { return false; } const auto originMessage = [&]() -> HistoryItem* { @@ -2516,7 +2515,25 @@ void RepliesWidget::listUpdateDateLink( } bool RepliesWidget::listElementHideReply(not_null view) { - return (view->data()->replyToId() == _rootId); + if (const auto reply = view->data()->Get()) { + const auto replyToPeerId = reply->externalPeerId() + ? reply->externalPeerId() + : _history->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(); + if (forwarded + && forwarded->savedFromPeer + && forwarded->savedFromPeer->id == replyToPeerId + && forwarded->savedFromMsgId == reply->messageId()) { + return true; + } + } + } + return false; } bool RepliesWidget::listElementShownUnread(not_null view) { diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp new file mode 100644 index 000000000..6899a0832 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -0,0 +1,820 @@ +/* +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_reply.h" + +#include "core/click_handler_types.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_story.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_item_components.h" +#include "history/history_item_helpers.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_style.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/spoiler_mess.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" +#include "ui/painter.h" +#include "ui/power_saving.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_dialogs.h" + +namespace HistoryView { +namespace { + +constexpr auto kNonExpandedLinesLimit = 5; + +void ValidateBackgroundEmoji( + DocumentId backgroundEmojiId, + not_null data, + not_null cache, + not_null quote, + not_null view) { + if (data->firstFrameMask.isNull()) { + if (!cache->frames[0].isNull()) { + for (auto &frame : cache->frames) { + frame = QImage(); + } + } + const auto tag = Data::CustomEmojiSizeTag::Isolated; + if (!data->emoji) { + const auto owner = &view->history()->owner(); + const auto repaint = crl::guard(view, [=] { + view->history()->owner().requestViewRepaint(view); + }); + data->emoji = owner->customEmojiManager().create( + backgroundEmojiId, + repaint, + tag); + } + if (!data->emoji->ready()) { + return; + } + const auto size = Data::FrameSizeFromTag(tag); + data->firstFrameMask = QImage( + QSize(size, size), + QImage::Format_ARGB32_Premultiplied); + data->firstFrameMask.fill(Qt::transparent); + data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio()); + auto p = Painter(&data->firstFrameMask); + data->emoji->paint(p, { + .textColor = QColor(255, 255, 255), + .position = QPoint(0, 0), + .internal = { + .forceFirstFrame = true, + }, + }); + p.end(); + + data->emoji = nullptr; + } + if (!cache->frames[0].isNull() && cache->color == quote->icon) { + return; + } + cache->color = quote->icon; + const auto ratio = style::DevicePixelRatio(); + auto colorized = QImage( + data->firstFrameMask.size(), + QImage::Format_ARGB32_Premultiplied); + colorized.setDevicePixelRatio(ratio); + style::colorizeImage( + data->firstFrameMask, + cache->color, + &colorized, + QRect(), // src + QPoint(), // dst + true); // use alpha + const auto make = [&](int size) { + size = style::ConvertScale(size) * ratio; + auto result = colorized.scaled( + size, + size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + result.setDevicePixelRatio(ratio); + return result; + }; + + constexpr auto kSize1 = 12; + constexpr auto kSize2 = 16; + constexpr auto kSize3 = 20; + cache->frames[0] = make(kSize1); + cache->frames[1] = make(kSize2); + cache->frames[2] = make(kSize3); +} + +void FillBackgroundEmoji( + Painter &p, + const QRect &rect, + bool quote, + const Ui::BackgroundEmojiCache &cache) { + p.setClipRect(rect); + + const auto &frames = cache.frames; + const auto right = rect.x() + rect.width(); + const auto paint = [&](int x, int y, int index, float64 opacity) { + y = style::ConvertScale(y); + if (y >= rect.height()) { + return; + } + p.setOpacity(opacity); + p.drawImage( + right - style::ConvertScale(x + (quote ? 12 : 0)), + rect.y() + y, + frames[index]); + }; + + paint(28, 4, 2, 0.32); + paint(51, 15, 1, 0.32); + paint(64, -2, 0, 0.28); + paint(87, 11, 1, 0.24); + paint(125, -2, 2, 0.16); + + paint(28, 31, 1, 0.24); + paint(72, 33, 2, 0.2); + + paint(46, 52, 1, 0.24); + paint(24, 55, 2, 0.18); + + if (quote) { + paint(4, 23, 1, 0.28); + paint(0, 48, 0, 0.24); + } + + p.setClipping(false); + p.setOpacity(1.); +} + +} // namespace + +Reply::Reply() +: _name(st::maxSignatureSize / 2) +, _text(st::maxSignatureSize / 2) { +} + +Reply &Reply::operator=(Reply &&other) = default; + +Reply::~Reply() = default; + +void Reply::update( + not_null view, + not_null data) { + const auto item = view->data(); + const auto &fields = data->fields(); + const auto message = data->resolvedMessage.get(); + const auto story = data->resolvedStory.get(); + const auto externalMedia = fields.externalMedia.get(); + if (!_externalSender) { + if (const auto id = fields.externalSenderId) { + _externalSender = view->history()->owner().peer(id); + } + } + _colorPeer = message + ? message->displayFrom() + : story + ? story->peer().get() + : _externalSender + ? _externalSender + : nullptr; + _hiddenSenderColorIndexPlusOne = (!_colorPeer && message) + ? (message->hiddenSenderInfo()->colorIndex + 1) + : 0; + + const auto hasPreview = (story && story->hasReplyPreview()) + || (message + && message->media() + && message->media()->hasReplyPreview()) + || (externalMedia && externalMedia->hasReplyPreview()); + _hasPreview = hasPreview ? 1 : 0; + _displaying = data->displaying() ? 1 : 0; + _multiline = data->multiline() ? 1 : 0; + _replyToStory = (fields.storyId != 0); + const auto hasQuoteIcon = _displaying + && fields.manualQuote + && !fields.quote.empty(); + _hasQuoteIcon = hasQuoteIcon ? 1 : 0; + + const auto text = (!_displaying && data->unavailable()) + ? TextWithEntities() + : (message && (fields.quote.empty() || !fields.manualQuote)) + ? message->inReplyText() + : !fields.quote.empty() + ? fields.quote + : story + ? story->inReplyText() + : externalMedia + ? externalMedia->toPreview({ + .hideSender = true, + .hideCaption = true, + .ignoreMessageText = true, + .generateImages = false, + .ignoreGroup = true, + .ignoreTopic = true, + }).text + : TextWithEntities(); + const auto repaint = [=] { item->customEmojiRepaint(); }; + const auto context = Core::MarkedTextContext{ + .session = &view->history()->session(), + .customEmojiRepaint = repaint, + }; + _text.setMarkedText( + st::defaultTextStyle, + text, + _multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(), + context); + + updateName(view, data); + + if (_displaying) { + setLinkFrom(view, data); + const auto media = message ? message->media() : nullptr; + if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) { + _spoiler = nullptr; + } else if (!_spoiler) { + _spoiler = std::make_unique(repaint); + } + } else { + _spoiler = nullptr; + } +} + +bool Reply::expand() { + if (!_expandable || _expanded) { + return false; + } + _expanded = true; + return true; +} + +void Reply::setLinkFrom( + not_null view, + not_null data) { + const auto weak = base::make_weak(view); + const auto &fields = data->fields(); + const auto externalChannelId = peerToChannel(fields.externalPeerId); + const auto messageId = fields.messageId; + const auto quote = fields.manualQuote + ? fields.quote + : TextWithEntities(); + const auto returnToId = view->data()->fullId(); + const auto externalLink = [=](ClickContext context) { + const auto my = context.other.value(); + if (const auto controller = my.sessionWindow.get()) { + auto error = QString(); + const auto owner = &controller->session().data(); + if (const auto view = weak.get()) { + if (const auto reply = view->Get()) { + if (reply->expand()) { + owner->requestViewResize(view); + return; + } + } + } + if (externalChannelId) { + const auto channel = owner->channel(externalChannelId); + if (!channel->isForbidden()) { + if (messageId) { + JumpToMessageClickHandler( + channel, + messageId, + returnToId, + quote + )->onClick(context); + } else { + controller->showPeerInfo(channel); + } + } else if (channel->isBroadcast()) { + error = tr::lng_channel_not_accessible(tr::now); + } else { + error = tr::lng_group_not_accessible(tr::now); + } + } else { + error = tr::lng_reply_from_private_chat(tr::now); + } + if (!error.isEmpty()) { + controller->showToast(error); + } + } + }; + const auto message = data->resolvedMessage.get(); + const auto story = data->resolvedStory.get(); + _link = message + ? JumpToMessageClickHandler(message, returnToId, quote) + : story + ? JumpToStoryClickHandler(story) + : (data->external() + && (!fields.messageId + || (data->unavailable() && externalChannelId))) + ? std::make_shared(externalLink) + : nullptr; +} + +PeerData *Reply::sender( + not_null view, + not_null data) const { + const auto message = data->resolvedMessage.get(); + if (const auto story = data->resolvedStory.get()) { + return story->peer(); + } else if (!message) { + return _externalSender; + } else if (view->data()->Has()) { + // Forward of a reply. Show reply-to original sender. + const auto forwarded = message->Get(); + if (forwarded) { + return forwarded->originalSender; + } + } + if (const auto from = message->displayFrom()) { + return from; + } + return message->author().get(); +} + +QString Reply::senderName( + not_null view, + not_null data, + bool shorten) const { + if (const auto peer = sender(view, data)) { + return senderName(peer, shorten); + } else if (!data->resolvedMessage) { + return data->fields().externalSenderName; + } else if (view->data()->Has()) { + // Forward of a reply. Show reply-to original sender. + const auto forwarded + = data->resolvedMessage->Get(); + if (forwarded) { + Assert(forwarded->hiddenSenderInfo != nullptr); + return forwarded->hiddenSenderInfo->name; + } + } + return QString(); +} + +QString Reply::senderName( + not_null peer, + bool shorten) const { + const auto user = shorten ? peer->asUser() : nullptr; + return user ? user->firstName : peer->name(); +} + +bool Reply::isNameUpdated( + not_null view, + not_null data) const { + if (const auto from = sender(view, data)) { + if (_nameVersion < from->nameVersion()) { + updateName(view, data, from); + return true; + } + } + return false; +} + +void Reply::updateName( + not_null view, + not_null data, + std::optional resolvedSender) const { + auto viaBotUsername = QString(); + const auto message = data->resolvedMessage.get(); + if (message && !message->Has()) { + if (const auto bot = message->viaBot()) { + viaBotUsername = bot->username(); + } + } + const auto &fields = data->fields(); + const auto sender = resolvedSender.value_or(this->sender(view, data)); + const auto externalPeer = fields.externalPeerId + ? view->history()->owner().peer(fields.externalPeerId).get() + : nullptr; + const auto displayAsExternal = data->displayAsExternal(view->data()); + const auto groupNameAdded = displayAsExternal + && externalPeer + && (externalPeer != sender) + && (externalPeer->isChat() || externalPeer->isMegagroup()); + const auto shorten = !viaBotUsername.isEmpty() || groupNameAdded; + const auto name = sender + ? senderName(sender, shorten) + : senderName(view, data, shorten); + const auto previewSkip = _hasPreview + ? (st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right() + - st::historyReplyPadding.left()) + : 0; + const auto peerIcon = [](PeerData *peer) { + using namespace std; + return !peer + ? pair(&st::historyReplyUser, st::historyReplyUserPadding) + : peer->isBroadcast() + ? pair(&st::historyReplyChannel, st::historyReplyChannelPadding) + : (peer->isChannel() || peer->isChat()) + ? pair(&st::historyReplyGroup, st::historyReplyGroupPadding) + : pair(&st::historyReplyUser, st::historyReplyUserPadding); + }; + const auto peerEmoji = [&](PeerData *peer) { + const auto owner = &view->history()->owner(); + const auto icon = peerIcon(peer); + return Ui::Text::SingleCustomEmoji( + owner->customEmojiManager().registerInternalEmoji( + *icon.first, + icon.second)); + }; + auto nameFull = TextWithEntities(); + if (displayAsExternal && !groupNameAdded && !fields.storyId) { + nameFull.append(peerEmoji(sender)); + } + nameFull.append(name); + if (groupNameAdded) { + nameFull.append(' ').append(peerEmoji(externalPeer)); + nameFull.append(externalPeer->name()); + } + if (!viaBotUsername.isEmpty()) { + nameFull.append(u" @"_q).append(viaBotUsername); + } + const auto context = Core::MarkedTextContext{ + .session = &view->history()->session(), + .customEmojiRepaint = [] {}, + .customEmojiLoopLimit = 1, + }; + _name.setMarkedText( + st::fwdTextStyle, + nameFull, + Ui::NameTextOptions(), + context); + if (sender) { + _nameVersion = sender->nameVersion(); + } + const auto nameMaxWidth = previewSkip + + _name.maxWidth() + + (_hasQuoteIcon + ? st::messageTextStyle.blockquote.icon.width() + : 0); + const auto storySkip = fields.storyId + ? (st::dialogsMiniReplyStory.skipText + + st::dialogsMiniReplyStory.icon.icon.width()) + : 0; + const auto optimalTextSize = _multiline + ? countMultilineOptimalSize(previewSkip) + : QSize( + (previewSkip + + storySkip + + std::min(_text.maxWidth(), st::maxSignatureSize)), + st::normalFont->height); + _maxWidth = std::max(nameMaxWidth, optimalTextSize.width()); + if (!data->displaying()) { + const auto unavailable = data->unavailable(); + _stateText = ((fields.messageId || fields.storyId) && !unavailable) + ? tr::lng_profile_loading(tr::now) + : fields.storyId + ? tr::lng_deleted_story(tr::now) + : tr::lng_deleted_message(tr::now); + const auto phraseWidth = st::msgDateFont->width(_stateText); + _maxWidth = unavailable + ? phraseWidth + : std::max(_maxWidth, phraseWidth); + } else { + _stateText = QString(); + } + _maxWidth = st::historyReplyPadding.left() + + _maxWidth + + st::historyReplyPadding.right(); + _minHeight = st::historyReplyPadding.top() + + st::msgServiceNameFont->height + + optimalTextSize.height() + + st::historyReplyPadding.bottom(); +} + +int Reply::resizeToWidth(int width) const { + _ripple.animation = nullptr; + + const auto previewSkip = _hasPreview + ? (st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right() + - st::historyReplyPadding.left()) + : 0; + if (width >= _maxWidth || !_multiline) { + _nameTwoLines = 0; + _expandable = _minHeightExpandable; + _height = _minHeight; + return height(); + } + const auto innerw = width + - st::historyReplyPadding.left() + - st::historyReplyPadding.right(); + const auto namew = innerw - previewSkip; + const auto desiredNameHeight = _name.countHeight(namew); + _nameTwoLines = (desiredNameHeight > st::semiboldFont->height) ? 1 : 0; + const auto nameh = (_nameTwoLines ? 2 : 1) * st::semiboldFont->height; + const auto firstLineSkip = _nameTwoLines ? 0 : previewSkip; + auto elided = false; + const auto texth = _text.countDimensions( + textGeometry(innerw, firstLineSkip, &elided)).height; + _expandable = elided ? 1 : 0; + _height = st::historyReplyPadding.top() + + nameh + + std::max(texth, st::normalFont->height) + + st::historyReplyPadding.bottom(); + return height(); +} + +Ui::Text::GeometryDescriptor Reply::textGeometry( + int available, + int firstLineSkip, + bool *outElided) const { + return { .layout = [=](int line) { + const auto skip = (line ? 0 : firstLineSkip); + const auto elided = !_multiline + || (!_expanded && (line + 1 >= kNonExpandedLinesLimit)); + return Ui::Text::LineGeometry{ + .left = skip, + .width = available - skip, + .elided = elided, + }; + }, .outElided = outElided }; +} + +int Reply::height() const { + return _height + st::historyReplyTop + st::historyReplyBottom; +} + +QMargins Reply::margins() const { + return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom); +} + +QSize Reply::countMultilineOptimalSize( + int previewSkip) const { + auto elided = false; + const auto max = previewSkip + _text.maxWidth(); + const auto result = _text.countDimensions( + textGeometry(max, previewSkip, &elided)); + _minHeightExpandable = elided ? 1 : 0; + return { + result.width, + std::max(result.height, st::normalFont->height), + }; +} + +void Reply::paint( + Painter &p, + not_null view, + const Ui::ChatPaintContext &context, + int x, + int y, + int w, + bool inBubble) const { + const auto st = context.st; + const auto stm = context.messageStyle(); + + y += st::historyReplyTop; + const auto rect = QRect(x, y, w, _height); + const auto selected = context.selected(); + const auto backgroundEmojiId = _colorPeer + ? _colorPeer->backgroundEmojiId() + : DocumentId(); + const auto colorIndexPlusOne = _colorPeer + ? (_colorPeer->colorIndex() + 1) + : _hiddenSenderColorIndexPlusOne; + const auto useColorIndex = colorIndexPlusOne && !context.outbg; + const auto colorPattern = colorIndexPlusOne + ? st->colorPatternIndex(colorIndexPlusOne - 1) + : 0; + const auto cache = !inBubble + ? (_hasQuoteIcon + ? st->serviceQuoteCache(colorPattern) + : st->serviceReplyCache(colorPattern)).get() + : useColorIndex + ? (_hasQuoteIcon + ? st->coloredQuoteCache(selected, colorIndexPlusOne - 1) + : st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get() + : (_hasQuoteIcon + ? stm->quoteCache[colorPattern] + : stm->replyCache[colorPattern]).get(); + const auto "eSt = _hasQuoteIcon + ? st::messageTextStyle.blockquote + : st::messageQuoteStyle; + const auto backgroundEmoji = backgroundEmojiId + ? st->backgroundEmojiData(backgroundEmojiId).get() + : nullptr; + const auto backgroundEmojiCache = backgroundEmoji + ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex( + selected, + context.outbg, + inBubble, + colorIndexPlusOne)] + : nullptr; + const auto rippleColor = cache->bg; + if (!inBubble) { + cache->bg = QColor(0, 0, 0, 0); + } + Ui::Text::ValidateQuotePaintCache(*cache, quoteSt); + Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt); + if (backgroundEmoji) { + ValidateBackgroundEmoji( + backgroundEmojiId, + backgroundEmoji, + backgroundEmojiCache, + cache, + view); + if (!backgroundEmojiCache->frames[0].isNull()) { + FillBackgroundEmoji(p, rect, _hasQuoteIcon, *backgroundEmojiCache); + } + } + if (!inBubble) { + cache->bg = rippleColor; + } + + if (_ripple.animation) { + _ripple.animation->paint(p, x, y, w, &rippleColor); + if (_ripple.animation->empty()) { + _ripple.animation.reset(); + } + } + + auto hasPreview = (_hasPreview != 0); + auto previewSkip = hasPreview + ? (st::messageQuoteStyle.outline + + st::historyReplyPreviewMargin.left() + + st::historyReplyPreview + + st::historyReplyPreviewMargin.right() + - st::historyReplyPadding.left()) + : 0; + if (hasPreview && w <= st::historyReplyPadding.left() + previewSkip) { + hasPreview = false; + previewSkip = 0; + } + + const auto pausedSpoiler = context.paused + || On(PowerSaving::kChatSpoiler); + auto textLeft = x + st::historyReplyPadding.left(); + auto textTop = y + + st::historyReplyPadding.top() + + (st::msgServiceNameFont->height * (_nameTwoLines ? 2 : 1)); + if (w > st::historyReplyPadding.left()) { + if (_displaying) { + if (hasPreview) { + const auto data = view->data()->Get(); + const auto message = data + ? data->resolvedMessage.get() + : nullptr; + const auto media = message ? message->media() : nullptr; + const auto image = media + ? media->replyPreview() + : !data + ? nullptr + : data->resolvedStory + ? data->resolvedStory->replyPreview() + : data->fields().externalMedia + ? data->fields().externalMedia->replyPreview() + : nullptr; + if (image) { + auto to = style::rtlrect( + x + st::historyReplyPreviewMargin.left(), + y + st::historyReplyPreviewMargin.top(), + st::historyReplyPreview, + st::historyReplyPreview, + w + 2 * x); + const auto preview = image->pixSingle( + image->size() / style::DevicePixelRatio(), + { + .colored = (context.selected() + ? &st->msgStickerOverlay() + : nullptr), + .options = Images::Option::RoundSmall, + .outer = to.size(), + }); + p.drawPixmap(to.x(), to.y(), preview); + if (_spoiler) { + view->clearCustomEmojiRepaint(); + Ui::FillSpoilerRect( + p, + to, + Ui::DefaultImageSpoiler().frame( + _spoiler->index( + context.now, + pausedSpoiler))); + } + } + } + const auto textw = w + - st::historyReplyPadding.left() + - st::historyReplyPadding.right(); + const auto namew = textw - previewSkip; + auto firstLineSkip = _nameTwoLines ? 0 : previewSkip; + if (namew > 0) { + p.setPen(!inBubble + ? st->msgImgReplyBarColor()->c + : useColorIndex + ? FromNameFg(context, colorIndexPlusOne - 1) + : stm->msgServiceFg->c); + _name.drawLeftElided( + p, + x + st::historyReplyPadding.left() + previewSkip, + y + st::historyReplyPadding.top(), + namew, + w + 2 * x, + _nameTwoLines ? 2 : 1); + + p.setPen(inBubble + ? stm->historyTextFg + : st->msgImgReplyBarColor()); + view->prepareCustomEmojiPaint(p, context, _text); + auto replyToTextPalette = &(!inBubble + ? st->imgReplyTextPalette() + : useColorIndex + ? st->coloredTextPalette(selected, colorIndexPlusOne - 1) + : stm->replyTextPalette); + if (_replyToStory) { + st::dialogsMiniReplyStory.icon.icon.paint( + p, + textLeft + firstLineSkip, + textTop, + w + 2 * x, + replyToTextPalette->linkFg->c); + firstLineSkip += st::dialogsMiniReplyStory.skipText + + st::dialogsMiniReplyStory.icon.icon.width(); + } + auto owned = std::optional(); + auto copy = std::optional(); + if (inBubble && colorIndexPlusOne) { + copy.emplace(*replyToTextPalette); + owned.emplace(cache->icon); + copy->linkFg = owned->color(); + replyToTextPalette = &*copy; + } + _text.draw(p, { + .position = { textLeft, textTop }, + .geometry = textGeometry(textw, firstLineSkip), + .palette = replyToTextPalette, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = context.now, + .pausedEmoji = (context.paused + || On(PowerSaving::kEmojiChat)), + .pausedSpoiler = pausedSpoiler, + .elisionLines = 1, + }); + p.setTextPalette(stm->textPalette); + } + } else { + p.setFont(st::msgDateFont); + p.setPen(cache->icon); + p.drawTextLeft( + textLeft, + (y + + st::historyReplyPadding.top() + + (st::msgDateFont->height / 2)), + w + 2 * x, + st::msgDateFont->elided( + _stateText, + x + w - textLeft - st::historyReplyPadding.right())); + } + } +} + +void Reply::createRippleAnimation( + not_null view, + QSize size) { + _ripple.animation = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask( + size, + st::messageQuoteStyle.radius), + [=] { view->history()->owner().requestViewRepaint(view); }); +} + +void Reply::saveRipplePoint(QPoint point) const { + _ripple.lastPoint = point; +} + +void Reply::addRipple() { + if (_ripple.animation) { + _ripple.animation->add(_ripple.lastPoint); + } +} + +void Reply::stopLastRipple() { + if (_ripple.animation) { + _ripple.animation->lastStop(); + } +} + +void Reply::unloadPersistentAnimation() { + _text.unloadPersistentAnimation(); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_reply.h b/Telegram/SourceFiles/history/view/history_view_reply.h new file mode 100644 index 000000000..5322d5c09 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_reply.h @@ -0,0 +1,116 @@ +/* +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/history_view_element.h" + +namespace Ui { +class SpoilerAnimation; +} // namespace Ui + +namespace HistoryView { + +class Reply final : public RuntimeComponent { +public: + Reply(); + Reply(const Reply &other) = delete; + Reply(Reply &&other) = delete; + Reply &operator=(const Reply &other) = delete; + Reply &operator=(Reply &&other); + ~Reply(); + + void update( + not_null view, + not_null data); + + [[nodiscard]] bool isNameUpdated( + not_null view, + not_null data) const; + void updateName( + not_null view, + not_null data, + std::optional resolvedSender = std::nullopt) const; + [[nodiscard]] int resizeToWidth(int width) const; + [[nodiscard]] int height() const; + [[nodiscard]] QMargins margins() const; + + bool expand(); + + void paint( + Painter &p, + not_null view, + const Ui::ChatPaintContext &context, + int x, + int y, + int w, + bool inBubble) const; + void unloadPersistentAnimation(); + + void createRippleAnimation(not_null view, QSize size); + void saveRipplePoint(QPoint point) const; + void addRipple(); + void stopLastRipple(); + + [[nodiscard]] int maxWidth() const { + return _maxWidth; + } + [[nodiscard]] ClickHandlerPtr link() const { + return _link; + } + +private: + [[nodiscard]] Ui::Text::GeometryDescriptor textGeometry( + int available, + int firstLineSkip, + bool *outElided = nullptr) const; + [[nodiscard]] QSize countMultilineOptimalSize( + int firstLineSkip) const; + void setLinkFrom( + not_null view, + not_null data); + + [[nodiscard]] PeerData *sender( + not_null view, + not_null data) const; + [[nodiscard]] QString senderName( + not_null view, + not_null data, + bool shorten) const; + [[nodiscard]] QString senderName( + not_null peer, + bool shorten) const; + + ClickHandlerPtr _link; + std::unique_ptr _spoiler; + mutable PeerData *_externalSender = nullptr; + mutable PeerData *_colorPeer = nullptr; + mutable struct { + mutable std::unique_ptr animation; + QPoint lastPoint; + } _ripple; + mutable Ui::Text::String _name; + mutable Ui::Text::String _text; + mutable QString _stateText; + mutable int _maxWidth = 0; + mutable int _minHeight = 0; + mutable int _height = 0; + mutable int _nameVersion = 0; + uint8 _hiddenSenderColorIndexPlusOne : 7 = 0; + uint8 _hasQuoteIcon : 1 = 0; + uint8 _replyToStory : 1 = 0; + uint8 _expanded : 1 = 0; + mutable uint8 _expandable : 1 = 0; + mutable uint8 _minHeightExpandable : 1 = 0; + mutable uint8 _nameTwoLines : 1 = 0; + mutable uint8 _hasPreview : 1 = 0; + mutable uint8 _displaying : 1 = 0; + mutable uint8 _multiline : 1 = 0; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 3f9314c40..02570767e 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -429,8 +429,7 @@ bool ScheduledWidget::confirmSendingFiles( insertTextOnCancel)); //ActivateWindow(controller()); - const auto shown = controller()->show(std::move(box)); - shown->setCloseByOutsideClick(false); + controller()->show(std::move(box)); return true; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp index a82fd13fd..140fdd86a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_extended_preview.cpp @@ -419,7 +419,7 @@ bool ExtendedPreview::needsBubble() const { && (item->repliesAreComments() || item->externalReply() || item->viaBot() - || _parent->displayedReply() + || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index 340b1fa66..128797f8f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_reply.h" #include "history/view/history_view_transcribe_button.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_spoiler.h" @@ -217,12 +218,12 @@ QSize Gif::countOptimalSize() { } else if (isUnwrapped()) { const auto item = _parent->data(); auto via = item->Get(); - auto reply = _parent->displayedReply(); + auto reply = _parent->Get(); auto forwarded = item->Get(); if (forwarded) { forwarded->create(via); } - maxWidth += additionalWidth(via, reply, forwarded); + maxWidth += additionalWidth(reply, via, forwarded); accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); } return { maxWidth, minHeight }; @@ -274,10 +275,10 @@ QSize Gif::countCurrentSize(int newWidth) { const auto item = _parent->data(); auto via = item->Get(); - auto reply = _parent->displayedReply(); + auto reply = _parent->Get(); auto forwarded = item->Get(); if (via || reply || forwarded) { - auto additional = additionalWidth(via, reply, forwarded); + auto additional = additionalWidth(reply, via, forwarded); newWidth += additional; accumulate_min(newWidth, availableWidth); auto usew = maxWidth() - additional; @@ -385,13 +386,13 @@ void Gif::draw(Painter &p, const PaintContext &context) const { auto usex = 0, usew = paintw; const auto unwrapped = isUnwrapped(); const auto via = unwrapped ? item->Get() : nullptr; - const auto reply = unwrapped ? _parent->displayedReply() : nullptr; + const auto reply = unwrapped ? _parent->Get() : nullptr; const auto forwarded = unwrapped ? item->Get() : nullptr; const auto rightAligned = unwrapped && outbg && !_parent->delegate()->elementIsChatWide(); if (via || reply || forwarded) { - usew = maxWidth() - additionalWidth(via, reply, forwarded); + usew = maxWidth() - additionalWidth(reply, via, forwarded); if (rightAligned) { usex = width() - usew; } @@ -623,7 +624,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const { == PaintContext::SkipDrawingParts::Surrounding; if (!unwrapped && !skipDrawingSurrounding) { - drawCornerStatus(p, context, QPoint()); + if (!isRound || !inWebPage) { + drawCornerStatus(p, context, QPoint()); + } } else if (!skipDrawingSurrounding) { if (isRound) { const auto mediaUnread = item->hasUnreadMediaFlag(); @@ -1013,13 +1016,13 @@ TextState Gif::textState(QPoint point, StateRequest request) const { const auto item = _parent->data(); auto usew = paintw, usex = 0; const auto via = unwrapped ? item->Get() : nullptr; - const auto reply = unwrapped ? _parent->displayedReply() : nullptr; + const auto reply = unwrapped ? _parent->Get() : nullptr; const auto forwarded = unwrapped ? item->Get() : nullptr; const auto rightAligned = unwrapped && outbg && !_parent->delegate()->elementIsChatWide(); if (via || reply || forwarded) { - usew = maxWidth() - additionalWidth(via, reply, forwarded); + usew = maxWidth() - additionalWidth(reply, via, forwarded); if (rightAligned) { usex = width() - usew; } @@ -1094,15 +1097,8 @@ TextState Gif::textState(QPoint point, StateRequest request) const { const auto replyRect = QRect(rectx, recty, rectw, recth); if (replyRect.contains(point)) { result.link = reply->link(); - reply->ripple.lastPoint = point - replyRect.topLeft(); - if (!reply->ripple.animation) { - reply->ripple.animation = std::make_unique( - st::defaultRippleAnimation, - Ui::RippleAnimation::RoundRectMask( - replyRect.size(), - st::messageQuoteStyle.radius), - [=] { item->history()->owner().requestItemRepaint(item); }); - } + reply->saveRipplePoint(point - replyRect.topLeft()); + reply->createRippleAnimation(_parent, replyRect.size()); return result; } } @@ -1520,7 +1516,7 @@ bool Gif::needsBubble() const { return item->repliesAreComments() || item->externalReply() || item->viaBot() - || _parent->displayedReply() + || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton(); @@ -1542,10 +1538,10 @@ QRect Gif::contentRectForReactions() const { && !_parent->delegate()->elementIsChatWide(); const auto item = _parent->data(); const auto via = item->Get(); - const auto reply = _parent->displayedReply(); + const auto reply = _parent->Get(); const auto forwarded = item->Get(); if (via || reply || forwarded) { - usew = maxWidth() - additionalWidth(via, reply, forwarded); + usew = maxWidth() - additionalWidth(reply, via, forwarded); } accumulate_max(usew, _parent->reactionsOptimalWidth()); if (rightAligned) { @@ -1602,8 +1598,8 @@ QPoint Gif::resolveCustomInfoRightBottom() const { int Gif::additionalWidth() const { const auto item = _parent->data(); return additionalWidth( + _parent->Get(), item->Get(), - item->Get(), item->Get()); } @@ -1763,7 +1759,10 @@ void Gif::refreshCaption() { _caption = createCaption(_parent->data()); } -int Gif::additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const { +int Gif::additionalWidth( + const Reply *reply, + const HistoryMessageVia *via, + const HistoryMessageForwarded *forwarded) const { int result = 0; if (forwarded) { accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index eac33cc7a..f20549771 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -37,6 +37,7 @@ enum class Error; namespace HistoryView { +class Reply; class TranscribeButton; class Gif final : public File { @@ -176,8 +177,8 @@ private: [[nodiscard]] bool needInfoDisplay() const; [[nodiscard]] bool needCornerStatusDisplay() const; [[nodiscard]] int additionalWidth( + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const; [[nodiscard]] int additionalWidth() const; [[nodiscard]] bool isUnwrapped() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index dceb0cd1d..350ba3668 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -388,7 +388,7 @@ void Giveaway::paintChannels( .align = style::al_left, .palette = &stm->textPalette, .now = context.now, - .elisionOneLine = true, + .elisionLines = 1, .elisionBreakEverywhere = true, }); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.cpp b/Telegram/SourceFiles/history/view/media/history_view_location.cpp index ab75f2051..293849ba3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_location.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_location.cpp @@ -375,7 +375,7 @@ bool Location::needsBubble() const { return item->repliesAreComments() || item->externalReply() || item->viaBot() - || _parent->displayedReply() + || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index bea617b23..d0133aa76 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -869,7 +869,7 @@ bool GroupedMedia::computeNeedBubble() const { if (item->repliesAreComments() || item->externalReply() || item->viaBot() - || _parent->displayedReply() + || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton() diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index 69f4875e1..e61202239 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_sticker.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" +#include "history/view/history_view_reply.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "lottie/lottie_single_player.h" @@ -54,13 +55,13 @@ QSize UnwrappedMedia::countOptimalSize() { if (_parent->media() == this) { const auto item = _parent->data(); const auto via = item->Get(); - const auto reply = _parent->displayedReply(); + const auto reply = _parent->Get(); const auto topic = _parent->displayedTopicButton(); const auto forwarded = getDisplayedForwardedInfo(); if (forwarded) { forwarded->create(via); } - maxWidth += additionalWidth(topic, via, reply, forwarded); + maxWidth += additionalWidth(topic, reply, via, forwarded); accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); if (const auto size = _parent->rightActionSize()) { minHeight = std::max( @@ -93,11 +94,11 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) { accumulate_max(newWidth, _parent->reactionsOptimalWidth()); _topAdded = 0; const auto via = item->Get(); - const auto reply = _parent->displayedReply(); + const auto reply = _parent->Get(); const auto topic = _parent->displayedTopicButton(); const auto forwarded = getDisplayedForwardedInfo(); if (topic || via || reply || forwarded) { - const auto additional = additionalWidth(topic, via, reply, forwarded); + const auto additional = additionalWidth(topic, reply, via, forwarded); const auto optimalw = maxWidth() - additional; const auto additionalMinWidth = std::min(additional, st::msgReplyPadding.left() + st::msgMinWidth / 2); _additionalOnTop = (optimalw + additionalMinWidth) > newWidth; @@ -107,7 +108,7 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) { if (reply) { [[maybe_unused]] auto h = reply->resizeToWidth(surroundingWidth); } - const auto surrounding = surroundingInfo(topic, via, reply, forwarded, surroundingWidth); + const auto surrounding = surroundingInfo(topic, reply, via, forwarded, surroundingWidth); if (_additionalOnTop) { _topAdded = surrounding.height + st::msgMargin.bottom(); newHeight += _topAdded; @@ -166,17 +167,17 @@ void UnwrappedMedia::draw(Painter &p, const PaintContext &context) const { if (!inWebPage && (context.skipDrawingParts != PaintContext::SkipDrawingParts::Surrounding)) { const auto via = inWebPage ? nullptr : item->Get(); - const auto reply = inWebPage ? nullptr : _parent->displayedReply(); + const auto reply = inWebPage ? nullptr : _parent->Get(); const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton(); const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo(); - drawSurrounding(p, inner, context, topic, via, reply, forwarded); + drawSurrounding(p, inner, context, topic, reply, via, forwarded); } } UnwrappedMedia::SurroundingInfo UnwrappedMedia::surroundingInfo( const TopicButton *topic, + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded, int outerw) const { if (!topic && !via && !reply && !forwarded) { @@ -242,8 +243,8 @@ void UnwrappedMedia::drawSurrounding( const QRect &inner, const PaintContext &context, const TopicButton *topic, + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const { const auto st = context.st; const auto sti = context.imageStyle(); @@ -263,9 +264,9 @@ void UnwrappedMedia::drawSurrounding( } auto replyRight = 0; auto rectw = _additionalOnTop - ? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, via, reply, forwarded)) + ? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, reply, via, forwarded)) : (width() - inner.width() - st::msgReplyPadding.left()); - if (const auto surrounding = surroundingInfo(topic, via, reply, forwarded, rectw)) { + if (const auto surrounding = surroundingInfo(topic, reply, via, forwarded, rectw)) { auto recth = surrounding.panelHeight; if (!surrounding.topicSize.isEmpty()) { auto rectw = surrounding.topicSize.width(); @@ -416,14 +417,14 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const { if (_parent->media() == this) { const auto via = inWebPage ? nullptr : item->Get(); - const auto reply = inWebPage ? nullptr : _parent->displayedReply(); + const auto reply = inWebPage ? nullptr : _parent->Get(); const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton(); const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo(); auto replyRight = 0; auto rectw = _additionalOnTop - ? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, via, reply, forwarded)) + ? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, reply, via, forwarded)) : (width() - inner.width() - st::msgReplyPadding.left()); - if (const auto surrounding = surroundingInfo(topic, via, reply, forwarded, rectw)) { + if (const auto surrounding = surroundingInfo(topic, reply, via, forwarded, rectw)) { auto recth = surrounding.panelHeight; if (!surrounding.topicSize.isEmpty()) { auto rectw = surrounding.topicSize.width(); @@ -486,16 +487,8 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const { const auto replyRect = QRect(rectx, recty, rectw, recth); if (replyRect.contains(point)) { result.link = reply->link(); - reply->ripple.lastPoint = point - replyRect.topLeft(); - if (!reply->ripple.animation) { - reply->ripple.animation = std::make_unique( - st::defaultRippleAnimation, - Ui::RippleAnimation::RoundRectMask( - replyRect.size(), - st::messageQuoteStyle.radius), - [=] { item->history()->owner().requestItemRepaint(item); }); - } - return result; + reply->saveRipplePoint(point - replyRect.topLeft()); + reply->createRippleAnimation(_parent, replyRect.size()); } } replyRight = rectx + rectw - st::msgReplyPadding.right(); @@ -542,7 +535,7 @@ bool UnwrappedMedia::hasTextForCopy() const { } bool UnwrappedMedia::dragItemByHandler(const ClickHandlerPtr &p) const { - const auto reply = _parent->displayedReply(); + const auto reply = _parent->Get(); return !reply || (reply->link() != p); } @@ -649,8 +642,8 @@ bool UnwrappedMedia::needInfoDisplay() const { int UnwrappedMedia::additionalWidth( const TopicButton *topic, + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const { auto result = st::msgReplyPadding.left() + _parent->infoWidth() + 2 * st::msgDateImgPadding.x(); if (topic) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h index 9932c0521..1cd71c86a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.h @@ -17,6 +17,7 @@ struct HistoryMessageForwarded; namespace HistoryView { +class Reply; struct TopicButton; class UnwrappedMedia final : public Media { @@ -120,8 +121,8 @@ private: }; [[nodiscard]] SurroundingInfo surroundingInfo( const TopicButton *topic, + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded, int outerw) const; void drawSurrounding( @@ -129,8 +130,8 @@ private: const QRect &inner, const PaintContext &context, const TopicButton *topic, + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const; QSize countOptimalSize() override; @@ -139,8 +140,8 @@ private: bool needInfoDisplay() const; int additionalWidth( const TopicButton *topic, + const Reply *reply, const HistoryMessageVia *via, - const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const; int calculateFullRight(const QRect &inner) const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index de72e7786..b3d79c6fe 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -1077,7 +1077,7 @@ bool Photo::needsBubble() const { && (item->repliesAreComments() || item->externalReply() || item->viaBot() - || _parent->displayedReply() + || _parent->displayReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton()); diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp index ad5dfcac1..0fbc095f8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.cpp @@ -418,13 +418,6 @@ bool ThemeDocument::isReadyForOpen() const { return !_data || _dataMedia->loaded(); } -QString ThemeDocument::additionalInfoString() const { - // This will force message info (time) to be displayed below - // this attachment in WebPage media. - static auto result = QString(" "); - return result; -} - bool ThemeDocument::hasHeavyPart() const { return (_dataMedia != nullptr); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h index 94549e951..7ed9bcf91 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_theme_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_theme_document.h @@ -46,7 +46,6 @@ public: return true; } bool isReadyForOpen() const override; - QString additionalInfoString() const override; bool hasHeavyPart() const override; void unloadHeavyPart() override; 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 a6bb07528..56299bf45 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -710,7 +710,6 @@ TextState WebPage::textState(QPoint point, StateRequest request) const { auto inner = outer.marginsRemoved(innerMargin()); auto tshift = inner.top(); auto paintw = inner.width(); - auto attachAdditionalInfoText = _attach ? _attach->additionalInfoString() : QString(); auto lineHeight = UnitedLineHeight(); auto inThumb = false; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 13570306b..3571a9e63 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -47,6 +47,7 @@ public: QPoint shift, int index); + int width() override; QString entityData() override; void paint(QPainter &p, const Context &context) override; void unload() override; @@ -73,6 +74,10 @@ StripEmoji::StripEmoji( , _index(index) { } +int StripEmoji::width() { + return _wrapped->width(); +} + QString StripEmoji::entityData() { return _wrapped->entityData(); } diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp new file mode 100644 index 000000000..c1781ea03 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.cpp @@ -0,0 +1,692 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "info/boosts/create_giveaway_box.h" + +#include "api/api_premium.h" +#include "base/call_delayed.h" +#include "base/unixtime.h" +#include "countries/countries_instance.h" +#include "data/data_peer.h" +#include "info/boosts/giveaway/giveaway_list_controllers.h" +#include "info/boosts/giveaway/giveaway_type_row.h" +#include "info/boosts/giveaway/select_countries_box.h" +#include "info/boosts/info_boosts_widget.h" +#include "info/info_controller.h" +#include "info/info_memento.h" +#include "lang/lang_keys.h" +#include "payments/payments_checkout_process.h" // Payments::CheckoutProcess +#include "payments/payments_form.h" // Payments::InvoicePremiumGiftCode +#include "settings/settings_common.h" +#include "settings/settings_premium.h" // Settings::ShowPremium +#include "ui/boxes/choose_date_time.h" +#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/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_giveaway.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" +#include "styles/style_settings.h" + +namespace { + +constexpr auto kDoneTooltipDuration = 5 * crl::time(1000); + +[[nodiscard]] QDateTime ThreeDaysAfterToday() { + auto dateNow = QDateTime::currentDateTime(); + dateNow = dateNow.addDays(3); + auto timeNow = dateNow.time(); + while (timeNow.minute() % 5) { + timeNow = timeNow.addSecs(60); + } + dateNow.setTime(timeNow); + return dateNow; +} + +[[nodiscard]] Fn CreateErrorCallback( + int max, + tr::phrase phrase) { + return [=](int count) { + const auto error = (count >= max); + if (error) { + Ui::Toast::Show(phrase(tr::now, lt_count, max)); + } + return error; + }; +} + +} // namespace + +void CreateGiveawayBox( + not_null box, + not_null controller, + not_null peer) { + box->setWidth(st::boxWideWidth); + + const auto weakWindow = base::make_weak(controller->parentController()); + + const auto bar = box->verticalLayout()->add( + object_ptr( + box, + st::giveawayGiftCodeCover, + nullptr, + tr::lng_giveaway_new_title(), + tr::lng_giveaway_new_about(Ui::Text::RichLangValue), + true)); + { + bar->setPaused(true); + bar->setMaximumHeight(st::giveawayGiftCodeTopHeight); + bar->setMinimumHeight(st::infoLayerTopBarHeight); + bar->resize(bar->width(), bar->maximumHeight()); + + const auto container = box->verticalLayout(); + const auto &padding = st::giveawayGiftCodeCoverDividerPadding; + Settings::AddSkip(container, padding.top()); + Settings::AddDivider(container); + Settings::AddSkip(container, padding.bottom()); + + const auto close = Ui::CreateChild( + container.get(), + st::boxTitleClose); + close->setClickedCallback([=] { box->closeBox(); }); + box->widthValue( + ) | rpl::start_with_next([=](int) { + const auto &pos = st::giveawayGiftCodeCoverClosePosition; + close->moveToRight(pos.x(), pos.y()); + }, box->lifetime()); + } + + using GiveawayType = Giveaway::GiveawayTypeRow::Type; + using GiveawayGroup = Ui::RadioenumGroup; + struct State final { + State(not_null p) : apiOptions(p) { + } + + Api::PremiumGiftCodeOptions apiOptions; + rpl::lifetime lifetimeApi; + + std::vector> selectedToAward; + rpl::event_stream<> toAwardAmountChanged; + + std::vector> selectedToSubscribe; + + rpl::variable typeValue; + rpl::variable sliderValue; + rpl::variable dateValue; + rpl::variable> countriesValue; + + bool confirmButtonBusy = false; + }; + const auto state = box->lifetime().make_state(peer); + const auto typeGroup = std::make_shared(); + + const auto loading = box->addRow( + object_ptr>( + box, + object_ptr(box))); + { + loading->toggle(true, anim::type::instant); + const auto container = loading->entity(); + Settings::AddSkip(container); + Settings::AddSkip(container); + container->add( + object_ptr>( + box, + object_ptr( + box, + tr::lng_contacts_loading(), + st::giveawayLoadingLabel))); + Settings::AddSkip(container); + Settings::AddSkip(container); + } + const auto contentWrap = box->verticalLayout()->add( + object_ptr>( + box, + object_ptr(box))); + contentWrap->toggle(false, anim::type::instant); + + { + const auto row = contentWrap->entity()->add( + object_ptr( + box, + GiveawayType::Random, + tr::lng_giveaway_create_subtitle())); + row->addRadio(typeGroup); + row->setClickedCallback([=] { + state->typeValue.force_assign(GiveawayType::Random); + }); + } + { + const auto row = contentWrap->entity()->add( + object_ptr( + box, + GiveawayType::SpecificUsers, + state->toAwardAmountChanged.events_starting_with( + rpl::empty_value() + ) | rpl::map([=] { + const auto &selected = state->selectedToAward; + return selected.empty() + ? tr::lng_giveaway_award_subtitle() + : (selected.size() == 1) + ? rpl::single(selected.front()->name()) + : tr::lng_giveaway_award_chosen( + lt_count, + rpl::single(selected.size()) | tr::to_count()); + }) | rpl::flatten_latest())); + row->addRadio(typeGroup); + row->setClickedCallback([=] { + auto initBox = [=](not_null peersBox) { + peersBox->setTitle(tr::lng_giveaway_award_option()); + peersBox->addButton(tr::lng_settings_save(), [=] { + state->selectedToAward = peersBox->collectSelectedRows(); + state->toAwardAmountChanged.fire({}); + peersBox->closeBox(); + }); + peersBox->addButton(tr::lng_cancel(), [=] { + peersBox->closeBox(); + }); + peersBox->boxClosing( + ) | rpl::start_with_next([=] { + state->typeValue.force_assign( + state->selectedToAward.empty() + ? GiveawayType::Random + : GiveawayType::SpecificUsers); + }, peersBox->lifetime()); + }; + + using Controller = Giveaway::AwardMembersListController; + auto listController = std::make_unique( + controller, + peer); + listController->setCheckError(CreateErrorCallback( + state->apiOptions.giveawayAddPeersMax(), + tr::lng_giveaway_maximum_users_error)); + box->uiShow()->showBox( + Box( + std::move(listController), + std::move(initBox)), + Ui::LayerOption::KeepOther); + }); + } + + { + const auto &padding = st::giveawayGiftCodeTypeDividerPadding; + Settings::AddSkip(contentWrap->entity(), padding.top()); + Settings::AddDivider(contentWrap->entity()); + Settings::AddSkip(contentWrap->entity(), padding.bottom()); + } + + const auto randomWrap = contentWrap->entity()->add( + object_ptr>( + contentWrap, + object_ptr(box))); + state->typeValue.value( + ) | rpl::start_with_next([=](GiveawayType type) { + randomWrap->toggle(type == GiveawayType::Random, anim::type::instant); + }, randomWrap->lifetime()); + + const auto sliderContainer = randomWrap->entity()->add( + object_ptr(randomWrap)); + const auto fillSliderContainer = [=] { + const auto availablePresets = state->apiOptions.availablePresets(); + if (availablePresets.empty()) { + return; + } + state->sliderValue = availablePresets.front(); + const auto title = Settings::AddSubsectionTitle( + sliderContainer, + tr::lng_giveaway_quantity_title()); + const auto rightLabel = Ui::CreateChild( + sliderContainer, + st::giveawayGiftCodeQuantitySubtitle); + rightLabel->show(); + + const auto floatLabel = Ui::CreateChild( + sliderContainer, + st::giveawayGiftCodeQuantityFloat); + floatLabel->show(); + + rpl::combine( + tr::lng_giveaway_quantity( + lt_count, + state->sliderValue.value( + ) | rpl::map([=](int v) -> float64 { + return state->apiOptions.giveawayBoostsPerPremium() * v; + })), + title->positionValue(), + sliderContainer->geometryValue() + ) | rpl::start_with_next([=](QString s, const QPoint &p, QRect) { + rightLabel->setText(std::move(s)); + rightLabel->moveToRight(st::boxRowPadding.right(), p.y()); + }, rightLabel->lifetime()); + + const auto &padding = st::giveawayGiftCodeSliderPadding; + Settings::AddSkip(sliderContainer, padding.top()); + const auto slider = sliderContainer->add( + object_ptr(sliderContainer, st::settingsScale), + st::boxRowPadding); + Settings::AddSkip(sliderContainer, padding.bottom()); + slider->resize(slider->width(), st::settingsScale.seekSize.height()); + slider->setPseudoDiscrete( + availablePresets.size(), + [=](int index) { return availablePresets[index]; }, + availablePresets.front(), + [=](int boosts) { state->sliderValue = boosts; }, + [](int) {}); + + state->sliderValue.value( + ) | rpl::start_with_next([=](int boosts) { + floatLabel->setText(QString::number(boosts)); + + const auto count = availablePresets.size(); + const auto sliderWidth = slider->width() + - st::settingsScale.seekSize.width(); + for (auto i = 0; i < count; i++) { + if ((i + 1 == count || availablePresets[i + 1] > boosts) + && availablePresets[i] <= boosts) { + const auto x = (sliderWidth * i) / (count - 1); + floatLabel->moveToLeft( + slider->x() + + x + + st::settingsScale.seekSize.width() / 2 + - floatLabel->width() / 2, + slider->y() + - floatLabel->height() + - st::giveawayGiftCodeSliderFloatSkip); + break; + } + } + }, floatLabel->lifetime()); + + Settings::AddSkip(sliderContainer); + Settings::AddDividerText( + sliderContainer, + tr::lng_giveaway_quantity_about()); + Settings::AddSkip(sliderContainer); + + sliderContainer->resizeToWidth(box->width()); + }; + + { + const auto channelsContainer = randomWrap->entity()->add( + object_ptr(randomWrap)); + Settings::AddSubsectionTitle( + channelsContainer, + tr::lng_giveaway_channels_title(), + st::giveawayGiftCodeChannelsSubsectionPadding); + + struct ListState final { + ListState(not_null p) : controller(p) { + } + PeerListContentDelegateSimple delegate; + Giveaway::SelectedChannelsListController controller; + }; + const auto listState = box->lifetime().make_state(peer); + + listState->delegate.setContent(channelsContainer->add( + object_ptr( + channelsContainer, + &listState->controller))); + listState->controller.setDelegate(&listState->delegate); + listState->controller.channelRemoved( + ) | rpl::start_with_next([=](not_null peer) { + auto &list = state->selectedToSubscribe; + list.erase(ranges::remove(list, peer), end(list)); + }, box->lifetime()); + listState->controller.setTopStatus(tr::lng_giveaway_channels_this( + lt_count, + state->sliderValue.value( + ) | rpl::map([=](int v) -> float64 { + return state->apiOptions.giveawayBoostsPerPremium() * v; + }))); + + using IconType = Settings::IconType; + Settings::AddButton( + channelsContainer, + tr::lng_giveaway_channels_add(), + st::giveawayGiftCodeChannelsAddButton, + { &st::settingsIconAdd, IconType::Round, &st::windowBgActive } + )->setClickedCallback([=] { + auto initBox = [=](not_null peersBox) { + peersBox->setTitle(tr::lng_giveaway_channels_add()); + peersBox->addButton(tr::lng_settings_save(), [=] { + const auto selected = peersBox->collectSelectedRows(); + state->selectedToSubscribe = selected; + listState->controller.rebuild(selected); + peersBox->closeBox(); + }); + peersBox->addButton(tr::lng_cancel(), [=] { + peersBox->closeBox(); + }); + }; + + using Controller = Giveaway::MyChannelsListController; + auto controller = std::make_unique( + peer, + box->uiShow(), + state->selectedToSubscribe); + controller->setCheckError(CreateErrorCallback( + state->apiOptions.giveawayAddPeersMax(), + tr::lng_giveaway_maximum_channels_error)); + box->uiShow()->showBox( + Box(std::move(controller), std::move(initBox)), + Ui::LayerOption::KeepOther); + }); + + const auto &padding = st::giveawayGiftCodeChannelsDividerPadding; + Settings::AddSkip(channelsContainer, padding.top()); + Settings::AddDividerText( + channelsContainer, + tr::lng_giveaway_channels_about()); + Settings::AddSkip(channelsContainer, padding.bottom()); + } + + const auto membersGroup = std::make_shared(); + { + const auto countriesContainer = randomWrap->entity()->add( + object_ptr(randomWrap)); + Settings::AddSubsectionTitle( + countriesContainer, + tr::lng_giveaway_users_title()); + + membersGroup->setValue(GiveawayType::AllMembers); + auto subtitle = state->countriesValue.value( + ) | rpl::map([=](const std::vector &list) { + return list.empty() + ? tr::lng_giveaway_users_from_all_countries() + : (list.size() == 1) + ? tr::lng_giveaway_users_from_one_country( + lt_country, + rpl::single(Countries::Instance().countryNameByISO2( + list.front()))) + : tr::lng_giveaway_users_from_countries( + lt_count, + rpl::single(list.size()) | tr::to_count()); + }) | rpl::flatten_latest(); + + const auto showBox = [=] { + auto done = [=](std::vector list) { + state->countriesValue = std::move(list); + }; + auto error = CreateErrorCallback( + state->apiOptions.giveawayCountriesMax(), + tr::lng_giveaway_maximum_countries_error); + box->uiShow()->showBox(Box( + Ui::SelectCountriesBox, + state->countriesValue.current(), + std::move(done), + std::move(error))); + }; + + const auto createCallback = [=](GiveawayType type) { + return [=] { + const auto was = membersGroup->value(); + membersGroup->setValue(type); + const auto now = membersGroup->value(); + if (was == now) { + base::call_delayed( + st::defaultRippleAnimation.hideDuration, + box, + showBox); + } + }; + }; + + { + const auto row = countriesContainer->add( + object_ptr( + box, + GiveawayType::AllMembers, + rpl::duplicate(subtitle))); + row->addRadio(membersGroup); + row->setClickedCallback(createCallback(GiveawayType::AllMembers)); + } + const auto row = countriesContainer->add( + object_ptr( + box, + GiveawayType::OnlyNewMembers, + std::move(subtitle))); + row->addRadio(membersGroup); + row->setClickedCallback(createCallback(GiveawayType::OnlyNewMembers)); + + Settings::AddSkip(countriesContainer); + Settings::AddDividerText( + countriesContainer, + tr::lng_giveaway_users_about()); + Settings::AddSkip(countriesContainer); + } + + { + const auto dateContainer = randomWrap->entity()->add( + object_ptr(randomWrap)); + Settings::AddSubsectionTitle( + dateContainer, + tr::lng_giveaway_date_title(), + st::giveawayGiftCodeChannelsSubsectionPadding); + + state->dateValue = ThreeDaysAfterToday().toSecsSinceEpoch(); + const auto button = Settings::AddButtonWithLabel( + dateContainer, + tr::lng_giveaway_date(), + state->dateValue.value() | rpl::map( + base::unixtime::parse + ) | rpl::map(Ui::FormatDateTime), + st::defaultSettingsButton); + + button->setClickedCallback([=] { + box->uiShow()->showBox(Box([=](not_null b) { + Ui::ChooseDateTimeBox(b, { + .title = tr::lng_giveaway_date_select(), + .submit = tr::lng_settings_save(), + .done = [=](TimeId time) { + state->dateValue = time; + b->closeBox(); + }, + .min = QDateTime::currentSecsSinceEpoch, + .time = state->dateValue.current(), + .max = [=] { + return QDateTime::currentSecsSinceEpoch() + + state->apiOptions.giveawayPeriodMax();; + }, + }); + })); + }); + + Settings::AddSkip(dateContainer); + Settings::AddDividerText( + dateContainer, + tr::lng_giveaway_date_about( + lt_count, + state->sliderValue.value() | tr::to_count())); + Settings::AddSkip(dateContainer); + } + + const auto durationGroup = std::make_shared(0); + const auto listOptions = contentWrap->entity()->add( + object_ptr(box)); + const auto rebuildListOptions = [=](int amountUsers) { + while (listOptions->count()) { + delete listOptions->widgetAt(0); + } + Settings::AddSubsectionTitle( + listOptions, + tr::lng_giveaway_duration_title( + lt_count, + rpl::single(amountUsers) | tr::to_count()), + st::giveawayGiftCodeChannelsSubsectionPadding); + Ui::Premium::AddGiftOptions( + listOptions, + durationGroup, + state->apiOptions.options(amountUsers), + st::giveawayGiftCodeGiftOption, + true); + + Settings::AddSkip(listOptions); + + auto terms = object_ptr( + listOptions, + tr::lng_premium_gift_terms( + lt_link, + tr::lng_premium_gift_terms_link( + ) | rpl::map([](const QString &t) { + return Ui::Text::Link(t, 1); + }), + Ui::Text::WithEntities), + st::boxDividerLabel); + terms->setLink(1, std::make_shared([=] { + box->closeBox(); + Settings::ShowPremium(&peer->session(), QString()); + })); + listOptions->add(object_ptr( + listOptions, + std::move(terms), + st::settingsDividerLabelPadding)); + + box->verticalLayout()->resizeToWidth(box->width()); + }; + { + + rpl::combine( + state->sliderValue.value(), + state->typeValue.value() + ) | rpl::start_with_next([=](int users, GiveawayType type) { + typeGroup->setValue(type); + rebuildListOptions((type == GiveawayType::SpecificUsers) + ? state->selectedToAward.size() + : users); + }, box->lifetime()); + } + { + // TODO mini-icon. + const auto &stButton = st::premiumGiftBox; + box->setStyle(stButton); + auto button = object_ptr( + box, + state->toAwardAmountChanged.events_starting_with( + rpl::empty_value() + ) | rpl::map([=] { + return (typeGroup->value() == GiveawayType::SpecificUsers) + ? tr::lng_giveaway_award() + : tr::lng_giveaway_start(); + }) | rpl::flatten_latest(), + st::giveawayGiftCodeStartButton); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->resizeToWidth(box->width() + - stButton.buttonPadding.left() + - stButton.buttonPadding.right()); + button->setClickedCallback([=] { + if (state->confirmButtonBusy) { + return; + } + const auto type = typeGroup->value(); + const auto isSpecific = (type == GiveawayType::SpecificUsers); + const auto isRandom = (type == GiveawayType::Random); + if (!isSpecific && !isRandom) { + return; + } + auto invoice = state->apiOptions.invoice( + isSpecific + ? state->selectedToAward.size() + : state->sliderValue.current(), + durationGroup->value()); + if (isSpecific) { + if (state->selectedToAward.empty()) { + return; + } + invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{ + ranges::views::all( + state->selectedToAward + ) | ranges::views::transform([]( + const not_null p) { + return not_null{ p->asUser() }; + }) | ranges::to_vector, + peer->asChannel(), + }; + } else if (isRandom) { + invoice.purpose = Payments::InvoicePremiumGiftCodeGiveaway{ + .boostPeer = peer->asChannel(), + .additionalChannels = ranges::views::all( + state->selectedToSubscribe + ) | ranges::views::transform([]( + const not_null p) { + return not_null{ p->asChannel() }; + }) | ranges::to_vector, + .countries = state->countriesValue.current(), + .untilDate = state->dateValue.current(), + .onlyNewSubscribers = (membersGroup->value() + == GiveawayType::OnlyNewMembers), + }; + } + state->confirmButtonBusy = true; + const auto show = box->uiShow(); + const auto weak = Ui::MakeWeak(box.get()); + const auto done = [=](Payments::CheckoutResult result) { + if (const auto strong = weak.data()) { + state->confirmButtonBusy = false; + strong->window()->setFocus(); + strong->closeBox(); + } + if (result == Payments::CheckoutResult::Paid) { + const auto filter = [=](const auto &...) { + if (const auto window = weakWindow.get()) { + window->showSection(Info::Boosts::Make(peer)); + } + return false; + }; + const auto title = isSpecific + ? tr::lng_giveaway_awarded_title + : tr::lng_giveaway_created_title; + const auto body = isSpecific + ? tr::lng_giveaway_awarded_body + : tr::lng_giveaway_created_body; + show->showToast({ + .text = Ui::Text::Bold( + title(tr::now)).append('\n').append( + body( + tr::now, + lt_link, + Ui::Text::Link( + tr::lng_giveaway_created_link( + tr::now)), + Ui::Text::WithEntities)), + .duration = kDoneTooltipDuration, + .adaptive = true, + .filter = filter, + }); + } + }; + Payments::CheckoutProcess::Start(std::move(invoice), done); + }); + box->addButton(std::move(button)); + } + state->typeValue.force_assign(GiveawayType::Random); + + box->setShowFinishedCallback([=] { + if (!loading->toggled()) { + return; + } + bar->setPaused(false); + state->lifetimeApi = state->apiOptions.request( + ) | rpl::start_with_error_done([=](const QString &error) { + }, [=] { + state->lifetimeApi.destroy(); + loading->toggle(false, anim::type::instant); + fillSliderContainer(); + rebuildListOptions(1); + contentWrap->toggle(true, anim::type::instant); + contentWrap->resizeToWidth(box->width()); + }); + }); +} diff --git a/Telegram/SourceFiles/info/boosts/create_giveaway_box.h b/Telegram/SourceFiles/info/boosts/create_giveaway_box.h new file mode 100644 index 000000000..7463732dc --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/create_giveaway_box.h @@ -0,0 +1,23 @@ +/* +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 Info { +class Controller; +} // namespace Info + +namespace Ui { +class GenericBox; +} // namespace Ui + +void CreateGiveawayBox( + not_null box, + not_null controller, + not_null peer); diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style new file mode 100644 index 000000000..db9ad6f04 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway.style @@ -0,0 +1,165 @@ +/* +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 +*/ +using "ui/basic.style"; +using "boxes/boxes.style"; +using "ui/effects/premium.style"; +using "statistics/statistics.style"; + +giveawayTypeListItem: PeerListItem(defaultPeerListItem) { + height: 52px; + photoPosition: point(58px, 6px); + namePosition: point(110px, 8px); + statusPosition: point(110px, 28px); + photoSize: 42px; +} +giveawayUserpic: icon {{ "boosts/filled_gift", windowFgActive }}; +giveawayUserpicSkip: 1px; +giveawayUserpicGroup: icon {{ "limits/groups", windowFgActive }}; +giveawayRadioPosition: point(21px, 16px); + +giveawayGiftCodeCountryButton: SettingsButton(reportReasonButton) { +} +giveawayGiftCodeCountrySelect: MultiSelect(defaultMultiSelect) { +} + +giveawayGiftCodeChannelDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }}; +giveawayGiftCodeChannelDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }}; + +giveawayLoadingLabel: FlatLabel(membersAbout) { +} +giveawayGiftCodeTopHeight: 195px; +giveawayGiftCodeLink: FlatLabel(defaultFlatLabel) { + margin: margins(10px, 12px, 10px, 8px); + textFg: menuIconColor; + maxHeight: 24px; +} +giveawayGiftCodeLinkCopy: icon{{ "menu/copy", menuIconColor }}; +giveawayGiftCodeLinkHeight: 42px; +giveawayGiftCodeLinkCopyWidth: 40px; +giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px); + +giveawayGiftCodeGiftOption: PremiumOption(premiumGiftOption) { + badgeShift: point(5px, 0px); +} +giveawayGiftCodeStartButton: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + radius: 6px; +} +giveawayGiftCodeQuantitySubtitle: FlatLabel(defaultFlatLabel) { + style: TextStyle(semiboldTextStyle) { + font: font(boxFontSize semibold); + } + textFg: windowActiveTextFg; + minWidth: 240px; + align: align(right); +} +giveawayGiftCodeQuantityFloat: FlatLabel(defaultFlatLabel) { + style: TextStyle(semiboldTextStyle) { + font: font(13px); + } + textFg: windowActiveTextFg; + minWidth: 50px; + align: align(center); +} + +boostLinkStatsButton: IconButton(defaultIconButton) { + width: giveawayGiftCodeLinkCopyWidth; + height: giveawayGiftCodeLinkHeight; + icon: icon{{ "menu/stats", menuIconColor }}; + iconOver: icon{{ "menu/stats", menuIconColor }}; + ripple: emptyRippleAnimation; +} + +giveawayGiftCodeTable: Table(defaultTable) { + labelMinWidth: 91px; +} +giveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px); +giveawayGiftCodeLabel: FlatLabel(defaultFlatLabel) { + textFg: menuIconColor; + maxHeight: 24px; + style: TextStyle(semiboldTextStyle) { + font: font(12px semibold); + } +} +giveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px); +giveawayGiftCodeValue: FlatLabel(defaultFlatLabel) { + maxHeight: 24px; + style: TextStyle(defaultTextStyle) { + font: font(12px); + linkUnderline: kLinkUnderlineNever; + } +} +giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px); +giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px); +giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) { + size: size(24px, 24px); + photoSize: 24px; + photoPosition: point(-1px, -1px); +} +giveawayGiftCodeNamePosition: point(32px, 4px); +giveawayGiftCodeCover: PremiumCover(userPremiumCover) { + starSize: size(92px, 90px); + starTopSkip: 20px; + titlePadding: margins(0px, 15px, 0px, 17px); + titleFont: font(15px semibold); + about: FlatLabel(userPremiumCoverAbout) { + textFg: windowBoldFg; + style: TextStyle(premiumAboutTextStyle) { + lineHeight: 17px; + } + } +} +giveawayGiftCodeCoverClosePosition: point(5px, 0px); +giveawayGiftCodeCoverDividerPadding: margins(0px, 11px, 0px, 5px); +giveawayGiftCodeTypeDividerPadding: margins(0px, 7px, 0px, 5px); +giveawayGiftCodeSliderPadding: margins(0px, 24px, 0px, 10px); +giveawayGiftCodeSliderFloatSkip: 6px; +giveawayGiftCodeChannelsSubsectionPadding: margins(0px, -1px, 0px, -4px); + +giveawayGiftCodeChannelsPeerList: PeerList(boostsListBox) { + padding: margins(0px, 7px, 0px, 0px); +} +giveawayGiftCodeMembersPeerList: PeerList(defaultPeerList) { + item: PeerListItem(defaultPeerListItem) { + height: 50px; + namePosition: point(62px, 7px); + statusPosition: point(62px, 27px); + } +} +giveawayRadioMembersPosition: point(21px, 14px); + +giveawayGiftCodeChannelsAddButton: SettingsButton(defaultSettingsButton) { + textFg: lightButtonFg; + textFgOver: lightButtonFgOver; + padding: margins(70px, 10px, 22px, 8px); + iconLeft: 28px; +} +giveawayGiftCodeChannelsDividerPadding: margins(0px, 5px, 0px, 5px); + +giveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) { + align: align(top); + textFg: windowBoldFg; +} +giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px); +giveawayGiftCodeBox: Box(defaultBox) { + buttonPadding: margins(22px, 11px, 22px, 22px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + } + shadowIgnoreTopSkip: true; +} +giveawayRefundedLabel: FlatLabel(boxLabel) { + align: align(top); + style: semiboldTextStyle; + textFg: attentionButtonFg; +} +giveawayRefundedPadding: margins(8px, 10px, 8px, 10px); diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp new file mode 100644 index 000000000..dd6df700f --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.cpp @@ -0,0 +1,329 @@ +/* +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 "info/boosts/giveaway/giveaway_list_controllers.h" + +#include "apiwrap.h" +#include "data/data_channel.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/confirm_box.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" +#include "styles/style_giveaway.h" + +namespace Giveaway { +namespace { + +class ChannelRow final : public PeerListRow { +public: + using PeerListRow::PeerListRow; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + + void rightActionAddRipple( + QPoint point, + Fn updateCallback) override; + void rightActionStopLastRipple() override; + +private: + std::unique_ptr _actionRipple; + +}; + +QSize ChannelRow::rightActionSize() const { + return QSize( + st::giveawayGiftCodeChannelDeleteIcon.width(), + st::giveawayGiftCodeChannelDeleteIcon.height()) * 2; +} + +QMargins ChannelRow::rightActionMargins() const { + const auto itemHeight = st::giveawayGiftCodeChannelsPeerList.item.height; + return QMargins( + 0, + (itemHeight - rightActionSize().height()) / 2, + st::giveawayRadioPosition.x() / 2, + 0); +} + +void ChannelRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (_actionRipple) { + _actionRipple->paint( + p, + x, + y, + outerWidth); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + const auto rect = QRect(QPoint(x, y), ChannelRow::rightActionSize()); + (actionSelected + ? st::giveawayGiftCodeChannelDeleteIconOver + : st::giveawayGiftCodeChannelDeleteIcon).paintInCenter(p, rect); +} + +void ChannelRow::rightActionAddRipple( + QPoint point, + Fn updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize()); + _actionRipple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point); +} + +void ChannelRow::rightActionStopLastRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + +} // namespace + +AwardMembersListController::AwardMembersListController( + not_null navigation, + not_null peer) +: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members) { +} + +void AwardMembersListController::rowClicked(not_null row) { + const auto checked = !row->checked(); + if (checked + && _checkErrorCallback + && _checkErrorCallback(delegate()->peerListSelectedRowsCount())) { + return; + } + delegate()->peerListSetRowChecked(row, checked); +} + +std::unique_ptr AwardMembersListController::createRow( + not_null participant) const { + const auto user = participant->asUser(); + if (!user || user->isInaccessible() || user->isBot() || user->isSelf()) { + return nullptr; + } + return std::make_unique(participant); +} + +base::unique_qptr AwardMembersListController::rowContextMenu( + QWidget *parent, + not_null row) { + return nullptr; +} + +void AwardMembersListController::setCheckError(Fn callback) { + _checkErrorCallback = std::move(callback); +} + +MyChannelsListController::MyChannelsListController( + not_null peer, + std::shared_ptr show, + std::vector> selected) +: PeerListController( + std::make_unique(&peer->session())) +, _peer(peer) +, _show(show) +, _selected(std::move(selected)) { +} + +std::unique_ptr MyChannelsListController::createSearchRow( + not_null peer) { + if (const auto channel = peer->asChannel()) { + return createRow(channel); + } + return nullptr; +} + +std::unique_ptr MyChannelsListController::createRestoredRow( + not_null peer) { + if (const auto channel = peer->asChannel()) { + return createRow(channel); + } + return nullptr; +} + +void MyChannelsListController::rowClicked(not_null row) { + const auto channel = row->peer()->asChannel(); + const auto checked = !row->checked(); + if (checked + && _checkErrorCallback + && _checkErrorCallback(delegate()->peerListSelectedRowsCount())) { + return; + } + if (checked && channel && channel->username().isEmpty()) { + _show->showBox(Box(Ui::ConfirmBox, Ui::ConfirmBoxArgs{ + .text = tr::lng_giveaway_channels_confirm_about(), + .confirmed = [=](Fn close) { + delegate()->peerListSetRowChecked(row, checked); + close(); + }, + .confirmText = tr::lng_filters_recommended_add(), + .title = tr::lng_giveaway_channels_confirm_title(), + })); + } else { + delegate()->peerListSetRowChecked(row, checked); + } +} + +Main::Session &MyChannelsListController::session() const { + return _peer->session(); +} + +void MyChannelsListController::prepare() { + delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); + const auto api = _apiLifetime.make_state( + &session().api().instance()); + api->request( + MTPstories_GetChatsToSend() + ).done([=](const MTPmessages_Chats &result) { + _apiLifetime.destroy(); + const auto &chats = result.match([](const auto &data) { + return data.vchats().v; + }); + auto &owner = session().data(); + for (const auto &chat : chats) { + if (const auto peer = owner.processChat(chat)) { + if (!peer->isChannel() || (peer == _peer)) { + continue; + } + if (!delegate()->peerListFindRow(peer->id.value)) { + if (const auto channel = peer->asChannel()) { + auto row = createRow(channel); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + if (ranges::contains(_selected, peer)) { + delegate()->peerListSetRowChecked(raw, true); + _selected.erase( + ranges::remove(_selected, peer), + end(_selected)); + } + } + } + } + } + for (const auto &selected : _selected) { + if (const auto channel = selected->asChannel()) { + auto row = createRow(channel); + const auto raw = row.get(); + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListSetRowChecked(raw, true); + } + } + delegate()->peerListRefreshRows(); + _selected.clear(); + }).send(); +} + +void MyChannelsListController::setCheckError(Fn callback) { + _checkErrorCallback = std::move(callback); +} + +std::unique_ptr MyChannelsListController::createRow( + not_null channel) const { + if (channel->isMegagroup()) { + return nullptr; + } + auto row = std::make_unique(channel); + row->setCustomStatus(tr::lng_chat_status_subscribers( + tr::now, + lt_count, + channel->membersCount())); + return row; +} + +SelectedChannelsListController::SelectedChannelsListController( + not_null peer) +: _peer(peer) { + PeerListController::setStyleOverrides( + &st::giveawayGiftCodeChannelsPeerList); +} + +void SelectedChannelsListController::setTopStatus(rpl::producer s) { + _statusLifetime = std::move( + s + ) | rpl::start_with_next([=](const QString &t) { + if (delegate()->peerListFullRowsCount() > 0) { + delegate()->peerListRowAt(0)->setCustomStatus(t); + } + }); +} + +void SelectedChannelsListController::rebuild( + std::vector> selected) { + while (delegate()->peerListFullRowsCount() > 1) { + delegate()->peerListRemoveRow(delegate()->peerListRowAt(1)); + } + for (const auto &peer : selected) { + delegate()->peerListAppendRow(createRow(peer->asChannel())); + } + delegate()->peerListRefreshRows(); +} + +auto SelectedChannelsListController::channelRemoved() const +-> rpl::producer> { + return _channelRemoved.events(); +} + +void SelectedChannelsListController::rowClicked(not_null row) { +} + +void SelectedChannelsListController::rowRightActionClicked( + not_null row) { + const auto peer = row->peer(); + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + _channelRemoved.fire_copy(peer); +} + +Main::Session &SelectedChannelsListController::session() const { + return _peer->session(); +} + +void SelectedChannelsListController::prepare() { + delegate()->peerListAppendRow(createRow(_peer->asChannel())); +} + +std::unique_ptr SelectedChannelsListController::createRow( + not_null channel) const { + if (channel->isMegagroup()) { + return nullptr; + } + const auto isYourChannel = (_peer->asChannel() == channel); + auto row = isYourChannel + ? std::make_unique(channel) + : std::make_unique(channel); + row->setCustomStatus(isYourChannel + ? QString() + : tr::lng_chat_status_subscribers( + tr::now, + lt_count, + channel->membersCount())); + return row; +} + +} // namespace Giveaway diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h new file mode 100644 index 000000000..8d08a8a15 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_list_controllers.h @@ -0,0 +1,104 @@ +/* +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 "boxes/peers/edit_participants_box.h" + +class PeerData; +class PeerListRow; + +namespace Ui { +class PopupMenu; +class Show; +} // namespace Ui + +namespace Window { +class SessionNavigation; +} // namespace Window + +namespace Giveaway { + +class AwardMembersListController : public ParticipantsBoxController { +public: + AwardMembersListController( + not_null navigation, + not_null peer); + + void setCheckError(Fn callback); + + void rowClicked(not_null row) override; + std::unique_ptr createRow( + not_null participant) const override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + +private: + Fn _checkErrorCallback; + +}; + +class MyChannelsListController : public PeerListController { +public: + MyChannelsListController( + not_null peer, + std::shared_ptr show, + std::vector> selected); + + void setCheckError(Fn callback); + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + + std::unique_ptr createSearchRow( + not_null peer) override; + std::unique_ptr createRestoredRow( + not_null peer) override; + +private: + std::unique_ptr createRow( + not_null channel) const; + + const not_null _peer; + const std::shared_ptr _show; + + Fn _checkErrorCallback; + + std::vector> _selected; + + rpl::lifetime _apiLifetime; + +}; + +class SelectedChannelsListController : public PeerListController { +public: + SelectedChannelsListController(not_null peer); + + void setTopStatus(rpl::producer status); + + void rebuild(std::vector> selected); + [[nodiscard]] rpl::producer> channelRemoved() const; + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void rowRightActionClicked(not_null row) override; + +private: + std::unique_ptr createRow( + not_null channel) const; + + const not_null _peer; + + rpl::event_stream> _channelRemoved; + rpl::lifetime _statusLifetime; + +}; + +} // namespace Giveaway diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp new file mode 100644 index 000000000..db1fedb08 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.cpp @@ -0,0 +1,123 @@ +/* +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 "info/boosts/giveaway/giveaway_type_row.h" + +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/text/text_options.h" +#include "ui/widgets/checkbox.h" +#include "styles/style_boxes.h" +#include "styles/style_giveaway.h" + +namespace Giveaway { + +constexpr auto kColorIndexSpecific = int(4); +constexpr auto kColorIndexRandom = int(2); + +GiveawayTypeRow::GiveawayTypeRow( + not_null parent, + Type type, + rpl::producer subtitle) +: RippleButton(parent, st::defaultRippleAnimation) +, _type(type) +, _st((_type == Type::SpecificUsers || _type == Type::Random) + ? st::giveawayTypeListItem + : st::giveawayGiftCodeMembersPeerList.item) +, _userpic( + Ui::EmptyUserpic::UserpicColor((_type == Type::SpecificUsers) + ? kColorIndexSpecific + : kColorIndexRandom), + QString()) +, _name( + _st.nameStyle, + (type == Type::SpecificUsers) + ? tr::lng_giveaway_award_option(tr::now) + : (type == Type::Random) + ? tr::lng_giveaway_create_option(tr::now) + : (type == Type::AllMembers) + ? tr::lng_giveaway_users_all(tr::now) + : tr::lng_giveaway_users_new(tr::now), + Ui::NameTextOptions()) { + std::move( + subtitle + ) | rpl::start_with_next([=] (const QString &s) { + _status.setText(st::defaultTextStyle, s, Ui::NameTextOptions()); + }, lifetime()); +} + +int GiveawayTypeRow::resizeGetHeight(int) { + return _st.height; +} + +void GiveawayTypeRow::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + + const auto paintOver = (isOver() || isDown()) && !isDisabled(); + const auto skipRight = _st.photoPosition.x(); + const auto outerWidth = width(); + const auto isSpecific = (_type == Type::SpecificUsers); + const auto hasUserpic = (_type == Type::Random) || isSpecific; + + if (paintOver) { + p.fillRect(e->rect(), _st.button.textBgOver); + } + Ui::RippleButton::paintRipple(p, 0, 0); + if (hasUserpic) { + _userpic.paintCircle( + p, + _st.photoPosition.x(), + _st.photoPosition.y(), + outerWidth, + _st.photoSize); + + const auto &userpic = isSpecific + ? st::giveawayUserpicGroup + : st::giveawayUserpic; + const auto userpicRect = QRect( + _st.photoPosition + - QPoint( + isSpecific ? -st::giveawayUserpicSkip : 0, + isSpecific ? 0 : st::giveawayUserpicSkip), + Size(_st.photoSize)); + userpic.paintInCenter(p, userpicRect); + } + + const auto namex = _st.namePosition.x(); + const auto namey = _st.namePosition.y(); + const auto namew = outerWidth - namex - skipRight; + + p.setPen(_st.nameFg); + _name.drawLeftElided(p, namex, namey, namew, width()); + + const auto statusx = _st.statusPosition.x(); + const auto statusy = _st.statusPosition.y(); + const auto statusw = outerWidth - statusx - skipRight; + p.setFont(st::contactsStatusFont); + p.setPen((isSpecific || !hasUserpic) ? st::lightButtonFg : _st.statusFg); + _status.drawLeftElided(p, statusx, statusy, statusw, outerWidth); +} + +void GiveawayTypeRow::addRadio( + std::shared_ptr> typeGroup) { + const auto &st = st::defaultCheckbox; + const auto radio = Ui::CreateChild>( + this, + std::move(typeGroup), + _type, + QString(), + st); + const auto pos = (_type == Type::SpecificUsers || _type == Type::Random) + ? st::giveawayRadioPosition + : st::giveawayRadioMembersPosition; + radio->moveToLeft(pos.x(), pos.y()); + radio->setAttribute(Qt::WA_TransparentForMouseEvents); + radio->show(); +} + +} // namespace Giveaway diff --git a/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h new file mode 100644 index 000000000..0167b338e --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/giveaway_type_row.h @@ -0,0 +1,52 @@ +/* +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/empty_userpic.h" +#include "ui/widgets/buttons.h" + +namespace Ui { +template +class RadioenumGroup; +} // namespace Ui + +namespace Giveaway { + +class GiveawayTypeRow final : public Ui::RippleButton { +public: + enum class Type { + Random, + SpecificUsers, + + AllMembers, + OnlyNewMembers, + }; + + GiveawayTypeRow( + not_null parent, + Type type, + rpl::producer subtitle); + + void addRadio(std::shared_ptr> typeGroup); + +protected: + void paintEvent(QPaintEvent *e) override; + + int resizeGetHeight(int) override; + +private: + const Type _type; + const style::PeerListItem _st; + + Ui::EmptyUserpic _userpic; + Ui::Text::String _status; + Ui::Text::String _name; + +}; + +} // namespace Giveaway diff --git a/Telegram/SourceFiles/info/boosts/giveaway/select_countries_box.cpp b/Telegram/SourceFiles/info/boosts/giveaway/select_countries_box.cpp new file mode 100644 index 000000000..4194ed98c --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/select_countries_box.cpp @@ -0,0 +1,226 @@ +/* +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 "info/boosts/giveaway/select_countries_box.h" + +#include "countries/countries_instance.h" +#include "lang/lang_keys.h" +#include "ui/emoji_config.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/multi_select.h" +#include "ui/wrap/slide_wrap.h" +#include "styles/style_boxes.h" +#include "styles/style_giveaway.h" +#include "styles/style_settings.h" + +namespace Ui { +namespace { + +void AddSkip(not_null container) { + container->add(object_ptr( + container, + st::settingsSectionSkip)); +} + +[[nodiscard]] QImage CacheFlagEmoji(const QString &flag) { + const auto &st = st::giveawayGiftCodeCountrySelect.item; + auto roundPaintCache = QImage( + Size(st.height) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + roundPaintCache.setDevicePixelRatio(style::DevicePixelRatio()); + roundPaintCache.fill(Qt::transparent); + { + const auto size = st.height; + auto p = Painter(&roundPaintCache); + auto hq = PainterHighQualityEnabler(p); + const auto flagText = Ui::Text::String(st::defaultTextStyle, flag); + p.setPen(st.textBg); + p.setBrush(st.textBg); + p.drawEllipse(0, 0, size, size); + flagText.draw(p, { + .position = QPoint( + 0 + (size - flagText.maxWidth()) / 2, + 0 + (size - flagText.minHeight()) / 2), + .outerWidth = size, + .availableWidth = size, + }); + } + return roundPaintCache; +} + +} // namespace + +void SelectCountriesBox( + not_null box, + const std::vector &selected, + Fn)> doneCallback, + Fn checkErrorCallback) { + struct State final { + std::vector resultList; + }; + const auto state = box->lifetime().make_state(); + + const auto multiSelect = box->setPinnedToTopContent( + object_ptr( + box, + st::giveawayGiftCodeCountrySelect, + tr::lng_participant_filter())); + AddSkip(box->verticalLayout()); + const auto &buttonSt = st::giveawayGiftCodeCountryButton; + + struct Entry final { + Ui::SlideWrap *wrap = nullptr; + QStringList list; + QString iso2; + }; + + auto countries = Countries::Instance().list(); + ranges::sort(countries, []( + const Countries::Info &a, + const Countries::Info &b) { + return (a.name.compare(b.name, Qt::CaseInsensitive) < 0); + }); + auto buttons = std::vector(); + buttons.reserve(countries.size()); + for (const auto &country : countries) { + const auto flag = Countries::Instance().flagEmojiByISO2(country.iso2); + if (!Ui::Emoji::Find(flag)) { + continue; + } + const auto itemId = buttons.size(); + auto button = object_ptr( + box->verticalLayout(), + rpl::single(flag + ' ' + country.name), + buttonSt); + const auto radio = Ui::CreateChild(button.data()); + const auto radioView = std::make_shared( + st::defaultRadio, + false, + [=] { radio->update(); }); + + { + const auto radioSize = radioView->getSize(); + radio->resize(radioSize); + radio->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(radio); + radioView->paint(p, 0, 0, radioSize.width()); + }, radio->lifetime()); + const auto buttonHeight = buttonSt.height + + rect::m::sum::v(buttonSt.padding); + radio->moveToLeft( + st::giveawayRadioPosition.x(), + (buttonHeight - radioSize.height()) / 2); + } + + const auto roundPaintCache = CacheFlagEmoji(flag); + const auto paintCallback = [=](Painter &p, int x, int y, int, int) { + p.drawImage(x, y, roundPaintCache); + }; + const auto choose = [=](bool clicked) { + const auto value = !radioView->checked(); + if (value && checkErrorCallback(state->resultList.size())) { + return; + } + radioView->setChecked(value, anim::type::normal); + + if (value) { + state->resultList.push_back(country.iso2); + multiSelect->addItem( + itemId, + country.name, + st::activeButtonBg, + paintCallback, + clicked + ? Ui::MultiSelect::AddItemWay::Default + : Ui::MultiSelect::AddItemWay::SkipAnimation); + } else { + auto &list = state->resultList; + list.erase(ranges::remove(list, country.iso2), end(list)); + multiSelect->removeItem(itemId); + } + }; + button->setClickedCallback([=] { + choose(true); + }); + if (ranges::contains(selected, country.iso2)) { + choose(false); + } + + const auto wrap = box->verticalLayout()->add( + object_ptr>( + box, + std::move(button))); + wrap->toggle(true, anim::type::instant); + + { + auto list = QStringList{ + flag, + country.name, + country.alternativeName, + }; + buttons.push_back({ wrap, std::move(list), country.iso2 }); + } + } + + const auto noResults = box->addRow( + object_ptr>( + box, + object_ptr(box))); + { + noResults->toggle(false, anim::type::instant); + const auto container = noResults->entity(); + AddSkip(container); + AddSkip(container); + container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_search_messages_none(), + st::membersAbout))); + AddSkip(container); + AddSkip(container); + } + + multiSelect->setQueryChangedCallback([=](const QString &query) { + auto wasAnyFound = false; + for (const auto &entry : buttons) { + const auto found = ranges::any_of(entry.list, [&]( + const QString &s) { + return s.startsWith(query, Qt::CaseInsensitive); + }); + entry.wrap->toggle(found, anim::type::instant); + wasAnyFound |= found; + } + noResults->toggle(!wasAnyFound, anim::type::instant); + }); + multiSelect->setItemRemovedCallback([=](uint64 itemId) { + auto &list = state->resultList; + auto &button = buttons[itemId]; + const auto it = ranges::find(list, button.iso2); + if (it != end(list)) { + list.erase(it); + button.wrap->entity()->clicked({}, Qt::LeftButton); + } + }); + + box->addButton(tr::lng_settings_save(), [=] { + doneCallback(state->resultList); + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/info/boosts/giveaway/select_countries_box.h b/Telegram/SourceFiles/info/boosts/giveaway/select_countries_box.h new file mode 100644 index 000000000..0d0fb91a0 --- /dev/null +++ b/Telegram/SourceFiles/info/boosts/giveaway/select_countries_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 + +namespace Ui { + +class GenericBox; + +void SelectCountriesBox( + not_null box, + const std::vector &selected, + Fn)> doneCallback, + Fn checkErrorCallback); + +} // namespace Ui diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp index 7528c093b..494df7a99 100644 --- a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp +++ b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp @@ -7,10 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/boosts/info_boosts_inner_widget.h" +#include "api/api_premium.h" #include "api/api_statistics.h" +#include "boxes/gift_premium_box.h" #include "boxes/peers/edit_peer_invite_link.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "info/boosts/create_giveaway_box.h" #include "info/boosts/info_boosts_widget.h" #include "info/info_controller.h" +#include "info/profile/info_profile_icon.h" +#include "info/statistics/info_statistics_inner_widget.h" // FillLoading. #include "info/statistics/info_statistics_list_controllers.h" #include "lang/lang_keys.h" #include "settings/settings_common.h" @@ -19,7 +27,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/invite_link_buttons.h" #include "ui/controls/invite_link_label.h" #include "ui/rect.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.h" #include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" #include "styles/style_info.h" #include "styles/style_statistics.h" @@ -56,11 +67,12 @@ void FillOverview( object_ptr(content), st::statisticsLayerMargins); - const auto addPrimary = [&](float64 v) { + const auto addPrimary = [&](float64 v, bool approximately = false) { return Ui::CreateChild( container, (v >= 0) - ? Lang::FormatCountToShort(v).string + ? (approximately && v ? QChar(0x2248) : QChar()) + + Lang::FormatCountToShort(v).string : QString(), st::statisticsOverviewValue); }; @@ -98,10 +110,11 @@ void FillOverview( const auto topLeftLabel = addPrimary(stats.level); - const auto topRightLabel = addPrimary(stats.premiumMemberCount); + const auto topRightLabel = addPrimary(stats.premiumMemberCount, true); const auto bottomLeftLabel = addPrimary(stats.boostCount); - const auto bottomRightLabel = addPrimary( - stats.nextLevelBoostCount - stats.boostCount); + const auto bottomRightLabel = addPrimary(std::max( + stats.nextLevelBoostCount - stats.boostCount, + 0)); addSub( topLeftLabel, @@ -173,6 +186,36 @@ void FillShareLink( ::Settings::AddSkip(content, st::boostsLinkFieldPadding.bottom()); } +void FillGetBoostsButton( + not_null content, + not_null controller, + std::shared_ptr show, + not_null peer) { + if (!Api::PremiumGiftCodeOptions(peer).giveawayGiftsPurchaseAvailable()) { + return; + } + ::Settings::AddSkip(content); + const auto &st = st::getBoostsButton; + const auto &icon = st::getBoostsButtonIcon; + const auto button = content->add( + ::Settings::CreateButton( + content.get(), + tr::lng_boosts_get_boosts(), + st)); + button->setClickedCallback([=] { + show->showBox(Box(CreateGiveawayBox, controller, peer)); + }); + Ui::CreateChild( + button, + icon, + QPoint{ + st::infoSharedMediaButtonIconPosition.x(), + (st.height + rect::m::sum::v(st.padding) - icon.height()) / 2, + })->show(); + ::Settings::AddSkip(content); + ::Settings::AddDividerText(content, tr::lng_boosts_get_boosts_subtext()); +} + } // namespace InnerWidget::InnerWidget( @@ -188,12 +231,18 @@ InnerWidget::InnerWidget( void InnerWidget::load() { const auto api = lifetime().make_state(_peer); + Info::Statistics::FillLoading( + this, + _loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1), + _showFinished.events()); + _showFinished.events( ) | rpl::take(1) | rpl::start_with_next([=] { api->request( ) | rpl::start_with_error_done([](const QString &error) { }, [=] { _state = api->boostStatus(); + _loaded.fire(true); fill(); }, lifetime()); }, lifetime()); @@ -232,30 +281,89 @@ void InnerWidget::fill() { ::Settings::AddDivider(inner); ::Settings::AddSkip(inner); - if (status.firstSlice.total > 0) { - ::Settings::AddSkip(inner); - using PeerPtr = not_null; - const auto header = inner->add( - object_ptr(inner), - st::statisticsLayerMargins - + st::boostsChartHeaderPadding); - header->resizeToWidth(header->width()); - header->setTitle(tr::lng_boosts_list_title( + const auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0); + const auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0); + if (hasBoosts || hasGifts) { + auto boostClicked = [=](const Data::Boost &boost) { + if (!boost.giftCodeLink.slug.isEmpty()) { + ResolveGiftCode(_controller, boost.giftCodeLink.slug); + } else if (boost.userId) { + const auto user = _peer->owner().user(boost.userId); + crl::on_main(this, [=] { + _controller->showPeerInfo(user); + }); + } else if (!boost.isUnclaimed) { + _show->showToast(tr::lng_boosts_list_pending_about(tr::now)); + } + }; + +#ifdef _DEBUG + const auto hasOneTab = false; +#else + const auto hasOneTab = (hasBoosts != hasGifts); +#endif + const auto boostsTabText = tr::lng_boosts_list_title( tr::now, lt_count, - status.firstSlice.total)); - header->setSubTitle({}); + status.firstSliceBoosts.multipliedTotal); + const auto giftsTabText = tr::lng_boosts_list_tab_gifts( + tr::now, + lt_count, + status.firstSliceGifts.multipliedTotal); + if (hasOneTab) { + ::Settings::AddSkip(inner); + const auto header = inner->add( + object_ptr(inner), + st::statisticsLayerMargins + + st::boostsChartHeaderPadding); + header->resizeToWidth(header->width()); + header->setTitle(hasBoosts ? boostsTabText : giftsTabText); + header->setSubTitle({}); + } + + const auto slider = inner->add( + object_ptr>( + inner, + object_ptr( + inner, + st::defaultTabsSlider))); + slider->toggle(!hasOneTab, anim::type::instant); + slider->entity()->addSection(boostsTabText); + slider->entity()->addSection(giftsTabText); + + const auto boostsWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto giftsWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + boostsWrap->toggle(hasOneTab ? true : hasBoosts, anim::type::instant); + giftsWrap->toggle(hasOneTab ? false : hasGifts, anim::type::instant); + + slider->entity()->sectionActivated( + ) | rpl::start_with_next([=](int index) { + boostsWrap->toggle(!index, anim::type::instant); + giftsWrap->toggle(index, anim::type::instant); + }, inner->lifetime()); + Statistics::AddBoostsList( - status.firstSlice, - inner, - [=](PeerPtr p) { _controller->showPeerInfo(p); }, + status.firstSliceBoosts, + boostsWrap->entity(), + boostClicked, _peer, tr::lng_boosts_title()); + Statistics::AddBoostsList( + status.firstSliceGifts, + giftsWrap->entity(), + std::move(boostClicked), + _peer, + tr::lng_boosts_title()); + ::Settings::AddSkip(inner); - ::Settings::AddDividerText( - inner, - tr::lng_boosts_list_subtext()); ::Settings::AddSkip(inner); + ::Settings::AddDividerText(inner, tr::lng_boosts_list_subtext()); } ::Settings::AddSkip(inner); @@ -265,6 +373,8 @@ void InnerWidget::fill() { ::Settings::AddSkip(inner); ::Settings::AddDividerText(inner, tr::lng_boosts_link_subtext()); + FillGetBoostsButton(inner, _controller, _show, _peer); + resizeToWidth(width()); crl::on_main([=]{ fakeShowed->fire({}); }); } diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp index 2679f67a0..fac44f16a 100644 --- a/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp +++ b/Telegram/SourceFiles/info/boosts/info_boosts_widget.cpp @@ -118,4 +118,3 @@ std::shared_ptr Make(not_null peer) { } } // namespace Info::Boosts - diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index baa569ab3..b5b2ae8b0 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -247,50 +247,6 @@ void FillStatistic( } } -void FillLoading( - not_null container, - rpl::producer toggleOn, - rpl::producer<> showFinished) { - const auto emptyWrap = container->add( - object_ptr>( - container, - object_ptr(container))); - emptyWrap->toggleOn(std::move(toggleOn), anim::type::instant); - - const auto content = emptyWrap->entity(); - auto icon = ::Settings::CreateLottieIcon( - content, - { .name = u"stats"_q, .sizeOverride = Size(st::changePhoneIconSize) }, - st::settingsBlockedListIconPadding); - - ( - std::move(showFinished) | rpl::take(1) - ) | rpl::start_with_next([animate = std::move(icon.animate)] { - animate(anim::repeat::loop); - }, icon.widget->lifetime()); - content->add(std::move(icon.widget)); - - content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_stats_loading(), - st::changePhoneTitle)), - st::changePhoneTitlePadding + st::boxRowPadding); - - content->add( - object_ptr>( - content, - object_ptr( - content, - tr::lng_stats_loading_subtext(), - st::statisticsLoadingSubtext)), - st::changePhoneDescriptionPadding + st::boxRowPadding); - - ::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top()); -} - void AddHeader( not_null content, tr::phrase<> text, @@ -507,6 +463,50 @@ void FillOverview( } // namespace +void FillLoading( + not_null container, + rpl::producer toggleOn, + rpl::producer<> showFinished) { + const auto emptyWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + emptyWrap->toggleOn(std::move(toggleOn), anim::type::instant); + + const auto content = emptyWrap->entity(); + auto icon = ::Settings::CreateLottieIcon( + content, + { .name = u"stats"_q, .sizeOverride = Size(st::changePhoneIconSize) }, + st::settingsBlockedListIconPadding); + + ( + std::move(showFinished) | rpl::take(1) + ) | rpl::start_with_next([animate = std::move(icon.animate)] { + animate(anim::repeat::loop); + }, icon.widget->lifetime()); + content->add(std::move(icon.widget)); + + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_stats_loading(), + st::changePhoneTitle)), + st::changePhoneTitlePadding + st::boxRowPadding); + + content->add( + object_ptr>( + content, + object_ptr( + content, + tr::lng_stats_loading_subtext(), + st::statisticsLoadingSubtext)), + st::changePhoneDescriptionPadding + st::boxRowPadding); + + ::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top()); +} + InnerWidget::InnerWidget( QWidget *parent, not_null controller, diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h index cd3aa9348..f2e21a6c1 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.h @@ -21,6 +21,11 @@ namespace Info::Statistics { class Memento; class MessagePreview; +void FillLoading( + not_null container, + rpl::producer toggleOn, + rpl::producer<> showFinished); + class InnerWidget final : public Ui::VerticalLayout { public: struct ShowRequest final { diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index db764dff6..6d8c4307e 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "settings/settings_common.h" #include "ui/effects/toggle_arrow.h" +#include "ui/empty_userpic.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/widgets/buttons.h" @@ -30,6 +31,62 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Statistics { namespace { +using BoostCallback = Fn; +constexpr auto kColorIndexUnclaimed = int(3); +constexpr auto kColorIndexPending = int(4); + +[[nodiscard]] QImage Badge( + const style::TextStyle &textStyle, + const QString &text, + int badgeHeight, + const style::margins &textPadding, + const style::color &bg, + const style::color &fg, + float64 bgOpacity, + const style::margins &iconPadding, + const style::icon &icon) { + auto badgeText = Ui::Text::String(textStyle, text); + const auto badgeTextWidth = badgeText.maxWidth(); + const auto badgex = 0; + const auto badgey = 0; + const auto badgeh = 0 + badgeHeight; + const auto badgew = badgeTextWidth + + rect::m::sum::h(textPadding); + auto result = QImage( + QSize(badgew, badgeh) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + result.setDevicePixelRatio(style::DevicePixelRatio()); + { + auto p = Painter(&result); + + p.setPen(Qt::NoPen); + p.setBrush(bg); + + const auto r = QRect(badgex, badgey, badgew, badgeh); + { + auto hq = PainterHighQualityEnabler(p); + auto o = ScopedPainterOpacity(p, bgOpacity); + p.drawRoundedRect(r, badgeh / 2, badgeh / 2); + } + + p.setPen(fg); + p.setBrush(Qt::NoBrush); + badgeText.drawLeftElided( + p, + r.x() + textPadding.left(), + badgey + textPadding.top(), + badgew, + badgew * 2); + + icon.paint( + p, + QPoint(r.x() + iconPadding.left(), r.y() + iconPadding.top()), + badgew * 2); + } + return result; +} + void AddArrow(not_null parent) { const auto arrow = Ui::CreateChild(parent.get()); arrow->paintRequest( @@ -100,7 +157,7 @@ struct MembersDescriptor final { struct BoostsDescriptor final { Data::BoostsListSlice firstSlice; - Fn)> showPeerInfo; + BoostCallback boostClickedCallback; not_null peer; }; @@ -321,6 +378,192 @@ void PublicForwardsController::appendRow( return; } +class BoostRow final : public PeerListRow { +public: + BoostRow(not_null peer, const Data::Boost &boost); + BoostRow(const Data::Boost &boost); + + [[nodiscard]] const Data::Boost &boost() const; + [[nodiscard]] QString generateName() override; + + [[nodiscard]] PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + + int paintNameIconGetWidth( + Painter &p, + Fn repaint, + crl::time now, + int nameLeft, + int nameTop, + int nameWidth, + int availableWidth, + int outerWidth, + bool selected) override; + + QSize rightActionSize() const override; + QMargins rightActionMargins() const override; + bool rightActionDisabled() const override; + void rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + +private: + void init(); + void invalidateBadges(); + + const Data::Boost _boost; + Ui::EmptyUserpic _userpic; + QImage _badge; + QImage _rightBadge; + +}; + +BoostRow::BoostRow(not_null peer, const Data::Boost &boost) +: PeerListRow(peer, UniqueRowIdFromString(boost.id)) +, _boost(boost) +, _userpic(Ui::EmptyUserpic::UserpicColor(0), QString()) { + init(); +} + +BoostRow::BoostRow(const Data::Boost &boost) +: PeerListRow(UniqueRowIdFromString(boost.id)) +, _boost(boost) +, _userpic( + Ui::EmptyUserpic::UserpicColor(boost.isUnclaimed + ? kColorIndexUnclaimed + : kColorIndexPending), + QString()) { + init(); +} + +void BoostRow::init() { + invalidateBadges(); + constexpr auto kMonthsDivider = int(30 * 86400); + const auto months = (_boost.expiresAt - _boost.date.toSecsSinceEpoch()) + / kMonthsDivider; + auto status = !PeerListRow::special() + ? tr::lng_boosts_list_status( + tr::now, + lt_date, + langDateTime(_boost.date)) + : tr::lng_months_tiny(tr::now, lt_count, months) + + ' ' + + QChar(0x2022) + + ' ' + + langDateTime(_boost.date); + PeerListRow::setCustomStatus(std::move(status)); +} + +const Data::Boost &BoostRow::boost() const { + return _boost; +} + +QString BoostRow::generateName() { + return !PeerListRow::special() + ? PeerListRow::generateName() + : _boost.isUnclaimed + ? tr::lng_boosts_list_unclaimed(tr::now) + : tr::lng_boosts_list_pending(tr::now); +} + +PaintRoundImageCallback BoostRow::generatePaintUserpicCallback(bool force) { + if (!PeerListRow::special()) { + return PeerListRow::generatePaintUserpicCallback(force); + } + return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { + _userpic.paintCircle(p, x, y, outerWidth, size); + }; +} + +void BoostRow::invalidateBadges() { + _badge = _boost.multiplier + ? Badge( + st::statisticsDetailsBottomCaptionStyle, + QString::number(_boost.multiplier), + st::boostsListBadgeHeight, + st::boostsListBadgeTextPadding, + st::premiumButtonBg2, + st::premiumButtonFg, + 1., + st::boostsListMiniIconPadding, + st::boostsListMiniIcon) + : QImage(); + + constexpr auto kBadgeBgOpacity = 0.2; + const auto &rightColor = _boost.isGiveaway + ? st::historyPeer4UserpicBg2 + : st::historyPeer8UserpicBg2; + const auto &rightIcon = _boost.isGiveaway + ? st::boostsListGiveawayMiniIcon + : st::boostsListGiftMiniIcon; + _rightBadge = (_boost.isGift || _boost.isGiveaway) + ? Badge( + st::boostsListRightBadgeTextStyle, + _boost.isGiveaway + ? tr::lng_gift_link_reason_giveaway(tr::now) + : tr::lng_gift_link_label_gift(tr::now), + st::boostsListRightBadgeHeight, + st::boostsListRightBadgeTextPadding, + rightColor, + rightColor, + kBadgeBgOpacity, + st::boostsListGiftMiniIconPadding, + rightIcon) + : QImage(); +} + + +QSize BoostRow::rightActionSize() const { + return _rightBadge.size() / style::DevicePixelRatio(); +} + +QMargins BoostRow::rightActionMargins() const { + return st::boostsListRightBadgePadding; +} + +bool BoostRow::rightActionDisabled() const { + return true; +} + +void BoostRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (!_rightBadge.isNull()) { + p.drawImage(x, y, _rightBadge); + } +} + +int BoostRow::paintNameIconGetWidth( + Painter &p, + Fn repaint, + crl::time now, + int nameLeft, + int nameTop, + int nameWidth, + int availableWidth, + int outerWidth, + bool selected) { + if (_badge.isNull()) { + return 0; + } + const auto badgew = _badge.width() / style::DevicePixelRatio(); + const auto nameTooLarge = (nameWidth > availableWidth); + const auto &padding = st::boostsListBadgePadding; + const auto left = nameTooLarge + ? ((nameLeft + availableWidth) - badgew - padding.left()) + : (nameLeft + nameWidth + padding.right()); + p.drawImage(left, nameTop + padding.top(), _badge); + return badgew + (nameTooLarge ? padding.left() : 0); +} + class BoostsController final : public PeerListController { public: explicit BoostsController(BoostsDescriptor d); @@ -331,28 +574,30 @@ public: void loadMoreRows() override; [[nodiscard]] bool skipRequest() const; - void setLimit(int limit); + void requestNext(); + + [[nodiscard]] rpl::producer totalBoostsValue() const; private: void applySlice(const Data::BoostsListSlice &slice); const not_null _session; - Fn)> _showPeerInfo; + BoostCallback _boostClickedCallback; Api::Boosts _api; Data::BoostsListSlice _firstSlice; Data::BoostsListSlice::OffsetToken _apiToken; - int _limit = 0; - bool _allLoaded = false; bool _requesting = false; + rpl::variable _totalBoosts; + }; BoostsController::BoostsController(BoostsDescriptor d) : _session(&d.peer->session()) -, _showPeerInfo(std::move(d.showPeerInfo)) +, _boostClickedCallback(std::move(d.boostClickedCallback)) , _api(d.peer) , _firstSlice(std::move(d.firstSlice)) { PeerListController::setStyleOverrides(&st::boostsListBox); @@ -366,8 +611,7 @@ bool BoostsController::skipRequest() const { return _requesting || _allLoaded; } -void BoostsController::setLimit(int limit) { - _limit = limit; +void BoostsController::requestNext() { _requesting = true; _api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) { _requesting = false; @@ -387,26 +631,32 @@ void BoostsController::applySlice(const Data::BoostsListSlice &slice) { _allLoaded = slice.allLoaded; _apiToken = slice.token; - const auto formatter = u"MMM d, yyyy"_q; + auto sumFromSlice = 0; for (const auto &item : slice.list) { - const auto user = session().data().user(item.userId); - if (delegate()->peerListFindRow(user->id.value)) { - continue; - } - auto row = std::make_unique(user); - row->setCustomStatus(tr::lng_boosts_list_status( - tr::now, - lt_date, - QLocale().toString(item.expirationDate, formatter))); + sumFromSlice += item.multiplier ? item.multiplier : 1; + auto row = [&] { + if (item.userId && !item.isUnclaimed) { + const auto user = session().data().user(item.userId); + return std::make_unique(user, item); + } else { + return std::make_unique(item); + } + }(); delegate()->peerListAppendRow(std::move(row)); } delegate()->peerListRefreshRows(); + _totalBoosts = _totalBoosts.current() + sumFromSlice; } void BoostsController::rowClicked(not_null row) { - crl::on_main([=, peer = row->peer()] { - _showPeerInfo(peer); - }); + if (_boostClickedCallback) { + _boostClickedCallback( + static_cast(row.get())->boost()); + } +} + +rpl::producer BoostsController::totalBoostsValue() const { + return _totalBoosts.value(); } } // namespace @@ -512,53 +762,52 @@ void AddMembersList( void AddBoostsList( const Data::BoostsListSlice &firstSlice, not_null container, - Fn)> showPeerInfo, + BoostCallback boostClickedCallback, not_null peer, rpl::producer title) { - const auto max = firstSlice.total; + const auto max = firstSlice.multipliedTotal; struct State final { State(BoostsDescriptor d) : controller(std::move(d)) { } PeerListContentDelegateSimple delegate; BoostsController controller; - int limit = Api::Boosts::kFirstSlice; }; - auto d = BoostsDescriptor{ firstSlice, std::move(showPeerInfo), peer }; + auto d = BoostsDescriptor{ firstSlice, boostClickedCallback, peer }; const auto state = container->lifetime().make_state(std::move(d)); state->delegate.setContent(container->add( object_ptr(container, &state->controller))); state->controller.setDelegate(&state->delegate); - if (max <= state->limit) { - return; - } const auto wrap = container->add( object_ptr>( container, object_ptr( container, - tr::lng_boosts_show_more(), + (firstSlice.token.gifts + ? tr::lng_boosts_show_more_gifts + : tr::lng_boosts_show_more_boosts)( + lt_count, + state->controller.totalBoostsValue( + ) | rpl::map( + max - rpl::mappers::_1 + ) | tr::to_count()), st::statisticsShowMoreButton)), { 0, -st::settingsButton.padding.top(), 0, 0 }); const auto button = wrap->entity(); AddArrow(button); const auto showMore = [=] { - if (state->controller.skipRequest()) { - return; + if (!state->controller.skipRequest()) { + state->controller.requestNext(); + container->resizeToWidth(container->width()); } - state->limit = std::min(int(max), state->limit + Api::Boosts::kLimit); - state->controller.setLimit(state->limit); - if (state->limit == max) { - wrap->toggle(false, anim::type::instant); - } - container->resizeToWidth(container->width()); }; + wrap->toggleOn( + state->controller.totalBoostsValue( + ) | rpl::map(rpl::mappers::_1 > 0 && rpl::mappers::_1 < max), + anim::type::instant); button->setClickedCallback(showMore); - if (state->limit == max) { - wrap->toggle(false, anim::type::instant); - } } } // namespace Info::Statistics diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h index dc619362d..de4bc5f2f 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.h @@ -14,6 +14,7 @@ class VerticalLayout; } // namespace Ui namespace Data { +struct Boost; struct BoostsListSlice; struct PublicForwardsSlice; struct SupergroupStatistics; @@ -38,7 +39,7 @@ void AddMembersList( void AddBoostsList( const Data::BoostsListSlice &firstSlice, not_null container, - Fn)> showPeerInfo, + Fn boostClickedCallback, not_null peer, rpl::producer title); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 5a4bc0a93..9e671a890 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -233,7 +233,7 @@ void FillDisclaimerBox(not_null box, Fn done) { tr::lng_mini_apps_disclaimer_link(tr::now), tr::lng_mini_apps_tos_url(tr::now))), Ui::Text::WithEntities), - st::defaultBoxCheckbox, + st::urlAuthCheckbox, std::move(checkView)), { st::boxRowPadding.left(), @@ -241,7 +241,7 @@ void FillDisclaimerBox(not_null box, Fn done) { st::boxRowPadding.right(), 0, }); - row->setAllowTextLines(5); + row->setAllowTextLines(); row->setClickHandlerFilter([=]( const ClickHandlerPtr &link, Qt::MouseButton button) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index bb3a834a6..8a2088785 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -500,7 +500,7 @@ bool ReplyArea::confirmSendingFiles( auto confirmed = [=](auto &&...args) { sendingFilesConfirmed(std::forward(args)...); }; - auto box = Box(SendFilesBoxDescriptor{ + show->show(Box(SendFilesBoxDescriptor{ .show = show, .list = std::move(list), .caption = _controls->getTextWithAppliedMarkdown(), @@ -511,10 +511,7 @@ bool ReplyArea::confirmSendingFiles( .stOverride = &st::storiesComposeControls, .confirmed = crl::guard(this, confirmed), .cancelled = _controls->restoreTextCallback(insertTextOnCancel), - }); - if (const auto shown = show->show(std::move(box))) { - shown->setCloseByOutsideClick(false); - } + })); return true; } diff --git a/Telegram/SourceFiles/mtproto/core_types.h b/Telegram/SourceFiles/mtproto/core_types.h index 05aafa69a..0437644be 100644 --- a/Telegram/SourceFiles/mtproto/core_types.h +++ b/Telegram/SourceFiles/mtproto/core_types.h @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include #include using mtpPrime = int32; @@ -243,6 +244,18 @@ inline MTPvector MTP_vector() { return tl::make_vector(); } +// ranges::to doesn't work with Qt 6 in Clang, +// because QVector is a type alias for QList there. +template +inline auto MTP_vector_from_range(Rng &&range) { + using T = std::remove_cvref_t; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0 ,0) + return MTP_vector(std::forward(range) | ranges::to()); +#else // QT_VERSION >= 6.0 + return MTP_vector(std::forward(range) | ranges::to()); +#endif // QT_VERSION < 6.0 +} + namespace tl { template diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index b07d2c5e5..f20bb78e0 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/116.0.5845.96 Safari/537.36"); + "Chrome/118.0.5993.117 Safari/537.36"); return kResult; } diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 6099314be..9d58ad590 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -295,11 +295,11 @@ MTPInputInvoice Form::inputInvoice() const { return MTP_inputInvoicePremiumGiftCode( MTP_inputStorePaymentPremiumGiftCode( MTP_flags(users->boostPeer ? Flag::f_boost_peer : Flag()), - MTP_vector(ranges::views::all( + MTP_vector_from_range(ranges::views::all( users->users ) | ranges::views::transform([](not_null user) { return MTPInputUser(user->inputUser); - }) | ranges::to), + })), users->boostPeer ? users->boostPeer->input : MTPInputPeer(), MTP_string(giftCode.currency), MTP_long(giftCode.amount)), @@ -321,16 +321,16 @@ MTPInputInvoice Form::inputInvoice() const { ? Flag() : Flag::f_countries_iso2)), giveaway.boostPeer->input, - MTP_vector(ranges::views::all( + MTP_vector_from_range(ranges::views::all( giveaway.additionalChannels ) | ranges::views::transform([](not_null c) { return MTPInputPeer(c->input); - }) | ranges::to()), - MTP_vector(ranges::views::all( + })), + MTP_vector_from_range(ranges::views::all( giveaway.countries ) | ranges::views::transform([](QString value) { return MTP_string(value); - }) | ranges::to()), + })), MTP_long(giftCode.randomId), MTP_int(giveaway.untilDate), MTP_string(giftCode.currency), diff --git a/Telegram/SourceFiles/platform/linux/tray_linux.cpp b/Telegram/SourceFiles/platform/linux/tray_linux.cpp index 2a96e1f1b..a9e1de383 100644 --- a/Telegram/SourceFiles/platform/linux/tray_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/tray_linux.cpp @@ -58,7 +58,7 @@ private: const QString _mutePanelTrayIconName; const QString _attentionPanelTrayIconName; - const int _iconSizes[5]; + const int _iconSizes[7]; bool _muted = true; int32 _count = 0; @@ -73,7 +73,7 @@ IconGraphic::IconGraphic() : _panelTrayIconName("telegram-panel") , _mutePanelTrayIconName("telegram-mute-panel") , _attentionPanelTrayIconName("telegram-attention-panel") -, _iconSizes{ 16, 22, 24, 32, 48 } { +, _iconSizes{ 16, 22, 32, 48, 64, 128, 256 } { } IconGraphic::~IconGraphic() = default; @@ -214,46 +214,13 @@ QIcon IconGraphic::trayIcon( } } - auto iconImage = currentImageBack; - - if (counter > 0) { - const auto &bg = muted - ? st::trayCounterBgMute - : st::trayCounterBg; - const auto &fg = st::trayCounterFg; - if (iconSize >= 22) { - const auto imageSize = dprSize(iconImage); - const auto layerSize = (iconSize >= 48) - ? 32 - : (iconSize >= 36) - ? 24 - : (iconSize >= 32) - ? 20 - : 16; - const auto layer = Window::GenerateCounterLayer({ - .size = layerSize, - .devicePixelRatio = iconImage.devicePixelRatio(), - .count = counter, - .bg = bg, - .fg = fg, - }); - - QPainter p(&iconImage); - p.drawImage( - imageSize.width() - layer.width() - 1, - imageSize.height() - layer.height() - 1, - layer); - } else { - iconImage = Window::WithSmallCounter(std::move(iconImage), { - .size = 16, - .count = counter, - .bg = bg, - .fg = fg, - }); - } - } - - result.addPixmap(Ui::PixmapFromImage(std::move(iconImage))); + result.addPixmap(Ui::PixmapFromImage(counter > 0 + ? Window::WithSmallCounter(std::move(currentImageBack), { + .size = iconSize, + .count = counter, + .bg = muted ? st::trayCounterBgMute : st::trayCounterBg, + .fg = st::trayCounterFg, + }) : std::move(currentImageBack))); } updateIconRegenerationNeeded( diff --git a/Telegram/SourceFiles/platform/win/tray_win.cpp b/Telegram/SourceFiles/platform/win/tray_win.cpp index 49d3cccac..abccadea7 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.cpp +++ b/Telegram/SourceFiles/platform/win/tray_win.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "main/main_session.h" #include "storage/localstorage.h" +#include "ui/painter.h" #include "ui/ui_utility.h" #include "ui/widgets/popup_menu.h" #include "window/window_controller.h" @@ -23,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include +#include // AyuGram includes #include "ayu/ui/ayu_assets.h" @@ -66,48 +68,67 @@ constexpr auto kTooltipDelay = crl::time(10000); return (value == 0); } +[[nodiscard]] QImage MonochromeIconFor(int size, bool darkMode) { + Expects(size > 0); + + static const auto Content = [&] { + auto f = QFile(u":/gui/icons/tray/monochrome.svg"_q); + f.open(QIODevice::ReadOnly); + return f.readAll(); + }(); + static auto Mask = QImage(); + static auto Size = 0; + if (Mask.isNull() || Size != size) { + Size = size; + Mask = QImage(size, size, QImage::Format_ARGB32_Premultiplied); + Mask.fill(Qt::transparent); + auto p = QPainter(&Mask); + QSvgRenderer(Content).render(&p, QRectF(0, 0, size, size)); + } + static auto Colored = QImage(); + static auto ColoredDark = QImage(); + auto &use = darkMode ? ColoredDark : Colored; + if (use.size() != Mask.size()) { + const auto color = darkMode ? 255 : 0; + const auto alpha = darkMode ? 255 : 228; + use = style::colorizeImage(Mask, { color, color, color, alpha }); + } + return use; +} + +[[nodiscard]] QImage MonochromeWithDot(QImage image, style::color color) { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + const auto xm = image.width() / 16.; + const auto ym = image.height() / 16.; + p.setBrush(color); + p.setPen(Qt::NoPen); + p.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2 + 1.7 * xm, + 10.5 * ym, + 4.4 * xm, + 4.4 * ym)); + return image; +} + [[nodiscard]] QImage ImageIconWithCounter( Window::CounterLayerArgs &&args, bool supportMode, bool smallIcon, bool monochrome) { - static constexpr auto kCount = 3; - static auto ScaledLogo = std::array(); - static auto ScaledLogoNoMargin = std::array(); - static auto ScaledLogoDark = std::array(); - static auto ScaledLogoLight = std::array(); + static auto ScaledLogo = base::flat_map(); + static auto ScaledLogoNoMargin = base::flat_map(); + static auto ScaledLogoDark = base::flat_map(); + static auto ScaledLogoLight = base::flat_map(); static auto lastUsedIcon = currentAppLogoName(); if (lastUsedIcon != currentAppLogoName()) { - ScaledLogo = std::array(); - ScaledLogoNoMargin = std::array(); + ScaledLogo = base::flat_map(); + ScaledLogoNoMargin = base::flat_map(); + ScaledLogoDark = base::flat_map(); + ScaledLogoLight = base::flat_map(); } - struct Dimensions { - int index = 0; - int size = 0; - }; - const auto d = [&]() -> Dimensions { - switch (args.size) { - case 16: - return { - .index = 0, - .size = 16, - }; - case 32: - return { - .index = 1, - .size = 32, - }; - default: - return { - .index = 2, - .size = 64, - }; - } - }(); - Assert(d.index < kCount); - const auto darkMode = IsDarkTaskbar(); auto &scaled = (monochrome && darkMode) ? (*darkMode @@ -118,37 +139,18 @@ constexpr auto kTooltipDelay = crl::time(10000); : ScaledLogo; auto result = [&] { - auto &image = scaled[d.index]; - if (image.isNull()) { - if (monochrome && darkMode) { - const auto withColor = [&](QColor color) { - switch (d.size) { - case 16: - return st::macTrayIcon.instance(color, 100 / cIntRetinaFactor()); - case 32: - return st::macTrayIcon.instance(color, 200 / cIntRetinaFactor()); - default: - return st::macTrayIcon.instance(color, 300 / cIntRetinaFactor()); - } - }; - const auto darkModeResult = withColor({ 255, 255, 255 }); - const auto lightModeResult = withColor({ 0, 0, 0, 228 }); - image = *darkMode ? darkModeResult : lightModeResult; - const auto monochromeMargin = QPoint( - (image.width() - d.size) / 2, - (image.height() - d.size) / 2); - image = image.copy( - QRect(monochromeMargin, QSize(d.size, d.size))); - image.setDevicePixelRatio(1); - } else { - image = (smallIcon - ? Window::LogoNoMargin() - : Window::Logo()).scaledToWidth( - d.size, - Qt::SmoothTransformation); - } + if (const auto it = scaled.find(args.size); it != scaled.end()) { + return it->second; + } else if (monochrome && darkMode) { + return MonochromeIconFor(args.size, *darkMode); } - return image; + return scaled.emplace( + args.size, + (smallIcon + ? Window::LogoNoMargin() + : Window::Logo() + ).scaledToWidth(args.size, Qt::SmoothTransformation) + ).first->second; }(); if ((!monochrome || !darkMode) && supportMode) { Window::ConvertIconToBlack(result); @@ -156,11 +158,14 @@ constexpr auto kTooltipDelay = crl::time(10000); if (!args.count) { return result; } else if (smallIcon) { + if (monochrome && darkMode) { + return MonochromeWithDot(std::move(result), args.bg); + } return Window::WithSmallCounter(std::move(result), std::move(args)); } QPainter p(&result); PainterHighQualityEnabler hq(p); // AyuGram: fix for lq icons - const auto half = d.size / 2; + const auto half = args.size / 2; args.size = half; p.drawPixmap( half, @@ -225,36 +230,24 @@ void Tray::updateIcon() { if (!_icon) { return; } - const auto counter = Core::App().unreadBadge(); - const auto muted = Core::App().unreadBadgeMuted(); const auto controller = Core::App().activePrimaryWindow(); const auto session = !controller ? nullptr : !controller->sessionController() ? nullptr : &controller->sessionController()->session(); - const auto monochrome = Core::App().settings().trayIconMonochrome(); - const auto supportMode = session && session->supportMode(); - const auto iconSizeWidth = GetSystemMetrics(SM_CXSMICON); - auto iconSmallPixmap16 = Tray::IconWithCounter( - CounterLayerArgs(16, counter, muted), - true, - monochrome, - supportMode); - auto iconSmallPixmap32 = Tray::IconWithCounter( - CounterLayerArgs(32, counter, muted), - true, - monochrome, - supportMode); - auto iconSmall = QIcon(); - iconSmall.addPixmap(iconSmallPixmap16); - iconSmall.addPixmap(iconSmallPixmap32); // Force Qt to use right icon size, not the larger one. QIcon forTrayIcon; - forTrayIcon.addPixmap(iconSizeWidth >= 20 - ? iconSmallPixmap32 - : iconSmallPixmap16); + forTrayIcon.addPixmap( + Tray::IconWithCounter( + CounterLayerArgs( + GetSystemMetrics(SM_CXSMICON), + Core::App().unreadBadge(), + Core::App().unreadBadgeMuted()), + true, + Core::App().settings().trayIconMonochrome(), + session && session->supportMode())); _icon->updateIcon(forTrayIcon); } diff --git a/Telegram/SourceFiles/settings/settings_intro.cpp b/Telegram/SourceFiles/settings/settings_intro.cpp index 83c930c1a..6aecfc0b9 100644 --- a/Telegram/SourceFiles/settings/settings_intro.cpp +++ b/Telegram/SourceFiles/settings/settings_intro.cpp @@ -63,7 +63,7 @@ object_ptr CreateIntroSettings( AddDivider(result); AddSkip(result); - SetupLanguageButton(window, result, false); + SetupLanguageButton(window, result); SetupConnectionType(window, &window->account(), result); AddSkip(result); if (HasUpdate()) { diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 03118bdfc..6ea96b63d 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -276,8 +276,7 @@ void SetupPowerSavingButton( void SetupLanguageButton( not_null window, - not_null container, - bool icon) { + not_null container) { const auto button = AddButtonWithLabel( container, tr::lng_settings_language(), @@ -286,8 +285,8 @@ void SetupLanguageButton( ) | rpl::then( Lang::GetInstance().idChanges() ) | rpl::map([] { return Lang::GetInstance().nativeName(); }), - icon ? st::settingsButton : st::settingsButtonNoIcon, - { icon ? &st::menuIconTranslate : nullptr }); + st::settingsButton, + { &st::menuIconTranslate }); const auto guard = Ui::CreateChild(button.get()); button->addClickHandler([=] { const auto m = button->clickModifiers(); diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index 4c1529d7d..10220623d 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -22,8 +22,7 @@ namespace Settings { void SetupLanguageButton( not_null window, - not_null container, - bool icon = true); + not_null container); bool HasInterfaceScale(); void SetupInterfaceScale( not_null window, diff --git a/Telegram/SourceFiles/statistics/statistics.style b/Telegram/SourceFiles/statistics/statistics.style index f797dbc1a..036fb16e5 100644 --- a/Telegram/SourceFiles/statistics/statistics.style +++ b/Telegram/SourceFiles/statistics/statistics.style @@ -142,3 +142,26 @@ boostsButton: SettingsButton(defaultSettingsButton) { textFg: lightButtonFg; textFgOver: lightButtonFgOver; } + +getBoostsButton: SettingsButton(reportReasonButton) { + textFg: lightButtonFg; + textFgOver: lightButtonFg; +} +getBoostsButtonIcon: icon {{ "menu/gift_premium", lightButtonFg }}; + +boostsListMiniIcon: icon{{ "boosts/boost_mini2", premiumButtonFg }}; +boostsListMiniIconPadding: margins(1px, 0px, 0px, 0px); +boostsListMiniIconSkip: 1px; +boostsListBadgeTextPadding: margins(16px, 1px, 6px, 0px); +boostsListBadgePadding: margins(4px, 1px, 4px, 0px); +boostsListBadgeHeight: 16px; + +boostsListRightBadgeTextStyle: TextStyle(defaultTextStyle) { + font: font(12px semibold); +} +boostsListRightBadgeTextPadding: margins(16px, 1px, 6px, 0px); +boostsListRightBadgePadding: margins(4px, 5px, 8px, 0px); +boostsListRightBadgeHeight: 20px; +boostsListGiftMiniIconPadding: margins(1px, 2px, 0px, 0px); +boostsListGiftMiniIcon: icon{{ "boosts/boost_mini2", historyPeer8UserpicBg2 }}; +boostsListGiveawayMiniIcon: icon{{ "boosts/boost_mini2", historyPeer4UserpicBg2 }}; diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 408b54c4f..e9ffe759d 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "styles/style_giveaway.h" #include "styles/style_layers.h" #include "styles/style_premium.h" @@ -62,6 +63,14 @@ void BoostBox( data.boost, st::boxRowPadding); + { + const auto &d = data.boost; + if (!d.nextLevelBoosts + || ((d.thisLevelBoosts == d.boosts) && d.mine)) { + --data.boost.level; + } + } + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); const auto name = data.name; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 70018cad1..d4f189ac0 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -32,6 +32,12 @@ historyReplyBottom: 2px; historyReplyPreview: 32px; historyReplyPreviewMargin: margins(7px, 4px, 4px, 4px); historyReplyPadding: margins(11px, 2px, 6px, 2px); +historyReplyUser: icon {{ "chat/reply_type_user", windowFg }}; +historyReplyUserPadding: margins(0px, 4px, 4px, 0px); +historyReplyGroup: icon {{ "chat/reply_type_group", windowFg }}; +historyReplyGroupPadding: margins(0px, 4px, 4px, 0px); +historyReplyChannel: icon {{ "chat/reply_type_channel", windowFg }}; +historyReplyChannelPadding: margins(0px, 5px, 4px, 0px); msgReplyPadding: margins(6px, 6px, 11px, 6px); msgReplyBarPos: point(1px, 0px); diff --git a/Telegram/SourceFiles/ui/chat/message_bar.cpp b/Telegram/SourceFiles/ui/chat/message_bar.cpp index d6b441771..47f7056e4 100644 --- a/Telegram/SourceFiles/ui/chat/message_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/message_bar.cpp @@ -455,7 +455,7 @@ void MessageBar::paint(Painter &p) { .now = now, .pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = pausedSpoiler, - .elisionOneLine = true, + .elisionLines = 1, }); } } else if (_animation->bodyAnimation == BodyAnimation::Text) { diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 1891e26e0..46305a2c7 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -276,83 +276,3 @@ boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px); boostReplaceArrow: icon{{ "mediaview/next", windowSubTextFg }}; - -giveawayGiftCodeTopHeight: 195px; -giveawayGiftCodeLink: FlatLabel(defaultFlatLabel) { - margin: margins(10px, 12px, 10px, 8px); - textFg: menuIconColor; - maxHeight: 24px; -} -giveawayGiftCodeLinkCopy: icon{{ "menu/copy", menuIconColor }}; -giveawayGiftCodeLinkHeight: 42px; -giveawayGiftCodeLinkCopyWidth: 40px; -giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px); - -boostLinkStatsButton: IconButton(defaultIconButton) { - width: giveawayGiftCodeLinkCopyWidth; - height: giveawayGiftCodeLinkHeight; - icon: icon{{ "menu/stats", menuIconColor }}; - iconOver: icon{{ "menu/stats", menuIconColor }}; - ripple: emptyRippleAnimation; -} - -giveawayGiftCodeTable: Table(defaultTable) { - labelMinWidth: 91px; -} -giveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px); -giveawayGiftCodeLabel: FlatLabel(defaultFlatLabel) { - textFg: menuIconColor; - maxHeight: 24px; - style: TextStyle(semiboldTextStyle) { - font: font(12px semibold); - } -} -giveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px); -giveawayGiftCodeValue: FlatLabel(defaultFlatLabel) { - maxHeight: 24px; - style: TextStyle(defaultTextStyle) { - font: font(12px); - linkUnderline: kLinkUnderlineNever; - } -} -giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px); -giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px); -giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) { - size: size(24px, 24px); - photoSize: 24px; - photoPosition: point(-1px, -1px); -} -giveawayGiftCodeNamePosition: point(32px, 4px); -giveawayGiftCodeCover: PremiumCover(userPremiumCover) { - starSize: size(92px, 90px); - starTopSkip: 20px; - titlePadding: margins(0px, 15px, 0px, 17px); - titleFont: font(15px semibold); - about: FlatLabel(userPremiumCoverAbout) { - textFg: windowBoldFg; - style: TextStyle(premiumAboutTextStyle) { - lineHeight: 17px; - } - } -} -giveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) { - align: align(top); - textFg: windowBoldFg; -} -giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px); -giveawayGiftCodeBox: Box(defaultBox) { - buttonPadding: margins(22px, 11px, 22px, 22px); - buttonHeight: 42px; - button: RoundButton(defaultActiveButton) { - height: 42px; - textTop: 12px; - font: font(13px semibold); - } - shadowIgnoreTopSkip: true; -} -giveawayRefundedLabel: FlatLabel(boxLabel) { - align: align(top); - style: semiboldTextStyle; - textFg: attentionButtonFg; -} -giveawayRefundedPadding: margins(8px, 10px, 8px, 10px); diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index 310f2291d..c7c8cadbe 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -1320,6 +1320,7 @@ void AddGiftOptions( stCheckbox, std::move(radioView)); radio->setAttribute(Qt::WA_TransparentForMouseEvents); + radio->show(); { // Paint the last frame instantly for the layer animation. group->setValue(0); radio->finishAnimating(); diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h index e1513e693..e3ae54986 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h @@ -29,7 +29,7 @@ public: void setActiveSectionFast(int index); void finishAnimating(); - auto sectionActivated() const { + [[nodiscard]] rpl::producer sectionActivated() const { return _sectionActivated.events(); } diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index 1a5c6ef65..0f2dc81cf 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -285,31 +285,12 @@ QImage WithSmallCounter(QImage image, CounterLayerArgs &&args) { int delta = 0; int radius = 0; }; - const auto d = [&]() -> Dimensions { - switch (args.size.value()) { - case 16: - return { - .size = 16, - .font = 8, - .delta = ((textSize < 2) ? 2 : 1), - .radius = ((textSize < 2) ? 4 : 3), - }; - case 32: - return { - .size = 32, - .font = 12, - .delta = ((textSize < 2) ? 5 : 2), - .radius = ((textSize < 2) ? 8 : 7), - }; - default: - return { - .size = 64, - .font = 22, - .delta = ((textSize < 2) ? 9 : 4), - .radius = ((textSize < 2) ? 16 : 14), - }; - } - }(); + const auto d = Dimensions{ + .size = args.size.value(), + .font = args.size.value() / 2, + .delta = args.size.value() / ((textSize < 2) ? 8 : 16), + .radius = args.size.value() / ((textSize < 2) ? 4 : 5), + }; auto p = QPainter(&image); auto hq = PainterHighQualityEnabler(p); diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 392956993..0fa144372 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -847,7 +847,7 @@ void Notification::paintTitle(Painter &p) { .spoiler = Ui::Text::DefaultSpoilerCache(), .pausedEmoji = On(PowerSaving::kEmojiChat), .pausedSpoiler = On(PowerSaving::kChatSpoiler), - .elisionOneLine = true, + .elisionLines = 1, }); } diff --git a/Telegram/Telegram.plist b/Telegram/Telegram.plist index ea6395228..53b8daf8b 100644 --- a/Telegram/Telegram.plist +++ b/Telegram/Telegram.plist @@ -3,15 +3,19 @@ CFBundleExecutable - $(PRODUCT_NAME) + @output_name@ CFBundleGetInfoString Telegram Desktop messaging app + CFBundleIconFile + Icon.icns + CFBundleIconName + Icon.icns CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) + @bundle_identifier_plist@ CFBundlePackageType APPL CFBundleShortVersionString - $(CURRENT_PROJECT_VERSION) + @desktop_app_version_string@ CFBundleSignature ???? CFBundleURLTypes @@ -22,7 +26,7 @@ CFBundleURLIconFile Icon.icns CFBundleURLName - $(PRODUCT_BUNDLE_IDENTIFIER) + @bundle_identifier_plist@ CFBundleURLSchemes tg @@ -30,13 +34,13 @@ CFBundleVersion - $(CURRENT_PROJECT_VERSION) + @desktop_app_version_string@ ITSAppUsesNonExemptEncryption LSApplicationCategoryType public.app-category.social-networking LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) + @CMAKE_OSX_DEPLOYMENT_TARGET@ LSFileQuarantineEnabled NOTE diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index c39d9b3c3..8cd67af64 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -265,7 +265,7 @@ def winFailOnEach(command): startingCommand = True for command in commands: command = re.sub(r'\$([A-Za-z0-9_]+)', r'%\1%', command) - if re.search(r'\$', command): + if re.search(r'\$[^<]', command): error('Bad command: ' + command) appendCall = startingCommand and not re.match(r'(if|for) ', command) called = 'call ' + command if appendCall else command @@ -418,7 +418,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 81a81ffb5a + git checkout f603f4f986 """) stage('msys64', """ @@ -501,7 +501,7 @@ release: stage('xz', """ !win: - git clone -b v5.2.9 https://git.tukaani.org/xz.git + git clone -b v5.4.5 https://git.tukaani.org/xz.git cd xz sed -i '' '\\@check_symbol_exists(futimens "sys/types.h;sys/stat.h" HAVE_FUTIMENS)@d' CMakeLists.txt CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake -B build . \\ @@ -513,11 +513,12 @@ stage('xz', """ """) stage('zlib', """ - git clone -b v1.2.11 https://github.com/madler/zlib.git + git clone -b v1.3 https://github.com/madler/zlib.git cd zlib win: cmake . ^ -A %WIN32X64% ^ + -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ -DCMAKE_C_FLAGS="/DZLIB_WINAPI" @@ -534,7 +535,7 @@ mac: """) stage('mozjpeg', """ - git clone -b v4.1.3 https://github.com/mozilla/mozjpeg.git + git clone -b v4.1.5 https://github.com/mozilla/mozjpeg.git cd mozjpeg win: cmake . ^ @@ -621,6 +622,7 @@ win: cmake -B out . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ + -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" cmake --build out --config Debug @@ -729,6 +731,7 @@ win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ + -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ -DBUILD_SHARED_LIBS=OFF ^ @@ -748,6 +751,7 @@ win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ + -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS="/DLIBDE265_STATIC_BUILD" ^ -DCMAKE_CXX_FLAGS="/DLIBDE265_STATIC_BUILD" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ @@ -774,6 +778,7 @@ win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ + -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_CXX_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" ^ @@ -800,6 +805,7 @@ win: cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ + -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" ^ -DCMAKE_C_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^ -DCMAKE_CXX_FLAGS="/DJXL_STATIC_DEFINE /DJXL_THREADS_STATIC_DEFINE" ^ -DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^ @@ -900,9 +906,8 @@ depends:yasm/yasm """) stage('libwebp', """ - git clone https://github.com/webmproject/libwebp.git + git clone -b v1.3.2 https://github.com/webmproject/libwebp.git cd libwebp - git checkout chrome-m116-5845 win: nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all nmake /f Makefile.vc CFG=release-static OBJDIR=out RTLIBCFG=static all @@ -1211,7 +1216,7 @@ stage('crashpad', """ mac: git clone https://github.com/desktop-app/crashpad.git cd crashpad - git checkout f07f49e287 + git checkout 3279fae3f0 git submodule init git submodule update third_party/mini_chromium ZLIB_PATH=$USED_PREFIX/include @@ -1294,23 +1299,23 @@ release: """) if buildQt5: - stage('qt_5_15_10', """ - git clone https://github.com/qt/qt5.git qt_5_15_10 - cd qt_5_15_10 + stage('qt_5_15_11', """ + git clone https://github.com/qt/qt5.git qt_5_15_11 + cd qt_5_15_11 perl init-repository --module-subset=qtbase,qtimageformats,qtsvg - git checkout v5.15.10-lts-lgpl + git checkout v5.15.11-lts-lgpl git submodule update qtbase qtimageformats qtsvg -depends:patches/qtbase_5.15.10/*.patch +depends:patches/qtbase_5.15.11/*.patch cd qtbase win: - for /r %%i in (..\\..\\patches\\qtbase_5.15.10\\*) do git apply %%i + for /r %%i in (..\\..\\patches\\qtbase_5.15.11\\*) do git apply %%i -v cd .. SET CONFIGURATIONS=-debug release: SET CONFIGURATIONS=-debug-and-release win: - """ + removeDir("\"%LIBS_DIR%\\Qt-5.15.10\"") + """ + """ + removeDir("\"%LIBS_DIR%\\Qt-5.15.11\"") + """ SET ANGLE_DIR=%LIBS_DIR%\\tg_angle SET ANGLE_LIBS_DIR=%ANGLE_DIR%\\out SET MOZJPEG_DIR=%LIBS_DIR%\\mozjpeg @@ -1318,7 +1323,7 @@ win: SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\out SET ZLIB_LIBS_DIR=%LIBS_DIR%\\zlib SET WEBP_DIR=%LIBS_DIR%\\libwebp - configure -prefix "%LIBS_DIR%\\Qt-5.15.10" ^ + configure -prefix "%LIBS_DIR%\\Qt-5.15.11" ^ %CONFIGURATIONS% ^ -force-debug-info ^ -opensource ^ @@ -1353,14 +1358,14 @@ win: jom -j16 jom -j16 install mac: - find ../../patches/qtbase_5.15.10 -type f -print0 | sort -z | xargs -0 git apply + find ../../patches/qtbase_5.15.11 -type f -print0 | sort -z | xargs -0 git apply cd .. CONFIGURATIONS=-debug release: CONFIGURATIONS=-debug-and-release mac: - ./configure -prefix "$USED_PREFIX/Qt-5.15.10" \ + ./configure -prefix "$USED_PREFIX/Qt-5.15.11" \ $CONFIGURATIONS \ -force-debug-info \ -opensource \ @@ -1381,14 +1386,14 @@ mac: """) if buildQt6: - stage('qt_6_2_5', """ + stage('qt_6_2_6', """ mac: - git clone -b v6.2.5-lts-lgpl https://code.qt.io/qt/qt5.git qt_6_2_5 - cd qt_6_2_5 + git clone -b v6.2.6-lts-lgpl https://code.qt.io/qt/qt5.git qt_6_2_6 + cd qt_6_2_6 perl init-repository --module-subset=qtbase,qtimageformats,qtsvg -depends:patches/qtbase_6.2.5/*.patch +depends:patches/qtbase_6.2.6/*.patch cd qtbase - find ../../patches/qtbase_6.2.5 -type f -print0 | sort -z | xargs -0 git apply + find ../../patches/qtbase_6.2.6 -type f -print0 | sort -z | xargs -0 git apply -v cd .. sed -i.bak 's/tqtc-//' {qtimageformats,qtsvg}/dependencies.yaml @@ -1396,7 +1401,7 @@ depends:patches/qtbase_6.2.5/*.patch release: CONFIGURATIONS=-debug-and-release mac: - ./configure -prefix "$USED_PREFIX/Qt-6.2.5" \ + ./configure -prefix "$USED_PREFIX/Qt-6.2.6" \ $CONFIGURATIONS \ -force-debug-info \ -opensource \ diff --git a/Telegram/build/version b/Telegram/build/version index 1c347fd92..0ba84356e 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 4011003 +AppVersion 4011005 AppVersionStrMajor 4.11 -AppVersionStrSmall 4.11.3 -AppVersionStr 4.11.3 +AppVersionStrSmall 4.11.5 +AppVersionStr 4.11.5 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 4.11.3 +AppVersionOriginal 4.11.5 diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 1f7bf651b..13252621c 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -26,6 +26,7 @@ set(style_files calls/calls.style export/view/export.style info/info.style + info/boosts/giveaway/giveaway.style info/userpic/info_userpic_builder.style intro/intro.style media/player/media_player.style @@ -112,6 +113,11 @@ PRIVATE info/userpic/info_userpic_emoji_builder_layer.cpp info/userpic/info_userpic_emoji_builder_layer.h + info/boosts/giveaway/giveaway_type_row.cpp + info/boosts/giveaway/giveaway_type_row.h + info/boosts/giveaway/select_countries_box.cpp + info/boosts/giveaway/select_countries_box.h + layout/abstract_layout_item.cpp layout/abstract_layout_item.h layout/layout_mosaic.cpp diff --git a/changelog.txt b/changelog.txt index b053efcf0..c68073a13 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,18 @@ +4.11.5 (06.11.23) + +- Giveaway phrases and sticker fixes. +- Show quoted part in channel comments. +- Show replies with icons and multiline preview. +- Send correct replies in topics and channel comments. +- In monochrome Windows tray icon use dot instead of counter. + +4.11.4 beta (06.11.23) + +- Show quoted part in channel comments. +- Show replies with icons and multiline preview. +- Send correct replies in topics and channel comments. +- In monochrome Windows tray icon use dot instead of counter. + 4.11.3 (02.11.23) - Fix adding a link to media captions in scheduled / comments. diff --git a/cmake b/cmake index 60474a576..78098ede7 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 60474a576690f63510d87d18ca348d9875c89f08 +Subproject commit 78098ede77a09e41da6233d765d02b43a60e7138