Merge tag 'v4.11.5' into dev

# Conflicts:
#	Telegram/Resources/winrc/Telegram.rc
#	Telegram/Resources/winrc/Updater.rc
#	Telegram/SourceFiles/core/version.h
#	Telegram/SourceFiles/platform/win/tray_win.cpp
#	Telegram/lib_ui
This commit is contained in:
ZavaruKitsu 2023-11-08 21:33:14 +03:00
commit 4b28e910e8
119 changed files with 4356 additions and 1285 deletions

158
.github/workflows/mac_packaged.yml vendored Normal file
View file

@ -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/

View file

@ -59,9 +59,9 @@ include(cmake/options.cmake)
if (NOT DESKTOP_APP_USE_PACKAGED) if (NOT DESKTOP_APP_USE_PACKAGED)
if (WIN32) if (WIN32)
set(qt_version 5.15.10) set(qt_version 5.15.11)
elseif (APPLE) elseif (APPLE)
set(qt_version 6.2.5) set(qt_version 6.2.6)
endif() endif()
endif() endif()
include(cmake/external/qt/package.cmake) include(cmake/external/qt/package.cmake)

View file

@ -837,6 +837,8 @@ PRIVATE
history/view/history_view_quick_action.h history/view/history_view_quick_action.h
history/view/history_view_replies_section.cpp history/view/history_view_replies_section.cpp
history/view/history_view_replies_section.h 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.cpp
history/view/history_view_requests_bar.h history/view/history_view_requests_bar.h
history/view/history_view_schedule_box.cpp history/view/history_view_schedule_box.cpp
@ -891,6 +893,10 @@ PRIVATE
history/history_view_highlight_manager.h history/history_view_highlight_manager.h
history/history_widget.cpp history/history_widget.cpp
history/history_widget.h 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.cpp
info/boosts/info_boosts_inner_widget.h info/boosts/info_boosts_inner_widget.h
info/boosts/info_boosts_widget.cpp info/boosts/info_boosts_widget.cpp
@ -1609,7 +1615,39 @@ elseif (APPLE)
endif() endif()
set(icons_path ${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Images.xcassets) 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 set(lang_packs
en en
@ -1636,24 +1674,17 @@ elseif (APPLE)
COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $<TARGET_FILE_DIR:Telegram>/../Resources COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $<TARGET_FILE_DIR:Telegram>/../Resources
COMMAND cp ${CMAKE_BINARY_DIR}/lib_spellcheck.rcc $<TARGET_FILE_DIR:Telegram>/../Resources COMMAND cp ${CMAKE_BINARY_DIR}/lib_spellcheck.rcc $<TARGET_FILE_DIR:Telegram>/../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 add_custom_command(TARGET Telegram
PRE_LINK PRE_LINK
COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Frameworks COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Helpers
COMMAND cp $<TARGET_FILE:Updater> $<TARGET_FILE_DIR:Telegram>/../Frameworks/ COMMAND cp ${libs_loc}/crashpad/out/$<IF:$<CONFIG:Debug>,Debug,Release>${crashpad_dir_part}/crashpad_handler $<TARGET_FILE_DIR:Telegram>/../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 $<TARGET_FILE_DIR:Telegram>/../Helpers
COMMAND cp ${libs_loc}/crashpad/out/$<IF:$<CONFIG:Debug>,Debug,Release>${crashpad_dir_part}/crashpad_handler $<TARGET_FILE_DIR:Telegram>/../Helpers/
)
endif()
endif() endif()
else() else()
target_link_libraries(Telegram target_link_libraries(Telegram
@ -1702,7 +1733,11 @@ if (build_macstore)
COMMAND rm -rf $<TARGET_FILE_DIR:Telegram>/../Frameworks/Breakpad.framework/Resources/Inspector COMMAND rm -rf $<TARGET_FILE_DIR:Telegram>/../Frameworks/Breakpad.framework/Resources/Inspector
) )
else() else()
set(bundle_identifier "com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>") if (CMAKE_GENERATOR STREQUAL Xcode)
set(bundle_identifier "com.tdesktop.Telegram$<$<CONFIG:Debug>:Debug>")
else()
set(bundle_identifier "com.tdesktop.Telegram")
endif()
set(bundle_entitlements "Telegram.entitlements") set(bundle_entitlements "Telegram.entitlements")
if (LINUX AND DESKTOP_APP_USE_PACKAGED) if (LINUX AND DESKTOP_APP_USE_PACKAGED)
set(output_name "ayugram-desktop") set(output_name "ayugram-desktop")
@ -1711,6 +1746,12 @@ else()
endif() endif()
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 set_target_properties(Telegram PROPERTIES
OUTPUT_NAME ${output_name} OUTPUT_NAME ${output_name}
MACOSX_BUNDLE_GUI_IDENTIFIER ${bundle_identifier} MACOSX_BUNDLE_GUI_IDENTIFIER ${bundle_identifier}
@ -1784,7 +1825,7 @@ endif()
target_prepare_qrc(Telegram) 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) add_executable(Updater WIN32)
init_non_host_target(Updater) init_non_host_target(Updater)
@ -1822,6 +1863,12 @@ if ((NOT DESKTOP_APP_DISABLE_AUTOUPDATE OR APPLE) AND NOT build_macstore AND NOT
else() else()
target_link_options(Updater PRIVATE -municode) target_link_options(Updater PRIVATE -municode)
endif() endif()
elseif (APPLE)
add_custom_command(TARGET Updater
PRE_LINK
COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Frameworks
COMMAND cp $<TARGET_FILE:Updater> $<TARGET_FILE_DIR:Telegram>/../Frameworks/
)
endif() endif()
if (DESKTOP_APP_SPECIAL_TARGET) if (DESKTOP_APP_SPECIAL_TARGET)

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="plane" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M1.3311718,6.36592184 C5.3576954,4.67244493 8.04267511,3.5560013 9.38611094,3.01659096 C13.2218932,1.47646481 14.0189359,1.2089284 14.5384372,1.2 C14.6526967,1.19815119 14.9081723,1.22548649 15.0736587,1.35511219 C15.2133922,1.4645656 15.2518384,1.61242159 15.2702362,1.71619544 C15.288634,1.81996929 15.3115436,2.05636876 15.2933322,2.24108442 C15.0854698,4.34939964 14.1860526,9.46572464 13.7284802,11.8270738 C13.5348641,12.8262491 13.1536281,13.1612675 12.7845475,13.1940535 C11.9824498,13.265305 11.3733733,12.6823476 10.5965026,12.190753 C9.3808532,11.4215044 8.69408865,10.9426448 7.51409044,10.1920004 C6.15039834,9.32450079 7.03442319,8.84770795 7.81158733,8.06849502 C8.01497489,7.86457129 11.5490353,4.7615061 11.6174372,4.48000946 C11.625992,4.44480359 11.6339313,4.31357282 11.5531696,4.24427815 C11.472408,4.17498349 11.3532107,4.19867957 11.2671947,4.21752527 C11.1452695,4.24423848 9.20325394,5.48334063 5.44114787,7.93483171 C4.88991321,8.30022994 4.39062196,8.47826423 3.94327414,8.46893456 C3.45010907,8.45864936 2.50145729,8.19975808 1.79623221,7.97846422 C0.931244952,7.70703829 0.243770289,7.56353344 0.303633888,7.10256824 C0.334814555,6.86246904 0.677327192,6.61692024 1.3311718,6.36592184 Z" id="Path-3" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -2078,9 +2078,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_award_subtitle" = "Select recipients >"; "lng_giveaway_award_subtitle" = "Select recipients >";
"lng_giveaway_award_chosen#one" = "{count} recipient >"; "lng_giveaway_award_chosen#one" = "{count} recipient >";
"lng_giveaway_award_chosen#other" = "{count} recipients >"; "lng_giveaway_award_chosen#other" = "{count} recipients >";
"lng_giveaway_quantity_title" = "Quantity of prizes / boosts"; "lng_giveaway_quantity_title" = "Quantity of prizes";
"lng_giveaway_quantity#one" = "{count} Subscription / Boost"; "lng_giveaway_quantity#one" = "{count} boost";
"lng_giveaway_quantity#other" = "{count} Subscriptions / Boosts"; "lng_giveaway_quantity#other" = "{count} boosts";
"lng_giveaway_quantity_about" = "Choose how many Premium subscriptions to give away and boosts to receive."; "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_title" = "Channels included in the giveaway";
"lng_giveaway_channels_this#one" = "this channel will receive {count} boost"; "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_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_title" = "Users eligible for the giveaway";
"lng_giveaway_users_all" = "All subscribers"; "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_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_start" = "Start Giveaway";
"lng_giveaway_award" = "Gift Premium"; "lng_giveaway_award" = "Gift Premium";
"lng_giveaway_date_title" = "Date when giveaway ends"; "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#one" = "Duration of Premium subscription";
"lng_giveaway_duration_title#other" = "Duration of Premium subscriptions"; "lng_giveaway_duration_title#other" = "Duration of Premium subscriptions";
"lng_giveaway_duration_price" = "{price} x {amount}"; "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_select" = "Select Date and Time";
"lng_giveaway_date_confirm" = "Confirm"; "lng_giveaway_date_confirm" = "Confirm";
"lng_giveaway_channels_select#one" = "Select up to {count} channel"; "lng_giveaway_channels_select#one" = "Select up to {count} channel";
"lng_giveaway_channels_select#other" = "Select up to {count} channels"; "lng_giveaway_channels_select#other" = "Select up to {count} channels";
"lng_giveaway_recipients_save" = "Save Recipients"; "lng_giveaway_recipients_save" = "Save Recipients";
"lng_giveaway_recipients_deselect" = "Deselect All"; "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_title" = "Congratulations!";
"lng_prize_about" = "You won a prize in a giveaway organized by {channel}."; "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_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#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_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#one" = "{count} random subscribers of {channel}";
"lng_prizes_winners_all_of_one#other" = "{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#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_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#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_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}"; "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#one" = "{count} booster";
"lng_boosts_list_title#other" = "{count} boosters"; "lng_boosts_list_title#other" = "{count} boosters";
"lng_boosts_list_subtext" = "Your channel is currently boosted by these users."; "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_list_status" = "boost expires on {date}";
"lng_boosts_link_title" = "Link for boosting"; "lng_boosts_link_title" = "Link for boosting";
"lng_boosts_link_subtext" = "Share this link with your subscribers to get more boosts."; "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 // Wnd specific

View file

@ -28,6 +28,7 @@
<file alias="icons/settings/dino.svg">../../icons/settings/dino.svg</file> <file alias="icons/settings/dino.svg">../../icons/settings/dino.svg</file>
<file alias="icons/settings/star.svg">../../icons/settings/star.svg</file> <file alias="icons/settings/star.svg">../../icons/settings/star.svg</file>
<file alias="icons/settings/starmini.svg">../../icons/settings/starmini.svg</file> <file alias="icons/settings/starmini.svg">../../icons/settings/starmini.svg</file>
<file alias="icons/tray/monochrome.svg">../../icons/tray_monochrome.svg</file>
<file alias="topic_icons/blue.svg">../../art/topic_icons/blue.svg</file> <file alias="topic_icons/blue.svg">../../art/topic_icons/blue.svg</file>
<file alias="topic_icons/yellow.svg">../../art/topic_icons/yellow.svg</file> <file alias="topic_icons/yellow.svg">../../art/topic_icons/yellow.svg</file>
<file alias="topic_icons/violet.svg">../../art/topic_icons/violet.svg</file> <file alias="topic_icons/violet.svg">../../art/topic_icons/violet.svg</file>

View file

@ -10,7 +10,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop" <Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE" ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="4.11.3.0" /> Version="4.11.5.0" />
<Properties> <Properties>
<DisplayName>Telegram Desktop</DisplayName> <DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>

View file

@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,11,3,0 FILEVERSION 4,11,5,0
PRODUCTVERSION 4,11,3,0 PRODUCTVERSION 4,11,5,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -62,10 +62,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop" VALUE "FileDescription", "AyuGram Desktop"
VALUE "FileVersion", "4.11.3.0" VALUE "FileVersion", "4.11.5.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.11.3.0" VALUE "ProductVersion", "4.11.5.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 4,11,3,0 FILEVERSION 4,11,5,0
PRODUCTVERSION 4,11,3,0 PRODUCTVERSION 4,11,5,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
FILEFLAGS 0x1L FILEFLAGS 0x1L
@ -53,10 +53,10 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "Radolyn Labs" VALUE "CompanyName", "Radolyn Labs"
VALUE "FileDescription", "AyuGram Desktop Updater" VALUE "FileDescription", "AyuGram Desktop Updater"
VALUE "FileVersion", "4.11.3.0" VALUE "FileVersion", "4.11.5.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2023" VALUE "LegalCopyright", "Copyright (C) 2014-2023"
VALUE "ProductName", "AyuGram Desktop" VALUE "ProductName", "AyuGram Desktop"
VALUE "ProductVersion", "4.11.3.0" VALUE "ProductVersion", "4.11.5.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -9,12 +9,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_premium_option.h" #include "api/api_premium_option.h"
#include "api/api_text_entities.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 "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 Api {
namespace { namespace {
@ -31,6 +36,24 @@ namespace {
}; };
} }
[[nodiscard]] Data::SubscriptionOptions GiftCodesFromTL(
const QVector<MTPPremiumGiftCodeOption> &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 } // namespace
Premium::Premium(not_null<ApiWrap*> api) Premium::Premium(not_null<ApiWrap*> api)
@ -311,4 +334,139 @@ const Data::SubscriptionOptions &Premium::subscriptionOptions() const {
return _subscriptionOptions; return _subscriptionOptions;
} }
PremiumGiftCodeOptions::PremiumGiftCodeOptions(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
rpl::producer<rpl::no_value, QString> 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<TLOption> &result) {
auto tlMapOptions = base::flat_map<Amount, QVector<TLOption>>();
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<int> &PremiumGiftCodeOptions::availablePresets() const {
return _availablePresets;
}
Payments::InvoicePremiumGiftCode PremiumGiftCodeOptions::invoice(
int users,
int monthsIndex) {
const auto randomId = base::RandomValue<uint64>();
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<MTPPremiumGiftCodeOption>();
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<int>(
u"giveaway_boosts_per_premium"_q,
kFallbackCount);
}
int PremiumGiftCodeOptions::giveawayCountriesMax() const {
constexpr auto kFallbackCount = 10;
return _peer->session().account().appConfig().get<int>(
u"giveaway_countries_max"_q,
kFallbackCount);
}
int PremiumGiftCodeOptions::giveawayAddPeersMax() const {
constexpr auto kFallbackCount = 10;
return _peer->session().account().appConfig().get<int>(
u"giveaway_add_peers_max"_q,
kFallbackCount);
}
int PremiumGiftCodeOptions::giveawayPeriodMax() const {
constexpr auto kFallbackCount = 3600 * 24 * 7;
return _peer->session().account().appConfig().get<int>(
u"giveaway_period_max"_q,
kFallbackCount);
}
bool PremiumGiftCodeOptions::giveawayGiftsPurchaseAvailable() const {
return _peer->session().account().appConfig().get<bool>(
u"giveaway_gifts_purchase_available"_q,
false);
}
} // namespace Api } // namespace Api

View file

@ -16,6 +16,10 @@ namespace Main {
class Session; class Session;
} // namespace Main } // namespace Main
namespace Payments {
struct InvoicePremiumGiftCode;
} // namespace Payments
namespace Api { namespace Api {
struct GiftCode { struct GiftCode {
@ -141,4 +145,51 @@ private:
}; };
class PremiumGiftCodeOptions final {
public:
PremiumGiftCodeOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::SubscriptionOptions options(int amount);
[[nodiscard]] const std::vector<int> &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<PeerData*> _peer;
base::flat_map<Amount, Data::SubscriptionOptions> _subscriptionOptions;
struct {
std::vector<int> months;
std::vector<float64> totalCosts;
QString currency;
} _optionsForOnePerson;
std::vector<int> _availablePresets;
base::flat_map<Token, Store> _stores;
MTP::Sender _api;
};
} // namespace Api } // namespace Api

View file

@ -36,7 +36,10 @@ template<typename Option>
result.reserve(tlOptions.size()); result.reserve(tlOptions.size());
for (const auto &tlOption : tlOptions) { for (const auto &tlOption : tlOptions) {
const auto &option = tlOption.data(); const auto &option = tlOption.data();
const auto botUrl = qs(option.vbot_url()); auto botUrl = QString();
if constexpr (!std::is_same_v<Option, MTPPremiumGiftCodeOption>) {
botUrl = qs(option.vbot_url());
}
const auto months = option.vmonths().v; const auto months = option.vmonths().v;
const auto amount = option.vamount().v; const auto amount = option.vamount().v;
const auto currency = qs(option.vcurrency()); const auto currency = qs(option.vcurrency());

View file

@ -533,8 +533,13 @@ rpl::producer<rpl::no_value, QString> Boosts::request() {
}; };
_boostStatus.link = qs(data.vboost_url()); _boostStatus.link = qs(data.vboost_url());
requestBoosts({}, [=](Data::BoostsListSlice &&slice) { using namespace Data;
_boostStatus.firstSlice = std::move(slice); 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(); consumer.put_done();
}); });
}).fail([=](const MTP::Error &error) { }).fail([=](const MTP::Error &error) {
@ -553,8 +558,11 @@ void Boosts::requestBoosts(
} }
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice); constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit); constexpr auto kTlLimit = tl::make_int(kLimit);
const auto gifts = token.gifts;
_requestId = _api.request(MTPpremium_GetBoostsList( _requestId = _api.request(MTPpremium_GetBoostsList(
MTP_flags(0), gifts
? MTP_flags(MTPpremium_GetBoostsList::Flag::f_gifts)
: MTP_flags(0),
_peer->input, _peer->input,
MTP_string(token.next), MTP_string(token.next),
token.next.isEmpty() ? kTlFirstSlice : kTlLimit token.next.isEmpty() ? kTlFirstSlice : kTlLimit
@ -567,19 +575,41 @@ void Boosts::requestBoosts(
auto list = std::vector<Data::Boost>(); auto list = std::vector<Data::Boost>();
list.reserve(data.vboosts().v.size()); list.reserve(data.vboosts().v.size());
for (const auto &boost : data.vboosts().v) { 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({ list.push_back({
boost.data().vuser_id().value_or_empty(), data.is_gift(),
QDateTime::fromSecsSinceEpoch(boost.data().vexpires().v), 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{ done(Data::BoostsListSlice{
.list = std::move(list), .list = std::move(list),
.total = data.vcount().v, .multipliedTotal = data.vcount().v,
.allLoaded = (data.vcount().v == data.vboosts().v.size()), .allLoaded = (data.vcount().v == data.vboosts().v.size()),
.token = Data::BoostsListSlice::OffsetToken{ .token = Data::BoostsListSlice::OffsetToken{
data.vnext_offset() .next = data.vnext_offset()
? qs(*data.vnext_offset()) ? qs(*data.vnext_offset())
: QString() : QString(),
.gifts = gifts,
}, },
}); });
}).fail([=] { }).fail([=] {

View file

@ -3614,7 +3614,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendAction(action); sendAction(action);
const auto clearCloudDraft = action.clearDraft; 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); const auto topic = peer->forumTopicFor(topicRootId);
if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer)) if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer))
|| Api::SendDice(message)) { || Api::SendDice(message)) {

View file

@ -37,8 +37,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include "styles/style_settings.h" #include "styles/style_settings.h"
#include <xxhash.h>
namespace { namespace {
constexpr auto kMaxLinkTitleLength = 32; 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) { [[nodiscard]] Color ComputeColor(const InviteLinkData &link) {
return Color::Permanent; return Color::Permanent;
} }
@ -242,7 +232,7 @@ private:
LinkRow::LinkRow( LinkRow::LinkRow(
not_null<LinkRowDelegate*> delegate, not_null<LinkRowDelegate*> delegate,
const InviteLinkData &data) const InviteLinkData &data)
: PeerListRow(ComputeRowId(data)) : PeerListRow(UniqueRowIdFromString(data.url))
, _delegate(delegate) , _delegate(delegate)
, _data(data) , _data(data)
, _color(ComputeColor(data)) { , _color(ComputeColor(data)) {

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h" #include "base/unixtime.h"
#include "base/weak_ptr.h" #include "base/weak_ptr.h"
#include "boxes/peers/prepare_short_info_box.h" #include "boxes/peers/prepare_short_info_box.h"
#include "data/data_boosts.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_media_types.h" // Data::Giveaway #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_stars_colored.h"
#include "ui/effects/premium_top_bar.h" #include "ui/effects/premium_top_bar.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/widgets/gradient_round_button.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_peer_menu.h" // ShowChooseRecipientBox.
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
#include "styles/style_layers.h" #include "styles/style_giveaway.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h" #include "styles/style_premium.h"
#include <QtGui/QGuiApplication> #include <QtGui/QGuiApplication>
@ -237,11 +237,7 @@ void GiftBox(
}, box->lifetime()); }, box->lifetime());
} }
struct GiftCodeLink { [[nodiscard]] Data::GiftCodeLink MakeGiftCodeLink(
QString text;
QString link;
};
[[nodiscard]] GiftCodeLink MakeGiftCodeLink(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const QString &slug) { const QString &slug) {
const auto path = u"giftcode/"_q + slug; const auto path = u"giftcode/"_q + slug;
@ -693,7 +689,8 @@ void GiveawayInfoBox(
text.append(' ').append(tr::lng_prizes_end_activated( text.append(' ').append(tr::lng_prizes_end_activated(
tr::now, tr::now,
lt_count, lt_count,
info.activatedCount)); info.activatedCount,
Ui::Text::RichLangValue));
} }
if (!info.giftCode.isEmpty()) { if (!info.giftCode.isEmpty()) {
text.append("\n\n"); text.append("\n\n");

View file

@ -35,6 +35,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
#include "styles/style_widgets.h" #include "styles/style_widgets.h"
#include <xxhash.h> // XXH64.
[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d) {
return XXH64(d.data(), d.size() * sizeof(ushort), 0);
}
PaintRoundImageCallback PaintUserpicCallback( PaintRoundImageCallback PaintUserpicCallback(
not_null<PeerData*> peer, not_null<PeerData*> peer,
bool respectSavedMessagesChat) { bool respectSavedMessagesChat) {

View file

@ -54,6 +54,8 @@ using PaintRoundImageCallback = Fn<void(
using PeerListRowId = uint64; using PeerListRowId = uint64;
[[nodiscard]] PeerListRowId UniqueRowIdFromString(const QString &d);
class PeerListRow { class PeerListRow {
public: public:
enum class State { enum class State {

View file

@ -55,6 +55,7 @@ public:
rpl::producer<DefaultIcon> value, rpl::producer<DefaultIcon> value,
Fn<void()> repaint); Fn<void()> repaint);
int width() override;
QString entityData() override; QString entityData() override;
void paint(QPainter &p, const Context &context) override; void paint(QPainter &p, const Context &context) override;
@ -80,6 +81,10 @@ DefaultIconEmoji::DefaultIconEmoji(
}, _lifetime); }, _lifetime);
} }
int DefaultIconEmoji::width() {
return st::emojiSize + 2 * st::emojiPadding;
}
QString DefaultIconEmoji::entityData() { QString DefaultIconEmoji::entityData() {
return u"topic_icon:%1"_q.arg(_icon.colorId); return u"topic_icon:%1"_q.arg(_icon.colorId);
} }

View file

@ -32,8 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_settings.h" // st::settingsDividerLabelPadding #include "styles/style_settings.h" // st::settingsDividerLabelPadding
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include <xxhash.h>
namespace { namespace {
enum class Color { 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) { [[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) {
return ComputeRowId(data.link); return UniqueRowIdFromString(data.link);
} }
[[nodiscard]] float64 ComputeProgress( [[nodiscard]] float64 ComputeProgress(
@ -628,7 +622,8 @@ void LinksController::updateRow(const InviteLinkData &data, TimeId now) {
} }
bool LinksController::removeRow(const QString &link) { 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); delegate()->peerListRemoveRow(row);
return true; return true;
} }

View file

@ -432,6 +432,7 @@ void SendFilesBox::prepare() {
preparePreview(); preparePreview();
initPreview(); initPreview();
SetupShadowsToScrollContent(this, _scroll, _inner->heightValue()); SetupShadowsToScrollContent(this, _scroll, _inner->heightValue());
setCloseByOutsideClick(false);
boxClosing() | rpl::start_with_next([=] { boxClosing() | rpl::start_with_next([=] {
if (!_confirmed && _cancelledCallback) { if (!_confirmed && _cancelledCallback) {
@ -1451,7 +1452,12 @@ void SendFilesBox::sendScheduled() {
? SendMenu::Type::ScheduledToUser ? SendMenu::Type::ScheduledToUser
: _sendMenuType; : _sendMenuType;
const auto callback = [=](Api::SendOptions options) { send(options); }; 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() { void SendFilesBox::sendWhenOnline() {

View file

@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs;
constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs;
constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppName = "AyuGram Desktop"_cs;
constexpr auto AppFile = "AyuGram"_cs; constexpr auto AppFile = "AyuGram"_cs;
constexpr auto AppVersion = 4011003; constexpr auto AppVersion = 4011005;
constexpr auto AppVersionStr = "4.11.3"; constexpr auto AppVersionStr = "4.11.5";
constexpr auto AppBetaVersion = false; constexpr auto AppBetaVersion = false;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View file

@ -19,24 +19,41 @@ struct BoostsOverview final {
float64 premiumMemberPercentage = 0; float64 premiumMemberPercentage = 0;
}; };
struct GiftCodeLink final {
QString text;
QString link;
QString slug;
};
struct Boost final { struct Boost final {
bool isGift = false;
bool isGiveaway = false;
bool isUnclaimed = false;
QString id;
UserId userId = UserId(0); UserId userId = UserId(0);
QDateTime expirationDate; FullMsgId giveawayMessage;
QDateTime date;
crl::time expiresAt = 0;
GiftCodeLink giftCodeLink;
int multiplier = 0;
}; };
struct BoostsListSlice final { struct BoostsListSlice final {
struct OffsetToken final { struct OffsetToken final {
QString next; QString next;
bool gifts = false;
}; };
std::vector<Boost> list; std::vector<Boost> list;
int total = 0; int multipliedTotal = 0;
bool allLoaded = false; bool allLoaded = false;
OffsetToken token; OffsetToken token;
}; };
struct BoostStatus final { struct BoostStatus final {
BoostsOverview overview; BoostsOverview overview;
BoostsListSlice firstSlice; BoostsListSlice firstSliceBoosts;
BoostsListSlice firstSliceGifts;
QString link; QString link;
}; };

View file

@ -86,9 +86,16 @@ struct FileReferenceAccumulator {
}, [](const auto &data) { }, [](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) { void push(const MTPMessage &data) {
data.match([&](const MTPDmessage &data) { data.match([&](const MTPDmessage &data) {
push(data.vmedia()); push(data.vmedia());
push(data.vreply_to());
}, [&](const MTPDmessageService &data) { }, [&](const MTPDmessageService &data) {
data.vaction().match( data.vaction().match(
[&](const MTPDmessageActionChatEditPhoto &data) { [&](const MTPDmessageActionChatEditPhoto &data) {
@ -99,6 +106,7 @@ struct FileReferenceAccumulator {
push(data.vwallpaper()); push(data.vwallpaper());
}, [](const auto &data) { }, [](const auto &data) {
}); });
push(data.vreply_to());
}, [](const MTPDmessageEmpty &data) { }, [](const MTPDmessageEmpty &data) {
}); });
} }

View file

@ -54,14 +54,17 @@ MTPInputReplyTo ReplyToForMTP(
} }
} else if (replyTo.messageId || replyTo.topicRootId) { } else if (replyTo.messageId || replyTo.topicRootId) {
const auto to = LookupReplyTo(history, replyTo.messageId); const auto to = LookupReplyTo(history, replyTo.messageId);
const auto replyingToTopic = replyTo.topicRootId
? history->peer->forumTopicFor(replyTo.topicRootId)
: nullptr;
const auto replyingToTopicId = replyTo.topicRootId const auto replyingToTopicId = replyTo.topicRootId
? replyTo.topicRootId ? (replyingToTopic
: Data::ForumTopic::kGeneralId; ? replyingToTopic->rootId()
const auto replyToTopicId = !to : Data::ForumTopic::kGeneralId)
? replyingToTopicId : (to ? to->topicRootId() : Data::ForumTopic::kGeneralId);
: to->topicRootId() const auto replyToTopicId = to
? to->topicRootId() ? to->topicRootId()
: Data::ForumTopic::kGeneralId; : replyingToTopicId;
const auto external = replyTo.messageId const auto external = replyTo.messageId
&& (replyTo.messageId.peer != history->peer->id && (replyTo.messageId.peer != history->peer->id
|| replyingToTopicId != replyToTopicId); || replyingToTopicId != replyToTopicId);

View file

@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/storage_shared_media.h" #include "storage/storage_shared_media.h"
#include "storage/localstorage.h" #include "storage/localstorage.h"
#include "chat_helpers/stickers_dice_pack.h" // Stickers::DicePacks::IsSlot. #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_session.h"
#include "data/data_auto_download.h" #include "data/data_auto_download.h"
#include "data/data_photo.h" #include "data/data_photo.h"
@ -369,6 +370,7 @@ Giveaway ComputeGiveawayData(
.untilDate = data.vuntil_date().v, .untilDate = data.vuntil_date().v,
.quantity = data.vquantity().v, .quantity = data.vquantity().v,
.months = data.vmonths().v, .months = data.vmonths().v,
.all = !data.is_only_new_subscribers(),
}; };
result.channels.reserve(data.vchannels().v.size()); result.channels.reserve(data.vchannels().v.size());
const auto owner = &item->history()->owner(); 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 type = tr::lng_in_dlg_photo(tr::now);
const auto caption = options.hideCaption const auto caption = (options.hideCaption || options.ignoreMessageText)
? TextWithEntities() ? TextWithEntities()
: options.translated : options.translated
? parent()->translatedText() ? parent()->translatedText()
@ -951,7 +953,7 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
} }
return tr::lng_in_dlg_file(tr::now); return tr::lng_in_dlg_file(tr::now);
}(); }();
const auto caption = options.hideCaption const auto caption = (options.hideCaption || options.ignoreMessageText)
? TextWithEntities() ? TextWithEntities()
: options.translated : options.translated
? parent()->translatedText() ? parent()->translatedText()
@ -1500,7 +1502,9 @@ bool MediaWebPage::replyPreviewLoaded() const {
} }
ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const { ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const {
auto text = options.translated auto text = options.ignoreMessageText
? TextWithEntities()
: options.translated
? parent()->translatedText() ? parent()->translatedText()
: parent()->originalText(); : parent()->originalText();
if (text.empty()) { if (text.empty()) {
@ -2038,28 +2042,23 @@ MediaStory::MediaStory(
owner->registerStoryItem(storyId, parent); owner->registerStoryItem(storyId, parent);
const auto stories = &owner->stories(); const auto stories = &owner->stories();
if (const auto maybeStory = stories->lookup(storyId)) { const auto maybeStory = stories->lookup(storyId);
if (!_mention) { if (!maybeStory && maybeStory.error() == NoStory::Unknown) {
parent->setText((*maybeStory)->caption()); stories->resolve(storyId, crl::guard(this, [=] {
} if (const auto maybeStory = stories->lookup(storyId)) {
} else { if (!_mention && _viewMayExist) {
if (maybeStory.error() == NoStory::Unknown) { parent->setText((*maybeStory)->caption());
stories->resolve(storyId, crl::guard(this, [=] {
if (const auto maybeStory = stories->lookup(storyId)) {
if (!_mention) {
parent->setText((*maybeStory)->caption());
}
} else {
_expired = true;
} }
if (_mention) { } else {
parent->updateStoryMentionText(); _expired = true;
} }
parent->history()->owner().requestItemViewRefresh(parent); if (_mention) {
})); parent->updateStoryMentionText();
} else { }
_expired = true; parent->history()->owner().requestItemViewRefresh(parent);
} }));
} else if (!maybeStory) {
_expired = true;
} }
} }
@ -2154,6 +2153,7 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
if (_mention) { if (_mention) {
return nullptr; return nullptr;
} }
_viewMayExist = true;
return std::make_unique<HistoryView::Photo>( return std::make_unique<HistoryView::Photo>(
message, message,
realParent, realParent,
@ -2161,6 +2161,7 @@ std::unique_ptr<HistoryView::Media> MediaStory::createView(
spoiler); spoiler);
} }
_expired = false; _expired = false;
_viewMayExist = true;
const auto story = *maybeStory; const auto story = *maybeStory;
if (_mention) { if (_mention) {
return std::make_unique<HistoryView::ServiceBox>( return std::make_unique<HistoryView::ServiceBox>(
@ -2189,6 +2190,7 @@ MediaGiveaway::MediaGiveaway(
const Giveaway &data) const Giveaway &data)
: Media(parent) : Media(parent)
, _giveaway(data) { , _giveaway(data) {
parent->history()->session().giftBoxStickersPacks().load();
} }
std::unique_ptr<Media> MediaGiveaway::clone(not_null<HistoryItem*> parent) { std::unique_ptr<Media> MediaGiveaway::clone(not_null<HistoryItem*> parent) {

View file

@ -623,6 +623,7 @@ public:
private: private:
const FullStoryId _storyId; const FullStoryId _storyId;
const bool _mention = false; const bool _mention = false;
bool _viewMayExist = false;
bool _expired = false; bool _expired = false;
}; };

View file

@ -89,6 +89,18 @@ private:
: FrameSizeFromTag(tag); : 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 } // namespace
class CustomEmojiLoader final class CustomEmojiLoader final
@ -514,6 +526,9 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
Fn<void()> update, Fn<void()> update,
SizeTag tag, SizeTag tag,
int sizeOverride) { int sizeOverride) {
if (data.startsWith(InternalPrefix())) {
return internal(data);
}
const auto parsed = ParseCustomEmojiData(data); const auto parsed = ParseCustomEmojiData(data);
return parsed return parsed
? create(parsed, std::move(update), tag, sizeOverride) ? create(parsed, std::move(update), tag, sizeOverride)
@ -540,6 +555,26 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
}); });
} }
std::unique_ptr<Ui::Text::CustomEmoji> 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<Ui::CustomEmoji::Internal>(
data.toString(),
info.image,
padding,
info.textColor);
}
void CustomEmojiManager::resolve( void CustomEmojiManager::resolve(
QStringView data, QStringView data,
not_null<Listener*> listener) { not_null<Listener*> listener) {
@ -885,6 +920,41 @@ uint64 CustomEmojiManager::coloredSetId() const {
return _coloredSetId; 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) { int FrameSizeFromTag(SizeTag tag) {
const auto emoji = EmojiSizeFromTag(tag); const auto emoji = EmojiSizeFromTag(tag);
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();

View file

@ -83,11 +83,24 @@ public:
[[nodiscard]] Main::Session &session() const; [[nodiscard]] Main::Session &session() const;
[[nodiscard]] Session &owner() 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; [[nodiscard]] uint64 coloredSetId() const;
private: private:
static constexpr auto kSizeCount = int(SizeTag::kCount); static constexpr auto kSizeCount = int(SizeTag::kCount);
struct InternalEmojiData {
QImage image;
bool textColor = true;
};
struct RepaintBunch { struct RepaintBunch {
crl::time when = 0; crl::time when = 0;
std::vector<base::weak_ptr<Ui::CustomEmoji::Instance>> instances; std::vector<base::weak_ptr<Ui::CustomEmoji::Instance>> instances;
@ -131,6 +144,8 @@ private:
SizeTag tag, SizeTag tag,
int sizeOverride, int sizeOverride,
LoaderFactory factory); LoaderFactory factory);
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> internal(
QStringView data);
[[nodiscard]] static int SizeIndex(SizeTag tag); [[nodiscard]] static int SizeIndex(SizeTag tag);
const not_null<Session*> _owner; const not_null<Session*> _owner;
@ -163,6 +178,9 @@ private:
bool _repaintTimerScheduled = false; bool _repaintTimerScheduled = false;
bool _requestSetsScheduled = false; bool _requestSetsScheduled = false;
std::vector<InternalEmojiData> _internalEmoji;
base::flat_map<not_null<const style::icon*>, QString> _iconEmoji;
#if 0 // inject-to-on_main #if 0 // inject-to-on_main
crl::time _repaintsLastAdded = 0; crl::time _repaintsLastAdded = 0;
rpl::lifetime _repaintsLifetime; rpl::lifetime _repaintsLifetime;

View file

@ -420,7 +420,7 @@ void PaintRow(
.now = context.now, .now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionOneLine = true, .elisionLines = 1,
}); });
} else if (draft } else if (draft
|| (supportMode || (supportMode
@ -514,7 +514,7 @@ void PaintRow(
.now = context.now, .now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionOneLine = true, .elisionLines = 1,
}); });
} }
} else if (!item) { } else if (!item) {

View file

@ -141,7 +141,7 @@ void TopicsView::paint(
.now = context.now, .now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionOneLine = true, .elisionLines = 1,
}); });
const auto skip = skipBig const auto skip = skipBig
? context.st->topicsSkipBig ? context.st->topicsSkipBig

View file

@ -23,7 +23,13 @@ void ItemImage::paint(
QPainter *p, QPainter *p,
const QStyleOptionGraphicsItem *option, const QStyleOptionGraphicsItem *option,
QWidget *w) { 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); ItemBase::paint(p, option, w);
} }

View file

@ -110,7 +110,13 @@ void ItemSticker::paint(
QPainter *p, QPainter *p,
const QStyleOptionGraphicsItem *option, const QStyleOptionGraphicsItem *option,
QWidget *w) { 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); ItemBase::paint(p, option, w);
} }

View file

@ -265,7 +265,11 @@ public:
return _widget ? _widget->elementAnimationsPaused() : false; return _widget ? _widget->elementAnimationsPaused() : false;
} }
bool elementHideReply(not_null<const Element*> view) override { bool elementHideReply(not_null<const Element*> view) override {
return view->isTopicRootReply(); if (!view->isTopicRootReply()) {
return false;
}
const auto reply = view->data()->Get<HistoryMessageReply>();
return reply && !reply->fields().manualQuote;
} }
bool elementShownUnread(not_null<const Element*> view) override { bool elementShownUnread(not_null<const Element*> view) override {
return view->data()->unread(view->data()->history()); return view->data()->unread(view->data()->history());

View file

@ -472,7 +472,7 @@ HistoryItem::HistoryItem(
config.reply.topicPost = (topicRootId != 0); config.reply.topicPost = (topicRootId != 0);
if (const auto originalReply = original->Get<HistoryMessageReply>()) { if (const auto originalReply = original->Get<HistoryMessageReply>()) {
if (originalReply->external()) { if (originalReply->external()) {
config.reply = originalReply->fields(); config.reply = originalReply->fields().clone(this);
if (!config.reply.externalPeerId) { if (!config.reply.externalPeerId) {
config.reply.messageId = 0; config.reply.messageId = 0;
} }
@ -744,7 +744,7 @@ HistoryItem::HistoryItem(
HistoryItem::~HistoryItem() { HistoryItem::~HistoryItem() {
_media = nullptr; _media = nullptr;
clearSavedMedia(); clearSavedMedia();
if (auto reply = Get<HistoryMessageReply>()) { if (const auto reply = Get<HistoryMessageReply>()) {
reply->clearData(this); reply->clearData(this);
} }
clearDependencyMessage(); clearDependencyMessage();
@ -1695,7 +1695,6 @@ void HistoryItem::applySentMessage(const MTPDmessage &data) {
setForwardsCount(data.vforwards().value_or(-1)); setForwardsCount(data.vforwards().value_or(-1));
if (const auto reply = data.vreply_to()) { if (const auto reply = data.vreply_to()) {
reply->match([&](const MTPDmessageReplyHeader &data) { reply->match([&](const MTPDmessageReplyHeader &data) {
// #TODO replies
const auto replyToPeer = data.vreply_to_peer_id() const auto replyToPeer = data.vreply_to_peer_id()
? peerFromMTP(*data.vreply_to_peer_id()) ? peerFromMTP(*data.vreply_to_peer_id())
: PeerId(); : PeerId();
@ -2001,9 +2000,6 @@ void HistoryItem::setRealId(MsgId newId) {
_history->owner().requestItemResize(this); _history->owner().requestItemResize(this);
if (const auto reply = Get<HistoryMessageReply>()) { if (const auto reply = Get<HistoryMessageReply>()) {
if (reply->link()) {
reply->setLinkFrom(this);
}
incrementReplyToTopCounter(); incrementReplyToTopCounter();
} }
} }
@ -3189,7 +3185,7 @@ void HistoryItem::createComponents(CreateConfig &&config) {
UpdateComponents(mask); UpdateComponents(mask);
if (const auto reply = Get<HistoryMessageReply>()) { if (const auto reply = Get<HistoryMessageReply>()) {
reply->set(config.reply); reply->set(std::move(config.reply));
if (!reply->updateData(this)) { if (!reply->updateData(this)) {
if (const auto messageId = reply->messageId()) { if (const auto messageId = reply->messageId()) {
RequestDependentMessageItem( RequestDependentMessageItem(
@ -3405,13 +3401,15 @@ void HistoryItem::createComponentsHelper(
: (replyTo.messageId.peer == history()->peer->id) : (replyTo.messageId.peer == history()->peer->id)
? replyTo.messageId.msg ? replyTo.messageId.msg
: MsgId(); : MsgId();
const auto forum = _history->asForum();
const auto topic = forum
? forum->topicFor(replyTo.topicRootId)
: nullptr;
if (!config.reply.externalPeerId if (!config.reply.externalPeerId
&& to && topic
&& config.reply.topicPost && topic->rootId() != to->topicRootId()) {
&& replyTo.topicRootId != to->topicRootId()) {
config.reply.externalPeerId = replyTo.messageId.peer; config.reply.externalPeerId = replyTo.messageId.peer;
} }
const auto forum = _history->asForum();
config.reply.topicPost = config.reply.externalPeerId config.reply.topicPost = config.reply.externalPeerId
? (replyTo.topicRootId ? (replyTo.topicRootId
&& (replyTo.topicRootId != Data::ForumTopic::kGeneralId)) && (replyTo.topicRootId != Data::ForumTopic::kGeneralId))
@ -3512,7 +3510,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) {
}); });
} }
if (const auto reply = data.vreply_to()) { 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.viaBotId = data.vvia_bot_id().value_or_empty();
config.viewsCount = data.vviews().value_or(-1); config.viewsCount = data.vviews().value_or(-1);

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio.h" #include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h" #include "media/player/media_player_instance.h"
#include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_media_types.h" #include "data/data_media_types.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_user.h" #include "data/data_user.h"
@ -56,127 +57,6 @@ namespace {
const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_"; const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_";
void ValidateBackgroundEmoji(
DocumentId backgroundEmojiId,
not_null<Ui::BackgroundEmojiData*> data,
not_null<Ui::BackgroundEmojiCache*> cache,
not_null<Ui::Text::QuotePaintCache*> quote,
not_null<const HistoryView::Element*> 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 } // namespace
void HistoryMessageVia::create( void HistoryMessageVia::create(
@ -390,15 +270,33 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
} }
} }
ReplyFields ReplyFields::clone(not_null<HistoryItem*> 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( ReplyFields ReplyFieldsFromMTP(
not_null<History*> history, not_null<HistoryItem*> item,
const MTPMessageReplyHeader &reply) { const MTPMessageReplyHeader &reply) {
return reply.match([&](const MTPDmessageReplyHeader &data) { return reply.match([&](const MTPDmessageReplyHeader &data) {
auto result = ReplyFields(); auto result = ReplyFields();
if (const auto peer = data.vreply_to_peer_id()) { if (const auto peer = data.vreply_to_peer_id()) {
result.externalPeerId = peerFromMTP(*peer); 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()) { if (const auto id = data.vreply_to_msg_id().value_or_empty()) {
result.messageId = data.is_reply_to_scheduled() result.messageId = data.is_reply_to_scheduled()
? owner->scheduledMessages().localMessageId(id) ? owner->scheduledMessages().localMessageId(id)
@ -417,6 +315,9 @@ ReplyFields ReplyFieldsFromMTP(
result.externalSenderName result.externalSenderName
= qs(data.vfrom_name().value_or_empty()); = qs(data.vfrom_name().value_or_empty());
} }
if (const auto media = data.vreply_media()) {
result.externalMedia = HistoryItem::CreateMedia(item, *media);
}
result.quote = TextWithEntities{ result.quote = TextWithEntities{
qs(data.vquote_text().value_or_empty()), qs(data.vquote_text().value_or_empty()),
Api::EntitiesFromMTP( Api::EntitiesFromMTP(
@ -477,7 +378,7 @@ HistoryMessageReply &HistoryMessageReply::operator=(
HistoryMessageReply::~HistoryMessageReply() { HistoryMessageReply::~HistoryMessageReply() {
// clearData() should be called by holder. // clearData() should be called by holder.
Expects(resolvedMessage.empty()); Expects(resolvedMessage.empty());
Expects(originalVia == nullptr); _fields.externalMedia = nullptr;
} }
bool HistoryMessageReply::updateData( bool HistoryMessageReply::updateData(
@ -523,64 +424,31 @@ bool HistoryMessageReply::updateData(
} }
} }
const auto external = this->external(); const auto asExternal = displayAsExternal(holder);
if (resolvedMessage const auto nonEmptyQuote = !_fields.quote.empty()
&& (asExternal || _fields.manualQuote);
_multiline = !_fields.storyId && (asExternal || nonEmptyQuote);
const auto displaying = resolvedMessage
|| resolvedStory || resolvedStory
|| (external && (!_fields.messageId || force))) { || ((nonEmptyQuote || _fields.externalMedia)
const auto repaint = [=] { holder->customEmojiRepaint(); }; && (!_fields.messageId || force));
const auto context = Core::MarkedTextContext{ _displaying = displaying ? 1 : 0;
.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);
updateName(holder); const auto unavailable = !resolvedMessage
setLinkFrom(holder); && !resolvedStory
if (resolvedMessage && ((!_fields.storyId && !_fields.messageId) || force);
&& !resolvedMessage->Has<HistoryMessageForwarded>()) { _unavailable = unavailable ? 1 : 0;
if (const auto bot = resolvedMessage->viaBot()) {
originalVia = std::make_unique<HistoryMessageVia>();
originalVia->create(
&holder->history()->owner(),
peerToUser(bot->id));
}
}
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<Ui::SpoilerAnimation>(repaint);
}
} else if (force) {
if (_fields.messageId || _fields.storyId) {
_unavailable = 1;
}
spoiler = nullptr;
}
if (force) { if (force) {
if (!_displaying && (_fields.messageId || _fields.storyId)) {
_unavailable = 1;
}
holder->history()->owner().requestItemResize(holder); holder->history()->owner().requestItemResize(holder);
} }
return resolvedMessage return resolvedMessage
|| resolvedStory || resolvedStory
|| (external && !_fields.messageId) || (!_fields.messageId && !_fields.storyId && external())
|| _unavailable; || _unavailable;
} }
@ -610,39 +478,11 @@ void HistoryMessageReply::updateFields(
} }
} }
void HistoryMessageReply::setLinkFrom(
not_null<HistoryItem*> holder) {
const auto externalPeerId = _fields.externalSenderId;
const auto external = externalPeerId
|| !_fields.externalSenderName.isEmpty();
const auto externalLink = [=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
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<LambdaClickHandler>(externalLink)
: nullptr;
}
void HistoryMessageReply::setTopMessageId(MsgId topMessageId) { void HistoryMessageReply::setTopMessageId(MsgId topMessageId) {
_fields.topMessageId = topMessageId; _fields.topMessageId = topMessageId;
} }
void HistoryMessageReply::clearData(not_null<HistoryItem*> holder) { void HistoryMessageReply::clearData(not_null<HistoryItem*> holder) {
originalVia = nullptr;
if (resolvedMessage) { if (resolvedMessage) {
holder->history()->owner().unregisterDependentMessage( holder->history()->owner().unregisterDependentMessage(
holder, holder,
@ -655,9 +495,12 @@ void HistoryMessageReply::clearData(not_null<HistoryItem*> holder) {
resolvedStory.get()); resolvedStory.get());
resolvedStory = nullptr; resolvedStory = nullptr;
} }
_name.clear();
_text.clear();
_unavailable = 1; _unavailable = 1;
_displaying = 0;
if (_multiline) {
holder->history()->owner().requestItemResize(holder);
_multiline = 0;
}
refreshReplyToMedia(); refreshReplyToMedia();
} }
@ -667,145 +510,13 @@ bool HistoryMessageReply::external() const {
|| !_fields.externalSenderName.isEmpty(); || !_fields.externalSenderName.isEmpty();
} }
PeerData *HistoryMessageReply::sender(not_null<HistoryItem*> holder) const { bool HistoryMessageReply::displayAsExternal(
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<HistoryMessageForwarded>()) {
// Forward of a reply. Show reply-to original sender.
const auto forwarded
= resolvedMessage->Get<HistoryMessageForwarded>();
if (forwarded) {
return forwarded->originalSender;
}
}
if (const auto from = resolvedMessage->displayFrom()) {
return from;
}
return resolvedMessage->author().get();
}
QString HistoryMessageReply::senderName(
not_null<HistoryItem*> holder) const { not_null<HistoryItem*> holder) const {
if (const auto peer = sender(holder)) { // Don't display replies that could be local as external.
return senderName(peer); return external()
} else if (!resolvedMessage) { && (!resolvedMessage
return _fields.externalSenderName; || (holder->history() != resolvedMessage->history())
} else if (holder->Has<HistoryMessageForwarded>()) { || (holder->topicRootId() != resolvedMessage->topicRootId()));
// Forward of a reply. Show reply-to original sender.
const auto forwarded
= resolvedMessage->Get<HistoryMessageForwarded>();
if (forwarded) {
Assert(forwarded->hiddenSenderInfo != nullptr);
return forwarded->hiddenSenderInfo->name;
}
}
return QString();
}
QString HistoryMessageReply::senderName(not_null<PeerData*> peer) const {
if (const auto user = originalVia ? peer->asUser() : nullptr) {
return user->firstName;
}
return peer->name();
}
bool HistoryMessageReply::isNameUpdated(
not_null<HistoryItem*> holder) const {
if (const auto from = sender(holder)) {
if (_nameVersion < from->nameVersion()) {
updateName(holder, from);
return true;
}
}
return false;
}
void HistoryMessageReply::updateName(
not_null<HistoryItem*> holder,
std::optional<PeerData*> 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);
} }
void HistoryMessageReply::itemRemoved( void HistoryMessageReply::itemRemoved(
@ -826,224 +537,6 @@ void HistoryMessageReply::storyRemoved(
} }
} }
void HistoryMessageReply::paint(
Painter &p,
not_null<const HistoryView::Element*> 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 &quoteSt = 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<style::owned_color>();
auto copy = std::optional<style::TextPalette>();
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() { void HistoryMessageReply::refreshReplyToMedia() {
replyToDocumentId = 0; replyToDocumentId = 0;
replyToWebPageId = 0; replyToWebPageId = 0;

View file

@ -22,9 +22,12 @@ namespace Ui {
struct ChatPaintContext; struct ChatPaintContext;
class ChatStyle; class ChatStyle;
struct PeerUserpicView; struct PeerUserpicView;
class SpoilerAnimation;
} // namespace Ui } // namespace Ui
namespace Ui::Text {
struct GeometryDescriptor;
} // namespace Ui::Text
namespace Data { namespace Data {
class Session; class Session;
class Story; class Story;
@ -230,7 +233,10 @@ private:
}; };
struct ReplyFields { struct ReplyFields {
ReplyFields clone(not_null<HistoryItem*> parent) const;
TextWithEntities quote; TextWithEntities quote;
std::unique_ptr<Data::Media> externalMedia;
PeerId externalSenderId = 0; PeerId externalSenderId = 0;
QString externalSenderName; QString externalSenderName;
QString externalPostAuthor; QString externalPostAuthor;
@ -243,7 +249,7 @@ struct ReplyFields {
}; };
[[nodiscard]] ReplyFields ReplyFieldsFromMTP( [[nodiscard]] ReplyFields ReplyFieldsFromMTP(
not_null<History*> history, not_null<HistoryItem*> item,
const MTPMessageReplyHeader &reply); const MTPMessageReplyHeader &reply);
[[nodiscard]] FullReplyTo ReplyToFromMTP( [[nodiscard]] FullReplyTo ReplyToFromMTP(
@ -260,8 +266,6 @@ struct HistoryMessageReply
HistoryMessageReply &operator=(HistoryMessageReply &&other); HistoryMessageReply &operator=(HistoryMessageReply &&other);
~HistoryMessageReply(); ~HistoryMessageReply();
static constexpr auto kBarAlpha = 230. / 255.;
void set(ReplyFields fields); void set(ReplyFields fields);
void updateFields( void updateFields(
@ -275,16 +279,8 @@ struct HistoryMessageReply
void clearData(not_null<HistoryItem*> holder); void clearData(not_null<HistoryItem*> holder);
[[nodiscard]] bool external() const; [[nodiscard]] bool external() const;
[[nodiscard]] PeerData *sender(not_null<HistoryItem*> holder) const; [[nodiscard]] bool displayAsExternal(
[[nodiscard]] QString senderName(not_null<HistoryItem*> holder) const; not_null<HistoryItem*> holder) const;
[[nodiscard]] QString senderName(not_null<PeerData*> peer) const;
[[nodiscard]] bool isNameUpdated(not_null<HistoryItem*> holder) const;
void updateName(
not_null<HistoryItem*> holder,
std::optional<PeerData*> resolvedSender = std::nullopt) const;
[[nodiscard]] int resizeToWidth(int width) const;
[[nodiscard]] int height() const;
[[nodiscard]] QMargins margins() const;
void itemRemoved( void itemRemoved(
not_null<HistoryItem*> holder, not_null<HistoryItem*> holder,
not_null<HistoryItem*> removed); not_null<HistoryItem*> removed);
@ -292,17 +288,7 @@ struct HistoryMessageReply
not_null<HistoryItem*> holder, not_null<HistoryItem*> holder,
not_null<Data::Story*> removed); not_null<Data::Story*> removed);
void paint( [[nodiscard]] const ReplyFields &fields() const {
Painter &p,
not_null<const HistoryView::Element*> holder,
const Ui::ChatPaintContext &context,
int x,
int y,
int w,
bool inBubble) const;
void unloadPersistentAnimation();
[[nodiscard]] ReplyFields fields() const {
return _fields; return _fields;
} }
[[nodiscard]] PeerId externalPeerId() const { [[nodiscard]] PeerId externalPeerId() const {
@ -317,21 +303,22 @@ struct HistoryMessageReply
[[nodiscard]] MsgId topMessageId() const { [[nodiscard]] MsgId topMessageId() const {
return _fields.topMessageId; return _fields.topMessageId;
} }
[[nodiscard]] int maxWidth() const {
return _maxWidth;
}
[[nodiscard]] ClickHandlerPtr link() const {
return _link;
}
[[nodiscard]] bool topicPost() const { [[nodiscard]] bool topicPost() const {
return _fields.topicPost; return _fields.topicPost;
} }
[[nodiscard]] bool manualQuote() const { [[nodiscard]] bool manualQuote() const {
return _fields.manualQuote; 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<HistoryItem*> holder);
void setTopMessageId(MsgId topMessageId); void setTopMessageId(MsgId topMessageId);
void refreshReplyToMedia(); void refreshReplyToMedia();
@ -340,25 +327,12 @@ struct HistoryMessageReply
WebPageId replyToWebPageId = 0; WebPageId replyToWebPageId = 0;
ReplyToMessagePointer resolvedMessage; ReplyToMessagePointer resolvedMessage;
ReplyToStoryPointer resolvedStory; ReplyToStoryPointer resolvedStory;
std::unique_ptr<HistoryMessageVia> originalVia;
std::unique_ptr<Ui::SpoilerAnimation> spoiler;
struct {
mutable std::unique_ptr<Ui::RippleAnimation> animation;
QPoint lastPoint;
} ripple;
private: private:
ReplyFields _fields; 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 _unavailable : 1 = 0;
uint8 _displaying : 1 = 0;
uint8 _multiline : 1 = 0;
}; };

View file

@ -5391,8 +5391,7 @@ bool HistoryWidget::confirmSendingFiles(
})); }));
Window::ActivateWindow(controller()); Window::ActivateWindow(controller());
const auto shown = controller()->show(std::move(box)); controller()->show(std::move(box));
shown->setCloseByOutsideClick(false);
return true; return true;
} }
@ -7972,7 +7971,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) {
.now = now, .now = now,
.pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedEmoji = paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = pausedSpoiler, .pausedSpoiler = pausedSpoiler,
.elisionOneLine = true, .elisionLines = 1,
}); });
} else { } else {
p.setFont(st::msgDateFont); p.setFont(st::msgDateFont);

View file

@ -629,7 +629,7 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) {
.now = crl::now(), .now = crl::now(),
.pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat), .pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat),
.pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler), .pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler),
.elisionOneLine = true, .elisionLines = 1,
}); });
} }

View file

@ -669,6 +669,7 @@ void DraftOptionsBox(
}); });
} }
const auto weak = Ui::MakeWeak(box);
Settings::AddButton( Settings::AddButton(
bottom, bottom,
tr::lng_reply_show_in_chat(), tr::lng_reply_show_in_chat(),
@ -676,6 +677,9 @@ void DraftOptionsBox(
{ &st::menuIconShowInChat } { &st::menuIconShowInChat }
)->setClickedCallback([=] { )->setClickedCallback([=] {
highlight(resolveReply()); highlight(resolveReply());
if (const auto strong = weak.data()) {
strong->closeBox();
}
}); });
Settings::AddButton( Settings::AddButton(

View file

@ -396,7 +396,7 @@ void ForwardPanel::paint(
.now = now, .now = now,
.pausedEmoji = paused || On(PowerSaving::kEmojiChat), .pausedEmoji = paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = pausedSpoiler, .pausedSpoiler = pausedSpoiler,
.elisionOneLine = true, .elisionLines = 1,
}); });
} }

View file

@ -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_button.h"
#include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions.h"
#include "history/view/history_view_cursor_state.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/view/history_view_spoiler_click_handler.h"
#include "history/history.h" #include "history/history.h"
#include "history/history_item.h" #include "history/history_item.h"
@ -500,7 +501,8 @@ Element::Element(
| Flag::NeedsResize | Flag::NeedsResize
| (IsItemScheduledUntilOnline(data) | (IsItemScheduledUntilOnline(data)
? Flag::ScheduledUntilOnline ? Flag::ScheduledUntilOnline
: Flag())) : Flag())
| (countIsTopicRootReply() ? Flag::TopicRootReply : Flag()))
, _context(delegate->elementContext()) { , _context(delegate->elementContext()) {
history()->owner().registerItemView(this); history()->owner().registerItemView(this);
refreshMedia(replacing); refreshMedia(replacing);
@ -1262,15 +1264,6 @@ QSize Element::countCurrentSize(int newWidth) {
return performCountCurrentSize(newWidth); return performCountCurrentSize(newWidth);
} }
void Element::refreshIsTopicRootReply() {
const auto topicRootReply = countIsTopicRootReply();
if (topicRootReply) {
_flags |= Flag::TopicRootReply;
} else {
_flags &= ~Flag::TopicRootReply;
}
}
bool Element::countIsTopicRootReply() const { bool Element::countIsTopicRootReply() const {
const auto item = data(); const auto item = data();
if (!item->history()->isForum()) { if (!item->history()->isForum()) {
@ -1365,6 +1358,10 @@ bool Element::hasFromName() const {
return false; return false;
} }
bool Element::displayReply() const {
return Has<Reply>();
}
bool Element::displayFromName() const { bool Element::displayFromName() const {
return false; return false;
} }
@ -1422,10 +1419,6 @@ TimeId Element::displayedEditDate() const {
return TimeId(0); return TimeId(0);
} }
HistoryMessageReply *Element::displayedReply() const {
return nullptr;
}
bool Element::toggleSelectionByHandlerClick( bool Element::toggleSelectionByHandlerClick(
const ClickHandlerPtr &handler) const { const ClickHandlerPtr &handler) const {
return false; return false;
@ -1485,7 +1478,7 @@ void Element::unloadHeavyPart() {
if (_flags & Flag::HeavyCustomEmoji) { if (_flags & Flag::HeavyCustomEmoji) {
_flags &= ~Flag::HeavyCustomEmoji; _flags &= ~Flag::HeavyCustomEmoji;
_text.unloadPersistentAnimation(); _text.unloadPersistentAnimation();
if (const auto reply = data()->Get<HistoryMessageReply>()) { if (const auto reply = Get<Reply>()) {
reply->unloadPersistentAnimation(); reply->unloadPersistentAnimation();
} }
} }

View file

@ -49,6 +49,7 @@ enum class InfoDisplayType : char;
struct StateRequest; struct StateRequest;
struct TextState; struct TextState;
class Media; class Media;
class Reply;
enum class Context : char { enum class Context : char {
History, History,
@ -433,6 +434,7 @@ public:
[[nodiscard]] virtual bool hasFromPhoto() const; [[nodiscard]] virtual bool hasFromPhoto() const;
[[nodiscard]] virtual bool displayFromPhoto() const; [[nodiscard]] virtual bool displayFromPhoto() const;
[[nodiscard]] virtual bool hasFromName() const; [[nodiscard]] virtual bool hasFromName() const;
[[nodiscard]] bool displayReply() const;
[[nodiscard]] virtual bool displayFromName() const; [[nodiscard]] virtual bool displayFromName() const;
[[nodiscard]] virtual TopicButton *displayedTopicButton() const; [[nodiscard]] virtual TopicButton *displayedTopicButton() const;
[[nodiscard]] virtual bool displayForwardedFrom() const; [[nodiscard]] virtual bool displayForwardedFrom() const;
@ -456,7 +458,6 @@ public:
std::optional<QPoint> pressPoint) const; std::optional<QPoint> pressPoint) const;
[[nodiscard]] virtual TimeId displayedEditDate() const; [[nodiscard]] virtual TimeId displayedEditDate() const;
[[nodiscard]] virtual bool hasVisibleText() const; [[nodiscard]] virtual bool hasVisibleText() const;
[[nodiscard]] virtual HistoryMessageReply *displayedReply() const;
virtual void applyGroupAdminChanges( virtual void applyGroupAdminChanges(
const base::flat_set<UserId> &changes) { const base::flat_set<UserId> &changes) {
} }
@ -564,7 +565,6 @@ protected:
void clearSpecialOnlyEmoji(); void clearSpecialOnlyEmoji();
void checkSpecialOnlyEmoji(); void checkSpecialOnlyEmoji();
void refreshIsTopicRootReply();
private: private:
// This should be called only from previousInBlocksChanged() // This should be called only from previousInBlocksChanged()

View file

@ -40,6 +40,7 @@ struct ToPreviewOptions {
const std::vector<ItemPreviewImage> *existing = nullptr; const std::vector<ItemPreviewImage> *existing = nullptr;
bool hideSender = false; bool hideSender = false;
bool hideCaption = false; bool hideCaption = false;
bool ignoreMessageText = false;
bool generateImages = true; bool generateImages = true;
bool ignoreGroup = false; bool ignoreGroup = false;
bool ignoreTopic = true; bool ignoreTopic = true;

View file

@ -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.h"
#include "history/view/reactions/history_view_reactions_button.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_group_call_bar.h" // UserpicInRow.
#include "history/view/history_view_reply.h"
#include "history/view/history_view_view_button.h" // ViewButton. #include "history/view/history_view_view_button.h" // ViewButton.
#include "history/history.h" #include "history/history.h"
#include "boxes/share_box.h" #include "boxes/share_box.h"
@ -406,6 +407,7 @@ Message::Message(
Element *replacing) Element *replacing)
: Element(delegate, data, replacing, Flag(0)) : Element(delegate, data, replacing, Flag(0))
, _invertMedia(data->invertMedia() && !data->emptyText()) , _invertMedia(data->invertMedia() && !data->emptyText())
, _hideReply(delegate->elementHideReply(this))
, _bottomInfo( , _bottomInfo(
&data->history()->owner().reactions(), &data->history()->owner().reactions(),
BottomInfoDataFromMessage(this)) { BottomInfoDataFromMessage(this)) {
@ -597,6 +599,14 @@ auto Message::takeReactionAnimations()
QSize Message::performCountOptimalSize() { QSize Message::performCountOptimalSize() {
const auto item = data(); const auto item = data();
const auto replyData = item->Get<HistoryMessageReply>();
if (replyData && !_hideReply) {
AddComponents(Reply::Bit());
} else {
RemoveComponents(Reply::Bit());
}
const auto markup = item->inlineReplyMarkup(); const auto markup = item->inlineReplyMarkup();
const auto reactionsKey = [&] { const auto reactionsKey = [&] {
return embedReactionsInBottomInfo() return embedReactionsInBottomInfo()
@ -606,7 +616,6 @@ QSize Message::performCountOptimalSize() {
: 2; : 2;
}; };
const auto oldKey = reactionsKey(); const auto oldKey = reactionsKey();
refreshIsTopicRootReply();
validateText(); validateText();
validateInlineKeyboard(markup); validateInlineKeyboard(markup);
updateViewButtonExistence(); updateViewButtonExistence();
@ -633,17 +642,19 @@ QSize Message::performCountOptimalSize() {
if (_reactions) { if (_reactions) {
_reactions->initDimensions(); _reactions->initDimensions();
} }
const auto reply = Get<Reply>();
if (reply) {
reply->update(this, replyData);
}
if (drawBubble()) { if (drawBubble()) {
const auto forwarded = item->Get<HistoryMessageForwarded>(); const auto forwarded = item->Get<HistoryMessageForwarded>();
const auto reply = displayedReply();
const auto via = item->Get<HistoryMessageVia>(); const auto via = item->Get<HistoryMessageVia>();
const auto entry = logEntryOriginal(); const auto entry = logEntryOriginal();
if (forwarded) { if (forwarded) {
forwarded->create(via); forwarded->create(via);
} }
if (reply) {
reply->updateName(item);
}
auto mediaDisplayed = false; auto mediaDisplayed = false;
if (media) { if (media) {
@ -672,6 +683,10 @@ QSize Message::performCountOptimalSize() {
std::min(st::msgMaxWidth, reactionsMaxWidth)); std::min(st::msgMaxWidth, reactionsMaxWidth));
if (!mediaDisplayed || _viewButton) { if (!mediaDisplayed || _viewButton) {
minHeight += st::mediaInBubbleSkip; 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) { if (maxWidth >= reactionsMaxWidth) {
minHeight += _reactions->minHeight(); minHeight += _reactions->minHeight();
@ -771,13 +786,9 @@ QSize Message::performCountOptimalSize() {
accumulate_max(maxWidth, namew); accumulate_max(maxWidth, namew);
} }
if (reply) { if (reply) {
auto replyw = st::msgPadding.left() const auto replyw = st::msgPadding.left()
+ reply->maxWidth() + reply->maxWidth()
+ st::msgPadding.right(); + st::msgPadding.right();
if (reply->originalVia) {
replyw += st::msgServiceFont->spacew
+ reply->originalVia->maxWidth;
}
accumulate_max(maxWidth, replyw); accumulate_max(maxWidth, replyw);
} }
if (entry) { if (entry) {
@ -1221,9 +1232,11 @@ void Message::draw(Painter &p, const PaintContext &context) const {
p.restore(); p.restore();
} }
if (const auto reply = displayedReply()) { if (const auto reply = Get<Reply>()) {
if (reply->isNameUpdated(data())) { if (const auto replyData = item->Get<HistoryMessageReply>()) {
const_cast<Message*>(this)->setPendingResize(); if (reply->isNameUpdated(this, replyData)) {
const_cast<Message*>(this)->setPendingResize();
}
} }
} }
} }
@ -1602,8 +1615,15 @@ void Message::paintReplyInfo(
Painter &p, Painter &p,
QRect &trect, QRect &trect,
const PaintContext &context) const { const PaintContext &context) const {
if (const auto reply = displayedReply()) { if (const auto reply = Get<Reply>()) {
reply->paint(p, this, context, trect.x(), trect.y(), trect.width(), true); reply->paint(
p,
this,
context,
trect.x(),
trect.y(),
trect.width(),
true);
trect.setY(trect.y() + reply->height()); trect.setY(trect.y() + reply->height());
} }
} }
@ -1757,7 +1777,7 @@ void Message::clickHandlerPressedChanged(
toggleTopicButtonRipple(pressed); toggleTopicButtonRipple(pressed);
} else if (_viewButton) { } else if (_viewButton) {
_viewButton->checkLink(handler, pressed); _viewButton->checkLink(handler, pressed);
} else if (const auto reply = displayedReply() } else if (const auto reply = Get<Reply>()
; reply && (handler == reply->link())) { ; reply && (handler == reply->link())) {
toggleReplyRipple(pressed); toggleReplyRipple(pressed);
} }
@ -1800,32 +1820,24 @@ void Message::toggleRightActionRipple(bool pressed) {
} }
void Message::toggleReplyRipple(bool pressed) { void Message::toggleReplyRipple(bool pressed) {
const auto reply = displayedReply(); const auto reply = Get<Reply>();
if (!reply) { if (!reply) {
return; return;
} }
if (pressed) { if (pressed) {
if (!reply->ripple.animation && !unwrapped()) { if (!unwrapped()) {
const auto &padding = st::msgPadding; const auto &padding = st::msgPadding;
const auto geometry = countGeometry(); const auto geometry = countGeometry();
const auto item = data();
const auto margins = reply->margins(); const auto margins = reply->margins();
const auto size = QSize( const auto size = QSize(
geometry.width() - padding.left() - padding.right(), geometry.width() - padding.left() - padding.right(),
reply->height() - margins.top() - margins.bottom()); reply->height() - margins.top() - margins.bottom());
reply->ripple.animation = std::make_unique<Ui::RippleAnimation>( reply->createRippleAnimation(this, size);
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
size,
st::messageQuoteStyle.radius),
[=] { item->history()->owner().requestItemRepaint(item); });
} }
if (reply->ripple.animation) { reply->addRipple();
reply->ripple.animation->add(reply->ripple.lastPoint); } else {
} reply->stopLastRipple();
} else if (reply->ripple.animation) {
reply->ripple.animation->lastStop();
} }
} }
@ -2486,7 +2498,7 @@ bool Message::getStateReplyInfo(
QPoint point, QPoint point,
QRect &trect, QRect &trect,
not_null<TextState*> outResult) const { not_null<TextState*> outResult) const {
if (const auto reply = displayedReply()) { if (const auto reply = Get<Reply>()) {
const auto margins = reply->margins(); const auto margins = reply->margins();
const auto height = reply->height(); const auto height = reply->height();
if (point.y() >= trect.top() && point.y() < trect.top() + height) { if (point.y() >= trect.top() && point.y() < trect.top() + height) {
@ -2498,7 +2510,7 @@ bool Message::getStateReplyInfo(
if (g.contains(point)) { if (g.contains(point)) {
if (const auto link = reply->link()) { if (const auto link = reply->link()) {
outResult->link = reply->link(); outResult->link = reply->link();
reply->ripple.lastPoint = point - g.topLeft(); reply->saveRipplePoint(point - g.topLeft());
} }
} }
return true; return true;
@ -2581,7 +2593,7 @@ void Message::updatePressed(QPoint point) {
auto fwdheight = ((forwarded->text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height; auto fwdheight = ((forwarded->text.maxWidth() > trect.width()) ? 2 : 1) * st::semiboldFont->height;
trect.setTop(trect.top() + fwdheight); trect.setTop(trect.top() + fwdheight);
} }
if (const auto reply = item->Get<HistoryMessageReply>()) { if (const auto reply = Get<Reply>()) {
trect.setTop(trect.top() + reply->height()); trect.setTop(trect.top() + reply->height());
} }
if (const auto via = item->Get<HistoryMessageVia>()) { if (const auto via = item->Get<HistoryMessageVia>()) {
@ -3127,13 +3139,6 @@ WebPage *Message::logEntryOriginal() const {
return nullptr; return nullptr;
} }
HistoryMessageReply *Message::displayedReply() const {
if (const auto reply = data()->Get<HistoryMessageReply>()) {
return delegate()->elementHideReply(this) ? nullptr : reply;
}
return nullptr;
}
bool Message::toggleSelectionByHandlerClick( bool Message::toggleSelectionByHandlerClick(
const ClickHandlerPtr &handler) const { const ClickHandlerPtr &handler) const {
if (_comments && _comments->link == handler) { if (_comments && _comments->link == handler) {
@ -3584,7 +3589,7 @@ void Message::updateMediaInBubbleState() {
return displayFromName() return displayFromName()
|| displayedTopicButton() || displayedTopicButton()
|| displayForwardedFrom() || displayForwardedFrom()
|| displayedReply() || Has<Reply>()
|| item->Has<HistoryMessageVia>(); || item->Has<HistoryMessageVia>();
}; };
auto entry = logEntryOriginal(); auto entry = logEntryOriginal();
@ -3709,7 +3714,7 @@ QRect Message::innerGeometry() const {
+ st::topicButtonSkip); + st::topicButtonSkip);
} }
// Skip displayForwardedFrom() until there are no animations for it. // Skip displayForwardedFrom() until there are no animations for it.
if (const auto reply = displayedReply()) { if (const auto reply = Get<Reply>()) {
// See paintReplyInfo(). // See paintReplyInfo().
result.translate(0, reply->height()); result.translate(0, reply->height());
} }
@ -3876,7 +3881,7 @@ int Message::resizeContentGetHeight(int newWidth) {
textWidth - 2 * st::msgDateDelta.x())); textWidth - 2 * st::msgDateDelta.x()));
if (bubble) { if (bubble) {
auto reply = displayedReply(); auto reply = Get<Reply>();
auto via = item->Get<HistoryMessageVia>(); auto via = item->Get<HistoryMessageVia>();
auto entry = logEntryOriginal(); auto entry = logEntryOriginal();
@ -3966,7 +3971,6 @@ int Message::resizeContentGetHeight(int newWidth) {
newHeight += reply->resizeToWidth(contentWidth newHeight += reply->resizeToWidth(contentWidth
- st::msgPadding.left() - st::msgPadding.left()
- st::msgPadding.right()); - st::msgPadding.right());
reply->ripple.animation = nullptr;
} }
if (needInfoDisplay()) { if (needInfoDisplay()) {
newHeight += (bottomInfoHeight - st::msgDateFont->height); newHeight += (bottomInfoHeight - st::msgDateFont->height);

View file

@ -137,7 +137,6 @@ public:
[[nodiscard]] ClickHandlerPtr rightActionLink( [[nodiscard]] ClickHandlerPtr rightActionLink(
std::optional<QPoint> pressPoint) const override; std::optional<QPoint> pressPoint) const override;
[[nodiscard]] TimeId displayedEditDate() const override; [[nodiscard]] TimeId displayedEditDate() const override;
[[nodiscard]] HistoryMessageReply *displayedReply() const override;
[[nodiscard]] bool toggleSelectionByHandlerClick( [[nodiscard]] bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &handler) const override; const ClickHandlerPtr &handler) const override;
[[nodiscard]] bool allowTextSelectionByHandler( [[nodiscard]] bool allowTextSelectionByHandler(
@ -308,8 +307,9 @@ private:
mutable std::unique_ptr<FromNameStatus> _fromNameStatus; mutable std::unique_ptr<FromNameStatus> _fromNameStatus;
Ui::Text::String _rightBadge; Ui::Text::String _rightBadge;
mutable int _fromNameVersion = 0; mutable int _fromNameVersion = 0;
uint32 _bubbleWidthLimit : 31 = 0; uint32 _bubbleWidthLimit : 30 = 0;
uint32 _invertMedia : 1 = 0; uint32 _invertMedia : 1 = 0;
uint32 _hideReply : 1 = 0;
BottomInfo _bottomInfo; BottomInfo _bottomInfo;

View file

@ -597,7 +597,11 @@ void PinnedWidget::listUpdateDateLink(
} }
bool PinnedWidget::listElementHideReply(not_null<const Element*> view) { bool PinnedWidget::listElementHideReply(not_null<const Element*> view) {
return (view->data()->replyToId() == _thread->topicRootId()); if (const auto reply = view->data()->Get<HistoryMessageReply>()) {
return !reply->fields().manualQuote
&& (reply->messageId() == _thread->topicRootId());
}
return false;
} }
bool PinnedWidget::listElementShownUnread(not_null<const Element*> view) { bool PinnedWidget::listElementShownUnread(not_null<const Element*> view) {

View file

@ -972,8 +972,7 @@ bool RepliesWidget::confirmSendingFiles(
insertTextOnCancel)); insertTextOnCancel));
//ActivateWindow(controller()); //ActivateWindow(controller());
const auto shown = controller()->show(std::move(box)); controller()->show(std::move(box));
shown->setCloseByOutsideClick(false);
return true; return true;
} }
@ -2020,7 +2019,7 @@ bool RepliesWidget::showMessage(
} }
const auto id = FullMsgId(_history->peer->id, messageId); const auto id = FullMsgId(_history->peer->id, messageId);
const auto message = _history->owner().message(id); const auto message = _history->owner().message(id);
if (!message || !message->inThread(_rootId)) { if (!message || (!message->inThread(_rootId) && id.msg != _rootId)) {
return false; return false;
} }
const auto originMessage = [&]() -> HistoryItem* { const auto originMessage = [&]() -> HistoryItem* {
@ -2516,7 +2515,25 @@ void RepliesWidget::listUpdateDateLink(
} }
bool RepliesWidget::listElementHideReply(not_null<const Element*> view) { bool RepliesWidget::listElementHideReply(not_null<const Element*> view) {
return (view->data()->replyToId() == _rootId); if (const auto reply = view->data()->Get<HistoryMessageReply>()) {
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<HistoryMessageForwarded>();
if (forwarded
&& forwarded->savedFromPeer
&& forwarded->savedFromPeer->id == replyToPeerId
&& forwarded->savedFromMsgId == reply->messageId()) {
return true;
}
}
}
return false;
} }
bool RepliesWidget::listElementShownUnread(not_null<const Element*> view) { bool RepliesWidget::listElementShownUnread(not_null<const Element*> view) {

View file

@ -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<Ui::BackgroundEmojiData*> data,
not_null<Ui::BackgroundEmojiCache*> cache,
not_null<Ui::Text::QuotePaintCache*> quote,
not_null<const Element*> 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<Element*> view,
not_null<HistoryMessageReply*> 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<Ui::SpoilerAnimation>(repaint);
}
} else {
_spoiler = nullptr;
}
}
bool Reply::expand() {
if (!_expandable || _expanded) {
return false;
}
_expanded = true;
return true;
}
void Reply::setLinkFrom(
not_null<Element*> view,
not_null<HistoryMessageReply*> 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<ClickHandlerContext>();
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<Reply>()) {
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<LambdaClickHandler>(externalLink)
: nullptr;
}
PeerData *Reply::sender(
not_null<const Element*> view,
not_null<HistoryMessageReply*> 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<HistoryMessageForwarded>()) {
// Forward of a reply. Show reply-to original sender.
const auto forwarded = message->Get<HistoryMessageForwarded>();
if (forwarded) {
return forwarded->originalSender;
}
}
if (const auto from = message->displayFrom()) {
return from;
}
return message->author().get();
}
QString Reply::senderName(
not_null<const Element*> view,
not_null<HistoryMessageReply*> 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<HistoryMessageForwarded>()) {
// Forward of a reply. Show reply-to original sender.
const auto forwarded
= data->resolvedMessage->Get<HistoryMessageForwarded>();
if (forwarded) {
Assert(forwarded->hiddenSenderInfo != nullptr);
return forwarded->hiddenSenderInfo->name;
}
}
return QString();
}
QString Reply::senderName(
not_null<PeerData*> peer,
bool shorten) const {
const auto user = shorten ? peer->asUser() : nullptr;
return user ? user->firstName : peer->name();
}
bool Reply::isNameUpdated(
not_null<const Element*> view,
not_null<HistoryMessageReply*> 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<const Element*> view,
not_null<HistoryMessageReply*> data,
std::optional<PeerData*> resolvedSender) const {
auto viaBotUsername = QString();
const auto message = data->resolvedMessage.get();
if (message && !message->Has<HistoryMessageForwarded>()) {
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<const Element*> 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 &quoteSt = _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<HistoryMessageReply>();
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<style::owned_color>();
auto copy = std::optional<style::TextPalette>();
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<const Element*> view,
QSize size) {
_ripple.animation = std::make_unique<Ui::RippleAnimation>(
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

View file

@ -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<Reply, Element> {
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<Element*> view,
not_null<HistoryMessageReply*> data);
[[nodiscard]] bool isNameUpdated(
not_null<const Element*> view,
not_null<HistoryMessageReply*> data) const;
void updateName(
not_null<const Element*> view,
not_null<HistoryMessageReply*> data,
std::optional<PeerData*> 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<const Element*> view,
const Ui::ChatPaintContext &context,
int x,
int y,
int w,
bool inBubble) const;
void unloadPersistentAnimation();
void createRippleAnimation(not_null<const Element*> 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<Element*> view,
not_null<HistoryMessageReply*> data);
[[nodiscard]] PeerData *sender(
not_null<const Element*> view,
not_null<HistoryMessageReply*> data) const;
[[nodiscard]] QString senderName(
not_null<const Element*> view,
not_null<HistoryMessageReply*> data,
bool shorten) const;
[[nodiscard]] QString senderName(
not_null<PeerData*> peer,
bool shorten) const;
ClickHandlerPtr _link;
std::unique_ptr<Ui::SpoilerAnimation> _spoiler;
mutable PeerData *_externalSender = nullptr;
mutable PeerData *_colorPeer = nullptr;
mutable struct {
mutable std::unique_ptr<Ui::RippleAnimation> 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

View file

@ -429,8 +429,7 @@ bool ScheduledWidget::confirmSendingFiles(
insertTextOnCancel)); insertTextOnCancel));
//ActivateWindow(controller()); //ActivateWindow(controller());
const auto shown = controller()->show(std::move(box)); controller()->show(std::move(box));
shown->setCloseByOutsideClick(false);
return true; return true;
} }

View file

@ -419,7 +419,7 @@ bool ExtendedPreview::needsBubble() const {
&& (item->repliesAreComments() && (item->repliesAreComments()
|| item->externalReply() || item->externalReply()
|| item->viaBot() || item->viaBot()
|| _parent->displayedReply() || _parent->displayReply()
|| _parent->displayForwardedFrom() || _parent->displayForwardedFrom()
|| _parent->displayFromName() || _parent->displayFromName()
|| _parent->displayedTopicButton()); || _parent->displayedTopicButton());

View file

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h" #include "history/history.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.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/history_view_transcribe_button.h"
#include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_media_spoiler.h" #include "history/view/media/history_view_media_spoiler.h"
@ -217,12 +218,12 @@ QSize Gif::countOptimalSize() {
} else if (isUnwrapped()) { } else if (isUnwrapped()) {
const auto item = _parent->data(); const auto item = _parent->data();
auto via = item->Get<HistoryMessageVia>(); auto via = item->Get<HistoryMessageVia>();
auto reply = _parent->displayedReply(); auto reply = _parent->Get<Reply>();
auto forwarded = item->Get<HistoryMessageForwarded>(); auto forwarded = item->Get<HistoryMessageForwarded>();
if (forwarded) { if (forwarded) {
forwarded->create(via); forwarded->create(via);
} }
maxWidth += additionalWidth(via, reply, forwarded); maxWidth += additionalWidth(reply, via, forwarded);
accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); accumulate_max(maxWidth, _parent->reactionsOptimalWidth());
} }
return { maxWidth, minHeight }; return { maxWidth, minHeight };
@ -274,10 +275,10 @@ QSize Gif::countCurrentSize(int newWidth) {
const auto item = _parent->data(); const auto item = _parent->data();
auto via = item->Get<HistoryMessageVia>(); auto via = item->Get<HistoryMessageVia>();
auto reply = _parent->displayedReply(); auto reply = _parent->Get<Reply>();
auto forwarded = item->Get<HistoryMessageForwarded>(); auto forwarded = item->Get<HistoryMessageForwarded>();
if (via || reply || forwarded) { if (via || reply || forwarded) {
auto additional = additionalWidth(via, reply, forwarded); auto additional = additionalWidth(reply, via, forwarded);
newWidth += additional; newWidth += additional;
accumulate_min(newWidth, availableWidth); accumulate_min(newWidth, availableWidth);
auto usew = maxWidth() - additional; auto usew = maxWidth() - additional;
@ -385,13 +386,13 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
auto usex = 0, usew = paintw; auto usex = 0, usew = paintw;
const auto unwrapped = isUnwrapped(); const auto unwrapped = isUnwrapped();
const auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr; const auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr;
const auto reply = unwrapped ? _parent->displayedReply() : nullptr; const auto reply = unwrapped ? _parent->Get<Reply>() : nullptr;
const auto forwarded = unwrapped ? item->Get<HistoryMessageForwarded>() : nullptr; const auto forwarded = unwrapped ? item->Get<HistoryMessageForwarded>() : nullptr;
const auto rightAligned = unwrapped const auto rightAligned = unwrapped
&& outbg && outbg
&& !_parent->delegate()->elementIsChatWide(); && !_parent->delegate()->elementIsChatWide();
if (via || reply || forwarded) { if (via || reply || forwarded) {
usew = maxWidth() - additionalWidth(via, reply, forwarded); usew = maxWidth() - additionalWidth(reply, via, forwarded);
if (rightAligned) { if (rightAligned) {
usex = width() - usew; usex = width() - usew;
} }
@ -623,7 +624,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
== PaintContext::SkipDrawingParts::Surrounding; == PaintContext::SkipDrawingParts::Surrounding;
if (!unwrapped && !skipDrawingSurrounding) { if (!unwrapped && !skipDrawingSurrounding) {
drawCornerStatus(p, context, QPoint()); if (!isRound || !inWebPage) {
drawCornerStatus(p, context, QPoint());
}
} else if (!skipDrawingSurrounding) { } else if (!skipDrawingSurrounding) {
if (isRound) { if (isRound) {
const auto mediaUnread = item->hasUnreadMediaFlag(); const auto mediaUnread = item->hasUnreadMediaFlag();
@ -1013,13 +1016,13 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
const auto item = _parent->data(); const auto item = _parent->data();
auto usew = paintw, usex = 0; auto usew = paintw, usex = 0;
const auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr; const auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr;
const auto reply = unwrapped ? _parent->displayedReply() : nullptr; const auto reply = unwrapped ? _parent->Get<Reply>() : nullptr;
const auto forwarded = unwrapped ? item->Get<HistoryMessageForwarded>() : nullptr; const auto forwarded = unwrapped ? item->Get<HistoryMessageForwarded>() : nullptr;
const auto rightAligned = unwrapped const auto rightAligned = unwrapped
&& outbg && outbg
&& !_parent->delegate()->elementIsChatWide(); && !_parent->delegate()->elementIsChatWide();
if (via || reply || forwarded) { if (via || reply || forwarded) {
usew = maxWidth() - additionalWidth(via, reply, forwarded); usew = maxWidth() - additionalWidth(reply, via, forwarded);
if (rightAligned) { if (rightAligned) {
usex = width() - usew; usex = width() - usew;
} }
@ -1094,15 +1097,8 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
const auto replyRect = QRect(rectx, recty, rectw, recth); const auto replyRect = QRect(rectx, recty, rectw, recth);
if (replyRect.contains(point)) { if (replyRect.contains(point)) {
result.link = reply->link(); result.link = reply->link();
reply->ripple.lastPoint = point - replyRect.topLeft(); reply->saveRipplePoint(point - replyRect.topLeft());
if (!reply->ripple.animation) { reply->createRippleAnimation(_parent, replyRect.size());
reply->ripple.animation = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
replyRect.size(),
st::messageQuoteStyle.radius),
[=] { item->history()->owner().requestItemRepaint(item); });
}
return result; return result;
} }
} }
@ -1520,7 +1516,7 @@ bool Gif::needsBubble() const {
return item->repliesAreComments() return item->repliesAreComments()
|| item->externalReply() || item->externalReply()
|| item->viaBot() || item->viaBot()
|| _parent->displayedReply() || _parent->displayReply()
|| _parent->displayForwardedFrom() || _parent->displayForwardedFrom()
|| _parent->displayFromName() || _parent->displayFromName()
|| _parent->displayedTopicButton(); || _parent->displayedTopicButton();
@ -1542,10 +1538,10 @@ QRect Gif::contentRectForReactions() const {
&& !_parent->delegate()->elementIsChatWide(); && !_parent->delegate()->elementIsChatWide();
const auto item = _parent->data(); const auto item = _parent->data();
const auto via = item->Get<HistoryMessageVia>(); const auto via = item->Get<HistoryMessageVia>();
const auto reply = _parent->displayedReply(); const auto reply = _parent->Get<Reply>();
const auto forwarded = item->Get<HistoryMessageForwarded>(); const auto forwarded = item->Get<HistoryMessageForwarded>();
if (via || reply || forwarded) { if (via || reply || forwarded) {
usew = maxWidth() - additionalWidth(via, reply, forwarded); usew = maxWidth() - additionalWidth(reply, via, forwarded);
} }
accumulate_max(usew, _parent->reactionsOptimalWidth()); accumulate_max(usew, _parent->reactionsOptimalWidth());
if (rightAligned) { if (rightAligned) {
@ -1602,8 +1598,8 @@ QPoint Gif::resolveCustomInfoRightBottom() const {
int Gif::additionalWidth() const { int Gif::additionalWidth() const {
const auto item = _parent->data(); const auto item = _parent->data();
return additionalWidth( return additionalWidth(
_parent->Get<Reply>(),
item->Get<HistoryMessageVia>(), item->Get<HistoryMessageVia>(),
item->Get<HistoryMessageReply>(),
item->Get<HistoryMessageForwarded>()); item->Get<HistoryMessageForwarded>());
} }
@ -1763,7 +1759,10 @@ void Gif::refreshCaption() {
_caption = createCaption(_parent->data()); _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; int result = 0;
if (forwarded) { if (forwarded) {
accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right()); accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right());

View file

@ -37,6 +37,7 @@ enum class Error;
namespace HistoryView { namespace HistoryView {
class Reply;
class TranscribeButton; class TranscribeButton;
class Gif final : public File { class Gif final : public File {
@ -176,8 +177,8 @@ private:
[[nodiscard]] bool needInfoDisplay() const; [[nodiscard]] bool needInfoDisplay() const;
[[nodiscard]] bool needCornerStatusDisplay() const; [[nodiscard]] bool needCornerStatusDisplay() const;
[[nodiscard]] int additionalWidth( [[nodiscard]] int additionalWidth(
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded) const; const HistoryMessageForwarded *forwarded) const;
[[nodiscard]] int additionalWidth() const; [[nodiscard]] int additionalWidth() const;
[[nodiscard]] bool isUnwrapped() const; [[nodiscard]] bool isUnwrapped() const;

View file

@ -388,7 +388,7 @@ void Giveaway::paintChannels(
.align = style::al_left, .align = style::al_left,
.palette = &stm->textPalette, .palette = &stm->textPalette,
.now = context.now, .now = context.now,
.elisionOneLine = true, .elisionLines = 1,
.elisionBreakEverywhere = true, .elisionBreakEverywhere = true,
}); });
} }

View file

@ -375,7 +375,7 @@ bool Location::needsBubble() const {
return item->repliesAreComments() return item->repliesAreComments()
|| item->externalReply() || item->externalReply()
|| item->viaBot() || item->viaBot()
|| _parent->displayedReply() || _parent->displayReply()
|| _parent->displayForwardedFrom() || _parent->displayForwardedFrom()
|| _parent->displayFromName() || _parent->displayFromName()
|| _parent->displayedTopicButton(); || _parent->displayedTopicButton();

View file

@ -869,7 +869,7 @@ bool GroupedMedia::computeNeedBubble() const {
if (item->repliesAreComments() if (item->repliesAreComments()
|| item->externalReply() || item->externalReply()
|| item->viaBot() || item->viaBot()
|| _parent->displayedReply() || _parent->displayReply()
|| _parent->displayForwardedFrom() || _parent->displayForwardedFrom()
|| _parent->displayFromName() || _parent->displayFromName()
|| _parent->displayedTopicButton() || _parent->displayedTopicButton()

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_sticker.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.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.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "lottie/lottie_single_player.h" #include "lottie/lottie_single_player.h"
@ -54,13 +55,13 @@ QSize UnwrappedMedia::countOptimalSize() {
if (_parent->media() == this) { if (_parent->media() == this) {
const auto item = _parent->data(); const auto item = _parent->data();
const auto via = item->Get<HistoryMessageVia>(); const auto via = item->Get<HistoryMessageVia>();
const auto reply = _parent->displayedReply(); const auto reply = _parent->Get<Reply>();
const auto topic = _parent->displayedTopicButton(); const auto topic = _parent->displayedTopicButton();
const auto forwarded = getDisplayedForwardedInfo(); const auto forwarded = getDisplayedForwardedInfo();
if (forwarded) { if (forwarded) {
forwarded->create(via); forwarded->create(via);
} }
maxWidth += additionalWidth(topic, via, reply, forwarded); maxWidth += additionalWidth(topic, reply, via, forwarded);
accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); accumulate_max(maxWidth, _parent->reactionsOptimalWidth());
if (const auto size = _parent->rightActionSize()) { if (const auto size = _parent->rightActionSize()) {
minHeight = std::max( minHeight = std::max(
@ -93,11 +94,11 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) {
accumulate_max(newWidth, _parent->reactionsOptimalWidth()); accumulate_max(newWidth, _parent->reactionsOptimalWidth());
_topAdded = 0; _topAdded = 0;
const auto via = item->Get<HistoryMessageVia>(); const auto via = item->Get<HistoryMessageVia>();
const auto reply = _parent->displayedReply(); const auto reply = _parent->Get<Reply>();
const auto topic = _parent->displayedTopicButton(); const auto topic = _parent->displayedTopicButton();
const auto forwarded = getDisplayedForwardedInfo(); const auto forwarded = getDisplayedForwardedInfo();
if (topic || via || reply || forwarded) { 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 optimalw = maxWidth() - additional;
const auto additionalMinWidth = std::min(additional, st::msgReplyPadding.left() + st::msgMinWidth / 2); const auto additionalMinWidth = std::min(additional, st::msgReplyPadding.left() + st::msgMinWidth / 2);
_additionalOnTop = (optimalw + additionalMinWidth) > newWidth; _additionalOnTop = (optimalw + additionalMinWidth) > newWidth;
@ -107,7 +108,7 @@ QSize UnwrappedMedia::countCurrentSize(int newWidth) {
if (reply) { if (reply) {
[[maybe_unused]] auto h = reply->resizeToWidth(surroundingWidth); [[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) { if (_additionalOnTop) {
_topAdded = surrounding.height + st::msgMargin.bottom(); _topAdded = surrounding.height + st::msgMargin.bottom();
newHeight += _topAdded; newHeight += _topAdded;
@ -166,17 +167,17 @@ void UnwrappedMedia::draw(Painter &p, const PaintContext &context) const {
if (!inWebPage && (context.skipDrawingParts if (!inWebPage && (context.skipDrawingParts
!= PaintContext::SkipDrawingParts::Surrounding)) { != PaintContext::SkipDrawingParts::Surrounding)) {
const auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>(); const auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>();
const auto reply = inWebPage ? nullptr : _parent->displayedReply(); const auto reply = inWebPage ? nullptr : _parent->Get<Reply>();
const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton(); const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton();
const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo(); 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( UnwrappedMedia::SurroundingInfo UnwrappedMedia::surroundingInfo(
const TopicButton *topic, const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded, const HistoryMessageForwarded *forwarded,
int outerw) const { int outerw) const {
if (!topic && !via && !reply && !forwarded) { if (!topic && !via && !reply && !forwarded) {
@ -242,8 +243,8 @@ void UnwrappedMedia::drawSurrounding(
const QRect &inner, const QRect &inner,
const PaintContext &context, const PaintContext &context,
const TopicButton *topic, const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded) const { const HistoryMessageForwarded *forwarded) const {
const auto st = context.st; const auto st = context.st;
const auto sti = context.imageStyle(); const auto sti = context.imageStyle();
@ -263,9 +264,9 @@ void UnwrappedMedia::drawSurrounding(
} }
auto replyRight = 0; auto replyRight = 0;
auto rectw = _additionalOnTop 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()); : (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; auto recth = surrounding.panelHeight;
if (!surrounding.topicSize.isEmpty()) { if (!surrounding.topicSize.isEmpty()) {
auto rectw = surrounding.topicSize.width(); auto rectw = surrounding.topicSize.width();
@ -416,14 +417,14 @@ TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const {
if (_parent->media() == this) { if (_parent->media() == this) {
const auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>(); const auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>();
const auto reply = inWebPage ? nullptr : _parent->displayedReply(); const auto reply = inWebPage ? nullptr : _parent->Get<Reply>();
const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton(); const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton();
const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo(); const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo();
auto replyRight = 0; auto replyRight = 0;
auto rectw = _additionalOnTop 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()); : (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; auto recth = surrounding.panelHeight;
if (!surrounding.topicSize.isEmpty()) { if (!surrounding.topicSize.isEmpty()) {
auto rectw = surrounding.topicSize.width(); 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); const auto replyRect = QRect(rectx, recty, rectw, recth);
if (replyRect.contains(point)) { if (replyRect.contains(point)) {
result.link = reply->link(); result.link = reply->link();
reply->ripple.lastPoint = point - replyRect.topLeft(); reply->saveRipplePoint(point - replyRect.topLeft());
if (!reply->ripple.animation) { reply->createRippleAnimation(_parent, replyRect.size());
reply->ripple.animation = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
replyRect.size(),
st::messageQuoteStyle.radius),
[=] { item->history()->owner().requestItemRepaint(item); });
}
return result;
} }
} }
replyRight = rectx + rectw - st::msgReplyPadding.right(); replyRight = rectx + rectw - st::msgReplyPadding.right();
@ -542,7 +535,7 @@ bool UnwrappedMedia::hasTextForCopy() const {
} }
bool UnwrappedMedia::dragItemByHandler(const ClickHandlerPtr &p) const { bool UnwrappedMedia::dragItemByHandler(const ClickHandlerPtr &p) const {
const auto reply = _parent->displayedReply(); const auto reply = _parent->Get<Reply>();
return !reply || (reply->link() != p); return !reply || (reply->link() != p);
} }
@ -649,8 +642,8 @@ bool UnwrappedMedia::needInfoDisplay() const {
int UnwrappedMedia::additionalWidth( int UnwrappedMedia::additionalWidth(
const TopicButton *topic, const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded) const { const HistoryMessageForwarded *forwarded) const {
auto result = st::msgReplyPadding.left() + _parent->infoWidth() + 2 * st::msgDateImgPadding.x(); auto result = st::msgReplyPadding.left() + _parent->infoWidth() + 2 * st::msgDateImgPadding.x();
if (topic) { if (topic) {

View file

@ -17,6 +17,7 @@ struct HistoryMessageForwarded;
namespace HistoryView { namespace HistoryView {
class Reply;
struct TopicButton; struct TopicButton;
class UnwrappedMedia final : public Media { class UnwrappedMedia final : public Media {
@ -120,8 +121,8 @@ private:
}; };
[[nodiscard]] SurroundingInfo surroundingInfo( [[nodiscard]] SurroundingInfo surroundingInfo(
const TopicButton *topic, const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded, const HistoryMessageForwarded *forwarded,
int outerw) const; int outerw) const;
void drawSurrounding( void drawSurrounding(
@ -129,8 +130,8 @@ private:
const QRect &inner, const QRect &inner,
const PaintContext &context, const PaintContext &context,
const TopicButton *topic, const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded) const; const HistoryMessageForwarded *forwarded) const;
QSize countOptimalSize() override; QSize countOptimalSize() override;
@ -139,8 +140,8 @@ private:
bool needInfoDisplay() const; bool needInfoDisplay() const;
int additionalWidth( int additionalWidth(
const TopicButton *topic, const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via, const HistoryMessageVia *via,
const HistoryMessageReply *reply,
const HistoryMessageForwarded *forwarded) const; const HistoryMessageForwarded *forwarded) const;
int calculateFullRight(const QRect &inner) const; int calculateFullRight(const QRect &inner) const;

View file

@ -1077,7 +1077,7 @@ bool Photo::needsBubble() const {
&& (item->repliesAreComments() && (item->repliesAreComments()
|| item->externalReply() || item->externalReply()
|| item->viaBot() || item->viaBot()
|| _parent->displayedReply() || _parent->displayReply()
|| _parent->displayForwardedFrom() || _parent->displayForwardedFrom()
|| _parent->displayFromName() || _parent->displayFromName()
|| _parent->displayedTopicButton()); || _parent->displayedTopicButton());

View file

@ -418,13 +418,6 @@ bool ThemeDocument::isReadyForOpen() const {
return !_data || _dataMedia->loaded(); 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 { bool ThemeDocument::hasHeavyPart() const {
return (_dataMedia != nullptr); return (_dataMedia != nullptr);
} }

View file

@ -46,7 +46,6 @@ public:
return true; return true;
} }
bool isReadyForOpen() const override; bool isReadyForOpen() const override;
QString additionalInfoString() const override;
bool hasHeavyPart() const override; bool hasHeavyPart() const override;
void unloadHeavyPart() override; void unloadHeavyPart() override;

View file

@ -710,7 +710,6 @@ TextState WebPage::textState(QPoint point, StateRequest request) const {
auto inner = outer.marginsRemoved(innerMargin()); auto inner = outer.marginsRemoved(innerMargin());
auto tshift = inner.top(); auto tshift = inner.top();
auto paintw = inner.width(); auto paintw = inner.width();
auto attachAdditionalInfoText = _attach ? _attach->additionalInfoString() : QString();
auto lineHeight = UnitedLineHeight(); auto lineHeight = UnitedLineHeight();
auto inThumb = false; auto inThumb = false;

View file

@ -47,6 +47,7 @@ public:
QPoint shift, QPoint shift,
int index); int index);
int width() override;
QString entityData() override; QString entityData() override;
void paint(QPainter &p, const Context &context) override; void paint(QPainter &p, const Context &context) override;
void unload() override; void unload() override;
@ -73,6 +74,10 @@ StripEmoji::StripEmoji(
, _index(index) { , _index(index) {
} }
int StripEmoji::width() {
return _wrapped->width();
}
QString StripEmoji::entityData() { QString StripEmoji::entityData() {
return _wrapped->entityData(); return _wrapped->entityData();
} }

View file

@ -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<bool(int)> CreateErrorCallback(
int max,
tr::phrase<lngtag_count> 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<Ui::GenericBox*> box,
not_null<Info::Controller*> controller,
not_null<PeerData*> peer) {
box->setWidth(st::boxWideWidth);
const auto weakWindow = base::make_weak(controller->parentController());
const auto bar = box->verticalLayout()->add(
object_ptr<Ui::Premium::TopBar>(
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<Ui::IconButton>(
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<GiveawayType>;
struct State final {
State(not_null<PeerData*> p) : apiOptions(p) {
}
Api::PremiumGiftCodeOptions apiOptions;
rpl::lifetime lifetimeApi;
std::vector<not_null<PeerData*>> selectedToAward;
rpl::event_stream<> toAwardAmountChanged;
std::vector<not_null<PeerData*>> selectedToSubscribe;
rpl::variable<GiveawayType> typeValue;
rpl::variable<int> sliderValue;
rpl::variable<TimeId> dateValue;
rpl::variable<std::vector<QString>> countriesValue;
bool confirmButtonBusy = false;
};
const auto state = box->lifetime().make_state<State>(peer);
const auto typeGroup = std::make_shared<GiveawayGroup>();
const auto loading = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)));
{
loading->toggle(true, anim::type::instant);
const auto container = loading->entity();
Settings::AddSkip(container);
Settings::AddSkip(container);
container->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_contacts_loading(),
st::giveawayLoadingLabel)));
Settings::AddSkip(container);
Settings::AddSkip(container);
}
const auto contentWrap = box->verticalLayout()->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)));
contentWrap->toggle(false, anim::type::instant);
{
const auto row = contentWrap->entity()->add(
object_ptr<Giveaway::GiveawayTypeRow>(
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<Giveaway::GiveawayTypeRow>(
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<PeerListBox*> 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>(
controller,
peer);
listController->setCheckError(CreateErrorCallback(
state->apiOptions.giveawayAddPeersMax(),
tr::lng_giveaway_maximum_users_error));
box->uiShow()->showBox(
Box<PeerListBox>(
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<Ui::SlideWrap<Ui::VerticalLayout>>(
contentWrap,
object_ptr<Ui::VerticalLayout>(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<Ui::VerticalLayout>(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<Ui::FlatLabel>(
sliderContainer,
st::giveawayGiftCodeQuantitySubtitle);
rightLabel->show();
const auto floatLabel = Ui::CreateChild<Ui::FlatLabel>(
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<Ui::MediaSlider>(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<Ui::VerticalLayout>(randomWrap));
Settings::AddSubsectionTitle(
channelsContainer,
tr::lng_giveaway_channels_title(),
st::giveawayGiftCodeChannelsSubsectionPadding);
struct ListState final {
ListState(not_null<PeerData*> p) : controller(p) {
}
PeerListContentDelegateSimple delegate;
Giveaway::SelectedChannelsListController controller;
};
const auto listState = box->lifetime().make_state<ListState>(peer);
listState->delegate.setContent(channelsContainer->add(
object_ptr<PeerListContent>(
channelsContainer,
&listState->controller)));
listState->controller.setDelegate(&listState->delegate);
listState->controller.channelRemoved(
) | rpl::start_with_next([=](not_null<PeerData*> 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<PeerListBox*> 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<Controller>(
peer,
box->uiShow(),
state->selectedToSubscribe);
controller->setCheckError(CreateErrorCallback(
state->apiOptions.giveawayAddPeersMax(),
tr::lng_giveaway_maximum_channels_error));
box->uiShow()->showBox(
Box<PeerListBox>(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<GiveawayGroup>();
{
const auto countriesContainer = randomWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(randomWrap));
Settings::AddSubsectionTitle(
countriesContainer,
tr::lng_giveaway_users_title());
membersGroup->setValue(GiveawayType::AllMembers);
auto subtitle = state->countriesValue.value(
) | rpl::map([=](const std::vector<QString> &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<QString> 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<Giveaway::GiveawayTypeRow>(
box,
GiveawayType::AllMembers,
rpl::duplicate(subtitle)));
row->addRadio(membersGroup);
row->setClickedCallback(createCallback(GiveawayType::AllMembers));
}
const auto row = countriesContainer->add(
object_ptr<Giveaway::GiveawayTypeRow>(
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<Ui::VerticalLayout>(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<Ui::GenericBox*> 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<Ui::RadiobuttonGroup>(0);
const auto listOptions = contentWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(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<Ui::FlatLabel>(
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<LambdaClickHandler>([=] {
box->closeBox();
Settings::ShowPremium(&peer->session(), QString());
}));
listOptions->add(object_ptr<Ui::DividerLabel>(
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<Ui::RoundButton>(
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<PeerData*> 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<PeerData*> 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());
});
});
}

View file

@ -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<Ui::GenericBox*> box,
not_null<Info::Controller*> controller,
not_null<PeerData*> peer);

View file

@ -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);

View file

@ -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<void()> updateCallback) override;
void rightActionStopLastRipple() override;
private:
std::unique_ptr<Ui::RippleAnimation> _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<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());
_actionRipple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point);
}
void ChannelRow::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
} // namespace
AwardMembersListController::AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer)
: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members) {
}
void AwardMembersListController::rowClicked(not_null<PeerListRow*> row) {
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {
return;
}
delegate()->peerListSetRowChecked(row, checked);
}
std::unique_ptr<PeerListRow> AwardMembersListController::createRow(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (!user || user->isInaccessible() || user->isBot() || user->isSelf()) {
return nullptr;
}
return std::make_unique<PeerListRow>(participant);
}
base::unique_qptr<Ui::PopupMenu> AwardMembersListController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
void AwardMembersListController::setCheckError(Fn<bool(int)> callback) {
_checkErrorCallback = std::move(callback);
}
MyChannelsListController::MyChannelsListController(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
std::vector<not_null<PeerData*>> selected)
: PeerListController(
std::make_unique<PeerListGlobalSearchController>(&peer->session()))
, _peer(peer)
, _show(show)
, _selected(std::move(selected)) {
}
std::unique_ptr<PeerListRow> MyChannelsListController::createSearchRow(
not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
return createRow(channel);
}
return nullptr;
}
std::unique_ptr<PeerListRow> MyChannelsListController::createRestoredRow(
not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
return createRow(channel);
}
return nullptr;
}
void MyChannelsListController::rowClicked(not_null<PeerListRow*> 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<void()> 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<MTP::Sender>(
&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<bool(int)> callback) {
_checkErrorCallback = std::move(callback);
}
std::unique_ptr<PeerListRow> MyChannelsListController::createRow(
not_null<ChannelData*> channel) const {
if (channel->isMegagroup()) {
return nullptr;
}
auto row = std::make_unique<PeerListRow>(channel);
row->setCustomStatus(tr::lng_chat_status_subscribers(
tr::now,
lt_count,
channel->membersCount()));
return row;
}
SelectedChannelsListController::SelectedChannelsListController(
not_null<PeerData*> peer)
: _peer(peer) {
PeerListController::setStyleOverrides(
&st::giveawayGiftCodeChannelsPeerList);
}
void SelectedChannelsListController::setTopStatus(rpl::producer<QString> 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<not_null<PeerData*>> 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<not_null<PeerData*>> {
return _channelRemoved.events();
}
void SelectedChannelsListController::rowClicked(not_null<PeerListRow*> row) {
}
void SelectedChannelsListController::rowRightActionClicked(
not_null<PeerListRow*> 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<PeerListRow> SelectedChannelsListController::createRow(
not_null<ChannelData*> channel) const {
if (channel->isMegagroup()) {
return nullptr;
}
const auto isYourChannel = (_peer->asChannel() == channel);
auto row = isYourChannel
? std::make_unique<PeerListRow>(channel)
: std::make_unique<ChannelRow>(channel);
row->setCustomStatus(isYourChannel
? QString()
: tr::lng_chat_status_subscribers(
tr::now,
lt_count,
channel->membersCount()));
return row;
}
} // namespace Giveaway

View file

@ -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<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
void setCheckError(Fn<bool(int)> callback);
void rowClicked(not_null<PeerListRow*> row) override;
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
private:
Fn<bool(int)> _checkErrorCallback;
};
class MyChannelsListController : public PeerListController {
public:
MyChannelsListController(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
std::vector<not_null<PeerData*>> selected);
void setCheckError(Fn<bool(int)> callback);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<ChannelData*> channel) const;
const not_null<PeerData*> _peer;
const std::shared_ptr<Ui::Show> _show;
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
rpl::lifetime _apiLifetime;
};
class SelectedChannelsListController : public PeerListController {
public:
SelectedChannelsListController(not_null<PeerData*> peer);
void setTopStatus(rpl::producer<QString> status);
void rebuild(std::vector<not_null<PeerData*>> selected);
[[nodiscard]] rpl::producer<not_null<PeerData*>> channelRemoved() const;
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<ChannelData*> channel) const;
const not_null<PeerData*> _peer;
rpl::event_stream<not_null<PeerData*>> _channelRemoved;
rpl::lifetime _statusLifetime;
};
} // namespace Giveaway

View file

@ -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<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> 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<Ui::RadioenumGroup<Type>> typeGroup) {
const auto &st = st::defaultCheckbox;
const auto radio = Ui::CreateChild<Ui::Radioenum<Type>>(
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

View file

@ -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 <typename Enum>
class RadioenumGroup;
} // namespace Ui
namespace Giveaway {
class GiveawayTypeRow final : public Ui::RippleButton {
public:
enum class Type {
Random,
SpecificUsers,
AllMembers,
OnlyNewMembers,
};
GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> subtitle);
void addRadio(std::shared_ptr<Ui::RadioenumGroup<Type>> 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

View file

@ -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<Ui::VerticalLayout*> container) {
container->add(object_ptr<Ui::FixedHeightWidget>(
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<Ui::GenericBox*> box,
const std::vector<QString> &selected,
Fn<void(std::vector<QString>)> doneCallback,
Fn<bool(int)> checkErrorCallback) {
struct State final {
std::vector<QString> resultList;
};
const auto state = box->lifetime().make_state<State>();
const auto multiSelect = box->setPinnedToTopContent(
object_ptr<Ui::MultiSelect>(
box,
st::giveawayGiftCodeCountrySelect,
tr::lng_participant_filter()));
AddSkip(box->verticalLayout());
const auto &buttonSt = st::giveawayGiftCodeCountryButton;
struct Entry final {
Ui::SlideWrap<Ui::SettingsButton> *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<Entry>();
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<SettingsButton>(
box->verticalLayout(),
rpl::single(flag + ' ' + country.name),
buttonSt);
const auto radio = Ui::CreateChild<Ui::RpWidget>(button.data());
const auto radioView = std::make_shared<Ui::RadioView>(
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<Ui::SlideWrap<Ui::SettingsButton>>(
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<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)));
{
noResults->toggle(false, anim::type::instant);
const auto container = noResults->entity();
AddSkip(container);
AddSkip(container);
container->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
container,
object_ptr<Ui::FlatLabel>(
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

View file

@ -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<Ui::GenericBox*> box,
const std::vector<QString> &selected,
Fn<void(std::vector<QString>)> doneCallback,
Fn<bool(int)> checkErrorCallback);
} // namespace Ui

View file

@ -7,10 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "info/boosts/info_boosts_inner_widget.h" #include "info/boosts/info_boosts_inner_widget.h"
#include "api/api_premium.h"
#include "api/api_statistics.h" #include "api/api_statistics.h"
#include "boxes/gift_premium_box.h"
#include "boxes/peers/edit_peer_invite_link.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/boosts/info_boosts_widget.h"
#include "info/info_controller.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 "info/statistics/info_statistics_list_controllers.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "settings/settings_common.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_buttons.h"
#include "ui/controls/invite_link_label.h" #include "ui/controls/invite_link_label.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_statistics.h" #include "styles/style_statistics.h"
@ -56,11 +67,12 @@ void FillOverview(
object_ptr<Ui::RpWidget>(content), object_ptr<Ui::RpWidget>(content),
st::statisticsLayerMargins); st::statisticsLayerMargins);
const auto addPrimary = [&](float64 v) { const auto addPrimary = [&](float64 v, bool approximately = false) {
return Ui::CreateChild<Ui::FlatLabel>( return Ui::CreateChild<Ui::FlatLabel>(
container, container,
(v >= 0) (v >= 0)
? Lang::FormatCountToShort(v).string ? (approximately && v ? QChar(0x2248) : QChar())
+ Lang::FormatCountToShort(v).string
: QString(), : QString(),
st::statisticsOverviewValue); st::statisticsOverviewValue);
}; };
@ -98,10 +110,11 @@ void FillOverview(
const auto topLeftLabel = addPrimary(stats.level); 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 bottomLeftLabel = addPrimary(stats.boostCount);
const auto bottomRightLabel = addPrimary( const auto bottomRightLabel = addPrimary(std::max(
stats.nextLevelBoostCount - stats.boostCount); stats.nextLevelBoostCount - stats.boostCount,
0));
addSub( addSub(
topLeftLabel, topLeftLabel,
@ -173,6 +186,36 @@ void FillShareLink(
::Settings::AddSkip(content, st::boostsLinkFieldPadding.bottom()); ::Settings::AddSkip(content, st::boostsLinkFieldPadding.bottom());
} }
void FillGetBoostsButton(
not_null<Ui::VerticalLayout*> content,
not_null<Controller*> controller,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> 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<Info::Profile::FloatingIcon>(
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 } // namespace
InnerWidget::InnerWidget( InnerWidget::InnerWidget(
@ -188,12 +231,18 @@ InnerWidget::InnerWidget(
void InnerWidget::load() { void InnerWidget::load() {
const auto api = lifetime().make_state<Api::Boosts>(_peer); const auto api = lifetime().make_state<Api::Boosts>(_peer);
Info::Statistics::FillLoading(
this,
_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),
_showFinished.events());
_showFinished.events( _showFinished.events(
) | rpl::take(1) | rpl::start_with_next([=] { ) | rpl::take(1) | rpl::start_with_next([=] {
api->request( api->request(
) | rpl::start_with_error_done([](const QString &error) { ) | rpl::start_with_error_done([](const QString &error) {
}, [=] { }, [=] {
_state = api->boostStatus(); _state = api->boostStatus();
_loaded.fire(true);
fill(); fill();
}, lifetime()); }, lifetime());
}, lifetime()); }, lifetime());
@ -232,30 +281,89 @@ void InnerWidget::fill() {
::Settings::AddDivider(inner); ::Settings::AddDivider(inner);
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
if (status.firstSlice.total > 0) { const auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0);
::Settings::AddSkip(inner); const auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0);
using PeerPtr = not_null<PeerData*>; if (hasBoosts || hasGifts) {
const auto header = inner->add( auto boostClicked = [=](const Data::Boost &boost) {
object_ptr<Statistic::Header>(inner), if (!boost.giftCodeLink.slug.isEmpty()) {
st::statisticsLayerMargins ResolveGiftCode(_controller, boost.giftCodeLink.slug);
+ st::boostsChartHeaderPadding); } else if (boost.userId) {
header->resizeToWidth(header->width()); const auto user = _peer->owner().user(boost.userId);
header->setTitle(tr::lng_boosts_list_title( 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, tr::now,
lt_count, lt_count,
status.firstSlice.total)); status.firstSliceBoosts.multipliedTotal);
header->setSubTitle({}); 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<Statistic::Header>(inner),
st::statisticsLayerMargins
+ st::boostsChartHeaderPadding);
header->resizeToWidth(header->width());
header->setTitle(hasBoosts ? boostsTabText : giftsTabText);
header->setSubTitle({});
}
const auto slider = inner->add(
object_ptr<Ui::SlideWrap<Ui::SettingsSlider>>(
inner,
object_ptr<Ui::SettingsSlider>(
inner,
st::defaultTabsSlider)));
slider->toggle(!hasOneTab, anim::type::instant);
slider->entity()->addSection(boostsTabText);
slider->entity()->addSection(giftsTabText);
const auto boostsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto giftsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(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( Statistics::AddBoostsList(
status.firstSlice, status.firstSliceBoosts,
inner, boostsWrap->entity(),
[=](PeerPtr p) { _controller->showPeerInfo(p); }, boostClicked,
_peer, _peer,
tr::lng_boosts_title()); tr::lng_boosts_title());
Statistics::AddBoostsList(
status.firstSliceGifts,
giftsWrap->entity(),
std::move(boostClicked),
_peer,
tr::lng_boosts_title());
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
::Settings::AddDividerText(
inner,
tr::lng_boosts_list_subtext());
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
::Settings::AddDividerText(inner, tr::lng_boosts_list_subtext());
} }
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
@ -265,6 +373,8 @@ void InnerWidget::fill() {
::Settings::AddSkip(inner); ::Settings::AddSkip(inner);
::Settings::AddDividerText(inner, tr::lng_boosts_link_subtext()); ::Settings::AddDividerText(inner, tr::lng_boosts_link_subtext());
FillGetBoostsButton(inner, _controller, _show, _peer);
resizeToWidth(width()); resizeToWidth(width());
crl::on_main([=]{ fakeShowed->fire({}); }); crl::on_main([=]{ fakeShowed->fire({}); });
} }

View file

@ -118,4 +118,3 @@ std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
} }
} // namespace Info::Boosts } // namespace Info::Boosts

View file

@ -247,50 +247,6 @@ void FillStatistic(
} }
} }
void FillLoading(
not_null<Ui::VerticalLayout*> container,
rpl::producer<bool> toggleOn,
rpl::producer<> showFinished) {
const auto emptyWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(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<Ui::CenterWrap<>>(
content,
object_ptr<Ui::FlatLabel>(
content,
tr::lng_stats_loading(),
st::changePhoneTitle)),
st::changePhoneTitlePadding + st::boxRowPadding);
content->add(
object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::FlatLabel>(
content,
tr::lng_stats_loading_subtext(),
st::statisticsLoadingSubtext)),
st::changePhoneDescriptionPadding + st::boxRowPadding);
::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top());
}
void AddHeader( void AddHeader(
not_null<Ui::VerticalLayout*> content, not_null<Ui::VerticalLayout*> content,
tr::phrase<> text, tr::phrase<> text,
@ -507,6 +463,50 @@ void FillOverview(
} // namespace } // namespace
void FillLoading(
not_null<Ui::VerticalLayout*> container,
rpl::producer<bool> toggleOn,
rpl::producer<> showFinished) {
const auto emptyWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(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<Ui::CenterWrap<>>(
content,
object_ptr<Ui::FlatLabel>(
content,
tr::lng_stats_loading(),
st::changePhoneTitle)),
st::changePhoneTitlePadding + st::boxRowPadding);
content->add(
object_ptr<Ui::CenterWrap<>>(
content,
object_ptr<Ui::FlatLabel>(
content,
tr::lng_stats_loading_subtext(),
st::statisticsLoadingSubtext)),
st::changePhoneDescriptionPadding + st::boxRowPadding);
::Settings::AddSkip(content, st::settingsBlockedListIconPadding.top());
}
InnerWidget::InnerWidget( InnerWidget::InnerWidget(
QWidget *parent, QWidget *parent,
not_null<Controller*> controller, not_null<Controller*> controller,

View file

@ -21,6 +21,11 @@ namespace Info::Statistics {
class Memento; class Memento;
class MessagePreview; class MessagePreview;
void FillLoading(
not_null<Ui::VerticalLayout*> container,
rpl::producer<bool> toggleOn,
rpl::producer<> showFinished);
class InnerWidget final : public Ui::VerticalLayout { class InnerWidget final : public Ui::VerticalLayout {
public: public:
struct ShowRequest final { struct ShowRequest final {

View file

@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h" #include "main/main_session.h"
#include "settings/settings_common.h" #include "settings/settings_common.h"
#include "ui/effects/toggle_arrow.h" #include "ui/effects/toggle_arrow.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
@ -30,6 +31,62 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Info::Statistics { namespace Info::Statistics {
namespace { namespace {
using BoostCallback = Fn<void(const Data::Boost &)>;
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<Ui::RpWidget*> parent) { void AddArrow(not_null<Ui::RpWidget*> parent) {
const auto arrow = Ui::CreateChild<Ui::RpWidget>(parent.get()); const auto arrow = Ui::CreateChild<Ui::RpWidget>(parent.get());
arrow->paintRequest( arrow->paintRequest(
@ -100,7 +157,7 @@ struct MembersDescriptor final {
struct BoostsDescriptor final { struct BoostsDescriptor final {
Data::BoostsListSlice firstSlice; Data::BoostsListSlice firstSlice;
Fn<void(not_null<PeerData*>)> showPeerInfo; BoostCallback boostClickedCallback;
not_null<PeerData*> peer; not_null<PeerData*> peer;
}; };
@ -321,6 +378,192 @@ void PublicForwardsController::appendRow(
return; return;
} }
class BoostRow final : public PeerListRow {
public:
BoostRow(not_null<PeerData*> 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<void()> 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<PeerData*> 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<void()> 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 { class BoostsController final : public PeerListController {
public: public:
explicit BoostsController(BoostsDescriptor d); explicit BoostsController(BoostsDescriptor d);
@ -331,28 +574,30 @@ public:
void loadMoreRows() override; void loadMoreRows() override;
[[nodiscard]] bool skipRequest() const; [[nodiscard]] bool skipRequest() const;
void setLimit(int limit); void requestNext();
[[nodiscard]] rpl::producer<int> totalBoostsValue() const;
private: private:
void applySlice(const Data::BoostsListSlice &slice); void applySlice(const Data::BoostsListSlice &slice);
const not_null<Main::Session*> _session; const not_null<Main::Session*> _session;
Fn<void(not_null<PeerData*>)> _showPeerInfo; BoostCallback _boostClickedCallback;
Api::Boosts _api; Api::Boosts _api;
Data::BoostsListSlice _firstSlice; Data::BoostsListSlice _firstSlice;
Data::BoostsListSlice::OffsetToken _apiToken; Data::BoostsListSlice::OffsetToken _apiToken;
int _limit = 0;
bool _allLoaded = false; bool _allLoaded = false;
bool _requesting = false; bool _requesting = false;
rpl::variable<int> _totalBoosts;
}; };
BoostsController::BoostsController(BoostsDescriptor d) BoostsController::BoostsController(BoostsDescriptor d)
: _session(&d.peer->session()) : _session(&d.peer->session())
, _showPeerInfo(std::move(d.showPeerInfo)) , _boostClickedCallback(std::move(d.boostClickedCallback))
, _api(d.peer) , _api(d.peer)
, _firstSlice(std::move(d.firstSlice)) { , _firstSlice(std::move(d.firstSlice)) {
PeerListController::setStyleOverrides(&st::boostsListBox); PeerListController::setStyleOverrides(&st::boostsListBox);
@ -366,8 +611,7 @@ bool BoostsController::skipRequest() const {
return _requesting || _allLoaded; return _requesting || _allLoaded;
} }
void BoostsController::setLimit(int limit) { void BoostsController::requestNext() {
_limit = limit;
_requesting = true; _requesting = true;
_api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) { _api.requestBoosts(_apiToken, [=](const Data::BoostsListSlice &slice) {
_requesting = false; _requesting = false;
@ -387,26 +631,32 @@ void BoostsController::applySlice(const Data::BoostsListSlice &slice) {
_allLoaded = slice.allLoaded; _allLoaded = slice.allLoaded;
_apiToken = slice.token; _apiToken = slice.token;
const auto formatter = u"MMM d, yyyy"_q; auto sumFromSlice = 0;
for (const auto &item : slice.list) { for (const auto &item : slice.list) {
const auto user = session().data().user(item.userId); sumFromSlice += item.multiplier ? item.multiplier : 1;
if (delegate()->peerListFindRow(user->id.value)) { auto row = [&] {
continue; if (item.userId && !item.isUnclaimed) {
} const auto user = session().data().user(item.userId);
auto row = std::make_unique<PeerListRow>(user); return std::make_unique<BoostRow>(user, item);
row->setCustomStatus(tr::lng_boosts_list_status( } else {
tr::now, return std::make_unique<BoostRow>(item);
lt_date, }
QLocale().toString(item.expirationDate, formatter))); }();
delegate()->peerListAppendRow(std::move(row)); delegate()->peerListAppendRow(std::move(row));
} }
delegate()->peerListRefreshRows(); delegate()->peerListRefreshRows();
_totalBoosts = _totalBoosts.current() + sumFromSlice;
} }
void BoostsController::rowClicked(not_null<PeerListRow*> row) { void BoostsController::rowClicked(not_null<PeerListRow*> row) {
crl::on_main([=, peer = row->peer()] { if (_boostClickedCallback) {
_showPeerInfo(peer); _boostClickedCallback(
}); static_cast<const BoostRow*>(row.get())->boost());
}
}
rpl::producer<int> BoostsController::totalBoostsValue() const {
return _totalBoosts.value();
} }
} // namespace } // namespace
@ -512,53 +762,52 @@ void AddMembersList(
void AddBoostsList( void AddBoostsList(
const Data::BoostsListSlice &firstSlice, const Data::BoostsListSlice &firstSlice,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
Fn<void(not_null<PeerData*>)> showPeerInfo, BoostCallback boostClickedCallback,
not_null<PeerData*> peer, not_null<PeerData*> peer,
rpl::producer<QString> title) { rpl::producer<QString> title) {
const auto max = firstSlice.total; const auto max = firstSlice.multipliedTotal;
struct State final { struct State final {
State(BoostsDescriptor d) : controller(std::move(d)) { State(BoostsDescriptor d) : controller(std::move(d)) {
} }
PeerListContentDelegateSimple delegate; PeerListContentDelegateSimple delegate;
BoostsController controller; 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<State>(std::move(d)); const auto state = container->lifetime().make_state<State>(std::move(d));
state->delegate.setContent(container->add( state->delegate.setContent(container->add(
object_ptr<PeerListContent>(container, &state->controller))); object_ptr<PeerListContent>(container, &state->controller)));
state->controller.setDelegate(&state->delegate); state->controller.setDelegate(&state->delegate);
if (max <= state->limit) {
return;
}
const auto wrap = container->add( const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>( object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container, container,
object_ptr<Ui::SettingsButton>( object_ptr<Ui::SettingsButton>(
container, 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)), st::statisticsShowMoreButton)),
{ 0, -st::settingsButton.padding.top(), 0, 0 }); { 0, -st::settingsButton.padding.top(), 0, 0 });
const auto button = wrap->entity(); const auto button = wrap->entity();
AddArrow(button); AddArrow(button);
const auto showMore = [=] { const auto showMore = [=] {
if (state->controller.skipRequest()) { if (!state->controller.skipRequest()) {
return; 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); button->setClickedCallback(showMore);
if (state->limit == max) {
wrap->toggle(false, anim::type::instant);
}
} }
} // namespace Info::Statistics } // namespace Info::Statistics

View file

@ -14,6 +14,7 @@ class VerticalLayout;
} // namespace Ui } // namespace Ui
namespace Data { namespace Data {
struct Boost;
struct BoostsListSlice; struct BoostsListSlice;
struct PublicForwardsSlice; struct PublicForwardsSlice;
struct SupergroupStatistics; struct SupergroupStatistics;
@ -38,7 +39,7 @@ void AddMembersList(
void AddBoostsList( void AddBoostsList(
const Data::BoostsListSlice &firstSlice, const Data::BoostsListSlice &firstSlice,
not_null<Ui::VerticalLayout*> container, not_null<Ui::VerticalLayout*> container,
Fn<void(not_null<PeerData*>)> showPeerInfo, Fn<void(const Data::Boost &)> boostClickedCallback,
not_null<PeerData*> peer, not_null<PeerData*> peer,
rpl::producer<QString> title); rpl::producer<QString> title);

View file

@ -233,7 +233,7 @@ void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
tr::lng_mini_apps_disclaimer_link(tr::now), tr::lng_mini_apps_disclaimer_link(tr::now),
tr::lng_mini_apps_tos_url(tr::now))), tr::lng_mini_apps_tos_url(tr::now))),
Ui::Text::WithEntities), Ui::Text::WithEntities),
st::defaultBoxCheckbox, st::urlAuthCheckbox,
std::move(checkView)), std::move(checkView)),
{ {
st::boxRowPadding.left(), st::boxRowPadding.left(),
@ -241,7 +241,7 @@ void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
st::boxRowPadding.right(), st::boxRowPadding.right(),
0, 0,
}); });
row->setAllowTextLines(5); row->setAllowTextLines();
row->setClickHandlerFilter([=]( row->setClickHandlerFilter([=](
const ClickHandlerPtr &link, const ClickHandlerPtr &link,
Qt::MouseButton button) { Qt::MouseButton button) {

View file

@ -500,7 +500,7 @@ bool ReplyArea::confirmSendingFiles(
auto confirmed = [=](auto &&...args) { auto confirmed = [=](auto &&...args) {
sendingFilesConfirmed(std::forward<decltype(args)>(args)...); sendingFilesConfirmed(std::forward<decltype(args)>(args)...);
}; };
auto box = Box<SendFilesBox>(SendFilesBoxDescriptor{ show->show(Box<SendFilesBox>(SendFilesBoxDescriptor{
.show = show, .show = show,
.list = std::move(list), .list = std::move(list),
.caption = _controls->getTextWithAppliedMarkdown(), .caption = _controls->getTextWithAppliedMarkdown(),
@ -511,10 +511,7 @@ bool ReplyArea::confirmSendingFiles(
.stOverride = &st::storiesComposeControls, .stOverride = &st::storiesComposeControls,
.confirmed = crl::guard(this, confirmed), .confirmed = crl::guard(this, confirmed),
.cancelled = _controls->restoreTextCallback(insertTextOnCancel), .cancelled = _controls->restoreTextCallback(insertTextOnCancel),
}); }));
if (const auto shown = show->show(std::move(box))) {
shown->setCloseByOutsideClick(false);
}
return true; return true;
} }

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QVector> #include <QtCore/QVector>
#include <QtCore/QString> #include <QtCore/QString>
#include <QtCore/QByteArray> #include <QtCore/QByteArray>
#include <range/v3/range/conversion.hpp>
#include <gsl/gsl> #include <gsl/gsl>
using mtpPrime = int32; using mtpPrime = int32;
@ -243,6 +244,18 @@ inline MTPvector<T> MTP_vector() {
return tl::make_vector<T>(); return tl::make_vector<T>();
} }
// ranges::to<QVector> doesn't work with Qt 6 in Clang,
// because QVector is a type alias for QList there.
template <typename Rng>
inline auto MTP_vector_from_range(Rng &&range) {
using T = std::remove_cvref_t<decltype(*ranges::begin(range))>;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0 ,0)
return MTP_vector<T>(std::forward<Rng>(range) | ranges::to<QList>());
#else // QT_VERSION >= 6.0
return MTP_vector<T>(std::forward<Rng>(range) | ranges::to<QVector>());
#endif // QT_VERSION < 6.0
}
namespace tl { namespace tl {
template <typename Accumulator> template <typename Accumulator>

View file

@ -65,7 +65,7 @@ QByteArray DnsUserAgent() {
static const auto kResult = QByteArray( static const auto kResult = QByteArray(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) " "AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/116.0.5845.96 Safari/537.36"); "Chrome/118.0.5993.117 Safari/537.36");
return kResult; return kResult;
} }

View file

@ -295,11 +295,11 @@ MTPInputInvoice Form::inputInvoice() const {
return MTP_inputInvoicePremiumGiftCode( return MTP_inputInvoicePremiumGiftCode(
MTP_inputStorePaymentPremiumGiftCode( MTP_inputStorePaymentPremiumGiftCode(
MTP_flags(users->boostPeer ? Flag::f_boost_peer : Flag()), MTP_flags(users->boostPeer ? Flag::f_boost_peer : Flag()),
MTP_vector<MTPInputUser>(ranges::views::all( MTP_vector_from_range(ranges::views::all(
users->users users->users
) | ranges::views::transform([](not_null<UserData*> user) { ) | ranges::views::transform([](not_null<UserData*> user) {
return MTPInputUser(user->inputUser); return MTPInputUser(user->inputUser);
}) | ranges::to<QVector>), })),
users->boostPeer ? users->boostPeer->input : MTPInputPeer(), users->boostPeer ? users->boostPeer->input : MTPInputPeer(),
MTP_string(giftCode.currency), MTP_string(giftCode.currency),
MTP_long(giftCode.amount)), MTP_long(giftCode.amount)),
@ -321,16 +321,16 @@ MTPInputInvoice Form::inputInvoice() const {
? Flag() ? Flag()
: Flag::f_countries_iso2)), : Flag::f_countries_iso2)),
giveaway.boostPeer->input, giveaway.boostPeer->input,
MTP_vector<MTPInputPeer>(ranges::views::all( MTP_vector_from_range(ranges::views::all(
giveaway.additionalChannels giveaway.additionalChannels
) | ranges::views::transform([](not_null<ChannelData*> c) { ) | ranges::views::transform([](not_null<ChannelData*> c) {
return MTPInputPeer(c->input); return MTPInputPeer(c->input);
}) | ranges::to<QVector>()), })),
MTP_vector<MTPstring>(ranges::views::all( MTP_vector_from_range(ranges::views::all(
giveaway.countries giveaway.countries
) | ranges::views::transform([](QString value) { ) | ranges::views::transform([](QString value) {
return MTP_string(value); return MTP_string(value);
}) | ranges::to<QVector>()), })),
MTP_long(giftCode.randomId), MTP_long(giftCode.randomId),
MTP_int(giveaway.untilDate), MTP_int(giveaway.untilDate),
MTP_string(giftCode.currency), MTP_string(giftCode.currency),

View file

@ -58,7 +58,7 @@ private:
const QString _mutePanelTrayIconName; const QString _mutePanelTrayIconName;
const QString _attentionPanelTrayIconName; const QString _attentionPanelTrayIconName;
const int _iconSizes[5]; const int _iconSizes[7];
bool _muted = true; bool _muted = true;
int32 _count = 0; int32 _count = 0;
@ -73,7 +73,7 @@ IconGraphic::IconGraphic()
: _panelTrayIconName("telegram-panel") : _panelTrayIconName("telegram-panel")
, _mutePanelTrayIconName("telegram-mute-panel") , _mutePanelTrayIconName("telegram-mute-panel")
, _attentionPanelTrayIconName("telegram-attention-panel") , _attentionPanelTrayIconName("telegram-attention-panel")
, _iconSizes{ 16, 22, 24, 32, 48 } { , _iconSizes{ 16, 22, 32, 48, 64, 128, 256 } {
} }
IconGraphic::~IconGraphic() = default; IconGraphic::~IconGraphic() = default;
@ -214,46 +214,13 @@ QIcon IconGraphic::trayIcon(
} }
} }
auto iconImage = currentImageBack; result.addPixmap(Ui::PixmapFromImage(counter > 0
? Window::WithSmallCounter(std::move(currentImageBack), {
if (counter > 0) { .size = iconSize,
const auto &bg = muted .count = counter,
? st::trayCounterBgMute .bg = muted ? st::trayCounterBgMute : st::trayCounterBg,
: st::trayCounterBg; .fg = st::trayCounterFg,
const auto &fg = st::trayCounterFg; }) : std::move(currentImageBack)));
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)));
} }
updateIconRegenerationNeeded( updateIconRegenerationNeeded(

Some files were not shown because too many files have changed in this diff Show more