Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bf6dbc1e | ||
|
|
64b5269648 | ||
|
|
f394cecf55 | ||
|
|
8b56676c23 | ||
|
|
e2713ea627 | ||
|
|
f5e50409d3 | ||
|
|
050916a56a | ||
|
|
cdf36cc387 | ||
|
|
f909a36cbd | ||
|
|
1d36255ca5 | ||
|
|
acfdae2d72 | ||
|
|
ab59e97b92 | ||
|
|
1060b04b1e | ||
|
|
df044dbd83 | ||
|
|
5eb210ec12 | ||
|
|
f24f78c0cc | ||
|
|
8a071fe1fe | ||
|
|
f3e84de5fb | ||
|
|
2dec1b72f7 | ||
|
|
604a827a52 | ||
|
|
3d8b303ab7 | ||
|
|
2c599e60c3 | ||
|
|
928d8feb21 | ||
|
|
490e688a91 | ||
|
|
34c36d77c3 | ||
|
|
0ab26f0c82 | ||
|
|
db453ab7ae | ||
|
|
e032dbf383 | ||
|
|
3b4ed03105 | ||
|
|
963694330d | ||
|
|
2733b12cff | ||
|
|
a377364621 | ||
|
|
58f4884deb | ||
|
|
c2c7a25487 | ||
|
|
f98c08f4c6 | ||
|
|
cfc2a959cf | ||
|
|
6a1630a84c | ||
|
|
a51be85199 | ||
|
|
e0fd5d8795 | ||
|
|
8659f60b46 | ||
|
|
c43699fb43 | ||
|
|
c56a22c8d5 | ||
|
|
7f27ce6dee | ||
|
|
508ba4750c | ||
|
|
c0b19000d6 | ||
|
|
409a3357da | ||
|
|
82523978c9 | ||
|
|
718ba2d0e3 | ||
|
|
2317dd8820 | ||
|
|
df06f55c7f | ||
|
|
d43853460e | ||
|
|
28fee318d7 | ||
|
|
1ec2ecac11 | ||
|
|
7aa3956792 | ||
|
|
d349759618 | ||
|
|
ac3e4fb42f | ||
|
|
eccb01e5b5 | ||
|
|
7c8d10022f | ||
|
|
f1244e19a1 | ||
|
|
e17143dd8b | ||
|
|
39d5d3a1cf | ||
|
|
f8be5731a5 | ||
|
|
ab248febcd | ||
|
|
d4afba3a24 | ||
|
|
4ee9751feb | ||
|
|
46fb5ee1d2 | ||
|
|
749f837df5 | ||
|
|
e11904e05b | ||
|
|
e1aa08b985 | ||
|
|
2af3770b29 | ||
|
|
74f9d0935b | ||
|
|
f9c50fdc06 | ||
|
|
1fa825321d | ||
|
|
d9147562e5 | ||
|
|
5b569718ec | ||
|
|
a5d4746202 | ||
|
|
50d150302d | ||
|
|
e451eb5126 | ||
|
|
a626364430 | ||
|
|
b55ed7214a | ||
|
|
d6801517bb | ||
|
|
97dde7eb56 | ||
|
|
10df3dce7c | ||
|
|
889d7c0c15 | ||
|
|
799155279f | ||
|
|
10e7bd0d6e | ||
|
|
0a99487091 | ||
|
|
4965c19314 | ||
|
|
30810e95f4 | ||
|
|
a3d84f69ea | ||
|
|
b3bb1a537c | ||
|
|
726aa3316d | ||
|
|
ba6c3eaf73 | ||
|
|
ebe45f3fa1 | ||
|
|
3f0fed19d8 | ||
|
|
609cab6e2f | ||
|
|
7b3cb0c3dd | ||
|
|
43559fb6b7 | ||
|
|
8788692fb3 | ||
|
|
072e346324 | ||
|
|
99f65ab5ec | ||
|
|
fe7b120003 | ||
|
|
cb8f86bc8d | ||
|
|
18e6e2da9e | ||
|
|
54247cd11b | ||
|
|
8b0725650d | ||
|
|
20411be9bd | ||
|
|
f4f36d85b9 | ||
|
|
9f887237eb | ||
|
|
4e3f917a2c | ||
|
|
5c9c836857 | ||
|
|
31b7fe6ba0 | ||
|
|
102c0a96ed | ||
|
|
9a0be43ef5 | ||
|
|
c1d948ef63 | ||
|
|
9df229a230 | ||
|
|
a1c342c822 | ||
|
|
c313cfb4ec | ||
|
|
8d4a658d0b | ||
|
|
86f53d3eff | ||
|
|
3cb89339c8 | ||
|
|
ba98a8df32 | ||
|
|
f3faa52bc7 | ||
|
|
3dac08d34f | ||
|
|
deba090cbd | ||
|
|
5b01f9530b | ||
|
|
df66162bca | ||
|
|
d413080f83 | ||
|
|
38ee57f852 | ||
|
|
c632316ad7 | ||
|
|
bba7010e74 | ||
|
|
edf93b0031 | ||
|
|
611be90880 | ||
|
|
68886e1b61 | ||
|
|
67319c1612 | ||
|
|
da8db0157f | ||
|
|
6188268afd | ||
|
|
cd0db53bac | ||
|
|
5bb90679a8 | ||
|
|
72df3a8f91 | ||
|
|
5fe2e649fb | ||
|
|
9eba8ccc73 | ||
|
|
bb3c91aa44 | ||
|
|
9f1268b6c8 | ||
|
|
a6f1a1bd62 | ||
|
|
1b2642b017 | ||
|
|
e722645e7c | ||
|
|
9486c266b5 | ||
|
|
dc21491099 |
5
.github/workflows/mac.yml
vendored
5
.github/workflows/mac.yml
vendored
@@ -91,6 +91,11 @@ jobs:
|
||||
run: |
|
||||
./$REPO_NAME/Telegram/build/prepare/mac.sh skip-release silent
|
||||
|
||||
- name: Free up some disk space.
|
||||
run: |
|
||||
cd Libraries
|
||||
find . -iname "*.dir" -exec rm -rf {} || true \;
|
||||
|
||||
- name: Telegram Desktop build.
|
||||
if: env.ONLY_CACHE == 'false'
|
||||
run: |
|
||||
|
||||
407
.github/workflows/win.yml
vendored
407
.github/workflows/win.yml
vendored
@@ -44,385 +44,84 @@ jobs:
|
||||
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
defines:
|
||||
- ""
|
||||
arch: [Win32, x64]
|
||||
|
||||
env:
|
||||
SDK: "10.0.18362.0"
|
||||
GIT: "https://github.com"
|
||||
QT_VER: "5.15.2"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
MANUAL_CACHING: "0"
|
||||
PREPARE_PATH: "Telegram/build/prepare/prepare.py"
|
||||
AUTO_CACHING: "1"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: cmd
|
||||
working-directory: Libraries
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
steps:
|
||||
- name: Prepare directories.
|
||||
run: |
|
||||
mkdir %userprofile%\TBuild\Libraries
|
||||
echo TBUILD=%userprofile%\TBuild>>%GITHUB_ENV%
|
||||
|
||||
- name: Get repository name.
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
|
||||
|
||||
- uses: ilammy/msvc-dev-cmd@v1.9.0
|
||||
name: x86 Native Tools Command Prompt.
|
||||
- uses: ilammy/msvc-dev-cmd@v1.10.0
|
||||
name: Native Tools Command Prompt.
|
||||
with:
|
||||
arch: win32
|
||||
|
||||
- name: Set up environment paths.
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
echo "C:\\Strawberry\\perl\\bin\\" >> $GITHUB_PATH
|
||||
echo "C:\\Program Files\\NASM\\" >> $GITHUB_PATH
|
||||
echo "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise\\VC\\Auxiliary\\Build\\" >> $GITHUB_PATH
|
||||
|
||||
mkdir Libraries && cd Libraries
|
||||
echo "Convert unix path to win path."
|
||||
p=`pwd | sed 's#^/[d]#d:#g' |sed 's#/#\\\\#g'`
|
||||
echo "LibrariesPath=$p" >> $GITHUB_ENV
|
||||
|
||||
echo "QT=${QT_VER//./_}" >> $GITHUB_ENV
|
||||
|
||||
- name: Save msbuild version.
|
||||
run: |
|
||||
call vcvars32.bat
|
||||
msbuild -version > CACHE_KEY.txt
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v2
|
||||
uses: LebedevRI/checkout@issue197
|
||||
with:
|
||||
submodules: recursive
|
||||
path: ${{ env.REPO_NAME }}
|
||||
|
||||
- name: Generate cache key.
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
curl -o $LibrariesPath/tg_owt-version.json https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master
|
||||
curl -o $LibrariesPath/tg_angle-version.json https://api.github.com/repos/desktop-app/tg_angle/git/refs/heads/master
|
||||
echo $MANUAL_CACHING >> CACHE_KEY.txt
|
||||
if [ "$AUTO_CACHING" == "1" ]; then
|
||||
thisFile=$REPO_NAME/.github/workflows/win.yml
|
||||
echo `md5sum $thisFile | awk '{ print $1 }'` >> CACHE_KEY.txt
|
||||
fi
|
||||
echo "CACHE_KEY=`md5sum CACHE_KEY.txt | awk '{ print $1 }'`" >> $GITHUB_ENV
|
||||
path: ${{ env.TBUILD }}\${{ env.REPO_NAME }}
|
||||
|
||||
- name: Choco installs.
|
||||
run: |
|
||||
choco install --allow-empty-checksums --no-progress -y yasm
|
||||
choco install --no-progress -y nasm jom ninja
|
||||
python -m pip install pywin32
|
||||
choco install --no-progress -y nasm strawberryperl yasm jom ninja
|
||||
py -m pip install pywin32
|
||||
|
||||
- name: Install msys64.
|
||||
run: |
|
||||
mkdir %TBUILD%\ThirdParty
|
||||
xcopy /E /I C:\msys64 %TBUILD%\ThirdParty\msys64
|
||||
|
||||
- name: Set up environment paths.
|
||||
shell: bash
|
||||
run: |
|
||||
echo "C:\\Strawberry\\perl\\bin\\" >> $GITHUB_PATH
|
||||
echo "C:\\Program Files\\NASM\\" >> $GITHUB_PATH
|
||||
echo "C:\\ProgramData\\chocolatey\\lib\\ninja\\tools\\" >> $GITHUB_PATH
|
||||
|
||||
echo "Configurate git for cherry-picks."
|
||||
git config --global user.email "you@example.com"
|
||||
git config --global user.name "Sample"
|
||||
|
||||
- name: NuGet sources.
|
||||
run: |
|
||||
nuget sources Disable -Name "Microsoft Visual Studio Offline Packages"
|
||||
nuget sources Add -Source https://api.nuget.org/v3/index.json & exit 0
|
||||
|
||||
- name: Patches.
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
echo "Find necessary commit from doc."
|
||||
checkoutCommit=$(grep -A 1 "cd patches" $REPO_NAME/$PREPARE_PATH | sed -n 2p)
|
||||
cd $LibrariesPath
|
||||
git clone $GIT/desktop-app/patches.git
|
||||
cd patches
|
||||
eval $checkoutCommit
|
||||
|
||||
- name: LZMA.
|
||||
run: |
|
||||
git clone %GIT%/telegramdesktop/lzma.git
|
||||
cd lzma
|
||||
cd C\Util\LzmaLib
|
||||
msbuild -m LzmaLib.sln /property:Configuration=Debug
|
||||
|
||||
- name: OpenSSL cache.
|
||||
id: cache-openssl
|
||||
- name: Libraries cache.
|
||||
id: cache-libs
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/openssl
|
||||
key: ${{ runner.OS }}-${{ env.CACHE_KEY }}-${{ env.OPENSSL_VER }}
|
||||
- name: OpenSSL.
|
||||
if: steps.cache-openssl.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
git clone -b OpenSSL_%OPENSSL_VER%-stable %GIT%/openssl/openssl.git
|
||||
cd openssl
|
||||
perl Configure no-shared no-tests debug-VC-WIN32
|
||||
nmake
|
||||
mkdir out.dbg
|
||||
move libcrypto.lib out.dbg
|
||||
move libssl.lib out.dbg
|
||||
move ossl_static.pdb out.dbg\ossl_static
|
||||
nmake clean
|
||||
move out.dbg\ossl_static out.dbg\ossl_static.pdb
|
||||
perl Configure no-shared no-tests VC-WIN32
|
||||
nmake
|
||||
mkdir out
|
||||
move libcrypto.lib out
|
||||
move libssl.lib out
|
||||
move ossl_static.pdb out
|
||||
path: ${{ env.TBUILD }}/Libraries
|
||||
key: ${{ runner.OS }}-libs
|
||||
|
||||
rmdir /S /Q test
|
||||
rmdir /S /Q .git
|
||||
|
||||
- name: Zlib.
|
||||
run: |
|
||||
git clone %GIT%/telegramdesktop/zlib.git
|
||||
cd zlib
|
||||
git checkout tdesktop
|
||||
cd contrib\vstudio\vc14
|
||||
msbuild -m zlibstat.vcxproj /property:Configuration=Debug
|
||||
|
||||
- name: MozJPEG.
|
||||
shell: cmd
|
||||
run: |
|
||||
git clone -b v4.0.3 %GIT%/mozilla/mozjpeg.git
|
||||
cd mozjpeg
|
||||
cmake . ^
|
||||
-G "Visual Studio 16 2019" ^
|
||||
-A Win32 ^
|
||||
-DWITH_JPEG8=ON ^
|
||||
-DPNG_SUPPORTED=OFF
|
||||
cmake --build . --config Debug
|
||||
|
||||
- name: OpenAL Soft cache.
|
||||
id: cache-openal
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/openal-soft
|
||||
key: ${{ runner.OS }}-openal-soft-${{ env.CACHE_KEY }}
|
||||
- name: OpenAL Soft.
|
||||
if: steps.cache-openal.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
git clone -b openal-soft-1.21.0 --depth=1 %GIT%/kcat/openal-soft.git
|
||||
cd openal-soft\build
|
||||
cmake .. ^
|
||||
-G "Visual Studio 16 2019" ^
|
||||
-A Win32 ^
|
||||
-D LIBTYPE:STRING=STATIC ^
|
||||
-D FORCE_STATIC_VCRT=ON ^
|
||||
-D ALSOFT_BACKEND_DSOUND=OFF
|
||||
|
||||
msbuild -m OpenAL.vcxproj /property:Configuration=Debug
|
||||
|
||||
- name: Breakpad cache.
|
||||
id: cache-breakpad
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/breakpad
|
||||
key: ${{ runner.OS }}-breakpad-${{ env.CACHE_KEY }}-${{ hashFiles('**/breakpad.diff') }}
|
||||
- name: Breakpad.
|
||||
- name: Libraries.
|
||||
env:
|
||||
GYP_MSVS_OVERRIDE_PATH: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\'
|
||||
GYP_MSVS_VERSION: 2019
|
||||
if: steps.cache-breakpad.outputs.cache-hit != 'true'
|
||||
GYP_MSVS_OVERRIDE_PATH: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\'
|
||||
GYP_MSVS_VERSION: 2022
|
||||
run: |
|
||||
git clone https://chromium.googlesource.com/external/gyp
|
||||
cd gyp
|
||||
SET PATH=%cd%;%PATH%
|
||||
git checkout d6c5dd51dc
|
||||
git apply ../patches/gyp.diff
|
||||
|
||||
cd %LibrariesPath%
|
||||
|
||||
git clone https://chromium.googlesource.com/breakpad/breakpad
|
||||
cd breakpad
|
||||
git checkout dfcb7b6799
|
||||
git apply ../patches/breakpad.diff
|
||||
cd src
|
||||
git clone -b release-1.11.0 %GIT%/google/googletest testing
|
||||
cd client\windows
|
||||
call gyp --no-circular-check breakpad_client.gyp --format=ninja
|
||||
cd ..\..
|
||||
ninja -C out/Debug common crash_generation_client exception_handler
|
||||
ninja -C out/Release common crash_generation_client exception_handler
|
||||
cd tools\windows\dump_syms
|
||||
call gyp dump_syms.gyp --format=ninja
|
||||
cd ..\..\..
|
||||
ninja -C out/Release dump_syms
|
||||
|
||||
- name: Opus cache.
|
||||
id: cache-opus
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/opus
|
||||
key: ${{ runner.OS }}-opus-${{ env.CACHE_KEY }}
|
||||
- name: Opus.
|
||||
if: steps.cache-opus.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
git clone -b v1.3.1 %GIT%/xiph/opus.git
|
||||
cd opus
|
||||
git cherry-pick 927de8453c
|
||||
cmake -B out . ^
|
||||
-A Win32 ^
|
||||
-DCMAKE_INSTALL_PREFIX=%LibrariesPath%/local/opus ^
|
||||
-DCMAKE_C_FLAGS_DEBUG="/MTd /Zi /Ob0 /Od /RTC1" ^
|
||||
-DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG"
|
||||
cmake --build out --config Debug
|
||||
cmake --build out --config Release
|
||||
cmake --install out --config Release
|
||||
|
||||
- name: Rnnoise.
|
||||
run: |
|
||||
git clone %GIT%/desktop-app/rnnoise.git
|
||||
mkdir rnnoise\out
|
||||
cd rnnoise\out
|
||||
cmake -A Win32 ..
|
||||
cmake --build . --config Debug
|
||||
|
||||
- name: FFmpeg cache.
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/ffmpeg
|
||||
key: ${{ runner.OS }}-ffmpeg-${{ env.CACHE_KEY }}-2-${{ hashFiles('**/build_ffmpeg_win.sh') }}
|
||||
- name: FFmpeg.
|
||||
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
choco install --no-progress -y msys2
|
||||
|
||||
git clone %GIT%/FFmpeg/FFmpeg.git ffmpeg
|
||||
cd ffmpeg
|
||||
git checkout release/4.4
|
||||
set CHERE_INVOKING=enabled_from_arguments
|
||||
set MSYS2_PATH_TYPE=inherit
|
||||
call c:\tools\msys64\usr\bin\bash --login ../patches/build_ffmpeg_win.sh
|
||||
|
||||
rmdir /S /Q .git
|
||||
|
||||
- name: Angle cache.
|
||||
id: cache-angle
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/tg_angle
|
||||
key: ${{ runner.OS }}-angle-${{ env.CACHE_KEY }}-${{ hashFiles('**/tg_angle-version.json') }}
|
||||
- name: Angle.
|
||||
if: steps.cache-angle.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
git clone --recursive %GIT%/desktop-app/tg_angle.git
|
||||
mkdir tg_angle\out\Debug
|
||||
cd tg_angle\out\Debug
|
||||
cmake -G Ninja ^
|
||||
-DCMAKE_BUILD_TYPE=Debug ^
|
||||
-DTG_ANGLE_SPECIAL_TARGET=win64 ^
|
||||
-DTG_ANGLE_ZLIB_INCLUDE_PATH=%cd%/../../../zlib ../..
|
||||
ninja
|
||||
|
||||
:: Cleanup.
|
||||
cd %LibrariesPath%\tg_angle
|
||||
move out\Debug\tg_angle.lib tg_angle.lib
|
||||
rmdir /S /Q out
|
||||
mkdir out\Debug
|
||||
move tg_angle.lib out\Debug\tg_angle.lib
|
||||
|
||||
- name: Qt 5.15.2 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/Qt-${{ env.QT_VER }}
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_15_2/*') }}
|
||||
- name: Configure Qt 5.15.2.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
git clone git://code.qt.io/qt/qt5.git qt_%QT%
|
||||
cd qt_%QT%
|
||||
perl init-repository --module-subset=qtbase,qtimageformats,qtsvg
|
||||
git checkout v%QT_VER%
|
||||
git submodule update qtbase
|
||||
git submodule update qtimageformats
|
||||
git submodule update qtsvg
|
||||
cd qtbase
|
||||
for /r %%i in (..\..\patches\qtbase_%QT%\*) do git apply %%i
|
||||
cd ..
|
||||
|
||||
SET SSL=%LibrariesPath%\openssl
|
||||
SET SSL_LIBS=libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib
|
||||
|
||||
SET ANGLE=%LibrariesPath%\tg_angle
|
||||
SET ANGLE_LIBS=d3d9.lib dxgi.lib dxguid.lib
|
||||
|
||||
SET ZLIB=%LibrariesPath%\zlib\contrib\vstudio\vc14\x86
|
||||
|
||||
configure ^
|
||||
-prefix "%LibrariesPath%\Qt-%QT_VER%" ^
|
||||
-debug ^
|
||||
-force-debug-info ^
|
||||
-opensource ^
|
||||
-confirm-license ^
|
||||
-static ^
|
||||
-static-runtime ^
|
||||
-opengl es2 -no-angle ^
|
||||
-I "%ANGLE%\include" ^
|
||||
-D "GL_APICALL=" ^
|
||||
QMAKE_LIBS_OPENGL_ES2_DEBUG="%ANGLE%\out\Debug\tg_angle.lib %ZLIB%\ZlibStatDebug\zlibstat.lib %ANGLE_LIBS%" ^
|
||||
QMAKE_LIBS_OPENGL_ES2_RELEASE="%ANGLE%\out\Release\tg_angle.lib %ZLIB%\ZlibStatReleaseWithoutAsm\zlibstat.lib %ANGLE_LIBS%" ^
|
||||
-egl ^
|
||||
-D "EGLAPI=" ^
|
||||
-D "DESKTOP_APP_QT_STATIC_ANGLE=" ^
|
||||
QMAKE_LIBS_EGL_DEBUG="%ANGLE%\out\Debug\tg_angle.lib %ZLIB%\ZlibStatDebug\zlibstat.lib %ANGLE_LIBS% Gdi32.lib User32.lib" ^
|
||||
QMAKE_LIBS_EGL_RELEASE="%ANGLE%\out\Release\tg_angle.lib %ZLIB%\ZlibStatReleaseWithoutAsm\zlibstat.lib %ANGLE_LIBS% Gdi32.lib User32.lib" ^
|
||||
-openssl-linked ^
|
||||
-I "%SSL%\include" ^
|
||||
OPENSSL_LIBS_DEBUG="%SSL%\out.dbg\libssl.lib %SSL%\out.dbg\%SSL_LIBS%" ^
|
||||
OPENSSL_LIBS_RELEASE="%SSL%\out\libssl.lib %SSL%\out\%SSL_LIBS%" ^
|
||||
-I "%LibrariesPath%\mozjpeg" ^
|
||||
LIBJPEG_LIBS_DEBUG="%LibrariesPath%\mozjpeg\Debug\jpeg-static.lib" ^
|
||||
LIBJPEG_LIBS_RELEASE="%LibrariesPath%\mozjpeg\Release\jpeg-static.lib" ^
|
||||
-mp ^
|
||||
-nomake examples ^
|
||||
-nomake tests ^
|
||||
-platform win32-msvc
|
||||
|
||||
- name: Qt 5.15.2 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd qt_%QT%
|
||||
|
||||
jom -j%NUMBER_OF_PROCESSORS%
|
||||
jom -j%NUMBER_OF_PROCESSORS% install
|
||||
|
||||
cd ..
|
||||
rmdir /S /Q qt_%QT%
|
||||
|
||||
- name: WebRTC cache.
|
||||
id: cache-webrtc
|
||||
uses: actions/cache@v2
|
||||
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: |
|
||||
git clone --recursive %GIT%/desktop-app/tg_owt.git
|
||||
mkdir tg_owt\out\Debug
|
||||
cd tg_owt\out\Debug
|
||||
cmake -G Ninja ^
|
||||
-DCMAKE_BUILD_TYPE=Debug ^
|
||||
-DTG_OWT_SPECIAL_TARGET=win ^
|
||||
-DTG_OWT_BUILD_AUDIO_BACKENDS=OFF ^
|
||||
-DTG_OWT_LIBJPEG_INCLUDE_PATH=%cd%/../../../mozjpeg ^
|
||||
-DTG_OWT_OPENSSL_INCLUDE_PATH=%cd%/../../../openssl/include ^
|
||||
-DTG_OWT_OPUS_INCLUDE_PATH=%cd%/../../../opus/include ^
|
||||
-DTG_OWT_FFMPEG_INCLUDE_PATH=%cd%/../../../ffmpeg ^
|
||||
../..
|
||||
|
||||
ninja
|
||||
|
||||
:: Cleanup.
|
||||
cd %LibrariesPath%\tg_owt
|
||||
move out\Debug\tg_owt.lib tg_owt.lib
|
||||
rmdir /S /Q out
|
||||
mkdir out\Debug
|
||||
move tg_owt.lib out\Debug\tg_owt.lib
|
||||
C:
|
||||
cd %TBUILD%
|
||||
%REPO_NAME%/Telegram/build/prepare/win.bat skip-release silent
|
||||
|
||||
- name: Read defines.
|
||||
shell: bash
|
||||
@@ -438,16 +137,21 @@ jobs:
|
||||
echo "TDESKTOP_BUILD_DEFINE=$DEFINE" >> $GITHUB_ENV
|
||||
|
||||
- name: Free up some disk space.
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: del /S *.pdb
|
||||
run: |
|
||||
C:
|
||||
cd %TBUILD%
|
||||
del /S Libraries\*.pdb
|
||||
del /S Libraries\*.pch
|
||||
del /S Libraries\*.obj
|
||||
|
||||
- name: Telegram Desktop build.
|
||||
if: env.ONLY_CACHE == 'false'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
cd %REPO_NAME%\Telegram
|
||||
C:
|
||||
cd %TBUILD%\%REPO_NAME%\Telegram
|
||||
|
||||
call configure.bat ^
|
||||
${{ matrix.arch }} ^
|
||||
-D TDESKTOP_API_TEST=ON ^
|
||||
-D DESKTOP_APP_USE_PACKAGED=OFF ^
|
||||
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF ^
|
||||
@@ -455,20 +159,17 @@ jobs:
|
||||
%TDESKTOP_BUILD_DEFINE% ^
|
||||
-DCMAKE_SYSTEM_VERSION=%SDK%
|
||||
|
||||
call vcvars32.bat
|
||||
cd ..\out
|
||||
msbuild -m Telegram.sln /nologo /p:Configuration=Debug,Platform=Win32
|
||||
msbuild -m Telegram.sln /p:Configuration=Debug,Platform=${{ matrix.arch }},DebugSymbols=false,DebugType=none
|
||||
|
||||
- name: Move artifact.
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
cd %REPO_NAME%\out\Debug
|
||||
mkdir artifact
|
||||
move Telegram.exe artifact/
|
||||
move %TBUILD%\%REPO_NAME%\out\Debug\Telegram.exe artifact/
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload artifact.
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: ${{ env.REPO_NAME }}\out\Debug\artifact\
|
||||
path: artifact\
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -94,3 +94,12 @@
|
||||
[submodule "Telegram/ThirdParty/dispatch"]
|
||||
path = Telegram/ThirdParty/dispatch
|
||||
url = https://github.com/apple/swift-corelibs-libdispatch
|
||||
[submodule "Telegram/ThirdParty/extra-cmake-modules"]
|
||||
path = Telegram/ThirdParty/extra-cmake-modules
|
||||
url = https://github.com/KDE/extra-cmake-modules.git
|
||||
[submodule "Telegram/ThirdParty/plasma-wayland-protocols"]
|
||||
path = Telegram/ThirdParty/plasma-wayland-protocols
|
||||
url = https://github.com/KDE/plasma-wayland-protocols.git
|
||||
[submodule "Telegram/ThirdParty/wayland-protocols"]
|
||||
path = Telegram/ThirdParty/wayland-protocols
|
||||
url = https://github.com/gitlab-freedesktop-mirrors/wayland-protocols.git
|
||||
|
||||
2
LEGAL
2
LEGAL
@@ -1,7 +1,7 @@
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
Copyright (c) 2014-2021 The Telegram Desktop Authors.
|
||||
Copyright (c) 2014-2022 The Telegram Desktop Authors.
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -525,33 +525,16 @@ PRIVATE
|
||||
editor/controllers/controllers.h
|
||||
editor/controllers/stickers_panel_controller.cpp
|
||||
editor/controllers/stickers_panel_controller.h
|
||||
editor/controllers/undo_controller.cpp
|
||||
editor/controllers/undo_controller.h
|
||||
editor/editor_crop.cpp
|
||||
editor/editor_crop.h
|
||||
editor/editor_paint.cpp
|
||||
editor/editor_paint.h
|
||||
editor/photo_editor.cpp
|
||||
editor/photo_editor.h
|
||||
editor/photo_editor_common.cpp
|
||||
editor/photo_editor_common.h
|
||||
editor/photo_editor_content.cpp
|
||||
editor/photo_editor_content.h
|
||||
editor/photo_editor_controls.cpp
|
||||
editor/photo_editor_controls.h
|
||||
editor/photo_editor_inner_common.h
|
||||
editor/photo_editor_layer_widget.cpp
|
||||
editor/photo_editor_layer_widget.h
|
||||
editor/scene/scene.cpp
|
||||
editor/scene/scene.h
|
||||
editor/scene/scene_item_base.cpp
|
||||
editor/scene/scene_item_base.h
|
||||
editor/scene/scene_item_canvas.cpp
|
||||
editor/scene/scene_item_canvas.h
|
||||
editor/scene/scene_item_image.cpp
|
||||
editor/scene/scene_item_image.h
|
||||
editor/scene/scene_item_line.cpp
|
||||
editor/scene/scene_item_line.h
|
||||
editor/scene/scene_item_sticker.cpp
|
||||
editor/scene/scene_item_sticker.h
|
||||
export/export_manager.cpp
|
||||
@@ -655,6 +638,8 @@ PRIVATE
|
||||
history/view/history_view_pinned_section.h
|
||||
history/view/history_view_pinned_tracker.cpp
|
||||
history/view/history_view_pinned_tracker.h
|
||||
history/view/history_view_react_animation.cpp
|
||||
history/view/history_view_react_animation.h
|
||||
history/view/history_view_react_button.cpp
|
||||
history/view/history_view_react_button.h
|
||||
history/view/history_view_reactions.cpp
|
||||
@@ -948,6 +933,8 @@ PRIVATE
|
||||
platform/linux/file_utilities_linux.h
|
||||
platform/linux/launcher_linux.cpp
|
||||
platform/linux/launcher_linux.h
|
||||
platform/linux/integration_linux.cpp
|
||||
platform/linux/integration_linux.h
|
||||
platform/linux/main_window_linux.cpp
|
||||
platform/linux/main_window_linux.h
|
||||
platform/linux/notifications_manager_linux_dummy.cpp
|
||||
@@ -959,6 +946,8 @@ PRIVATE
|
||||
platform/mac/file_utilities_mac.h
|
||||
platform/mac/launcher_mac.mm
|
||||
platform/mac/launcher_mac.h
|
||||
platform/mac/integration_mac.mm
|
||||
platform/mac/integration_mac.h
|
||||
platform/mac/mac_iconv_helper.c
|
||||
platform/mac/main_window_mac.mm
|
||||
platform/mac/main_window_mac.h
|
||||
@@ -993,6 +982,8 @@ PRIVATE
|
||||
platform/win/file_utilities_win.h
|
||||
platform/win/launcher_win.cpp
|
||||
platform/win/launcher_win.h
|
||||
platform/win/integration_win.cpp
|
||||
platform/win/integration_win.h
|
||||
platform/win/main_window_win.cpp
|
||||
platform/win/main_window_win.h
|
||||
platform/win/notifications_manager_win.cpp
|
||||
@@ -1003,8 +994,6 @@ PRIVATE
|
||||
platform/win/windows_app_user_model_id.h
|
||||
platform/win/windows_dlls.cpp
|
||||
platform/win/windows_dlls.h
|
||||
platform/win/windows_event_filter.cpp
|
||||
platform/win/windows_event_filter.h
|
||||
platform/win/windows_autostart_task.cpp
|
||||
platform/win/windows_autostart_task.h
|
||||
platform/win/windows_toast_activator.cpp
|
||||
@@ -1012,6 +1001,8 @@ PRIVATE
|
||||
platform/platform_audio.h
|
||||
platform/platform_file_utilities.h
|
||||
platform/platform_launcher.h
|
||||
platform/platform_integration.cpp
|
||||
platform/platform_integration.h
|
||||
platform/platform_main_window.h
|
||||
platform/platform_notifications_manager.h
|
||||
platform/platform_specific.h
|
||||
|
||||
@@ -348,6 +348,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_notification_sample" = "This is a sample notification";
|
||||
"lng_notification_reminder" = "Reminder";
|
||||
|
||||
"lng_reaction_text" = "{reaction} to your «{text}»";
|
||||
"lng_reaction_notext" = "{reaction} to your message";
|
||||
"lng_reaction_photo" = "{reaction} to your photo";
|
||||
"lng_reaction_video" = "{reaction} to your video";
|
||||
"lng_reaction_video_message" = "{reaction} to your video message";
|
||||
"lng_reaction_document" = "{reaction} to your file";
|
||||
"lng_reaction_sticker" = "{reaction} to your {emoji}sticker";
|
||||
"lng_reaction_voice_message" = "{reaction} to your voice message";
|
||||
"lng_reaction_contact" = "{reaction} to your contact {name}";
|
||||
"lng_reaction_location" = "{reaction} to your map";
|
||||
"lng_reaction_live_location" = "{reaction} to your live location";
|
||||
"lng_reaction_poll" = "{reaction} to your poll {title}";
|
||||
"lng_reaction_quiz" = "{reaction} to your quiz {title}";
|
||||
"lng_reaction_game" = "{reaction} to your game";
|
||||
"lng_reaction_invoice" = "{reaction} to your invoice";
|
||||
"lng_reaction_gif" = "{reaction} to your GIF";
|
||||
|
||||
"lng_settings_section_general" = "General";
|
||||
"lng_settings_change_lang" = "Change language";
|
||||
"lng_languages" = "Languages";
|
||||
@@ -440,7 +457,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_phone_label" = "Phone number";
|
||||
"lng_settings_username_add" = "Add username";
|
||||
"lng_settings_close_sure" = "Are you sure you want to close this page? You didn't save your changes.";
|
||||
//"lng_settings_peer_to_peer" = "Peer-to-Peer";
|
||||
"lng_settings_peer_to_peer_about" = "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but may slightly decrease audio quality.";
|
||||
"lng_settings_advanced" = "Advanced";
|
||||
"lng_settings_stickers_emoji" = "Stickers and emoji";
|
||||
@@ -917,8 +933,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_profile_files_header" = "Files";
|
||||
"lng_profile_audios#one" = "{count} voice message";
|
||||
"lng_profile_audios#other" = "{count} voice messages";
|
||||
//"lng_profile_rounds#one" = "{count} video message";
|
||||
//"lng_profile_rounds#other" = "{count} video messages";
|
||||
"lng_profile_audios_header" = "Voice messages";
|
||||
"lng_profile_shared_links#one" = "{count} shared link";
|
||||
"lng_profile_shared_links#other" = "{count} shared links";
|
||||
@@ -978,8 +992,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_media_selected_file#other" = "{count} Files";
|
||||
"lng_media_selected_audio#one" = "{count} Voice message";
|
||||
"lng_media_selected_audio#other" = "{count} Voice messages";
|
||||
//"lng_media_selected_round#one" = "{count} Video message";
|
||||
//"lng_media_selected_round#other" = "{count} Video messages";
|
||||
"lng_media_selected_link#one" = "{count} Shared link";
|
||||
"lng_media_selected_link#other" = "{count} Shared links";
|
||||
"lng_media_photo_empty" = "No photos here yet";
|
||||
@@ -1259,10 +1271,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_action_you_proximity_reached" = "You are now within {distance} from {user}";
|
||||
"lng_action_you_theme_changed" = "You changed chat theme to {emoji}";
|
||||
"lng_action_theme_changed" = "{from} changed chat theme to {emoji}";
|
||||
"lng_action_theme_changed_channel" = "Channel theme changed to {emoji}";
|
||||
"lng_action_you_theme_disabled" = "You disabled chat theme";
|
||||
"lng_action_theme_disabled" = "{from} disabled chat theme";
|
||||
"lng_action_theme_disabled_channel" = "Channel theme disabled";
|
||||
"lng_action_proximity_distance_m#one" = "{count} meter";
|
||||
"lng_action_proximity_distance_m#other" = "{count} metres";
|
||||
"lng_action_proximity_distance_km#one" = "{count} km";
|
||||
@@ -1729,7 +1739,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_send_message" = "Send message";
|
||||
"lng_context_view_group" = "View group info";
|
||||
"lng_context_view_channel" = "View channel info";
|
||||
//"lng_context_view_feed_info" = "View feed info";
|
||||
"lng_context_hide_psa" = "Hide this announcement";
|
||||
"lng_context_pin_to_top" = "Pin to top";
|
||||
"lng_context_unpin_from_top" = "Unpin from top";
|
||||
@@ -1745,6 +1754,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_archive_to_list" = "Move to chats list";
|
||||
"lng_context_archive_to_menu_info" = "Archive moved to the main menu!\nYou can return it from the context menu of the archive button.";
|
||||
|
||||
"lng_context_mute" = "Mute notifications";
|
||||
"lng_context_unmute" = "Unmute";
|
||||
|
||||
"lng_context_promote_admin" = "Promote to admin";
|
||||
"lng_context_edit_permissions" = "Edit permissions";
|
||||
"lng_context_restrict_user" = "Restrict user";
|
||||
@@ -1807,6 +1819,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_context_seen_reacted#other" = "{count} Reacted";
|
||||
"lng_context_seen_reacted_none" = "Nobody Reacted";
|
||||
"lng_context_seen_reacted_all" = "Show All Reactions";
|
||||
"lng_context_set_as_quick" = "Set As Quick";
|
||||
|
||||
"lng_send_image_empty" = "Could not send an empty file: {name}";
|
||||
"lng_send_image_too_large" = "Could not send a file, because it is larger than 1500 MB: {name}";
|
||||
@@ -1829,7 +1842,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_forward_share_contact" = "Share contact to {recipient}?";
|
||||
"lng_forward_share_cant" = "Sorry, no way to share contact here :(";
|
||||
|
||||
|
||||
"lng_forward_send_files_cant" = "Sorry, no way to send media here :(";
|
||||
"lng_forward_send" = "Send";
|
||||
"lng_forward_messages#one" = "{count} forwarded message";
|
||||
@@ -1977,6 +1989,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_formatting_link_url" = "URL";
|
||||
"lng_formatting_link_create" = "Create";
|
||||
|
||||
"lng_text_copied" = "Text copied to clipboard.";
|
||||
|
||||
"lng_spellchecker_submenu" = "Spelling";
|
||||
"lng_spellchecker_add" = "Add to Dictionary";
|
||||
"lng_spellchecker_remove" = "Remove from Dictionary";
|
||||
|
||||
@@ -1313,7 +1313,7 @@ messageUserReaction#932844fa user_id:long reaction:string = MessageUserReaction;
|
||||
|
||||
messages.messageReactionsList#a366923c flags:# count:int reactions:Vector<MessageUserReaction> users:Vector<User> next_offset:flags.0?string = messages.MessageReactionsList;
|
||||
|
||||
availableReaction#21d7c4b flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document = AvailableReaction;
|
||||
availableReaction#c077ec01 flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction;
|
||||
|
||||
messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions;
|
||||
messages.availableReactions#768e3aad hash:int reactions:Vector<AvailableReaction> = messages.AvailableReactions;
|
||||
@@ -1750,4 +1750,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel
|
||||
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
|
||||
|
||||
// LAYER 136
|
||||
// LAYER 137
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="3.4.0.0" />
|
||||
Version="3.4.5.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 3,4,0,0
|
||||
PRODUCTVERSION 3,4,0,0
|
||||
FILEVERSION 3,4,5,0
|
||||
PRODUCTVERSION 3,4,5,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "3.4.0.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "FileVersion", "3.4.5.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2022"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "3.4.0.0"
|
||||
VALUE "ProductVersion", "3.4.5.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 3,4,0,0
|
||||
PRODUCTVERSION 3,4,0,0
|
||||
FILEVERSION 3,4,5,0
|
||||
PRODUCTVERSION 3,4,5,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "3.4.0.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
|
||||
VALUE "FileVersion", "3.4.5.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2022"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "3.4.0.0"
|
||||
VALUE "ProductVersion", "3.4.5.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -27,7 +27,7 @@ EntitiesInText EntitiesFromMTP(
|
||||
for (const auto &entity : entities) {
|
||||
switch (entity.type()) {
|
||||
case mtpc_messageEntityUrl: { auto &d = entity.c_messageEntityUrl(); result.push_back({ EntityType::Url, d.voffset().v, d.vlength().v }); } break;
|
||||
case mtpc_messageEntityTextUrl: { auto &d = entity.c_messageEntityTextUrl(); result.push_back({ EntityType::CustomUrl, d.voffset().v, d.vlength().v, Clean(qs(d.vurl())) }); } break;
|
||||
case mtpc_messageEntityTextUrl: { auto &d = entity.c_messageEntityTextUrl(); result.push_back({ EntityType::CustomUrl, d.voffset().v, d.vlength().v, qs(d.vurl()) }); } break;
|
||||
case mtpc_messageEntityEmail: { auto &d = entity.c_messageEntityEmail(); result.push_back({ EntityType::Email, d.voffset().v, d.vlength().v }); } break;
|
||||
case mtpc_messageEntityHashtag: { auto &d = entity.c_messageEntityHashtag(); result.push_back({ EntityType::Hashtag, d.voffset().v, d.vlength().v }); } break;
|
||||
case mtpc_messageEntityCashtag: { auto &d = entity.c_messageEntityCashtag(); result.push_back({ EntityType::Cashtag, d.voffset().v, d.vlength().v }); } break;
|
||||
@@ -71,7 +71,7 @@ EntitiesInText EntitiesFromMTP(
|
||||
case mtpc_messageEntityUnderline: { auto &d = entity.c_messageEntityUnderline(); result.push_back({ EntityType::Underline, d.voffset().v, d.vlength().v }); } break;
|
||||
case mtpc_messageEntityStrike: { auto &d = entity.c_messageEntityStrike(); result.push_back({ EntityType::StrikeOut, d.voffset().v, d.vlength().v }); } break;
|
||||
case mtpc_messageEntityCode: { auto &d = entity.c_messageEntityCode(); result.push_back({ EntityType::Code, d.voffset().v, d.vlength().v }); } break;
|
||||
case mtpc_messageEntityPre: { auto &d = entity.c_messageEntityPre(); result.push_back({ EntityType::Pre, d.voffset().v, d.vlength().v, Clean(qs(d.vlanguage())) }); } break;
|
||||
case mtpc_messageEntityPre: { auto &d = entity.c_messageEntityPre(); result.push_back({ EntityType::Pre, d.voffset().v, d.vlength().v, qs(d.vlanguage()) }); } break;
|
||||
case mtpc_messageEntityBankCard: break; // Skipping cards.
|
||||
case mtpc_messageEntitySpoiler: { auto &d = entity.c_messageEntitySpoiler(); result.push_back({ EntityType::Spoiler, d.voffset().v, d.vlength().v }); } break;
|
||||
// #TODO entities
|
||||
|
||||
@@ -31,34 +31,58 @@ namespace {
|
||||
|
||||
constexpr auto kContextReactionsLimit = 50;
|
||||
|
||||
struct Peers {
|
||||
std::vector<PeerId> list;
|
||||
bool unknown = false;
|
||||
};
|
||||
inline bool operator==(const Peers &a, const Peers &b) noexcept {
|
||||
return (a.list == b.list) && (a.unknown == b.unknown);
|
||||
}
|
||||
|
||||
struct PeerWithReaction {
|
||||
PeerId peer = 0;
|
||||
QString reaction;
|
||||
};
|
||||
bool operator==(const PeerWithReaction &a, const PeerWithReaction &b) {
|
||||
inline bool operator==(
|
||||
const PeerWithReaction &a,
|
||||
const PeerWithReaction &b) noexcept {
|
||||
return (a.peer == b.peer) && (a.reaction == b.reaction);
|
||||
}
|
||||
|
||||
struct PeersWithReactions {
|
||||
std::vector<PeerWithReaction> list;
|
||||
int fullReactionsCount = 0;
|
||||
bool unknown = false;
|
||||
};
|
||||
inline bool operator==(
|
||||
const PeersWithReactions &a,
|
||||
const PeersWithReactions &b) noexcept {
|
||||
return (a.fullReactionsCount == b.fullReactionsCount)
|
||||
&& (a.list == b.list)
|
||||
&& (a.unknown == b.unknown);
|
||||
}
|
||||
|
||||
struct CachedRead {
|
||||
explicit CachedRead(PeerId unknownFlag)
|
||||
: list(std::vector<PeerId>{ unknownFlag }) {
|
||||
CachedRead()
|
||||
: data(Peers{ .unknown = true }) {
|
||||
}
|
||||
rpl::variable<std::vector<PeerId>> list;
|
||||
rpl::variable<Peers> data;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
struct CachedReacted {
|
||||
explicit CachedReacted(PeerId unknownFlag)
|
||||
: list(
|
||||
std::vector<PeerWithReaction>{ PeerWithReaction{ unknownFlag } }) {
|
||||
CachedReacted()
|
||||
: data(PeersWithReactions{ .unknown = true }) {
|
||||
}
|
||||
rpl::variable<std::vector<PeerWithReaction>> list;
|
||||
rpl::variable<PeersWithReactions> data;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
|
||||
struct Context {
|
||||
base::flat_map<not_null<HistoryItem*>, CachedRead> cachedRead;
|
||||
base::flat_map<not_null<HistoryItem*>, CachedReacted> cachedReacted;
|
||||
base::flat_map<
|
||||
not_null<HistoryItem*>,
|
||||
base::flat_map<QString, CachedReacted>> cachedReacted;
|
||||
base::flat_map<not_null<Main::Session*>, rpl::lifetime> subscriptions;
|
||||
|
||||
[[nodiscard]] CachedRead &cacheRead(not_null<HistoryItem*> item) {
|
||||
@@ -66,21 +90,18 @@ struct Context {
|
||||
if (i != end(cachedRead)) {
|
||||
return i->second;
|
||||
}
|
||||
return cachedRead.emplace(
|
||||
item,
|
||||
CachedRead(item->history()->session().userPeerId())
|
||||
).first->second;
|
||||
return cachedRead.emplace(item, CachedRead()).first->second;
|
||||
}
|
||||
|
||||
[[nodiscard]] CachedReacted &cacheReacted(not_null<HistoryItem*> item) {
|
||||
const auto i = cachedReacted.find(item);
|
||||
if (i != end(cachedReacted)) {
|
||||
[[nodiscard]] CachedReacted &cacheReacted(
|
||||
not_null<HistoryItem*> item,
|
||||
const QString &reaction) {
|
||||
auto &map = cachedReacted[item];
|
||||
const auto i = map.find(reaction);
|
||||
if (i != end(map)) {
|
||||
return i->second;
|
||||
}
|
||||
return cachedReacted.emplace(
|
||||
item,
|
||||
CachedReacted(item->history()->session().userPeerId())
|
||||
).first->second;
|
||||
return map.emplace(reaction, CachedReacted()).first->second;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,9 +145,11 @@ struct State {
|
||||
item->history()->session().api().request(requestId).cancel();
|
||||
}
|
||||
}
|
||||
for (auto &[item, entry] : i->second->cachedReacted) {
|
||||
if (const auto requestId = entry.requestId) {
|
||||
item->history()->session().api().request(requestId).cancel();
|
||||
for (auto &[item, map] : i->second->cachedReacted) {
|
||||
for (auto &[reaction, entry] : map) {
|
||||
if (const auto requestId = entry.requestId) {
|
||||
item->history()->session().api().request(requestId).cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
contexts.erase(i);
|
||||
@@ -134,7 +157,9 @@ struct State {
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Context*> PreparedContextAt(not_null<QWidget*> key, not_null<Main::Session*> session) {
|
||||
[[nodiscard]] not_null<Context*> PreparedContextAt(
|
||||
not_null<QWidget*> key,
|
||||
not_null<Main::Session*> session) {
|
||||
const auto context = ContextAt(key);
|
||||
if (context->subscriptions.contains(session)) {
|
||||
return context;
|
||||
@@ -149,7 +174,9 @@ struct State {
|
||||
}
|
||||
const auto j = context->cachedReacted.find(update.item);
|
||||
if (j != end(context->cachedReacted)) {
|
||||
session->api().request(j->second.requestId).cancel();
|
||||
for (auto &[reaction, entry] : j->second) {
|
||||
session->api().request(entry.requestId).cancel();
|
||||
}
|
||||
context->cachedReacted.erase(j);
|
||||
}
|
||||
}, context->subscriptions[session]);
|
||||
@@ -163,21 +190,6 @@ struct State {
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ListUnknown(
|
||||
const std::vector<PeerId> &list,
|
||||
not_null<HistoryItem*> item) {
|
||||
return (list.size() == 1)
|
||||
&& (list.front() == item->history()->session().userPeerId());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ListUnknown(
|
||||
const std::vector<PeerWithReaction> &list,
|
||||
not_null<HistoryItem*> item) {
|
||||
return (list.size() == 1)
|
||||
&& list.front().reaction.isEmpty()
|
||||
&& (list.front().peer == item->history()->session().userPeerId());
|
||||
}
|
||||
|
||||
[[nodiscard]] Ui::WhoReadType DetectSeenType(not_null<HistoryItem*> item) {
|
||||
if (const auto media = item->media()) {
|
||||
if (!media->webpage()) {
|
||||
@@ -193,7 +205,7 @@ struct State {
|
||||
return Ui::WhoReadType::Seen;
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<PeerId>> WhoReadIds(
|
||||
[[nodiscard]] rpl::producer<Peers> WhoReadIds(
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<QWidget*> context) {
|
||||
auto weak = QPointer<QWidget>(context.get());
|
||||
@@ -213,33 +225,37 @@ struct State {
|
||||
).done([=](const MTPVector<MTPlong> &result) {
|
||||
auto &entry = context->cacheRead(item);
|
||||
entry.requestId = 0;
|
||||
auto peers = std::vector<PeerId>();
|
||||
peers.reserve(std::max(int(result.v.size()), 1));
|
||||
auto parsed = Peers();
|
||||
parsed.list.reserve(result.v.size());
|
||||
for (const auto &id : result.v) {
|
||||
peers.push_back(UserId(id));
|
||||
parsed.list.push_back(UserId(id));
|
||||
}
|
||||
entry.list = std::move(peers);
|
||||
entry.data = std::move(parsed);
|
||||
}).fail([=] {
|
||||
auto &entry = context->cacheRead(item);
|
||||
entry.requestId = 0;
|
||||
if (ListUnknown(entry.list.current(), item)) {
|
||||
entry.list = std::vector<PeerId>();
|
||||
if (entry.data.current().unknown) {
|
||||
entry.data = Peers();
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
return entry.list.value().start_existing(consumer);
|
||||
return entry.data.value().start_existing(consumer);
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector < PeerWithReaction> WithEmptyReactions(
|
||||
const std::vector<PeerId> &peers) {
|
||||
return peers | ranges::views::transform([](PeerId peer) {
|
||||
return PeerWithReaction{ .peer = peer };
|
||||
}) | ranges::to_vector;
|
||||
[[nodiscard]] PeersWithReactions WithEmptyReactions(
|
||||
const Peers &peers) {
|
||||
return PeersWithReactions{
|
||||
.list = peers.list | ranges::views::transform([](PeerId peer) {
|
||||
return PeerWithReaction{.peer = peer };
|
||||
}) | ranges::to_vector,
|
||||
.unknown = peers.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<PeerWithReaction>> WhoReactedIds(
|
||||
[[nodiscard]] rpl::producer<PeersWithReactions> WhoReactedIds(
|
||||
not_null<HistoryItem*> item,
|
||||
const QString &reaction,
|
||||
not_null<QWidget*> context) {
|
||||
auto weak = QPointer<QWidget>(context.get());
|
||||
const auto session = &item->history()->session();
|
||||
@@ -248,65 +264,69 @@ struct State {
|
||||
return rpl::lifetime();
|
||||
}
|
||||
const auto context = PreparedContextAt(weak.data(), session);
|
||||
auto &entry = context->cacheReacted(item);
|
||||
auto &entry = context->cacheReacted(item, reaction);
|
||||
if (!entry.requestId) {
|
||||
using Flag = MTPmessages_GetMessageReactionsList::Flag;
|
||||
entry.requestId = session->api().request(
|
||||
MTPmessages_GetMessageReactionsList(
|
||||
MTP_flags(0),
|
||||
MTP_flags(reaction.isEmpty()
|
||||
? Flag(0)
|
||||
: Flag::f_reaction),
|
||||
item->history()->peer->input,
|
||||
MTP_int(item->id),
|
||||
MTPstring(), // reaction
|
||||
MTP_string(reaction),
|
||||
MTPstring(), // offset
|
||||
MTP_int(kContextReactionsLimit)
|
||||
)
|
||||
).done([=](const MTPmessages_MessageReactionsList &result) {
|
||||
auto &entry = context->cacheReacted(item);
|
||||
auto &entry = context->cacheReacted(item, reaction);
|
||||
entry.requestId = 0;
|
||||
|
||||
result.match([&](
|
||||
const MTPDmessages_messageReactionsList &data) {
|
||||
session->data().processUsers(data.vusers());
|
||||
|
||||
auto peers = std::vector<PeerWithReaction>();
|
||||
peers.reserve(data.vreactions().v.size());
|
||||
auto parsed = PeersWithReactions{
|
||||
.fullReactionsCount = data.vcount().v,
|
||||
};
|
||||
parsed.list.reserve(data.vreactions().v.size());
|
||||
for (const auto &vote : data.vreactions().v) {
|
||||
vote.match([&](const auto &data) {
|
||||
peers.push_back(PeerWithReaction{
|
||||
parsed.list.push_back(PeerWithReaction{
|
||||
.peer = peerFromUser(data.vuser_id()),
|
||||
.reaction = qs(data.vreaction()),
|
||||
});
|
||||
});
|
||||
}
|
||||
entry.list = std::move(peers);
|
||||
entry.data = std::move(parsed);
|
||||
});
|
||||
}).fail([=] {
|
||||
auto &entry = context->cacheReacted(item);
|
||||
auto &entry = context->cacheReacted(item, reaction);
|
||||
entry.requestId = 0;
|
||||
if (ListUnknown(entry.list.current(), item)) {
|
||||
entry.list = std::vector<PeerWithReaction>();
|
||||
if (entry.data.current().unknown) {
|
||||
entry.data = PeersWithReactions();
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
return entry.list.value().start_existing(consumer);
|
||||
return entry.data.value().start_existing(consumer);
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] auto WhoReadOrReactedIds(
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<QWidget*> context)
|
||||
-> rpl::producer<std::vector<PeerWithReaction>> {
|
||||
-> rpl::producer<PeersWithReactions> {
|
||||
return rpl::combine(
|
||||
WhoReactedIds(item, context),
|
||||
WhoReactedIds(item, QString(), context),
|
||||
WhoReadIds(item, context)
|
||||
) | rpl::map([=](
|
||||
std::vector<PeerWithReaction> reacted,
|
||||
std::vector<PeerId> read) {
|
||||
if (ListUnknown(reacted, item) || ListUnknown(read, item)) {
|
||||
return reacted;
|
||||
) | rpl::map([=](PeersWithReactions reacted, Peers read) {
|
||||
if (reacted.unknown || read.unknown) {
|
||||
return PeersWithReactions{ .unknown = true };
|
||||
}
|
||||
for (const auto &peer : read) {
|
||||
if (!ranges::contains(reacted, peer, &PeerWithReaction::peer)) {
|
||||
reacted.push_back({ .peer = peer });
|
||||
auto &list = reacted.list;
|
||||
for (const auto &peer : read.list) {
|
||||
if (!ranges::contains(list, peer, &PeerWithReaction::peer)) {
|
||||
list.push_back({ .peer = peer });
|
||||
}
|
||||
}
|
||||
return reacted;
|
||||
@@ -463,55 +483,72 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<QWidget*> context,
|
||||
const style::WhoRead &st) {
|
||||
return WhoReacted(item, QString(), context, st);
|
||||
}
|
||||
|
||||
rpl::producer<Ui::WhoReadContent> WhoReacted(
|
||||
not_null<HistoryItem*> item,
|
||||
const QString &reaction,
|
||||
not_null<QWidget*> context,
|
||||
const style::WhoRead &st) {
|
||||
const auto small = st.userpics.size;
|
||||
const auto large = st.photoSize;
|
||||
return [=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
|
||||
const auto resolveWhoRead = WhoReadExists(item);
|
||||
const auto resolveWhoRead = reaction.isEmpty() && WhoReadExists(item);
|
||||
|
||||
const auto state = lifetime.make_state<State>();
|
||||
const auto pushNext = [=] {
|
||||
consumer.put_next_copy(state->current);
|
||||
};
|
||||
|
||||
const auto resolveWhoReacted = item->canViewReactions();
|
||||
const auto resolveWhoReacted = !reaction.isEmpty()
|
||||
|| item->canViewReactions();
|
||||
auto idsWithReactions = (resolveWhoRead && resolveWhoReacted)
|
||||
? WhoReadOrReactedIds(item, context)
|
||||
: resolveWhoRead
|
||||
? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions))
|
||||
: WhoReactedIds(item, context);
|
||||
: WhoReactedIds(item, reaction, context);
|
||||
state->current.type = resolveWhoRead
|
||||
? DetectSeenType(item)
|
||||
: Ui::WhoReadType::Reacted;
|
||||
if (resolveWhoReacted) {
|
||||
const auto &list = item->reactions();
|
||||
state->current.fullReactionsCount = ranges::accumulate(
|
||||
list,
|
||||
0,
|
||||
ranges::plus{},
|
||||
[](const auto &pair) { return pair.second; });
|
||||
state->current.fullReactionsCount = reaction.isEmpty()
|
||||
? ranges::accumulate(
|
||||
list,
|
||||
0,
|
||||
ranges::plus{},
|
||||
[](const auto &pair) { return pair.second; })
|
||||
: list.contains(reaction)
|
||||
? list.find(reaction)->second
|
||||
: 0;
|
||||
|
||||
// #TODO reactions
|
||||
state->current.singleReaction = (list.size() == 1)
|
||||
state->current.singleReaction = !reaction.isEmpty()
|
||||
? reaction
|
||||
: (list.size() == 1)
|
||||
? list.front().first
|
||||
: QString();
|
||||
}
|
||||
std::move(
|
||||
idsWithReactions
|
||||
) | rpl::start_with_next([=](
|
||||
const std::vector<PeerWithReaction> &peers) {
|
||||
if (ListUnknown(peers, item)) {
|
||||
) | rpl::start_with_next([=](const PeersWithReactions &peers) {
|
||||
if (peers.unknown) {
|
||||
state->userpics.clear();
|
||||
consumer.put_next(Ui::WhoReadContent{
|
||||
.type = state->current.type,
|
||||
.fullReactionsCount = state->current.fullReactionsCount,
|
||||
.unknown = true,
|
||||
});
|
||||
return;
|
||||
} else if (UpdateUserpics(state, item, peers)) {
|
||||
}
|
||||
state->current.fullReactionsCount = peers.fullReactionsCount;
|
||||
if (UpdateUserpics(state, item, peers.list)) {
|
||||
RegenerateParticipants(state, small, large);
|
||||
pushNext();
|
||||
} else if (peers.empty()) {
|
||||
} else if (peers.list.empty()) {
|
||||
pushNext();
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
@@ -27,5 +27,10 @@ namespace Api {
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<QWidget*> context,
|
||||
const style::WhoRead &st); // Cache results for this lifetime.
|
||||
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhoReacted(
|
||||
not_null<HistoryItem*> item,
|
||||
const QString &reaction,
|
||||
not_null<QWidget*> context,
|
||||
const style::WhoRead &st); // Cache results for this lifetime.
|
||||
|
||||
} // namespace Api
|
||||
|
||||
@@ -73,6 +73,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "inline_bots/inline_bot_result.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "ui/item_text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/toasts/common_toasts.h"
|
||||
@@ -470,13 +471,14 @@ void ApiWrap::sendMessageFail(
|
||||
Ui::show(Box<Ui::InformBox>(
|
||||
PeerFloodErrorText(&session(), PeerFloodType::Send)));
|
||||
} else if (error.type() == qstr("USER_BANNED_IN_CHANNEL")) {
|
||||
const auto link = textcmdLink(
|
||||
session().createInternalLinkFull(qsl("spambot")),
|
||||
tr::lng_cant_more_info(tr::now));
|
||||
const auto link = Ui::Text::Link(
|
||||
tr::lng_cant_more_info(tr::now),
|
||||
session().createInternalLinkFull(qsl("spambot")));
|
||||
Ui::show(Box<Ui::InformBox>(tr::lng_error_public_groups_denied(
|
||||
tr::now,
|
||||
lt_more_info,
|
||||
link)));
|
||||
link,
|
||||
Ui::Text::WithEntities)));
|
||||
} else if (error.type().startsWith(qstr("SLOWMODE_WAIT_"))) {
|
||||
const auto chop = qstr("SLOWMODE_WAIT_").size();
|
||||
const auto left = base::StringViewMid(error.type(), chop).toInt();
|
||||
|
||||
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/special_buttons.h"
|
||||
#include "ui/special_fields.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/unread_badge.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "data/data_channel.h"
|
||||
@@ -112,16 +113,19 @@ style::InputField CreateBioFieldStyle() {
|
||||
return result;
|
||||
}
|
||||
|
||||
QString PeerFloodErrorText(
|
||||
TextWithEntities PeerFloodErrorText(
|
||||
not_null<Main::Session*> session,
|
||||
PeerFloodType type) {
|
||||
const auto link = textcmdLink(
|
||||
session->createInternalLinkFull(qsl("spambot")),
|
||||
tr::lng_cant_more_info(tr::now));
|
||||
if (type == PeerFloodType::InviteGroup) {
|
||||
return tr::lng_cant_invite_not_contact(tr::now, lt_more_info, link);
|
||||
}
|
||||
return tr::lng_cant_send_to_not_contact(tr::now, lt_more_info, link);
|
||||
const auto link = Ui::Text::Link(
|
||||
tr::lng_cant_more_info(tr::now),
|
||||
session->createInternalLinkFull(qsl("spambot")));
|
||||
return ((type == PeerFloodType::InviteGroup)
|
||||
? tr::lng_cant_invite_not_contact
|
||||
: tr::lng_cant_send_to_not_contact)(
|
||||
tr::now,
|
||||
lt_more_info,
|
||||
link,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
|
||||
void ShowAddParticipantsError(
|
||||
@@ -167,6 +171,14 @@ void ShowAddParticipantsError(
|
||||
}
|
||||
}
|
||||
const auto hasBot = ranges::any_of(users, &UserData::isBot);
|
||||
if (error == u"PEER_FLOOD"_q) {
|
||||
const auto type = (chat->isChat() || chat->isMegagroup())
|
||||
? PeerFloodType::InviteGroup
|
||||
: PeerFloodType::InviteChannel;
|
||||
const auto text = PeerFloodErrorText(&chat->session(), type);
|
||||
Ui::show(Box<Ui::InformBox>(text), Ui::LayerOption::KeepOther);
|
||||
return;
|
||||
}
|
||||
const auto text = [&] {
|
||||
if (error == u"USER_BOT"_q) {
|
||||
return tr::lng_cant_invite_bot_to_channel(tr::now);
|
||||
@@ -184,11 +196,6 @@ void ShowAddParticipantsError(
|
||||
return tr::lng_bot_already_in_group(tr::now);
|
||||
} else if (error == u"BOT_GROUPS_BLOCKED"_q) {
|
||||
return tr::lng_error_cant_add_bot(tr::now);
|
||||
} else if (error == u"PEER_FLOOD"_q) {
|
||||
const auto type = (chat->isChat() || chat->isMegagroup())
|
||||
? PeerFloodType::InviteGroup
|
||||
: PeerFloodType::InviteChannel;
|
||||
return PeerFloodErrorText(&chat->session(), type);
|
||||
} else if (error == u"ADMINS_TOO_MUCH"_q) {
|
||||
return ((chat->isChat() || chat->isMegagroup())
|
||||
? tr::lng_error_admin_limit
|
||||
@@ -1521,11 +1528,10 @@ RevokePublicLinkBox::Inner::Inner(
|
||||
st::contactsNameStyle,
|
||||
peer->name,
|
||||
Ui::NameTextOptions());
|
||||
row.status.setText(
|
||||
row.status.setMarkedText(
|
||||
st::defaultTextStyle,
|
||||
_session->createInternalLink(
|
||||
textcmdLink(1, peer->userName())),
|
||||
Ui::DialogTextOptions());
|
||||
Ui::Text::Link(peer->userName())));
|
||||
_rows.push_back(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ enum class PeerFloodType {
|
||||
|
||||
[[nodiscard]] style::InputField CreateBioFieldStyle();
|
||||
|
||||
[[nodiscard]] QString PeerFloodErrorText(
|
||||
[[nodiscard]] TextWithEntities PeerFloodErrorText(
|
||||
not_null<Main::Session*> session,
|
||||
PeerFloodType type);
|
||||
void ShowAddParticipantsError(
|
||||
|
||||
@@ -302,7 +302,7 @@ bool ServiceCheck::checkRippleStartPosition(QPoint position) const {
|
||||
base::unixtime::now(),
|
||||
out ? history->session().userId() : peerToUser(history->peer->id),
|
||||
QString(),
|
||||
TextWithEntities{ TextUtilities::Clean(text) },
|
||||
TextWithEntities{ text },
|
||||
MTP_messageMediaEmpty(),
|
||||
HistoryMessageMarkupData(),
|
||||
groupedId);
|
||||
|
||||
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/basic_click_handlers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
@@ -312,9 +313,12 @@ void ProxyRow::updateFields(View &&view) {
|
||||
}
|
||||
_view = std::move(view);
|
||||
const auto endpoint = _view.host + ':' + QString::number(_view.port);
|
||||
_title.setText(
|
||||
_title.setMarkedText(
|
||||
st::proxyRowTitleStyle,
|
||||
_view.type + ' ' + textcmdLink(1, endpoint),
|
||||
TextWithEntities()
|
||||
.append(_view.type)
|
||||
.append(' ')
|
||||
.append(Ui::Text::Link(endpoint, {})),
|
||||
Ui::ItemTextDefaultOptions());
|
||||
|
||||
const auto state = _view.state;
|
||||
|
||||
@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "main/main_session.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
@@ -190,9 +191,12 @@ not_null<Ui::FlatLabel*> CreateWarningLabel(
|
||||
const auto value = valueLimit - length;
|
||||
const auto shown = (value < warnLimit)
|
||||
&& (field->height() > st::createPollOptionField.heightMin);
|
||||
result->setRichText((value >= 0)
|
||||
? QString::number(value)
|
||||
: textcmdLink(1, QString::number(value)));
|
||||
if (value >= 0) {
|
||||
result->setText(QString::number(value));
|
||||
} else {
|
||||
result->setMarkedText(Ui::Text::PlainLink(
|
||||
QString::number(value)));
|
||||
}
|
||||
result->setVisible(shown);
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -28,8 +28,7 @@ namespace {
|
||||
TextParseOptions kInformBoxTextOptions = {
|
||||
(TextParseLinks
|
||||
| TextParseMultiline
|
||||
| TextParseMarkdown
|
||||
| TextParseRichText), // flags
|
||||
| TextParseMarkdown), // flags
|
||||
0, // maxw
|
||||
0, // maxh
|
||||
Qt::LayoutDirectionAuto, // dir
|
||||
|
||||
@@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "history/admin_log/history_admin_log_section.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
@@ -1060,7 +1061,7 @@ void Controller::fillManageSection() {
|
||||
!_peer->isBroadcast(),
|
||||
session->data().reactions().list(
|
||||
Data::Reactions::Type::Active),
|
||||
session->data().reactions().list(_peer),
|
||||
*Data::PeerAllowedReactions(_peer),
|
||||
done));
|
||||
},
|
||||
st::infoIconReactions);
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
@@ -32,6 +33,7 @@ void AddReactionIcon(
|
||||
not_null<DocumentData*> document) {
|
||||
struct State {
|
||||
std::shared_ptr<Data::DocumentMedia> media;
|
||||
std::unique_ptr<Lottie::Icon> icon;
|
||||
QImage image;
|
||||
};
|
||||
|
||||
@@ -45,37 +47,40 @@ void AddReactionIcon(
|
||||
button->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
icon->moveToLeft(
|
||||
st::settingsSectionIconLeft,
|
||||
st::editPeerReactionsIconLeft,
|
||||
(size.height() - icon->height()) / 2,
|
||||
size.width());
|
||||
}, icon->lifetime());
|
||||
|
||||
const auto setImage = [=](not_null<Image*> image) {
|
||||
state->image = Images::prepare(
|
||||
image->original(),
|
||||
size * style::DevicePixelRatio(),
|
||||
size * style::DevicePixelRatio(),
|
||||
Images::Option::Smooth | Images::Option::TransparentBackground,
|
||||
size,
|
||||
size);
|
||||
icon->update();
|
||||
const auto initLottie = [=] {
|
||||
state->icon = std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{
|
||||
.path = state->media->owner()->filepath(true),
|
||||
.json = state->media->bytes(),
|
||||
.sizeOverride = QSize(size, size),
|
||||
.frame = -1,
|
||||
});
|
||||
state->media = nullptr;
|
||||
};
|
||||
if (const auto image = state->media->getStickerLarge()) {
|
||||
setImage(image);
|
||||
state->media->checkStickerLarge();
|
||||
if (state->media->loaded()) {
|
||||
initLottie();
|
||||
} else {
|
||||
document->session().downloaderTaskFinished(
|
||||
) | rpl::map([=] {
|
||||
return state->media->getStickerLarge();
|
||||
}) | rpl::filter_nullptr() | rpl::take(
|
||||
1
|
||||
) | rpl::start_with_next([=](not_null<Image*> image) {
|
||||
setImage(image);
|
||||
}, button->lifetime());
|
||||
) | rpl::filter([=] {
|
||||
return state->media->loaded();
|
||||
}) | rpl::take(1) | rpl::start_with_next([=] {
|
||||
initLottie();
|
||||
icon->update();
|
||||
}, icon->lifetime());
|
||||
}
|
||||
|
||||
icon->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
Painter p(icon);
|
||||
QPainter p(icon);
|
||||
if (state->image.isNull() && state->icon) {
|
||||
state->image = state->icon->frame();
|
||||
crl::async([icon = std::move(state->icon)]{});
|
||||
}
|
||||
if (!state->image.isNull()) {
|
||||
p.drawImage(0, 0, state->image);
|
||||
}
|
||||
@@ -88,7 +93,7 @@ void EditAllowedReactionsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
bool isGroup,
|
||||
const std::vector<Reaction> &list,
|
||||
const std::vector<Reaction> &selected,
|
||||
const base::flat_set<QString> &selected,
|
||||
Fn<void(const std::vector<QString> &)> callback) {
|
||||
box->setTitle(tr::lng_manage_peer_reactions());
|
||||
|
||||
@@ -141,14 +146,16 @@ void EditAllowedReactionsBox(
|
||||
tr::lng_manage_peer_reactions_available());
|
||||
|
||||
const auto active = [&](const Data::Reaction &entry) {
|
||||
return ranges::contains(selected, entry.emoji, &Reaction::emoji);
|
||||
return selected.contains(entry.emoji);
|
||||
};
|
||||
const auto add = [&](const Data::Reaction &entry) {
|
||||
const auto button = Settings::AddButton(
|
||||
container,
|
||||
rpl::single(entry.title),
|
||||
st::manageGroupButton.button);
|
||||
AddReactionIcon(button, entry.staticIcon);
|
||||
AddReactionIcon(button, entry.centerIcon
|
||||
? entry.centerIcon
|
||||
: entry.appearAnimation.get());
|
||||
state->toggles.emplace(entry.emoji, button);
|
||||
button->toggleOn(rpl::single(
|
||||
active(entry)
|
||||
@@ -191,9 +198,9 @@ void SaveAllowedReactions(
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
peer->session().api().applyUpdates(result);
|
||||
if (const auto chat = peer->asChat()) {
|
||||
chat->setAllowedReactions(allowed);
|
||||
chat->setAllowedReactions({ begin(allowed), end(allowed) });
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
channel->setAllowedReactions(allowed);
|
||||
channel->setAllowedReactions({ begin(allowed), end(allowed) });
|
||||
} else {
|
||||
Unexpected("Invalid peer type in SaveAllowedReactions.");
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ void EditAllowedReactionsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
bool isGroup,
|
||||
const std::vector<Data::Reaction> &list,
|
||||
const std::vector<Data::Reaction> &selected,
|
||||
const base::flat_set<QString> &selected,
|
||||
Fn<void(const std::vector<QString> &)> callback);
|
||||
|
||||
void SaveAllowedReactions(
|
||||
|
||||
@@ -289,12 +289,9 @@ void RenameBox(not_null<Ui::GenericBox*> box) {
|
||||
Unexpected("Type in LottieForType.");
|
||||
}();
|
||||
const auto size = st::sessionBigLottieSize;
|
||||
static const auto kWhite = style::owned_color(Qt::white);
|
||||
return std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{
|
||||
.path = u":/icons/settings/devices/"_q + path + u".lottie"_q,
|
||||
.color = kWhite.color(),
|
||||
.sizeOverride = QSize(size, size),
|
||||
.frame = 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -360,7 +357,7 @@ void RenameBox(not_null<Ui::GenericBox*> box) {
|
||||
state->lottie->animate(
|
||||
[=] { result->update(); },
|
||||
0,
|
||||
state->lottie->framesCount());
|
||||
state->lottie->framesCount() - 1);
|
||||
}, result->lifetime());
|
||||
}
|
||||
|
||||
|
||||
@@ -247,6 +247,7 @@ private:
|
||||
int32 _rowHeight;
|
||||
|
||||
std::vector<std::unique_ptr<Row>> _rows;
|
||||
std::vector<std::unique_ptr<Row>> _oldRows;
|
||||
std::vector<crl::time> _shiftingStartTimes;
|
||||
crl::time _aboveShadowFadeStart = 0;
|
||||
anim::value _aboveShadowFadeOpacity;
|
||||
@@ -547,7 +548,9 @@ void StickersBox::prepare() {
|
||||
}
|
||||
setNoContentMargin(true);
|
||||
_tabs->sectionActivated(
|
||||
) | rpl::start_with_next(
|
||||
) | rpl::filter([=] {
|
||||
return !_ignoreTabActivation;
|
||||
}) | rpl::start_with_next(
|
||||
[this] { switchTab(); },
|
||||
lifetime());
|
||||
refreshTabs();
|
||||
@@ -665,12 +668,16 @@ void StickersBox::refreshTabs() {
|
||||
|| (_tab == &_featured && !_tabIndices.contains(Section::Featured))
|
||||
|| (_tab == &_masks && !_tabIndices.contains(Section::Masks))) {
|
||||
switchTab();
|
||||
} else if (_tab == &_archived) {
|
||||
_tabs->setActiveSectionFast(_tabIndices.indexOf(Section::Archived));
|
||||
} else if (_tab == &_featured) {
|
||||
_tabs->setActiveSectionFast(_tabIndices.indexOf(Section::Featured));
|
||||
} else if (_tab == &_masks) {
|
||||
_tabs->setActiveSectionFast(_tabIndices.indexOf(Section::Masks));
|
||||
} else {
|
||||
_ignoreTabActivation = true;
|
||||
_tabs->setActiveSectionFast(_tabIndices.indexOf((_tab == &_archived)
|
||||
? Section::Archived
|
||||
: (_tab == &_featured)
|
||||
? Section::Featured
|
||||
: (_tab == &_masks)
|
||||
? Section::Masks
|
||||
: Section::Installed));
|
||||
_ignoreTabActivation = false;
|
||||
}
|
||||
updateTabsGeometry();
|
||||
}
|
||||
@@ -1987,6 +1994,7 @@ void StickersBox::Inner::rebuild(bool masks) {
|
||||
|
||||
auto maxNameWidth = countMaxNameWidth();
|
||||
|
||||
_oldRows = std::move(_rows);
|
||||
clear();
|
||||
const auto &order = ([&]() -> const StickersSetsOrder & {
|
||||
if (_section == Section::Installed) {
|
||||
@@ -2038,6 +2046,7 @@ void StickersBox::Inner::rebuild(bool masks) {
|
||||
set->accessHash);
|
||||
}
|
||||
}
|
||||
_oldRows.clear();
|
||||
session().api().requestStickerSets();
|
||||
updateSize();
|
||||
}
|
||||
@@ -2150,19 +2159,55 @@ void StickersBox::Inner::rebuildAppendSet(
|
||||
QString title = fillSetTitle(set, maxNameWidth, &titleWidth);
|
||||
int count = fillSetCount(set);
|
||||
|
||||
_rows.push_back(std::make_unique<Row>(
|
||||
set,
|
||||
sticker,
|
||||
count,
|
||||
title,
|
||||
titleWidth,
|
||||
installed,
|
||||
official,
|
||||
unread,
|
||||
archived,
|
||||
removed,
|
||||
pixw,
|
||||
pixh));
|
||||
const auto existing = [&]{
|
||||
const auto now = int(_rows.size());
|
||||
const auto setProj = [](const std::unique_ptr<Row> &row) {
|
||||
return row ? row->set.get() : nullptr;
|
||||
};
|
||||
if (_oldRows.size() > now
|
||||
&& setProj(_oldRows[now]) == set.get()) {
|
||||
return _oldRows.begin() + now;
|
||||
}
|
||||
return ranges::find(_oldRows, set.get(), setProj);
|
||||
}();
|
||||
if (existing != end(_oldRows)) {
|
||||
const auto raw = existing->get();
|
||||
raw->sticker = sticker;
|
||||
raw->count = count;
|
||||
raw->title = title;
|
||||
raw->titleWidth = titleWidth;
|
||||
raw->installed = installed;
|
||||
raw->official = official;
|
||||
raw->unread = unread;
|
||||
raw->archived = archived;
|
||||
raw->removed = removed;
|
||||
raw->pixw = pixw;
|
||||
raw->pixh = pixh;
|
||||
raw->yadd = {};
|
||||
auto oldStickerMedia = std::move(raw->stickerMedia);
|
||||
auto oldThumbnailMedia = std::move(raw->thumbnailMedia);
|
||||
raw->stickerMedia = sticker->activeMediaView();
|
||||
raw->thumbnailMedia = set->activeThumbnailView();
|
||||
if (raw->thumbnailMedia != oldThumbnailMedia
|
||||
|| (!raw->thumbnailMedia && raw->stickerMedia != oldStickerMedia)) {
|
||||
raw->lottie = nullptr;
|
||||
}
|
||||
_rows.push_back(std::move(*existing));
|
||||
} else {
|
||||
_rows.push_back(std::make_unique<Row>(
|
||||
set,
|
||||
sticker,
|
||||
count,
|
||||
title,
|
||||
titleWidth,
|
||||
installed,
|
||||
official,
|
||||
unread,
|
||||
archived,
|
||||
removed,
|
||||
pixw,
|
||||
pixh));
|
||||
}
|
||||
_shiftingStartTimes.push_back(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ private:
|
||||
|
||||
object_ptr<Ui::SettingsSlider> _tabs = { nullptr };
|
||||
QList<Section> _tabIndices;
|
||||
bool _ignoreTabActivation = false;
|
||||
|
||||
class CounterWidget;
|
||||
object_ptr<CounterWidget> _unreadBadge = { nullptr };
|
||||
|
||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
@@ -255,11 +256,11 @@ not_null<Ui::RpWidget*> UrlAuthBox::setupContent(
|
||||
tr::lng_url_auth_open_confirm(tr::now, lt_link, url),
|
||||
st::boxLabel),
|
||||
st::boxPadding);
|
||||
const auto addCheckbox = [&](const QString &text) {
|
||||
const auto addCheckbox = [&](const TextWithEntities &text) {
|
||||
const auto checkbox = result->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
result,
|
||||
QString(),
|
||||
text,
|
||||
true,
|
||||
st::urlAuthCheckbox),
|
||||
style::margins(
|
||||
@@ -268,23 +269,22 @@ not_null<Ui::RpWidget*> UrlAuthBox::setupContent(
|
||||
st::boxPadding.right(),
|
||||
st::boxPadding.bottom()));
|
||||
checkbox->setAllowTextLines();
|
||||
checkbox->setText(text, true);
|
||||
return checkbox;
|
||||
};
|
||||
const auto auth = addCheckbox(
|
||||
tr::lng_url_auth_login_option(
|
||||
tr::now,
|
||||
lt_domain,
|
||||
textcmdStartSemibold() + domain + textcmdStopSemibold(),
|
||||
Ui::Text::Bold(domain),
|
||||
lt_user,
|
||||
(textcmdStartSemibold()
|
||||
+ session->user()->name
|
||||
+ textcmdStopSemibold())));
|
||||
Ui::Text::Bold(session->user()->name),
|
||||
Ui::Text::WithEntities));
|
||||
const auto allow = bot
|
||||
? addCheckbox(tr::lng_url_auth_allow_messages(
|
||||
tr::now,
|
||||
lt_bot,
|
||||
textcmdStartSemibold() + bot->firstName + textcmdStopSemibold()))
|
||||
Ui::Text::Bold(bot->firstName),
|
||||
Ui::Text::WithEntities))
|
||||
: nullptr;
|
||||
if (allow) {
|
||||
rpl::single(
|
||||
|
||||
@@ -1195,7 +1195,10 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
||||
const auto admin = IsGroupCallAdmin(_peer, participantPeer);
|
||||
const auto session = &_peer->session();
|
||||
const auto getCurrentWindow = [=]() -> Window::SessionController* {
|
||||
if (const auto window = Core::App().activeWindow()) {
|
||||
if (const auto window = Core::App().separateWindowForPeer(
|
||||
participantPeer)) {
|
||||
return window->sessionController();
|
||||
} else if (const auto window = Core::App().activeWindow()) {
|
||||
if (const auto controller = window->sessionController()) {
|
||||
if (&controller->session() == session) {
|
||||
return controller;
|
||||
@@ -1221,7 +1224,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
||||
? st::groupCallPopupMenuWithVolume
|
||||
: st::groupCallPopupMenu));
|
||||
const auto weakMenu = Ui::MakeWeak(result.get());
|
||||
const auto performOnMainWindow = [=](auto callback) {
|
||||
const auto withActiveWindow = [=](auto callback) {
|
||||
if (const auto window = getWindow()) {
|
||||
if (const auto menu = weakMenu.data()) {
|
||||
menu->discardParentReActivate();
|
||||
@@ -1236,12 +1239,12 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
|
||||
}
|
||||
};
|
||||
const auto showProfile = [=] {
|
||||
performOnMainWindow([=](not_null<Window::SessionController*> window) {
|
||||
withActiveWindow([=](not_null<Window::SessionController*> window) {
|
||||
window->showPeerInfo(participantPeer);
|
||||
});
|
||||
};
|
||||
const auto showHistory = [=] {
|
||||
performOnMainWindow([=](not_null<Window::SessionController*> window) {
|
||||
withActiveWindow([=](not_null<Window::SessionController*> window) {
|
||||
window->showPeerHistory(
|
||||
participantPeer,
|
||||
Window::SectionShow::Way::Forward);
|
||||
|
||||
@@ -108,7 +108,7 @@ private:
|
||||
};
|
||||
|
||||
TextParseOptions MenuTextOptions = {
|
||||
TextParseLinks | TextParseRichText, // flags
|
||||
TextParseLinks, // flags
|
||||
0, // maxw
|
||||
0, // maxh
|
||||
Qt::LayoutDirectionAuto, // dir
|
||||
|
||||
@@ -139,6 +139,9 @@ void DownloadDictionaryInBackground(
|
||||
not_null<Main::Session*> session,
|
||||
int counter,
|
||||
std::vector<int> langs) {
|
||||
if (counter >= langs.size()) {
|
||||
return;
|
||||
}
|
||||
const auto id = langs[counter];
|
||||
counter++;
|
||||
const auto destroyer = [=] {
|
||||
@@ -154,9 +157,6 @@ void DownloadDictionaryInBackground(
|
||||
}
|
||||
}
|
||||
|
||||
if (counter >= langs.size()) {
|
||||
return;
|
||||
}
|
||||
DownloadDictionaryInBackground(session, counter, langs);
|
||||
};
|
||||
if (DictionaryExists(id)) {
|
||||
|
||||
@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/platform/base_platform_last_input.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "platform/platform_specific.h"
|
||||
#include "platform/platform_integration.h"
|
||||
#include "mainwindow.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "history/history.h"
|
||||
@@ -123,12 +124,14 @@ Application *Application::Instance = nullptr;
|
||||
struct Application::Private {
|
||||
base::Timer quitTimer;
|
||||
UiIntegration uiIntegration;
|
||||
Settings settings;
|
||||
};
|
||||
|
||||
Application::Application(not_null<Launcher*> launcher)
|
||||
: QObject()
|
||||
, _launcher(launcher)
|
||||
, _private(std::make_unique<Private>())
|
||||
, _platformIntegration(Platform::Integration::Create())
|
||||
, _databases(std::make_unique<Storage::Databases>())
|
||||
, _animationsManager(std::make_unique<Ui::Animations::Manager>())
|
||||
, _clearEmojiImageLoaderTimer([=] { clearEmojiSourceImages(); })
|
||||
@@ -144,6 +147,8 @@ Application::Application(not_null<Launcher*> launcher)
|
||||
, _autoLockTimer([=] { checkAutoLock(); }) {
|
||||
Ui::Integration::Set(&_private->uiIntegration);
|
||||
|
||||
_platformIntegration->init();
|
||||
|
||||
passcodeLockChanges(
|
||||
) | rpl::start_with_next([=] {
|
||||
_shouldLockAt = 0;
|
||||
@@ -167,10 +172,11 @@ Application::~Application() {
|
||||
Local::writeSettings();
|
||||
}
|
||||
|
||||
// Depend on activeWindow() for now :(
|
||||
// Depend on primaryWindow() for now :(
|
||||
Shortcuts::Finish();
|
||||
|
||||
_window = nullptr;
|
||||
_secondaryWindows.clear();
|
||||
_primaryWindow = nullptr;
|
||||
_mediaView = nullptr;
|
||||
_notifications->clearAllFast();
|
||||
|
||||
@@ -263,12 +269,13 @@ void Application::run() {
|
||||
// Create mime database, so it won't be slow later.
|
||||
QMimeDatabase().mimeTypeForName(qsl("text/plain"));
|
||||
|
||||
_window = std::make_unique<Window::Controller>();
|
||||
_primaryWindow = std::make_unique<Window::Controller>();
|
||||
_lastActiveWindow = _primaryWindow.get();
|
||||
|
||||
_domain->activeChanges(
|
||||
) | rpl::start_with_next([=](not_null<Main::Account*> account) {
|
||||
_window->showAccount(account);
|
||||
}, _window->widget()->lifetime());
|
||||
_primaryWindow->showAccount(account);
|
||||
}, _primaryWindow->widget()->lifetime());
|
||||
|
||||
QCoreApplication::instance()->installEventFilter(this);
|
||||
|
||||
@@ -287,20 +294,20 @@ void Application::run() {
|
||||
startShortcuts();
|
||||
startDomain();
|
||||
|
||||
_window->widget()->show();
|
||||
_primaryWindow->widget()->show();
|
||||
|
||||
const auto currentGeometry = _window->widget()->geometry();
|
||||
const auto currentGeometry = _primaryWindow->widget()->geometry();
|
||||
_mediaView = std::make_unique<Media::View::OverlayWidget>();
|
||||
_window->widget()->Ui::RpWidget::setGeometry(currentGeometry);
|
||||
_primaryWindow->widget()->Ui::RpWidget::setGeometry(currentGeometry);
|
||||
|
||||
DEBUG_LOG(("Application Info: showing."));
|
||||
_window->finishFirstShow();
|
||||
_primaryWindow->finishFirstShow();
|
||||
|
||||
if (!_window->locked() && cStartToSettings()) {
|
||||
_window->showSettings();
|
||||
if (!_primaryWindow->locked() && cStartToSettings()) {
|
||||
_primaryWindow->showSettings();
|
||||
}
|
||||
|
||||
_window->updateIsActiveFocus();
|
||||
_primaryWindow->updateIsActiveFocus();
|
||||
|
||||
for (const auto &error : Shortcuts::Errors()) {
|
||||
LOG(("Shortcuts Error: %1").arg(error));
|
||||
@@ -311,12 +318,12 @@ void Application::run() {
|
||||
showOpenGLCrashNotification();
|
||||
}
|
||||
|
||||
_window->openInMediaViewRequests(
|
||||
_primaryWindow->openInMediaViewRequests(
|
||||
) | rpl::start_with_next([=](Media::View::OpenRequest &&request) {
|
||||
if (_mediaView) {
|
||||
_mediaView->show(std::move(request));
|
||||
}
|
||||
}, _window->lifetime());
|
||||
}, _primaryWindow->lifetime());
|
||||
|
||||
{
|
||||
const auto countries = std::make_shared<Countries::Manager>(
|
||||
@@ -341,7 +348,7 @@ void Application::showOpenGLCrashNotification() {
|
||||
Core::App().settings().setDisableOpenGL(true);
|
||||
Local::writeSettings();
|
||||
};
|
||||
_window->show(Box<Ui::ConfirmBox>(
|
||||
_primaryWindow->show(Box<Ui::ConfirmBox>(
|
||||
"There may be a problem with your graphics drivers and OpenGL. "
|
||||
"Try updating your drivers.\n\n"
|
||||
"OpenGL has been disabled. You can try to enable it again "
|
||||
@@ -371,8 +378,8 @@ void Application::startSettingsAndBackground() {
|
||||
}
|
||||
|
||||
void Application::checkSystemDarkMode() {
|
||||
const auto maybeDarkMode = _settings.systemDarkMode();
|
||||
const auto darkModeEnabled = _settings.systemDarkModeEnabled();
|
||||
const auto maybeDarkMode = settings().systemDarkMode();
|
||||
const auto darkModeEnabled = settings().systemDarkModeEnabled();
|
||||
const auto needToSwitch = darkModeEnabled
|
||||
&& maybeDarkMode
|
||||
&& (*maybeDarkMode != Window::Theme::IsNightMode());
|
||||
@@ -384,11 +391,11 @@ void Application::checkSystemDarkMode() {
|
||||
|
||||
void Application::startSystemDarkModeViewer() {
|
||||
if (Window::Theme::Background()->editingTheme()) {
|
||||
_settings.setSystemDarkModeEnabled(false);
|
||||
settings().setSystemDarkModeEnabled(false);
|
||||
}
|
||||
rpl::merge(
|
||||
_settings.systemDarkModeChanges() | rpl::to_empty,
|
||||
_settings.systemDarkModeEnabledChanges() | rpl::to_empty
|
||||
settings().systemDarkModeChanges() | rpl::to_empty,
|
||||
settings().systemDarkModeEnabledChanges() | rpl::to_empty
|
||||
) | rpl::start_with_next([=] {
|
||||
checkSystemDarkMode();
|
||||
}, _lifetime);
|
||||
@@ -397,7 +404,7 @@ void Application::startSystemDarkModeViewer() {
|
||||
auto Application::prepareEmojiSourceImages()
|
||||
-> std::shared_ptr<Ui::Emoji::UniversalImages> {
|
||||
const auto &images = Ui::Emoji::SourceImages();
|
||||
if (_settings.largeEmoji()) {
|
||||
if (settings().largeEmoji()) {
|
||||
return images;
|
||||
}
|
||||
Ui::Emoji::ClearSourceImages(images);
|
||||
@@ -462,7 +469,7 @@ bool Application::eventFilter(QObject *object, QEvent *e) {
|
||||
checkStartUrl();
|
||||
}
|
||||
if (StartUrlRequiresActivate(url)) {
|
||||
_window->activate();
|
||||
_primaryWindow->activate();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
@@ -471,6 +478,10 @@ bool Application::eventFilter(QObject *object, QEvent *e) {
|
||||
return QObject::eventFilter(object, e);
|
||||
}
|
||||
|
||||
Settings &Application::settings() {
|
||||
return _private->settings;
|
||||
}
|
||||
|
||||
void Application::saveSettingsDelayed(crl::time delay) {
|
||||
if (_saveSettingsTimer) {
|
||||
_saveSettingsTimer->callOnce(delay);
|
||||
@@ -508,18 +519,17 @@ void Application::constructFallbackProductionConfig(
|
||||
void Application::setCurrentProxy(
|
||||
const MTP::ProxyData &proxy,
|
||||
MTP::ProxyData::Settings settings) {
|
||||
auto &my = _private->settings.proxy();
|
||||
const auto current = [&] {
|
||||
return _settings.proxy().isEnabled()
|
||||
? _settings.proxy().selected()
|
||||
: MTP::ProxyData();
|
||||
return my.isEnabled() ? my.selected() : MTP::ProxyData();
|
||||
};
|
||||
const auto was = current();
|
||||
_settings.proxy().setSelected(proxy);
|
||||
_settings.proxy().setSettings(settings);
|
||||
my.setSelected(proxy);
|
||||
my.setSettings(settings);
|
||||
const auto now = current();
|
||||
refreshGlobalProxy();
|
||||
_proxyChanges.fire({ was, now });
|
||||
_settings.proxy().connectionTypeChangesNotify();
|
||||
my.connectionTypeChangesNotify();
|
||||
}
|
||||
|
||||
auto Application::proxyChanges() const -> rpl::producer<ProxyChange> {
|
||||
@@ -527,10 +537,10 @@ auto Application::proxyChanges() const -> rpl::producer<ProxyChange> {
|
||||
}
|
||||
|
||||
void Application::badMtprotoConfigurationError() {
|
||||
if (_settings.proxy().isEnabled() && !_badProxyDisableBox) {
|
||||
if (settings().proxy().isEnabled() && !_badProxyDisableBox) {
|
||||
const auto disableCallback = [=] {
|
||||
setCurrentProxy(
|
||||
_settings.proxy().selected(),
|
||||
settings().proxy().selected(),
|
||||
MTP::ProxyData::Settings::System);
|
||||
};
|
||||
_badProxyDisableBox = Ui::show(Box<Ui::InformBox>(
|
||||
@@ -542,7 +552,7 @@ void Application::badMtprotoConfigurationError() {
|
||||
void Application::startLocalStorage() {
|
||||
Local::start();
|
||||
_saveSettingsTimer.emplace([=] { saveSettings(); });
|
||||
_settings.saveDelayedRequests() | rpl::start_with_next([=] {
|
||||
settings().saveDelayedRequests() | rpl::start_with_next([=] {
|
||||
saveSettingsDelayed();
|
||||
}, _lifetime);
|
||||
}
|
||||
@@ -550,12 +560,12 @@ void Application::startLocalStorage() {
|
||||
void Application::startEmojiImageLoader() {
|
||||
_emojiImageLoader.with([
|
||||
source = prepareEmojiSourceImages(),
|
||||
large = _settings.largeEmoji()
|
||||
large = settings().largeEmoji()
|
||||
](Stickers::EmojiImageLoader &loader) mutable {
|
||||
loader.init(std::move(source), large);
|
||||
});
|
||||
|
||||
_settings.largeEmojiChanges(
|
||||
settings().largeEmojiChanges(
|
||||
) | rpl::start_with_next([=](bool large) {
|
||||
if (large) {
|
||||
_clearEmojiImageLoaderTimer.cancel();
|
||||
@@ -659,14 +669,14 @@ void Application::checkLocalTime() {
|
||||
|
||||
void Application::handleAppActivated() {
|
||||
checkLocalTime();
|
||||
if (_window) {
|
||||
_window->updateIsActiveFocus();
|
||||
if (_primaryWindow) {
|
||||
_primaryWindow->updateIsActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::handleAppDeactivated() {
|
||||
if (_window) {
|
||||
_window->updateIsActiveBlur();
|
||||
if (_primaryWindow) {
|
||||
_primaryWindow->updateIsActiveBlur();
|
||||
}
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
@@ -766,7 +776,7 @@ bool Application::canApplyLangPackWithoutRestart() const {
|
||||
}
|
||||
|
||||
void Application::checkStartUrl() {
|
||||
if (!cStartUrl().isEmpty() && _window && !_window->locked()) {
|
||||
if (!cStartUrl().isEmpty() && _primaryWindow && !_primaryWindow->locked()) {
|
||||
const auto url = cStartUrl();
|
||||
cSetStartUrl(QString());
|
||||
if (!openLocalUrl(url, {})) {
|
||||
@@ -827,7 +837,9 @@ bool Application::openCustomUrl(
|
||||
return false;
|
||||
}
|
||||
const auto command = base::StringViewMid(urlTrimmed, protocol.size(), 8192);
|
||||
const auto controller = _window ? _window->sessionController() : nullptr;
|
||||
const auto controller = _primaryWindow
|
||||
? _primaryWindow->sessionController()
|
||||
: nullptr;
|
||||
|
||||
using namespace qthelp;
|
||||
const auto options = RegExOption::CaseInsensitive;
|
||||
@@ -842,22 +854,22 @@ bool Application::openCustomUrl(
|
||||
}
|
||||
|
||||
void Application::preventOrInvoke(Fn<void()> &&callback) {
|
||||
_window->preventOrInvoke(std::move(callback));
|
||||
_primaryWindow->preventOrInvoke(std::move(callback));
|
||||
}
|
||||
|
||||
void Application::lockByPasscode() {
|
||||
preventOrInvoke([=] {
|
||||
if (_window) {
|
||||
if (_primaryWindow) {
|
||||
_passcodeLock = true;
|
||||
_window->setupPasscodeLock();
|
||||
_primaryWindow->setupPasscodeLock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Application::unlockPasscode() {
|
||||
clearPasscodeLock();
|
||||
if (_window) {
|
||||
_window->clearPasscodeLock();
|
||||
if (_primaryWindow) {
|
||||
_primaryWindow->clearPasscodeLock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -913,9 +925,11 @@ void Application::checkAutoLock(crl::time lastNonIdleTime) {
|
||||
|
||||
checkLocalTime();
|
||||
const auto now = crl::now();
|
||||
const auto shouldLockInMs = _settings.autoLock() * 1000LL;
|
||||
const auto shouldLockInMs = settings().autoLock() * 1000LL;
|
||||
const auto checkTimeMs = now - lastNonIdleTime;
|
||||
if (checkTimeMs >= shouldLockInMs || (_shouldLockAt > 0 && now > _shouldLockAt + kAutoLockTimeoutLateMs)) {
|
||||
if (checkTimeMs >= shouldLockInMs
|
||||
|| (_shouldLockAt > 0
|
||||
&& now > _shouldLockAt + kAutoLockTimeoutLateMs)) {
|
||||
_shouldLockAt = 0;
|
||||
_autoLockTimer.cancel();
|
||||
lockByPasscode();
|
||||
@@ -940,13 +954,13 @@ void Application::localPasscodeChanged() {
|
||||
}
|
||||
|
||||
bool Application::hasActiveWindow(not_null<Main::Session*> session) const {
|
||||
if (App::quitting() || !_window) {
|
||||
if (App::quitting() || !_primaryWindow) {
|
||||
return false;
|
||||
} else if (_calls->hasActivePanel(session)) {
|
||||
return true;
|
||||
} else if (const auto controller = _window->sessionController()) {
|
||||
} else if (const auto controller = _primaryWindow->sessionController()) {
|
||||
if (&controller->session() == session
|
||||
&& _window->widget()->isActive()) {
|
||||
&& _primaryWindow->widget()->isActive()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -954,15 +968,53 @@ bool Application::hasActiveWindow(not_null<Main::Session*> session) const {
|
||||
}
|
||||
|
||||
void Application::saveCurrentDraftsToHistories() {
|
||||
if (!_window) {
|
||||
if (!_primaryWindow) {
|
||||
return;
|
||||
} else if (const auto controller = _window->sessionController()) {
|
||||
} else if (const auto controller = _primaryWindow->sessionController()) {
|
||||
controller->content()->saveFieldToHistoryLocalDraft();
|
||||
}
|
||||
}
|
||||
|
||||
Window::Controller *Application::primaryWindow() const {
|
||||
return _primaryWindow.get();
|
||||
}
|
||||
|
||||
Window::Controller *Application::separateWindowForPeer(
|
||||
not_null<PeerData*> peer) const {
|
||||
for (const auto &[history, window] : _secondaryWindows) {
|
||||
if (history->peer == peer) {
|
||||
return window.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Window::Controller *Application::ensureSeparateWindowForPeer(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId showAtMsgId) {
|
||||
const auto activate = [&](not_null<Window::Controller*> window) {
|
||||
window->activate();
|
||||
return window;
|
||||
};
|
||||
|
||||
if (const auto existing = separateWindowForPeer(peer)) {
|
||||
existing->sessionController()->showPeerHistory(
|
||||
peer,
|
||||
Window::SectionShow::Way::ClearStack,
|
||||
showAtMsgId);
|
||||
return activate(existing);
|
||||
}
|
||||
const auto result = _secondaryWindows.emplace(
|
||||
peer->owner().history(peer),
|
||||
std::make_unique<Window::Controller>(peer, showAtMsgId)
|
||||
).first->second.get();
|
||||
result->widget()->show();
|
||||
result->finishFirstShow();
|
||||
return activate(result);
|
||||
}
|
||||
|
||||
Window::Controller *Application::activeWindow() const {
|
||||
return _window.get();
|
||||
return _lastActiveWindow;
|
||||
}
|
||||
|
||||
bool Application::closeActiveWindow() {
|
||||
|
||||
@@ -17,6 +17,10 @@ class MainWidget;
|
||||
class FileUploader;
|
||||
class Translator;
|
||||
|
||||
namespace Platform {
|
||||
class Integration;
|
||||
} // namespace Platform
|
||||
|
||||
namespace Storage {
|
||||
class Databases;
|
||||
} // namespace Storage
|
||||
@@ -115,8 +119,11 @@ public:
|
||||
Application &operator=(const Application &other) = delete;
|
||||
~Application();
|
||||
|
||||
[[nodiscard]] not_null<Launcher*> launcher() const {
|
||||
return _launcher;
|
||||
[[nodiscard]] Launcher &launcher() const {
|
||||
return *_launcher;
|
||||
}
|
||||
[[nodiscard]] Platform::Integration &platformIntegration() const {
|
||||
return *_platformIntegration;
|
||||
}
|
||||
|
||||
void run();
|
||||
@@ -133,7 +140,13 @@ public:
|
||||
// Windows interface.
|
||||
bool hasActiveWindow(not_null<Main::Session*> session) const;
|
||||
void saveCurrentDraftsToHistories();
|
||||
[[nodiscard]] Window::Controller *primaryWindow() const;
|
||||
[[nodiscard]] Window::Controller *activeWindow() const;
|
||||
[[nodiscard]] Window::Controller *separateWindowForPeer(
|
||||
not_null<PeerData*> peer) const;
|
||||
Window::Controller *ensureSeparateWindowForPeer(
|
||||
not_null<PeerData*> peer,
|
||||
MsgId showAtMsgId);
|
||||
bool closeActiveWindow();
|
||||
bool minimizeActiveWindow();
|
||||
[[nodiscard]] QWidget *getFileDialogParent();
|
||||
@@ -147,9 +160,7 @@ public:
|
||||
[[nodiscard]] QPoint getPointForCallPanelCenter() const;
|
||||
|
||||
void startSettingsAndBackground();
|
||||
[[nodiscard]] Settings &settings() {
|
||||
return _settings;
|
||||
}
|
||||
[[nodiscard]] Settings &settings();
|
||||
void saveSettingsDelayed(crl::time delay = kDefaultSaveDelay);
|
||||
void saveSettings();
|
||||
|
||||
@@ -314,13 +325,13 @@ private:
|
||||
};
|
||||
InstanceSetter _setter = { this };
|
||||
|
||||
not_null<Launcher*> _launcher;
|
||||
const not_null<Launcher*> _launcher;
|
||||
rpl::event_stream<ProxyChange> _proxyChanges;
|
||||
|
||||
// Some fields are just moved from the declaration.
|
||||
struct Private;
|
||||
const std::unique_ptr<Private> _private;
|
||||
Settings _settings;
|
||||
const std::unique_ptr<Platform::Integration> _platformIntegration;
|
||||
|
||||
const std::unique_ptr<Storage::Databases> _databases;
|
||||
const std::unique_ptr<Ui::Animations::Manager> _animationsManager;
|
||||
@@ -336,7 +347,12 @@ private:
|
||||
const std::unique_ptr<Main::Domain> _domain;
|
||||
const std::unique_ptr<Export::Manager> _exportManager;
|
||||
const std::unique_ptr<Calls::Instance> _calls;
|
||||
std::unique_ptr<Window::Controller> _window;
|
||||
std::unique_ptr<Window::Controller> _primaryWindow;
|
||||
base::flat_map<
|
||||
not_null<History*>,
|
||||
std::unique_ptr<Window::Controller>> _secondaryWindows;
|
||||
Window::Controller *_lastActiveWindow = nullptr;
|
||||
|
||||
std::unique_ptr<Media::View::OverlayWidget> _mediaView;
|
||||
const std::unique_ptr<Lang::Instance> _langpack;
|
||||
const std::unique_ptr<Lang::CloudManager> _langCloudManager;
|
||||
|
||||
@@ -66,6 +66,12 @@ std::map<int, const char*> BetaLogs() {
|
||||
|
||||
"- Spoiler formatting hides text in chat, "
|
||||
"as well as in the chat list and notifications.\n"
|
||||
},
|
||||
{
|
||||
3004005,
|
||||
"- Fix crash in monospace blocks processing.\n"
|
||||
|
||||
"- Fix reaction animations stopping after an hour uptime.\n"
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwindow.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/text/text_entity.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "base/qthelp_regex.h"
|
||||
#include "storage/storage_account.h"
|
||||
#include "history/history.h"
|
||||
@@ -266,3 +268,41 @@ void BotCommandClickHandler::onClick(ClickContext context) const {
|
||||
auto BotCommandClickHandler::getTextEntity() const -> TextEntity {
|
||||
return { EntityType::BotCommand };
|
||||
}
|
||||
|
||||
MonospaceClickHandler::MonospaceClickHandler(
|
||||
const QString &text,
|
||||
EntityType type)
|
||||
: _text(text)
|
||||
, _entity({ type }) {
|
||||
}
|
||||
|
||||
void MonospaceClickHandler::onClick(ClickContext context) const {
|
||||
const auto button = context.button;
|
||||
if (button != Qt::LeftButton && button != Qt::MiddleButton) {
|
||||
return;
|
||||
}
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
if (const auto controller = my.sessionWindow.get()) {
|
||||
auto &data = controller->session().data();
|
||||
const auto item = data.message(my.itemId);
|
||||
const auto hasCopyRestriction = item
|
||||
&& (!item->history()->peer->allowsForwarding()
|
||||
|| item->forbidsForward());
|
||||
if (hasCopyRestriction) {
|
||||
Ui::Toast::Show(item->history()->peer->isBroadcast()
|
||||
? tr::lng_error_nocopy_channel(tr::now)
|
||||
: tr::lng_error_nocopy_group(tr::now));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ui::Toast::Show(tr::lng_text_copied(tr::now));
|
||||
TextUtilities::SetClipboardText(TextForMimeData::Simple(_text.trimmed()));
|
||||
}
|
||||
|
||||
auto MonospaceClickHandler::getTextEntity() const -> TextEntity {
|
||||
return _entity;
|
||||
}
|
||||
|
||||
QString MonospaceClickHandler::url() const {
|
||||
return _text;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/basic_click_handlers.h"
|
||||
|
||||
constexpr auto kPeerLinkPeerIdProperty = 0x01;
|
||||
constexpr auto kPhotoLinkMediaIdProperty = 0x02;
|
||||
constexpr auto kDocumentLinkMediaIdProperty = 0x03;
|
||||
constexpr auto kSendReactionEmojiProperty = 0x04;
|
||||
constexpr auto kReactionsCountEmojiProperty = 0x05;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
@@ -195,3 +199,20 @@ private:
|
||||
QString _cmd;
|
||||
|
||||
};
|
||||
|
||||
class MonospaceClickHandler : public TextClickHandler {
|
||||
public:
|
||||
MonospaceClickHandler(const QString &text, EntityType type);
|
||||
|
||||
void onClick(ClickContext context) const override;
|
||||
|
||||
TextEntity getTextEntity() const override;
|
||||
|
||||
protected:
|
||||
QString url() const override;
|
||||
|
||||
private:
|
||||
const QString _text;
|
||||
const TextEntity _entity;
|
||||
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "platform/platform_launcher.h"
|
||||
#include "platform/platform_specific.h"
|
||||
#include "platform/linux/linux_desktop_environment.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/platform/base_platform_file_utilities.h"
|
||||
#include "ui/main_queue_processor.h"
|
||||
@@ -59,7 +60,12 @@ FilteredCommandLineArguments::FilteredCommandLineArguments(
|
||||
pushArgument("cocoa:fontengine=freetype");
|
||||
#endif // !Q_OS_WIN
|
||||
}
|
||||
#endif // Q_OS_WIN || Q_OS_MAC
|
||||
#elif defined Q_OS_UNIX
|
||||
if (Platform::DesktopEnvironment::IsGnome() && qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
|
||||
pushArgument("-platform");
|
||||
pushArgument("xcb;wayland");
|
||||
}
|
||||
#endif // Q_OS_WIN || Q_OS_MAC || Q_OS_UNIX
|
||||
|
||||
pushArgument(nullptr);
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ void Manager::set(const QString &keys, Command command, bool replace) {
|
||||
}
|
||||
auto shortcut = base::make_unique_q<QShortcut>(
|
||||
result,
|
||||
Core::App().activeWindow()->widget().get(),
|
||||
Core::App().primaryWindow()->widget().get(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
Qt::ApplicationShortcut);
|
||||
|
||||
@@ -202,6 +202,11 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
|
||||
LOG(("Bad mention name: %1").arg(data.data));
|
||||
}
|
||||
} break;
|
||||
|
||||
case EntityType::Code:
|
||||
return std::make_shared<MonospaceClickHandler>(data.text, data.type);
|
||||
case EntityType::Pre:
|
||||
return std::make_shared<MonospaceClickHandler>(data.text, data.type);
|
||||
}
|
||||
return Integration::createLinkHandler(data, context);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 3004000;
|
||||
constexpr auto AppVersionStr = "3.4";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppVersion = 3004005;
|
||||
constexpr auto AppVersionStr = "3.4.5";
|
||||
constexpr auto AppBetaVersion = true;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -761,7 +761,7 @@ PeerId ChannelData::groupCallDefaultJoinAs() const {
|
||||
return _callDefaultJoinAs;
|
||||
}
|
||||
|
||||
void ChannelData::setAllowedReactions(std::vector<QString> list) {
|
||||
void ChannelData::setAllowedReactions(base::flat_set<QString> list) {
|
||||
if (_allowedReactions != list) {
|
||||
const auto toggled = (_allowedReactions.empty() != list.empty());
|
||||
_allowedReactions = std::move(list);
|
||||
@@ -774,7 +774,7 @@ void ChannelData::setAllowedReactions(std::vector<QString> list) {
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<QString> &ChannelData::allowedReactions() const {
|
||||
const base::flat_set<QString> &ChannelData::allowedReactions() const {
|
||||
return _allowedReactions;
|
||||
}
|
||||
|
||||
|
||||
@@ -410,8 +410,8 @@ public:
|
||||
void setGroupCallDefaultJoinAs(PeerId peerId);
|
||||
[[nodiscard]] PeerId groupCallDefaultJoinAs() const;
|
||||
|
||||
void setAllowedReactions(std::vector<QString> list);
|
||||
[[nodiscard]] const std::vector<QString> &allowedReactions() const;
|
||||
void setAllowedReactions(base::flat_set<QString> list);
|
||||
[[nodiscard]] const base::flat_set<QString> &allowedReactions() const;
|
||||
|
||||
// Still public data members.
|
||||
uint64 access = 0;
|
||||
@@ -460,7 +460,7 @@ private:
|
||||
QString _inviteLink;
|
||||
std::optional<ChannelData*> _linkedChat;
|
||||
|
||||
std::vector<QString> _allowedReactions;
|
||||
base::flat_set<QString> _allowedReactions;
|
||||
|
||||
std::unique_ptr<Data::GroupCall> _call;
|
||||
PeerId _callDefaultJoinAs = 0;
|
||||
|
||||
@@ -287,7 +287,7 @@ void ChatData::setPendingRequestsCount(
|
||||
}
|
||||
}
|
||||
|
||||
void ChatData::setAllowedReactions(std::vector<QString> list) {
|
||||
void ChatData::setAllowedReactions(base::flat_set<QString> list) {
|
||||
if (_allowedReactions != list) {
|
||||
const auto toggled = (_allowedReactions.empty() != list.empty());
|
||||
_allowedReactions = std::move(list);
|
||||
@@ -300,7 +300,7 @@ void ChatData::setAllowedReactions(std::vector<QString> list) {
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<QString> &ChatData::allowedReactions() const {
|
||||
const base::flat_set<QString> &ChatData::allowedReactions() const {
|
||||
return _allowedReactions;
|
||||
}
|
||||
|
||||
|
||||
@@ -175,8 +175,8 @@ public:
|
||||
int count,
|
||||
std::vector<UserId> recentRequesters);
|
||||
|
||||
void setAllowedReactions(std::vector<QString> list);
|
||||
[[nodiscard]] const std::vector<QString> &allowedReactions() const;
|
||||
void setAllowedReactions(base::flat_set<QString> list);
|
||||
[[nodiscard]] const base::flat_set<QString> &allowedReactions() const;
|
||||
|
||||
// Still public data members.
|
||||
const MTPlong inputChat;
|
||||
@@ -202,7 +202,7 @@ private:
|
||||
int _pendingRequestsCount = 0;
|
||||
std::vector<UserId> _recentRequesters;
|
||||
|
||||
std::vector<QString> _allowedReactions;
|
||||
base::flat_set<QString> _allowedReactions;
|
||||
|
||||
std::unique_ptr<Data::GroupCall> _call;
|
||||
PeerId _callDefaultJoinAs = 0;
|
||||
|
||||
@@ -172,11 +172,25 @@ void UpdateCloudFile(
|
||||
return;
|
||||
}
|
||||
|
||||
const auto needStickerThumbnailUpdate = [&] {
|
||||
const auto was = std::get_if<StorageFileLocation>(
|
||||
&file.location.file().data);
|
||||
const auto now = std::get_if<StorageFileLocation>(
|
||||
&data.location.file().data);
|
||||
using Type = StorageFileLocation::Type;
|
||||
if (!was || !now || was->type() != Type::StickerSetThumb) {
|
||||
return false;
|
||||
}
|
||||
return now->valid()
|
||||
&& (now->type() != Type::StickerSetThumb
|
||||
|| now->cacheKey() != was->cacheKey());
|
||||
};
|
||||
const auto update = !file.location.valid()
|
||||
|| (data.location.file().cacheKey()
|
||||
&& (!file.location.file().cacheKey()
|
||||
|| (file.location.width() < data.location.width())
|
||||
|| (file.location.height() < data.location.height())));
|
||||
|| (file.location.height() < data.location.height())
|
||||
|| needStickerThumbnailUpdate()));
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
@@ -204,6 +218,8 @@ void UpdateCloudFile(
|
||||
} else if (file.loader) {
|
||||
const auto origin = base::take(file.loader)->fileOrigin();
|
||||
restartLoader(origin);
|
||||
} else if (file.flags & CloudFile::Flag::Failed) {
|
||||
file.flags &= ~CloudFile::Flag::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1336,7 +1336,7 @@ bool DocumentData::isSongWithCover() const {
|
||||
}
|
||||
|
||||
bool DocumentData::isAudioFile() const {
|
||||
if (isVoiceMessage()) {
|
||||
if (isVoiceMessage() || isVideoFile()) {
|
||||
return false;
|
||||
} else if (isSong()) {
|
||||
return true;
|
||||
|
||||
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "data/data_file_click_handler.h"
|
||||
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_photo.h"
|
||||
@@ -44,6 +45,9 @@ DocumentClickHandler::DocumentClickHandler(
|
||||
FullMsgId context)
|
||||
: FileClickHandler(context)
|
||||
, _document(document) {
|
||||
setProperty(
|
||||
kDocumentLinkMediaIdProperty,
|
||||
QVariant(qulonglong(_document->id)));
|
||||
}
|
||||
|
||||
DocumentOpenClickHandler::DocumentOpenClickHandler(
|
||||
@@ -146,6 +150,7 @@ PhotoClickHandler::PhotoClickHandler(
|
||||
: FileClickHandler(context)
|
||||
, _photo(photo)
|
||||
, _peer(peer) {
|
||||
setProperty(kPhotoLinkMediaIdProperty, QVariant(qulonglong(_photo->id)));
|
||||
}
|
||||
|
||||
not_null<PhotoData*> PhotoClickHandler::photo() const {
|
||||
|
||||
@@ -66,44 +66,27 @@ constexpr auto kMaxPreviewImages = 3;
|
||||
using ItemPreview = HistoryView::ItemPreview;
|
||||
using ItemPreviewImage = HistoryView::ItemPreviewImage;
|
||||
|
||||
[[nodiscard]] QString WithCaptionDialogsText(
|
||||
[[nodiscard]] TextWithEntities WithCaptionNotificationText(
|
||||
const QString &attachType,
|
||||
const QString &caption,
|
||||
bool hasMiniImages,
|
||||
const HistoryView::ToPreviewOptions &options) {
|
||||
if (caption.isEmpty()) {
|
||||
return textcmdLink(1, TextUtilities::Clean(attachType));
|
||||
const TextWithEntities &caption,
|
||||
bool hasMiniImages = false) {
|
||||
if (caption.text.isEmpty()) {
|
||||
return Ui::Text::PlainLink(attachType);
|
||||
}
|
||||
|
||||
return hasMiniImages
|
||||
? TextUtilities::Clean(caption, !options.ignoreSpoilers)
|
||||
? caption
|
||||
: tr::lng_dialogs_text_media(
|
||||
tr::now,
|
||||
lt_media_part,
|
||||
textcmdLink(1, tr::lng_dialogs_text_media_wrapped(
|
||||
tr::lng_dialogs_text_media_wrapped(
|
||||
tr::now,
|
||||
lt_media,
|
||||
TextUtilities::Clean(attachType))),
|
||||
Ui::Text::PlainLink(attachType),
|
||||
Ui::Text::WithEntities),
|
||||
lt_caption,
|
||||
TextUtilities::Clean(caption, !options.ignoreSpoilers));
|
||||
}
|
||||
|
||||
[[nodiscard]] QString WithCaptionNotificationText(
|
||||
const QString &attachType,
|
||||
const QString &caption) {
|
||||
if (caption.isEmpty()) {
|
||||
return attachType;
|
||||
}
|
||||
|
||||
return tr::lng_dialogs_text_media(
|
||||
tr::now,
|
||||
lt_media_part,
|
||||
tr::lng_dialogs_text_media_wrapped(
|
||||
tr::now,
|
||||
lt_media,
|
||||
attachType),
|
||||
lt_caption,
|
||||
caption);
|
||||
caption,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage PreparePreviewImage(
|
||||
@@ -352,15 +335,7 @@ bool Media::canBeGrouped() const {
|
||||
}
|
||||
|
||||
ItemPreview Media::toPreview(ToPreviewOptions options) const {
|
||||
auto result = notificationText();
|
||||
auto text = result.isEmpty()
|
||||
? QString()
|
||||
: textcmdLink(
|
||||
1,
|
||||
TextUtilities::Clean(
|
||||
std::move(result),
|
||||
!options.ignoreSpoilers));
|
||||
return { .text = std::move(text) };
|
||||
return { .text = notificationText() };
|
||||
}
|
||||
|
||||
bool Media::hasReplyPreview() const {
|
||||
@@ -428,9 +403,8 @@ std::unique_ptr<HistoryView::Media> Media::createView(
|
||||
ItemPreview Media::toGroupPreview(
|
||||
const HistoryItemsList &items,
|
||||
ToPreviewOptions options) const {
|
||||
const auto genericText = textcmdLink(
|
||||
1,
|
||||
TextUtilities::Clean(tr::lng_in_dlg_album(tr::now)));
|
||||
const auto genericText = Ui::Text::PlainLink(
|
||||
tr::lng_in_dlg_album(tr::now));
|
||||
auto result = ItemPreview();
|
||||
auto loadingContext = std::vector<std::any>();
|
||||
for (const auto &item : items) {
|
||||
@@ -452,17 +426,17 @@ ItemPreview Media::toGroupPreview(
|
||||
if (single.loadingContext.has_value()) {
|
||||
loadingContext.push_back(std::move(single.loadingContext));
|
||||
}
|
||||
const auto original = item->originalText().text;
|
||||
if (!original.isEmpty()) {
|
||||
if (result.text.isEmpty()) {
|
||||
result.text = TextUtilities::Clean(original);
|
||||
const auto original = item->originalText();
|
||||
if (!original.text.isEmpty()) {
|
||||
if (result.text.text.isEmpty()) {
|
||||
result.text = original;
|
||||
} else {
|
||||
result.text = genericText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.text.isEmpty()) {
|
||||
if (result.text.text.isEmpty()) {
|
||||
result.text = genericText;
|
||||
}
|
||||
if (!loadingContext.empty()) {
|
||||
@@ -536,10 +510,10 @@ bool MediaPhoto::replyPreviewLoaded() const {
|
||||
return _photo->replyPreviewLoaded();
|
||||
}
|
||||
|
||||
QString MediaPhoto::notificationText() const {
|
||||
TextWithEntities MediaPhoto::notificationText() const {
|
||||
return WithCaptionNotificationText(
|
||||
tr::lng_in_dlg_photo(tr::now),
|
||||
TextUtilities::TextWithSpoilerCommands(parent()->originalText()));
|
||||
parent()->originalText());
|
||||
}
|
||||
|
||||
ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const {
|
||||
@@ -569,13 +543,11 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const {
|
||||
}
|
||||
const auto type = tr::lng_in_dlg_photo(tr::now);
|
||||
const auto caption = options.hideCaption
|
||||
? QString()
|
||||
: options.ignoreSpoilers
|
||||
? parent()->originalText().text
|
||||
: TextUtilities::TextWithSpoilerCommands(parent()->originalText());
|
||||
? TextWithEntities()
|
||||
: parent()->originalText();
|
||||
const auto hasMiniImages = !images.empty();
|
||||
return {
|
||||
.text = WithCaptionDialogsText(type, caption, hasMiniImages, options),
|
||||
.text = WithCaptionNotificationText(type, caption, hasMiniImages),
|
||||
.images = std::move(images),
|
||||
.loadingContext = std::move(context),
|
||||
};
|
||||
@@ -791,23 +763,22 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const {
|
||||
return tr::lng_in_dlg_file(tr::now);
|
||||
}();
|
||||
const auto caption = options.hideCaption
|
||||
? QString()
|
||||
: options.ignoreSpoilers
|
||||
? parent()->originalText().text
|
||||
: TextUtilities::TextWithSpoilerCommands(parent()->originalText());
|
||||
? TextWithEntities()
|
||||
: parent()->originalText();
|
||||
const auto hasMiniImages = !images.empty();
|
||||
return {
|
||||
.text = WithCaptionDialogsText(type, caption, hasMiniImages, options),
|
||||
.text = WithCaptionNotificationText(type, caption, hasMiniImages),
|
||||
.images = std::move(images),
|
||||
.loadingContext = std::move(context),
|
||||
};
|
||||
}
|
||||
|
||||
QString MediaFile::notificationText() const {
|
||||
TextWithEntities MediaFile::notificationText() const {
|
||||
if (const auto sticker = _document->sticker()) {
|
||||
return _emoji.isEmpty()
|
||||
const auto text = _emoji.isEmpty()
|
||||
? tr::lng_in_dlg_sticker(tr::now)
|
||||
: tr::lng_in_dlg_sticker_emoji(tr::now, lt_emoji, _emoji);
|
||||
return Ui::Text::PlainLink(text);
|
||||
}
|
||||
const auto type = [&] {
|
||||
if (_document->isVideoMessage()) {
|
||||
@@ -825,9 +796,7 @@ QString MediaFile::notificationText() const {
|
||||
}
|
||||
return tr::lng_in_dlg_file(tr::now);
|
||||
}();
|
||||
return WithCaptionNotificationText(
|
||||
type,
|
||||
TextUtilities::TextWithSpoilerCommands(parent()->originalText()));
|
||||
return WithCaptionNotificationText(type, parent()->originalText());
|
||||
}
|
||||
|
||||
QString MediaFile::pinnedTextSubstring() const {
|
||||
@@ -1035,8 +1004,8 @@ const SharedContact *MediaContact::sharedContact() const {
|
||||
return &_contact;
|
||||
}
|
||||
|
||||
QString MediaContact::notificationText() const {
|
||||
return tr::lng_in_dlg_contact(tr::now);
|
||||
TextWithEntities MediaContact::notificationText() const {
|
||||
return tr::lng_in_dlg_contact(tr::now, Ui::Text::WithEntities);
|
||||
}
|
||||
|
||||
QString MediaContact::pinnedTextSubstring() const {
|
||||
@@ -1124,13 +1093,16 @@ Data::CloudImage *MediaLocation::location() const {
|
||||
ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const {
|
||||
const auto type = tr::lng_maps_point(tr::now);
|
||||
const auto hasMiniImages = false;
|
||||
const auto text = TextWithEntities{ .text = _title };
|
||||
return {
|
||||
.text = WithCaptionDialogsText(type, _title, hasMiniImages, options),
|
||||
.text = WithCaptionNotificationText(type, text, hasMiniImages),
|
||||
};
|
||||
}
|
||||
|
||||
QString MediaLocation::notificationText() const {
|
||||
return WithCaptionNotificationText(tr::lng_maps_point(tr::now), _title);
|
||||
TextWithEntities MediaLocation::notificationText() const {
|
||||
return WithCaptionNotificationText(
|
||||
tr::lng_maps_point(tr::now),
|
||||
{ .text = _title});
|
||||
}
|
||||
|
||||
QString MediaLocation::pinnedTextSubstring() const {
|
||||
@@ -1141,11 +1113,11 @@ TextForMimeData MediaLocation::clipboardText() const {
|
||||
auto result = TextForMimeData::Simple(
|
||||
qstr("[ ") + tr::lng_maps_point(tr::now) + qstr(" ]\n"));
|
||||
auto titleResult = TextUtilities::ParseEntities(
|
||||
TextUtilities::Clean(_title),
|
||||
_title,
|
||||
Ui::WebpageTextTitleOptions().flags);
|
||||
auto descriptionResult = TextUtilities::ParseEntities(
|
||||
TextUtilities::Clean(_description),
|
||||
TextParseLinks | TextParseMultiline | TextParseRichText);
|
||||
_description,
|
||||
TextParseLinks | TextParseMultiline);
|
||||
if (!titleResult.empty()) {
|
||||
result.append(std::move(titleResult));
|
||||
}
|
||||
@@ -1194,7 +1166,7 @@ const Call *MediaCall::call() const {
|
||||
return &_call;
|
||||
}
|
||||
|
||||
QString MediaCall::notificationText() const {
|
||||
TextWithEntities MediaCall::notificationText() const {
|
||||
auto result = Text(parent(), _call.finishReason, _call.video);
|
||||
if (_call.duration > 0) {
|
||||
result = tr::lng_call_type_and_duration(
|
||||
@@ -1204,7 +1176,7 @@ QString MediaCall::notificationText() const {
|
||||
lt_duration,
|
||||
Ui::FormatDurationWords(_call.duration));
|
||||
}
|
||||
return result;
|
||||
return { .text = result };
|
||||
}
|
||||
|
||||
QString MediaCall::pinnedTextSubstring() const {
|
||||
@@ -1212,8 +1184,7 @@ QString MediaCall::pinnedTextSubstring() const {
|
||||
}
|
||||
|
||||
TextForMimeData MediaCall::clipboardText() const {
|
||||
return TextForMimeData::Simple(
|
||||
qstr("[ ") + notificationText() + qstr(" ]"));
|
||||
return { .rich = notificationText() };
|
||||
}
|
||||
|
||||
bool MediaCall::allowsForward() const {
|
||||
@@ -1321,8 +1292,8 @@ ItemPreview MediaWebPage::toPreview(ToPreviewOptions options) const {
|
||||
return { .text = notificationText() };
|
||||
}
|
||||
|
||||
QString MediaWebPage::notificationText() const {
|
||||
return TextUtilities::TextWithSpoilerCommands(parent()->originalText());
|
||||
TextWithEntities MediaWebPage::notificationText() const {
|
||||
return parent()->originalText();
|
||||
}
|
||||
|
||||
QString MediaWebPage::pinnedTextSubstring() const {
|
||||
@@ -1390,7 +1361,7 @@ bool MediaGame::replyPreviewLoaded() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MediaGame::notificationText() const {
|
||||
TextWithEntities MediaGame::notificationText() const {
|
||||
// Add a game controller emoji before game title.
|
||||
auto result = QString();
|
||||
result.reserve(_game->title.size() + 3);
|
||||
@@ -1401,7 +1372,7 @@ QString MediaGame::notificationText() const {
|
||||
).append(
|
||||
QChar(' ')
|
||||
).append(_game->title);
|
||||
return result;
|
||||
return { .text = result };
|
||||
}
|
||||
|
||||
GameData *MediaGame::game() const {
|
||||
@@ -1496,8 +1467,8 @@ bool MediaInvoice::replyPreviewLoaded() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MediaInvoice::notificationText() const {
|
||||
return _invoice.title;
|
||||
TextWithEntities MediaInvoice::notificationText() const {
|
||||
return { .text = _invoice.title };
|
||||
}
|
||||
|
||||
QString MediaInvoice::pinnedTextSubstring() const {
|
||||
@@ -1541,8 +1512,8 @@ PollData *MediaPoll::poll() const {
|
||||
return _poll;
|
||||
}
|
||||
|
||||
QString MediaPoll::notificationText() const {
|
||||
return _poll->question;
|
||||
TextWithEntities MediaPoll::notificationText() const {
|
||||
return Ui::Text::PlainLink(_poll->question);
|
||||
}
|
||||
|
||||
QString MediaPoll::pinnedTextSubstring() const {
|
||||
@@ -1616,16 +1587,16 @@ bool MediaDice::allowsRevoke(TimeId now) const {
|
||||
return (now >= parent()->date() + kFastRevokeRestriction);
|
||||
}
|
||||
|
||||
QString MediaDice::notificationText() const {
|
||||
return _emoji;
|
||||
TextWithEntities MediaDice::notificationText() const {
|
||||
return { .text = _emoji };
|
||||
}
|
||||
|
||||
QString MediaDice::pinnedTextSubstring() const {
|
||||
return QChar(171) + notificationText() + QChar(187);
|
||||
return QChar(171) + notificationText().text + QChar(187);
|
||||
}
|
||||
|
||||
TextForMimeData MediaDice::clipboardText() const {
|
||||
return { notificationText() };
|
||||
return { .rich = notificationText() };
|
||||
}
|
||||
|
||||
bool MediaDice::forceForwardedInfo() const {
|
||||
|
||||
@@ -100,7 +100,7 @@ public:
|
||||
// Returns text with link-start and link-end commands for service-color highlighting.
|
||||
// Example: "[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text"
|
||||
virtual ItemPreview toPreview(ToPreviewOptions way) const;
|
||||
virtual QString notificationText() const = 0;
|
||||
virtual TextWithEntities notificationText() const = 0;
|
||||
virtual QString pinnedTextSubstring() const = 0;
|
||||
virtual TextForMimeData clipboardText() const = 0;
|
||||
virtual bool allowsForward() const;
|
||||
@@ -161,7 +161,7 @@ public:
|
||||
Image *replyPreview() const override;
|
||||
bool replyPreviewLoaded() const override;
|
||||
ItemPreview toPreview(ToPreviewOptions options) const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
bool allowsEditCaption() const override;
|
||||
@@ -199,7 +199,7 @@ public:
|
||||
Image *replyPreview() const override;
|
||||
bool replyPreviewLoaded() const override;
|
||||
ItemPreview toPreview(ToPreviewOptions options) const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
bool allowsEditCaption() const override;
|
||||
@@ -234,7 +234,7 @@ public:
|
||||
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
|
||||
|
||||
const SharedContact *sharedContact() const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
|
||||
@@ -265,7 +265,7 @@ public:
|
||||
|
||||
Data::CloudImage *location() const override;
|
||||
ItemPreview toPreview(ToPreviewOptions options) const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
|
||||
@@ -292,7 +292,7 @@ public:
|
||||
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
|
||||
|
||||
const Call *call() const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
bool allowsForward() const override;
|
||||
@@ -331,7 +331,7 @@ public:
|
||||
Image *replyPreview() const override;
|
||||
bool replyPreviewLoaded() const override;
|
||||
ItemPreview toPreview(ToPreviewOptions options) const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
bool allowsEdit() const override;
|
||||
@@ -361,7 +361,7 @@ public:
|
||||
bool hasReplyPreview() const override;
|
||||
Image *replyPreview() const override;
|
||||
bool replyPreviewLoaded() const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
QString errorTextForForward(not_null<PeerData*> peer) const override;
|
||||
@@ -396,7 +396,7 @@ public:
|
||||
bool hasReplyPreview() const override;
|
||||
Image *replyPreview() const override;
|
||||
bool replyPreviewLoaded() const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
|
||||
@@ -423,7 +423,7 @@ public:
|
||||
|
||||
PollData *poll() const override;
|
||||
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
QString errorTextForForward(not_null<PeerData*> peer) const override;
|
||||
@@ -450,7 +450,7 @@ public:
|
||||
[[nodiscard]] int value() const;
|
||||
|
||||
bool allowsRevoke(TimeId now) const override;
|
||||
QString notificationText() const override;
|
||||
TextWithEntities notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
TextForMimeData clipboardText() const override;
|
||||
bool forceForwardedInfo() const override;
|
||||
|
||||
@@ -10,12 +10,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_chat.h"
|
||||
@@ -25,6 +26,7 @@ namespace {
|
||||
|
||||
constexpr auto kRefreshFullListEach = 60 * 60 * crl::time(1000);
|
||||
constexpr auto kPollEach = 20 * crl::time(1000);
|
||||
constexpr auto kSizeForDownscale = 64;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -47,8 +49,22 @@ Reactions::Reactions(not_null<Session*> owner)
|
||||
_pollItems.remove(item);
|
||||
_repaintItems.remove(item);
|
||||
}, _lifetime);
|
||||
|
||||
const auto appConfig = &_owner->session().account().appConfig();
|
||||
appConfig->value(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto favorite = appConfig->get<QString>(
|
||||
u"reactions_default"_q,
|
||||
QString::fromUtf8("\xf0\x9f\x91\x8d"));
|
||||
if (_favorite != favorite && !_saveFaveRequestId) {
|
||||
_favorite = favorite;
|
||||
_updated.fire({});
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
Reactions::~Reactions() = default;
|
||||
|
||||
void Reactions::refresh() {
|
||||
request();
|
||||
}
|
||||
@@ -61,13 +77,26 @@ const std::vector<Reaction> &Reactions::list(Type type) const {
|
||||
Unexpected("Type in Reactions::list.");
|
||||
}
|
||||
|
||||
std::vector<Reaction> Reactions::list(not_null<PeerData*> peer) const {
|
||||
if (const auto chat = peer->asChat()) {
|
||||
return filtered(chat->allowedReactions());
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
return filtered(channel->allowedReactions());
|
||||
} else {
|
||||
return list(Type::Active);
|
||||
QString Reactions::favorite() const {
|
||||
return _favorite;
|
||||
}
|
||||
|
||||
void Reactions::setFavorite(const QString &emoji) {
|
||||
const auto api = &_owner->session().api();
|
||||
if (_saveFaveRequestId) {
|
||||
api->request(_saveFaveRequestId).cancel();
|
||||
}
|
||||
_saveFaveRequestId = api->request(MTPmessages_SetDefaultReaction(
|
||||
MTP_string(emoji)
|
||||
)).done([=] {
|
||||
_saveFaveRequestId = 0;
|
||||
}).fail([=] {
|
||||
_saveFaveRequestId = 0;
|
||||
}).send();
|
||||
|
||||
if (_favorite != emoji) {
|
||||
_favorite = emoji;
|
||||
_updated.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,16 +110,37 @@ void Reactions::preloadImageFor(const QString &emoji) {
|
||||
}
|
||||
auto &set = _images.emplace(emoji).first->second;
|
||||
const auto i = ranges::find(_available, emoji, &Reaction::emoji);
|
||||
const auto document = (i != end(_available))
|
||||
? i->staticIcon.get()
|
||||
: nullptr;
|
||||
const auto document = (i == end(_available))
|
||||
? nullptr
|
||||
: i->centerIcon
|
||||
? i->centerIcon
|
||||
: i->appearAnimation.get();
|
||||
if (document) {
|
||||
loadImage(set, document);
|
||||
loadImage(set, document, !i->centerIcon);
|
||||
} else if (!_waitingForList) {
|
||||
_waitingForList = true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void Reactions::preloadAnimationsFor(const QString &emoji) {
|
||||
const auto i = ranges::find(_available, emoji, &Reaction::emoji);
|
||||
if (i == end(_available)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto preload = [&](DocumentData *document) {
|
||||
const auto view = document
|
||||
? document->activeMediaView()
|
||||
: nullptr;
|
||||
if (view) {
|
||||
view->checkStickerLarge();
|
||||
}
|
||||
};
|
||||
preload(i->centerIcon);
|
||||
preload(i->aroundAnimation);
|
||||
}
|
||||
|
||||
QImage Reactions::resolveImageFor(
|
||||
const QString &emoji,
|
||||
ImageSize size) {
|
||||
@@ -99,6 +149,39 @@ QImage Reactions::resolveImageFor(
|
||||
preloadImageFor(emoji);
|
||||
}
|
||||
auto &set = (i != end(_images)) ? i->second : _images[emoji];
|
||||
const auto resolve = [&](QImage &image, int size) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto frameSize = set.fromAppearAnimation
|
||||
? (size / 2)
|
||||
: size;
|
||||
image = set.icon->frame().scaled(
|
||||
frameSize * factor,
|
||||
frameSize * factor,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
if (set.fromAppearAnimation) {
|
||||
auto result = QImage(
|
||||
size * factor,
|
||||
size * factor,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
|
||||
auto p = QPainter(&result);
|
||||
p.drawImage(
|
||||
(size - frameSize) * factor / 2,
|
||||
(size - frameSize) * factor / 2,
|
||||
image);
|
||||
p.end();
|
||||
|
||||
std::swap(result, image);
|
||||
}
|
||||
image.setDevicePixelRatio(factor);
|
||||
};
|
||||
if (set.bottomInfo.isNull() && set.icon) {
|
||||
resolve(set.bottomInfo, st::reactionInfoImage);
|
||||
resolve(set.inlineList, st::reactionInlineImage);
|
||||
crl::async([icon = std::move(set.icon)]{});
|
||||
}
|
||||
switch (size) {
|
||||
case ImageSize::BottomInfo: return set.bottomInfo;
|
||||
case ImageSize::InlineList: return set.inlineList;
|
||||
@@ -108,15 +191,17 @@ QImage Reactions::resolveImageFor(
|
||||
|
||||
void Reactions::resolveImages() {
|
||||
for (auto &[emoji, set] : _images) {
|
||||
if (!set.bottomInfo.isNull() || set.media) {
|
||||
if (!set.bottomInfo.isNull() || set.icon || set.media) {
|
||||
continue;
|
||||
}
|
||||
const auto i = ranges::find(_available, emoji, &Reaction::emoji);
|
||||
const auto document = (i != end(_available))
|
||||
? i->staticIcon.get()
|
||||
: nullptr;
|
||||
const auto document = (i == end(_available))
|
||||
? nullptr
|
||||
: i->centerIcon
|
||||
? i->centerIcon
|
||||
: i->appearAnimation.get();
|
||||
if (document) {
|
||||
loadImage(set, document);
|
||||
loadImage(set, document, !i->centerIcon);
|
||||
} else {
|
||||
LOG(("API Error: Reaction for emoji '%1' not found!"
|
||||
).arg(emoji));
|
||||
@@ -126,14 +211,17 @@ void Reactions::resolveImages() {
|
||||
|
||||
void Reactions::loadImage(
|
||||
ImageSet &set,
|
||||
not_null<DocumentData*> document) {
|
||||
if (!set.bottomInfo.isNull()) {
|
||||
not_null<DocumentData*> document,
|
||||
bool fromAppearAnimation) {
|
||||
if (!set.bottomInfo.isNull() || set.icon) {
|
||||
return;
|
||||
} else if (!set.media) {
|
||||
set.fromAppearAnimation = fromAppearAnimation;
|
||||
set.media = document->createMediaView();
|
||||
set.media->checkStickerLarge();
|
||||
}
|
||||
if (const auto image = set.media->getStickerLarge()) {
|
||||
setImage(set, image->original());
|
||||
if (set.media->loaded()) {
|
||||
setLottie(set);
|
||||
} else if (!_imagesLoadLifetime) {
|
||||
document->session().downloaderTaskFinished(
|
||||
) | rpl::start_with_next([=] {
|
||||
@@ -142,20 +230,15 @@ void Reactions::loadImage(
|
||||
}
|
||||
}
|
||||
|
||||
void Reactions::setImage(ImageSet &set, QImage large) {
|
||||
void Reactions::setLottie(ImageSet &set) {
|
||||
const auto size = style::ConvertScale(kSizeForDownscale);
|
||||
set.icon = std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{
|
||||
.path = set.media->owner()->filepath(true),
|
||||
.json = set.media->bytes(),
|
||||
.sizeOverride = QSize(size, size),
|
||||
.frame = -1,
|
||||
});
|
||||
set.media = nullptr;
|
||||
const auto scale = [&](int size) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
return Images::prepare(
|
||||
large,
|
||||
size * factor,
|
||||
size * factor,
|
||||
Images::Option::Smooth,
|
||||
size,
|
||||
size);
|
||||
};
|
||||
set.bottomInfo = scale(st::reactionInfoSize);
|
||||
set.inlineList = scale(st::reactionBottomSize);
|
||||
}
|
||||
|
||||
void Reactions::downloadTaskFinished() {
|
||||
@@ -163,8 +246,8 @@ void Reactions::downloadTaskFinished() {
|
||||
for (auto &[emoji, set] : _images) {
|
||||
if (!set.media) {
|
||||
continue;
|
||||
} else if (const auto image = set.media->getStickerLarge()) {
|
||||
setImage(set, image->original());
|
||||
} else if (set.media->loaded()) {
|
||||
setLottie(set);
|
||||
} else {
|
||||
hasOne = true;
|
||||
}
|
||||
@@ -174,33 +257,17 @@ void Reactions::downloadTaskFinished() {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Reaction> Reactions::Filtered(
|
||||
const std::vector<Reaction> &reactions,
|
||||
const std::vector<QString> &emoji) {
|
||||
auto result = std::vector<Reaction>();
|
||||
result.reserve(emoji.size());
|
||||
for (const auto &single : emoji) {
|
||||
const auto i = ranges::find(reactions, single, &Reaction::emoji);
|
||||
if (i != end(reactions)) {
|
||||
result.push_back(*i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Reaction> Reactions::filtered(
|
||||
const std::vector<QString> &emoji) const {
|
||||
return Filtered(list(Type::Active), emoji);
|
||||
}
|
||||
|
||||
std::vector<QString> Reactions::ParseAllowed(
|
||||
base::flat_set<QString> Reactions::ParseAllowed(
|
||||
const MTPVector<MTPstring> *list) {
|
||||
if (!list) {
|
||||
return {};
|
||||
}
|
||||
return list->v | ranges::view::transform([](const MTPstring &string) {
|
||||
const auto parsed = ranges::views::all(
|
||||
list->v
|
||||
) | ranges::views::transform([](const MTPstring &string) {
|
||||
return qs(string);
|
||||
}) | ranges::to_vector;
|
||||
return { begin(parsed), end(parsed) };
|
||||
}
|
||||
|
||||
void Reactions::request() {
|
||||
@@ -213,26 +280,7 @@ void Reactions::request() {
|
||||
)).done([=](const MTPmessages_AvailableReactions &result) {
|
||||
_requestId = 0;
|
||||
result.match([&](const MTPDmessages_availableReactions &data) {
|
||||
_hash = data.vhash().v;
|
||||
|
||||
const auto &list = data.vreactions().v;
|
||||
_active.clear();
|
||||
_available.clear();
|
||||
_active.reserve(list.size());
|
||||
_available.reserve(list.size());
|
||||
for (const auto &reaction : list) {
|
||||
if (const auto parsed = parse(reaction)) {
|
||||
_available.push_back(*parsed);
|
||||
if (parsed->active) {
|
||||
_active.push_back(*parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_waitingForList) {
|
||||
_waitingForList = false;
|
||||
resolveImages();
|
||||
}
|
||||
_updated.fire({});
|
||||
updateFromData(data);
|
||||
}, [&](const MTPDmessages_availableReactionsNotModified &) {
|
||||
});
|
||||
}).fail([=] {
|
||||
@@ -241,6 +289,40 @@ void Reactions::request() {
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Reactions::updateFromData(const MTPDmessages_availableReactions &data) {
|
||||
_hash = data.vhash().v;
|
||||
|
||||
const auto &list = data.vreactions().v;
|
||||
const auto oldCache = base::take(_iconsCache);
|
||||
const auto toCache = [&](DocumentData *document) {
|
||||
if (document) {
|
||||
_iconsCache.emplace(document, document->createMediaView());
|
||||
}
|
||||
};
|
||||
_active.clear();
|
||||
_available.clear();
|
||||
_active.reserve(list.size());
|
||||
_available.reserve(list.size());
|
||||
_iconsCache.reserve(list.size() * 4);
|
||||
for (const auto &reaction : list) {
|
||||
if (const auto parsed = parse(reaction)) {
|
||||
_available.push_back(*parsed);
|
||||
if (parsed->active) {
|
||||
_active.push_back(*parsed);
|
||||
toCache(parsed->appearAnimation);
|
||||
toCache(parsed->selectAnimation);
|
||||
toCache(parsed->centerIcon);
|
||||
toCache(parsed->aroundAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_waitingForList) {
|
||||
_waitingForList = false;
|
||||
resolveImages();
|
||||
}
|
||||
_updated.fire({});
|
||||
}
|
||||
|
||||
std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
|
||||
return entry.match([&](const MTPDavailableReaction &data) {
|
||||
const auto emoji = qs(data.vreaction());
|
||||
@@ -248,6 +330,8 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
|
||||
if (!known) {
|
||||
LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji));
|
||||
}
|
||||
const auto selectAnimation = _owner->processDocument(
|
||||
data.vselect_animation());
|
||||
return known
|
||||
? std::make_optional(Reaction{
|
||||
.emoji = emoji,
|
||||
@@ -255,12 +339,18 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
|
||||
.staticIcon = _owner->processDocument(data.vstatic_icon()),
|
||||
.appearAnimation = _owner->processDocument(
|
||||
data.vappear_animation()),
|
||||
.selectAnimation = _owner->processDocument(
|
||||
data.vselect_animation()),
|
||||
.activateAnimation = _owner->processDocument(
|
||||
data.vactivate_animation()),
|
||||
.activateEffects = _owner->processDocument(
|
||||
data.veffect_animation()),
|
||||
.selectAnimation = selectAnimation,
|
||||
//.activateAnimation = _owner->processDocument(
|
||||
// data.vactivate_animation()),
|
||||
//.activateEffects = _owner->processDocument(
|
||||
// data.veffect_animation()),
|
||||
.centerIcon = (data.vcenter_icon()
|
||||
? _owner->processDocument(*data.vcenter_icon()).get()
|
||||
: nullptr),
|
||||
.aroundAnimation = (data.varound_animation()
|
||||
? _owner->processDocument(
|
||||
*data.varound_animation()).get()
|
||||
: nullptr),
|
||||
.active = !data.is_inactive(),
|
||||
})
|
||||
: std::nullopt;
|
||||
@@ -389,19 +479,32 @@ void MessageReactions::add(const QString &reaction) {
|
||||
if (_chosen == reaction) {
|
||||
return;
|
||||
}
|
||||
const auto history = _item->history();
|
||||
const auto self = history->session().user();
|
||||
if (!_chosen.isEmpty()) {
|
||||
const auto i = _list.find(_chosen);
|
||||
Assert(i != end(_list));
|
||||
--i->second;
|
||||
if (!i->second) {
|
||||
const auto removed = !i->second;
|
||||
if (removed) {
|
||||
_list.erase(i);
|
||||
}
|
||||
const auto j = _recent.find(_chosen);
|
||||
if (j != end(_recent)) {
|
||||
j->second.erase(ranges::remove(j->second, self), end(j->second));
|
||||
if (j->second.empty() || removed) {
|
||||
_recent.erase(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
_chosen = reaction;
|
||||
if (!reaction.isEmpty()) {
|
||||
if (_item->canViewReactions()) {
|
||||
_recent[reaction].push_back(self);
|
||||
}
|
||||
++_list[reaction];
|
||||
}
|
||||
auto &owner = _item->history()->owner();
|
||||
auto &owner = history->owner();
|
||||
owner.reactions().send(_item, _chosen);
|
||||
owner.notifyItemDataChange(_item);
|
||||
}
|
||||
@@ -412,8 +515,10 @@ void MessageReactions::remove() {
|
||||
|
||||
void MessageReactions::set(
|
||||
const QVector<MTPReactionCount> &list,
|
||||
const QVector<MTPMessageUserReaction> &recent,
|
||||
bool ignoreChosen) {
|
||||
if (_item->history()->owner().reactions().sending(_item)) {
|
||||
auto &owner = _item->history()->owner();
|
||||
if (owner.reactions().sending(_item)) {
|
||||
// We'll apply non-stale data from the request response.
|
||||
return;
|
||||
}
|
||||
@@ -450,8 +555,24 @@ void MessageReactions::set(
|
||||
_chosen = QString();
|
||||
}
|
||||
}
|
||||
auto parsed = base::flat_map<
|
||||
QString,
|
||||
std::vector<not_null<UserData*>>>();
|
||||
for (const auto &reaction : recent) {
|
||||
reaction.match([&](const MTPDmessageUserReaction &data) {
|
||||
const auto emoji = qs(data.vreaction());
|
||||
if (_list.contains(emoji)) {
|
||||
parsed[emoji].push_back(owner.user(data.vuser_id()));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_recent != parsed) {
|
||||
_recent = std::move(parsed);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
_item->history()->owner().notifyItemDataChange(_item);
|
||||
owner.notifyItemDataChange(_item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,6 +580,11 @@ const base::flat_map<QString, int> &MessageReactions::list() const {
|
||||
return _list;
|
||||
}
|
||||
|
||||
auto MessageReactions::recent() const
|
||||
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
|
||||
return _recent;
|
||||
}
|
||||
|
||||
bool MessageReactions::empty() const {
|
||||
return _list.empty();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "base/timer.h"
|
||||
|
||||
namespace Lottie {
|
||||
class Icon;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Data {
|
||||
|
||||
class DocumentMedia;
|
||||
@@ -20,14 +24,17 @@ struct Reaction {
|
||||
not_null<DocumentData*> staticIcon;
|
||||
not_null<DocumentData*> appearAnimation;
|
||||
not_null<DocumentData*> selectAnimation;
|
||||
not_null<DocumentData*> activateAnimation;
|
||||
not_null<DocumentData*> activateEffects;
|
||||
//not_null<DocumentData*> activateAnimation;
|
||||
//not_null<DocumentData*> activateEffects;
|
||||
DocumentData *centerIcon = nullptr;
|
||||
DocumentData *aroundAnimation = nullptr;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
class Reactions final {
|
||||
public:
|
||||
explicit Reactions(not_null<Session*> owner);
|
||||
~Reactions();
|
||||
|
||||
void refresh();
|
||||
|
||||
@@ -36,15 +43,10 @@ public:
|
||||
All,
|
||||
};
|
||||
[[nodiscard]] const std::vector<Reaction> &list(Type type) const;
|
||||
[[nodiscard]] std::vector<Reaction> list(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] QString favorite() const;
|
||||
void setFavorite(const QString &emoji);
|
||||
|
||||
[[nodiscard]] static std::vector<Reaction> Filtered(
|
||||
const std::vector<Reaction> &reactions,
|
||||
const std::vector<QString> &emoji);
|
||||
[[nodiscard]] std::vector<Reaction> filtered(
|
||||
const std::vector<QString> &emoji) const;
|
||||
|
||||
[[nodiscard]] static std::vector<QString> ParseAllowed(
|
||||
[[nodiscard]] static base::flat_set<QString> ParseAllowed(
|
||||
const MTPVector<MTPstring> *list);
|
||||
|
||||
[[nodiscard]] rpl::producer<> updates() const;
|
||||
@@ -54,6 +56,7 @@ public:
|
||||
InlineList,
|
||||
};
|
||||
void preloadImageFor(const QString &emoji);
|
||||
void preloadAnimationsFor(const QString &emoji);
|
||||
[[nodiscard]] QImage resolveImageFor(
|
||||
const QString &emoji,
|
||||
ImageSize size);
|
||||
@@ -70,15 +73,21 @@ private:
|
||||
QImage bottomInfo;
|
||||
QImage inlineList;
|
||||
std::shared_ptr<DocumentMedia> media;
|
||||
std::unique_ptr<Lottie::Icon> icon;
|
||||
bool fromAppearAnimation = false;
|
||||
};
|
||||
|
||||
void request();
|
||||
void updateFromData(const MTPDmessages_availableReactions &data);
|
||||
|
||||
[[nodiscard]] std::optional<Reaction> parse(
|
||||
const MTPAvailableReaction &entry);
|
||||
|
||||
void loadImage(ImageSet &set, not_null<DocumentData*> document);
|
||||
void setImage(ImageSet &set, QImage large);
|
||||
void loadImage(
|
||||
ImageSet &set,
|
||||
not_null<DocumentData*> document,
|
||||
bool fromAppearAnimation);
|
||||
void setLottie(ImageSet &set);
|
||||
void resolveImages();
|
||||
void downloadTaskFinished();
|
||||
|
||||
@@ -89,6 +98,10 @@ private:
|
||||
|
||||
std::vector<Reaction> _active;
|
||||
std::vector<Reaction> _available;
|
||||
QString _favorite;
|
||||
base::flat_map<
|
||||
not_null<DocumentData*>,
|
||||
std::shared_ptr<Data::DocumentMedia>> _iconsCache;
|
||||
rpl::event_stream<> _updated;
|
||||
|
||||
mtpRequestId _requestId = 0;
|
||||
@@ -106,6 +119,8 @@ private:
|
||||
base::flat_set<not_null<HistoryItem*>> _pollingItems;
|
||||
mtpRequestId _pollRequestId = 0;
|
||||
|
||||
mtpRequestId _saveFaveRequestId = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
@@ -116,8 +131,13 @@ public:
|
||||
|
||||
void add(const QString &reaction);
|
||||
void remove();
|
||||
void set(const QVector<MTPReactionCount> &list, bool ignoreChosen);
|
||||
void set(
|
||||
const QVector<MTPReactionCount> &list,
|
||||
const QVector<MTPMessageUserReaction> &recent,
|
||||
bool ignoreChosen);
|
||||
[[nodiscard]] const base::flat_map<QString, int> &list() const;
|
||||
[[nodiscard]] auto recent() const
|
||||
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> &;
|
||||
[[nodiscard]] QString chosen() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
@@ -126,6 +146,7 @@ private:
|
||||
|
||||
QString _chosen;
|
||||
base::flat_map<QString, int> _list;
|
||||
base::flat_map<QString, std::vector<not_null<UserData*>>> _recent;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ class CloudImageView;
|
||||
int PeerColorIndex(PeerId peerId);
|
||||
int PeerColorIndex(BareId bareId);
|
||||
style::color PeerUserpicColor(PeerId peerId);
|
||||
|
||||
// Must be used only for PeerColor-s.
|
||||
PeerId FakePeerIdForJustName(const QString &name);
|
||||
|
||||
class RestrictionCheckResult {
|
||||
|
||||
@@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "base/unixtime.h"
|
||||
|
||||
@@ -497,18 +499,36 @@ rpl::producer<QImage> PeerUserpicImageValue(
|
||||
};
|
||||
}
|
||||
|
||||
rpl::producer<std::vector<Data::Reaction>> PeerAllowedReactionsValue(
|
||||
std::optional<base::flat_set<QString>> PeerAllowedReactions(
|
||||
not_null<PeerData*> peer) {
|
||||
return rpl::combine(
|
||||
rpl::single(
|
||||
rpl::empty_value()
|
||||
) | rpl::then(peer->owner().reactions().updates()),
|
||||
peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::Reactions)
|
||||
) | rpl::map([=] {
|
||||
return peer->owner().reactions().list(peer);
|
||||
if (const auto chat = peer->asChat()) {
|
||||
return chat->allowedReactions();
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
return channel->allowedReactions();
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto PeerAllowedReactionsValue(
|
||||
not_null<PeerData*> peer)
|
||||
-> rpl::producer<std::optional<base::flat_set<QString>>> {
|
||||
return peer->session().changes().peerFlagsValue(
|
||||
peer,
|
||||
Data::PeerUpdate::Flag::Reactions
|
||||
) | rpl::map([=]{
|
||||
return PeerAllowedReactions(peer);
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<int> UniqueReactionsLimitValue(
|
||||
not_null<Main::Session*> session) {
|
||||
const auto config = &session->account().appConfig();
|
||||
return config->value(
|
||||
) | rpl::map([=] {
|
||||
return int(base::SafeRound(
|
||||
config->get<double>("reactions_uniq_max", 11)));
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
} // namespace Data
|
||||
|
||||
@@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct Reaction;
|
||||
@@ -122,7 +126,12 @@ inline auto PeerFullFlagValue(
|
||||
int size,
|
||||
ImageRoundRadius radius);
|
||||
|
||||
[[nodiscard]] std::optional<base::flat_set<QString>> PeerAllowedReactions(
|
||||
not_null<PeerData*> peer);
|
||||
[[nodiscard]] auto PeerAllowedReactionsValue(not_null<PeerData*> peer)
|
||||
-> rpl::producer<std::vector<Data::Reaction>>;
|
||||
-> rpl::producer<std::optional<base::flat_set<QString>>>;
|
||||
|
||||
[[nodiscard]] rpl::producer<int> UniqueReactionsLimitValue(
|
||||
not_null<Main::Session*> session);
|
||||
|
||||
} // namespace Data
|
||||
|
||||
@@ -32,7 +32,7 @@ constexpr auto kMessagesPerPage = 50;
|
||||
history->nextNonHistoryEntryId(),
|
||||
MessageFlag::FakeHistoryItem,
|
||||
date,
|
||||
HistoryService::PreparedText{ text });
|
||||
HistoryService::PreparedText{ { .text = text } });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -287,7 +287,7 @@ void RepliesList::injectRootDivider(
|
||||
text());
|
||||
} else if (_dividerWithComments != withComments) {
|
||||
_dividerWithComments = withComments;
|
||||
_divider->setServiceText(HistoryService::PreparedText{ text() });
|
||||
_divider->setServiceText(HistoryService::PreparedText{ { text() } });
|
||||
}
|
||||
slice->ids.push_back(_divider->fullId());
|
||||
}
|
||||
|
||||
@@ -3015,11 +3015,11 @@ not_null<WebPageData*> Session::webpage(
|
||||
void Session::webpageApplyFields(
|
||||
not_null<WebPageData*> page,
|
||||
const MTPDwebPage &data) {
|
||||
auto description = TextWithEntities {
|
||||
TextUtilities::Clean(qs(data.vdescription().value_or_empty()))
|
||||
auto description = TextWithEntities{
|
||||
qs(data.vdescription().value_or_empty())
|
||||
};
|
||||
const auto siteName = qs(data.vsite_name().value_or_empty());
|
||||
auto parseFlags = TextParseLinks | TextParseMultiline | TextParseRichText;
|
||||
auto parseFlags = TextParseLinks | TextParseMultiline;
|
||||
if (siteName == qstr("Twitter") || siteName == qstr("Instagram")) {
|
||||
parseFlags |= TextParseHashtags | TextParseMentions;
|
||||
}
|
||||
@@ -3191,9 +3191,9 @@ void Session::gameApplyFields(
|
||||
return;
|
||||
}
|
||||
game->accessHash = accessHash;
|
||||
game->shortName = TextUtilities::Clean(shortName);
|
||||
game->shortName = shortName;
|
||||
game->title = TextUtilities::SingleLine(title);
|
||||
game->description = TextUtilities::Clean(description);
|
||||
game->description = description;
|
||||
game->photo = photo;
|
||||
game->document = document;
|
||||
notifyGameUpdateDelayed(game);
|
||||
|
||||
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "api/api_text_entities.h"
|
||||
#include "apiwrap.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_peer_id.h"
|
||||
#include "data/data_session.h"
|
||||
@@ -71,23 +72,10 @@ bool SponsoredMessages::append(not_null<History*> history) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto flags = MessageFlags(0)
|
||||
| (history->peer->isChannel() ? MessageFlag::Post : MessageFlags(0))
|
||||
| MessageFlag::HasFromId
|
||||
| MessageFlag::IsSponsored
|
||||
| MessageFlag::Local;
|
||||
auto local = history->addNewLocalMessage(
|
||||
entryIt->item.reset(history->addNewLocalMessage(
|
||||
_session->data().nextLocalMessageId(),
|
||||
flags,
|
||||
UserId(0),
|
||||
MsgId(0),
|
||||
HistoryItem::NewMessageDate(0),
|
||||
entryIt->sponsored.fromId,
|
||||
QString(),
|
||||
entryIt->sponsored.textWithEntities,
|
||||
MTP_messageMediaEmpty(),
|
||||
HistoryMessageMarkupData());
|
||||
entryIt->item.reset(std::move(local));
|
||||
entryIt->sponsored.from,
|
||||
entryIt->sponsored.textWithEntities));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -163,36 +151,53 @@ void SponsoredMessages::append(
|
||||
});
|
||||
const auto randomId = data.vrandom_id().v;
|
||||
const auto hash = qs(data.vchat_invite_hash().value_or_empty());
|
||||
const auto fromId = [&] {
|
||||
const auto makeFrom = [](
|
||||
not_null<PeerData*> peer,
|
||||
bool exactPost = false) {
|
||||
const auto channel = peer->asChannel();
|
||||
return SponsoredFrom{
|
||||
.peer = peer,
|
||||
.title = peer->name,
|
||||
.isBroadcast = (channel && channel->isBroadcast()),
|
||||
.isMegagroup = (channel && channel->isMegagroup()),
|
||||
.isChannel = (channel != nullptr),
|
||||
.isPublic = (channel && channel->isPublic()),
|
||||
.isBot = (peer->isUser() && peer->asUser()->isBot()),
|
||||
.isExactPost = exactPost,
|
||||
};
|
||||
};
|
||||
const auto from = [&]() -> SponsoredFrom {
|
||||
if (data.vfrom_id()) {
|
||||
return peerFromMTP(*data.vfrom_id());
|
||||
return makeFrom(
|
||||
_session->data().peer(peerFromMTP(*data.vfrom_id())),
|
||||
(data.vchannel_post() != nullptr));
|
||||
}
|
||||
Assert(data.vchat_invite());
|
||||
return data.vchat_invite()->match([](const MTPDchatInvite &data) {
|
||||
return Data::FakePeerIdForJustName(qs(data.vtitle()));
|
||||
return SponsoredFrom{
|
||||
.title = qs(data.vtitle()),
|
||||
.isBroadcast = data.is_broadcast(),
|
||||
.isMegagroup = data.is_megagroup(),
|
||||
.isChannel = data.is_channel(),
|
||||
.isPublic = data.is_public(),
|
||||
};
|
||||
}, [&](const MTPDchatInviteAlready &data) {
|
||||
const auto chat = _session->data().processChat(data.vchat());
|
||||
if (!chat) {
|
||||
return PeerId(0);
|
||||
}
|
||||
if (const auto channel = chat->asChannel()) {
|
||||
channel->clearInvitePeek();
|
||||
}
|
||||
return chat->id;
|
||||
return makeFrom(chat);
|
||||
}, [&](const MTPDchatInvitePeek &data) {
|
||||
const auto chat = _session->data().processChat(data.vchat());
|
||||
if (!chat) {
|
||||
return PeerId(0);
|
||||
}
|
||||
if (const auto channel = chat->asChannel()) {
|
||||
channel->setInvitePeek(hash, data.vexpires().v);
|
||||
}
|
||||
return chat->id;
|
||||
return makeFrom(chat);
|
||||
});
|
||||
}();
|
||||
auto sharedMessage = SponsoredMessage{
|
||||
.randomId = randomId,
|
||||
.fromId = fromId,
|
||||
.from = from,
|
||||
.textWithEntities = {
|
||||
.text = qs(data.vmessage()),
|
||||
.entities = Api::EntitiesFromMTP(
|
||||
@@ -263,17 +268,17 @@ void SponsoredMessages::view(const FullMsgId &fullId) {
|
||||
}).send();
|
||||
}
|
||||
|
||||
SponsoredMessages::ChannelPost SponsoredMessages::channelPost(
|
||||
SponsoredMessages::Details SponsoredMessages::lookupDetails(
|
||||
const FullMsgId &fullId) const {
|
||||
const auto entryPtr = find(fullId);
|
||||
if (!entryPtr) {
|
||||
return { .msgId = ShowAtUnreadMsgId, .hash = std::nullopt };
|
||||
return {};
|
||||
}
|
||||
const auto msgId = entryPtr->sponsored.msgId;
|
||||
const auto hash = entryPtr->sponsored.chatInviteHash;
|
||||
const auto &hash = entryPtr->sponsored.chatInviteHash;
|
||||
return {
|
||||
.msgId = msgId ? msgId : ShowAtUnreadMsgId,
|
||||
.hash = hash.isEmpty() ? std::nullopt : std::make_optional(hash),
|
||||
.peer = entryPtr->sponsored.from.peer,
|
||||
.msgId = entryPtr->sponsored.msgId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,20 @@ namespace Data {
|
||||
|
||||
class Session;
|
||||
|
||||
struct SponsoredMessage final {
|
||||
struct SponsoredFrom {
|
||||
PeerData *peer = nullptr;
|
||||
QString title;
|
||||
bool isBroadcast = false;
|
||||
bool isMegagroup = false;
|
||||
bool isChannel = false;
|
||||
bool isPublic = false;
|
||||
bool isBot = false;
|
||||
bool isExactPost = false;
|
||||
};
|
||||
|
||||
struct SponsoredMessage {
|
||||
QByteArray randomId;
|
||||
PeerId fromId;
|
||||
SponsoredFrom from;
|
||||
TextWithEntities textWithEntities;
|
||||
History *history = nullptr;
|
||||
MsgId msgId;
|
||||
@@ -31,9 +42,10 @@ struct SponsoredMessage final {
|
||||
|
||||
class SponsoredMessages final {
|
||||
public:
|
||||
struct ChannelPost {
|
||||
MsgId msgId;
|
||||
struct Details {
|
||||
std::optional<QString> hash;
|
||||
PeerData *peer = nullptr;
|
||||
MsgId msgId;
|
||||
};
|
||||
using RandomId = QByteArray;
|
||||
explicit SponsoredMessages(not_null<Session*> owner);
|
||||
@@ -45,7 +57,7 @@ public:
|
||||
void request(not_null<History*> history);
|
||||
[[nodiscard]] bool append(not_null<History*> history);
|
||||
void clearItems(not_null<History*> history);
|
||||
[[nodiscard]] ChannelPost channelPost(const FullMsgId &fullId) const;
|
||||
[[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const;
|
||||
|
||||
void view(const FullMsgId &fullId);
|
||||
|
||||
|
||||
@@ -280,9 +280,6 @@ enum class MessageFlag : uint32 {
|
||||
|
||||
// Contact sign-up message, notification should be skipped for Silent.
|
||||
IsContactSignUp = (1U << 30),
|
||||
|
||||
// In channels.
|
||||
IsSponsored = (1U << 31),
|
||||
};
|
||||
inline constexpr bool is_flag_type(MessageFlag) { return true; }
|
||||
using MessageFlags = base::flags<MessageFlag>;
|
||||
|
||||
@@ -221,14 +221,11 @@ bool WebPageData::applyChanges(
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto resultUrl = TextUtilities::Clean(newUrl);
|
||||
const auto resultDisplayUrl = TextUtilities::Clean(
|
||||
newDisplayUrl);
|
||||
const auto possibleSiteName = TextUtilities::Clean(
|
||||
newSiteName);
|
||||
const auto resultTitle = TextUtilities::SingleLine(
|
||||
newTitle);
|
||||
const auto resultAuthor = TextUtilities::Clean(newAuthor);
|
||||
const auto resultUrl = newUrl;
|
||||
const auto resultDisplayUrl = newDisplayUrl;
|
||||
const auto possibleSiteName = newSiteName;
|
||||
const auto resultTitle = TextUtilities::SingleLine(newTitle);
|
||||
const auto resultAuthor = newAuthor;
|
||||
|
||||
const auto viewTitleText = resultTitle.isEmpty()
|
||||
? TextUtilities::SingleLine(resultAuthor)
|
||||
|
||||
@@ -1052,7 +1052,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
if (anim::Disabled()
|
||||
&& (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) {
|
||||
mousePressReleased(e->globalPos(), e->button());
|
||||
mousePressReleased(e->globalPos(), e->button(), e->modifiers());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1274,12 +1274,13 @@ bool InnerWidget::pinnedShiftAnimationCallback(crl::time now) {
|
||||
}
|
||||
|
||||
void InnerWidget::mouseReleaseEvent(QMouseEvent *e) {
|
||||
mousePressReleased(e->globalPos(), e->button());
|
||||
mousePressReleased(e->globalPos(), e->button(), e->modifiers());
|
||||
}
|
||||
|
||||
void InnerWidget::mousePressReleased(
|
||||
QPoint globalPosition,
|
||||
Qt::MouseButton button) {
|
||||
Qt::MouseButton button,
|
||||
Qt::KeyboardModifiers modifiers) {
|
||||
auto wasDragging = (_dragging != nullptr);
|
||||
if (wasDragging) {
|
||||
updateReorderIndexGetCount();
|
||||
@@ -1322,7 +1323,7 @@ void InnerWidget::mousePressReleased(
|
||||
&& peerSearchPressed == _peerSearchSelected)
|
||||
|| (searchedPressed >= 0
|
||||
&& searchedPressed == _searchedSelected)) {
|
||||
chooseRow();
|
||||
chooseRow(modifiers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1758,7 +1759,7 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
_menuRow = row;
|
||||
if (_pressButton != Qt::LeftButton) {
|
||||
mousePressReleased(e->globalPos(), _pressButton);
|
||||
mousePressReleased(e->globalPos(), _pressButton, e->modifiers());
|
||||
}
|
||||
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
@@ -2347,8 +2348,9 @@ void InnerWidget::refreshSearchInChatLabel() {
|
||||
const auto fromUserText = tr::lng_dlg_search_from(
|
||||
tr::now,
|
||||
lt_user,
|
||||
textcmdLink(1, from));
|
||||
_searchFromUserText.setText(
|
||||
Ui::Text::Link(from),
|
||||
Ui::Text::WithEntities);
|
||||
_searchFromUserText.setMarkedText(
|
||||
st::dialogsSearchFromStyle,
|
||||
fromUserText,
|
||||
Ui::DialogTextOptions());
|
||||
@@ -2689,13 +2691,21 @@ ChosenRow InnerWidget::computeChosenRow() const {
|
||||
return ChosenRow();
|
||||
}
|
||||
|
||||
bool InnerWidget::chooseRow() {
|
||||
bool InnerWidget::chooseRow(Qt::KeyboardModifiers modifiers) {
|
||||
if (chooseCollapsedRow()) {
|
||||
return true;
|
||||
} else if (chooseHashtag()) {
|
||||
return true;
|
||||
}
|
||||
const auto chosen = computeChosenRow();
|
||||
const auto modifyChosenRow = [](
|
||||
ChosenRow row,
|
||||
Qt::KeyboardModifiers modifiers) {
|
||||
#ifdef _DEBUG
|
||||
row.newWindow = (modifiers & Qt::ControlModifier);
|
||||
#endif
|
||||
return row;
|
||||
};
|
||||
const auto chosen = modifyChosenRow(computeChosenRow(), modifiers);
|
||||
if (chosen.key) {
|
||||
if (IsServerMsgId(chosen.message.fullId.msg)) {
|
||||
session().local().saveRecentSearchHashtags(_filter);
|
||||
|
||||
@@ -46,6 +46,7 @@ struct ChosenRow {
|
||||
Key key;
|
||||
Data::MessagePosition message;
|
||||
bool filteredRow = false;
|
||||
bool newWindow = false;
|
||||
};
|
||||
|
||||
enum class SearchRequestType {
|
||||
@@ -95,7 +96,7 @@ public:
|
||||
void refreshEmptyLabel();
|
||||
void resizeEmptyLabel();
|
||||
|
||||
bool chooseRow();
|
||||
bool chooseRow(Qt::KeyboardModifiers modifiers = {});
|
||||
|
||||
void scrollToEntry(const RowDescriptor &entry);
|
||||
|
||||
@@ -192,7 +193,10 @@ private:
|
||||
void refreshDialogRow(RowDescriptor row);
|
||||
|
||||
void clearMouseSelection(bool clearSelection = false);
|
||||
void mousePressReleased(QPoint globalPosition, Qt::MouseButton button);
|
||||
void mousePressReleased(
|
||||
QPoint globalPosition,
|
||||
Qt::MouseButton button,
|
||||
Qt::KeyboardModifiers modifiers);
|
||||
void clearIrrelevantState();
|
||||
void selectByMouse(QPoint globalPosition);
|
||||
void loadPeerPhotos();
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_peer_values.h"
|
||||
@@ -20,10 +21,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
QString ComposeFolderListEntryText(not_null<Data::Folder*> folder) {
|
||||
[[nodiscard]] TextWithEntities ComposeFolderListEntryText(
|
||||
not_null<Data::Folder*> folder) {
|
||||
const auto &list = folder->lastHistories();
|
||||
if (list.empty()) {
|
||||
return QString();
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto count = std::max(
|
||||
@@ -38,12 +40,16 @@ QString ComposeFolderListEntryText(not_null<Data::Folder*> folder) {
|
||||
list.size() - (throwAwayLastName ? 1 : 0)
|
||||
);
|
||||
const auto wrapName = [](not_null<History*> history) {
|
||||
const auto name = TextUtilities::Clean(history->peer->name);
|
||||
return (history->unreadCount() > 0)
|
||||
? (textcmdStartSemibold()
|
||||
+ textcmdLink(1, name)
|
||||
+ textcmdStopSemibold())
|
||||
: name;
|
||||
const auto name = history->peer->name;
|
||||
return TextWithEntities{
|
||||
.text = name,
|
||||
.entities = (history->unreadCount() > 0)
|
||||
? EntitiesInText{
|
||||
{ EntityType::Semibold, 0, int(name.size()), QString() },
|
||||
{ EntityType::CustomUrl, 0, int(name.size()), QString() },
|
||||
}
|
||||
: EntitiesInText{}
|
||||
};
|
||||
};
|
||||
const auto shown = int(peers.size());
|
||||
const auto accumulated = [&] {
|
||||
@@ -57,12 +63,19 @@ QString ComposeFolderListEntryText(not_null<Data::Folder*> folder) {
|
||||
lt_accumulated,
|
||||
result,
|
||||
lt_chat,
|
||||
wrapName(*i));
|
||||
wrapName(*i),
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return (shown < count)
|
||||
? tr::lng_archived_last(tr::now, lt_count, (count - shown), lt_chats, accumulated)
|
||||
? tr::lng_archived_last(
|
||||
tr::now,
|
||||
lt_count,
|
||||
(count - shown),
|
||||
lt_chats,
|
||||
accumulated,
|
||||
Ui::Text::WithEntities)
|
||||
: accumulated;
|
||||
}
|
||||
|
||||
@@ -277,7 +290,7 @@ void Row::validateListEntryCache() const {
|
||||
return;
|
||||
}
|
||||
_listEntryCacheVersion = version;
|
||||
_listEntryCache.setText(
|
||||
_listEntryCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
ComposeFolderListEntryText(folder),
|
||||
Ui::DialogTextOptions());
|
||||
@@ -288,4 +301,4 @@ FakeRow::FakeRow(Key searchInChat, not_null<HistoryItem*> item)
|
||||
, _item(item) {
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
} // namespace Dialogs
|
||||
|
||||
@@ -31,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peers/edit_participants_box.h"
|
||||
#include "window/window_adaptive.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/window_slide_animation.h"
|
||||
#include "window/window_connecting_widget.h"
|
||||
@@ -226,11 +227,24 @@ Widget::Widget(
|
||||
const auto openSearchResult = !controller->selectingPeer()
|
||||
&& row.filteredRow;
|
||||
if (const auto history = row.key.history()) {
|
||||
controller->content()->choosePeer(
|
||||
history->peer->id,
|
||||
(controller->uniqueChatsInSearchResults()
|
||||
? ShowAtUnreadMsgId
|
||||
: row.message.fullId.msg));
|
||||
const auto peer = history->peer;
|
||||
const auto showAtMsgId = controller->uniqueChatsInSearchResults()
|
||||
? ShowAtUnreadMsgId
|
||||
: row.message.fullId.msg;
|
||||
if (row.newWindow) {
|
||||
const auto active = controller->activeChatCurrent();
|
||||
if (const auto history = active.history()) {
|
||||
if (history->peer == peer) {
|
||||
controller->content()->ui_showPeerHistory(
|
||||
0,
|
||||
Window::SectionShow::Way::ClearStack,
|
||||
0);
|
||||
}
|
||||
}
|
||||
Core::App().ensureSeparateWindowForPeer(peer, showAtMsgId);
|
||||
} else {
|
||||
controller->content()->choosePeer(peer->id, showAtMsgId);
|
||||
}
|
||||
} else if (const auto folder = row.key.folder()) {
|
||||
controller->openFolder(folder);
|
||||
}
|
||||
@@ -593,6 +607,7 @@ void Widget::checkUpdateStatus() {
|
||||
Core::checkReadyUpdate();
|
||||
App::restart();
|
||||
});
|
||||
_connecting->raise();
|
||||
} else {
|
||||
if (!_updateTelegram) return;
|
||||
_updateTelegram.destroy();
|
||||
@@ -743,13 +758,13 @@ void Widget::escape() {
|
||||
controller()->closeFolder();
|
||||
} else if (!onCancelSearch()) {
|
||||
if (controller()->activeChatEntryCurrent().key) {
|
||||
cancelled();
|
||||
controller()->content()->dialogsCancelled();
|
||||
} else if (controller()->activeChatsFilterCurrent()) {
|
||||
controller()->setActiveChatsFilter(FilterId(0));
|
||||
}
|
||||
} else if (!_searchInChat && !controller()->selectingPeer()) {
|
||||
if (controller()->activeChatEntryCurrent().key) {
|
||||
cancelled();
|
||||
controller()->content()->dialogsCancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1802,7 +1817,7 @@ void Widget::onCancelSearchInChat() {
|
||||
}
|
||||
applyFilterUpdate(true);
|
||||
if (!isOneColumn && !controller()->selectingPeer()) {
|
||||
cancelled();
|
||||
controller()->content()->dialogsCancelled();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,9 +97,6 @@ public:
|
||||
|
||||
~Widget();
|
||||
|
||||
Q_SIGNALS:
|
||||
void cancelled();
|
||||
|
||||
public Q_SLOTS:
|
||||
void onDraggingScrollDelta(int delta);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "storage/localstorage.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/unread_badge.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "lang/lang_keys.h"
|
||||
@@ -367,11 +368,25 @@ void paintRow(
|
||||
if (!ShowSendActionInDialogs(history)
|
||||
|| !history->sendActionPainter()->paint(p, nameleft, texttop, availableWidth, fullWidth, color, ms)) {
|
||||
if (history->cloudDraftTextCache.isEmpty()) {
|
||||
auto draftWrapped = textcmdLink(1, tr::lng_dialogs_text_from_wrapped(tr::now, lt_from, tr::lng_from_draft(tr::now)));
|
||||
auto draftWrapped = Ui::Text::PlainLink(
|
||||
tr::lng_dialogs_text_from_wrapped(
|
||||
tr::now,
|
||||
lt_from,
|
||||
tr::lng_from_draft(tr::now)));
|
||||
auto draftText = supportMode
|
||||
? textcmdLink(1, Support::ChatOccupiedString(history))
|
||||
: tr::lng_dialogs_text_with_from(tr::now, lt_from_part, draftWrapped, lt_message, TextUtilities::Clean(draft->textWithTags.text));
|
||||
history->cloudDraftTextCache.setText(st::dialogsTextStyle, draftText, DialogTextOptions());
|
||||
? Ui::Text::PlainLink(
|
||||
Support::ChatOccupiedString(history))
|
||||
: tr::lng_dialogs_text_with_from(
|
||||
tr::now,
|
||||
lt_from_part,
|
||||
draftWrapped,
|
||||
lt_message,
|
||||
{ .text = draft->textWithTags.text },
|
||||
Ui::Text::WithEntities);
|
||||
history->cloudDraftTextCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
draftText,
|
||||
DialogTextOptions());
|
||||
}
|
||||
p.setPen(active ? st::dialogsTextFgActive : (selected ? st::dialogsTextFgOver : st::dialogsTextFg));
|
||||
if (supportMode) {
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/view/history_view_item_preview.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
@@ -109,17 +110,25 @@ void MessageView::paint(
|
||||
options.existing = &_imagesCache;
|
||||
auto preview = item->toPreview(options);
|
||||
if (!preview.images.empty() && preview.imagesInTextPosition > 0) {
|
||||
_senderCache.setText(
|
||||
auto sender = ::Ui::Text::Mid(
|
||||
preview.text,
|
||||
0,
|
||||
preview.imagesInTextPosition);
|
||||
TextUtilities::Trim(sender);
|
||||
_senderCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
preview.text.mid(0, preview.imagesInTextPosition).trimmed(),
|
||||
std::move(sender),
|
||||
DialogTextOptions());
|
||||
preview.text = preview.text.mid(preview.imagesInTextPosition);
|
||||
preview.text = ::Ui::Text::Mid(
|
||||
preview.text,
|
||||
preview.imagesInTextPosition);
|
||||
} else {
|
||||
_senderCache = { st::dialogsTextWidthMin };
|
||||
}
|
||||
_textCache.setText(
|
||||
TextUtilities::Trim(preview.text);
|
||||
_textCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
preview.text.trimmed(),
|
||||
preview.text,
|
||||
DialogTextOptions());
|
||||
_textCachedFor = item;
|
||||
_imagesCache = std::move(preview.images);
|
||||
@@ -191,18 +200,24 @@ void MessageView::paint(
|
||||
|
||||
HistoryView::ItemPreview PreviewWithSender(
|
||||
HistoryView::ItemPreview &&preview,
|
||||
const QString &sender) {
|
||||
auto textWithOffset = tr::lng_dialogs_text_with_from(
|
||||
const TextWithEntities &sender) {
|
||||
const auto textWithOffset = tr::lng_dialogs_text_with_from(
|
||||
tr::now,
|
||||
lt_from_part,
|
||||
sender.text,
|
||||
lt_message,
|
||||
preview.text.text,
|
||||
TextWithTagOffset<lt_from_part>::FromString);
|
||||
preview.text = tr::lng_dialogs_text_with_from(
|
||||
tr::now,
|
||||
lt_from_part,
|
||||
sender,
|
||||
lt_message,
|
||||
std::move(preview.text),
|
||||
TextWithTagOffset<lt_from_part>::FromString);
|
||||
preview.text = std::move(textWithOffset.text);
|
||||
Ui::Text::WithEntities);
|
||||
preview.imagesInTextPosition = (textWithOffset.offset < 0)
|
||||
? 0
|
||||
: textWithOffset.offset + sender.size();
|
||||
: textWithOffset.offset + sender.text.size();
|
||||
return std::move(preview);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,6 @@ private:
|
||||
|
||||
[[nodiscard]] HistoryView::ItemPreview PreviewWithSender(
|
||||
HistoryView::ItemPreview &&preview,
|
||||
const QString &sender);
|
||||
const TextWithEntities &sender);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
|
||||
@@ -18,6 +18,11 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
||||
return image;
|
||||
}
|
||||
if (mods.paint) {
|
||||
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
image = image.convertToFormat(
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
|
||||
Painter p(&image);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
|
||||
@@ -293,7 +293,23 @@ PhotoEditorControls::PhotoEditorControls(
|
||||
|
||||
controllers->undoController->setPerformRequestChanges(rpl::merge(
|
||||
_undoButton->clicks() | rpl::map_to(Undo::Undo),
|
||||
_redoButton->clicks() | rpl::map_to(Undo::Redo)));
|
||||
_redoButton->clicks() | rpl::map_to(Undo::Redo),
|
||||
_keyPresses.events(
|
||||
) | rpl::filter([=](not_null<QKeyEvent*> e) {
|
||||
using Mode = PhotoEditorMode::Mode;
|
||||
return (e->matches(QKeySequence::Undo)
|
||||
&& !_undoButton->isHidden()
|
||||
&& !_undoButton->testAttribute(
|
||||
Qt::WA_TransparentForMouseEvents)
|
||||
&& (_mode.current().mode == Mode::Paint))
|
||||
|| (e->matches(QKeySequence::Redo)
|
||||
&& !_redoButton->isHidden()
|
||||
&& !_redoButton->testAttribute(
|
||||
Qt::WA_TransparentForMouseEvents)
|
||||
&& (_mode.current().mode == Mode::Paint));
|
||||
}) | rpl::map([=](not_null<QKeyEvent*> e) {
|
||||
return e->matches(QKeySequence::Undo) ? Undo::Undo : Undo::Redo;
|
||||
})));
|
||||
|
||||
controllers->undoController->canPerformChanges(
|
||||
) | rpl::start_with_next([=](const UndoController::EnableRequest &r) {
|
||||
@@ -368,7 +384,8 @@ rpl::producer<> PhotoEditorControls::doneRequests() const {
|
||||
_transformDone->clicks() | rpl::to_empty,
|
||||
_paintDone->clicks() | rpl::to_empty,
|
||||
_keyPresses.events(
|
||||
) | rpl::filter([=](int key) {
|
||||
) | rpl::filter([=](not_null<QKeyEvent*> e) {
|
||||
const auto key = e->key();
|
||||
return ((key == Qt::Key_Enter) || (key == Qt::Key_Return))
|
||||
&& !_toggledBarAnimation.animating();
|
||||
}) | rpl::to_empty);
|
||||
@@ -379,7 +396,8 @@ rpl::producer<> PhotoEditorControls::cancelRequests() const {
|
||||
_transformCancel->clicks() | rpl::to_empty,
|
||||
_paintCancel->clicks() | rpl::to_empty,
|
||||
_keyPresses.events(
|
||||
) | rpl::filter([=](int key) {
|
||||
) | rpl::filter([=](not_null<QKeyEvent*> e) {
|
||||
const auto key = e->key();
|
||||
return (key == Qt::Key_Escape)
|
||||
&& !_toggledBarAnimation.animating();
|
||||
}) | rpl::to_empty);
|
||||
@@ -478,7 +496,7 @@ rpl::producer<bool> PhotoEditorControls::colorLineShownValue() const {
|
||||
}
|
||||
|
||||
bool PhotoEditorControls::handleKeyPress(not_null<QKeyEvent*> e) const {
|
||||
_keyPresses.fire(e->key());
|
||||
_keyPresses.fire(std::move(e));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ private:
|
||||
Ui::Animations::Simple _toggledBarAnimation;
|
||||
|
||||
rpl::variable<PhotoEditorMode> _mode;
|
||||
rpl::event_stream<int> _keyPresses;
|
||||
rpl::event_stream<not_null<QKeyEvent*>> _keyPresses;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -120,18 +120,6 @@ std::vector<ItemPtr> Scene::items(
|
||||
return copyItems;
|
||||
}
|
||||
|
||||
std::vector<not_null<DocumentData*>> Scene::attachedStickers() const {
|
||||
const auto allItems = items();
|
||||
|
||||
return ranges::views::all(
|
||||
allItems
|
||||
) | ranges::views::filter([](const ItemPtr &i) {
|
||||
return i->isVisible() && (i->type() == ItemSticker::Type);
|
||||
}) | ranges::views::transform([](const ItemPtr &i) {
|
||||
return static_cast<ItemSticker*>(i.get())->sticker();
|
||||
}) | ranges::to_vector;
|
||||
}
|
||||
|
||||
std::shared_ptr<float64> Scene::lastZ() const {
|
||||
return _lastZ;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ public:
|
||||
[[nodiscard]] rpl::producer<> addsItem() const;
|
||||
[[nodiscard]] rpl::producer<> removesItem() const;
|
||||
|
||||
[[nodiscard]] auto attachedStickers() const
|
||||
-> std::vector<not_null<DocumentData*>>;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<float64> lastZ() const;
|
||||
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QGraphicsSceneHoverEvent>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <QtMath>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
@@ -86,8 +86,8 @@ void ItemCanvas::clearPixmap() {
|
||||
_p = nullptr;
|
||||
|
||||
_pixmap = QPixmap(
|
||||
(scene()->sceneRect().size() * cIntRetinaFactor()).toSize());
|
||||
_pixmap.setDevicePixelRatio(cRetinaFactor());
|
||||
(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());
|
||||
_pixmap.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_pixmap.fill(Qt::transparent);
|
||||
|
||||
_p = std::make_unique<Painter>(&_pixmap);
|
||||
@@ -170,10 +170,10 @@ void ItemCanvas::handleMouseReleaseEvent(
|
||||
|
||||
if (_contentRect.isValid()) {
|
||||
const auto scaledContentRect = QRectF(
|
||||
_contentRect.x() * cRetinaFactor(),
|
||||
_contentRect.y() * cRetinaFactor(),
|
||||
_contentRect.width() * cRetinaFactor(),
|
||||
_contentRect.height() * cRetinaFactor());
|
||||
_contentRect.x() * style::DevicePixelRatio(),
|
||||
_contentRect.y() * style::DevicePixelRatio(),
|
||||
_contentRect.width() * style::DevicePixelRatio(),
|
||||
_contentRect.height() * style::DevicePixelRatio());
|
||||
|
||||
_grabContentRequests.fire({
|
||||
.pixmap = _pixmap.copy(scaledContentRect.toRect()),
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Editor {
|
||||
|
||||
ItemLine::ItemLine(const QPixmap &&pixmap)
|
||||
: _pixmap(std::move(pixmap))
|
||||
, _rect(QPointF(), _pixmap.size() / cRetinaFactor()) {
|
||||
, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) {
|
||||
}
|
||||
|
||||
QRectF ItemLine::boundingRect() const {
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "export/view/export_view_top_bar.h"
|
||||
|
||||
#include "export/view/export_view_content.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
@@ -39,13 +40,12 @@ void TopBar::updateData(Content &&content) {
|
||||
return;
|
||||
}
|
||||
const auto &row = content.rows[0];
|
||||
_info->setRichText(textcmdStartSemibold()
|
||||
+ TextUtilities::Clean(tr::lng_export_progress_title(tr::now))
|
||||
+ textcmdStopSemibold()
|
||||
+ QString::fromUtf8(" \xe2\x80\x93 ")
|
||||
+ TextUtilities::Clean(row.label)
|
||||
+ ' '
|
||||
+ textcmdLink(1, TextUtilities::Clean(row.info)));
|
||||
_info->setMarkedText(
|
||||
Ui::Text::Bold(tr::lng_export_progress_title(tr::now))
|
||||
.append(" \xe2\x80\x93 ")
|
||||
.append(row.label)
|
||||
.append(' ')
|
||||
.append(Ui::Text::PlainLink(row.info)));
|
||||
_progress->setValue(row.progress);
|
||||
}
|
||||
|
||||
|
||||
@@ -515,7 +515,7 @@ void InnerWidget::updateEmptyText() {
|
||||
? tr::lng_admin_log_no_results_search_text(
|
||||
tr::now,
|
||||
lt_query,
|
||||
TextUtilities::Clean(_searchQuery))
|
||||
_searchQuery)
|
||||
: hasFilter
|
||||
? tr::lng_admin_log_no_results_text(tr::now)
|
||||
: _channel->isMegagroup()
|
||||
@@ -1169,11 +1169,21 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
auto view = App::hoveredItem()
|
||||
? App::hoveredItem()
|
||||
: App::hoveredLinkItem();
|
||||
auto lnkPhoto = dynamic_cast<PhotoClickHandler*>(link.get());
|
||||
auto lnkDocument = dynamic_cast<DocumentClickHandler*>(link.get());
|
||||
auto lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideoFile() : false;
|
||||
auto lnkIsVoice = lnkDocument ? lnkDocument->document()->isVoiceMessage() : false;
|
||||
auto lnkIsAudio = lnkDocument ? lnkDocument->document()->isAudioFile() : false;
|
||||
const auto lnkPhotoId = PhotoId(link
|
||||
? link->property(kPhotoLinkMediaIdProperty).toULongLong()
|
||||
: 0);
|
||||
const auto lnkDocumentId = DocumentId(link
|
||||
? link->property(kDocumentLinkMediaIdProperty).toULongLong()
|
||||
: 0);
|
||||
const auto lnkPhoto = lnkPhotoId
|
||||
? session().data().photo(lnkPhotoId).get()
|
||||
: nullptr;
|
||||
const auto lnkDocument = lnkDocumentId
|
||||
? session().data().document(lnkDocumentId).get()
|
||||
: nullptr;
|
||||
auto lnkIsVideo = lnkDocument ? lnkDocument->isVideoFile() : false;
|
||||
auto lnkIsVoice = lnkDocument ? lnkDocument->isVoiceMessage() : false;
|
||||
auto lnkIsAudio = lnkDocument ? lnkDocument->isAudioFile() : false;
|
||||
const auto fromId = PeerId(link
|
||||
? link->property(kPeerLinkPeerIdProperty).toULongLong()
|
||||
: 0);
|
||||
@@ -1184,21 +1194,20 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
}, &st::menuIconCopy);
|
||||
}
|
||||
if (lnkPhoto) {
|
||||
const auto photo = lnkPhoto->photo();
|
||||
const auto media = photo->activeMediaView();
|
||||
if (!photo->isNull() && media && media->loaded()) {
|
||||
const auto media = lnkPhoto->activeMediaView();
|
||||
if (!lnkPhoto->isNull() && media && media->loaded()) {
|
||||
_menu->addAction(tr::lng_context_save_image(tr::now), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [=] {
|
||||
savePhotoToFile(photo);
|
||||
savePhotoToFile(lnkPhoto);
|
||||
}), &st::menuIconSaveImage);
|
||||
_menu->addAction(tr::lng_context_copy_image(tr::now), [=] {
|
||||
copyContextImage(photo);
|
||||
copyContextImage(lnkPhoto);
|
||||
}, &st::menuIconCopy);
|
||||
}
|
||||
if (photo->hasAttachedStickers()) {
|
||||
if (lnkPhoto->hasAttachedStickers()) {
|
||||
const auto controller = _controller;
|
||||
auto callback = [=] {
|
||||
auto &attached = session().api().attachedStickers();
|
||||
attached.requestAttachedStickerSets(controller, photo);
|
||||
attached.requestAttachedStickerSets(controller, lnkPhoto);
|
||||
};
|
||||
_menu->addAction(
|
||||
tr::lng_context_attached_stickers(tr::now),
|
||||
@@ -1206,22 +1215,21 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
&st::menuIconStickers);
|
||||
}
|
||||
} else {
|
||||
auto document = lnkDocument->document();
|
||||
if (document->loading()) {
|
||||
if (lnkDocument->loading()) {
|
||||
_menu->addAction(tr::lng_context_cancel_download(tr::now), [=] {
|
||||
cancelContextDownload(document);
|
||||
cancelContextDownload(lnkDocument);
|
||||
}, &st::menuIconCancel);
|
||||
} else {
|
||||
const auto itemId = view
|
||||
? view->data()->fullId()
|
||||
: FullMsgId();
|
||||
if (const auto item = document->session().data().message(itemId)) {
|
||||
if (const auto item = session().data().message(itemId)) {
|
||||
const auto notAutoplayedGif = [&] {
|
||||
return document->isGifv()
|
||||
return lnkDocument->isGifv()
|
||||
&& !Data::AutoDownload::ShouldAutoPlay(
|
||||
document->session().settings().autoDownload(),
|
||||
session().settings().autoDownload(),
|
||||
item->history()->peer,
|
||||
document);
|
||||
lnkDocument);
|
||||
}();
|
||||
if (notAutoplayedGif) {
|
||||
_menu->addAction(tr::lng_context_open_gif(tr::now), [=] {
|
||||
@@ -1229,19 +1237,19 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
}, &st::menuIconShowInChat);
|
||||
}
|
||||
}
|
||||
if (!document->filepath(true).isEmpty()) {
|
||||
if (!lnkDocument->filepath(true).isEmpty()) {
|
||||
_menu->addAction(Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), [=] {
|
||||
showContextInFolder(document);
|
||||
showContextInFolder(lnkDocument);
|
||||
}, &st::menuIconShowInFolder);
|
||||
}
|
||||
_menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
|
||||
saveDocumentToFile(document);
|
||||
_menu->addAction(lnkIsVideo ? tr::lng_context_save_video(tr::now) : (lnkIsVoice ? tr::lng_context_save_audio(tr::now) : (lnkIsAudio ? tr::lng_context_save_audio_file(tr::now) : tr::lng_context_save_file(tr::now))), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, lnkDocument] {
|
||||
saveDocumentToFile(lnkDocument);
|
||||
}), &st::menuIconDownload);
|
||||
if (document->hasAttachedStickers()) {
|
||||
if (lnkDocument->hasAttachedStickers()) {
|
||||
const auto controller = _controller;
|
||||
auto callback = [=, doc = document] {
|
||||
auto callback = [=] {
|
||||
auto &attached = session().api().attachedStickers();
|
||||
attached.requestAttachedStickerSets(controller, doc);
|
||||
attached.requestAttachedStickerSets(controller, lnkDocument);
|
||||
};
|
||||
_menu->addAction(
|
||||
tr::lng_context_attached_stickers(tr::now),
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace {
|
||||
TextWithEntities PrepareText(
|
||||
const QString &value,
|
||||
const QString &emptyValue) {
|
||||
auto result = TextWithEntities { TextUtilities::Clean(value) };
|
||||
auto result = TextWithEntities{ value };
|
||||
if (result.text.isEmpty()) {
|
||||
result.text = emptyValue;
|
||||
if (!emptyValue.isEmpty()) {
|
||||
@@ -143,7 +143,7 @@ TextWithEntities ExtractEditedText(
|
||||
}
|
||||
const auto &data = message.c_message();
|
||||
return {
|
||||
TextUtilities::Clean(qs(data.vmessage())),
|
||||
qs(data.vmessage()),
|
||||
Api::EntitiesFromMTP(session, data.ventities().value_or_empty())
|
||||
};
|
||||
}
|
||||
@@ -319,11 +319,11 @@ QString GenerateInviteLinkText(const MTPExportedChatInvite &data) {
|
||||
) : label;
|
||||
}
|
||||
|
||||
QString GenerateInviteLinkLink(const MTPExportedChatInvite &data) {
|
||||
TextWithEntities GenerateInviteLinkLink(const MTPExportedChatInvite &data) {
|
||||
const auto text = GenerateInviteLinkText(data);
|
||||
return text.endsWith(Ui::kQEllipsis)
|
||||
? text
|
||||
: textcmdLink(InternalInviteLinkUrl(data), text);
|
||||
? TextWithEntities{ .text = text }
|
||||
: Ui::Text::Link(text, InternalInviteLinkUrl(data));
|
||||
}
|
||||
|
||||
TextWithEntities GenerateInviteLinkChangeText(
|
||||
@@ -669,12 +669,12 @@ void GenerateItems(
|
||||
|
||||
const auto fromName = from->name;
|
||||
const auto fromLink = from->createOpenLink();
|
||||
const auto fromLinkText = textcmdLink(1, fromName);
|
||||
const auto fromLinkText = Ui::Text::Link(fromName, {});
|
||||
|
||||
const auto addSimpleServiceMessage = [&](
|
||||
const QString &text,
|
||||
const TextWithEntities &text,
|
||||
PhotoData *photo = nullptr) {
|
||||
auto message = HistoryService::PreparedText { text };
|
||||
auto message = HistoryService::PreparedText{ text };
|
||||
message.links.push_back(fromLink);
|
||||
addPart(history->makeServiceMessage(
|
||||
history->nextNonHistoryEntryId(),
|
||||
@@ -693,8 +693,9 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_title,
|
||||
qs(action.vnew_value()));
|
||||
addSimpleServiceMessage(text);
|
||||
{ .text = qs(action.vnew_value()) },
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(std::move(text));
|
||||
};
|
||||
|
||||
const auto makeSimpleTextMessage = [&](TextWithEntities &&text) {
|
||||
@@ -731,7 +732,7 @@ void GenerateItems(
|
||||
: (newValue.isEmpty()
|
||||
? tr::lng_admin_log_removed_description_channel
|
||||
: tr::lng_admin_log_changed_description_channel)
|
||||
)(tr::now, lt_from, fromLinkText);
|
||||
)(tr::now, lt_from, fromLinkText, Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
const auto body = makeSimpleTextMessage(
|
||||
@@ -756,7 +757,7 @@ void GenerateItems(
|
||||
: (newValue.isEmpty()
|
||||
? tr::lng_admin_log_removed_link_channel
|
||||
: tr::lng_admin_log_changed_link_channel)
|
||||
)(tr::now, lt_from, fromLinkText);
|
||||
)(tr::now, lt_from, fromLinkText, Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
const auto body = makeSimpleTextMessage(newValue.isEmpty()
|
||||
@@ -784,7 +785,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_changed_photo_channel)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text, photo);
|
||||
}, [&](const MTPDphotoEmpty &data) {
|
||||
const auto text = (channel->isMegagroup()
|
||||
@@ -792,7 +794,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_removed_photo_channel)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
});
|
||||
};
|
||||
@@ -801,16 +804,24 @@ void GenerateItems(
|
||||
const auto enabled = (action.vnew_value().type() == mtpc_boolTrue);
|
||||
const auto text = (enabled
|
||||
? tr::lng_admin_log_invites_enabled
|
||||
: tr::lng_admin_log_invites_disabled);
|
||||
addSimpleServiceMessage(text(tr::now, lt_from, fromLinkText));
|
||||
: tr::lng_admin_log_invites_disabled)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto createToggleSignatures = [&](const LogSign &action) {
|
||||
const auto enabled = (action.vnew_value().type() == mtpc_boolTrue);
|
||||
const auto text = (enabled
|
||||
? tr::lng_admin_log_signatures_enabled
|
||||
: tr::lng_admin_log_signatures_disabled);
|
||||
addSimpleServiceMessage(text(tr::now, lt_from, fromLinkText));
|
||||
: tr::lng_admin_log_signatures_disabled)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto createUpdatePinned = [&](const LogPin &action) {
|
||||
@@ -821,7 +832,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_unpinned_message)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
const auto detachExistingItem = false;
|
||||
@@ -836,7 +848,8 @@ void GenerateItems(
|
||||
const auto text = tr::lng_admin_log_unpinned_message(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
});
|
||||
};
|
||||
@@ -854,7 +867,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_edited_caption)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
auto oldValue = ExtractEditedText(
|
||||
@@ -885,7 +899,8 @@ void GenerateItems(
|
||||
const auto text = tr::lng_admin_log_deleted_message(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
const auto detachExistingItem = false;
|
||||
@@ -901,15 +916,23 @@ void GenerateItems(
|
||||
const auto createParticipantJoin = [&]() {
|
||||
const auto text = (channel->isMegagroup()
|
||||
? tr::lng_admin_log_participant_joined
|
||||
: tr::lng_admin_log_participant_joined_channel);
|
||||
addSimpleServiceMessage(text(tr::now, lt_from, fromLinkText));
|
||||
: tr::lng_admin_log_participant_joined_channel)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto createParticipantLeave = [&]() {
|
||||
const auto text = (channel->isMegagroup()
|
||||
? tr::lng_admin_log_participant_left
|
||||
: tr::lng_admin_log_participant_left_channel);
|
||||
addSimpleServiceMessage(text(tr::now, lt_from, fromLinkText));
|
||||
: tr::lng_admin_log_participant_left_channel)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto createParticipantInvite = [&](const LogInvite &action) {
|
||||
@@ -947,7 +970,8 @@ void GenerateItems(
|
||||
const auto text = tr::lng_admin_log_removed_stickers_group(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
} else {
|
||||
const auto text = tr::lng_admin_log_changed_stickers_group(
|
||||
@@ -955,9 +979,10 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_sticker_set,
|
||||
textcmdLink(
|
||||
2,
|
||||
tr::lng_admin_log_changed_stickers_set(tr::now)));
|
||||
Ui::Text::Link(
|
||||
tr::lng_admin_log_changed_stickers_set(tr::now),
|
||||
{}),
|
||||
Ui::Text::WithEntities);
|
||||
const auto setLink = std::make_shared<LambdaClickHandler>([=](
|
||||
ClickContext context) {
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
@@ -986,8 +1011,12 @@ void GenerateItems(
|
||||
const auto hidden = (action.vnew_value().type() == mtpc_boolTrue);
|
||||
const auto text = (hidden
|
||||
? tr::lng_admin_log_history_made_hidden
|
||||
: tr::lng_admin_log_history_made_visible);
|
||||
addSimpleServiceMessage(text(tr::now, lt_from, fromLinkText));
|
||||
: tr::lng_admin_log_history_made_visible)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto createDefaultBannedRights = [&](
|
||||
@@ -1003,7 +1032,8 @@ void GenerateItems(
|
||||
const auto text = tr::lng_admin_log_stopped_poll(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
const auto detachExistingItem = false;
|
||||
@@ -1025,7 +1055,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_removed_linked_channel)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
} else {
|
||||
const auto text = (broadcast
|
||||
@@ -1035,7 +1066,8 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_chat,
|
||||
textcmdLink(2, now->name));
|
||||
Ui::Text::Link(now->name, {}),
|
||||
Ui::Text::WithEntities);
|
||||
const auto chatLink = std::make_shared<LambdaClickHandler>([=] {
|
||||
Ui::showPeerHistory(now, ShowAtUnreadMsgId);
|
||||
});
|
||||
@@ -1056,24 +1088,26 @@ void GenerateItems(
|
||||
const auto address = qs(data.vaddress());
|
||||
const auto link = data.vgeo_point().match([&](
|
||||
const MTPDgeoPoint &data) {
|
||||
return textcmdLink(
|
||||
LocationClickHandler::Url(Data::LocationPoint(data)),
|
||||
address);
|
||||
return Ui::Text::Link(
|
||||
address,
|
||||
LocationClickHandler::Url(Data::LocationPoint(data)));
|
||||
}, [&](const MTPDgeoPointEmpty &) {
|
||||
return address;
|
||||
return TextWithEntities{ .text = address };
|
||||
});
|
||||
const auto text = tr::lng_admin_log_changed_location_chat(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_address,
|
||||
link);
|
||||
link,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
}, [&](const MTPDchannelLocationEmpty &) {
|
||||
const auto text = tr::lng_admin_log_removed_location_chat(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
});
|
||||
};
|
||||
@@ -1094,13 +1128,15 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_duration,
|
||||
duration);
|
||||
{ .text = duration },
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
} else {
|
||||
const auto text = tr::lng_admin_log_removed_slow_mode(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
}
|
||||
};
|
||||
@@ -1111,7 +1147,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_started_group_call)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
@@ -1121,7 +1158,8 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_discarded_group_call)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
@@ -1133,7 +1171,7 @@ void GenerateItems(
|
||||
};
|
||||
|
||||
const auto addServiceMessageWithLink = [&](
|
||||
const QString &text,
|
||||
const TextWithEntities &text,
|
||||
const ClickHandlerPtr &link) {
|
||||
auto message = HistoryService::PreparedText{ text };
|
||||
message.links.push_back(fromLink);
|
||||
@@ -1150,9 +1188,9 @@ void GenerateItems(
|
||||
const auto participantPeer = groupCallParticipantPeer(
|
||||
data.vparticipant());
|
||||
const auto participantPeerLink = participantPeer->createOpenLink();
|
||||
const auto participantPeerLinkText = textcmdLink(
|
||||
2,
|
||||
participantPeer->name);
|
||||
const auto participantPeerLinkText = Ui::Text::Link(
|
||||
participantPeer->name,
|
||||
{});
|
||||
const auto text = (broadcast
|
||||
? tr::lng_admin_log_muted_participant_channel
|
||||
: tr::lng_admin_log_muted_participant)(
|
||||
@@ -1160,7 +1198,8 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_user,
|
||||
participantPeerLinkText);
|
||||
participantPeerLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addServiceMessageWithLink(text, participantPeerLink);
|
||||
};
|
||||
|
||||
@@ -1168,9 +1207,9 @@ void GenerateItems(
|
||||
const auto participantPeer = groupCallParticipantPeer(
|
||||
data.vparticipant());
|
||||
const auto participantPeerLink = participantPeer->createOpenLink();
|
||||
const auto participantPeerLinkText = textcmdLink(
|
||||
2,
|
||||
participantPeer->name);
|
||||
const auto participantPeerLinkText = Ui::Text::Link(
|
||||
participantPeer->name,
|
||||
{});
|
||||
const auto text = (broadcast
|
||||
? tr::lng_admin_log_unmuted_participant_channel
|
||||
: tr::lng_admin_log_unmuted_participant)(
|
||||
@@ -1178,7 +1217,8 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_user,
|
||||
participantPeerLinkText);
|
||||
participantPeerLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addServiceMessageWithLink(text, participantPeerLink);
|
||||
};
|
||||
|
||||
@@ -1193,12 +1233,13 @@ void GenerateItems(
|
||||
: tr::lng_admin_log_allowed_unmute_self))(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto addInviteLinkServiceMessage = [&](
|
||||
const QString &text,
|
||||
const TextWithEntities &text,
|
||||
const MTPExportedChatInvite &data,
|
||||
ClickHandlerPtr additional = nullptr) {
|
||||
auto message = HistoryService::PreparedText{ text };
|
||||
@@ -1230,7 +1271,8 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_link,
|
||||
GenerateInviteLinkLink(data.vinvite())),
|
||||
GenerateInviteLinkLink(data.vinvite()),
|
||||
Ui::Text::WithEntities),
|
||||
data.vinvite());
|
||||
};
|
||||
|
||||
@@ -1241,7 +1283,8 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_link,
|
||||
GenerateInviteLinkLink(data.vinvite())),
|
||||
GenerateInviteLinkLink(data.vinvite()),
|
||||
Ui::Text::WithEntities),
|
||||
data.vinvite());
|
||||
};
|
||||
|
||||
@@ -1252,7 +1295,8 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_link,
|
||||
GenerateInviteLinkLink(data.vinvite())),
|
||||
GenerateInviteLinkLink(data.vinvite()),
|
||||
Ui::Text::WithEntities),
|
||||
data.vinvite());
|
||||
};
|
||||
|
||||
@@ -1267,9 +1311,9 @@ void GenerateItems(
|
||||
const auto participantPeer = groupCallParticipantPeer(
|
||||
data.vparticipant());
|
||||
const auto participantPeerLink = participantPeer->createOpenLink();
|
||||
const auto participantPeerLinkText = textcmdLink(
|
||||
2,
|
||||
participantPeer->name);
|
||||
const auto participantPeerLinkText = Ui::Text::Link(
|
||||
participantPeer->name,
|
||||
{});
|
||||
const auto volume = data.vparticipant().match([&](
|
||||
const MTPDgroupCallParticipant &data) {
|
||||
return data.vvolume().value_or(10000);
|
||||
@@ -1278,27 +1322,29 @@ void GenerateItems(
|
||||
auto text = (broadcast
|
||||
? tr::lng_admin_log_participant_volume_channel
|
||||
: tr::lng_admin_log_participant_volume)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_user,
|
||||
participantPeerLinkText,
|
||||
lt_percent,
|
||||
volumeText);
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_user,
|
||||
participantPeerLinkText,
|
||||
lt_percent,
|
||||
{ .text = volumeText },
|
||||
Ui::Text::WithEntities);
|
||||
addServiceMessageWithLink(text, participantPeerLink);
|
||||
};
|
||||
|
||||
const auto createChangeHistoryTTL = [&](const LogTTL &data) {
|
||||
const auto was = data.vprev_value().v;
|
||||
const auto now = data.vnew_value().v;
|
||||
const auto wrap = [](int duration) {
|
||||
return (duration == 5)
|
||||
const auto wrap = [](int duration) -> TextWithEntities {
|
||||
const auto text = (duration == 5)
|
||||
? u"5 seconds"_q
|
||||
: (duration < 2 * 86400)
|
||||
? tr::lng_manage_messages_ttl_after1(tr::now)
|
||||
: (duration < 8 * 86400)
|
||||
? tr::lng_manage_messages_ttl_after2(tr::now)
|
||||
: tr::lng_manage_messages_ttl_after3(tr::now);
|
||||
return { .text = text };
|
||||
};
|
||||
const auto text = !was
|
||||
? tr::lng_admin_log_messages_ttl_set(
|
||||
@@ -1306,14 +1352,16 @@ void GenerateItems(
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_duration,
|
||||
wrap(now))
|
||||
wrap(now),
|
||||
Ui::Text::WithEntities)
|
||||
: !now
|
||||
? tr::lng_admin_log_messages_ttl_removed(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_duration,
|
||||
wrap(was))
|
||||
wrap(was),
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_admin_log_messages_ttl_changed(
|
||||
tr::now,
|
||||
lt_from,
|
||||
@@ -1321,7 +1369,8 @@ void GenerateItems(
|
||||
lt_previous,
|
||||
wrap(was),
|
||||
lt_duration,
|
||||
wrap(now));
|
||||
wrap(now),
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
@@ -1332,7 +1381,6 @@ void GenerateItems(
|
||||
? tr::lng_admin_log_participant_approved_by_link
|
||||
: tr::lng_admin_log_participant_approved_by_link_channel);
|
||||
const auto linkText = GenerateInviteLinkLink(data.vinvite());
|
||||
const auto adminIndex = linkText.endsWith(Ui::kQEllipsis) ? 2 : 3;
|
||||
addInviteLinkServiceMessage(
|
||||
text(
|
||||
tr::now,
|
||||
@@ -1341,7 +1389,8 @@ void GenerateItems(
|
||||
lt_link,
|
||||
linkText,
|
||||
lt_user,
|
||||
textcmdLink(adminIndex, user->name)),
|
||||
Ui::Text::Link(user->name, {}),
|
||||
Ui::Text::WithEntities),
|
||||
data.vinvite(),
|
||||
user->createOpenLink());
|
||||
};
|
||||
@@ -1350,15 +1399,20 @@ void GenerateItems(
|
||||
const auto disabled = (data.vnew_value().type() == mtpc_boolTrue);
|
||||
const auto text = (disabled
|
||||
? tr::lng_admin_log_forwards_disabled
|
||||
: tr::lng_admin_log_forwards_enabled);
|
||||
addSimpleServiceMessage(text(tr::now, lt_from, fromLinkText));
|
||||
: tr::lng_admin_log_forwards_enabled)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
const auto createSendMessage = [&](const LogActionSendMessage &data) {
|
||||
const auto text = tr::lng_admin_log_sent_message(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText);
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
|
||||
const auto detachExistingItem = false;
|
||||
@@ -1371,14 +1425,25 @@ void GenerateItems(
|
||||
ExtractSentDate(data.vmessage()));
|
||||
};
|
||||
|
||||
const auto createChangeAvailableReactions = [&](const LogEventActionChangeAvailableReactions &data) {
|
||||
const auto createChangeAvailableReactions = [&](
|
||||
const LogEventActionChangeAvailableReactions &data) {
|
||||
auto list = QStringList();
|
||||
for (const auto &emoji : data.vnew_value().v) {
|
||||
list.append(qs(emoji));
|
||||
}
|
||||
const auto text = list.isEmpty()
|
||||
? tr::lng_admin_log_reactions_disabled(tr::now, lt_from, fromLinkText)
|
||||
: tr::lng_admin_log_reactions_updated(tr::now, lt_from, fromLinkText, lt_emoji, list.join(", "));
|
||||
? tr::lng_admin_log_reactions_disabled(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_admin_log_reactions_updated(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText,
|
||||
lt_emoji,
|
||||
{ .text = list.join(", ") },
|
||||
Ui::Text::WithEntities);
|
||||
addSimpleServiceMessage(text);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_chat_filters.h"
|
||||
#include "data/data_scheduled_messages.h"
|
||||
#include "data/data_sponsored_messages.h"
|
||||
#include "data/data_send_action.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_photo.h"
|
||||
@@ -67,6 +68,7 @@ History::History(not_null<Data::Session*> owner, PeerId peerId)
|
||||
: Entry(owner, Type::History)
|
||||
, peer(owner->peer(peerId))
|
||||
, cloudDraftTextCache(st::dialogsTextWidthMin)
|
||||
, _delegateMixin(HistoryInner::DelegateMixin())
|
||||
, _mute(owner->notifyIsMuted(peer))
|
||||
, _chatListNameSortKey(owner->nameSortKey(peer->name))
|
||||
, _sendActionPainter(this) {
|
||||
@@ -95,14 +97,14 @@ int History::height() const {
|
||||
|
||||
void History::removeNotification(not_null<HistoryItem*> item) {
|
||||
_notifications.erase(
|
||||
ranges::remove(_notifications, item),
|
||||
ranges::remove(_notifications, item, &ItemNotification::item),
|
||||
end(_notifications));
|
||||
}
|
||||
|
||||
HistoryItem *History::currentNotification() {
|
||||
auto History::currentNotification() const -> std::optional<ItemNotification> {
|
||||
return empty(_notifications)
|
||||
? nullptr
|
||||
: _notifications.front().get();
|
||||
? std::nullopt
|
||||
: std::make_optional(_notifications.front());
|
||||
}
|
||||
|
||||
bool History::hasNotification() const {
|
||||
@@ -115,8 +117,12 @@ void History::skipNotification() {
|
||||
}
|
||||
}
|
||||
|
||||
void History::popNotification(HistoryItem *item) {
|
||||
if (!empty(_notifications) && (_notifications.back() == item)) {
|
||||
void History::pushNotification(ItemNotification notification) {
|
||||
_notifications.push_back(notification);
|
||||
}
|
||||
|
||||
void History::popNotification(ItemNotification notification) {
|
||||
if (!empty(_notifications) && (_notifications.back() == notification)) {
|
||||
_notifications.pop_back();
|
||||
}
|
||||
}
|
||||
@@ -673,6 +679,18 @@ not_null<HistoryItem*> History::addNewLocalMessage(
|
||||
true);
|
||||
}
|
||||
|
||||
not_null<HistoryItem*> History::addNewLocalMessage(
|
||||
MsgId id,
|
||||
Data::SponsoredFrom from,
|
||||
const TextWithEntities &textWithEntities) {
|
||||
return addNewItem(
|
||||
makeMessage(
|
||||
id,
|
||||
from,
|
||||
textWithEntities),
|
||||
true);
|
||||
}
|
||||
|
||||
void History::setUnreadMentionsCount(int count) {
|
||||
const auto had = _unreadMentionsCount && (*_unreadMentionsCount > 0);
|
||||
if (_unreadMentions.size() > count) {
|
||||
@@ -1132,13 +1150,17 @@ void History::newItemAdded(not_null<HistoryItem*> item) {
|
||||
from->madeAction(item->date());
|
||||
}
|
||||
item->contributeToSlowmode();
|
||||
auto notification = ItemNotification{
|
||||
item,
|
||||
ItemNotificationType::Message,
|
||||
};
|
||||
if (item->showNotification()) {
|
||||
_notifications.push_back(item);
|
||||
pushNotification(notification);
|
||||
}
|
||||
owner().notifyNewItemAdded(item);
|
||||
const auto stillShow = item->showNotification(); // Could be read already.
|
||||
if (stillShow) {
|
||||
Core::App().notifications().schedule(item);
|
||||
Core::App().notifications().schedule(notification);
|
||||
if (!item->out() && item->unread()) {
|
||||
if (unreadCountKnown()) {
|
||||
setUnreadCount(unreadCount() + 1);
|
||||
@@ -1231,8 +1253,7 @@ void History::addItemToBlock(not_null<HistoryItem*> item) {
|
||||
|
||||
auto block = prepareBlockForAddingItem();
|
||||
|
||||
block->messages.push_back(item->createView(
|
||||
HistoryInner::ElementDelegate()));
|
||||
block->messages.push_back(item->createView(_delegateMixin->delegate()));
|
||||
const auto view = block->messages.back().get();
|
||||
view->attachToBlock(block, block->messages.size() - 1);
|
||||
|
||||
@@ -1998,8 +2019,7 @@ not_null<HistoryItem*> History::addNewInTheMiddle(
|
||||
|
||||
const auto it = block->messages.insert(
|
||||
block->messages.begin() + itemIndex,
|
||||
item->createView(
|
||||
HistoryInner::ElementDelegate()));
|
||||
item->createView(_delegateMixin->delegate()));
|
||||
(*it)->attachToBlock(block.get(), itemIndex);
|
||||
if (itemIndex + 1 < block->messages.size()) {
|
||||
for (auto i = itemIndex + 1, l = int(block->messages.size()); i != l; ++i) {
|
||||
@@ -2135,8 +2155,11 @@ void History::clearNotifications() {
|
||||
|
||||
void History::clearIncomingNotifications() {
|
||||
if (!peer->isSelf()) {
|
||||
const auto proj = [](ItemNotification notification) {
|
||||
return notification.item->out();
|
||||
};
|
||||
_notifications.erase(
|
||||
ranges::remove(_notifications, false, &HistoryItem::out),
|
||||
ranges::remove(_notifications, false, proj),
|
||||
end(_notifications));
|
||||
}
|
||||
}
|
||||
@@ -2971,7 +2994,9 @@ void History::reactionsEnabledChanged(bool enabled) {
|
||||
item->updateReactions(nullptr);
|
||||
}
|
||||
} else {
|
||||
|
||||
for (const auto &item : _messages) {
|
||||
item->updateReactionsUnknown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3274,7 +3299,7 @@ void HistoryBlock::refreshView(not_null<Element*> view) {
|
||||
|
||||
const auto item = view->data();
|
||||
auto refreshed = item->createView(
|
||||
HistoryInner::ElementDelegate(),
|
||||
_history->delegateMixin()->delegate(),
|
||||
view);
|
||||
|
||||
auto blockIndex = indexInHistory();
|
||||
|
||||
@@ -25,6 +25,7 @@ class HistoryItem;
|
||||
class HistoryMessage;
|
||||
class HistoryService;
|
||||
struct HistoryMessageMarkupData;
|
||||
class HistoryMainElementDelegateMixin;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
@@ -35,6 +36,7 @@ struct Draft;
|
||||
class Session;
|
||||
class Folder;
|
||||
class ChatFilter;
|
||||
struct SponsoredFrom;
|
||||
|
||||
enum class ForwardOptions {
|
||||
PreserveInfo,
|
||||
@@ -74,6 +76,19 @@ enum class UnreadMentionType {
|
||||
Existing, // when some messages slice was received
|
||||
};
|
||||
|
||||
enum class ItemNotificationType {
|
||||
Message,
|
||||
Reaction,
|
||||
};
|
||||
struct ItemNotification {
|
||||
not_null<HistoryItem*> item;
|
||||
ItemNotificationType type = ItemNotificationType::Message;
|
||||
|
||||
friend inline bool operator==(ItemNotification a, ItemNotification b) {
|
||||
return (a.item == b.item) && (a.type == b.type);
|
||||
}
|
||||
};
|
||||
|
||||
class History final : public Dialogs::Entry {
|
||||
public:
|
||||
using Element = HistoryView::Element;
|
||||
@@ -83,6 +98,11 @@ public:
|
||||
History &operator=(const History &) = delete;
|
||||
~History();
|
||||
|
||||
[[nodiscard]] auto delegateMixin() const
|
||||
-> not_null<HistoryMainElementDelegateMixin*> {
|
||||
return _delegateMixin.get();
|
||||
}
|
||||
|
||||
not_null<History*> migrateToOrMe() const;
|
||||
History *migrateFrom() const;
|
||||
MsgRange rangeForDifferenceRequest() const;
|
||||
@@ -190,6 +210,10 @@ public:
|
||||
const QString &postAuthor,
|
||||
not_null<GameData*> game,
|
||||
HistoryMessageMarkupData &&markup);
|
||||
not_null<HistoryItem*> addNewLocalMessage(
|
||||
MsgId id,
|
||||
Data::SponsoredFrom from,
|
||||
const TextWithEntities &textWithEntities); // sponsored
|
||||
|
||||
// Used only internally and for channel admin log.
|
||||
not_null<HistoryItem*> createItem(
|
||||
@@ -293,10 +317,11 @@ public:
|
||||
void itemRemoved(not_null<HistoryItem*> item);
|
||||
void itemVanished(not_null<HistoryItem*> item);
|
||||
|
||||
HistoryItem *currentNotification();
|
||||
[[nodiscard]] std::optional<ItemNotification> currentNotification() const;
|
||||
bool hasNotification() const;
|
||||
void skipNotification();
|
||||
void popNotification(HistoryItem *item);
|
||||
void pushNotification(ItemNotification notification);
|
||||
void popNotification(ItemNotification notification);
|
||||
|
||||
bool hasPendingResizedItems() const;
|
||||
void setHasPendingResizedItems();
|
||||
@@ -580,6 +605,8 @@ private:
|
||||
|
||||
void setFolderPointer(Data::Folder *folder);
|
||||
|
||||
const std::unique_ptr<HistoryMainElementDelegateMixin> _delegateMixin;
|
||||
|
||||
Flags _flags = 0;
|
||||
bool _mute = false;
|
||||
int _width = 0;
|
||||
@@ -632,7 +659,7 @@ private:
|
||||
|
||||
HistoryView::SendActionPainter _sendActionPainter;
|
||||
|
||||
std::deque<not_null<HistoryItem*>> _notifications;
|
||||
std::deque<ItemNotification> _notifications;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -120,7 +120,148 @@ int BinarySearchBlocksOrItems(const T &list, int edge) {
|
||||
|
||||
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
|
||||
|
||||
HistoryInner *HistoryInner::Instance = nullptr;
|
||||
HistoryMainElementDelegateMixin::HistoryMainElementDelegateMixin() = default;
|
||||
|
||||
HistoryMainElementDelegateMixin::~HistoryMainElementDelegateMixin()
|
||||
= default;
|
||||
|
||||
class HistoryMainElementDelegate final
|
||||
: public HistoryView::ElementDelegate
|
||||
, public HistoryMainElementDelegateMixin {
|
||||
public:
|
||||
using Element = HistoryView::Element;
|
||||
|
||||
HistoryView::Context elementContext() override {
|
||||
return HistoryView::Context::History;
|
||||
}
|
||||
std::unique_ptr<Element> elementCreate(
|
||||
not_null<HistoryMessage*> message,
|
||||
Element *replacing = nullptr) override {
|
||||
return std::make_unique<HistoryView::Message>(
|
||||
this,
|
||||
message,
|
||||
replacing);
|
||||
}
|
||||
std::unique_ptr<HistoryView::Element> elementCreate(
|
||||
not_null<HistoryService*> message,
|
||||
Element *replacing = nullptr) override {
|
||||
return std::make_unique<HistoryView::Service>(
|
||||
this,
|
||||
message,
|
||||
replacing);
|
||||
}
|
||||
bool elementUnderCursor(
|
||||
not_null<const Element*> view) override {
|
||||
return (App::mousedItem() == view);
|
||||
}
|
||||
crl::time elementHighlightTime(
|
||||
not_null<const HistoryItem*> item) override {
|
||||
return _widget ? _widget->elementHighlightTime(item) : 0;
|
||||
}
|
||||
bool elementInSelectionMode() override {
|
||||
return _widget ? _widget->inSelectionMode() : false;
|
||||
}
|
||||
bool elementIntersectsRange(
|
||||
not_null<const Element*> view,
|
||||
int from,
|
||||
int till) override {
|
||||
return _widget
|
||||
? _widget->elementIntersectsRange(view, from, till)
|
||||
: false;
|
||||
}
|
||||
void elementStartStickerLoop(
|
||||
not_null<const Element*> view) override {
|
||||
if (_widget) {
|
||||
_widget->elementStartStickerLoop(view);
|
||||
}
|
||||
}
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) override {
|
||||
if (_widget) {
|
||||
_widget->elementShowPollResults(poll, context);
|
||||
}
|
||||
}
|
||||
void elementOpenPhoto(
|
||||
not_null<PhotoData*> photo,
|
||||
FullMsgId context) override {
|
||||
if (_widget) {
|
||||
_widget->elementOpenPhoto(photo, context);
|
||||
}
|
||||
}
|
||||
void elementOpenDocument(
|
||||
not_null<DocumentData*> document,
|
||||
FullMsgId context,
|
||||
bool showInMediaView = false) override {
|
||||
if (_widget) {
|
||||
_widget->elementOpenDocument(
|
||||
document,
|
||||
context,
|
||||
showInMediaView);
|
||||
}
|
||||
}
|
||||
void elementCancelUpload(const FullMsgId &context) override {
|
||||
if (_widget) {
|
||||
_widget->elementCancelUpload(context);
|
||||
}
|
||||
}
|
||||
void elementShowTooltip(
|
||||
const TextWithEntities &text,
|
||||
Fn<void()> hiddenCallback) override {
|
||||
if (_widget) {
|
||||
_widget->elementShowTooltip(text, hiddenCallback);
|
||||
}
|
||||
}
|
||||
bool elementIsGifPaused() override {
|
||||
return _widget ? _widget->elementIsGifPaused() : false;
|
||||
}
|
||||
bool elementHideReply(not_null<const Element*> view) override {
|
||||
return false;
|
||||
}
|
||||
bool elementShownUnread(not_null<const Element*> view) override {
|
||||
return view->data()->unread();
|
||||
}
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override {
|
||||
if (_widget) {
|
||||
_widget->elementSendBotCommand(command, context);
|
||||
}
|
||||
}
|
||||
void elementHandleViaClick(not_null<UserData*> bot) override {
|
||||
if (_widget) {
|
||||
_widget->elementHandleViaClick(bot);
|
||||
}
|
||||
}
|
||||
bool elementIsChatWide() override {
|
||||
return _widget ? _widget->elementIsChatWide() : false;
|
||||
}
|
||||
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {
|
||||
Expects(_widget != nullptr);
|
||||
|
||||
return _widget->elementPathShiftGradient();
|
||||
}
|
||||
void elementReplyTo(const FullMsgId &to) override {
|
||||
if (_widget) {
|
||||
_widget->elementReplyTo(to);
|
||||
}
|
||||
}
|
||||
void elementStartInteraction(not_null<const Element*> view) override {
|
||||
if (_widget) {
|
||||
_widget->elementStartInteraction(view);
|
||||
}
|
||||
}
|
||||
void elementShowSpoilerAnimation() override {
|
||||
if (_widget) {
|
||||
_widget->elementShowSpoilerAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
not_null<HistoryView::ElementDelegate*> delegate() override {
|
||||
return this;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class HistoryInner::BotAbout : public ClickHandlerHost {
|
||||
public:
|
||||
@@ -170,6 +311,7 @@ HistoryInner::HistoryInner(
|
||||
, _controller(controller)
|
||||
, _peer(history->peer)
|
||||
, _history(history)
|
||||
, _elementDelegate(_history->delegateMixin()->delegate())
|
||||
, _emojiInteractions(std::make_unique<HistoryView::EmojiInteractions>(
|
||||
&controller->session()))
|
||||
, _migrated(history->migrateFrom())
|
||||
@@ -180,12 +322,17 @@ HistoryInner::HistoryInner(
|
||||
, _reactionsManager(
|
||||
std::make_unique<HistoryView::Reactions::Manager>(
|
||||
this,
|
||||
[=](QRect updated) { update(updated); }))
|
||||
Data::UniqueReactionsLimitValue(&controller->session()),
|
||||
[=](QRect updated) { update(updated); },
|
||||
controller->cachedReactionIconFactory().createMethod()))
|
||||
, _touchSelectTimer([=] { onTouchSelect(); })
|
||||
, _touchScrollTimer([=] { onTouchScrollTimer(); })
|
||||
, _scrollDateCheck([this] { scrollDateCheck(); })
|
||||
, _scrollDateHideTimer([this] { scrollDateHideByTimer(); }) {
|
||||
Instance = this;
|
||||
_history->delegateMixin()->setCurrent(this);
|
||||
if (_migrated) {
|
||||
_migrated->delegateMixin()->setCurrent(this);
|
||||
}
|
||||
|
||||
Window::ChatThemeValueFromPeer(
|
||||
controller,
|
||||
@@ -230,8 +377,21 @@ HistoryInner::HistoryInner(
|
||||
using ChosenReaction = HistoryView::Reactions::Manager::Chosen;
|
||||
_reactionsManager->chosen(
|
||||
) | rpl::start_with_next([=](ChosenReaction reaction) {
|
||||
if (const auto item = session().data().message(reaction.context)) {
|
||||
item->toggleReaction(reaction.emoji);
|
||||
const auto item = session().data().message(reaction.context);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item->toggleReaction(reaction.emoji);
|
||||
if (item->chosenReaction() != reaction.emoji) {
|
||||
return;
|
||||
} else if (const auto view = item->mainView()) {
|
||||
if (const auto top = itemTop(view); top >= 0) {
|
||||
view->animateSendReaction({
|
||||
.emoji = reaction.emoji,
|
||||
.flyIcon = reaction.icon,
|
||||
.flyFrom = reaction.geometry.translated(0, -top),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
@@ -267,6 +427,7 @@ HistoryInner::HistoryInner(
|
||||
return item->mainView() != nullptr;
|
||||
}) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
|
||||
item->mainView()->itemDataChanged();
|
||||
_reactionsManager->updateUniqueLimit(item);
|
||||
}, lifetime());
|
||||
|
||||
session().changes().historyUpdates(
|
||||
@@ -276,11 +437,10 @@ HistoryInner::HistoryInner(
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
Data::PeerAllowedReactionsValue(
|
||||
_peer
|
||||
) | rpl::start_with_next([=](std::vector<Data::Reaction> &&list) {
|
||||
_reactionsManager->applyList(std::move(list));
|
||||
}, lifetime());
|
||||
HistoryView::Reactions::SetupManagerList(
|
||||
_reactionsManager.get(),
|
||||
&session(),
|
||||
Data::PeerAllowedReactionsValue(_peer));
|
||||
|
||||
controller->adaptive().chatWideValue(
|
||||
) | rpl::start_with_next([=](bool wide) {
|
||||
@@ -394,6 +554,10 @@ void HistoryInner::repaintItem(const Element *view) {
|
||||
if (top >= 0) {
|
||||
const auto range = view->verticalRepaintRange();
|
||||
update(0, top + range.top, width(), range.height);
|
||||
const auto id = view->data()->fullId();
|
||||
if (const auto area = _reactionsManager->lookupEffectArea(id)) {
|
||||
update(*area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,6 +843,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
if (hasPendingResizedItems()) {
|
||||
return;
|
||||
} else if (_recountedAfterPendingResizedItems) {
|
||||
_recountedAfterPendingResizedItems = false;
|
||||
mouseActionUpdate();
|
||||
}
|
||||
|
||||
const auto guard = gsl::finally([&] {
|
||||
@@ -731,6 +898,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
} else {
|
||||
_emptyPainter = nullptr;
|
||||
}
|
||||
|
||||
_reactionsManager->startEffectsCollection();
|
||||
if (!noHistoryDisplayed) {
|
||||
auto readMentions = base::flat_set<not_null<HistoryItem*>>();
|
||||
|
||||
@@ -760,12 +929,17 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
context.translate(0, -top);
|
||||
p.translate(0, top);
|
||||
if (context.clip.y() < view->height()) while (top < drawToY) {
|
||||
context.reactionEffects
|
||||
= _reactionsManager->currentReactionEffect();
|
||||
context.outbg = view->hasOutLayout();
|
||||
context.selection = itemRenderSelection(
|
||||
view,
|
||||
selfromy - mtop,
|
||||
seltoy - mtop);
|
||||
view->draw(p, context);
|
||||
_reactionsManager->recordCurrentReactionEffect(
|
||||
item->fullId(),
|
||||
QPoint(0, top));
|
||||
|
||||
const auto height = view->height();
|
||||
const auto middle = top + height / 2;
|
||||
@@ -815,14 +989,18 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
while (top < drawToY) {
|
||||
const auto height = view->height();
|
||||
if (context.clip.y() < height && hdrawtop < top + height) {
|
||||
context.reactionEffects
|
||||
= _reactionsManager->currentReactionEffect();
|
||||
context.outbg = view->hasOutLayout();
|
||||
context.selection = itemRenderSelection(
|
||||
view,
|
||||
selfromy - htop,
|
||||
seltoy - htop);
|
||||
view->draw(p, context);
|
||||
_reactionsManager->recordCurrentReactionEffect(
|
||||
item->fullId(),
|
||||
QPoint(0, top));
|
||||
|
||||
const auto item = view->data();
|
||||
const auto middle = top + height / 2;
|
||||
const auto bottom = top + height;
|
||||
if (_visibleAreaBottom >= bottom) {
|
||||
@@ -893,7 +1071,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
userpicTop,
|
||||
width(),
|
||||
st::msgPhotoSize);
|
||||
} else if (const auto info = view->data()->hiddenForwardedInfo()) {
|
||||
} else if (const auto info = view->data()->hiddenSenderInfo()) {
|
||||
info->userpic.paint(
|
||||
p,
|
||||
st::historyPhotoLeft,
|
||||
@@ -962,7 +1140,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
|
||||
});
|
||||
p.setOpacity(1.);
|
||||
|
||||
_reactionsManager->paintButtons(p, context);
|
||||
_reactionsManager->paint(p, context);
|
||||
|
||||
p.translate(0, _historyPaddingTop);
|
||||
_emojiInteractions->paint(p);
|
||||
@@ -1528,16 +1706,22 @@ void HistoryInner::mouseActionFinish(
|
||||
const auto pressedItemId = pressedItemView
|
||||
? pressedItemView->data()->fullId()
|
||||
: FullMsgId();
|
||||
const auto weak = base::make_weak(_controller.get());
|
||||
ActivateClickHandler(window(), activated, {
|
||||
button,
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.itemId = pressedItemId,
|
||||
.elementDelegate = [weak = Ui::MakeWeak(this)] {
|
||||
return weak
|
||||
? HistoryInner::ElementDelegate().get()
|
||||
: nullptr;
|
||||
.elementDelegate = [=]() -> HistoryView::ElementDelegate* {
|
||||
if (const auto strong = weak.get()) {
|
||||
auto &data = strong->session().data();
|
||||
if (const auto item = data.message(pressedItemId)) {
|
||||
const auto history = item->history();
|
||||
return history->delegateMixin()->delegate();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
},
|
||||
.sessionWindow = base::make_weak(_controller.get()),
|
||||
.sessionWindow = weak,
|
||||
})
|
||||
});
|
||||
return;
|
||||
@@ -1665,6 +1849,12 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
mouseActionUpdate(e->globalPos());
|
||||
}
|
||||
|
||||
const auto link = ClickHandler::getActive();
|
||||
if (link
|
||||
&& !link->property(kSendReactionEmojiProperty).toString().isEmpty()
|
||||
&& _reactionsManager->showContextMenu(this, e)) {
|
||||
return;
|
||||
}
|
||||
auto selectedState = getSelectionState();
|
||||
auto canSendMessages = _peer->canWrite();
|
||||
|
||||
@@ -1702,6 +1892,22 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
|
||||
const auto hasWhoReactedItem = _dragStateItem
|
||||
&& Api::WhoReactedExists(_dragStateItem);
|
||||
const auto clickedEmoji = link
|
||||
? link->property(kReactionsCountEmojiProperty).toString()
|
||||
: QString();
|
||||
_whoReactedMenuLifetime.destroy();
|
||||
if (hasWhoReactedItem && !clickedEmoji.isEmpty()) {
|
||||
HistoryView::ShowWhoReactedMenu(
|
||||
&_menu,
|
||||
e->globalPos(),
|
||||
this,
|
||||
_dragStateItem,
|
||||
clickedEmoji,
|
||||
_controller,
|
||||
_whoReactedMenuLifetime);
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
hasWhoReactedItem ? st::whoReadMenu : st::popupMenuWithIcons);
|
||||
@@ -1860,10 +2066,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
}
|
||||
};
|
||||
|
||||
const auto link = ClickHandler::getActive();
|
||||
auto lnkPhoto = dynamic_cast<PhotoClickHandler*>(link.get());
|
||||
auto lnkDocument = dynamic_cast<DocumentClickHandler*>(link.get());
|
||||
if (lnkPhoto || lnkDocument) {
|
||||
const auto lnkPhotoId = PhotoId(link
|
||||
? link->property(kPhotoLinkMediaIdProperty).toULongLong()
|
||||
: 0);
|
||||
const auto lnkDocumentId = DocumentId(link
|
||||
? link->property(kDocumentLinkMediaIdProperty).toULongLong()
|
||||
: 0);
|
||||
if (lnkPhotoId || lnkDocumentId) {
|
||||
const auto item = _dragStateItem;
|
||||
const auto itemId = item ? item->fullId() : FullMsgId();
|
||||
if (isUponSelected > 0 && !hasCopyRestrictionForSelected()) {
|
||||
@@ -1875,10 +2084,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
&st::menuIconCopy);
|
||||
}
|
||||
addItemActions(item, item);
|
||||
if (lnkPhoto) {
|
||||
addPhotoActions(lnkPhoto->photo(), item);
|
||||
if (lnkPhotoId) {
|
||||
addPhotoActions(session->data().photo(lnkPhotoId), item);
|
||||
} else {
|
||||
addDocumentActions(lnkDocument->document(), item);
|
||||
addDocumentActions(session->data().document(lnkDocumentId), item);
|
||||
}
|
||||
if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) {
|
||||
_menu->addAction(item->history()->peer->isMegagroup() ? tr::lng_context_copy_message_link(tr::now) : tr::lng_context_copy_post_link(tr::now), [=] {
|
||||
@@ -2370,6 +2579,11 @@ void HistoryInner::checkHistoryActivation() {
|
||||
void HistoryInner::recountHistoryGeometry() {
|
||||
_contentWidth = _scroll->width();
|
||||
|
||||
if (_history->hasPendingResizedItems()
|
||||
|| (_migrated && _migrated->hasPendingResizedItems())) {
|
||||
_recountedAfterPendingResizedItems = true;
|
||||
}
|
||||
|
||||
const auto visibleHeight = _scroll->height();
|
||||
int oldHistoryPaddingTop = qMax(visibleHeight - historyHeight() - st::historyPaddingBottom, 0);
|
||||
if (_botAbout && !_botAbout->info->text.isEmpty()) {
|
||||
@@ -2543,7 +2757,13 @@ void HistoryInner::visibleAreaUpdated(int top, int bottom) {
|
||||
const auto pages = kUnloadHeavyPartsPages;
|
||||
const auto from = _visibleAreaTop - pages * visibleAreaHeight;
|
||||
const auto till = _visibleAreaBottom + pages * visibleAreaHeight;
|
||||
session().data().unloadHeavyViewParts(ElementDelegate(), from, till);
|
||||
session().data().unloadHeavyViewParts(_elementDelegate, from, till);
|
||||
if (_migratedElementDelegate) {
|
||||
session().data().unloadHeavyViewParts(
|
||||
_migratedElementDelegate,
|
||||
from,
|
||||
till);
|
||||
}
|
||||
checkHistoryActivation();
|
||||
|
||||
_emojiInteractions->visibleAreaUpdated(
|
||||
@@ -2671,7 +2891,7 @@ void HistoryInner::enterEventHook(QEnterEvent *e) {
|
||||
}
|
||||
|
||||
void HistoryInner::leaveEventHook(QEvent *e) {
|
||||
_reactionsManager->updateButton({});
|
||||
_reactionsManager->updateButton({ .cursorLeft = true });
|
||||
if (auto item = App::hoveredItem()) {
|
||||
repaintItem(item);
|
||||
App::hoveredItem(nullptr);
|
||||
@@ -2693,8 +2913,9 @@ HistoryInner::~HistoryInner() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Instance == this) {
|
||||
Instance = nullptr;
|
||||
_history->delegateMixin()->setCurrent(nullptr);
|
||||
if (_migrated) {
|
||||
_migrated->delegateMixin()->setCurrent(nullptr);
|
||||
}
|
||||
delete _menu;
|
||||
_mouseAction = MouseAction::None;
|
||||
@@ -2966,6 +3187,7 @@ auto HistoryInner::reactionButtonParameters(
|
||||
if (top < 0
|
||||
|| !view->data()->canReact()
|
||||
|| _mouseAction == MouseAction::Dragging
|
||||
|| _mouseAction == MouseAction::Selecting
|
||||
|| inSelectionMode()) {
|
||||
return {};
|
||||
}
|
||||
@@ -3002,12 +3224,20 @@ void HistoryInner::mouseActionUpdate() {
|
||||
: nullptr;
|
||||
const auto item = view ? view->data().get() : nullptr;
|
||||
if (view) {
|
||||
App::mousedItem(view);
|
||||
const auto changed = (App::mousedItem() != view);
|
||||
if (changed) {
|
||||
repaintItem(App::mousedItem());
|
||||
App::mousedItem(view);
|
||||
repaintItem(App::mousedItem());
|
||||
}
|
||||
m = mapPointToItem(point, view);
|
||||
_reactionsManager->updateButton(reactionButtonParameters(
|
||||
view,
|
||||
m,
|
||||
reactionState));
|
||||
if (changed) {
|
||||
_reactionsManager->updateUniqueLimit(item);
|
||||
}
|
||||
if (view->pointState(m) != PointState::Outside) {
|
||||
if (App::hoveredItem() != view) {
|
||||
repaintItem(App::hoveredItem());
|
||||
@@ -3019,6 +3249,10 @@ void HistoryInner::mouseActionUpdate() {
|
||||
App::hoveredItem(nullptr);
|
||||
}
|
||||
} else {
|
||||
if (App::mousedItem()) {
|
||||
repaintItem(App::mousedItem());
|
||||
App::mousedItem(nullptr);
|
||||
}
|
||||
_reactionsManager->updateButton({});
|
||||
}
|
||||
if (_mouseActionItem && !_mouseActionItem->mainView()) {
|
||||
@@ -3720,139 +3954,7 @@ void HistoryInner::onParentGeometryChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
|
||||
class Result final : public HistoryView::ElementDelegate {
|
||||
public:
|
||||
HistoryView::Context elementContext() override {
|
||||
return HistoryView::Context::History;
|
||||
}
|
||||
std::unique_ptr<HistoryView::Element> elementCreate(
|
||||
not_null<HistoryMessage*> message,
|
||||
Element *replacing = nullptr) override {
|
||||
return std::make_unique<HistoryView::Message>(
|
||||
this,
|
||||
message,
|
||||
replacing);
|
||||
}
|
||||
std::unique_ptr<HistoryView::Element> elementCreate(
|
||||
not_null<HistoryService*> message,
|
||||
Element *replacing = nullptr) override {
|
||||
return std::make_unique<HistoryView::Service>(
|
||||
this,
|
||||
message,
|
||||
replacing);
|
||||
}
|
||||
bool elementUnderCursor(
|
||||
not_null<const Element*> view) override {
|
||||
return (App::hoveredItem() == view);
|
||||
}
|
||||
crl::time elementHighlightTime(
|
||||
not_null<const HistoryItem*> item) override {
|
||||
return Instance ? Instance->elementHighlightTime(item) : 0;
|
||||
}
|
||||
bool elementInSelectionMode() override {
|
||||
return Instance ? Instance->inSelectionMode() : false;
|
||||
}
|
||||
bool elementIntersectsRange(
|
||||
not_null<const Element*> view,
|
||||
int from,
|
||||
int till) override {
|
||||
return Instance
|
||||
? Instance->elementIntersectsRange(view, from, till)
|
||||
: false;
|
||||
}
|
||||
void elementStartStickerLoop(
|
||||
not_null<const Element*> view) override {
|
||||
if (Instance) {
|
||||
Instance->elementStartStickerLoop(view);
|
||||
}
|
||||
}
|
||||
void elementShowPollResults(
|
||||
not_null<PollData*> poll,
|
||||
FullMsgId context) override {
|
||||
if (Instance) {
|
||||
Instance->elementShowPollResults(poll, context);
|
||||
}
|
||||
}
|
||||
void elementOpenPhoto(
|
||||
not_null<PhotoData*> photo,
|
||||
FullMsgId context) override {
|
||||
if (Instance) {
|
||||
Instance->elementOpenPhoto(photo, context);
|
||||
}
|
||||
}
|
||||
void elementOpenDocument(
|
||||
not_null<DocumentData*> document,
|
||||
FullMsgId context,
|
||||
bool showInMediaView = false) override {
|
||||
if (Instance) {
|
||||
Instance->elementOpenDocument(
|
||||
document,
|
||||
context,
|
||||
showInMediaView);
|
||||
}
|
||||
}
|
||||
void elementCancelUpload(const FullMsgId &context) override {
|
||||
if (Instance) {
|
||||
Instance->elementCancelUpload(context);
|
||||
}
|
||||
}
|
||||
void elementShowTooltip(
|
||||
const TextWithEntities &text,
|
||||
Fn<void()> hiddenCallback) override {
|
||||
if (Instance) {
|
||||
Instance->elementShowTooltip(text, hiddenCallback);
|
||||
}
|
||||
}
|
||||
bool elementIsGifPaused() override {
|
||||
return Instance ? Instance->elementIsGifPaused() : false;
|
||||
}
|
||||
bool elementHideReply(not_null<const Element*> view) override {
|
||||
return false;
|
||||
}
|
||||
bool elementShownUnread(not_null<const Element*> view) override {
|
||||
return view->data()->unread();
|
||||
}
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override {
|
||||
if (Instance) {
|
||||
Instance->elementSendBotCommand(command, context);
|
||||
}
|
||||
}
|
||||
void elementHandleViaClick(not_null<UserData*> bot) override {
|
||||
if (Instance) {
|
||||
Instance->elementHandleViaClick(bot);
|
||||
}
|
||||
}
|
||||
bool elementIsChatWide() override {
|
||||
return Instance
|
||||
? Instance->elementIsChatWide()
|
||||
: false;
|
||||
}
|
||||
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {
|
||||
Expects(Instance != nullptr);
|
||||
|
||||
return Instance->elementPathShiftGradient();
|
||||
}
|
||||
void elementReplyTo(const FullMsgId &to) override {
|
||||
if (Instance) {
|
||||
Instance->elementReplyTo(to);
|
||||
}
|
||||
}
|
||||
void elementStartInteraction(not_null<const Element*> view) override {
|
||||
if (Instance) {
|
||||
Instance->elementStartInteraction(view);
|
||||
}
|
||||
}
|
||||
void elementShowSpoilerAnimation() override {
|
||||
if (Instance) {
|
||||
Instance->elementShowSpoilerAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
static Result result;
|
||||
return &result;
|
||||
auto HistoryInner::DelegateMixin()
|
||||
-> std::unique_ptr<HistoryMainElementDelegateMixin> {
|
||||
return std::make_unique<HistoryMainElementDelegate>();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,26 @@ enum class ReportReason;
|
||||
class PathShiftGradient;
|
||||
} // namespace Ui
|
||||
|
||||
class HistoryInner;
|
||||
class HistoryMainElementDelegate;
|
||||
class HistoryMainElementDelegateMixin {
|
||||
public:
|
||||
void setCurrent(HistoryInner *widget) {
|
||||
_widget = widget;
|
||||
}
|
||||
|
||||
virtual not_null<HistoryView::ElementDelegate*> delegate() = 0;
|
||||
virtual ~HistoryMainElementDelegateMixin();
|
||||
|
||||
private:
|
||||
friend class HistoryMainElementDelegate;
|
||||
|
||||
HistoryMainElementDelegateMixin();
|
||||
|
||||
HistoryInner *_widget = nullptr;
|
||||
|
||||
};
|
||||
|
||||
class HistoryWidget;
|
||||
class HistoryInner
|
||||
: public Ui::RpWidget
|
||||
@@ -160,8 +180,8 @@ public:
|
||||
|
||||
void onParentGeometryChanged();
|
||||
|
||||
// HistoryView::ElementDelegate interface.
|
||||
static not_null<HistoryView::ElementDelegate*> ElementDelegate();
|
||||
[[nodiscard]] static auto DelegateMixin()
|
||||
-> std::unique_ptr<HistoryMainElementDelegateMixin>;
|
||||
|
||||
protected:
|
||||
bool focusNextPrevChild(bool next) override;
|
||||
@@ -364,17 +384,17 @@ private:
|
||||
// Does any of the shown histories has this flag set.
|
||||
bool hasPendingResizedItems() const;
|
||||
|
||||
static HistoryInner *Instance;
|
||||
|
||||
const not_null<HistoryWidget*> _widget;
|
||||
const not_null<Ui::ScrollArea*> _scroll;
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<PeerData*> _peer;
|
||||
const not_null<History*> _history;
|
||||
const not_null<HistoryView::ElementDelegate*> _elementDelegate;
|
||||
const std::unique_ptr<HistoryView::EmojiInteractions> _emojiInteractions;
|
||||
std::shared_ptr<Ui::ChatTheme> _theme;
|
||||
|
||||
History *_migrated = nullptr;
|
||||
HistoryView::ElementDelegate *_migratedElementDelegate = nullptr;
|
||||
int _contentWidth = 0;
|
||||
int _historyPaddingTop = 0;
|
||||
int _revealHeight = 0;
|
||||
@@ -417,6 +437,7 @@ private:
|
||||
CursorState _mouseCursorState = CursorState();
|
||||
uint16 _mouseTextSymbol = 0;
|
||||
bool _pressWasInactive = false;
|
||||
bool _recountedAfterPendingResizedItems = false;
|
||||
|
||||
QPoint _trippleClickPoint;
|
||||
base::Timer _trippleClickTimer;
|
||||
@@ -448,6 +469,8 @@ private:
|
||||
|
||||
Ui::Animations::Simple _spoilerOpacity;
|
||||
|
||||
// _menu must be destroyed before _whoReactedMenuLifetime.
|
||||
rpl::lifetime _whoReactedMenuLifetime;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
bool _scrollDateShown = false;
|
||||
|
||||
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_isolated_emoji.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "storage/file_upload.h"
|
||||
#include "storage/storage_facade.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
@@ -45,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_sponsored_messages.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
@@ -294,6 +296,10 @@ HistoryItem *HistoryItem::lookupDiscussionPostOriginal() const {
|
||||
PeerData *HistoryItem::displayFrom() const {
|
||||
if (const auto sender = discussionPostOriginalSender()) {
|
||||
return sender;
|
||||
} else if (const auto sponsored = Get<HistoryMessageSponsored>()) {
|
||||
if (sponsored->sender) {
|
||||
return nullptr;
|
||||
}
|
||||
} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
||||
if (history()->peer->isSelf() || history()->peer->isRepliesChat() || forwarded->imported) {
|
||||
return forwarded->originalSender;
|
||||
@@ -377,7 +383,7 @@ void HistoryItem::setIsPinned(bool pinned) {
|
||||
id));
|
||||
}
|
||||
if (changed) {
|
||||
history()->owner().requestItemResize(this);
|
||||
history()->owner().notifyItemDataChange(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +490,7 @@ bool HistoryItem::isScheduled() const {
|
||||
}
|
||||
|
||||
bool HistoryItem::isSponsored() const {
|
||||
return (_flags & MessageFlag::IsSponsored);
|
||||
return Has<HistoryMessageSponsored>();
|
||||
}
|
||||
|
||||
bool HistoryItem::skipNotification() const {
|
||||
@@ -802,6 +808,26 @@ void HistoryItem::toggleReaction(const QString &reaction) {
|
||||
}
|
||||
|
||||
void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
|
||||
const auto history = this->history();
|
||||
const auto toUser = (reactions && out())
|
||||
? history->peer->asUser()
|
||||
: nullptr;
|
||||
const auto toContact = toUser && toUser->isContact();
|
||||
const auto maybeNotify = toContact && lookupHisReaction().isEmpty();
|
||||
setReactions(reactions);
|
||||
if (maybeNotify) {
|
||||
if (const auto reaction = lookupHisReaction(); !reaction.isEmpty()) {
|
||||
const auto notification = ItemNotification{
|
||||
this,
|
||||
ItemNotificationType::Reaction,
|
||||
};
|
||||
history->pushNotification(notification);
|
||||
Core::App().notifications().schedule(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryItem::setReactions(const MTPMessageReactions *reactions) {
|
||||
if (reactions || _reactionsLastRefreshed) {
|
||||
_reactionsLastRefreshed = crl::now();
|
||||
}
|
||||
@@ -828,7 +854,10 @@ void HistoryItem::updateReactions(const MTPMessageReactions *reactions) {
|
||||
} else if (!_reactions) {
|
||||
_reactions = std::make_unique<Data::MessageReactions>(this);
|
||||
}
|
||||
_reactions->set(data.vresults().v, data.is_min());
|
||||
_reactions->set(
|
||||
data.vresults().v,
|
||||
data.vrecent_reactons().value_or_empty(),
|
||||
data.is_min());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -841,6 +870,14 @@ const base::flat_map<QString, int> &HistoryItem::reactions() const {
|
||||
return _reactions ? _reactions->list() : kEmpty;
|
||||
}
|
||||
|
||||
auto HistoryItem::recentReactions() const
|
||||
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> & {
|
||||
static const auto kEmpty = base::flat_map<
|
||||
QString,
|
||||
std::vector<not_null<UserData*>>>();
|
||||
return _reactions ? _reactions->recent() : kEmpty;
|
||||
}
|
||||
|
||||
bool HistoryItem::canViewReactions() const {
|
||||
return (_flags & MessageFlag::CanViewReactions)
|
||||
&& _reactions
|
||||
@@ -851,6 +888,24 @@ QString HistoryItem::chosenReaction() const {
|
||||
return _reactions ? _reactions->chosen() : QString();
|
||||
}
|
||||
|
||||
QString HistoryItem::lookupHisReaction() const {
|
||||
if (!_reactions) {
|
||||
return QString();
|
||||
}
|
||||
const auto &list = _reactions->list();
|
||||
if (list.empty()) {
|
||||
return QString();
|
||||
}
|
||||
const auto chosen = _reactions->chosen();
|
||||
const auto &[first, count] = list.front();
|
||||
if (chosen.isEmpty() || first != chosen || count > 1) {
|
||||
return first;
|
||||
} else if (list.size() == 1) {
|
||||
return QString();
|
||||
}
|
||||
return list.back().first;
|
||||
}
|
||||
|
||||
crl::time HistoryItem::lastReactionsRefreshTime() const {
|
||||
return _reactionsLastRefreshed;
|
||||
}
|
||||
@@ -900,8 +955,10 @@ PeerData *HistoryItem::senderOriginal() const {
|
||||
return (peer->isChannel() && !peer->isMegagroup()) ? peer : from();
|
||||
}
|
||||
|
||||
const HiddenSenderInfo *HistoryItem::hiddenForwardedInfo() const {
|
||||
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
||||
const HiddenSenderInfo *HistoryItem::hiddenSenderInfo() const {
|
||||
if (const auto sponsored = Get<HistoryMessageSponsored>()) {
|
||||
return sponsored->sender.get();
|
||||
} else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
|
||||
return forwarded->hiddenSenderInfo.get();
|
||||
}
|
||||
return nullptr;
|
||||
@@ -1067,23 +1124,20 @@ bool HistoryItem::isEmpty() const {
|
||||
&& !Has<HistoryMessageLogEntryOriginal>();
|
||||
}
|
||||
|
||||
QString HistoryItem::notificationText() const {
|
||||
TextWithEntities HistoryItem::notificationText() const {
|
||||
const auto result = [&] {
|
||||
if (_media && !isService()) {
|
||||
return _media->notificationText();
|
||||
} else if (!emptyText()) {
|
||||
return TextUtilities::TextWithSpoilerCommands(
|
||||
_text.toTextWithEntities());
|
||||
return _text.toTextWithEntities();
|
||||
}
|
||||
return QString();
|
||||
return TextWithEntities();
|
||||
}();
|
||||
return (result.size() <= kNotificationTextLimit)
|
||||
? result
|
||||
: TextUtilities::CutTextWithCommands(
|
||||
result,
|
||||
kNotificationTextLimit,
|
||||
textcmdStartSpoiler(),
|
||||
textcmdStopSpoiler());
|
||||
if (result.text.size() <= kNotificationTextLimit) {
|
||||
return result;
|
||||
}
|
||||
return Ui::Text::Mid(result, 0, kNotificationTextLimit).append(
|
||||
Ui::kQEllipsis);
|
||||
}
|
||||
|
||||
ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {
|
||||
@@ -1092,12 +1146,7 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {
|
||||
return _media->toPreview(options);
|
||||
} else if (!emptyText()) {
|
||||
return {
|
||||
.text = TextUtilities::Clean(
|
||||
options.ignoreSpoilers
|
||||
? _text.toString()
|
||||
: TextUtilities::TextWithSpoilerCommands(
|
||||
_text.toTextWithEntities()),
|
||||
!options.ignoreSpoilers),
|
||||
.text = _text.toTextWithEntities()
|
||||
};
|
||||
}
|
||||
return {};
|
||||
@@ -1118,6 +1167,8 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {
|
||||
const auto sender = [&]() -> std::optional<QString> {
|
||||
if (options.hideSender || isPost() || isEmpty()) {
|
||||
return {};
|
||||
} else if (const auto sponsored = Get<HistoryMessageSponsored>()) {
|
||||
return sponsored->sender->name;
|
||||
} else if (!_history->peer->isUser()) {
|
||||
if (const auto from = displayFrom()) {
|
||||
return fromSender(from);
|
||||
@@ -1131,16 +1182,12 @@ ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {
|
||||
if (!sender) {
|
||||
return result;
|
||||
}
|
||||
const auto fromWrapped = textcmdLink(
|
||||
1,
|
||||
tr::lng_dialogs_text_from_wrapped(
|
||||
tr::now,
|
||||
lt_from,
|
||||
TextUtilities::Clean(*sender)));
|
||||
const auto fromWrapped = Ui::Text::PlainLink(
|
||||
tr::lng_dialogs_text_from_wrapped(tr::now, lt_from, *sender));
|
||||
return Dialogs::Ui::PreviewWithSender(std::move(result), fromWrapped);
|
||||
}
|
||||
|
||||
QString HistoryItem::inReplyText() const {
|
||||
TextWithEntities HistoryItem::inReplyText() const {
|
||||
return toPreview({
|
||||
.hideSender = true,
|
||||
.generateImages = false,
|
||||
@@ -1265,7 +1312,7 @@ not_null<HistoryItem*> HistoryItem::Create(
|
||||
data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0));
|
||||
} else if (checked == MediaCheckResult::Empty) {
|
||||
const auto text = HistoryService::PreparedText{
|
||||
tr::lng_message_empty(tr::now)
|
||||
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
return history->makeServiceMessage(
|
||||
id,
|
||||
@@ -1284,7 +1331,7 @@ not_null<HistoryItem*> HistoryItem::Create(
|
||||
return history->makeServiceMessage(id, data, localFlags);
|
||||
}, [&](const MTPDmessageEmpty &data) -> HistoryItem* {
|
||||
const auto text = HistoryService::PreparedText{
|
||||
tr::lng_message_empty(tr::now)
|
||||
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
return history->makeServiceMessage(id, localFlags, TimeId(0), text);
|
||||
});
|
||||
|
||||
@@ -290,7 +290,7 @@ public:
|
||||
[[nodiscard]] virtual QString notificationHeader() const {
|
||||
return QString();
|
||||
}
|
||||
[[nodiscard]] virtual QString notificationText() const;
|
||||
[[nodiscard]] virtual TextWithEntities notificationText() const;
|
||||
|
||||
using ToPreviewOptions = HistoryView::ToPreviewOptions;
|
||||
using ItemPreview = HistoryView::ItemPreview;
|
||||
@@ -299,7 +299,7 @@ public:
|
||||
// Example: "[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text"
|
||||
[[nodiscard]] virtual ItemPreview toPreview(
|
||||
ToPreviewOptions options) const;
|
||||
[[nodiscard]] virtual QString inReplyText() const;
|
||||
[[nodiscard]] virtual TextWithEntities inReplyText() const;
|
||||
[[nodiscard]] virtual Ui::Text::IsolatedEmoji isolatedEmoji() const;
|
||||
[[nodiscard]] virtual TextWithEntities originalText() const {
|
||||
return TextWithEntities();
|
||||
@@ -359,8 +359,11 @@ public:
|
||||
void updateReactions(const MTPMessageReactions *reactions);
|
||||
void updateReactionsUnknown();
|
||||
[[nodiscard]] const base::flat_map<QString, int> &reactions() const;
|
||||
[[nodiscard]] auto recentReactions() const
|
||||
-> const base::flat_map<QString, std::vector<not_null<UserData*>>> &;
|
||||
[[nodiscard]] bool canViewReactions() const;
|
||||
[[nodiscard]] QString chosenReaction() const;
|
||||
[[nodiscard]] QString lookupHisReaction() const;
|
||||
[[nodiscard]] crl::time lastReactionsRefreshTime() const;
|
||||
|
||||
[[nodiscard]] bool hasDirectLink() const;
|
||||
@@ -387,7 +390,7 @@ public:
|
||||
|
||||
[[nodiscard]] TimeId dateOriginal() const;
|
||||
[[nodiscard]] PeerData *senderOriginal() const;
|
||||
[[nodiscard]] const HiddenSenderInfo *hiddenForwardedInfo() const;
|
||||
[[nodiscard]] const HiddenSenderInfo *hiddenSenderInfo() const;
|
||||
[[nodiscard]] not_null<PeerData*> fromOriginal() const;
|
||||
[[nodiscard]] QString authorOriginal() const;
|
||||
[[nodiscard]] MsgId idOriginal() const;
|
||||
@@ -440,6 +443,8 @@ protected:
|
||||
void finishEdition(int oldKeyboardTop);
|
||||
void finishEditionToEmpty();
|
||||
|
||||
void setReactions(const MTPMessageReactions *reactions);
|
||||
|
||||
const not_null<History*> _history;
|
||||
const not_null<PeerData*> _from;
|
||||
MessageFlags _flags = 0;
|
||||
@@ -467,7 +472,6 @@ protected:
|
||||
crl::time _reactionsLastRefreshed = 0;
|
||||
|
||||
private:
|
||||
|
||||
TimeId _date = 0;
|
||||
TimeId _ttlDestroyAt = 0;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/image/image.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "history/history.h"
|
||||
@@ -106,20 +107,21 @@ HiddenSenderInfo::HiddenSenderInfo(const QString &name, bool external)
|
||||
}
|
||||
|
||||
void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
|
||||
auto phrase = QString();
|
||||
auto phrase = TextWithEntities();
|
||||
const auto fromChannel = originalSender
|
||||
&& originalSender->isChannel()
|
||||
&& !originalSender->isMegagroup();
|
||||
const auto name = originalSender
|
||||
? originalSender->name
|
||||
: hiddenSenderInfo->name;
|
||||
const auto name = TextWithEntities{
|
||||
.text = originalSender ? originalSender->name : hiddenSenderInfo->name
|
||||
};
|
||||
if (!originalAuthor.isEmpty()) {
|
||||
phrase = tr::lng_forwarded_signed(
|
||||
tr::now,
|
||||
lt_channel,
|
||||
name,
|
||||
lt_user,
|
||||
originalAuthor);
|
||||
{ .text = originalAuthor },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
phrase = name;
|
||||
}
|
||||
@@ -128,16 +130,18 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
|
||||
phrase = tr::lng_forwarded_channel_via(
|
||||
tr::now,
|
||||
lt_channel,
|
||||
textcmdLink(1, phrase),
|
||||
Ui::Text::Link(phrase.text, QString()), // Link 1.
|
||||
lt_inline_bot,
|
||||
textcmdLink(2, '@' + via->bot->username));
|
||||
Ui::Text::Link('@' + via->bot->username, {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
phrase = tr::lng_forwarded_via(
|
||||
tr::now,
|
||||
lt_user,
|
||||
textcmdLink(1, phrase),
|
||||
Ui::Text::Link(phrase.text, QString()), // Link 1.
|
||||
lt_inline_bot,
|
||||
textcmdLink(2, '@' + via->bot->username));
|
||||
Ui::Text::Link('@' + via->bot->username, {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else {
|
||||
if (fromChannel || !psaType.isEmpty()) {
|
||||
@@ -145,28 +149,32 @@ void HistoryMessageForwarded::create(const HistoryMessageVia *via) const {
|
||||
? QString()
|
||||
: Lang::GetNonDefaultValue(
|
||||
kPsaForwardedPrefix + psaType.toUtf8());
|
||||
phrase = !custom.isEmpty()
|
||||
? custom.replace("{channel}", textcmdLink(1, phrase))
|
||||
: (psaType.isEmpty()
|
||||
if (!custom.isEmpty()) {
|
||||
custom = custom.replace("{channel}", phrase.text);
|
||||
const auto index = int(custom.indexOf(phrase.text));
|
||||
const auto size = int(phrase.text.size());
|
||||
phrase = TextWithEntities{
|
||||
.text = custom,
|
||||
.entities = {{ EntityType::CustomUrl, index, size, {} }},
|
||||
};
|
||||
} else {
|
||||
phrase = (psaType.isEmpty()
|
||||
? tr::lng_forwarded_channel
|
||||
: tr::lng_forwarded_psa_default)(
|
||||
tr::now,
|
||||
lt_channel,
|
||||
textcmdLink(1, phrase));
|
||||
Ui::Text::Link(phrase.text, QString()), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else {
|
||||
phrase = tr::lng_forwarded(
|
||||
tr::now,
|
||||
lt_user,
|
||||
textcmdLink(1, phrase));
|
||||
Ui::Text::Link(phrase.text, QString()), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}
|
||||
TextParseOptions opts = {
|
||||
TextParseRichText,
|
||||
0,
|
||||
0,
|
||||
Qt::LayoutDirectionAuto
|
||||
};
|
||||
text.setText(st::fwdTextStyle, phrase, opts);
|
||||
text.setMarkedText(st::fwdTextStyle, phrase);
|
||||
static const auto hidden = std::make_shared<LambdaClickHandler>([] {
|
||||
Ui::Toast::Show(tr::lng_forwarded_hidden(tr::now));
|
||||
});
|
||||
@@ -210,7 +218,7 @@ bool HistoryMessageReply::updateData(
|
||||
}
|
||||
|
||||
if (replyToMsg) {
|
||||
replyToText.setText(
|
||||
replyToText.setMarkedText(
|
||||
st::messageTextStyle,
|
||||
replyToMsg->inReplyText(),
|
||||
Ui::DialogTextOptions());
|
||||
|
||||
@@ -102,6 +102,18 @@ struct HistoryMessageForwarded : public RuntimeComponent<HistoryMessageForwarded
|
||||
bool imported = false;
|
||||
};
|
||||
|
||||
struct HistoryMessageSponsored : public RuntimeComponent<HistoryMessageSponsored, HistoryItem> {
|
||||
enum class Type : uchar {
|
||||
User,
|
||||
Group,
|
||||
Broadcast,
|
||||
Post,
|
||||
Bot,
|
||||
};
|
||||
std::unique_ptr<HiddenSenderInfo> sender;
|
||||
Type type = Type::User;
|
||||
};
|
||||
|
||||
struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply, HistoryItem> {
|
||||
HistoryMessageReply() = default;
|
||||
HistoryMessageReply(const HistoryMessageReply &other) = delete;
|
||||
|
||||
@@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "data/data_web_page.h"
|
||||
#include "data/data_sponsored_messages.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_chat.h"
|
||||
@@ -500,7 +501,7 @@ HistoryMessage::HistoryMessage(
|
||||
setMedia(*media);
|
||||
}
|
||||
const auto textWithEntities = TextWithEntities{
|
||||
TextUtilities::Clean(qs(data.vmessage())),
|
||||
qs(data.vmessage()),
|
||||
Api::EntitiesFromMTP(
|
||||
&history->session(),
|
||||
data.ventities().value_or_empty())
|
||||
@@ -511,7 +512,7 @@ HistoryMessage::HistoryMessage(
|
||||
MessageGroupId::FromRaw(history->peer->id, groupedId->v));
|
||||
}
|
||||
if (const auto reactions = data.vreactions()) {
|
||||
updateReactions(reactions);
|
||||
setReactions(reactions);
|
||||
}
|
||||
|
||||
applyTTL(data);
|
||||
@@ -582,7 +583,7 @@ HistoryMessage::HistoryMessage(
|
||||
&& (!originalMedia || !originalMedia->forceForwardedInfo()));
|
||||
if (!dropForwardInfo) {
|
||||
config.originalDate = original->dateOriginal();
|
||||
if (const auto info = original->hiddenForwardedInfo()) {
|
||||
if (const auto info = original->hiddenSenderInfo()) {
|
||||
config.senderNameOriginal = info->name;
|
||||
} else if (const auto senderOriginal = original->senderOriginal()) {
|
||||
config.senderOriginal = senderOriginal->id;
|
||||
@@ -768,6 +769,29 @@ HistoryMessage::HistoryMessage(
|
||||
setEmptyText();
|
||||
}
|
||||
|
||||
HistoryMessage::HistoryMessage(
|
||||
not_null<History*> history,
|
||||
MsgId id,
|
||||
Data::SponsoredFrom from,
|
||||
const TextWithEntities &textWithEntities)
|
||||
: HistoryItem(
|
||||
history,
|
||||
id,
|
||||
((history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0))
|
||||
//| (from.peer ? MessageFlag::HasFromId : MessageFlag(0))
|
||||
| MessageFlag::Local),
|
||||
HistoryItem::NewMessageDate(0),
|
||||
/*from.peer ? from.peer->id : */PeerId(0)) {
|
||||
createComponentsHelper(
|
||||
_flags,
|
||||
MsgId(0), // replyTo
|
||||
UserId(0), // viaBotId
|
||||
QString(), // postAuthor
|
||||
HistoryMessageMarkupData());
|
||||
setText(textWithEntities);
|
||||
setSponsoredFrom(from);
|
||||
}
|
||||
|
||||
void HistoryMessage::createComponentsHelper(
|
||||
MessageFlags flags,
|
||||
MsgId replyTo,
|
||||
@@ -1915,6 +1939,25 @@ void HistoryMessage::setUnreadRepliesCount(
|
||||
Data::MessageUpdate::Flag::RepliesUnreadCount);
|
||||
}
|
||||
|
||||
void HistoryMessage::setSponsoredFrom(const Data::SponsoredFrom &from) {
|
||||
AddComponents(HistoryMessageSponsored::Bit());
|
||||
const auto sponsored = Get<HistoryMessageSponsored>();
|
||||
sponsored->sender = std::make_unique<HiddenSenderInfo>(
|
||||
from.title,
|
||||
false);
|
||||
|
||||
using Type = HistoryMessageSponsored::Type;
|
||||
sponsored->type = from.isExactPost
|
||||
? Type::Post
|
||||
: from.isBot
|
||||
? Type::Bot
|
||||
: from.isBroadcast
|
||||
? Type::Broadcast
|
||||
: (from.peer && from.peer->isUser())
|
||||
? Type::User
|
||||
: Type::Group;
|
||||
}
|
||||
|
||||
void HistoryMessage::setReplyToTop(MsgId replyToTop) {
|
||||
const auto reply = Get<HistoryMessageReply>();
|
||||
if (!reply
|
||||
|
||||
@@ -14,6 +14,10 @@ struct SendAction;
|
||||
struct SendOptions;
|
||||
} // namespace Api
|
||||
|
||||
namespace Data {
|
||||
struct SponsoredFrom;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
class Message;
|
||||
} // namespace HistoryView
|
||||
@@ -115,6 +119,11 @@ public:
|
||||
const QString &postAuthor,
|
||||
not_null<GameData*> game,
|
||||
HistoryMessageMarkupData &&markup); // local game
|
||||
HistoryMessage(
|
||||
not_null<History*> history,
|
||||
MsgId id,
|
||||
Data::SponsoredFrom from,
|
||||
const TextWithEntities &textWithEntities); // sponsored
|
||||
|
||||
void refreshMedia(const MTPMessageMedia *media);
|
||||
void refreshSentMedia(const MTPMessageMedia *media);
|
||||
@@ -251,6 +260,7 @@ private:
|
||||
void setUnreadRepliesCount(
|
||||
not_null<HistoryMessageViews*> views,
|
||||
int count);
|
||||
void setSponsoredFrom(const Data::SponsoredFrom &from);
|
||||
|
||||
static void FillForwardedInfo(
|
||||
CreateConfig &config,
|
||||
|
||||
@@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "payments/payments_checkout_process.h" // CheckoutProcess::Start.
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -111,31 +112,65 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto u = history()->owner().user(users[0].v);
|
||||
if (u == _from) {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_user_joined(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_user_joined(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.links.push_back(u->createOpenLink());
|
||||
result.text = tr::lng_action_add_user(tr::now, lt_from, fromLinkText(), lt_user, textcmdLink(2, u->name));
|
||||
result.text = tr::lng_action_add_user(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_user,
|
||||
Ui::Text::Link(u->name, {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else if (users.isEmpty()) {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_add_user(tr::now, lt_from, fromLinkText(), lt_user, qsl("somebody"));
|
||||
result.text = tr::lng_action_add_user(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_user,
|
||||
{ .text = qsl("somebody") },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
for (auto i = 0, l = int(users.size()); i != l; ++i) {
|
||||
auto user = history()->owner().user(users[i].v);
|
||||
result.links.push_back(user->createOpenLink());
|
||||
|
||||
auto linkText = textcmdLink(i + 2, user->name);
|
||||
auto linkText = Ui::Text::Link(user->name, {});
|
||||
if (i == 0) {
|
||||
result.text = linkText;
|
||||
} else if (i + 1 == l) {
|
||||
result.text = tr::lng_action_add_users_and_last(tr::now, lt_accumulated, result.text, lt_user, linkText);
|
||||
result.text = tr::lng_action_add_users_and_last(
|
||||
tr::now,
|
||||
lt_accumulated,
|
||||
result.text,
|
||||
lt_user,
|
||||
linkText,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_add_users_and_one(tr::now, lt_accumulated, result.text, lt_user, linkText);
|
||||
result.text = tr::lng_action_add_users_and_one(
|
||||
tr::now,
|
||||
lt_accumulated,
|
||||
result.text,
|
||||
lt_user,
|
||||
linkText,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}
|
||||
result.text = tr::lng_action_add_users_many(tr::now, lt_from, fromLinkText(), lt_users, result.text);
|
||||
result.text = tr::lng_action_add_users_many(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_users,
|
||||
result.text,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -143,24 +178,42 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto prepareChatJoinedByLink = [this](const MTPDmessageActionChatJoinedByLink &action) {
|
||||
auto result = PreparedText{};
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_user_joined_by_link(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_user_joined_by_link(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
};
|
||||
|
||||
auto prepareChatCreate = [this](const MTPDmessageActionChatCreate &action) {
|
||||
auto result = PreparedText{};
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_created_chat(tr::now, lt_from, fromLinkText(), lt_title, TextUtilities::Clean(qs(action.vtitle())));
|
||||
result.text = tr::lng_action_created_chat(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_title,
|
||||
{ .text = qs(action.vtitle()) },
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
};
|
||||
|
||||
auto prepareChannelCreate = [this](const MTPDmessageActionChannelCreate &action) {
|
||||
auto result = PreparedText {};
|
||||
if (isPost()) {
|
||||
result.text = tr::lng_action_created_channel(tr::now);
|
||||
result.text = tr::lng_action_created_channel(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_created_chat(tr::now, lt_from, fromLinkText(), lt_title, TextUtilities::Clean(qs(action.vtitle())));
|
||||
result.text = tr::lng_action_created_chat(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_title,
|
||||
{ .text = qs(action.vtitle()) },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -168,10 +221,16 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto prepareChatDeletePhoto = [this] {
|
||||
auto result = PreparedText{};
|
||||
if (isPost()) {
|
||||
result.text = tr::lng_action_removed_photo_channel(tr::now);
|
||||
result.text = tr::lng_action_removed_photo_channel(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_removed_photo(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_removed_photo(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -180,12 +239,22 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto result = PreparedText{};
|
||||
if (peerFromUser(action.vuser_id()) == _from->id) {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_user_left(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_user_left(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
auto user = history()->owner().user(action.vuser_id().v);
|
||||
result.links.push_back(fromLink());
|
||||
result.links.push_back(user->createOpenLink());
|
||||
result.text = tr::lng_action_kick_user(tr::now, lt_from, fromLinkText(), lt_user, textcmdLink(2, user->name));
|
||||
result.text = tr::lng_action_kick_user(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_user,
|
||||
Ui::Text::Link(user->name, {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -193,10 +262,16 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto prepareChatEditPhoto = [this](const MTPDmessageActionChatEditPhoto &action) {
|
||||
auto result = PreparedText{};
|
||||
if (isPost()) {
|
||||
result.text = tr::lng_action_changed_photo_channel(tr::now);
|
||||
result.text = tr::lng_action_changed_photo_channel(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_changed_photo(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_changed_photo(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -204,10 +279,20 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto prepareChatEditTitle = [this](const MTPDmessageActionChatEditTitle &action) {
|
||||
auto result = PreparedText{};
|
||||
if (isPost()) {
|
||||
result.text = tr::lng_action_changed_title_channel(tr::now, lt_title, TextUtilities::Clean(qs(action.vtitle())));
|
||||
result.text = tr::lng_action_changed_title_channel(
|
||||
tr::now,
|
||||
lt_title,
|
||||
{ .text = (qs(action.vtitle())) },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_changed_title(tr::now, lt_from, fromLinkText(), lt_title, TextUtilities::Clean(qs(action.vtitle())));
|
||||
result.text = tr::lng_action_changed_title(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_title,
|
||||
{ .text = qs(action.vtitle()) },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -215,17 +300,23 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto prepareScreenshotTaken = [this] {
|
||||
auto result = PreparedText{};
|
||||
if (out()) {
|
||||
result.text = tr::lng_action_you_took_screenshot(tr::now);
|
||||
result.text = tr::lng_action_you_took_screenshot(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_took_screenshot(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_took_screenshot(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
auto prepareCustomAction = [&](const MTPDmessageActionCustomAction &action) {
|
||||
auto result = PreparedText{};
|
||||
result.text = qs(action.vmessage());
|
||||
result.text = { .text = qs(action.vmessage()) };
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -235,7 +326,8 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
result.text = tr::lng_action_bot_allowed_from_domain(
|
||||
tr::now,
|
||||
lt_domain,
|
||||
textcmdLink(qstr("http://") + domain, domain));
|
||||
Ui::Text::Link(domain, qstr("http://") + domain),
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -272,16 +364,21 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
result.text = tr::lng_action_secure_values_sent(
|
||||
tr::now,
|
||||
lt_user,
|
||||
textcmdLink(1, history()->peer->name),
|
||||
Ui::Text::Link(history()->peer->name, {}), // Link 1.
|
||||
lt_documents,
|
||||
documents.join(", "));
|
||||
{ .text = documents.join(", ") },
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
};
|
||||
|
||||
auto prepareContactSignUp = [this] {
|
||||
auto result = PreparedText{};
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_user_registered(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_user_registered(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -313,28 +410,31 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
return tr::lng_action_you_proximity_reached(
|
||||
tr::now,
|
||||
lt_distance,
|
||||
distance,
|
||||
{ .text = distance },
|
||||
lt_user,
|
||||
textcmdLink(1, toPeer->name));
|
||||
Ui::Text::Link(toPeer->name, {}), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
} else if (toId == selfId) {
|
||||
result.links.push_back(fromPeer->createOpenLink());
|
||||
return tr::lng_action_proximity_reached_you(
|
||||
tr::now,
|
||||
lt_from,
|
||||
textcmdLink(1, fromPeer->name),
|
||||
Ui::Text::Link(fromPeer->name, {}), // Link 1.
|
||||
lt_distance,
|
||||
distance);
|
||||
{ .text = distance },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromPeer->createOpenLink());
|
||||
result.links.push_back(toPeer->createOpenLink());
|
||||
return tr::lng_action_proximity_reached(
|
||||
tr::now,
|
||||
lt_from,
|
||||
textcmdLink(1, fromPeer->name),
|
||||
Ui::Text::Link(fromPeer->name, {}), // Link 1.
|
||||
lt_distance,
|
||||
distance,
|
||||
{ .text = distance },
|
||||
lt_user,
|
||||
textcmdLink(2, toPeer->name));
|
||||
Ui::Text::Link(toPeer->name, {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}();
|
||||
return result;
|
||||
@@ -358,26 +458,31 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
result.text = tr::lng_action_group_call_finished(
|
||||
tr::now,
|
||||
lt_duration,
|
||||
text);
|
||||
{ .text = text },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_group_call_finished_group(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(),
|
||||
fromLinkText(), // Link 1.
|
||||
lt_duration,
|
||||
text);
|
||||
{ .text = text },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (history()->peer->isBroadcast()) {
|
||||
result.text = tr::lng_action_group_call_started_channel(tr::now);
|
||||
result.text = tr::lng_action_group_call_started_channel(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_group_call_started_group(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText());
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -410,22 +515,44 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
: tr::lng_ttl_about_duration3(tr::now);
|
||||
if (isPost()) {
|
||||
if (!period) {
|
||||
result.text = tr::lng_action_ttl_removed_channel(tr::now);
|
||||
result.text = tr::lng_action_ttl_removed_channel(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_ttl_changed_channel(tr::now, lt_duration, duration);
|
||||
result.text = tr::lng_action_ttl_changed_channel(
|
||||
tr::now,
|
||||
lt_duration,
|
||||
{ .text = duration },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else if (_from->isSelf()) {
|
||||
if (!period) {
|
||||
result.text = tr::lng_action_ttl_removed_you(tr::now);
|
||||
result.text = tr::lng_action_ttl_removed_you(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_ttl_changed_you(tr::now, lt_duration, duration);
|
||||
result.text = tr::lng_action_ttl_changed_you(
|
||||
tr::now,
|
||||
lt_duration,
|
||||
{ .text = duration },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
if (!period) {
|
||||
result.text = tr::lng_action_ttl_removed(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_ttl_removed(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_ttl_changed(tr::now, lt_from, fromLinkText(), lt_duration, duration);
|
||||
result.text = tr::lng_action_ttl_changed(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_duration,
|
||||
{ .text = duration },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -435,22 +562,34 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto result = PreparedText{};
|
||||
const auto text = qs(action.vemoticon());
|
||||
if (!text.isEmpty()) {
|
||||
if (isPost()) {
|
||||
result.text = tr::lng_action_theme_changed_channel(tr::now, lt_emoji, text);
|
||||
} else if (_from->isSelf()) {
|
||||
result.text = tr::lng_action_you_theme_changed(tr::now, lt_emoji, text);
|
||||
if (_from->isSelf()) {
|
||||
result.text = tr::lng_action_you_theme_changed(
|
||||
tr::now,
|
||||
lt_emoji,
|
||||
{ .text = text },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_theme_changed(tr::now, lt_from, fromLinkText(), lt_emoji, text);
|
||||
result.text = tr::lng_action_theme_changed(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_emoji,
|
||||
{ .text = text },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else {
|
||||
if (isPost()) {
|
||||
result.text = tr::lng_action_theme_disabled_channel(tr::now);
|
||||
} else if (_from->isSelf()) {
|
||||
result.text = tr::lng_action_you_theme_disabled(tr::now);
|
||||
if (_from->isSelf()) {
|
||||
result.text = tr::lng_action_you_theme_disabled(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_theme_disabled(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_theme_disabled(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -459,7 +598,11 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
auto prepareChatJoinedByRequest = [this](const MTPDmessageActionChatJoinedByRequest &action) {
|
||||
auto result = PreparedText{};
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_user_joined_by_request(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_action_user_joined_by_request(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -508,10 +651,14 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
return prepareProximityReached(data);
|
||||
}, [](const MTPDmessageActionPaymentSentMe &) {
|
||||
LOG(("API Error: messageActionPaymentSentMe received."));
|
||||
return PreparedText{ tr::lng_message_empty(tr::now) };
|
||||
return PreparedText{
|
||||
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
}, [](const MTPDmessageActionSecureValuesSentMe &) {
|
||||
LOG(("API Error: messageActionSecureValuesSentMe received."));
|
||||
return PreparedText{ tr::lng_message_empty(tr::now) };
|
||||
return PreparedText{
|
||||
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
}, [&](const MTPDmessageActionGroupCall &data) {
|
||||
return prepareGroupCall(data);
|
||||
}, [&](const MTPDmessageActionInviteToGroupCall &data) {
|
||||
@@ -525,7 +672,9 @@ void HistoryService::setMessageByAction(const MTPmessageAction &action) {
|
||||
}, [&](const MTPDmessageActionChatJoinedByRequest &data) {
|
||||
return prepareChatJoinedByRequest(data);
|
||||
}, [](const MTPDmessageActionEmpty &) {
|
||||
return PreparedText{ tr::lng_message_empty(tr::now) };
|
||||
return PreparedText{
|
||||
tr::lng_message_empty(tr::now, Ui::Text::WithEntities)
|
||||
};
|
||||
});
|
||||
|
||||
setServiceText(messageText);
|
||||
@@ -642,36 +791,73 @@ HistoryService::PreparedText HistoryService::prepareInvitedToCallText(
|
||||
const QVector<MTPlong> &users,
|
||||
CallId linkCallId) {
|
||||
const auto owner = &history()->owner();
|
||||
auto chatText = tr::lng_action_invite_user_chat(tr::now);
|
||||
auto chatText = tr::lng_action_invite_user_chat(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities);
|
||||
auto result = PreparedText{};
|
||||
result.links.push_back(fromLink());
|
||||
auto linkIndex = 1;
|
||||
if (linkCallId) {
|
||||
const auto peer = history()->peer;
|
||||
result.links.push_back(GroupCallClickHandler(peer, linkCallId));
|
||||
chatText = textcmdLink(++linkIndex, chatText);
|
||||
chatText = Ui::Text::Link(chatText.text, {});
|
||||
}
|
||||
if (users.size() == 1) {
|
||||
auto user = owner->user(users[0].v);
|
||||
result.links.push_back(user->createOpenLink());
|
||||
result.text = tr::lng_action_invite_user(tr::now, lt_from, fromLinkText(), lt_user, textcmdLink(++linkIndex, user->name), lt_chat, chatText);
|
||||
result.text = tr::lng_action_invite_user(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_user,
|
||||
Ui::Text::Link(user->name, {}), // Link N.
|
||||
lt_chat,
|
||||
chatText,
|
||||
Ui::Text::WithEntities);
|
||||
} else if (users.isEmpty()) {
|
||||
result.text = tr::lng_action_invite_user(tr::now, lt_from, fromLinkText(), lt_user, qsl("somebody"), lt_chat, chatText);
|
||||
result.text = tr::lng_action_invite_user(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_user,
|
||||
{ .text = qsl("somebody") },
|
||||
lt_chat,
|
||||
chatText,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
for (auto i = 0, l = int(users.size()); i != l; ++i) {
|
||||
auto user = owner->user(users[i].v);
|
||||
result.links.push_back(user->createOpenLink());
|
||||
|
||||
auto linkText = textcmdLink(++linkIndex, user->name);
|
||||
auto linkText = Ui::Text::Link(user->name, {});
|
||||
if (i == 0) {
|
||||
result.text = linkText;
|
||||
} else if (i + 1 == l) {
|
||||
result.text = tr::lng_action_invite_users_and_last(tr::now, lt_accumulated, result.text, lt_user, linkText);
|
||||
result.text = tr::lng_action_invite_users_and_last(
|
||||
tr::now,
|
||||
lt_accumulated,
|
||||
result.text,
|
||||
lt_user,
|
||||
linkText,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_invite_users_and_one(tr::now, lt_accumulated, result.text, lt_user, linkText);
|
||||
result.text = tr::lng_action_invite_users_and_one(
|
||||
tr::now,
|
||||
lt_accumulated,
|
||||
result.text,
|
||||
lt_user,
|
||||
linkText,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}
|
||||
result.text = tr::lng_action_invite_users_many(tr::now, lt_from, fromLinkText(), lt_users, result.text, lt_chat, chatText);
|
||||
result.text = tr::lng_action_invite_users_many(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_users,
|
||||
result.text,
|
||||
lt_chat,
|
||||
chatText,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -696,38 +882,63 @@ HistoryService::PreparedText HistoryService::preparePinnedText() {
|
||||
result.links.push_back(fromLink());
|
||||
result.links.push_back(pinned->lnk);
|
||||
if (mediaText.isEmpty()) {
|
||||
auto original = TextUtilities::TextWithSpoilerCommands(
|
||||
pinned->msg->originalText());
|
||||
auto original = pinned->msg->originalText();
|
||||
auto cutAt = 0;
|
||||
auto limit = kPinnedMessageTextLimit;
|
||||
auto size = original.size();
|
||||
auto size = original.text.size();
|
||||
for (; limit != 0;) {
|
||||
--limit;
|
||||
if (cutAt >= size) break;
|
||||
if (original.at(cutAt).isLowSurrogate() && cutAt + 1 < size && original.at(cutAt + 1).isHighSurrogate()) {
|
||||
if (original.text.at(cutAt).isLowSurrogate()
|
||||
&& (cutAt + 1 < size)
|
||||
&& original.text.at(cutAt + 1).isHighSurrogate()) {
|
||||
cutAt += 2;
|
||||
} else {
|
||||
++cutAt;
|
||||
}
|
||||
}
|
||||
if (!limit && cutAt + 5 < size) {
|
||||
original = TextUtilities::CutTextWithCommands(
|
||||
std::move(original),
|
||||
cutAt,
|
||||
textcmdStartSpoiler(),
|
||||
textcmdStopSpoiler());
|
||||
original = Ui::Text::Mid(original, 0, cutAt).append(
|
||||
Ui::kQEllipsis);
|
||||
}
|
||||
result.text = tr::lng_action_pinned_message(tr::now, lt_from, fromLinkText(), lt_text, textcmdLink(2, original));
|
||||
original = Ui::Text::Wrapped(
|
||||
std::move(original),
|
||||
EntityType::CustomUrl);
|
||||
result.text = tr::lng_action_pinned_message(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_text,
|
||||
std::move(original), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_pinned_media(tr::now, lt_from, fromLinkText(), lt_media, textcmdLink(2, mediaText));
|
||||
result.text = tr::lng_action_pinned_media(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_media,
|
||||
Ui::Text::Link(mediaText, {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else if (pinned && pinned->msgId) {
|
||||
result.links.push_back(fromLink());
|
||||
result.links.push_back(pinned->lnk);
|
||||
result.text = tr::lng_action_pinned_media(tr::now, lt_from, fromLinkText(), lt_media, textcmdLink(2, tr::lng_contacts_loading(tr::now)));
|
||||
result.text = tr::lng_action_pinned_media(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_media,
|
||||
Ui::Text::Link(tr::lng_contacts_loading(tr::now), {}), // Link 2.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_pinned_media(tr::now, lt_from, fromLinkText(), lt_media, tr::lng_deleted_message(tr::now));
|
||||
result.text = tr::lng_action_pinned_media(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
lt_media,
|
||||
{ .text = tr::lng_deleted_message(tr::now) },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -736,7 +947,7 @@ HistoryService::PreparedText HistoryService::prepareGameScoreText() {
|
||||
auto result = PreparedText {};
|
||||
auto gamescore = Get<HistoryServiceGameScore>();
|
||||
|
||||
auto computeGameTitle = [&]() -> QString {
|
||||
auto computeGameTitle = [&]() -> TextWithEntities {
|
||||
if (gamescore && gamescore->msg) {
|
||||
if (const auto media = gamescore->msg->media()) {
|
||||
if (const auto game = media->game()) {
|
||||
@@ -749,51 +960,55 @@ HistoryService::PreparedText HistoryService::prepareGameScoreText() {
|
||||
column,
|
||||
gamescore->msg->fullId()));
|
||||
auto titleText = game->title;
|
||||
return textcmdLink(result.links.size(), titleText);
|
||||
return Ui::Text::Link(titleText, {});
|
||||
}
|
||||
}
|
||||
return tr::lng_deleted_message(tr::now);
|
||||
return tr::lng_deleted_message(tr::now, Ui::Text::WithEntities);
|
||||
} else if (gamescore && gamescore->msgId) {
|
||||
return tr::lng_contacts_loading(tr::now);
|
||||
return tr::lng_contacts_loading(tr::now, Ui::Text::WithEntities);
|
||||
}
|
||||
return QString();
|
||||
return {};
|
||||
};
|
||||
|
||||
const auto scoreNumber = gamescore ? gamescore->score : 0;
|
||||
if (_from->isSelf()) {
|
||||
auto gameTitle = computeGameTitle();
|
||||
if (gameTitle.isEmpty()) {
|
||||
if (gameTitle.text.isEmpty()) {
|
||||
result.text = tr::lng_action_game_you_scored_no_game(
|
||||
tr::now,
|
||||
lt_count,
|
||||
scoreNumber);
|
||||
scoreNumber,
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_game_you_scored(
|
||||
tr::now,
|
||||
lt_count,
|
||||
scoreNumber,
|
||||
lt_game,
|
||||
gameTitle);
|
||||
gameTitle,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
auto gameTitle = computeGameTitle();
|
||||
if (gameTitle.isEmpty()) {
|
||||
if (gameTitle.text.isEmpty()) {
|
||||
result.text = tr::lng_action_game_score_no_game(
|
||||
tr::now,
|
||||
lt_count,
|
||||
scoreNumber,
|
||||
lt_from,
|
||||
fromLinkText());
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_game_score(
|
||||
tr::now,
|
||||
lt_count,
|
||||
scoreNumber,
|
||||
lt_from,
|
||||
fromLinkText(),
|
||||
fromLinkText(), // Link 1.
|
||||
lt_game,
|
||||
gameTitle);
|
||||
gameTitle,
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -808,17 +1023,31 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() {
|
||||
if (payment->msg) {
|
||||
if (const auto media = payment->msg->media()) {
|
||||
if (const auto invoice = media->invoice()) {
|
||||
return textcmdLink(1, invoice->title);
|
||||
return Ui::Text::Link(invoice->title, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
return TextWithEntities();
|
||||
}();
|
||||
|
||||
if (invoiceTitle.isEmpty()) {
|
||||
result.text = tr::lng_action_payment_done(tr::now, lt_amount, payment->amount, lt_user, history()->peer->name);
|
||||
if (invoiceTitle.text.isEmpty()) {
|
||||
result.text = tr::lng_action_payment_done(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
{ .text = payment->amount },
|
||||
lt_user,
|
||||
{ .text = history()->peer->name },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.text = tr::lng_action_payment_done_for(tr::now, lt_amount, payment->amount, lt_user, history()->peer->name, lt_invoice, invoiceTitle);
|
||||
result.text = tr::lng_action_payment_done_for(
|
||||
tr::now,
|
||||
lt_amount,
|
||||
{ .text = payment->amount },
|
||||
lt_user,
|
||||
{ .text = history()->peer->name },
|
||||
lt_invoice,
|
||||
invoiceTitle,
|
||||
Ui::Text::WithEntities);
|
||||
if (payment->msg) {
|
||||
result.links.push_back(payment->lnk);
|
||||
}
|
||||
@@ -843,15 +1072,17 @@ HistoryService::PreparedText HistoryService::prepareCallScheduledText(
|
||||
result.text = tr::lng_action_group_call_scheduled_channel(
|
||||
tr::now,
|
||||
lt_date,
|
||||
date);
|
||||
{ .text = date },
|
||||
Ui::Text::WithEntities);
|
||||
} else {
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_action_group_call_scheduled_group(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(),
|
||||
fromLinkText(), // Link 1.
|
||||
lt_date,
|
||||
date);
|
||||
{ .text = date },
|
||||
Ui::Text::WithEntities);
|
||||
}
|
||||
};
|
||||
const auto time = scheduled.time().toString(cTimeFormat());
|
||||
@@ -952,21 +1183,21 @@ ItemPreview HistoryService::toPreview(ToPreviewOptions options) const {
|
||||
// Because larger version is shown exactly to the left of the preview.
|
||||
//auto media = _media ? _media->toPreview(options) : ItemPreview();
|
||||
return {
|
||||
.text = textcmdLink(
|
||||
1,
|
||||
TextUtilities::Clean(notificationText(), true)),
|
||||
.text = Ui::Text::Wrapped(notificationText(), EntityType::PlainLink),
|
||||
//.images = std::move(media.images),
|
||||
//.loadingContext = std::move(media.loadingContext),
|
||||
};
|
||||
}
|
||||
|
||||
QString HistoryService::inReplyText() const {
|
||||
const auto result = HistoryService::notificationText();
|
||||
TextWithEntities HistoryService::inReplyText() const {
|
||||
auto result = HistoryService::notificationText();
|
||||
const auto &name = author()->name;
|
||||
const auto text = result.trimmed().startsWith(name)
|
||||
? result.trimmed().mid(name.size()).trimmed()
|
||||
: result;
|
||||
return textcmdLink(1, text);
|
||||
TextUtilities::Trim(result);
|
||||
if (result.text.startsWith(name)) {
|
||||
result = Ui::Text::Mid(result, name.size());
|
||||
TextUtilities::Trim(result);
|
||||
}
|
||||
return Ui::Text::Wrapped(result, EntityType::PlainLink);
|
||||
}
|
||||
|
||||
std::unique_ptr<HistoryView::Element> HistoryService::createView(
|
||||
@@ -975,8 +1206,8 @@ std::unique_ptr<HistoryView::Element> HistoryService::createView(
|
||||
return delegate->elementCreate(this, replacing);
|
||||
}
|
||||
|
||||
QString HistoryService::fromLinkText() const {
|
||||
return textcmdLink(1, _from->name);
|
||||
TextWithEntities HistoryService::fromLinkText() const {
|
||||
return Ui::Text::Link(_from->name, {});
|
||||
}
|
||||
|
||||
ClickHandlerPtr HistoryService::fromLink() const {
|
||||
@@ -984,7 +1215,7 @@ ClickHandlerPtr HistoryService::fromLink() const {
|
||||
}
|
||||
|
||||
void HistoryService::setServiceText(const PreparedText &prepared) {
|
||||
_text.setText(
|
||||
_text.setMarkedText(
|
||||
st::serviceTextStyle,
|
||||
prepared.text,
|
||||
Ui::ItemTextServiceOptions());
|
||||
@@ -1022,7 +1253,7 @@ crl::time HistoryService::getSelfDestructIn(crl::time now) {
|
||||
}
|
||||
Unexpected("Type in HistoryServiceSelfDestruct::Type");
|
||||
};
|
||||
setServiceText({ text() });
|
||||
setServiceText({ TextWithEntities{ .text = text() } });
|
||||
return 0;
|
||||
}
|
||||
return selfdestruct->destructAt - now;
|
||||
@@ -1045,15 +1276,23 @@ void HistoryService::createFromMtp(const MTPDmessage &message) {
|
||||
|
||||
setSelfDestruct(HistoryServiceSelfDestruct::Type::Photo, ttl->v);
|
||||
if (out()) {
|
||||
setServiceText({ tr::lng_ttl_photo_sent(tr::now) });
|
||||
setServiceText({
|
||||
tr::lng_ttl_photo_sent(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
} else {
|
||||
auto result = PreparedText();
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_ttl_photo_received(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_ttl_photo_received(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
setServiceText(std::move(result));
|
||||
}
|
||||
} else {
|
||||
setServiceText({ tr::lng_ttl_photo_expired(tr::now) });
|
||||
setServiceText({
|
||||
tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
}
|
||||
} break;
|
||||
case mtpc_messageMediaDocument: {
|
||||
@@ -1064,15 +1303,23 @@ void HistoryService::createFromMtp(const MTPDmessage &message) {
|
||||
|
||||
setSelfDestruct(HistoryServiceSelfDestruct::Type::Video, ttl->v);
|
||||
if (out()) {
|
||||
setServiceText({ tr::lng_ttl_video_sent(tr::now) });
|
||||
setServiceText({
|
||||
tr::lng_ttl_video_sent(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
} else {
|
||||
auto result = PreparedText();
|
||||
result.links.push_back(fromLink());
|
||||
result.text = tr::lng_ttl_video_received(tr::now, lt_from, fromLinkText());
|
||||
result.text = tr::lng_ttl_video_received(
|
||||
tr::now,
|
||||
lt_from,
|
||||
fromLinkText(), // Link 1.
|
||||
Ui::Text::WithEntities);
|
||||
setServiceText(std::move(result));
|
||||
}
|
||||
} else {
|
||||
setServiceText({ tr::lng_ttl_video_expired(tr::now) });
|
||||
setServiceText({
|
||||
tr::lng_ttl_video_expired(tr::now, Ui::Text::WithEntities)
|
||||
});
|
||||
}
|
||||
} break;
|
||||
|
||||
@@ -1268,11 +1515,14 @@ HistoryService::PreparedText GenerateJoinedText(
|
||||
: tr::lng_action_add_you)(
|
||||
tr::now,
|
||||
lt_from,
|
||||
textcmdLink(1, inviter->name));
|
||||
Ui::Text::Link(inviter->name, {}),
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
} else if (history->peer->isMegagroup()) {
|
||||
if (viaRequest) {
|
||||
return { tr::lng_action_you_joined_by_request(tr::now) };
|
||||
return { tr::lng_action_you_joined_by_request(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities) };
|
||||
}
|
||||
auto self = history->session().user();
|
||||
auto result = HistoryService::PreparedText{};
|
||||
@@ -1280,12 +1530,15 @@ HistoryService::PreparedText GenerateJoinedText(
|
||||
result.text = tr::lng_action_user_joined(
|
||||
tr::now,
|
||||
lt_from,
|
||||
textcmdLink(1, self->name));
|
||||
Ui::Text::Link(self->name, {}),
|
||||
Ui::Text::WithEntities);
|
||||
return result;
|
||||
}
|
||||
return { viaRequest
|
||||
? tr::lng_action_you_joined_by_request_channel(tr::now)
|
||||
: tr::lng_action_you_joined(tr::now) };
|
||||
? tr::lng_action_you_joined_by_request_channel(
|
||||
tr::now,
|
||||
Ui::Text::WithEntities)
|
||||
: tr::lng_action_you_joined(tr::now, Ui::Text::WithEntities) };
|
||||
}
|
||||
|
||||
not_null<HistoryService*> GenerateJoinedMessage(
|
||||
|
||||
@@ -63,7 +63,7 @@ class ServiceMessagePainter;
|
||||
class HistoryService : public HistoryItem {
|
||||
public:
|
||||
struct PreparedText {
|
||||
QString text;
|
||||
TextWithEntities text;
|
||||
QList<ClickHandlerPtr> links;
|
||||
};
|
||||
|
||||
@@ -112,7 +112,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
ItemPreview toPreview(ToPreviewOptions options) const override;
|
||||
QString inReplyText() const override;
|
||||
TextWithEntities inReplyText() const override;
|
||||
|
||||
std::unique_ptr<HistoryView::Element> createView(
|
||||
not_null<HistoryView::ElementDelegate*> delegate,
|
||||
@@ -129,7 +129,7 @@ protected:
|
||||
|
||||
void markMediaAsReadHook() override;
|
||||
|
||||
QString fromLinkText() const;
|
||||
TextWithEntities fromLinkText() const;
|
||||
ClickHandlerPtr fromLink() const;
|
||||
|
||||
void removeMedia();
|
||||
|
||||
@@ -479,18 +479,6 @@ HistoryWidget::HistoryWidget(
|
||||
Window::ActivateWindow(controller);
|
||||
});
|
||||
|
||||
controller->adaptive().changes(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (_history) {
|
||||
_history->forceFullResize();
|
||||
if (_migrated) {
|
||||
_migrated->forceFullResize();
|
||||
}
|
||||
updateHistoryGeometry();
|
||||
update();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
session().data().newItemAdded(
|
||||
) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
|
||||
newItemAdded(item);
|
||||
@@ -503,7 +491,10 @@ HistoryWidget::HistoryWidget(
|
||||
|
||||
session().data().viewResizeRequest(
|
||||
) | rpl::start_with_next([=](not_null<HistoryView::Element*> view) {
|
||||
if (view->data()->mainView() == view) {
|
||||
const auto item = view->data();
|
||||
const auto history = item->history();
|
||||
if (item->mainView() == view
|
||||
&& (history == _history || history == _migrated)) {
|
||||
updateHistoryGeometry();
|
||||
}
|
||||
}, lifetime());
|
||||
@@ -2068,9 +2059,6 @@ void HistoryWidget::showHistory(
|
||||
_historyInited = false;
|
||||
_contactStatus = nullptr;
|
||||
|
||||
// Unload lottie animations.
|
||||
session().data().unloadHeavyViewParts(HistoryInner::ElementDelegate());
|
||||
|
||||
if (peerId) {
|
||||
_peer = session().data().peer(peerId);
|
||||
_canSendMessages = _peer->canWrite();
|
||||
@@ -2151,6 +2139,16 @@ void HistoryWidget::showHistory(
|
||||
object_ptr<HistoryInner>(this, _scroll, controller(), _history));
|
||||
_list->show();
|
||||
|
||||
controller()->adaptive().changes(
|
||||
) | rpl::start_with_next([=] {
|
||||
_history->forceFullResize();
|
||||
if (_migrated) {
|
||||
_migrated->forceFullResize();
|
||||
}
|
||||
updateHistoryGeometry();
|
||||
update();
|
||||
}, _list->lifetime());
|
||||
|
||||
if (_chooseForReport && _chooseForReport->active) {
|
||||
_list->setChooseReportReason(_chooseForReport->reason);
|
||||
}
|
||||
@@ -2242,6 +2240,21 @@ void HistoryWidget::setHistory(History *history) {
|
||||
if (_history == history) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto wasHistory = base::take(_history);
|
||||
const auto wasMigrated = base::take(_migrated);
|
||||
|
||||
// Unload lottie animations.
|
||||
const auto unloadHeavyViewParts = [](History *history) {
|
||||
if (history) {
|
||||
history->owner().unloadHeavyViewParts(
|
||||
history->delegateMixin()->delegate());
|
||||
history->forceFullResize();
|
||||
}
|
||||
};
|
||||
unloadHeavyViewParts(wasHistory);
|
||||
unloadHeavyViewParts(wasMigrated);
|
||||
|
||||
unregisterDraftSources();
|
||||
_history = history;
|
||||
_migrated = _history ? _history->migrateFrom() : nullptr;
|
||||
@@ -6693,7 +6706,7 @@ void HistoryWidget::updatePreview() {
|
||||
auto linkText = QStringView(_previewLinks).split(' ').at(0).toString();
|
||||
_previewDescription.setText(
|
||||
st::messageTextStyle,
|
||||
TextUtilities::Clean(linkText),
|
||||
linkText,
|
||||
Ui::DialogTextOptions());
|
||||
|
||||
const auto timeout = (_previewData->pendingTill - base::unixtime::now());
|
||||
@@ -6714,7 +6727,7 @@ void HistoryWidget::updatePreview() {
|
||||
Ui::NameTextOptions());
|
||||
_previewDescription.setText(
|
||||
st::messageTextStyle,
|
||||
TextUtilities::Clean(preview.description),
|
||||
preview.description,
|
||||
Ui::DialogTextOptions());
|
||||
}
|
||||
} else if (!readyToForward() && !replyToId() && !_editMsgId) {
|
||||
@@ -6939,7 +6952,7 @@ void HistoryWidget::messageDataReceived(
|
||||
}
|
||||
|
||||
void HistoryWidget::updateReplyEditText(not_null<HistoryItem*> item) {
|
||||
_replyEditMsgText.setText(
|
||||
_replyEditMsgText.setMarkedText(
|
||||
st::messageTextStyle,
|
||||
item->inReplyText(),
|
||||
Ui::DialogTextOptions());
|
||||
@@ -6987,7 +7000,8 @@ void HistoryWidget::updateForwarding() {
|
||||
|
||||
void HistoryWidget::updateForwardingTexts() {
|
||||
int32 version = 0;
|
||||
QString from, text;
|
||||
QString from;
|
||||
TextWithEntities text;
|
||||
const auto keepNames = (_toForward.options
|
||||
== Data::ForwardOptions::PreserveInfo);
|
||||
const auto keepCaptions = (_toForward.options
|
||||
@@ -7006,7 +7020,7 @@ void HistoryWidget::updateForwardingTexts() {
|
||||
fullname = from->name;
|
||||
}
|
||||
version += from->nameVersion;
|
||||
} else if (const auto info = item->hiddenForwardedInfo()) {
|
||||
} else if (const auto info = item->hiddenSenderInfo()) {
|
||||
if (!insertedNames.contains(info->name)) {
|
||||
insertedNames.emplace(info->name);
|
||||
names.push_back(info->firstName);
|
||||
@@ -7034,13 +7048,12 @@ void HistoryWidget::updateForwardingTexts() {
|
||||
.generateImages = false,
|
||||
}).text;
|
||||
} else {
|
||||
text = textcmdLink(
|
||||
1,
|
||||
text = Ui::Text::PlainLink(
|
||||
tr::lng_forward_messages(tr::now, lt_count, count));
|
||||
}
|
||||
}
|
||||
_toForwardFrom.setText(st::msgNameStyle, from, Ui::NameTextOptions());
|
||||
_toForwardText.setText(
|
||||
_toForwardText.setMarkedText(
|
||||
st::messageTextStyle,
|
||||
text,
|
||||
Ui::DialogTextOptions());
|
||||
@@ -7058,7 +7071,7 @@ void HistoryWidget::checkForwardingInfo() {
|
||||
for (const auto item : _toForward.items) {
|
||||
if (const auto from = item->senderOriginal()) {
|
||||
version += from->nameVersion;
|
||||
} else if (const auto info = item->hiddenForwardedInfo()) {
|
||||
} else if (const auto info = item->hiddenSenderInfo()) {
|
||||
++version;
|
||||
} else {
|
||||
Unexpected("Corrupt forwarded information in message.");
|
||||
|
||||
@@ -276,7 +276,7 @@ void FieldHeader::init() {
|
||||
) | rpl::start_with_next([=](const auto &d) {
|
||||
_preview.description.setText(
|
||||
st::messageTextStyle,
|
||||
TextUtilities::Clean(d),
|
||||
d,
|
||||
Ui::DialogTextOptions());
|
||||
}, lifetime());
|
||||
|
||||
@@ -325,7 +325,7 @@ void FieldHeader::init() {
|
||||
void FieldHeader::updateShownMessageText() {
|
||||
Expects(_shownMessage != nullptr);
|
||||
|
||||
_shownMessageText.setText(
|
||||
_shownMessageText.setMarkedText(
|
||||
st::messageTextStyle,
|
||||
_shownMessage->inReplyText(),
|
||||
Ui::DialogTextOptions());
|
||||
|
||||
@@ -17,7 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history.h"
|
||||
#include "history/view/history_view_message.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "history/view/history_view_react_animation.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "main/main_session.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
@@ -31,6 +37,8 @@ BottomInfo::BottomInfo(
|
||||
layout();
|
||||
}
|
||||
|
||||
BottomInfo::~BottomInfo() = default;
|
||||
|
||||
void BottomInfo::update(Data &&data, int availableWidth) {
|
||||
_data = std::move(data);
|
||||
layout();
|
||||
@@ -103,6 +111,10 @@ TextState BottomInfo::textState(
|
||||
not_null<const HistoryItem*> item,
|
||||
QPoint position) const {
|
||||
auto result = TextState(item);
|
||||
if (const auto link = revokeReactionLink(item, position)) {
|
||||
result.link = link;
|
||||
return result;
|
||||
}
|
||||
const auto inTime = QRect(
|
||||
width() - _dateWidth,
|
||||
0,
|
||||
@@ -115,6 +127,75 @@ TextState BottomInfo::textState(
|
||||
return result;
|
||||
}
|
||||
|
||||
ClickHandlerPtr BottomInfo::revokeReactionLink(
|
||||
not_null<const HistoryItem*> item,
|
||||
QPoint position) const {
|
||||
if (_reactions.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto left = 0;
|
||||
auto top = 0;
|
||||
auto available = width();
|
||||
if (height() != minHeight()) {
|
||||
available = std::min(available, _reactionsMaxWidth);
|
||||
left += width() - available;
|
||||
top += st::msgDateFont->height;
|
||||
}
|
||||
auto x = left;
|
||||
auto y = top;
|
||||
auto widthLeft = available;
|
||||
for (const auto &reaction : _reactions) {
|
||||
const auto chosen = (reaction.emoji == _data.chosenReaction);
|
||||
const auto add = (reaction.countTextWidth > 0)
|
||||
? st::reactionInfoDigitSkip
|
||||
: st::reactionInfoBetween;
|
||||
const auto width = st::reactionInfoSize
|
||||
+ (reaction.countTextWidth > 0
|
||||
? (st::reactionInfoSkip + reaction.countTextWidth)
|
||||
: 0);
|
||||
if (x > left && widthLeft < width) {
|
||||
x = left;
|
||||
y += st::msgDateFont->height;
|
||||
widthLeft = available;
|
||||
}
|
||||
const auto image = QRect(
|
||||
x,
|
||||
y,
|
||||
st::reactionInfoSize,
|
||||
st::msgDateFont->height);
|
||||
if (chosen && image.contains(position)) {
|
||||
if (!_revokeLink) {
|
||||
_revokeLink = revokeReactionLink(item);
|
||||
}
|
||||
return _revokeLink;
|
||||
}
|
||||
x += width + add;
|
||||
widthLeft -= width + add;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ClickHandlerPtr BottomInfo::revokeReactionLink(
|
||||
not_null<const HistoryItem*> item) const {
|
||||
const auto itemId = item->fullId();
|
||||
const auto sessionId = item->history()->session().uniqueId();
|
||||
return std::make_shared<LambdaClickHandler>([=](
|
||||
ClickContext context) {
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
if (const auto controller = my.sessionWindow.get()) {
|
||||
if (controller->session().uniqueId() == sessionId) {
|
||||
auto &owner = controller->session().data();
|
||||
if (const auto item = owner.message(itemId)) {
|
||||
const auto chosen = item->chosenReaction();
|
||||
if (!chosen.isEmpty()) {
|
||||
item->toggleReaction(chosen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool BottomInfo::isSignedAuthorElided() const {
|
||||
return _authorElided;
|
||||
}
|
||||
@@ -159,6 +240,17 @@ void BottomInfo::paint(
|
||||
authorEditedWidth,
|
||||
outerWidth);
|
||||
|
||||
if (_data.flags & Data::Flag::Pinned) {
|
||||
const auto &icon = inverted
|
||||
? st->historyPinInvertedIcon()
|
||||
: stm->historyPinIcon;
|
||||
right -= st::historyPinWidth;
|
||||
icon.paint(
|
||||
p,
|
||||
right,
|
||||
firstLineBottom + st::historyPinTop,
|
||||
outerWidth);
|
||||
}
|
||||
if (!_views.isEmpty()) {
|
||||
const auto viewsWidth = _views.maxWidth();
|
||||
right -= st::historyViewsSpace + viewsWidth;
|
||||
@@ -167,9 +259,10 @@ void BottomInfo::paint(
|
||||
const auto &icon = inverted
|
||||
? st->historyViewsInvertedIcon()
|
||||
: stm->historyViewsIcon;
|
||||
right -= st::historyViewsWidth;
|
||||
icon.paint(
|
||||
p,
|
||||
right - st::historyViewsWidth,
|
||||
right,
|
||||
firstLineBottom + st::historyViewsTop,
|
||||
outerWidth);
|
||||
}
|
||||
@@ -181,9 +274,10 @@ void BottomInfo::paint(
|
||||
const auto &icon = inverted
|
||||
? st->historyRepliesInvertedIcon()
|
||||
: stm->historyRepliesIcon;
|
||||
right -= st::historyViewsWidth;
|
||||
icon.paint(
|
||||
p,
|
||||
right - st::historyViewsWidth,
|
||||
right,
|
||||
firstLineBottom + st::historyViewsTop,
|
||||
outerWidth);
|
||||
}
|
||||
@@ -208,19 +302,30 @@ void BottomInfo::paint(
|
||||
left += width() - available;
|
||||
top += st::msgDateFont->height;
|
||||
}
|
||||
paintReactions(p, left, top, available);
|
||||
paintReactions(p, position, left, top, available, context);
|
||||
}
|
||||
}
|
||||
|
||||
void BottomInfo::paintReactions(
|
||||
Painter &p,
|
||||
QPoint origin,
|
||||
int left,
|
||||
int top,
|
||||
int availableWidth) const {
|
||||
int availableWidth,
|
||||
const PaintContext &context) const {
|
||||
auto x = left;
|
||||
auto y = top;
|
||||
auto widthLeft = availableWidth;
|
||||
const auto animated = _reactionAnimation
|
||||
? _reactionAnimation->playingAroundEmoji()
|
||||
: QString();
|
||||
if (_reactionAnimation
|
||||
&& context.reactionEffects
|
||||
&& animated.isEmpty()) {
|
||||
_reactionAnimation = nullptr;
|
||||
}
|
||||
for (const auto &reaction : _reactions) {
|
||||
const auto animating = (reaction.emoji == animated);
|
||||
const auto add = (reaction.countTextWidth > 0)
|
||||
? st::reactionInfoDigitSkip
|
||||
: st::reactionInfoBetween;
|
||||
@@ -238,11 +343,20 @@ void BottomInfo::paintReactions(
|
||||
reaction.emoji,
|
||||
::Data::Reactions::ImageSize::BottomInfo);
|
||||
}
|
||||
if (!reaction.image.isNull()) {
|
||||
p.drawImage(
|
||||
x,
|
||||
y + (st::msgDateFont->height - st::reactionInfoSize) / 2,
|
||||
reaction.image);
|
||||
const auto image = QRect(
|
||||
x + (st::reactionInfoSize - st::reactionInfoImage) / 2,
|
||||
y + (st::msgDateFont->height - st::reactionInfoImage) / 2,
|
||||
st::reactionInfoImage,
|
||||
st::reactionInfoImage);
|
||||
const auto skipImage = animating
|
||||
&& (reaction.count < 2 || !_reactionAnimation->flying());
|
||||
if (!reaction.image.isNull() && !skipImage) {
|
||||
p.drawImage(image.topLeft(), reaction.image);
|
||||
}
|
||||
if (animating) {
|
||||
context.reactionEffects->paint = [=](QPainter &p) {
|
||||
return _reactionAnimation->paintGetArea(p, origin, image);
|
||||
};
|
||||
}
|
||||
if (reaction.countTextWidth > 0) {
|
||||
p.drawText(
|
||||
@@ -279,7 +393,7 @@ void BottomInfo::layoutDateText() {
|
||||
? (tr::lng_edited(tr::now) + ' ')
|
||||
: QString();
|
||||
const auto author = _data.author;
|
||||
const auto prefix = author.isEmpty() ? qsl(", ") : QString();
|
||||
const auto prefix = !author.isEmpty() ? qsl(", ") : QString();
|
||||
const auto date = edited + _data.date.toString(cTimeFormat());
|
||||
_dateWidth = st::msgDateFont->width(date);
|
||||
const auto afterAuthor = prefix + date;
|
||||
@@ -326,6 +440,11 @@ void BottomInfo::layoutRepliesText() {
|
||||
}
|
||||
|
||||
void BottomInfo::layoutReactionsText() {
|
||||
if (_reactionAnimation
|
||||
&& !_data.reactions.contains(
|
||||
_reactionAnimation->playingAroundEmoji())) {
|
||||
_reactionAnimation = nullptr;
|
||||
}
|
||||
if (_data.reactions.empty()) {
|
||||
_reactions.clear();
|
||||
return;
|
||||
@@ -365,6 +484,9 @@ QSize BottomInfo::countOptimalSize() {
|
||||
+ _replies.maxWidth()
|
||||
+ st::historyViewsWidth;
|
||||
}
|
||||
if (_data.flags & Data::Flag::Pinned) {
|
||||
width += st::historyPinWidth;
|
||||
}
|
||||
_reactionsMaxWidth = countReactionsMaxWidth();
|
||||
width += _reactionsMaxWidth;
|
||||
return QSize(width, st::msgDateFont->height);
|
||||
@@ -390,6 +512,26 @@ void BottomInfo::setReactionCount(Reaction &reaction, int count) {
|
||||
: 0;
|
||||
}
|
||||
|
||||
void BottomInfo::animateReactionSend(
|
||||
SendReactionAnimationArgs &&args,
|
||||
Fn<void()> repaint) {
|
||||
_reactionAnimation = std::make_unique<Reactions::SendAnimation>(
|
||||
_reactionsOwner,
|
||||
args.translated(QPoint(width(), height())),
|
||||
std::move(repaint),
|
||||
st::reactionInfoImage);
|
||||
}
|
||||
|
||||
auto BottomInfo::takeSendReactionAnimation()
|
||||
-> std::unique_ptr<Reactions::SendAnimation> {
|
||||
return std::move(_reactionAnimation);
|
||||
}
|
||||
|
||||
void BottomInfo::continueSendReactionAnimation(
|
||||
std::unique_ptr<Reactions::SendAnimation> animation) {
|
||||
_reactionAnimation = std::move(animation);
|
||||
}
|
||||
|
||||
BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
|
||||
using Flag = BottomInfo::Data::Flag;
|
||||
const auto item = message->message();
|
||||
@@ -398,6 +540,7 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
|
||||
result.date = message->dateTime();
|
||||
if (message->embedReactionsInBottomInfo()) {
|
||||
result.reactions = item->reactions();
|
||||
result.chosenReaction = item->chosenReaction();
|
||||
}
|
||||
if (message->hasOutLayout()) {
|
||||
result.flags |= Flag::OutLayout;
|
||||
@@ -408,6 +551,9 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
|
||||
if (item->isSponsored()) {
|
||||
result.flags |= Flag::Sponsored;
|
||||
}
|
||||
if (item->isPinned() && message->context() != Context::Pinned) {
|
||||
result.flags |= Flag::Pinned;
|
||||
}
|
||||
if (const auto msgsigned = item->Get<HistoryMessageSigned>()) {
|
||||
if (!msgsigned->isAnonymousRank) {
|
||||
result.author = msgsigned->author;
|
||||
|
||||
@@ -20,11 +20,15 @@ class Reactions;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
namespace Reactions {
|
||||
class SendAnimation;
|
||||
} // namespace Reactions
|
||||
|
||||
using PaintContext = Ui::ChatPaintContext;
|
||||
|
||||
class Message;
|
||||
struct TextState;
|
||||
struct SendReactionAnimationArgs;
|
||||
|
||||
class BottomInfo final : public Object {
|
||||
public:
|
||||
@@ -35,6 +39,7 @@ public:
|
||||
Sending = 0x04,
|
||||
RepliesContext = 0x08,
|
||||
Sponsored = 0x10,
|
||||
Pinned = 0x20,
|
||||
//Unread, // We don't want to pass and update it in Date for now.
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
@@ -43,11 +48,13 @@ public:
|
||||
QDateTime date;
|
||||
QString author;
|
||||
base::flat_map<QString, int> reactions;
|
||||
QString chosenReaction;
|
||||
std::optional<int> views;
|
||||
std::optional<int> replies;
|
||||
Flags flags;
|
||||
};
|
||||
BottomInfo(not_null<::Data::Reactions*> reactionsOwner, Data &&data);
|
||||
~BottomInfo();
|
||||
|
||||
void update(Data &&data, int availableWidth);
|
||||
|
||||
@@ -66,6 +73,14 @@ public:
|
||||
bool inverted,
|
||||
const PaintContext &context) const;
|
||||
|
||||
void animateReactionSend(
|
||||
SendReactionAnimationArgs &&args,
|
||||
Fn<void()> repaint);
|
||||
[[nodiscard]] auto takeSendReactionAnimation()
|
||||
-> std::unique_ptr<Reactions::SendAnimation>;
|
||||
void continueSendReactionAnimation(
|
||||
std::unique_ptr<Reactions::SendAnimation> animation);
|
||||
|
||||
private:
|
||||
struct Reaction {
|
||||
mutable QImage image;
|
||||
@@ -85,15 +100,22 @@ private:
|
||||
[[nodiscard]] int countReactionsHeight(int newWidth) const;
|
||||
void paintReactions(
|
||||
Painter &p,
|
||||
QPoint origin,
|
||||
int left,
|
||||
int top,
|
||||
int availableWidth) const;
|
||||
int availableWidth,
|
||||
const PaintContext &context) const;
|
||||
|
||||
QSize countOptimalSize() override;
|
||||
QSize countCurrentSize(int newWidth) override;
|
||||
|
||||
void setReactionCount(Reaction &reaction, int count);
|
||||
[[nodiscard]] Reaction prepareReactionWithEmoji(const QString &emoji);
|
||||
[[nodiscard]] ClickHandlerPtr revokeReactionLink(
|
||||
not_null<const HistoryItem*> item,
|
||||
QPoint position) const;
|
||||
[[nodiscard]] ClickHandlerPtr revokeReactionLink(
|
||||
not_null<const HistoryItem*> item) const;
|
||||
|
||||
const not_null<::Data::Reactions*> _reactionsOwner;
|
||||
Data _data;
|
||||
@@ -101,6 +123,8 @@ private:
|
||||
Ui::Text::String _views;
|
||||
Ui::Text::String _replies;
|
||||
std::vector<Reaction> _reactions;
|
||||
mutable ClickHandlerPtr _revokeLink;
|
||||
mutable std::unique_ptr<Reactions::SendAnimation> _reactionAnimation;
|
||||
int _reactionsMaxWidth = 0;
|
||||
int _dateWidth = 0;
|
||||
bool _authorElided = false;
|
||||
|
||||
@@ -44,7 +44,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_file_click_handler.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_scheduled_messages.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_controller.h"
|
||||
@@ -921,12 +923,17 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
|
||||
const auto view = request.view;
|
||||
const auto item = request.item;
|
||||
const auto itemId = item ? item->fullId() : FullMsgId();
|
||||
const auto rawLink = link.get();
|
||||
const auto linkPhoto = dynamic_cast<PhotoClickHandler*>(rawLink);
|
||||
const auto linkDocument = dynamic_cast<DocumentClickHandler*>(rawLink);
|
||||
const auto photo = linkPhoto ? linkPhoto->photo().get() : nullptr;
|
||||
const auto document = linkDocument
|
||||
? linkDocument->document().get()
|
||||
const auto lnkPhotoId = PhotoId(link
|
||||
? link->property(kPhotoLinkMediaIdProperty).toULongLong()
|
||||
: 0);
|
||||
const auto lnkDocumentId = DocumentId(link
|
||||
? link->property(kDocumentLinkMediaIdProperty).toULongLong()
|
||||
: 0);
|
||||
const auto photo = lnkPhotoId
|
||||
? list->session().data().photo(lnkPhotoId).get()
|
||||
: nullptr;
|
||||
const auto document = lnkDocumentId
|
||||
? list->session().data().document(lnkDocumentId).get()
|
||||
: nullptr;
|
||||
const auto poll = item
|
||||
? (item->media() ? item->media()->poll() : nullptr)
|
||||
@@ -951,9 +958,9 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu(
|
||||
}
|
||||
|
||||
AddTopMessageActions(result, request, list);
|
||||
if (linkPhoto) {
|
||||
if (photo) {
|
||||
AddPhotoActions(result, photo, item, list);
|
||||
} else if (linkDocument) {
|
||||
} else if (document) {
|
||||
AddDocumentActions(result, document, item, list);
|
||||
} else if (poll) {
|
||||
AddPollActions(result, poll, item, list->elementContext());
|
||||
@@ -1101,6 +1108,67 @@ void AddWhoReactedAction(
|
||||
showAllChosen));
|
||||
}
|
||||
|
||||
void ShowWhoReactedMenu(
|
||||
not_null<base::unique_qptr<Ui::PopupMenu>*> menu,
|
||||
QPoint position,
|
||||
not_null<QWidget*> context,
|
||||
not_null<HistoryItem*> item,
|
||||
const QString &emoji,
|
||||
not_null<Window::SessionController*> controller,
|
||||
rpl::lifetime &lifetime) {
|
||||
const auto participantChosen = [=](uint64 id) {
|
||||
controller->showPeerInfo(PeerId(id));
|
||||
};
|
||||
const auto showAllChosen = [=, itemId = item->fullId()]{
|
||||
if (const auto item = controller->session().data().message(itemId)) {
|
||||
controller->window().show(ReactionsListBox(
|
||||
controller,
|
||||
item,
|
||||
emoji));
|
||||
}
|
||||
};
|
||||
const auto reactions = &controller->session().data().reactions();
|
||||
const auto &list = reactions->list(
|
||||
Data::Reactions::Type::Active);
|
||||
const auto active = ranges::contains(
|
||||
list,
|
||||
emoji,
|
||||
&Data::Reaction::emoji);
|
||||
const auto filler = lifetime.make_state<Ui::WhoReactedListMenu>(
|
||||
participantChosen,
|
||||
showAllChosen);
|
||||
Api::WhoReacted(
|
||||
item,
|
||||
emoji,
|
||||
context,
|
||||
st::defaultWhoRead
|
||||
) | rpl::filter([=](const Ui::WhoReadContent &content) {
|
||||
return !content.unknown;
|
||||
}) | rpl::start_with_next([=, &lifetime](Ui::WhoReadContent &&content) {
|
||||
const auto creating = !*menu;
|
||||
const auto refill = [=] {
|
||||
if (active) {
|
||||
(*menu)->addAction(tr::lng_context_set_as_quick(tr::now), [=] {
|
||||
reactions->setFavorite(emoji);
|
||||
}, &st::menuIconFave);
|
||||
(*menu)->addSeparator();
|
||||
}
|
||||
};
|
||||
if (creating) {
|
||||
*menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
context,
|
||||
st::whoReadMenu);
|
||||
(*menu)->lifetime().add(base::take(lifetime));
|
||||
refill();
|
||||
}
|
||||
filler->populate(menu->get(), content);
|
||||
|
||||
if (creating) {
|
||||
(*menu)->popup(position);
|
||||
}
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
void ShowReportItemsBox(not_null<PeerData*> peer, MessageIdsList ids) {
|
||||
const auto chosen = [=](Ui::ReportReason reason) {
|
||||
Ui::show(Box(Ui::ReportDetailsBox, [=](const QString &text) {
|
||||
|
||||
@@ -65,6 +65,14 @@ void AddWhoReactedAction(
|
||||
not_null<QWidget*> context,
|
||||
not_null<HistoryItem*> item,
|
||||
not_null<Window::SessionController*> controller);
|
||||
void ShowWhoReactedMenu(
|
||||
not_null<base::unique_qptr<Ui::PopupMenu>*> menu,
|
||||
QPoint position,
|
||||
not_null<QWidget*> context,
|
||||
not_null<HistoryItem*> item,
|
||||
const QString &emoji,
|
||||
not_null<Window::SessionController*> controller,
|
||||
rpl::lifetime &lifetime);
|
||||
|
||||
void ShowReportItemsBox(not_null<PeerData*> peer, MessageIdsList ids);
|
||||
void ShowReportPeerBox(
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/view/media/history_view_media_grouped.h"
|
||||
#include "history/view/media/history_view_sticker.h"
|
||||
#include "history/view/media/history_view_large_emoji.h"
|
||||
#include "history/view/history_view_react_animation.h"
|
||||
#include "history/view/history_view_react_button.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "history/history.h"
|
||||
@@ -45,7 +46,7 @@ namespace {
|
||||
// A new message from the same sender is attached to previous within 15 minutes.
|
||||
constexpr int kAttachMessageToPreviousSecondsDelta = 900;
|
||||
|
||||
bool IsAttachedToPreviousInSavedMessages(
|
||||
[[nodiscard]] bool IsAttachedToPreviousInSavedMessages(
|
||||
not_null<HistoryItem*> previous,
|
||||
HistoryMessageForwarded *prevForwarded,
|
||||
not_null<HistoryItem*> item,
|
||||
@@ -65,6 +66,22 @@ bool IsAttachedToPreviousInSavedMessages(
|
||||
return (*previousInfo == *itemInfo);
|
||||
}
|
||||
|
||||
[[nodiscard]] Window::SessionController *ContextOrSessionWindow(
|
||||
const ClickHandlerContext &context,
|
||||
not_null<Main::Session*> session) {
|
||||
if (const auto controller = context.sessionWindow.get()) {
|
||||
return controller;
|
||||
}
|
||||
const auto &windows = session->windows();
|
||||
if (windows.empty()) {
|
||||
session->domain().activate(&session->account());
|
||||
if (windows.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return windows.front();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Ui::PathShiftGradient> MakePathShiftGradient(
|
||||
@@ -325,6 +342,15 @@ void DateBadge::paint(
|
||||
ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide);
|
||||
}
|
||||
|
||||
SendReactionAnimationArgs SendReactionAnimationArgs::translated(
|
||||
QPoint point) const {
|
||||
return {
|
||||
.emoji = emoji,
|
||||
.flyIcon = flyIcon,
|
||||
.flyFrom = flyFrom.translated(point),
|
||||
};
|
||||
}
|
||||
|
||||
Element::Element(
|
||||
not_null<ElementDelegate*> delegate,
|
||||
not_null<HistoryItem*> data,
|
||||
@@ -376,6 +402,10 @@ void Element::setY(int y) {
|
||||
void Element::refreshDataIdHook() {
|
||||
}
|
||||
|
||||
void Element::repaint() const {
|
||||
history()->owner().requestViewRepaint(this);
|
||||
}
|
||||
|
||||
void Element::paintHighlight(
|
||||
Painter &p,
|
||||
const PaintContext &context,
|
||||
@@ -464,10 +494,6 @@ int Element::skipBlockHeight() const {
|
||||
return st::msgDateFont->height - st::msgDateDelta.y();
|
||||
}
|
||||
|
||||
QString Element::skipBlock() const {
|
||||
return textcmdSkipBlock(skipBlockWidth(), skipBlockHeight());
|
||||
}
|
||||
|
||||
int Element::infoWidth() const {
|
||||
return 0;
|
||||
}
|
||||
@@ -599,7 +625,26 @@ ClickHandlerPtr Element::fromLink() const {
|
||||
return _fromLink;
|
||||
}
|
||||
const auto item = data();
|
||||
if (const auto from = item->displayFrom()) {
|
||||
if (item->isSponsored()) {
|
||||
const auto session = &item->history()->session();
|
||||
_fromLink = std::make_shared<LambdaClickHandler>([=](
|
||||
ClickContext context) {
|
||||
if (context.button != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
if (const auto window = ContextOrSessionWindow(my, session)) {
|
||||
auto &sponsored = session->data().sponsoredMessages();
|
||||
const auto details = sponsored.lookupDetails(my.itemId);
|
||||
if (const auto &hash = details.hash) {
|
||||
Api::CheckChatInvite(window, *hash);
|
||||
} else if (const auto peer = details.peer) {
|
||||
window->showPeerInfo(peer);
|
||||
}
|
||||
}
|
||||
});
|
||||
return _fromLink;
|
||||
} else if (const auto from = item->displayFrom()) {
|
||||
_fromLink = std::make_shared<LambdaClickHandler>([=](
|
||||
ClickContext context) {
|
||||
if (context.button != Qt::LeftButton) {
|
||||
@@ -607,29 +652,8 @@ ClickHandlerPtr Element::fromLink() const {
|
||||
}
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
const auto session = &from->session();
|
||||
const auto window = [&]() -> Window::SessionController* {
|
||||
if (const auto controller = my.sessionWindow.get()) {
|
||||
return controller;
|
||||
}
|
||||
const auto &windows = session->windows();
|
||||
if (windows.empty()) {
|
||||
session->domain().activate(&session->account());
|
||||
if (windows.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return windows.front();
|
||||
}();
|
||||
if (window) {
|
||||
const auto inviteHash = item->isSponsored()
|
||||
? session->data().sponsoredMessages().channelPost(
|
||||
my.itemId).hash
|
||||
: std::nullopt;
|
||||
if (inviteHash) {
|
||||
Api::CheckChatInvite(window, *inviteHash);
|
||||
} else {
|
||||
window->showPeerInfo(from);
|
||||
}
|
||||
if (const auto window = ContextOrSessionWindow(my, session)) {
|
||||
window->showPeerInfo(from);
|
||||
}
|
||||
});
|
||||
_fromLink->setProperty(kPeerLinkPeerIdProperty, from->id.value);
|
||||
@@ -1010,7 +1034,7 @@ void Element::clickHandlerActiveChanged(
|
||||
}
|
||||
}
|
||||
App::hoveredLinkItem(active ? this : nullptr);
|
||||
history()->owner().requestViewRepaint(this);
|
||||
repaint();
|
||||
if (const auto media = this->media()) {
|
||||
media->clickHandlerActiveChanged(handler, active);
|
||||
}
|
||||
@@ -1025,12 +1049,20 @@ void Element::clickHandlerPressedChanged(
|
||||
}
|
||||
}
|
||||
App::pressedLinkItem(pressed ? this : nullptr);
|
||||
history()->owner().requestViewRepaint(this);
|
||||
repaint();
|
||||
if (const auto media = this->media()) {
|
||||
media->clickHandlerPressedChanged(handler, pressed);
|
||||
}
|
||||
}
|
||||
|
||||
void Element::animateSendReaction(SendReactionAnimationArgs &&args) {
|
||||
}
|
||||
|
||||
auto Element::takeSendReactionAnimation()
|
||||
-> std::unique_ptr<Reactions::SendAnimation> {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Element::~Element() {
|
||||
// Delete media while owner still exists.
|
||||
base::take(_media);
|
||||
|
||||
@@ -33,6 +33,10 @@ struct ChatPaintContext;
|
||||
class ChatStyle;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Lottie {
|
||||
class Icon;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
enum class PointState : char;
|
||||
@@ -45,6 +49,7 @@ using PaintContext = Ui::ChatPaintContext;
|
||||
|
||||
namespace Reactions {
|
||||
struct ButtonParameters;
|
||||
class SendAnimation;
|
||||
} // namespace Reactions
|
||||
|
||||
enum class Context : char {
|
||||
@@ -224,6 +229,14 @@ struct DateBadge : public RuntimeComponent<DateBadge, Element> {
|
||||
|
||||
};
|
||||
|
||||
struct SendReactionAnimationArgs {
|
||||
QString emoji;
|
||||
std::shared_ptr<Lottie::Icon> flyIcon;
|
||||
QRect flyFrom;
|
||||
|
||||
[[nodiscard]] SendReactionAnimationArgs translated(QPoint point) const;
|
||||
};
|
||||
|
||||
class Element
|
||||
: public Object
|
||||
, public RuntimeComposer<Element>
|
||||
@@ -269,7 +282,6 @@ public:
|
||||
|
||||
int skipBlockWidth() const;
|
||||
int skipBlockHeight() const;
|
||||
QString skipBlock() const;
|
||||
virtual int infoWidth() const;
|
||||
virtual int bottomInfoFirstLineWidth() const;
|
||||
virtual bool bottomInfoIsWide() const;
|
||||
@@ -408,9 +420,15 @@ public:
|
||||
|
||||
[[nodiscard]] bool markSponsoredViewed(int shownFromTop) const;
|
||||
|
||||
virtual void animateSendReaction(SendReactionAnimationArgs &&args);
|
||||
[[nodiscard]] virtual auto takeSendReactionAnimation()
|
||||
-> std::unique_ptr<Reactions::SendAnimation>;
|
||||
|
||||
virtual ~Element();
|
||||
|
||||
protected:
|
||||
void repaint() const;
|
||||
|
||||
void paintHighlight(
|
||||
Painter &p,
|
||||
const PaintContext &context,
|
||||
|
||||
@@ -19,7 +19,7 @@ struct ItemPreviewImage {
|
||||
};
|
||||
|
||||
struct ItemPreview {
|
||||
QString text;
|
||||
TextWithEntities text;
|
||||
std::vector<ItemPreviewImage> images;
|
||||
int imagesInTextPosition = 0;
|
||||
std::any loadingContext;
|
||||
|
||||
@@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwidget.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_who_reacted.h"
|
||||
#include "layout/layout_selection.h"
|
||||
#include "window/window_adaptive.h"
|
||||
#include "window/window_session_controller.h"
|
||||
@@ -49,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_file_click_handler.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
@@ -267,7 +269,9 @@ ListWidget::ListWidget(
|
||||
, _reactionsManager(
|
||||
std::make_unique<Reactions::Manager>(
|
||||
this,
|
||||
[=](QRect updated) { update(updated); }))
|
||||
Data::UniqueReactionsLimitValue(&controller->session()),
|
||||
[=](QRect updated) { update(updated); },
|
||||
controller->cachedReactionIconFactory().createMethod()))
|
||||
, _scrollDateCheck([this] { scrollDateCheck(); })
|
||||
, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
|
||||
, _selectEnabled(_delegate->listAllowsMultiSelect())
|
||||
@@ -339,15 +343,28 @@ ListWidget::ListWidget(
|
||||
using ChosenReaction = Reactions::Manager::Chosen;
|
||||
_reactionsManager->chosen(
|
||||
) | rpl::start_with_next([=](ChosenReaction reaction) {
|
||||
if (const auto item = session().data().message(reaction.context)) {
|
||||
item->toggleReaction(reaction.emoji);
|
||||
const auto item = session().data().message(reaction.context);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item->toggleReaction(reaction.emoji);
|
||||
if (item->chosenReaction() != reaction.emoji) {
|
||||
return;
|
||||
} else if (const auto view = viewForItem(item)) {
|
||||
if (const auto top = itemTop(view); top >= 0) {
|
||||
view->animateSendReaction({
|
||||
.emoji = reaction.emoji,
|
||||
.flyIcon = reaction.icon,
|
||||
.flyFrom = reaction.geometry.translated(0, -top),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_delegate->listAllowedReactionsValue(
|
||||
) | rpl::start_with_next([=](std::vector<Data::Reaction> &&list) {
|
||||
_reactionsManager->applyList(std::move(list));
|
||||
}, lifetime());
|
||||
Reactions::SetupManagerList(
|
||||
_reactionsManager.get(),
|
||||
&session(),
|
||||
_delegate->listAllowedReactionsValue());
|
||||
|
||||
controller->adaptive().chatWideValue(
|
||||
) | rpl::start_with_next([=](bool wide) {
|
||||
@@ -1715,6 +1732,8 @@ void ListWidget::paintEvent(QPaintEvent *e) {
|
||||
});
|
||||
|
||||
if (from != end(_items)) {
|
||||
_reactionsManager->startEffectsCollection();
|
||||
|
||||
auto top = itemTop(from->get());
|
||||
auto context = controller()->preparePaintContext({
|
||||
.theme = _delegate->listChatTheme(),
|
||||
@@ -1726,9 +1745,14 @@ void ListWidget::paintEvent(QPaintEvent *e) {
|
||||
p.translate(0, top);
|
||||
for (auto i = from; i != to; ++i) {
|
||||
const auto view = *i;
|
||||
context.reactionEffects
|
||||
= _reactionsManager->currentReactionEffect();
|
||||
context.outbg = view->hasOutLayout();
|
||||
context.selection = itemRenderSelection(view);
|
||||
view->draw(p, context);
|
||||
_reactionsManager->recordCurrentReactionEffect(
|
||||
view->data()->fullId(),
|
||||
QPoint(0, top));
|
||||
const auto height = view->height();
|
||||
top += height;
|
||||
context.translate(0, -height);
|
||||
@@ -1753,7 +1777,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
|
||||
userpicTop,
|
||||
view->width(),
|
||||
st::msgPhotoSize);
|
||||
} else if (const auto info = view->data()->hiddenForwardedInfo()) {
|
||||
} else if (const auto info = view->data()->hiddenSenderInfo()) {
|
||||
info->userpic.paint(
|
||||
p,
|
||||
st::historyPhotoLeft,
|
||||
@@ -1813,7 +1837,7 @@ void ListWidget::paintEvent(QPaintEvent *e) {
|
||||
return true;
|
||||
});
|
||||
|
||||
_reactionsManager->paintButtons(p, context);
|
||||
_reactionsManager->paint(p, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2069,15 +2093,41 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
mouseActionUpdate(e->globalPos());
|
||||
}
|
||||
|
||||
auto request = ContextMenuRequest(_controller);
|
||||
|
||||
request.link = ClickHandler::getActive();
|
||||
request.view = _overElement;
|
||||
request.item = _overItemExact
|
||||
const auto link = ClickHandler::getActive();
|
||||
if (link
|
||||
&& !link->property(kSendReactionEmojiProperty).toString().isEmpty()
|
||||
&& _reactionsManager->showContextMenu(this, e)) {
|
||||
return;
|
||||
}
|
||||
const auto overItem = _overItemExact
|
||||
? _overItemExact
|
||||
: _overElement
|
||||
? _overElement->data().get()
|
||||
: nullptr;
|
||||
const auto hasWhoReactedItem = overItem
|
||||
&& Api::WhoReactedExists(overItem);
|
||||
const auto clickedEmoji = link
|
||||
? link->property(kReactionsCountEmojiProperty).toString()
|
||||
: QString();
|
||||
_whoReactedMenuLifetime.destroy();
|
||||
if (hasWhoReactedItem && !clickedEmoji.isEmpty()) {
|
||||
HistoryView::ShowWhoReactedMenu(
|
||||
&_menu,
|
||||
e->globalPos(),
|
||||
this,
|
||||
overItem,
|
||||
clickedEmoji,
|
||||
_controller,
|
||||
_whoReactedMenuLifetime);
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
auto request = ContextMenuRequest(_controller);
|
||||
|
||||
request.link = link;
|
||||
request.view = _overElement;
|
||||
request.item = overItem;
|
||||
request.pointState = _overState.pointState;
|
||||
request.selectedText = _selectedText;
|
||||
request.selectedItems = collectSelectedItems();
|
||||
@@ -2135,7 +2185,7 @@ void ListWidget::enterEventHook(QEnterEvent *e) {
|
||||
}
|
||||
|
||||
void ListWidget::leaveEventHook(QEvent *e) {
|
||||
_reactionsManager->updateButton({});
|
||||
_reactionsManager->updateButton({ .cursorLeft = true });
|
||||
if (const auto view = _overElement) {
|
||||
if (_overState.pointState != PointState::Outside) {
|
||||
repaintItem(view);
|
||||
@@ -2563,7 +2613,8 @@ void ListWidget::mouseActionUpdate() {
|
||||
view ? view->height() : 0,
|
||||
itemPoint,
|
||||
view ? view->pointState(itemPoint) : PointState::Outside);
|
||||
if (_overElement != view) {
|
||||
const auto viewChanged = (_overElement != view);
|
||||
if (viewChanged) {
|
||||
repaintItem(_overElement);
|
||||
_overElement = view;
|
||||
repaintItem(_overElement);
|
||||
@@ -2574,6 +2625,9 @@ void ListWidget::mouseActionUpdate() {
|
||||
itemPoint,
|
||||
reactionState)
|
||||
: Reactions::ButtonParameters());
|
||||
if (viewChanged && view) {
|
||||
_reactionsManager->updateUniqueLimit(item);
|
||||
}
|
||||
|
||||
TextState dragState;
|
||||
ClickHandlerHost *lnkhost = nullptr;
|
||||
@@ -2875,6 +2929,10 @@ void ListWidget::repaintItem(const Element *view) {
|
||||
const auto top = itemTop(view);
|
||||
const auto range = view->verticalRepaintRange();
|
||||
update(0, top + range.top, width(), range.height);
|
||||
const auto id = view->data()->fullId();
|
||||
if (const auto area = _reactionsManager->lookupEffectArea(id)) {
|
||||
update(*area);
|
||||
}
|
||||
}
|
||||
|
||||
void ListWidget::repaintItem(FullMsgId itemId) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user