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 (WIN32)
set(qt_version 5.15.10)
set(qt_version 5.15.11)
elseif (APPLE)
set(qt_version 6.2.5)
set(qt_version 6.2.6)
endif()
endif()
include(cmake/external/qt/package.cmake)

View file

@ -837,6 +837,8 @@ PRIVATE
history/view/history_view_quick_action.h
history/view/history_view_replies_section.cpp
history/view/history_view_replies_section.h
history/view/history_view_reply.cpp
history/view/history_view_reply.h
history/view/history_view_requests_bar.cpp
history/view/history_view_requests_bar.h
history/view/history_view_schedule_box.cpp
@ -891,6 +893,10 @@ PRIVATE
history/history_view_highlight_manager.h
history/history_widget.cpp
history/history_widget.h
info/boosts/giveaway/giveaway_list_controllers.cpp
info/boosts/giveaway/giveaway_list_controllers.h
info/boosts/create_giveaway_box.cpp
info/boosts/create_giveaway_box.h
info/boosts/info_boosts_inner_widget.cpp
info/boosts/info_boosts_inner_widget.h
info/boosts/info_boosts_widget.cpp
@ -1609,7 +1615,39 @@ elseif (APPLE)
endif()
set(icons_path ${CMAKE_CURRENT_SOURCE_DIR}/Telegram/Images.xcassets)
target_add_resource(Telegram ${icons_path})
if (CMAKE_GENERATOR STREQUAL Xcode)
target_add_resource(Telegram ${icons_path})
else()
set(icon_path ${icons_path}/Icon.iconset)
find_program(ICONUTIL iconutil)
find_program(PNG2ICNS png2icns)
if (ICONUTIL)
add_custom_command(
OUTPUT Icon.icns
COMMAND ${ICONUTIL}
ARGS
--convert icns
--output Icon.icns
${icon_path}
)
elseif (PNG2ICNS)
add_custom_command(
OUTPUT Icon.icns
COMMAND ${PNG2ICNS}
ARGS
Icon.icns
${icon_path}/icon_16x16.png
${icon_path}/icon_32x32.png
${icon_path}/icon_128x128.png
${icon_path}/icon_256x256.png
${icon_path}/icon_512x512.png
)
endif()
if (ICONUTIL OR PNG2ICNS)
set_source_files_properties(Icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
target_add_resource(Telegram Icon.icns)
endif()
endif()
set(lang_packs
en
@ -1636,24 +1674,17 @@ elseif (APPLE)
COMMAND cp ${CMAKE_BINARY_DIR}/lib_ui.rcc $<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
PRE_LINK
COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Frameworks
COMMAND cp $<TARGET_FILE:Updater> $<TARGET_FILE_DIR:Telegram>/../Frameworks/
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/
)
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()
else()
target_link_libraries(Telegram
@ -1702,7 +1733,11 @@ if (build_macstore)
COMMAND rm -rf $<TARGET_FILE_DIR:Telegram>/../Frameworks/Breakpad.framework/Resources/Inspector
)
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")
if (LINUX AND DESKTOP_APP_USE_PACKAGED)
set(output_name "ayugram-desktop")
@ -1711,6 +1746,12 @@ else()
endif()
endif()
if (CMAKE_GENERATOR STREQUAL Xcode)
set(bundle_identifier_plist "$(PRODUCT_BUNDLE_IDENTIFIER)")
else()
set(bundle_identifier_plist ${bundle_identifier})
endif()
set_target_properties(Telegram PROPERTIES
OUTPUT_NAME ${output_name}
MACOSX_BUNDLE_GUI_IDENTIFIER ${bundle_identifier}
@ -1784,7 +1825,7 @@ endif()
target_prepare_qrc(Telegram)
if ((NOT DESKTOP_APP_DISABLE_AUTOUPDATE OR APPLE) AND NOT build_macstore AND NOT build_winstore)
if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_winstore)
add_executable(Updater WIN32)
init_non_host_target(Updater)
@ -1822,6 +1863,12 @@ if ((NOT DESKTOP_APP_DISABLE_AUTOUPDATE OR APPLE) AND NOT build_macstore AND NOT
else()
target_link_options(Updater PRIVATE -municode)
endif()
elseif (APPLE)
add_custom_command(TARGET Updater
PRE_LINK
COMMAND mkdir -p $<TARGET_FILE_DIR:Telegram>/../Frameworks
COMMAND cp $<TARGET_FILE:Updater> $<TARGET_FILE_DIR:Telegram>/../Frameworks/
)
endif()
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_chosen#one" = "{count} recipient >";
"lng_giveaway_award_chosen#other" = "{count} recipients >";
"lng_giveaway_quantity_title" = "Quantity of prizes / boosts";
"lng_giveaway_quantity#one" = "{count} Subscription / Boost";
"lng_giveaway_quantity#other" = "{count} Subscriptions / Boosts";
"lng_giveaway_quantity_title" = "Quantity of prizes";
"lng_giveaway_quantity#one" = "{count} boost";
"lng_giveaway_quantity#other" = "{count} boosts";
"lng_giveaway_quantity_about" = "Choose how many Premium subscriptions to give away and boosts to receive.";
"lng_giveaway_channels_title" = "Channels included in the giveaway";
"lng_giveaway_channels_this#one" = "this channel will receive {count} boost";
@ -2089,8 +2089,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_channels_about" = "Choose the channels the users need to join to take part in the giveaway.";
"lng_giveaway_users_title" = "Users eligible for the giveaway";
"lng_giveaway_users_all" = "All subscribers";
"lng_giveaway_users_from_all_countries" = "from all countries";
"lng_giveaway_users_from_one_country" = "from {country}";
"lng_giveaway_users_from_countries#one" = "from {count} country";
"lng_giveaway_users_from_countries#other" = "from {count} countries";
"lng_giveaway_users_new" = "Only new subscribers";
"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to the newly joined subscribers.";
"lng_giveaway_users_about" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started or to users from specific countries.";
"lng_giveaway_start" = "Start Giveaway";
"lng_giveaway_award" = "Gift Premium";
"lng_giveaway_date_title" = "Date when giveaway ends";
@ -2100,14 +2104,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_duration_title#one" = "Duration of Premium subscription";
"lng_giveaway_duration_title#other" = "Duration of Premium subscriptions";
"lng_giveaway_duration_price" = "{price} x {amount}";
"lng_giveaway_duration_about" = "You can review the list of features and terms of use for Telegram Premium {link}.";
"lng_giveaway_duration_about_link" = "here";
"lng_giveaway_date_select" = "Select Date and Time";
"lng_giveaway_date_confirm" = "Confirm";
"lng_giveaway_channels_select#one" = "Select up to {count} channel";
"lng_giveaway_channels_select#other" = "Select up to {count} channels";
"lng_giveaway_recipients_save" = "Save Recipients";
"lng_giveaway_recipients_deselect" = "Deselect All";
"lng_giveaway_maximum_countries_error#one" = "You can select maximum {count} country.";
"lng_giveaway_maximum_countries_error#other" = "You can select maximum {count} countries.";
"lng_giveaway_maximum_channels_error#one" = "You can select maximum {count} channel.";
"lng_giveaway_maximum_channels_error#other" = "You can select maximum {count} channels.";
"lng_giveaway_maximum_users_error#one" = "You can select maximum {count} user.";
"lng_giveaway_maximum_users_error#other" = "You can select maximum {count} users.";
"lng_giveaway_channels_confirm_title" = "Channel is Private";
"lng_giveaway_channels_confirm_about" = "Are you sure you want to add a private channel? Users won't be able to join it without an invite link.";
"lng_giveaway_created_title" = "Giveaway created";
"lng_giveaway_created_body" = "Check your channels' {link} to see how this giveaway boosted your channel.";
"lng_giveaway_awarded_title" = "Premium subscriptions gifted";
"lng_giveaway_awarded_body" = "Check your channels' {link} to see how gifts boosted your channel.";
"lng_giveaway_created_link" = "Statistics";
"lng_prize_title" = "Congratulations!";
"lng_prize_about" = "You won a prize in a giveaway organized by {channel}.";
@ -2143,10 +2159,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_prizes_end_when_finish" = "On {date}, Telegram automatically selected {winners}.";
"lng_prizes_end_activated#one" = "**{count}** of the winners already used their gift link.";
"lng_prizes_end_activated#other" = "**{count}** of the winners already used their gift links.";
"lng_prizes_winners_all_of_one#one" = "{count} random subscribers of {channel}.";
"lng_prizes_winners_all_of_one#other" = "{count} random subscribers of {channel}.";
"lng_prizes_winners_all_of_many#one" = "{count} random subscribers of {channel} and other listed channels.";
"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed channels.";
"lng_prizes_winners_all_of_one#one" = "{count} random subscribers of {channel}";
"lng_prizes_winners_all_of_one#other" = "{count} random subscribers of {channel}";
"lng_prizes_winners_all_of_many#one" = "{count} random subscribers of {channel} and other listed channels";
"lng_prizes_winners_all_of_many#other" = "{count} random subscribers of {channel} and other listed channels";
"lng_prizes_winners_new_of_one#one" = "{count} random user that joined {channel} after {start_date}";
"lng_prizes_winners_new_of_one#other" = "{count} random users that joined {channel} after {start_date}";
"lng_prizes_winners_new_of_many#one" = "{count} random user that joined {channel} and other listed channels after {start_date}";
@ -4312,10 +4328,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_boosts_list_title#one" = "{count} booster";
"lng_boosts_list_title#other" = "{count} boosters";
"lng_boosts_list_subtext" = "Your channel is currently boosted by these users.";
"lng_boosts_show_more" = "Show More Boosts";
"lng_boosts_show_more_boosts#one" = "Show {count} More Boosts";
"lng_boosts_show_more_boosts#other" = "Show {count} More Boosts";
"lng_boosts_show_more_gifts#one" = "Show {count} More Boosts";
"lng_boosts_show_more_gifts#other" = "Show {count} More Boosts";
"lng_boosts_list_status" = "boost expires on {date}";
"lng_boosts_link_title" = "Link for boosting";
"lng_boosts_link_subtext" = "Share this link with your subscribers to get more boosts.";
"lng_boosts_get_boosts" = "Get Boosts via Gifts";
"lng_boosts_get_boosts_subtext" = "Get more boosts for your channel by gifting Telegram Premium to your subscribers.";
"lng_boosts_list_unclaimed" = "Unclaimed";
"lng_boosts_list_pending" = "To be distributed";
"lng_boosts_list_pending_about" = "The recipient will be selected when the giveaway ends.";
"lng_boosts_list_tab_gifts#one" = "{count} Gifts";
"lng_boosts_list_tab_gifts#other" = "{count} Gifts";
// Wnd specific

View file

@ -28,6 +28,7 @@
<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/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/yellow.svg">../../art/topic_icons/yellow.svg</file>
<file alias="topic_icons/violet.svg">../../art/topic_icons/violet.svg</file>

View file

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

View file

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

View file

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

View file

@ -9,12 +9,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_premium_option.h"
#include "api/api_text_entities.h"
#include "main/main_session.h"
#include "data/data_peer_values.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_peer.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_peer_values.h"
#include "data/data_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "payments/payments_form.h"
#include "ui/text/format_values.h"
namespace Api {
namespace {
@ -31,6 +36,24 @@ namespace {
};
}
[[nodiscard]] Data::SubscriptionOptions GiftCodesFromTL(
const QVector<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
Premium::Premium(not_null<ApiWrap*> api)
@ -311,4 +334,139 @@ const Data::SubscriptionOptions &Premium::subscriptionOptions() const {
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

View file

@ -16,6 +16,10 @@ namespace Main {
class Session;
} // namespace Main
namespace Payments {
struct InvoicePremiumGiftCode;
} // namespace Payments
namespace Api {
struct GiftCode {
@ -141,4 +145,51 @@ private:
};
class PremiumGiftCodeOptions final {
public:
PremiumGiftCodeOptions(not_null<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

View file

@ -36,7 +36,10 @@ template<typename Option>
result.reserve(tlOptions.size());
for (const auto &tlOption : tlOptions) {
const auto &option = tlOption.data();
const auto botUrl = qs(option.vbot_url());
auto botUrl = QString();
if constexpr (!std::is_same_v<Option, MTPPremiumGiftCodeOption>) {
botUrl = qs(option.vbot_url());
}
const auto months = option.vmonths().v;
const auto amount = option.vamount().v;
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());
requestBoosts({}, [=](Data::BoostsListSlice &&slice) {
_boostStatus.firstSlice = std::move(slice);
using namespace Data;
requestBoosts({ .gifts = false }, [=](BoostsListSlice &&slice) {
_boostStatus.firstSliceBoosts = std::move(slice);
requestBoosts({ .gifts = true }, [=](BoostsListSlice &&s) {
_boostStatus.firstSliceGifts = std::move(s);
consumer.put_done();
});
consumer.put_done();
});
}).fail([=](const MTP::Error &error) {
@ -553,8 +558,11 @@ void Boosts::requestBoosts(
}
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit);
const auto gifts = token.gifts;
_requestId = _api.request(MTPpremium_GetBoostsList(
MTP_flags(0),
gifts
? MTP_flags(MTPpremium_GetBoostsList::Flag::f_gifts)
: MTP_flags(0),
_peer->input,
MTP_string(token.next),
token.next.isEmpty() ? kTlFirstSlice : kTlLimit
@ -567,19 +575,41 @@ void Boosts::requestBoosts(
auto list = std::vector<Data::Boost>();
list.reserve(data.vboosts().v.size());
for (const auto &boost : data.vboosts().v) {
const auto &data = boost.data();
const auto path = data.vused_gift_slug()
? (u"giftcode/"_q + qs(data.vused_gift_slug()->v))
: QString();
auto giftCodeLink = !path.isEmpty()
? Data::GiftCodeLink{
_peer->session().createInternalLink(path),
_peer->session().createInternalLinkFull(path),
qs(data.vused_gift_slug()->v),
}
: Data::GiftCodeLink();
list.push_back({
boost.data().vuser_id().value_or_empty(),
QDateTime::fromSecsSinceEpoch(boost.data().vexpires().v),
data.is_gift(),
data.is_giveaway(),
data.is_unclaimed(),
qs(data.vid()),
data.vuser_id().value_or_empty(),
data.vgiveaway_msg_id()
? FullMsgId{ _peer->id, data.vgiveaway_msg_id()->v }
: FullMsgId(),
QDateTime::fromSecsSinceEpoch(data.vdate().v),
data.vexpires().v,
std::move(giftCodeLink),
data.vmultiplier().value_or_empty(),
});
}
done(Data::BoostsListSlice{
.list = std::move(list),
.total = data.vcount().v,
.multipliedTotal = data.vcount().v,
.allLoaded = (data.vcount().v == data.vboosts().v.size()),
.token = Data::BoostsListSlice::OffsetToken{
data.vnext_offset()
.next = data.vnext_offset()
? qs(*data.vnext_offset())
: QString()
: QString(),
.gifts = gifts,
},
});
}).fail([=] {

View file

@ -3614,7 +3614,14 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
sendAction(action);
const auto clearCloudDraft = action.clearDraft;
const auto topicRootId = action.replyTo.topicRootId;
const auto replyTo = action.replyTo.messageId
? peer->owner().message(action.replyTo.messageId)
: nullptr;
const auto topicRootId = action.replyTo.topicRootId
? action.replyTo.topicRootId
: replyTo
? replyTo->topicRootId()
: Data::ForumTopic::kGeneralId;
const auto topic = peer->forumTopicFor(topicRootId);
if (!(topic ? Data::CanSendTexts(topic) : Data::CanSendTexts(peer))
|| Api::SendDice(message)) {

View file

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

View file

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

View file

@ -35,6 +35,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_dialogs.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(
not_null<PeerData*> peer,
bool respectSavedMessagesChat) {

View file

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

View file

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

View file

@ -32,8 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_settings.h" // st::settingsDividerLabelPadding
#include "styles/style_menu_icons.h"
#include <xxhash.h>
namespace {
enum class Color {
@ -112,12 +110,8 @@ private:
};
[[nodiscard]] uint64 ComputeRowId(const QString &link) {
return XXH64(link.data(), link.size() * sizeof(ushort), 0);
}
[[nodiscard]] uint64 ComputeRowId(const InviteLinkData &data) {
return ComputeRowId(data.link);
return UniqueRowIdFromString(data.link);
}
[[nodiscard]] float64 ComputeProgress(
@ -628,7 +622,8 @@ void LinksController::updateRow(const InviteLinkData &data, TimeId now) {
}
bool LinksController::removeRow(const QString &link) {
if (const auto row = delegate()->peerListFindRow(ComputeRowId(link))) {
const auto id = UniqueRowIdFromString(link);
if (const auto row = delegate()->peerListFindRow(id)) {
delegate()->peerListRemoveRow(row);
return true;
}

View file

@ -432,6 +432,7 @@ void SendFilesBox::prepare() {
preparePreview();
initPreview();
SetupShadowsToScrollContent(this, _scroll, _inner->heightValue());
setCloseByOutsideClick(false);
boxClosing() | rpl::start_with_next([=] {
if (!_confirmed && _cancelledCallback) {
@ -1451,7 +1452,12 @@ void SendFilesBox::sendScheduled() {
? SendMenu::Type::ScheduledToUser
: _sendMenuType;
const auto callback = [=](Api::SendOptions options) { send(options); };
_show->showBox(HistoryView::PrepareScheduleBox(this, type, callback));
auto box = HistoryView::PrepareScheduleBox(this, type, callback);
const auto weak = Ui::MakeWeak(box.data());
_show->showBox(std::move(box));
if (const auto strong = weak.data()) {
strong->setCloseByOutsideClick(false);
}
}
void SendFilesBox::sendWhenOnline() {

View file

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

View file

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

View file

@ -86,9 +86,16 @@ struct FileReferenceAccumulator {
}, [](const auto &data) {
});
}
void push(const MTPMessageReplyHeader &data) {
data.match([&](const MTPDmessageReplyHeader &data) {
push(data.vreply_media());
}, [](const MTPDmessageReplyStoryHeader &data) {
});
}
void push(const MTPMessage &data) {
data.match([&](const MTPDmessage &data) {
push(data.vmedia());
push(data.vreply_to());
}, [&](const MTPDmessageService &data) {
data.vaction().match(
[&](const MTPDmessageActionChatEditPhoto &data) {
@ -99,6 +106,7 @@ struct FileReferenceAccumulator {
push(data.vwallpaper());
}, [](const auto &data) {
});
push(data.vreply_to());
}, [](const MTPDmessageEmpty &data) {
});
}

View file

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

View file

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

View file

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

View file

@ -89,6 +89,18 @@ private:
: FrameSizeFromTag(tag);
}
[[nodiscard]] QString InternalPrefix() {
return u"internal:"_q;
}
[[nodiscard]] QString InternalPadding(QMargins value) {
return value.isNull() ? QString() : QString(",%1,%2,%3,%4"
).arg(value.left()
).arg(value.top()
).arg(value.right()
).arg(value.bottom());
}
} // namespace
class CustomEmojiLoader final
@ -514,6 +526,9 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
Fn<void()> update,
SizeTag tag,
int sizeOverride) {
if (data.startsWith(InternalPrefix())) {
return internal(data);
}
const auto parsed = ParseCustomEmojiData(data);
return parsed
? create(parsed, std::move(update), tag, sizeOverride)
@ -540,6 +555,26 @@ std::unique_ptr<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(
QStringView data,
not_null<Listener*> listener) {
@ -885,6 +920,41 @@ uint64 CustomEmojiManager::coloredSetId() const {
return _coloredSetId;
}
QString CustomEmojiManager::registerInternalEmoji(
QImage emoji,
QMargins padding,
bool textColor) {
_internalEmoji.push_back({ std::move(emoji), textColor });
return InternalPrefix()
+ QString::number(_internalEmoji.size() - 1)
+ InternalPadding(padding);
}
QString CustomEmojiManager::registerInternalEmoji(
const style::icon &icon,
QMargins padding,
bool textColor) {
const auto i = _iconEmoji.find(&icon);
if (i != end(_iconEmoji)) {
return i->second + InternalPadding(padding);
}
auto image = QImage(
icon.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
image.setDevicePixelRatio(style::DevicePixelRatio());
auto p = QPainter(&image);
icon.paint(p, 0, 0, icon.width());
p.end();
const auto result = registerInternalEmoji(
std::move(image),
QMargins{},
textColor);
_iconEmoji.emplace(&icon, result);
return result + InternalPadding(padding);
}
int FrameSizeFromTag(SizeTag tag) {
const auto emoji = EmojiSizeFromTag(tag);
const auto factor = style::DevicePixelRatio();

View file

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

View file

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

View file

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

View file

@ -23,7 +23,13 @@ void ItemImage::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
p->drawPixmap(contentRect().toRect(), _pixmap);
const auto rect = contentRect();
const auto pixmapSize = QSizeF(_pixmap.size() / style::DevicePixelRatio())
.scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(rect.topLeft(), pixmapSize).translated(
(rect.width() - pixmapSize.width()) / 2.,
(rect.height() - pixmapSize.height()) / 2.);
p->drawPixmap(resultRect.toRect(), _pixmap);
ItemBase::paint(p, option, w);
}

View file

@ -110,7 +110,13 @@ void ItemSticker::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
p->drawImage(contentRect().toRect(), _image);
const auto rect = contentRect();
const auto imageSize = QSizeF(_image.size() / style::DevicePixelRatio())
.scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(rect.topLeft(), imageSize).translated(
(rect.width() - imageSize.width()) / 2.,
(rect.height() - imageSize.height()) / 2.);
p->drawImage(resultRect, _image);
ItemBase::paint(p, option, w);
}

View file

@ -265,7 +265,11 @@ public:
return _widget ? _widget->elementAnimationsPaused() : false;
}
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 {
return view->data()->unread(view->data()->history());

View file

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

View file

@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -56,127 +57,6 @@ namespace {
const auto kPsaForwardedPrefix = "cloud_lng_forwarded_psa_";
void ValidateBackgroundEmoji(
DocumentId backgroundEmojiId,
not_null<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
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(
not_null<History*> history,
not_null<HistoryItem*> item,
const MTPMessageReplyHeader &reply) {
return reply.match([&](const MTPDmessageReplyHeader &data) {
auto result = ReplyFields();
if (const auto peer = data.vreply_to_peer_id()) {
result.externalPeerId = peerFromMTP(*peer);
}
const auto owner = &history->owner();
const auto owner = &item->history()->owner();
if (const auto id = data.vreply_to_msg_id().value_or_empty()) {
result.messageId = data.is_reply_to_scheduled()
? owner->scheduledMessages().localMessageId(id)
@ -417,6 +315,9 @@ ReplyFields ReplyFieldsFromMTP(
result.externalSenderName
= qs(data.vfrom_name().value_or_empty());
}
if (const auto media = data.vreply_media()) {
result.externalMedia = HistoryItem::CreateMedia(item, *media);
}
result.quote = TextWithEntities{
qs(data.vquote_text().value_or_empty()),
Api::EntitiesFromMTP(
@ -477,7 +378,7 @@ HistoryMessageReply &HistoryMessageReply::operator=(
HistoryMessageReply::~HistoryMessageReply() {
// clearData() should be called by holder.
Expects(resolvedMessage.empty());
Expects(originalVia == nullptr);
_fields.externalMedia = nullptr;
}
bool HistoryMessageReply::updateData(
@ -523,64 +424,31 @@ bool HistoryMessageReply::updateData(
}
}
const auto external = this->external();
if (resolvedMessage
const auto asExternal = displayAsExternal(holder);
const auto nonEmptyQuote = !_fields.quote.empty()
&& (asExternal || _fields.manualQuote);
_multiline = !_fields.storyId && (asExternal || nonEmptyQuote);
const auto displaying = resolvedMessage
|| resolvedStory
|| (external && (!_fields.messageId || force))) {
const auto repaint = [=] { holder->customEmojiRepaint(); };
const auto context = Core::MarkedTextContext{
.session = &holder->history()->session(),
.customEmojiRepaint = repaint,
};
const auto text = !_fields.quote.empty()
? _fields.quote
: resolvedMessage
? resolvedMessage->inReplyText()
: resolvedStory
? resolvedStory->inReplyText()
: TextWithEntities{ u"..."_q };
_text.setMarkedText(
st::defaultTextStyle,
text,
Ui::DialogTextOptions(),
context);
|| ((nonEmptyQuote || _fields.externalMedia)
&& (!_fields.messageId || force));
_displaying = displaying ? 1 : 0;
updateName(holder);
setLinkFrom(holder);
if (resolvedMessage
&& !resolvedMessage->Has<HistoryMessageForwarded>()) {
if (const auto bot = resolvedMessage->viaBot()) {
originalVia = std::make_unique<HistoryMessageVia>();
originalVia->create(
&holder->history()->owner(),
peerToUser(bot->id));
}
}
const auto unavailable = !resolvedMessage
&& !resolvedStory
&& ((!_fields.storyId && !_fields.messageId) || force);
_unavailable = unavailable ? 1 : 0;
if (!resolvedMessage && !resolvedStory) {
_unavailable = 1;
}
const auto media = resolvedMessage
? resolvedMessage->media()
: nullptr;
if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) {
spoiler = nullptr;
} else if (!spoiler) {
spoiler = std::make_unique<Ui::SpoilerAnimation>(repaint);
}
} else if (force) {
if (_fields.messageId || _fields.storyId) {
_unavailable = 1;
}
spoiler = nullptr;
}
if (force) {
if (!_displaying && (_fields.messageId || _fields.storyId)) {
_unavailable = 1;
}
holder->history()->owner().requestItemResize(holder);
}
return resolvedMessage
|| resolvedStory
|| (external && !_fields.messageId)
|| (!_fields.messageId && !_fields.storyId && external())
|| _unavailable;
}
@ -610,39 +478,11 @@ void HistoryMessageReply::updateFields(
}
}
void HistoryMessageReply::setLinkFrom(
not_null<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) {
_fields.topMessageId = topMessageId;
}
void HistoryMessageReply::clearData(not_null<HistoryItem*> holder) {
originalVia = nullptr;
if (resolvedMessage) {
holder->history()->owner().unregisterDependentMessage(
holder,
@ -655,9 +495,12 @@ void HistoryMessageReply::clearData(not_null<HistoryItem*> holder) {
resolvedStory.get());
resolvedStory = nullptr;
}
_name.clear();
_text.clear();
_unavailable = 1;
_displaying = 0;
if (_multiline) {
holder->history()->owner().requestItemResize(holder);
_multiline = 0;
}
refreshReplyToMedia();
}
@ -667,145 +510,13 @@ bool HistoryMessageReply::external() const {
|| !_fields.externalSenderName.isEmpty();
}
PeerData *HistoryMessageReply::sender(not_null<HistoryItem*> holder) const {
if (resolvedStory) {
return resolvedStory->peer();
} else if (!resolvedMessage) {
if (!_externalSender && _fields.externalSenderId) {
_externalSender = holder->history()->owner().peer(
_fields.externalSenderId);
}
return _externalSender;
} else if (holder->Has<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(
bool HistoryMessageReply::displayAsExternal(
not_null<HistoryItem*> holder) const {
if (const auto peer = sender(holder)) {
return senderName(peer);
} else if (!resolvedMessage) {
return _fields.externalSenderName;
} else if (holder->Has<HistoryMessageForwarded>()) {
// 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);
// Don't display replies that could be local as external.
return external()
&& (!resolvedMessage
|| (holder->history() != resolvedMessage->history())
|| (holder->topicRootId() != resolvedMessage->topicRootId()));
}
void HistoryMessageReply::itemRemoved(
@ -826,224 +537,6 @@ void HistoryMessageReply::storyRemoved(
}
}
void HistoryMessageReply::paint(
Painter &p,
not_null<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() {
replyToDocumentId = 0;
replyToWebPageId = 0;

View file

@ -22,9 +22,12 @@ namespace Ui {
struct ChatPaintContext;
class ChatStyle;
struct PeerUserpicView;
class SpoilerAnimation;
} // namespace Ui
namespace Ui::Text {
struct GeometryDescriptor;
} // namespace Ui::Text
namespace Data {
class Session;
class Story;
@ -230,7 +233,10 @@ private:
};
struct ReplyFields {
ReplyFields clone(not_null<HistoryItem*> parent) const;
TextWithEntities quote;
std::unique_ptr<Data::Media> externalMedia;
PeerId externalSenderId = 0;
QString externalSenderName;
QString externalPostAuthor;
@ -243,7 +249,7 @@ struct ReplyFields {
};
[[nodiscard]] ReplyFields ReplyFieldsFromMTP(
not_null<History*> history,
not_null<HistoryItem*> item,
const MTPMessageReplyHeader &reply);
[[nodiscard]] FullReplyTo ReplyToFromMTP(
@ -260,8 +266,6 @@ struct HistoryMessageReply
HistoryMessageReply &operator=(HistoryMessageReply &&other);
~HistoryMessageReply();
static constexpr auto kBarAlpha = 230. / 255.;
void set(ReplyFields fields);
void updateFields(
@ -275,16 +279,8 @@ struct HistoryMessageReply
void clearData(not_null<HistoryItem*> holder);
[[nodiscard]] bool external() const;
[[nodiscard]] PeerData *sender(not_null<HistoryItem*> holder) const;
[[nodiscard]] QString senderName(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;
[[nodiscard]] bool displayAsExternal(
not_null<HistoryItem*> holder) const;
void itemRemoved(
not_null<HistoryItem*> holder,
not_null<HistoryItem*> removed);
@ -292,17 +288,7 @@ struct HistoryMessageReply
not_null<HistoryItem*> holder,
not_null<Data::Story*> removed);
void paint(
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 {
[[nodiscard]] const ReplyFields &fields() const {
return _fields;
}
[[nodiscard]] PeerId externalPeerId() const {
@ -317,21 +303,22 @@ struct HistoryMessageReply
[[nodiscard]] MsgId topMessageId() const {
return _fields.topMessageId;
}
[[nodiscard]] int maxWidth() const {
return _maxWidth;
}
[[nodiscard]] ClickHandlerPtr link() const {
return _link;
}
[[nodiscard]] bool topicPost() const {
return _fields.topicPost;
}
[[nodiscard]] bool manualQuote() const {
return _fields.manualQuote;
}
[[nodiscard]] QString statePhrase() const;
[[nodiscard]] bool unavailable() const {
return _unavailable;
}
[[nodiscard]] bool displaying() const {
return _displaying;
}
[[nodiscard]] bool multiline() const {
return _multiline;
}
void setLinkFrom(not_null<HistoryItem*> holder);
void setTopMessageId(MsgId topMessageId);
void refreshReplyToMedia();
@ -340,25 +327,12 @@ struct HistoryMessageReply
WebPageId replyToWebPageId = 0;
ReplyToMessagePointer resolvedMessage;
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:
ReplyFields _fields;
ClickHandlerPtr _link;
mutable Ui::Text::String _name;
mutable Ui::Text::String _text;
mutable PeerData *_externalSender = nullptr;
mutable int _maxWidth = 0;
mutable int _minHeight = 0;
mutable int _height = 0;
mutable int _nameVersion = 0;
uint8 _unavailable : 1 = 0;
uint8 _displaying : 1 = 0;
uint8 _multiline : 1 = 0;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -597,7 +597,11 @@ void PinnedWidget::listUpdateDateLink(
}
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) {

View file

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

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));
//ActivateWindow(controller());
const auto shown = controller()->show(std::move(box));
shown->setCloseByOutsideClick(false);
controller()->show(std::move(box));
return true;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -418,13 +418,6 @@ bool ThemeDocument::isReadyForOpen() const {
return !_data || _dataMedia->loaded();
}
QString ThemeDocument::additionalInfoString() const {
// This will force message info (time) to be displayed below
// this attachment in WebPage media.
static auto result = QString(" ");
return result;
}
bool ThemeDocument::hasHeavyPart() const {
return (_dataMedia != nullptr);
}

View file

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

View file

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

View file

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

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

View file

@ -118,4 +118,3 @@ std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
}
} // 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(
not_null<Ui::VerticalLayout*> content,
tr::phrase<> text,
@ -507,6 +463,50 @@ void FillOverview(
} // 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(
QWidget *parent,
not_null<Controller*> controller,

View file

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

View file

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

View file

@ -14,6 +14,7 @@ class VerticalLayout;
} // namespace Ui
namespace Data {
struct Boost;
struct BoostsListSlice;
struct PublicForwardsSlice;
struct SupergroupStatistics;
@ -38,7 +39,7 @@ void AddMembersList(
void AddBoostsList(
const Data::BoostsListSlice &firstSlice,
not_null<Ui::VerticalLayout*> container,
Fn<void(not_null<PeerData*>)> showPeerInfo,
Fn<void(const Data::Boost &)> boostClickedCallback,
not_null<PeerData*> peer,
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_tos_url(tr::now))),
Ui::Text::WithEntities),
st::defaultBoxCheckbox,
st::urlAuthCheckbox,
std::move(checkView)),
{
st::boxRowPadding.left(),
@ -241,7 +241,7 @@ void FillDisclaimerBox(not_null<Ui::GenericBox*> box, Fn<void()> done) {
st::boxRowPadding.right(),
0,
});
row->setAllowTextLines(5);
row->setAllowTextLines();
row->setClickHandlerFilter([=](
const ClickHandlerPtr &link,
Qt::MouseButton button) {

View file

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

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QVector>
#include <QtCore/QString>
#include <QtCore/QByteArray>
#include <range/v3/range/conversion.hpp>
#include <gsl/gsl>
using mtpPrime = int32;
@ -243,6 +244,18 @@ inline MTPvector<T> MTP_vector() {
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 {
template <typename Accumulator>

View file

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

View file

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

View file

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

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