Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f3fe1961 | ||
|
|
cfd733c54c | ||
|
|
3fa5e004fe | ||
|
|
862e4e45ad | ||
|
|
53df4d1b10 | ||
|
|
5cfd402b70 | ||
|
|
57e9651a8a | ||
|
|
53d206c12c | ||
|
|
46f3cf3395 | ||
|
|
dfc0491524 | ||
|
|
54f757e770 | ||
|
|
abfd3ad1b9 | ||
|
|
bed208d621 | ||
|
|
33c453a13c | ||
|
|
e118972d5c | ||
|
|
fb8a9a930c | ||
|
|
7a9cfcc40d | ||
|
|
e1dc15321a | ||
|
|
2b5e575b67 | ||
|
|
42e216603c | ||
|
|
d46e145c61 | ||
|
|
5dcb232b77 | ||
|
|
b34d5b8306 | ||
|
|
76d81ff197 | ||
|
|
71637d2a0e | ||
|
|
423daecbde | ||
|
|
3cb76fb80b | ||
|
|
6882093ed1 | ||
|
|
699761b42f | ||
|
|
f50c50a152 | ||
|
|
13d22947df | ||
|
|
6c08bab550 | ||
|
|
3e2f4bed50 | ||
|
|
41d39012d2 | ||
|
|
a7764f84f0 | ||
|
|
85fcec2fb5 | ||
|
|
82e835fbc2 | ||
|
|
80684d9073 | ||
|
|
b04f0e0d3d | ||
|
|
65cc9bcd87 | ||
|
|
bc06a3aea3 | ||
|
|
de78f4255e | ||
|
|
d67dafaccb | ||
|
|
4f8ea4c807 | ||
|
|
15b19f8565 | ||
|
|
b16696db93 | ||
|
|
63129072ba | ||
|
|
1fdd591aa0 | ||
|
|
f370ca97d0 | ||
|
|
f5aba5a907 | ||
|
|
1d613995db | ||
|
|
5bb1c77199 | ||
|
|
5b39c7013a | ||
|
|
ed91c07f99 | ||
|
|
a66b2a4056 | ||
|
|
a1a7399023 | ||
|
|
e71b7dd384 | ||
|
|
664b43acd7 | ||
|
|
eac867ce85 | ||
|
|
2ad48f18f2 | ||
|
|
e823fe5891 |
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -25,6 +25,8 @@ Tell us what happens instead
|
||||
|
||||
**Version of Telegram Desktop:**
|
||||
|
||||
**Installation source (Linux Only)** - the official website / GitHub releases / flatpak / snap / distribution package:
|
||||
|
||||
**Used theme**:
|
||||
|
||||
<details><summary><b>Logs</b>:</summary>
|
||||
|
||||
16
.github/workflows/issue_closer.yml
vendored
@@ -21,14 +21,24 @@ jobs:
|
||||
script: |
|
||||
let errorStr = "Version not found.";
|
||||
|
||||
function maxIndexOf(str, i) {
|
||||
let index = str.indexOf(i);
|
||||
return (index == -1) ? Number.MAX_SAFE_INTEGER : index;
|
||||
}
|
||||
|
||||
let item1 = "Version of Telegram Desktop";
|
||||
let item2 = "Used theme";
|
||||
let item2 = "Installation source";
|
||||
let item3 = "Used theme";
|
||||
let item4 = "<details>";
|
||||
let body = context.payload.issue.body;
|
||||
|
||||
console.log("Body of issue:\n" + body);
|
||||
let index1 = body.indexOf(item1);
|
||||
let index2 = body.indexOf(item2);
|
||||
index2 = (index2 == -1) ? Number.MAX_SAFE_INTEGER : index2;
|
||||
let index2 = Math.min(
|
||||
Math.min(
|
||||
maxIndexOf(body, item2),
|
||||
maxIndexOf(body, item3)),
|
||||
maxIndexOf(body, item4));
|
||||
|
||||
console.log("Index 1: " + index1);
|
||||
console.log("Index 2: " + index2);
|
||||
|
||||
77
.github/workflows/linux.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
|
||||
env:
|
||||
GIT: "https://github.com"
|
||||
QT: "5_12_5"
|
||||
QT_PREFIX: "/usr/local/desktop-app/Qt-5.12.5"
|
||||
QT: "5_12_8"
|
||||
QT_PREFIX: "/usr/local/desktop-app/Qt-5.12.8"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
OPENSSL_PREFIX: "/usr/local/desktop-app/openssl-1.1.1"
|
||||
CMAKE_VER: "3.17.0"
|
||||
@@ -189,23 +189,28 @@ jobs:
|
||||
|
||||
git clone --branch release/3.4 $GIT/FFmpeg/FFmpeg ffmpeg
|
||||
cd ffmpeg
|
||||
./configure --prefix=$LibrariesPath/ffmpeg-cache \
|
||||
--enable-protocol=file --enable-libopus \
|
||||
./configure \
|
||||
--disable-debug \
|
||||
--disable-programs \
|
||||
--disable-doc \
|
||||
--disable-network \
|
||||
--disable-autodetect \
|
||||
--disable-everything \
|
||||
--disable-neon \
|
||||
--disable-iconv \
|
||||
--enable-libopus \
|
||||
--enable-vaapi \
|
||||
--enable-vdpau \
|
||||
--enable-protocol=file \
|
||||
--enable-hwaccel=h264_vaapi \
|
||||
--enable-hwaccel=h264_vdpau \
|
||||
--enable-hwaccel=mpeg4_vaapi \
|
||||
--enable-hwaccel=mpeg4_vdpau \
|
||||
--enable-decoder=aac \
|
||||
--enable-decoder=aac_at \
|
||||
--enable-decoder=aac_fixed \
|
||||
--enable-decoder=aac_latm \
|
||||
--enable-decoder=aasc \
|
||||
--enable-decoder=alac \
|
||||
--enable-decoder=alac_at \
|
||||
--enable-decoder=flac \
|
||||
--enable-decoder=gif \
|
||||
--enable-decoder=h264 \
|
||||
@@ -227,14 +232,12 @@ jobs:
|
||||
--enable-decoder=msmpeg4v3 \
|
||||
--enable-decoder=opus \
|
||||
--enable-decoder=pcm_alaw \
|
||||
--enable-decoder=pcm_alaw_at \
|
||||
--enable-decoder=pcm_f32be \
|
||||
--enable-decoder=pcm_f32le \
|
||||
--enable-decoder=pcm_f64be \
|
||||
--enable-decoder=pcm_f64le \
|
||||
--enable-decoder=pcm_lxf \
|
||||
--enable-decoder=pcm_mulaw \
|
||||
--enable-decoder=pcm_mulaw_at \
|
||||
--enable-decoder=pcm_s16be \
|
||||
--enable-decoder=pcm_s16be_planar \
|
||||
--enable-decoder=pcm_s16le \
|
||||
@@ -289,7 +292,7 @@ jobs:
|
||||
--enable-muxer=opus
|
||||
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
sudo make DESTDIR="$LibrariesPath/ffmpeg-cache" install
|
||||
cd ..
|
||||
rm -rf ffmpeg
|
||||
- name: FFmpeg install.
|
||||
@@ -298,7 +301,7 @@ jobs:
|
||||
#List of files from cmake/external/ffmpeg/CMakeLists.txt.
|
||||
copyLib() {
|
||||
mkdir -p ffmpeg/$1
|
||||
yes | cp -i ffmpeg-cache/lib/$1.a ffmpeg/$1/$1.a
|
||||
yes | cp -i ffmpeg-cache/usr/local/lib/$1.a ffmpeg/$1/$1.a
|
||||
}
|
||||
copyLib libavformat
|
||||
copyLib libavcodec
|
||||
@@ -306,7 +309,7 @@ jobs:
|
||||
copyLib libswscale
|
||||
copyLib libavutil
|
||||
|
||||
sudo cp -R ffmpeg-cache/. /usr/local/
|
||||
sudo cp -R ffmpeg-cache/. /
|
||||
|
||||
- name: PortAudio.
|
||||
run: |
|
||||
@@ -327,7 +330,13 @@ jobs:
|
||||
|
||||
git clone -b openal-soft-1.20.1 --depth=1 $GIT/kcat/openal-soft.git
|
||||
cd openal-soft/build
|
||||
cmake -D LIBTYPE:STRING=STATIC ..
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DLIBTYPE:STRING=STATIC \
|
||||
-DALSOFT_EXAMPLES=OFF \
|
||||
-DALSOFT_TESTS=OFF \
|
||||
-DALSOFT_UTILS=OFF \
|
||||
-DALSOFT_CONFIG=OFF
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
cd -
|
||||
@@ -348,16 +357,15 @@ jobs:
|
||||
git clone -b OpenSSL_${OPENSSL_VER}-stable --depth=1 \
|
||||
$GIT/openssl/openssl $opensslDir
|
||||
cd $opensslDir
|
||||
./config --prefix=$LibrariesPath/openssl-cache
|
||||
./config --prefix="$OPENSSL_PREFIX"
|
||||
make -j$(nproc)
|
||||
sudo make install_sw
|
||||
sudo make DESTDIR="$LibrariesPath/openssl-cache" install_sw
|
||||
cd ..
|
||||
rm -rf $opensslDir
|
||||
- name: OpenSSL install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
sudo mkdir -p $OPENSSL_PREFIX
|
||||
sudo cp -R openssl-cache/. $OPENSSL_PREFIX/
|
||||
sudo cp -R openssl-cache/. /
|
||||
|
||||
- name: Libxkbcommon.
|
||||
run: |
|
||||
@@ -377,24 +385,24 @@ jobs:
|
||||
|
||||
git clone -b 1.16 https://gitlab.freedesktop.org/wayland/wayland
|
||||
cd wayland
|
||||
./autogen.sh --enable-static --disable-documentation
|
||||
./autogen.sh --enable-static --disable-documentation --disable-dtd-validation
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
cd ..
|
||||
rm -rf wayland
|
||||
|
||||
- name: Qt 5.12.5 cache.
|
||||
- name: Qt 5.12.8 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/qt-cache
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_5.diff') }}
|
||||
- name: Qt 5.12.5 build.
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_8.diff') }}
|
||||
- name: Qt 5.12.8 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b v5.12.5 --depth=1 git://code.qt.io/qt/qt5.git qt_${QT}
|
||||
git clone -b v5.12.8 --depth=1 git://code.qt.io/qt/qt5.git qt_${QT}
|
||||
cd qt_${QT}
|
||||
perl init-repository --module-subset=qtbase,qtwayland,qtimageformats,qtsvg
|
||||
git submodule update qtbase qtwayland qtimageformats qtsvg
|
||||
@@ -406,9 +414,8 @@ jobs:
|
||||
git clone $GIT/desktop-app/nimf.git
|
||||
cd ../../../..
|
||||
|
||||
./configure -prefix "$LibrariesPath/qt-cache" \
|
||||
./configure -prefix "$QT_PREFIX" \
|
||||
-release \
|
||||
-force-debug-info \
|
||||
-opensource \
|
||||
-confirm-license \
|
||||
-qt-zlib \
|
||||
@@ -417,8 +424,6 @@ jobs:
|
||||
-qt-harfbuzz \
|
||||
-qt-pcre \
|
||||
-qt-xcb \
|
||||
-system-freetype \
|
||||
-fontconfig \
|
||||
-no-gtk \
|
||||
-static \
|
||||
-dbus-runtime \
|
||||
@@ -428,14 +433,13 @@ jobs:
|
||||
-nomake tests
|
||||
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
sudo make INSTALL_ROOT="$LibrariesPath/qt-cache" install
|
||||
cd ..
|
||||
rm -rf qt_${QT}
|
||||
- name: Qt 5.12.5 install.
|
||||
- name: Qt 5.12.8 install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
sudo mkdir -p $QT_PREFIX
|
||||
sudo cp -R qt-cache/. $QT_PREFIX/
|
||||
sudo cp -R qt-cache/. /
|
||||
|
||||
- name: Breakpad cache.
|
||||
id: cache-breakpad
|
||||
@@ -467,9 +471,9 @@ jobs:
|
||||
cd ..
|
||||
|
||||
cd breakpad
|
||||
./configure --prefix=$BreakpadCache
|
||||
./configure
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
sudo make DESTDIR="$BreakpadCache" install
|
||||
cd src
|
||||
rm -r testing
|
||||
git clone $GIT/google/googletest testing
|
||||
@@ -486,7 +490,7 @@ jobs:
|
||||
- name: Breakpad install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
sudo cp -R breakpad-cache/. /usr/local/
|
||||
sudo cp -R breakpad-cache/. /
|
||||
mkdir -p breakpad/out/Default/
|
||||
cp breakpad-cache/dump_syms breakpad/out/Default/dump_syms
|
||||
|
||||
@@ -499,6 +503,9 @@ jobs:
|
||||
if [ -n "${{ matrix.defines }}" ]; then
|
||||
DEFINE="-D ${{ matrix.defines }}=ON"
|
||||
echo Define from matrix: $DEFINE
|
||||
echo ::set-env name=ARTIFACT_NAME::Telegram_${{ matrix.defines }}
|
||||
else
|
||||
echo ::set-env name=ARTIFACT_NAME::Telegram
|
||||
fi
|
||||
|
||||
./configure.sh \
|
||||
@@ -534,5 +541,5 @@ jobs:
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
name: Upload artifact.
|
||||
with:
|
||||
name: Telegram
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: ${{ env.REPO_NAME }}/out/Debug/bin/artifact/
|
||||
|
||||
27
.github/workflows/mac.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -27,9 +27,9 @@ jobs:
|
||||
PREFIX: "/usr/local/macos"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.12"
|
||||
XZ: "xz-5.2.4"
|
||||
QT: "5_12_5"
|
||||
QT: "5_12_8"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
QT_PREFIX: "/usr/local/desktop-app/Qt-5.12.5"
|
||||
QT_PREFIX: "/usr/local/desktop-app/Qt-5.12.8"
|
||||
LIBICONV_VER: "libiconv-1.16"
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
@@ -376,20 +376,20 @@ jobs:
|
||||
build/gyp_crashpad.py -Dmac_deployment_target=10.10
|
||||
ninja -C out/Debug
|
||||
|
||||
- name: Qt 5.12.5 cache.
|
||||
- name: Qt 5.12.8 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/qt-cache
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_5.diff') }}
|
||||
- name: Use cached Qt 5.12.5.
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_8.diff') }}
|
||||
- name: Use cached Qt 5.12.8.
|
||||
if: steps.cache-qt.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
mv qt-cache Qt-5.12.5
|
||||
mv qt-cache Qt-5.12.8
|
||||
sudo mkdir -p $QT_PREFIX
|
||||
sudo mv -f Qt-5.12.5 "$(dirname "$QT_PREFIX")"/
|
||||
- name: Qt 5.12.5 build.
|
||||
sudo mv -f Qt-5.12.8 "$(dirname "$QT_PREFIX")"/
|
||||
- name: Qt 5.12.8 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
@@ -397,7 +397,7 @@ jobs:
|
||||
git clone git://code.qt.io/qt/qt5.git qt$QT
|
||||
cd qt$QT
|
||||
perl init-repository --module-subset=qtbase,qtimageformats
|
||||
git checkout v5.12.5
|
||||
git checkout v5.12.8
|
||||
git submodule update qtbase
|
||||
git submodule update qtimageformats
|
||||
cd qtbase
|
||||
@@ -433,6 +433,9 @@ jobs:
|
||||
if [ -n "${{ matrix.defines }}" ]; then
|
||||
DEFINE="-D ${{ matrix.defines }}=ON"
|
||||
echo Define from matrix: $DEFINE
|
||||
echo ::set-env name=ARTIFACT_NAME::Telegram_${{ matrix.defines }}
|
||||
else
|
||||
echo ::set-env name=ARTIFACT_NAME::Telegram
|
||||
fi
|
||||
|
||||
./configure.sh -D TDESKTOP_API_TEST=ON -D DESKTOP_APP_USE_PACKAGED=OFF $DEFINE
|
||||
@@ -453,5 +456,5 @@ jobs:
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
name: Upload artifact.
|
||||
with:
|
||||
name: Telegram
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: ${{ env.REPO_NAME }}/out/Debug/artifact/
|
||||
|
||||
21
.github/workflows/snap.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
env:
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
MANUAL_CACHING: "3"
|
||||
MANUAL_CACHING: "4"
|
||||
|
||||
steps:
|
||||
- name: Clone.
|
||||
@@ -29,9 +29,6 @@ jobs:
|
||||
|
||||
- name: First set up.
|
||||
run: |
|
||||
# Workaround for Heroku
|
||||
curl https://cli-assets.heroku.com/apt/release.key | sudo apt-key add -
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-8 g++-8 -y
|
||||
sudo snap install --classic snapcraft
|
||||
@@ -50,7 +47,7 @@ jobs:
|
||||
md5 $keyName
|
||||
}
|
||||
|
||||
snapcraft --version > CACHE_KEY.txt
|
||||
snap run snapcraft --version > CACHE_KEY.txt
|
||||
gcc-8 --version >> CACHE_KEY.txt
|
||||
echo $MANUAL_CACHING >> CACHE_KEY.txt
|
||||
md5 CACHE_KEY
|
||||
@@ -67,7 +64,7 @@ jobs:
|
||||
|
||||
- name: CMake build.
|
||||
if: steps.cache-cmake.outputs.cache-hit != 'true'
|
||||
run: sudo snapcraft build --destructive-mode cmake
|
||||
run: sudo snap run snapcraft build --destructive-mode cmake
|
||||
|
||||
- name: FFmpeg cache.
|
||||
id: cache-ffmpeg
|
||||
@@ -78,11 +75,11 @@ jobs:
|
||||
|
||||
- name: FFmpeg build.
|
||||
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
|
||||
run: sudo snapcraft build --destructive-mode ffmpeg
|
||||
run: sudo snap run snapcraft build --destructive-mode ffmpeg
|
||||
|
||||
- name: Telegram Desktop snap build.
|
||||
if: env.ONLY_CACHE == 'false'
|
||||
run: sudo snapcraft --destructive-mode
|
||||
run: sudo snap run snapcraft --destructive-mode
|
||||
|
||||
- name: Move artifact.
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -102,5 +99,5 @@ jobs:
|
||||
|
||||
- name: Remove unneeded directories for cache.
|
||||
run: |
|
||||
sudo rm -rf parts/{cmake,ffmpeg}/{build,src,ubuntu}
|
||||
sudo rm -rf parts/{cmake,ffmpeg}/state/{stage,prime}
|
||||
sudo rm -rf parts/*/{build,src,ubuntu}
|
||||
sudo rm -rf parts/*/state/{stage,prime}
|
||||
|
||||
25
.github/workflows/win.yml
vendored
@@ -4,11 +4,11 @@ on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
SDK: "10.0.18362.0"
|
||||
VC: "call vcvars32.bat && cd Libraries"
|
||||
GIT: "https://github.com"
|
||||
QT: "5_12_5"
|
||||
QT: "5_12_8"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
@@ -263,13 +263,13 @@ jobs:
|
||||
|
||||
rmdir /S /Q .git
|
||||
|
||||
- name: Qt 5.12.5 cache.
|
||||
- name: Qt 5.12.8 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/Qt-5.12.5
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_5.diff') }}
|
||||
- name: Configure Qt 5.12.5.
|
||||
path: ${{ env.LibrariesPath }}/Qt-5.12.8
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_8.diff') }}
|
||||
- name: Configure Qt 5.12.8.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
shell: cmd
|
||||
run: |
|
||||
@@ -278,7 +278,7 @@ jobs:
|
||||
git clone git://code.qt.io/qt/qt5.git qt_%QT%
|
||||
cd qt_%QT%
|
||||
perl init-repository --module-subset=qtbase,qtimageformats
|
||||
git checkout v5.12.5
|
||||
git checkout v5.12.8
|
||||
git submodule update qtbase
|
||||
git submodule update qtimageformats
|
||||
cd qtbase
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
SET LIBS=libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib
|
||||
|
||||
configure ^
|
||||
-prefix "%LibrariesPath%\Qt-5.12.5" ^
|
||||
-prefix "%LibrariesPath%\Qt-5.12.8" ^
|
||||
-debug ^
|
||||
-force-debug-info ^
|
||||
-opensource ^
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
-nomake examples ^
|
||||
-nomake tests ^
|
||||
-platform win32-msvc
|
||||
- name: Qt 5.12.5 build.
|
||||
- name: Qt 5.12.8 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
shell: cmd
|
||||
run: |
|
||||
@@ -324,6 +324,9 @@ jobs:
|
||||
if [ -n "${{ matrix.defines }}" ]; then
|
||||
DEFINE="-D ${{ matrix.defines }}=ON"
|
||||
echo Define from matrix: $DEFINE
|
||||
echo ::set-env name=ARTIFACT_NAME::Telegram_${{ matrix.defines }}
|
||||
else
|
||||
echo ::set-env name=ARTIFACT_NAME::Telegram
|
||||
fi
|
||||
echo "::set-env name=TDESKTOP_BUILD_DEFINE::$DEFINE"
|
||||
|
||||
@@ -355,5 +358,5 @@ jobs:
|
||||
name: Upload artifact.
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
with:
|
||||
name: Telegram
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: ${{ env.REPO_NAME }}\out\Debug\artifact\
|
||||
|
||||
@@ -23,7 +23,7 @@ The source code is published under GPLv3 with OpenSSL exception, the license is
|
||||
|
||||
## Third-party
|
||||
|
||||
* Qt 5.12.5 and 5.6.2, slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
|
||||
* Qt 5.12.8 and 5.6.2, slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
|
||||
* OpenSSL 1.1.1 ([OpenSSL License](https://www.openssl.org/source/license.html))
|
||||
* zlib 1.2.11 ([zlib License](http://www.zlib.net/zlib_license.html))
|
||||
* LZMA SDK 9.20 ([public domain](http://www.7-zip.org/sdk.html))
|
||||
|
||||
@@ -1187,7 +1187,7 @@ endif()
|
||||
|
||||
set_target_properties(Telegram PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder})
|
||||
|
||||
if ((NOT disable_autoupdate OR NOT LINUX) AND NOT build_macstore AND NOT build_winstore)
|
||||
if ((NOT DESKTOP_APP_DISABLE_AUTOUPDATE OR NOT LINUX) AND NOT build_macstore AND NOT build_winstore)
|
||||
add_executable(Updater WIN32)
|
||||
init_target(Updater)
|
||||
|
||||
|
||||
BIN
Telegram/Resources/art/dart_idle.tgs
Normal file
|
Before Width: | Height: | Size: 763 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
BIN
Telegram/Resources/icons/filters/filters_edit.png
Normal file
|
After Width: | Height: | Size: 339 B |
BIN
Telegram/Resources/icons/filters/filters_edit@2x.png
Normal file
|
After Width: | Height: | Size: 686 B |
BIN
Telegram/Resources/icons/filters/filters_edit@3x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/quiz_explain.png
Normal file
|
After Width: | Height: | Size: 542 B |
BIN
Telegram/Resources/icons/quiz_explain@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
Telegram/Resources/icons/quiz_explain@3x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Telegram/Resources/icons/quiz_timer.png
Normal file
|
After Width: | Height: | Size: 409 B |
BIN
Telegram/Resources/icons/quiz_timer@2x.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
Telegram/Resources/icons/quiz_timer@3x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 506 B |
|
Before Width: | Height: | Size: 120 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 1.6 KiB |
BIN
Telegram/Resources/icons/stickers_add_dot.png
Normal file
|
After Width: | Height: | Size: 204 B |
BIN
Telegram/Resources/icons/stickers_add_dot@2x.png
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
Telegram/Resources/icons/stickers_add_dot@3x.png
Normal file
|
After Width: | Height: | Size: 522 B |
BIN
Telegram/Resources/icons/stickers_add_unread.png
Normal file
|
After Width: | Height: | Size: 535 B |
BIN
Telegram/Resources/icons/stickers_add_unread@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/stickers_add_unread@3x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Telegram/Resources/icons/stickers_recent.png
Normal file
|
After Width: | Height: | Size: 527 B |
BIN
Telegram/Resources/icons/stickers_recent@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/stickers_recent@3x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1329,7 +1329,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_dialogs_skip_archive_in_search" = "Skip results from archive";
|
||||
"lng_dialogs_show_archive_in_search" = "With results from archive";
|
||||
|
||||
"lng_about_dice" = "Send a 🎲 emoji to any chat to get a random number from Telegram.";
|
||||
"lng_about_random" = "Send a {emoji} emoji to any chat to get a random number from Telegram.";
|
||||
"lng_about_random_send" = "Send";
|
||||
|
||||
"lng_open_this_link" = "Open this link?";
|
||||
"lng_open_link" = "Open";
|
||||
@@ -2223,6 +2224,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_polls_choose_question" = "Please enter a question.";
|
||||
"lng_polls_choose_answers" = "Please enter at least two options.";
|
||||
"lng_polls_choose_correct" = "Please choose the correct answer.";
|
||||
"lng_polls_solution_title" = "Explanation";
|
||||
"lng_polls_solution_placeholder" = "Add a Comment (Optional)";
|
||||
"lng_polls_solution_about" = "Users will see this comment after choosing a wrong answer, good for educational purposes.";
|
||||
|
||||
"lng_polls_poll_results_title" = "Poll results";
|
||||
"lng_polls_quiz_results_title" = "Quiz results";
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file>
|
||||
<file alias="art/sunrise.jpg">../../art/sunrise.jpg</file>
|
||||
<file alias="art/dice_idle.tgs">../../art/dice_idle.tgs</file>
|
||||
<file alias="art/dart_idle.tgs">../../art/dart_idle.tgs</file>
|
||||
<file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file>
|
||||
<file alias="night.tdesktop-theme">../../night.tdesktop-theme</file>
|
||||
<file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file>
|
||||
|
||||
@@ -71,8 +71,8 @@ inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int =
|
||||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||
inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
|
||||
inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia;
|
||||
inputMediaPoll#abe9ca25 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> = InputMedia;
|
||||
inputMediaDice#aeffa807 = InputMedia;
|
||||
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
|
||||
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
|
||||
|
||||
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
||||
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
|
||||
@@ -157,7 +157,7 @@ messageMediaGame#fdb19008 game:Game = MessageMedia;
|
||||
messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia;
|
||||
messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia;
|
||||
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
|
||||
messageMediaDice#638fe46b value:int = MessageMedia;
|
||||
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
|
||||
|
||||
messageActionEmpty#b6aef7b0 = MessageAction;
|
||||
messageActionChatCreate#a6638b9a title:string users:Vector<int> = MessageAction;
|
||||
@@ -533,7 +533,7 @@ inputStickerSetEmpty#ffb62b95 = InputStickerSet;
|
||||
inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;
|
||||
inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet;
|
||||
inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet;
|
||||
inputStickerSetDice#79e21a53 = InputStickerSet;
|
||||
inputStickerSetDice#e67f520e emoticon:string = InputStickerSet;
|
||||
|
||||
stickerSet#eeb46f27 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumb:flags.4?PhotoSize thumb_dc_id:flags.4?int count:int hash:int = StickerSet;
|
||||
|
||||
@@ -692,8 +692,8 @@ contacts.topPeersDisabled#b52c939d = contacts.TopPeers;
|
||||
draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage;
|
||||
draftMessage#fd8e711f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int message:string entities:flags.3?Vector<MessageEntity> date:int = DraftMessage;
|
||||
|
||||
messages.featuredStickersNotModified#4ede3cf = messages.FeaturedStickers;
|
||||
messages.featuredStickers#f89d88e5 hash:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;
|
||||
messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers;
|
||||
messages.featuredStickers#b6abc341 hash:int count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;
|
||||
|
||||
messages.recentStickersNotModified#b17f890 = messages.RecentStickers;
|
||||
messages.recentStickers#22f3afb3 hash:int packs:Vector<StickerPack> stickers:Vector<Document> dates:Vector<int> = messages.RecentStickers;
|
||||
@@ -1024,11 +1024,11 @@ help.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:strin
|
||||
|
||||
pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer;
|
||||
|
||||
poll#d5529d06 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> = Poll;
|
||||
poll#86e18161 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> close_period:flags.4?int close_date:flags.5?int = Poll;
|
||||
|
||||
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters;
|
||||
|
||||
pollResults#c87024a2 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<int> = PollResults;
|
||||
pollResults#badcc1a3 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<int> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> = PollResults;
|
||||
|
||||
chatOnlines#f041e250 onlines:int = ChatOnlines;
|
||||
|
||||
@@ -1144,7 +1144,7 @@ stats.broadcastStats#bdf78394 period:StatsDateRangeDays followers:StatsAbsValueA
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;
|
||||
initConnection#785188b8 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy query:!X = X;
|
||||
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
|
||||
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
|
||||
invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;
|
||||
invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;
|
||||
@@ -1382,6 +1382,7 @@ messages.getDialogFilters#f19ed96d = Vector<DialogFilter>;
|
||||
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
||||
messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;
|
||||
messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;
|
||||
messages.getOldFeaturedStickers#5fe7025b offset:int limit:int hash:int = messages.FeaturedStickers;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
@@ -1459,6 +1460,7 @@ channels.getInactiveChannels#11e831ee = messages.InactiveChats;
|
||||
|
||||
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
|
||||
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
|
||||
bots.setBotCommands#805d46f6 commands:Vector<BotCommand> = Bool;
|
||||
|
||||
payments.getPaymentForm#99f09745 msg_id:int = payments.PaymentForm;
|
||||
payments.getPaymentReceipt#a092a980 msg_id:int = payments.PaymentReceipt;
|
||||
@@ -1468,10 +1470,11 @@ payments.getSavedInfo#227d824b = payments.SavedInfo;
|
||||
payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;
|
||||
payments.getBankCardData#2e79d779 number:string = payments.BankCardData;
|
||||
|
||||
stickers.createStickerSet#9bd86e6a flags:# masks:flags.0?true user_id:InputUser title:string short_name:string stickers:Vector<InputStickerSetItem> = messages.StickerSet;
|
||||
stickers.createStickerSet#f1036780 flags:# masks:flags.0?true animated:flags.1?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector<InputStickerSetItem> = messages.StickerSet;
|
||||
stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;
|
||||
stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet;
|
||||
stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet;
|
||||
stickers.setStickerSetThumb#9a364e30 stickerset:InputStickerSet thumb:InputDocument = messages.StickerSet;
|
||||
|
||||
phone.getCallConfig#55451fa9 = DataJSON;
|
||||
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||
@@ -1491,7 +1494,7 @@ langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLangua
|
||||
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
|
||||
folders.deleteFolder#1c295881 folder_id:int = Updates;
|
||||
|
||||
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
|
||||
stats.getBroadcastStats#e6300dba flags:# dark:flags.0?true channel:InputChannel tz_offset:int = stats.BroadcastStats;
|
||||
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
|
||||
|
||||
// LAYER 111
|
||||
// LAYER 112
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.0.1.0" />
|
||||
Version="2.1.0.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
||||
@@ -33,8 +33,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,0,1,0
|
||||
PRODUCTVERSION 2,0,1,0
|
||||
FILEVERSION 2,1,0,0
|
||||
PRODUCTVERSION 2,1,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -51,10 +51,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.0.1.0"
|
||||
VALUE "FileVersion", "2.1.0.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.0.1.0"
|
||||
VALUE "ProductVersion", "2.1.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -24,8 +24,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,0,1,0
|
||||
PRODUCTVERSION 2,0,1,0
|
||||
FILEVERSION 2,1,0,0
|
||||
PRODUCTVERSION 2,1,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -42,10 +42,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.0.1.0"
|
||||
VALUE "FileVersion", "2.1.0.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.0.1.0"
|
||||
VALUE "ProductVersion", "2.1.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -21,6 +21,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "chat_helpers/message_field.h" // ConvertTextTagsToEntities.
|
||||
#include "ui/text/text_entity.h" // TextWithEntities.
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "mainwidget.h"
|
||||
#include "apiwrap.h"
|
||||
#include "app.h"
|
||||
@@ -199,8 +201,23 @@ void SendExistingPhoto(
|
||||
}
|
||||
|
||||
bool SendDice(Api::MessageToSend &message) {
|
||||
static const auto kDiceString = QString::fromUtf8("\xF0\x9F\x8E\xB2");
|
||||
if (message.textWithTags.text.midRef(0).trimmed() != kDiceString) {
|
||||
const auto full = message.textWithTags.text.midRef(0).trimmed();
|
||||
auto length = 0;
|
||||
if (!Ui::Emoji::Find(full.data(), full.data() + full.size(), &length)
|
||||
|| length != full.size()) {
|
||||
return false;
|
||||
}
|
||||
auto &account = message.action.history->session().account();
|
||||
auto &config = account.appConfig();
|
||||
static const auto hardcoded = std::vector<QString>{
|
||||
QString::fromUtf8("\xF0\x9F\x8E\xB2"),
|
||||
QString::fromUtf8("\xF0\x9F\x8E\xAF")
|
||||
};
|
||||
const auto list = config.get<std::vector<QString>>(
|
||||
"emojies_send_dice",
|
||||
hardcoded);
|
||||
const auto emoji = full.toString();
|
||||
if (!ranges::contains(list, emoji)) {
|
||||
return false;
|
||||
}
|
||||
const auto history = message.action.history;
|
||||
@@ -266,7 +283,7 @@ bool SendDice(Api::MessageToSend &message) {
|
||||
MTP_int(HistoryItem::NewMessageDate(
|
||||
message.action.options.scheduled)),
|
||||
MTP_string(),
|
||||
MTP_messageMediaDice(MTP_int(0)),
|
||||
MTP_messageMediaDice(MTP_int(0), MTP_string(emoji)),
|
||||
MTPReplyMarkup(),
|
||||
MTP_vector<MTPMessageEntity>(),
|
||||
MTP_int(1),
|
||||
@@ -284,7 +301,7 @@ bool SendDice(Api::MessageToSend &message) {
|
||||
MTP_flags(sendFlags),
|
||||
peer->input,
|
||||
MTP_int(replyTo),
|
||||
MTP_inputMediaDice(),
|
||||
MTP_inputMediaDice(MTP_string(emoji)),
|
||||
MTP_string(),
|
||||
MTP_long(randomId),
|
||||
MTPReplyMarkup(),
|
||||
|
||||
@@ -23,6 +23,6 @@ void SendExistingPhoto(
|
||||
Api::MessageToSend &&message,
|
||||
not_null<PhotoData*> photo);
|
||||
|
||||
[[nodiscard]] bool SendDice(Api::MessageToSend &message);
|
||||
bool SendDice(Api::MessageToSend &message);
|
||||
|
||||
} // namespace Api
|
||||
|
||||
@@ -5754,16 +5754,6 @@ void ApiWrap::createPoll(
|
||||
if (action.options.scheduled) {
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
|
||||
}
|
||||
|
||||
const auto inputFlags = data.quiz()
|
||||
? MTPDinputMediaPoll::Flag::f_correct_answers
|
||||
: MTPDinputMediaPoll::Flag(0);
|
||||
auto correct = QVector<MTPbytes>();
|
||||
for (const auto &answer : data.answers) {
|
||||
if (answer.correct) {
|
||||
correct.push_back(MTP_bytes(answer.option));
|
||||
}
|
||||
}
|
||||
auto &histories = history->owner().histories();
|
||||
const auto requestType = Data::Histories::RequestType::Send;
|
||||
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
|
||||
@@ -5772,10 +5762,7 @@ void ApiWrap::createPoll(
|
||||
MTP_flags(sendFlags),
|
||||
peer->input,
|
||||
MTP_int(replyTo),
|
||||
MTP_inputMediaPoll(
|
||||
MTP_flags(inputFlags),
|
||||
PollDataToMTP(&data),
|
||||
MTP_vector<MTPbytes>(correct)),
|
||||
PollDataToInputMedia(&data),
|
||||
MTP_string(),
|
||||
MTP_long(rand_value<uint64>()),
|
||||
MTPReplyMarkup(),
|
||||
@@ -5852,25 +5839,12 @@ void ApiWrap::closePoll(not_null<HistoryItem*> item) {
|
||||
if (!poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto inputFlags = poll->quiz()
|
||||
? MTPDinputMediaPoll::Flag::f_correct_answers
|
||||
: MTPDinputMediaPoll::Flag(0);
|
||||
auto correct = QVector<MTPbytes>();
|
||||
for (const auto &answer : poll->answers) {
|
||||
if (answer.correct) {
|
||||
correct.push_back(MTP_bytes(answer.option));
|
||||
}
|
||||
}
|
||||
const auto requestId = request(MTPmessages_EditMessage(
|
||||
MTP_flags(MTPmessages_EditMessage::Flag::f_media),
|
||||
item->history()->peer->input,
|
||||
MTP_int(item->id),
|
||||
MTPstring(),
|
||||
MTP_inputMediaPoll(
|
||||
MTP_flags(inputFlags),
|
||||
PollDataToMTP(poll, true),
|
||||
MTP_vector<MTPbytes>(correct)),
|
||||
PollDataToInputMedia(poll, true),
|
||||
MTPReplyMarkup(),
|
||||
MTPVector<MTPMessageEntity>(),
|
||||
MTP_int(0) // schedule_date
|
||||
|
||||
@@ -57,7 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kImageAreaLimit = 6'016 * 3'384;
|
||||
constexpr auto kImageAreaLimit = 12'032 * 9'024;
|
||||
|
||||
App::LaunchState _launchState = App::Launched;
|
||||
|
||||
|
||||
@@ -800,7 +800,7 @@ themesMenuPosition: point(-2px, 25px);
|
||||
|
||||
createPollField: InputField(defaultInputField) {
|
||||
font: boxTextFont;
|
||||
textMargins: margins(0px, 0px, 0px, 0px);
|
||||
textMargins: margins(0px, 4px, 0px, 4px);
|
||||
textAlign: align(left);
|
||||
heightMin: 36px;
|
||||
heightMax: 86px;
|
||||
@@ -822,6 +822,11 @@ createPollOptionField: InputField(createPollField) {
|
||||
placeholderMargins: margins(2px, 0px, 2px, 0px);
|
||||
heightMax: 68px;
|
||||
}
|
||||
createPollSolutionField: InputField(createPollField) {
|
||||
textMargins: margins(0px, 4px, 0px, 4px);
|
||||
border: 1px;
|
||||
borderActive: 2px;
|
||||
}
|
||||
createPollLimitLabel: FlatLabel(defaultFlatLabel) {
|
||||
minWidth: 274px;
|
||||
align: align(topleft);
|
||||
|
||||
@@ -38,6 +38,8 @@ constexpr auto kMaxOptionsCount = PollData::kMaxOptions;
|
||||
constexpr auto kOptionLimit = 100;
|
||||
constexpr auto kWarnQuestionLimit = 80;
|
||||
constexpr auto kWarnOptionLimit = 30;
|
||||
constexpr auto kSolutionLimit = 200;
|
||||
constexpr auto kWarnSolutionLimit = 60;
|
||||
constexpr auto kErrorLimit = 99;
|
||||
|
||||
class Options {
|
||||
@@ -59,6 +61,7 @@ public:
|
||||
[[nodiscard]] rpl::producer<int> usedCount() const;
|
||||
[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
|
||||
[[nodiscard]] rpl::producer<> backspaceInFront() const;
|
||||
[[nodiscard]] rpl::producer<> tabbed() const;
|
||||
|
||||
private:
|
||||
class Option {
|
||||
@@ -146,6 +149,7 @@ private:
|
||||
bool _hasCorrect = false;
|
||||
rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
|
||||
rpl::event_stream<> _backspaceInFront;
|
||||
rpl::event_stream<> _tabbed;
|
||||
|
||||
};
|
||||
|
||||
@@ -217,6 +221,7 @@ Options::Option::Option(
|
||||
InitField(outer, _field, session);
|
||||
_field->setMaxLength(kOptionLimit + kErrorLimit);
|
||||
_field->show();
|
||||
_field->customTab(true);
|
||||
|
||||
_wrap->hide(anim::type::instant);
|
||||
|
||||
@@ -497,6 +502,10 @@ rpl::producer<> Options::backspaceInFront() const {
|
||||
return _backspaceInFront.events();
|
||||
}
|
||||
|
||||
rpl::producer<> Options::tabbed() const {
|
||||
return _tabbed.events();
|
||||
}
|
||||
|
||||
void Options::Option::show(anim::type animated) {
|
||||
_wrap->show(animated);
|
||||
}
|
||||
@@ -647,6 +656,14 @@ void Options::addEmptyOption() {
|
||||
QObject::connect(field, &Ui::InputField::focused, [=] {
|
||||
_scrollToWidget.fire_copy(field);
|
||||
});
|
||||
QObject::connect(field, &Ui::InputField::tabbed, [=] {
|
||||
const auto index = findField(field);
|
||||
if (index + 1 < _list.size()) {
|
||||
_list[index + 1]->setFocus();
|
||||
} else {
|
||||
_tabbed.fire({});
|
||||
}
|
||||
});
|
||||
base::install_event_filter(field, [=](not_null<QEvent*> event) {
|
||||
if (event->type() != QEvent::KeyPress
|
||||
|| !field->getLastText().isEmpty()) {
|
||||
@@ -768,6 +785,7 @@ not_null<Ui::InputField*> CreatePollBox::setupQuestion(
|
||||
InitField(getDelegate()->outerContainer(), question, _session);
|
||||
question->setMaxLength(kQuestionLimit + kErrorLimit);
|
||||
question->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
|
||||
question->customTab(true);
|
||||
|
||||
const auto warning = CreateWarningLabel(
|
||||
container,
|
||||
@@ -794,6 +812,69 @@ not_null<Ui::InputField*> CreatePollBox::setupQuestion(
|
||||
return question;
|
||||
}
|
||||
|
||||
not_null<Ui::InputField*> CreatePollBox::setupSolution(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
rpl::producer<bool> shown) {
|
||||
using namespace Settings;
|
||||
|
||||
const auto outer = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<Ui::VerticalLayout>(container))
|
||||
)->setDuration(0)->toggleOn(std::move(shown));
|
||||
const auto inner = outer->entity();
|
||||
|
||||
AddSkip(inner);
|
||||
AddSubsectionTitle(inner, tr::lng_polls_solution_title());
|
||||
const auto solution = inner->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
inner,
|
||||
st::createPollSolutionField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_polls_solution_placeholder()),
|
||||
st::createPollFieldPadding);
|
||||
InitField(getDelegate()->outerContainer(), solution, _session);
|
||||
solution->setMaxLength(kSolutionLimit + kErrorLimit);
|
||||
solution->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||
solution->setInstantReplacesEnabled(
|
||||
_session->settings().replaceEmojiValue());
|
||||
solution->setMarkdownReplacesEnabled(rpl::single(true));
|
||||
solution->setEditLinkCallback(
|
||||
DefaultEditLinkCallback(_session, solution));
|
||||
solution->customTab(true);
|
||||
|
||||
const auto warning = CreateWarningLabel(
|
||||
inner,
|
||||
solution,
|
||||
kSolutionLimit,
|
||||
kWarnSolutionLimit);
|
||||
rpl::combine(
|
||||
solution->geometryValue(),
|
||||
warning->sizeValue()
|
||||
) | rpl::start_with_next([=](QRect geometry, QSize label) {
|
||||
warning->moveToLeft(
|
||||
(inner->width()
|
||||
- label.width()
|
||||
- st::createPollWarningPosition.x()),
|
||||
(geometry.y()
|
||||
- st::createPollFieldPadding.top()
|
||||
- st::settingsSubsectionTitlePadding.bottom()
|
||||
- st::settingsSubsectionTitle.style.font->height
|
||||
+ st::settingsSubsectionTitle.style.font->ascent
|
||||
- st::createPollWarning.style.font->ascent),
|
||||
geometry.width());
|
||||
}, warning->lifetime());
|
||||
|
||||
inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
tr::lng_polls_solution_about(),
|
||||
st::boxDividerLabel),
|
||||
st::createPollFieldTitlePadding);
|
||||
|
||||
return solution;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
using namespace Settings;
|
||||
|
||||
@@ -836,6 +917,10 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
st::boxDividerLabel),
|
||||
st::createPollLimitPadding));
|
||||
|
||||
connect(question, &Ui::InputField::tabbed, [=] {
|
||||
options->focusFirst();
|
||||
});
|
||||
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(container, tr::lng_polls_create_settings());
|
||||
|
||||
@@ -866,6 +951,24 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
(_chosen & PollData::Flag::Quiz),
|
||||
st::defaultCheckbox),
|
||||
st::createPollCheckboxMargin);
|
||||
|
||||
const auto solution = setupSolution(
|
||||
container,
|
||||
rpl::single(quiz->checked()) | rpl::then(quiz->checkedChanges()));
|
||||
|
||||
options->tabbed(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (quiz->checked()) {
|
||||
solution->setFocus();
|
||||
} else {
|
||||
question->setFocus();
|
||||
}
|
||||
}, question->lifetime());
|
||||
|
||||
connect(solution, &Ui::InputField::tabbed, [=] {
|
||||
question->setFocus();
|
||||
});
|
||||
|
||||
quiz->setDisabled(_disabled & PollData::Flag::Quiz);
|
||||
if (multiple) {
|
||||
multiple->setDisabled((_disabled & PollData::Flag::MultiChoice)
|
||||
@@ -911,6 +1014,13 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
auto result = PollData(&_session->data(), id);
|
||||
result.question = question->getLastText().trimmed();
|
||||
result.answers = options->toPollAnswers();
|
||||
const auto solutionWithTags = quiz->checked()
|
||||
? solution->getTextWithAppliedMarkdown()
|
||||
: TextWithTags();
|
||||
result.solution = TextWithEntities{
|
||||
solutionWithTags.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(solutionWithTags.tags)
|
||||
};
|
||||
const auto publicVotes = (anonymous && !anonymous->checked());
|
||||
const auto multiChoice = (multiple && multiple->checked());
|
||||
result.setFlags(Flag(0)
|
||||
@@ -937,6 +1047,12 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
} else {
|
||||
*error &= ~Error::Correct;
|
||||
}
|
||||
if (quiz->checked()
|
||||
&& solution->getLastText().trimmed().size() > kSolutionLimit) {
|
||||
*error |= Error::Solution;
|
||||
} else {
|
||||
*error &= ~Error::Solution;
|
||||
}
|
||||
};
|
||||
const auto showError = [=](const QString &text) {
|
||||
Ui::Toast::Show(text);
|
||||
@@ -951,6 +1067,8 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
|
||||
options->focusFirst();
|
||||
} else if (*error & Error::Correct) {
|
||||
showError(tr::lng_polls_choose_correct(tr::now));
|
||||
} else if (*error & Error::Solution) {
|
||||
solution->showError();
|
||||
} else if (!*error) {
|
||||
_submitRequests.fire({ collectResult(), sendOptions });
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
PollData::Flags disabled,
|
||||
Api::SendType sendType);
|
||||
|
||||
rpl::producer<Result> submitRequests() const;
|
||||
[[nodiscard]] rpl::producer<Result> submitRequests() const;
|
||||
void submitFailed(const QString &error);
|
||||
|
||||
void setInnerFocus() override;
|
||||
@@ -50,13 +50,17 @@ private:
|
||||
Options = 0x02,
|
||||
Correct = 0x04,
|
||||
Other = 0x08,
|
||||
Solution = 0x10,
|
||||
};
|
||||
friend constexpr inline bool is_flag_type(Error) { return true; }
|
||||
using Errors = base::flags<Error>;
|
||||
|
||||
object_ptr<Ui::RpWidget> setupContent();
|
||||
not_null<Ui::InputField*> setupQuestion(
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> setupContent();
|
||||
[[nodiscard]] not_null<Ui::InputField*> setupQuestion(
|
||||
not_null<Ui::VerticalLayout*> container);
|
||||
[[nodiscard]] not_null<Ui::InputField*> setupSolution(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
rpl::producer<bool> shown);
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
const PollData::Flags _chosen = PollData::Flags();
|
||||
|
||||
@@ -89,11 +89,16 @@ stickersRowDisabledOpacity: 0.4;
|
||||
stickersRowDuration: 200;
|
||||
|
||||
stickersSettings: icon {{ "emoji_settings", emojiIconFg }};
|
||||
stickersTrending: icon {{ "emoji_trending", emojiIconFg }};
|
||||
stickersTrending: icon {{ "stickers_add", emojiIconFg }};
|
||||
stickersTrendingUnread: icon {
|
||||
{ "stickers_add_unread", emojiIconFg },
|
||||
{ "stickers_add_dot", dialogsUnreadBg }
|
||||
};
|
||||
stickersRecent: icon {{ "stickers_recent", emojiIconFg }};
|
||||
stickersSearch: icon {{ "stickers_search", emojiIconFg, point(0px, 1px) }};
|
||||
|
||||
stickersSettingsUnreadSize: 17px;
|
||||
stickersSettingsUnreadPosition: point(4px, 5px);
|
||||
stickersSettingsUnreadSize: 6px;
|
||||
stickersSettingsUnreadPosition: point(6px, 10px);
|
||||
|
||||
filtersRemove: IconButton(stickersRemove) {
|
||||
ripple: defaultRippleAnimation;
|
||||
|
||||
@@ -78,7 +78,7 @@ void ApplyArchivedResult(const MTPDmessages_stickerSetInstallResultArchive &d) {
|
||||
Local::writeArchivedStickers();
|
||||
|
||||
auto toast = Ui::Toast::Config();
|
||||
toast.text = tr::lng_stickers_packs_archived(tr::now);
|
||||
toast.text = { tr::lng_stickers_packs_archived(tr::now) };
|
||||
toast.maxWidth = toast.minWidth = st::stickersToastMaxWidth;
|
||||
toast.multiline = true;
|
||||
toast.padding = st::stickersToastPadding;
|
||||
|
||||
@@ -24,8 +24,9 @@ constexpr auto kZeroDiceDocumentId = 0xa3b83c9f84fa9e83ULL;
|
||||
|
||||
} // namespace
|
||||
|
||||
DicePack::DicePack(not_null<Main::Session*> session)
|
||||
: _session(session) {
|
||||
DicePack::DicePack(not_null<Main::Session*> session, const QString &emoji)
|
||||
: _session(session)
|
||||
, _emoji(emoji) {
|
||||
}
|
||||
|
||||
DicePack::~DicePack() = default;
|
||||
@@ -34,10 +35,7 @@ DocumentData *DicePack::lookup(int value) {
|
||||
if (!_requestId) {
|
||||
load();
|
||||
}
|
||||
if (!value) {
|
||||
ensureZeroGenerated();
|
||||
return _zero;
|
||||
}
|
||||
tryGenerateLocalZero();
|
||||
const auto i = _map.find(value);
|
||||
return (i != end(_map)) ? i->second.get() : nullptr;
|
||||
}
|
||||
@@ -47,7 +45,7 @@ void DicePack::load() {
|
||||
return;
|
||||
}
|
||||
_requestId = _session->api().request(MTPmessages_GetStickerSet(
|
||||
MTP_inputStickerSetDice()
|
||||
MTP_inputStickerSetDice(MTP_string(_emoji))
|
||||
)).done([=](const MTPmessages_StickerSet &result) {
|
||||
result.match([&](const MTPDmessages_stickerSet &data) {
|
||||
applySet(data);
|
||||
@@ -58,22 +56,50 @@ void DicePack::load() {
|
||||
}
|
||||
|
||||
void DicePack::applySet(const MTPDmessages_stickerSet &data) {
|
||||
auto index = 0;
|
||||
_map.clear();
|
||||
auto documents = base::flat_map<DocumentId, not_null<DocumentData*>>();
|
||||
for (const auto &sticker : data.vdocuments().v) {
|
||||
const auto document = _session->data().processDocument(
|
||||
sticker);
|
||||
if (document->sticker()) {
|
||||
_map.emplace(++index, document);
|
||||
documents.emplace(document->id, document);
|
||||
}
|
||||
}
|
||||
for (const auto pack : data.vpacks().v) {
|
||||
pack.match([&](const MTPDstickerPack &data) {
|
||||
const auto emoji = qs(data.vemoticon());
|
||||
if (emoji.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto ch = int(emoji[0].unicode());
|
||||
const auto index = (ch == '#') ? 0 : (ch + 1 - '1');
|
||||
if (index < 0 || index > 6) {
|
||||
return;
|
||||
}
|
||||
for (const auto id : data.vdocuments().v) {
|
||||
if (const auto document = documents.take(id.v)) {
|
||||
_map.emplace(index, *document);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void DicePack::ensureZeroGenerated() {
|
||||
if (_zero) {
|
||||
void DicePack::tryGenerateLocalZero() {
|
||||
if (!_map.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto path = qsl(":/gui/art/dice_idle.tgs");
|
||||
static const auto kDiceString = QString::fromUtf8("\xF0\x9F\x8E\xB2");
|
||||
static const auto kDartString = QString::fromUtf8("\xF0\x9F\x8E\xAF");
|
||||
const auto path = (_emoji == kDiceString)
|
||||
? qsl(":/gui/art/dice_idle.tgs")
|
||||
: (_emoji == kDartString)
|
||||
? qsl(":/gui/art/dart_idle.tgs")
|
||||
: QString();
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto task = FileLoadTask(
|
||||
path,
|
||||
QByteArray(),
|
||||
@@ -84,13 +110,29 @@ void DicePack::ensureZeroGenerated() {
|
||||
task.process();
|
||||
const auto result = task.peekResult();
|
||||
Assert(result != nullptr);
|
||||
_zero = _session->data().processDocument(
|
||||
const auto document = _session->data().processDocument(
|
||||
result->document,
|
||||
std::move(result->thumb));
|
||||
_zero->setLocation(FileLocation(path));
|
||||
document->setLocation(FileLocation(path));
|
||||
|
||||
Ensures(_zero->sticker());
|
||||
Ensures(_zero->sticker()->animated);
|
||||
_map.emplace(0, document);
|
||||
|
||||
Ensures(document->sticker());
|
||||
Ensures(document->sticker()->animated);
|
||||
}
|
||||
|
||||
DicePacks::DicePacks(not_null<Main::Session*> session) : _session(session) {
|
||||
}
|
||||
|
||||
DocumentData *DicePacks::lookup(const QString &emoji, int value) {
|
||||
const auto i = _packs.find(emoji);
|
||||
if (i != end(_packs)) {
|
||||
return i->second->lookup(value);
|
||||
}
|
||||
return _packs.emplace(
|
||||
emoji,
|
||||
std::make_unique<DicePack>(_session, emoji)
|
||||
).first->second->lookup(value);
|
||||
}
|
||||
|
||||
} // namespace Stickers
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
@@ -17,7 +18,7 @@ namespace Stickers {
|
||||
|
||||
class DicePack final {
|
||||
public:
|
||||
explicit DicePack(not_null<Main::Session*> session);
|
||||
DicePack(not_null<Main::Session*> session, const QString &emoji);
|
||||
~DicePack();
|
||||
|
||||
DocumentData *lookup(int value);
|
||||
@@ -25,13 +26,26 @@ public:
|
||||
private:
|
||||
void load();
|
||||
void applySet(const MTPDmessages_stickerSet &data);
|
||||
void ensureZeroGenerated();
|
||||
void tryGenerateLocalZero();
|
||||
|
||||
not_null<Main::Session*> _session;
|
||||
const not_null<Main::Session*> _session;
|
||||
QString _emoji;
|
||||
base::flat_map<int, not_null<DocumentData*>> _map;
|
||||
DocumentData *_zero = nullptr;
|
||||
mtpRequestId _requestId = 0;
|
||||
|
||||
};
|
||||
|
||||
class DicePacks final {
|
||||
public:
|
||||
explicit DicePacks(not_null<Main::Session*> session);
|
||||
|
||||
DocumentData *lookup(const QString &emoji, int value);
|
||||
|
||||
private:
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
base::flat_map<QString, std::unique_ptr<DicePack>> _packs;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Stickers
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace {
|
||||
constexpr auto kInlineItemsMaxPerRow = 5;
|
||||
constexpr auto kSearchRequestDelay = 400;
|
||||
constexpr auto kRecentDisplayLimit = 20;
|
||||
constexpr auto kPreloadOfficialPages = 4;
|
||||
constexpr auto kOfficialLoadLimit = 40;
|
||||
|
||||
bool SetInMyList(MTPDstickerSet::Flags flags) {
|
||||
return (flags & MTPDstickerSet::Flag::f_installed_date)
|
||||
@@ -138,7 +140,6 @@ private:
|
||||
void finishDragging();
|
||||
void paintStickerSettingsIcon(Painter &p) const;
|
||||
void paintSearchIcon(Painter &p) const;
|
||||
void paintFeaturedStickerSetsBadge(Painter &p, int iconLeft) const;
|
||||
void paintSetIcon(Painter &p, const StickerIcon &icon, int x) const;
|
||||
void paintSelectionBar(Painter &p) const;
|
||||
void paintLeftRightFading(Painter &p) const;
|
||||
@@ -694,18 +695,6 @@ void StickersListWidget::Footer::paintSearchIcon(Painter &p) const {
|
||||
st::stickersSearch.paint(p, searchLeft + (st::stickerIconWidth - st::stickersSearch.width()) / 2, _iconsTop + st::emojiCategory.iconPosition.y(), width());
|
||||
}
|
||||
|
||||
void StickersListWidget::Footer::paintFeaturedStickerSetsBadge(Painter &p, int iconLeft) const {
|
||||
if (const auto unread = _pan->session().data().featuredStickerSetsUnreadCount()) {
|
||||
Dialogs::Layout::UnreadBadgeStyle unreadSt;
|
||||
unreadSt.sizeId = Dialogs::Layout::UnreadBadgeInStickersPanel;
|
||||
unreadSt.size = st::stickersSettingsUnreadSize;
|
||||
int unreadRight = iconLeft + st::stickerIconWidth - st::stickersSettingsUnreadPosition.x();
|
||||
if (rtl()) unreadRight = width() - unreadRight;
|
||||
int unreadTop = _iconsTop + st::stickersSettingsUnreadPosition.y();
|
||||
Dialogs::Layout::paintUnreadCount(p, QString::number(unread), unreadRight, unreadTop, unreadSt);
|
||||
}
|
||||
}
|
||||
|
||||
void StickersListWidget::Footer::validateIconLottieAnimation(
|
||||
const StickerIcon &icon) {
|
||||
if (icon.lottie
|
||||
@@ -788,19 +777,22 @@ void StickersListWidget::Footer::paintSetIcon(
|
||||
} else if (icon.megagroup) {
|
||||
icon.megagroup->paintUserpicLeft(p, x + (st::stickerIconWidth - st::stickerGroupCategorySize) / 2, _iconsTop + (st::emojiFooterHeight - st::stickerGroupCategorySize) / 2, width(), st::stickerGroupCategorySize);
|
||||
} else {
|
||||
auto getSpecialSetIcon = [](uint64 setId) {
|
||||
if (setId == Stickers::FeaturedSetId) {
|
||||
return &st::stickersTrending;
|
||||
const auto paintedIcon = [&] {
|
||||
if (icon.setId == Stickers::FeaturedSetId) {
|
||||
const auto session = &_pan->session();
|
||||
return session->data().featuredStickerSetsUnreadCount()
|
||||
? &st::stickersTrendingUnread
|
||||
: &st::stickersTrending;
|
||||
//} else if (setId == Stickers::FavedSetId) {
|
||||
// return &st::stickersFaved;
|
||||
}
|
||||
return &st::emojiRecent;
|
||||
};
|
||||
auto paintedIcon = getSpecialSetIcon(icon.setId);
|
||||
paintedIcon->paint(p, x + (st::stickerIconWidth - paintedIcon->width()) / 2, _iconsTop + st::emojiCategory.iconPosition.y(), width());
|
||||
if (icon.setId == Stickers::FeaturedSetId) {
|
||||
paintFeaturedStickerSetsBadge(p, x);
|
||||
}
|
||||
return &st::stickersRecent;
|
||||
}();
|
||||
paintedIcon->paint(
|
||||
p,
|
||||
x + (st::stickerIconWidth - paintedIcon->width()) / 2,
|
||||
_iconsTop + (st::emojiFooterHeight - paintedIcon->height()) / 2,
|
||||
width());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,25 +893,86 @@ void StickersListWidget::checkVisibleFeatured(
|
||||
readVisibleFeatured(visibleTop, visibleBottom);
|
||||
|
||||
const auto visibleHeight = visibleBottom - visibleTop;
|
||||
|
||||
if (visibleBottom > height() - visibleHeight * kPreloadOfficialPages) {
|
||||
preloadMoreOfficial();
|
||||
}
|
||||
|
||||
const auto rowHeight = featuredRowHeight();
|
||||
const auto destroyAbove = floorclamp(visibleTop - visibleHeight, rowHeight, 0, _featuredSets.size());
|
||||
const auto destroyBelow = ceilclamp(visibleBottom + visibleHeight, rowHeight, 0, _featuredSets.size());
|
||||
const auto destroyAbove = floorclamp(visibleTop - visibleHeight, rowHeight, 0, _officialSets.size());
|
||||
const auto destroyBelow = ceilclamp(visibleBottom + visibleHeight, rowHeight, 0, _officialSets.size());
|
||||
for (auto i = 0; i != destroyAbove; ++i) {
|
||||
destroyLottieIn(_featuredSets[i]);
|
||||
destroyLottieIn(_officialSets[i]);
|
||||
}
|
||||
for (auto i = destroyBelow; i != _featuredSets.size(); ++i) {
|
||||
destroyLottieIn(_featuredSets[i]);
|
||||
for (auto i = destroyBelow; i != _officialSets.size(); ++i) {
|
||||
destroyLottieIn(_officialSets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void StickersListWidget::preloadMoreOfficial() {
|
||||
if (_officialRequestId) {
|
||||
return;
|
||||
}
|
||||
_officialRequestId = _api.request(MTPmessages_GetOldFeaturedStickers(
|
||||
MTP_int(_officialOffset),
|
||||
MTP_int(kOfficialLoadLimit),
|
||||
MTP_int(0)
|
||||
)).done([=](const MTPmessages_FeaturedStickers &result) {
|
||||
_officialRequestId = 0;
|
||||
result.match([&](const MTPDmessages_featuredStickersNotModified &d) {
|
||||
LOG(("Api Error: messages.featuredStickersNotModified."));
|
||||
}, [&](const MTPDmessages_featuredStickers &data) {
|
||||
const auto &list = data.vsets().v;
|
||||
_officialOffset += list.size();
|
||||
for (int i = 0, l = list.size(); i != l; ++i) {
|
||||
auto &data = list[i];
|
||||
const auto setData = data.match([&](const auto &data) {
|
||||
return data.vset().match([](const MTPDstickerSet &data) {
|
||||
return &data;
|
||||
});
|
||||
});
|
||||
const auto covers = data.match([](const MTPDstickerSetCovered &) {
|
||||
return Stickers::Pack();
|
||||
}, [&](const MTPDstickerSetMultiCovered &data) {
|
||||
auto result = Stickers::Pack();
|
||||
for (const auto &cover : data.vcovers().v) {
|
||||
const auto document = session().data().processDocument(cover);
|
||||
if (document->sticker()) {
|
||||
result.push_back(document);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
if (const auto set = Stickers::FeedSet(*setData)) {
|
||||
if (!covers.empty()) {
|
||||
set->covers = covers;
|
||||
}
|
||||
if (set->stickers.empty() && set->covers.empty()) {
|
||||
continue;
|
||||
}
|
||||
const auto externalLayout = true;
|
||||
appendSet(
|
||||
_officialSets,
|
||||
set->id,
|
||||
externalLayout,
|
||||
AppendSkip::Installed);
|
||||
}
|
||||
}
|
||||
});
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}).fail([=](const RPCError &error) {
|
||||
}).send();
|
||||
}
|
||||
|
||||
void StickersListWidget::readVisibleFeatured(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
const auto rowHeight = featuredRowHeight();
|
||||
const auto rowFrom = floorclamp(visibleTop, rowHeight, 0, _featuredSets.size());
|
||||
const auto rowTo = ceilclamp(visibleBottom, rowHeight, 0, _featuredSets.size());
|
||||
const auto rowFrom = floorclamp(visibleTop, rowHeight, 0, _featuredSetsCount);
|
||||
const auto rowTo = ceilclamp(visibleBottom, rowHeight, 0, _featuredSetsCount);
|
||||
for (auto i = rowFrom; i < rowTo; ++i) {
|
||||
auto &set = _featuredSets[i];
|
||||
auto &set = _officialSets[i];
|
||||
if (!(set.flags & MTPDstickerSet_ClientFlag::f_unread)) {
|
||||
continue;
|
||||
}
|
||||
@@ -935,7 +988,7 @@ void StickersListWidget::readVisibleFeatured(
|
||||
++loaded;
|
||||
}
|
||||
}
|
||||
if (loaded == count) {
|
||||
if (count > 0 && loaded == count) {
|
||||
session().api().readFeaturedSetDelayed(set.id);
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1280,7 @@ void StickersListWidget::addSearchRow(not_null<const Stickers::Set*> set) {
|
||||
|
||||
auto StickersListWidget::shownSets() const -> const std::vector<Set> & {
|
||||
switch (_section) {
|
||||
case Section::Featured: return _featuredSets;
|
||||
case Section::Featured: return _officialSets;
|
||||
case Section::Search: return _searchSets;
|
||||
case Section::Stickers: return _mySets;
|
||||
}
|
||||
@@ -1236,7 +1289,7 @@ auto StickersListWidget::shownSets() const -> const std::vector<Set> & {
|
||||
|
||||
auto StickersListWidget::shownSets() -> std::vector<Set> & {
|
||||
switch (_section) {
|
||||
case Section::Featured: return _featuredSets;
|
||||
case Section::Featured: return _officialSets;
|
||||
case Section::Search: return _searchSets;
|
||||
case Section::Stickers: return _mySets;
|
||||
}
|
||||
@@ -1349,10 +1402,11 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
||||
}
|
||||
auto &set = sets[info.section];
|
||||
if (set.externalLayout) {
|
||||
const auto size = (set.flags
|
||||
const auto loadedCount = int(set.stickers.size());
|
||||
const auto count = (set.flags
|
||||
& MTPDstickerSet_ClientFlag::f_not_loaded)
|
||||
? set.count
|
||||
: int(set.stickers.size());
|
||||
: loadedCount;
|
||||
|
||||
auto widthForTitle = stickersRight() - (st::emojiPanHeaderLeft - st::buttonRadius);
|
||||
if (featuredHasAddButton(info.section)) {
|
||||
@@ -1402,7 +1456,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
||||
}
|
||||
}
|
||||
|
||||
auto statusText = (size > 0) ? tr::lng_stickers_count(tr::now, lt_count, size) : tr::lng_contacts_loading(tr::now);
|
||||
auto statusText = (count > 0) ? tr::lng_stickers_count(tr::now, lt_count, count) : tr::lng_contacts_loading(tr::now);
|
||||
p.setFont(st::stickersTrendingSubheaderFont);
|
||||
p.setPen(st::stickersTrendingSubheaderFg);
|
||||
p.drawTextLeft(st::emojiPanHeaderLeft - st::buttonRadius, info.top + st::stickersTrendingSubheaderTop, width(), statusText);
|
||||
@@ -1413,7 +1467,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
|
||||
|
||||
for (int j = fromColumn; j < toColumn; ++j) {
|
||||
int index = j;
|
||||
if (index >= size) break;
|
||||
if (index >= loadedCount) break;
|
||||
|
||||
auto selected = selectedSticker ? (selectedSticker->section == info.section && selectedSticker->index == index) : false;
|
||||
auto deleteSelected = false;
|
||||
@@ -2116,12 +2170,33 @@ void StickersListWidget::refreshMySets() {
|
||||
}
|
||||
|
||||
void StickersListWidget::refreshFeaturedSets() {
|
||||
_featuredSets.clear();
|
||||
_featuredSets.reserve(session().data().featuredStickerSetsOrder().size());
|
||||
|
||||
auto wasFeaturedSetsCount = base::take(_featuredSetsCount);
|
||||
auto wereOfficial = base::take(_officialSets);
|
||||
_officialSets.reserve(
|
||||
session().data().featuredStickerSetsOrder().size()
|
||||
+ wereOfficial.size()
|
||||
- wasFeaturedSetsCount);
|
||||
for (const auto setId : session().data().featuredStickerSetsOrder()) {
|
||||
const auto externalLayout = true;
|
||||
appendSet(_featuredSets, setId, externalLayout, AppendSkip::Installed);
|
||||
appendSet(_officialSets, setId, externalLayout, AppendSkip::Installed);
|
||||
}
|
||||
_featuredSetsCount = _officialSets.size();
|
||||
if (wereOfficial.size() > wasFeaturedSetsCount) {
|
||||
auto &sets = session().data().stickerSets();
|
||||
const auto from = begin(wereOfficial) + wasFeaturedSetsCount;
|
||||
const auto till = end(wereOfficial);
|
||||
for (auto i = from; i != till; ++i) {
|
||||
auto &set = *i;
|
||||
auto it = sets.constFind(set.id);
|
||||
if (it == sets.cend()
|
||||
|| ((it->flags & MTPDstickerSet::Flag::f_installed_date)
|
||||
&& !(it->flags & MTPDstickerSet::Flag::f_archived)
|
||||
&& !_installedLocallySets.contains(set.id))) {
|
||||
continue;
|
||||
}
|
||||
set.flags = it->flags;
|
||||
_officialSets.push_back(std::move(set));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2247,28 +2322,27 @@ uint64 StickersListWidget::currentSet(int yOffset) const {
|
||||
: sets[sectionInfoByOffset(yOffset).section].id;
|
||||
}
|
||||
|
||||
void StickersListWidget::appendSet(
|
||||
bool StickersListWidget::appendSet(
|
||||
std::vector<Set> &to,
|
||||
uint64 setId,
|
||||
bool externalLayout,
|
||||
AppendSkip skip) {
|
||||
auto &sets = session().data().stickerSets();
|
||||
auto it = sets.constFind(setId);
|
||||
if (it == sets.cend() || it->stickers.isEmpty()) {
|
||||
return;
|
||||
if (it == sets.cend() || (!externalLayout && it->stickers.isEmpty())) {
|
||||
return false;
|
||||
}
|
||||
if ((skip == AppendSkip::Archived)
|
||||
&& (it->flags & MTPDstickerSet::Flag::f_archived)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if ((skip == AppendSkip::Installed)
|
||||
&& (it->flags & MTPDstickerSet::Flag::f_installed_date)
|
||||
&& !(it->flags & MTPDstickerSet::Flag::f_archived)) {
|
||||
if (!_installedLocallySets.contains(setId)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
to.emplace_back(
|
||||
it->id,
|
||||
it->flags,
|
||||
@@ -2277,7 +2351,10 @@ void StickersListWidget::appendSet(
|
||||
it->thumbnail,
|
||||
externalLayout,
|
||||
it->count,
|
||||
PrepareStickers(it->stickers));
|
||||
PrepareStickers((it->stickers.empty() && externalLayout)
|
||||
? it->covers
|
||||
: it->stickers));
|
||||
return true;
|
||||
}
|
||||
|
||||
void StickersListWidget::refreshRecent() {
|
||||
@@ -2487,8 +2564,7 @@ void StickersListWidget::refreshMegagroupStickers(GroupStickersPlace place) {
|
||||
void StickersListWidget::fillIcons(QList<StickerIcon> &icons) {
|
||||
icons.clear();
|
||||
icons.reserve(_mySets.size() + 1);
|
||||
if (session().data().featuredStickerSetsUnreadCount()
|
||||
&& !_featuredSets.empty()) {
|
||||
if (!_officialSets.empty()) {
|
||||
icons.push_back(StickerIcon(Stickers::FeaturedSetId));
|
||||
}
|
||||
|
||||
@@ -2535,11 +2611,6 @@ void StickersListWidget::fillIcons(QList<StickerIcon> &icons) {
|
||||
pixw,
|
||||
pixh));
|
||||
}
|
||||
|
||||
if (!session().data().featuredStickerSetsUnreadCount()
|
||||
&& !_featuredSets.empty()) {
|
||||
icons.push_back(StickerIcon(Stickers::FeaturedSetId));
|
||||
}
|
||||
}
|
||||
|
||||
bool StickersListWidget::preventAutoHide() {
|
||||
|
||||
@@ -179,6 +179,11 @@ private:
|
||||
bool externalLayout = false;
|
||||
int count = 0;
|
||||
};
|
||||
struct FeaturedSet {
|
||||
uint64 id = 0;
|
||||
MTPDstickerSet::Flags flags = MTPDstickerSet::Flags();
|
||||
std::vector<Sticker> stickers;
|
||||
};
|
||||
struct LottieSet {
|
||||
struct Item {
|
||||
not_null<Lottie::Animation*> animation;
|
||||
@@ -192,6 +197,7 @@ private:
|
||||
|
||||
static std::vector<Sticker> PrepareStickers(const Stickers::Pack &pack);
|
||||
|
||||
void preloadMoreOfficial();
|
||||
QSize boundingBoxSize() const;
|
||||
|
||||
template <typename Callback>
|
||||
@@ -273,7 +279,7 @@ private:
|
||||
Archived,
|
||||
Installed,
|
||||
};
|
||||
void appendSet(
|
||||
bool appendSet(
|
||||
std::vector<Set> &to,
|
||||
uint64 setId,
|
||||
bool externalLayout,
|
||||
@@ -303,13 +309,17 @@ private:
|
||||
ChannelData *_megagroupSet = nullptr;
|
||||
uint64 _megagroupSetIdRequested = 0;
|
||||
std::vector<Set> _mySets;
|
||||
std::vector<Set> _featuredSets;
|
||||
std::vector<Set> _officialSets;
|
||||
std::vector<Set> _searchSets;
|
||||
int _featuredSetsCount = 0;
|
||||
base::flat_set<uint64> _installedLocallySets;
|
||||
std::vector<bool> _custom;
|
||||
base::flat_set<not_null<DocumentData*>> _favedStickersMap;
|
||||
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
|
||||
|
||||
mtpRequestId _officialRequestId = 0;
|
||||
int _officialOffset = 0;
|
||||
|
||||
Section _section = Section::Stickers;
|
||||
|
||||
bool _displayingSet = false;
|
||||
|
||||
@@ -51,9 +51,14 @@ void HiddenUrlClickHandler::Open(QString url, QVariant context) {
|
||||
if (UrlRequiresConfirmation(url)
|
||||
&& QGuiApplication::keyboardModifiers() != Qt::ControlModifier) {
|
||||
Core::App().hideMediaView();
|
||||
const auto displayUrl = parsedUrl.isValid()
|
||||
const auto displayed = parsedUrl.isValid()
|
||||
? parsedUrl.toDisplayString()
|
||||
: url;
|
||||
const auto displayUrl = !IsSuspicious(displayed)
|
||||
? displayed
|
||||
: parsedUrl.isValid()
|
||||
? QString::fromUtf8(parsedUrl.toEncoded())
|
||||
: ShowEncoded(displayed);
|
||||
Ui::show(
|
||||
Box<ConfirmBox>(
|
||||
(tr::lng_open_this_link(tr::now)
|
||||
|
||||
@@ -42,9 +42,7 @@ PreLaunchWindow::PreLaunchWindow(QString title) {
|
||||
p.setColor(QPalette::Background, QColor(255, 255, 255));
|
||||
setPalette(p);
|
||||
|
||||
QLabel tmp(this);
|
||||
tmp.setText(qsl("Tmp"));
|
||||
_size = tmp.sizeHint().height();
|
||||
_size = QFontInfo(QApplication::font()).pixelSize();
|
||||
|
||||
int paddingVertical = (_size / 2);
|
||||
int paddingHorizontal = _size;
|
||||
@@ -74,7 +72,7 @@ PreLaunchWindow::~PreLaunchWindow() {
|
||||
|
||||
PreLaunchLabel::PreLaunchLabel(QWidget *parent) : QLabel(parent) {
|
||||
QFont labelFont(font());
|
||||
labelFont.setFamily(style::internal::GetFontOverride(qsl("Open Sans Semibold")));
|
||||
labelFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold));
|
||||
labelFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(labelFont);
|
||||
|
||||
@@ -92,7 +90,6 @@ void PreLaunchLabel::setText(const QString &text) {
|
||||
|
||||
PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(parent) {
|
||||
QFont logFont(font());
|
||||
logFont.setFamily(style::internal::GetFontOverride(qsl("Open Sans")));
|
||||
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(logFont);
|
||||
|
||||
@@ -110,7 +107,6 @@ PreLaunchInput::PreLaunchInput(QWidget *parent, bool password) : QLineEdit(paren
|
||||
|
||||
PreLaunchLog::PreLaunchLog(QWidget *parent) : QTextEdit(parent) {
|
||||
QFont logFont(font());
|
||||
logFont.setFamily(style::internal::GetFontOverride(qsl("Open Sans")));
|
||||
logFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(logFont);
|
||||
|
||||
@@ -132,7 +128,7 @@ PreLaunchButton::PreLaunchButton(QWidget *parent, bool confirm) : QPushButton(pa
|
||||
setObjectName(confirm ? "confirm" : "cancel");
|
||||
|
||||
QFont closeFont(font());
|
||||
closeFont.setFamily(style::internal::GetFontOverride(qsl("Open Sans Semibold")));
|
||||
closeFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold));
|
||||
closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(closeFont);
|
||||
|
||||
@@ -151,7 +147,7 @@ PreLaunchCheckbox::PreLaunchCheckbox(QWidget *parent) : QCheckBox(parent) {
|
||||
setCheckState(Qt::Checked);
|
||||
|
||||
QFont closeFont(font());
|
||||
closeFont.setFamily(style::internal::GetFontOverride(qsl("Open Sans Semibold")));
|
||||
closeFont.setFamily(style::internal::GetFontOverride(style::internal::FontSemibold));
|
||||
closeFont.setPixelSize(static_cast<PreLaunchWindow*>(parent)->basicSize());
|
||||
setFont(closeFont);
|
||||
|
||||
|
||||
@@ -427,7 +427,6 @@ void Manager::set(const QString &keys, Command command, bool replace) {
|
||||
} else if (replace) {
|
||||
unregister(std::exchange(i->second, std::move(shortcut)));
|
||||
} else {
|
||||
shortcut = nullptr;
|
||||
id = i->second->id();
|
||||
}
|
||||
if (!id) {
|
||||
@@ -435,10 +434,10 @@ void Manager::set(const QString &keys, Command command, bool replace) {
|
||||
return;
|
||||
}
|
||||
_commandByShortcutId.emplace(id, command);
|
||||
if (shortcut && isMediaShortcut) {
|
||||
if (!shortcut && isMediaShortcut) {
|
||||
_mediaShortcuts.emplace(i->second.get());
|
||||
}
|
||||
if (shortcut && isSupportShortcut) {
|
||||
if (!shortcut && isSupportShortcut) {
|
||||
_supportShortcuts.emplace(i->second.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
|
||||
const QString &data,
|
||||
const TextParseOptions &options) {
|
||||
switch (type) {
|
||||
case EntityType::Url:
|
||||
return (!data.isEmpty() && UrlClickHandler::IsSuspicious(data))
|
||||
? std::make_shared<HiddenUrlClickHandler>(data)
|
||||
: nullptr;
|
||||
|
||||
case EntityType::CustomUrl:
|
||||
return !data.isEmpty()
|
||||
? std::make_shared<HiddenUrlClickHandler>(data)
|
||||
|
||||
@@ -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 = 2000001;
|
||||
constexpr auto AppVersionStr = "2.0.1";
|
||||
constexpr auto AppVersion = 2001000;
|
||||
constexpr auto AppVersionStr = "2.1";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -1326,21 +1326,26 @@ std::unique_ptr<HistoryView::Media> MediaPoll::createView(
|
||||
return std::make_unique<HistoryView::Poll>(message, _poll);
|
||||
}
|
||||
|
||||
MediaDice::MediaDice(not_null<HistoryItem*> parent, int value)
|
||||
MediaDice::MediaDice(not_null<HistoryItem*> parent, QString emoji, int value)
|
||||
: Media(parent)
|
||||
, _emoji(emoji)
|
||||
, _value(value) {
|
||||
}
|
||||
|
||||
std::unique_ptr<Media> MediaDice::clone(not_null<HistoryItem*> parent) {
|
||||
return std::make_unique<MediaDice>(parent, _value);
|
||||
return std::make_unique<MediaDice>(parent, _emoji, _value);
|
||||
}
|
||||
|
||||
int MediaDice::diceValue() const {
|
||||
QString MediaDice::emoji() const {
|
||||
return _emoji;
|
||||
}
|
||||
|
||||
int MediaDice::value() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
QString MediaDice::notificationText() const {
|
||||
return QString::fromUtf8("\xF0\x9F\x8E\xB2");
|
||||
return _emoji;
|
||||
}
|
||||
|
||||
QString MediaDice::pinnedTextSubstring() const {
|
||||
|
||||
@@ -407,11 +407,12 @@ private:
|
||||
|
||||
class MediaDice final : public Media {
|
||||
public:
|
||||
MediaDice(not_null<HistoryItem*> parent, int value);
|
||||
MediaDice(not_null<HistoryItem*> parent, QString emoji, int value);
|
||||
|
||||
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
|
||||
|
||||
int diceValue() const;
|
||||
[[nodiscard]] QString emoji() const;
|
||||
[[nodiscard]] int value() const;
|
||||
|
||||
QString notificationText() const override;
|
||||
QString pinnedTextSubstring() const override;
|
||||
@@ -423,6 +424,7 @@ public:
|
||||
not_null<HistoryItem*> realParent) override;
|
||||
|
||||
private:
|
||||
QString _emoji;
|
||||
int _value = 0;
|
||||
|
||||
};
|
||||
|
||||
@@ -406,7 +406,8 @@ bool PeerData::canPinMessages() const {
|
||||
if (const auto user = asUser()) {
|
||||
return user->fullFlags() & MTPDuserFull::Flag::f_can_pin_message;
|
||||
} else if (const auto chat = asChat()) {
|
||||
return chat->amIn() && !chat->amRestricted(ChatRestriction::f_pin_messages);
|
||||
return chat->amIn()
|
||||
&& !chat->amRestricted(ChatRestriction::f_pin_messages);
|
||||
} else if (const auto channel = asChannel()) {
|
||||
return channel->isMegagroup()
|
||||
? !channel->amRestricted(ChatRestriction::f_pin_messages)
|
||||
@@ -416,6 +417,19 @@ bool PeerData::canPinMessages() const {
|
||||
Unexpected("Peer type in PeerData::canPinMessages.");
|
||||
}
|
||||
|
||||
bool PeerData::canEditMessagesIndefinitely() const {
|
||||
if (const auto user = asUser()) {
|
||||
return user->isSelf();
|
||||
} else if (const auto chat = asChat()) {
|
||||
return false;
|
||||
} else if (const auto channel = asChannel()) {
|
||||
return channel->isMegagroup()
|
||||
? channel->canPinMessages()
|
||||
: channel->canEditMessages();
|
||||
}
|
||||
Unexpected("Peer type in PeerData::canEditMessagesIndefinitely.");
|
||||
}
|
||||
|
||||
void PeerData::setPinnedMessageId(MsgId messageId) {
|
||||
const auto min = [&] {
|
||||
if (const auto channel = asChannel()) {
|
||||
|
||||
@@ -297,6 +297,7 @@ public:
|
||||
[[nodiscard]] ImagePtr currentUserpic() const;
|
||||
|
||||
[[nodiscard]] bool canPinMessages() const;
|
||||
[[nodiscard]] bool canEditMessagesIndefinitely() const;
|
||||
[[nodiscard]] MsgId pinnedMessageId() const {
|
||||
return _pinnedMessageId;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "apiwrap.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "main/main_session.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "ui/text_options.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kShortPollTimeout = 30 * crl::time(1000);
|
||||
constexpr auto kReloadAfterAutoCloseDelay = crl::time(1000);
|
||||
|
||||
const PollAnswer *AnswerByOption(
|
||||
const std::vector<PollAnswer> &list,
|
||||
@@ -41,6 +45,20 @@ PollData::PollData(not_null<Data::Session*> owner, PollId id)
|
||||
, _owner(owner) {
|
||||
}
|
||||
|
||||
bool PollData::closeByTimer() {
|
||||
if (closed()) {
|
||||
return false;
|
||||
}
|
||||
_flags |= Flag::Closed;
|
||||
++version;
|
||||
base::call_delayed(kReloadAfterAutoCloseDelay, &_owner->session(), [=] {
|
||||
_lastResultsUpdate = -1; // Force reload results.
|
||||
++version;
|
||||
_owner->notifyPollUpdateDelayed(this);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
Expects(poll.vid().v == id);
|
||||
|
||||
@@ -49,6 +67,8 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
| (poll.is_public_voters() ? Flag::PublicVotes : Flag(0))
|
||||
| (poll.is_multiple_choice() ? Flag::MultiChoice : Flag(0))
|
||||
| (poll.is_quiz() ? Flag::Quiz : Flag(0));
|
||||
const auto newCloseDate = poll.vclose_date().value_or_empty();
|
||||
const auto newClosePeriod = poll.vclose_period().value_or_empty();
|
||||
auto newAnswers = ranges::view::all(
|
||||
poll.vanswers().v
|
||||
) | ranges::view::transform([](const MTPPollAnswer &data) {
|
||||
@@ -63,6 +83,8 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
) | ranges::to_vector;
|
||||
|
||||
const auto changed1 = (question != newQuestion)
|
||||
|| (closeDate != newCloseDate)
|
||||
|| (closePeriod != newClosePeriod)
|
||||
|| (_flags != newFlags);
|
||||
const auto changed2 = (answers != newAnswers);
|
||||
if (!changed1 && !changed2) {
|
||||
@@ -70,6 +92,8 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
}
|
||||
if (changed1) {
|
||||
question = newQuestion;
|
||||
closeDate = newCloseDate;
|
||||
closePeriod = newClosePeriod;
|
||||
_flags = newFlags;
|
||||
}
|
||||
if (changed2) {
|
||||
@@ -88,7 +112,7 @@ bool PollData::applyChanges(const MTPDpoll &poll) {
|
||||
|
||||
bool PollData::applyResults(const MTPPollResults &results) {
|
||||
return results.match([&](const MTPDpollResults &results) {
|
||||
lastResultsUpdate = crl::now();
|
||||
_lastResultsUpdate = crl::now();
|
||||
|
||||
const auto newTotalVoters =
|
||||
results.vtotal_voters().value_or(totalVoters);
|
||||
@@ -123,6 +147,15 @@ bool PollData::applyResults(const MTPPollResults &results) {
|
||||
}) | ranges::to_vector;
|
||||
}
|
||||
}
|
||||
auto newSolution = TextWithEntities{
|
||||
results.vsolution().value_or_empty(),
|
||||
Api::EntitiesFromMTP(
|
||||
results.vsolution_entities().value_or_empty())
|
||||
};
|
||||
if (solution != newSolution) {
|
||||
solution = std::move(newSolution);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
return false;
|
||||
}
|
||||
@@ -132,13 +165,16 @@ bool PollData::applyResults(const MTPPollResults &results) {
|
||||
});
|
||||
}
|
||||
|
||||
void PollData::checkResultsReload(not_null<HistoryItem*> item, crl::time now) {
|
||||
if (lastResultsUpdate && lastResultsUpdate + kShortPollTimeout > now) {
|
||||
void PollData::checkResultsReload(
|
||||
not_null<HistoryItem*> item,
|
||||
crl::time now) {
|
||||
if (_lastResultsUpdate > 0
|
||||
&& _lastResultsUpdate + kShortPollTimeout > now) {
|
||||
return;
|
||||
} else if (closed()) {
|
||||
} else if (closed() && _lastResultsUpdate >= 0) {
|
||||
return;
|
||||
}
|
||||
lastResultsUpdate = now;
|
||||
_lastResultsUpdate = now;
|
||||
_owner->session().api().reloadPollResults(item);
|
||||
}
|
||||
|
||||
@@ -226,10 +262,49 @@ MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close) {
|
||||
const auto flags = ((poll->closed() || close) ? Flag::f_closed : Flag(0))
|
||||
| (poll->multiChoice() ? Flag::f_multiple_choice : Flag(0))
|
||||
| (poll->publicVotes() ? Flag::f_public_voters : Flag(0))
|
||||
| (poll->quiz() ? Flag::f_quiz : Flag(0));
|
||||
| (poll->quiz() ? Flag::f_quiz : Flag(0))
|
||||
| (poll->closePeriod > 0 ? Flag::f_close_period : Flag(0))
|
||||
| (poll->closeDate > 0 ? Flag::f_close_date : Flag(0));
|
||||
return MTP_poll(
|
||||
MTP_long(poll->id),
|
||||
MTP_flags(flags),
|
||||
MTP_string(poll->question),
|
||||
MTP_vector<MTPPollAnswer>(answers));
|
||||
MTP_vector<MTPPollAnswer>(answers),
|
||||
MTP_int(poll->closePeriod),
|
||||
MTP_int(poll->closeDate));
|
||||
}
|
||||
|
||||
MTPInputMedia PollDataToInputMedia(
|
||||
not_null<const PollData*> poll,
|
||||
bool close) {
|
||||
auto inputFlags = MTPDinputMediaPoll::Flag(0)
|
||||
| (poll->quiz()
|
||||
? MTPDinputMediaPoll::Flag::f_correct_answers
|
||||
: MTPDinputMediaPoll::Flag(0));
|
||||
auto correct = QVector<MTPbytes>();
|
||||
for (const auto &answer : poll->answers) {
|
||||
if (answer.correct) {
|
||||
correct.push_back(MTP_bytes(answer.option));
|
||||
}
|
||||
}
|
||||
|
||||
auto solution = poll->solution;
|
||||
const auto prepareFlags = Ui::ItemTextDefaultOptions().flags;
|
||||
TextUtilities::PrepareForSending(solution, prepareFlags);
|
||||
TextUtilities::Trim(solution);
|
||||
const auto sentEntities = Api::EntitiesToMTP(
|
||||
solution.entities,
|
||||
Api::ConvertOption::SkipLocal);
|
||||
if (!solution.text.isEmpty()) {
|
||||
inputFlags |= MTPDinputMediaPoll::Flag::f_solution;
|
||||
}
|
||||
if (!sentEntities.v.isEmpty()) {
|
||||
inputFlags |= MTPDinputMediaPoll::Flag::f_solution_entities;
|
||||
}
|
||||
return MTP_inputMediaPoll(
|
||||
MTP_flags(inputFlags),
|
||||
PollDataToMTP(poll, close),
|
||||
MTP_vector<MTPbytes>(correct),
|
||||
MTP_string(solution.text),
|
||||
sentEntities);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ struct PollData {
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
using Flags = base::flags<Flag>;
|
||||
|
||||
bool closeByTimer();
|
||||
bool applyChanges(const MTPDpoll &poll);
|
||||
bool applyResults(const MTPPollResults &results);
|
||||
void checkResultsReload(not_null<HistoryItem*> item, crl::time now);
|
||||
@@ -60,10 +61,11 @@ struct PollData {
|
||||
QString question;
|
||||
std::vector<PollAnswer> answers;
|
||||
std::vector<not_null<UserData*>> recentVoters;
|
||||
int totalVoters = 0;
|
||||
std::vector<QByteArray> sendingVotes;
|
||||
crl::time lastResultsUpdate = 0;
|
||||
|
||||
TextWithEntities solution;
|
||||
TimeId closePeriod = 0;
|
||||
TimeId closeDate = 0;
|
||||
int totalVoters = 0;
|
||||
int version = 0;
|
||||
|
||||
static constexpr auto kMaxOptions = 10;
|
||||
@@ -73,9 +75,15 @@ private:
|
||||
const MTPPollAnswerVoters &result,
|
||||
bool isMinResults);
|
||||
|
||||
not_null<Data::Session*> _owner;
|
||||
const not_null<Data::Session*> _owner;
|
||||
Flags _flags = Flags();
|
||||
crl::time _lastResultsUpdate = 0; // < 0 means force reload.
|
||||
|
||||
};
|
||||
|
||||
MTPPoll PollDataToMTP(not_null<const PollData*> poll, bool close = false);
|
||||
[[nodiscard]] MTPPoll PollDataToMTP(
|
||||
not_null<const PollData*> poll,
|
||||
bool close = false);
|
||||
[[nodiscard]] MTPInputMedia PollDataToInputMedia(
|
||||
not_null<const PollData*> poll,
|
||||
bool close = false);
|
||||
|
||||
@@ -191,6 +191,7 @@ Session::Session(not_null<Main::Session*> session)
|
||||
, _sendActionsAnimation([=](crl::time now) {
|
||||
return sendActionsAnimationCallback(now);
|
||||
})
|
||||
, _pollsClosingTimer([=] { checkPollsClosings(); })
|
||||
, _unmuteByFinishedTimer([=] { unmuteByFinished(); })
|
||||
, _groups(this)
|
||||
, _chatsFilters(std::make_unique<ChatFilters>(this))
|
||||
@@ -2877,6 +2878,10 @@ not_null<PollData*> Session::processPoll(const MTPPoll &data) {
|
||||
if (changed) {
|
||||
notifyPollUpdateDelayed(result);
|
||||
}
|
||||
if (result->closeDate > 0 && !result->closed()) {
|
||||
_pollsClosings.emplace(result->closeDate, result);
|
||||
checkPollsClosings();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@@ -2890,6 +2895,29 @@ not_null<PollData*> Session::processPoll(const MTPDmessageMediaPoll &data) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Session::checkPollsClosings() {
|
||||
const auto now = base::unixtime::now();
|
||||
auto closest = 0;
|
||||
for (auto i = _pollsClosings.begin(); i != _pollsClosings.end();) {
|
||||
if (i->first <= now) {
|
||||
if (i->second->closeByTimer()) {
|
||||
notifyPollUpdateDelayed(i->second);
|
||||
}
|
||||
i = _pollsClosings.erase(i);
|
||||
} else {
|
||||
if (!closest) {
|
||||
closest = i->first;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
if (closest) {
|
||||
_pollsClosingTimer.callOnce((closest - now) * crl::time(1000));
|
||||
} else {
|
||||
_pollsClosingTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void Session::applyUpdate(const MTPDupdateMessagePoll &update) {
|
||||
const auto updated = [&] {
|
||||
const auto poll = update.vpoll();
|
||||
|
||||
@@ -822,6 +822,8 @@ private:
|
||||
|
||||
void setWallpapers(const QVector<MTPWallPaper> &data, int32 hash);
|
||||
|
||||
void checkPollsClosings();
|
||||
|
||||
not_null<Main::Session*> _session;
|
||||
|
||||
Storage::DatabasePointer _cache;
|
||||
@@ -943,6 +945,9 @@ private:
|
||||
base::flat_set<not_null<GameData*>> _gamesUpdated;
|
||||
base::flat_set<not_null<PollData*>> _pollsUpdated;
|
||||
|
||||
base::flat_multi_map<TimeId, not_null<PollData*>> _pollsClosings;
|
||||
base::Timer _pollsClosingTimer;
|
||||
|
||||
base::flat_map<FolderId, std::unique_ptr<Folder>> _folders;
|
||||
//rpl::variable<FeedId> _defaultFeedId = FeedId(); // #feed
|
||||
|
||||
|
||||
@@ -1706,9 +1706,6 @@ void History::applyInboxReadUpdate(
|
||||
}
|
||||
|
||||
void History::inboxRead(MsgId upTo, std::optional<int> stillUnread) {
|
||||
if (unreadCount() > 0 && loadedAtBottom()) {
|
||||
App::main()->historyToDown(this);
|
||||
}
|
||||
if (stillUnread.has_value() && folderKnown()) {
|
||||
setUnreadCount(*stillUnread);
|
||||
} else if (const auto still = countStillUnreadLocal(upTo)) {
|
||||
|
||||
@@ -618,6 +618,15 @@ historyAudioInDownloadSelected: icon {{ "history_audio_download", historyFileInI
|
||||
historyAudioOutDownload: icon {{ "history_audio_download", historyFileOutIconFg }};
|
||||
historyAudioOutDownloadSelected: icon {{ "history_audio_download", historyFileOutIconFgSelected }};
|
||||
|
||||
historyQuizExplainIn: icon {{ "quiz_explain", msgFileThumbLinkInFg }};
|
||||
historyQuizExplainInSelected: icon {{ "quiz_explain", msgFileThumbLinkInFgSelected }};
|
||||
historyQuizExplainOut: icon {{ "quiz_explain", msgFileThumbLinkOutFg }};
|
||||
historyQuizExplainOutSelected: icon {{ "quiz_explain", msgFileThumbLinkOutFgSelected }};
|
||||
historyQuizTimerIn: icon {{ "quiz_timer", msgFileThumbLinkInFg }};
|
||||
historyQuizTimerInSelected: icon {{ "quiz_timer", msgFileThumbLinkInFgSelected }};
|
||||
historyQuizTimerOut: icon {{ "quiz_timer", msgFileThumbLinkOutFg }};
|
||||
historyQuizTimerOutSelected: icon {{ "quiz_timer", msgFileThumbLinkOutFgSelected }};
|
||||
|
||||
historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px);
|
||||
|
||||
largeEmojiSize: 36px;
|
||||
|
||||
@@ -768,15 +768,8 @@ bool HistoryMessage::allowsSendNow() const {
|
||||
}
|
||||
|
||||
bool HistoryMessage::isTooOldForEdit(TimeId now) const {
|
||||
const auto peer = _history->peer;
|
||||
if (peer->isSelf()) {
|
||||
return false;
|
||||
} else if (const auto megagroup = peer->asMegagroup()) {
|
||||
if (megagroup->canPinMessages()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (now - date() >= Global::EditTimeLimit());
|
||||
return !_history->peer->canEditMessagesIndefinitely()
|
||||
&& (now - date() >= Global::EditTimeLimit());
|
||||
}
|
||||
|
||||
bool HistoryMessage::allowsEdit(TimeId now) const {
|
||||
@@ -1025,7 +1018,10 @@ std::unique_ptr<Data::Media> HistoryMessage::CreateMedia(
|
||||
item,
|
||||
item->history()->owner().processPoll(media));
|
||||
}, [&](const MTPDmessageMediaDice &media) -> Result {
|
||||
return std::make_unique<Data::MediaDice>(item, media.vvalue().v);
|
||||
return std::make_unique<Data::MediaDice>(
|
||||
item,
|
||||
qs(media.vemoticon()),
|
||||
media.vvalue().v);
|
||||
}, [](const MTPDmessageMediaEmpty &) -> Result {
|
||||
return nullptr;
|
||||
}, [](const MTPDmessageMediaUnsupported &) -> Result {
|
||||
|
||||
@@ -238,7 +238,7 @@ void ShowErrorToast(const QString &text) {
|
||||
auto config = Ui::Toast::Config();
|
||||
config.multiline = true;
|
||||
config.minWidth = st::msgMinWidth;
|
||||
config.text = text;
|
||||
config.text = { text };
|
||||
Ui::Toast::Show(config);
|
||||
}
|
||||
|
||||
@@ -2913,7 +2913,26 @@ void HistoryWidget::historyDownClicked() {
|
||||
}
|
||||
|
||||
void HistoryWidget::showNextUnreadMention() {
|
||||
showHistory(_peer->id, _history->getMinLoadedUnreadMention());
|
||||
const auto msgId = _history->getMinLoadedUnreadMention();
|
||||
const auto already = (_showAtMsgId == msgId);
|
||||
|
||||
// Mark mention voice/video message as read.
|
||||
// See https://github.com/telegramdesktop/tdesktop/issues/5623
|
||||
if (msgId && already) {
|
||||
const auto item = _history->owner().message(
|
||||
_history->channelId(),
|
||||
msgId);
|
||||
if (const auto media = item ? item->media() : nullptr) {
|
||||
if (const auto document = media->document()) {
|
||||
if (!media->webpage()
|
||||
&& (document->isVoiceMessage()
|
||||
|| document->isVideoMessage())) {
|
||||
document->owner().markMediaRead(document);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showHistory(_peer->id, msgId);
|
||||
}
|
||||
|
||||
void HistoryWidget::saveEditMsg() {
|
||||
@@ -3083,6 +3102,8 @@ void HistoryWidget::send(Api::SendOptions options) {
|
||||
if (!_keyboard->hasMarkup() && _keyboard->forceReply() && !_kbReplyTo) {
|
||||
toggleKeyboard();
|
||||
}
|
||||
App::main()->historyToDown(_history);
|
||||
App::main()->dialogsToUp();
|
||||
}
|
||||
|
||||
void HistoryWidget::sendWithModifiers(Qt::KeyboardModifiers modifiers) {
|
||||
@@ -3628,7 +3649,7 @@ void HistoryWidget::botCallbackDone(
|
||||
updateSendAction(item->history(), SendAction::Type::PlayGame);
|
||||
}
|
||||
} else {
|
||||
UrlClickHandler(link).onClick({});
|
||||
UrlClickHandler::Open(link);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -6810,12 +6831,9 @@ void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int
|
||||
p.setFont(st::msgServiceNameFont);
|
||||
p.drawTextLeft(left, top + st::msgReplyPadding.top(), width(), tr::lng_edit_message(tr::now));
|
||||
|
||||
if (!_replyEditMsg || _replyEditMsg->history()->peer->isSelf()) return;
|
||||
|
||||
if (const auto megagroup = _replyEditMsg->history()->peer->asMegagroup()) {
|
||||
if (megagroup->amCreator() || megagroup->hasAdminRights()) {
|
||||
return;
|
||||
}
|
||||
if (!_replyEditMsg
|
||||
|| _replyEditMsg->history()->peer->canEditMessagesIndefinitely()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString editTimeLeftText;
|
||||
|
||||
@@ -51,7 +51,7 @@ void ShowErrorToast(const QString &text) {
|
||||
auto config = Ui::Toast::Config();
|
||||
config.multiline = true;
|
||||
config.minWidth = st::msgMinWidth;
|
||||
config.text = text;
|
||||
config.text = { text };
|
||||
Ui::Toast::Show(config);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,20 +9,56 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "data/data_session.h"
|
||||
#include "chat_helpers/stickers_dice_pack.h"
|
||||
#include "api/api_sending.h"
|
||||
#include "api/api_common.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
|
||||
DocumentData *Lookup(not_null<Element*> view, int value) {
|
||||
[[nodiscard]] DocumentData *Lookup(
|
||||
not_null<Element*> view,
|
||||
const QString &emoji,
|
||||
int value) {
|
||||
const auto &session = view->data()->history()->session();
|
||||
return session.diceStickersPack().lookup(value);
|
||||
return session.diceStickersPacks().lookup(emoji, value);
|
||||
}
|
||||
|
||||
[[nodiscard]] ClickHandlerPtr MakeDiceHandler(
|
||||
not_null<History*> history,
|
||||
const QString &emoji) {
|
||||
return std::make_shared<LambdaClickHandler>([=] {
|
||||
auto config = Ui::Toast::Config();
|
||||
config.multiline = true;
|
||||
config.minWidth = st::msgMinWidth;
|
||||
config.maxWidth = st::toastMaxWidth + st::msgMinWidth;
|
||||
config.text = { tr::lng_about_random(tr::now, lt_emoji, emoji) };
|
||||
config.durationMs = Ui::Toast::kDefaultDuration * 2;
|
||||
auto link = Ui::Text::Link(
|
||||
tr::lng_about_random_send(tr::now).toUpper());
|
||||
link.entities.push_back(
|
||||
EntityInText(EntityType::Bold, 0, link.text.size()));
|
||||
config.text.append(' ').append(std::move(link));
|
||||
config.filter = crl::guard(&history->session(), [=](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton button) {
|
||||
if (button == Qt::LeftButton) {
|
||||
auto message = Api::MessageToSend(history);
|
||||
message.action.clearDraft = false;
|
||||
message.textWithTags.text = emoji;
|
||||
Api::SendDice(message);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
Ui::Toast::Show(config);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -30,9 +66,14 @@ DocumentData *Lookup(not_null<Element*> view, int value) {
|
||||
Dice::Dice(not_null<Element*> parent, not_null<Data::MediaDice*> dice)
|
||||
: _parent(parent)
|
||||
, _dice(dice)
|
||||
, _start(parent, Lookup(parent, 0)) {
|
||||
, _link(_parent->data()->Has<HistoryMessageForwarded>()
|
||||
? nullptr
|
||||
: MakeDiceHandler(_parent->history(), dice->emoji())) {
|
||||
if (const auto document = Lookup(parent, dice->emoji(), 0)) {
|
||||
_start.emplace(parent, document);
|
||||
_start->setDiceIndex(_dice->emoji(), 0);
|
||||
}
|
||||
_showLastFrame = _parent->data()->Has<HistoryMessageForwarded>();
|
||||
_start.setDiceIndex(0);
|
||||
if (_showLastFrame) {
|
||||
_drawingEnd = true;
|
||||
}
|
||||
@@ -41,28 +82,27 @@ Dice::Dice(not_null<Element*> parent, not_null<Data::MediaDice*> dice)
|
||||
Dice::~Dice() = default;
|
||||
|
||||
QSize Dice::size() {
|
||||
return _start.size();
|
||||
return _start
|
||||
? _start->size()
|
||||
: Sticker::GetAnimatedEmojiSize(&_parent->history()->session());
|
||||
}
|
||||
|
||||
ClickHandlerPtr Dice::link() {
|
||||
if (_parent->data()->Has<HistoryMessageForwarded>()) {
|
||||
return nullptr;
|
||||
}
|
||||
static auto kHandler = std::make_shared<LambdaClickHandler>([] {
|
||||
auto config = Ui::Toast::Config();
|
||||
config.multiline = true;
|
||||
config.minWidth = st::msgMinWidth;
|
||||
config.text = tr::lng_about_dice(tr::now);
|
||||
Ui::Toast::Show(config);
|
||||
});
|
||||
return kHandler;
|
||||
return _link;
|
||||
}
|
||||
|
||||
void Dice::draw(Painter &p, const QRect &r, bool selected) {
|
||||
if (const auto value = _end ? 0 : _dice->diceValue()) {
|
||||
if (const auto document = Lookup(_parent, value)) {
|
||||
if (!_start) {
|
||||
if (const auto document = Lookup(_parent, _dice->emoji(), 0)) {
|
||||
_start.emplace(_parent, document);
|
||||
_start->setDiceIndex(_dice->emoji(), 0);
|
||||
_start->initSize();
|
||||
}
|
||||
}
|
||||
if (const auto value = _end ? 0 : _dice->value()) {
|
||||
if (const auto document = Lookup(_parent, _dice->emoji(), value)) {
|
||||
_end.emplace(_parent, document);
|
||||
_end->setDiceIndex(value);
|
||||
_end->setDiceIndex(_dice->emoji(), value);
|
||||
_end->initSize();
|
||||
}
|
||||
}
|
||||
@@ -71,9 +111,9 @@ void Dice::draw(Painter &p, const QRect &r, bool selected) {
|
||||
}
|
||||
if (_drawingEnd) {
|
||||
_end->draw(p, r, selected);
|
||||
} else {
|
||||
_start.draw(p, r, selected);
|
||||
if (_end && _end->readyToDrawLottie() && _start.atTheEnd()) {
|
||||
} else if (_start) {
|
||||
_start->draw(p, r, selected);
|
||||
if (_end && _end->readyToDrawLottie() && _start->atTheEnd()) {
|
||||
_drawingEnd = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ public:
|
||||
void clearStickerLoopPlayed() override {
|
||||
}
|
||||
void unloadHeavyPart() override {
|
||||
_start.unloadHeavyPart();
|
||||
if (_start) {
|
||||
_start->unloadHeavyPart();
|
||||
}
|
||||
if (_end) {
|
||||
_end->unloadHeavyPart();
|
||||
}
|
||||
@@ -41,8 +43,9 @@ public:
|
||||
private:
|
||||
const not_null<Element*> _parent;
|
||||
const not_null<Data::MediaDice*> _dice;
|
||||
ClickHandlerPtr _link;
|
||||
std::optional<Sticker> _start;
|
||||
std::optional<Sticker> _end;
|
||||
Sticker _start;
|
||||
mutable bool _showLastFrame = false;
|
||||
mutable bool _drawingEnd = false;
|
||||
|
||||
|
||||
@@ -267,8 +267,10 @@ void Gif::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms
|
||||
const auto selected = (selection == FullSelection);
|
||||
const auto autoPaused = App::wnd()->sessionController()->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
|
||||
const auto cornerDownload = downloadInCorner();
|
||||
const auto canBePlayed = _data->canBePlayed() && CanPlayInline(_data);
|
||||
const auto autoplay = autoplayEnabled() && canBePlayed;
|
||||
const auto canBePlayed = _data->canBePlayed();
|
||||
const auto autoplay = autoplayEnabled()
|
||||
&& canBePlayed
|
||||
&& CanPlayInline(_data);
|
||||
const auto activeRoundPlaying = activeRoundStreamed();
|
||||
const auto startPlay = autoplay
|
||||
&& !_streamed
|
||||
@@ -871,8 +873,11 @@ void Gif::drawGrouped(
|
||||
const auto autoPaused = App::wnd()->sessionController()->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
|
||||
const auto fullFeatured = fullFeaturedGrouped(sides);
|
||||
const auto cornerDownload = fullFeatured && downloadInCorner();
|
||||
const auto canBePlayed = _data->canBePlayed() && CanPlayInline(_data);;
|
||||
const auto autoplay = fullFeatured && autoplayEnabled() && canBePlayed;
|
||||
const auto canBePlayed = _data->canBePlayed();
|
||||
const auto autoplay = fullFeatured
|
||||
&& autoplayEnabled()
|
||||
&& canBePlayed
|
||||
&& CanPlayInline(_data);
|
||||
const auto startPlay = autoplay && !_streamed;
|
||||
if (startPlay) {
|
||||
const_cast<Gif*>(this)->playAnimation(true);
|
||||
|
||||
@@ -19,15 +19,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/effects/radial_animation.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/effects/fireworks_animation.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/timer.h"
|
||||
#include "layout.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_history.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
@@ -38,6 +42,8 @@ constexpr auto kRotateAmplitude = 3.;
|
||||
constexpr auto kScaleSegments = 2;
|
||||
constexpr auto kScaleAmplitude = 0.03;
|
||||
constexpr auto kRollDuration = crl::time(400);
|
||||
constexpr auto kLargestRadialDuration = 30 * crl::time(1000);
|
||||
constexpr auto kCriticalCloseDuration = 5 * crl::time(1000);
|
||||
|
||||
struct PercentCounterItem {
|
||||
int index = 0;
|
||||
@@ -116,6 +122,13 @@ void CountNicePercent(
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] crl::time CountToastDuration(const TextWithEntities &text) {
|
||||
return std::clamp(
|
||||
crl::time(1000) * text.text.size() / 14,
|
||||
crl::time(1000) * 5,
|
||||
crl::time(1000) * 8);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Poll::AnswerAnimation {
|
||||
@@ -161,6 +174,16 @@ struct Poll::Answer {
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
};
|
||||
|
||||
struct Poll::CloseInformation {
|
||||
CloseInformation(TimeId date, TimeId period, Fn<void()> repaint);
|
||||
|
||||
crl::time start = 0;
|
||||
crl::time finish = 0;
|
||||
crl::time duration = 0;
|
||||
base::Timer timer;
|
||||
Ui::Animations::Basic radial;
|
||||
};
|
||||
|
||||
template <typename Callback>
|
||||
Poll::SendingAnimation::SendingAnimation(
|
||||
const QByteArray &option,
|
||||
@@ -188,6 +211,16 @@ void Poll::Answer::fillData(
|
||||
Ui::WebpageTextTitleOptions());
|
||||
}
|
||||
|
||||
Poll::CloseInformation::CloseInformation(
|
||||
TimeId date,
|
||||
TimeId period,
|
||||
Fn<void()> repaint)
|
||||
: duration(period * crl::time(1000))
|
||||
, timer(std::move(repaint)) {
|
||||
const auto left = std::clamp(date - base::unixtime::now(), 0, period);
|
||||
finish = crl::now() + left * crl::time(1000);
|
||||
}
|
||||
|
||||
Poll::Poll(
|
||||
not_null<Element*> parent,
|
||||
not_null<PollData*> poll)
|
||||
@@ -405,9 +438,23 @@ void Poll::checkQuizAnswered() {
|
||||
1.,
|
||||
kRollDuration,
|
||||
anim::linear);
|
||||
showSolution();
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::showSolution() const {
|
||||
if (_poll->solution.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto config = Ui::Toast::Config();
|
||||
config.multiline = config.dark = true;
|
||||
config.minWidth = st::msgMinWidth;
|
||||
config.maxWidth = st::windowMinWidth;
|
||||
config.text = _poll->solution;
|
||||
config.durationMs = CountToastDuration(config.text);
|
||||
Ui::Toast::Show(config);
|
||||
}
|
||||
|
||||
void Poll::updateRecentVoters() {
|
||||
auto &&sliced = ranges::view::all(
|
||||
_poll->recentVoters
|
||||
@@ -653,6 +700,8 @@ void Poll::draw(Painter &p, const QRect &r, TextSelection selection, crl::time m
|
||||
p.setPen(regular);
|
||||
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
|
||||
paintRecentVoters(p, padding.left() + _subtitle.maxWidth(), tshift, selection);
|
||||
paintCloseByTimer(p, padding.left() + paintw, tshift, selection);
|
||||
paintShowSolution(p, padding.left() + paintw, tshift, selection);
|
||||
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
|
||||
|
||||
const auto progress = _answersAnimation
|
||||
@@ -799,6 +848,103 @@ void Poll::paintRecentVoters(
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::paintCloseByTimer(
|
||||
Painter &p,
|
||||
int right,
|
||||
int top,
|
||||
TextSelection selection) const {
|
||||
if (!canVote() || _poll->closeDate <= 0 || _poll->closePeriod <= 0) {
|
||||
_close = nullptr;
|
||||
return;
|
||||
}
|
||||
if (!_close) {
|
||||
_close = std::make_unique<CloseInformation>(
|
||||
_poll->closeDate,
|
||||
_poll->closePeriod,
|
||||
[=] { history()->owner().requestViewRepaint(_parent); });
|
||||
}
|
||||
const auto now = crl::now();
|
||||
const auto left = std::max(_close->finish - now, crl::time(0));
|
||||
const auto radial = std::min(_close->duration, kLargestRadialDuration);
|
||||
if (!left) {
|
||||
_close->radial.stop();
|
||||
} else if (left < radial && !anim::Disabled()) {
|
||||
if (!_close->radial.animating()) {
|
||||
_close->radial.init([=] {
|
||||
history()->owner().requestViewRepaint(_parent);
|
||||
});
|
||||
_close->radial.start();
|
||||
}
|
||||
} else {
|
||||
_close->radial.stop();
|
||||
}
|
||||
const auto time = formatDurationText(int(std::ceil(left / 1000.)));
|
||||
const auto outbg = _parent->hasOutLayout();
|
||||
const auto selected = (selection == FullSelection);
|
||||
const auto &icon = selected
|
||||
? (outbg
|
||||
? st::historyQuizTimerOutSelected
|
||||
: st::historyQuizTimerInSelected)
|
||||
: (outbg ? st::historyQuizTimerOut : st::historyQuizTimerIn);
|
||||
const auto x = right - icon.width();
|
||||
const auto y = top
|
||||
+ (st::normalFont->height - icon.height()) / 2
|
||||
- st::lineWidth;
|
||||
const auto ®ular = (left < kCriticalCloseDuration)
|
||||
? st::boxTextFgError
|
||||
: selected
|
||||
? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected)
|
||||
: (outbg ? st::msgOutDateFg : st::msgInDateFg);
|
||||
p.setPen(regular);
|
||||
const auto timeWidth = st::normalFont->width(time);
|
||||
p.drawTextLeft(x - timeWidth, top, width(), time, timeWidth);
|
||||
if (left < radial) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto part = std::max(
|
||||
left / float64(radial),
|
||||
1. / FullArcLength);
|
||||
const auto length = int(std::round(FullArcLength * part));
|
||||
auto pen = regular->p;
|
||||
pen.setWidth(st::historyPollRadio.thickness);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
const auto size = icon.width() / 2;
|
||||
const auto left = (x + (icon.width() - size) / 2);
|
||||
const auto top = (y + (icon.height() - size) / 2) + st::lineWidth;
|
||||
p.drawArc(left, top, size, size, (FullArcLength / 4), length);
|
||||
} else {
|
||||
icon.paint(p, x, y, width());
|
||||
}
|
||||
|
||||
if (left > (anim::Disabled() ? 0 : (radial - 1))) {
|
||||
const auto next = (left % 1000);
|
||||
_close->timer.callOnce((next ? next : 1000) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void Poll::paintShowSolution(
|
||||
Painter &p,
|
||||
int right,
|
||||
int top,
|
||||
TextSelection selection) const {
|
||||
if (!showVotes() || _poll->solution.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!_showSolutionLink) {
|
||||
_showSolutionLink = std::make_shared<LambdaClickHandler>(
|
||||
crl::guard(this, [=] { showSolution(); }));
|
||||
}
|
||||
const auto outbg = _parent->hasOutLayout();
|
||||
const auto &icon = (selection == FullSelection)
|
||||
? (outbg
|
||||
? st::historyQuizExplainOutSelected
|
||||
: st::historyQuizExplainInSelected)
|
||||
: (outbg ? st::historyQuizExplainOut : st::historyQuizExplainIn);
|
||||
const auto x = right - icon.width();
|
||||
const auto y = top + (st::normalFont->height - icon.height()) / 2;
|
||||
icon.paint(p, x, y, width());
|
||||
}
|
||||
|
||||
int Poll::paintAnswer(
|
||||
Painter &p,
|
||||
const Answer &answer,
|
||||
@@ -1111,6 +1257,10 @@ TextState Poll::textState(QPoint point, StateRequest request) const {
|
||||
paintw -= padding.left() + padding.right();
|
||||
|
||||
tshift += _question.countHeight(paintw) + st::historyPollSubtitleSkip;
|
||||
if (inShowSolution(point, padding.left() + paintw, tshift)) {
|
||||
result.link = _showSolutionLink;
|
||||
return result;
|
||||
}
|
||||
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
|
||||
const auto awidth = paintw
|
||||
- st::historyPollAnswerPadding.left()
|
||||
@@ -1244,6 +1394,23 @@ void Poll::toggleRipple(Answer &answer, bool pressed) {
|
||||
}
|
||||
}
|
||||
|
||||
bool Poll::canShowSolution() const {
|
||||
return showVotes() && !_poll->solution.text.isEmpty();
|
||||
}
|
||||
|
||||
bool Poll::inShowSolution(
|
||||
QPoint point,
|
||||
int right,
|
||||
int top) const {
|
||||
if (!canShowSolution()) {
|
||||
return false;
|
||||
}
|
||||
const auto &icon = st::historyQuizExplainIn;
|
||||
const auto x = right - icon.width();
|
||||
const auto y = top + (st::normalFont->height - icon.height()) / 2;
|
||||
return QRect(x, y, icon.width(), icon.height()).contains(point);
|
||||
}
|
||||
|
||||
int Poll::bottomButtonHeight() const {
|
||||
const auto skip = st::historyPollChoiceRight.height()
|
||||
- st::historyPollFillingBottom
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
Poll(
|
||||
not_null<Element*> parent,
|
||||
not_null<PollData*> poll);
|
||||
~Poll();
|
||||
|
||||
void draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms) const override;
|
||||
TextState textState(QPoint point, StateRequest request) const override;
|
||||
@@ -53,13 +54,12 @@ public:
|
||||
const ClickHandlerPtr &handler,
|
||||
bool pressed) override;
|
||||
|
||||
~Poll();
|
||||
|
||||
private:
|
||||
struct AnswerAnimation;
|
||||
struct AnswersAnimation;
|
||||
struct SendingAnimation;
|
||||
struct Answer;
|
||||
struct CloseInformation;
|
||||
|
||||
QSize countOptimalSize() override;
|
||||
QSize countCurrentSize(int newWidth) override;
|
||||
@@ -96,6 +96,16 @@ private:
|
||||
int left,
|
||||
int top,
|
||||
TextSelection selection) const;
|
||||
void paintCloseByTimer(
|
||||
Painter &p,
|
||||
int right,
|
||||
int top,
|
||||
TextSelection selection) const;
|
||||
void paintShowSolution(
|
||||
Painter &p,
|
||||
int right,
|
||||
int top,
|
||||
TextSelection selection) const;
|
||||
int paintAnswer(
|
||||
Painter &p,
|
||||
const Answer &answer,
|
||||
@@ -155,6 +165,13 @@ private:
|
||||
void sendMultiOptions();
|
||||
void showResults();
|
||||
void checkQuizAnswered();
|
||||
void showSolution() const;
|
||||
|
||||
[[nodiscard]] bool canShowSolution() const;
|
||||
[[nodiscard]] bool inShowSolution(
|
||||
QPoint point,
|
||||
int right,
|
||||
int top) const;
|
||||
|
||||
[[nodiscard]] int bottomButtonHeight() const;
|
||||
|
||||
@@ -173,6 +190,7 @@ private:
|
||||
Ui::Text::String _totalVotesLabel;
|
||||
ClickHandlerPtr _showResultsLink;
|
||||
ClickHandlerPtr _sendVotesLink;
|
||||
mutable ClickHandlerPtr _showSolutionLink;
|
||||
mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;
|
||||
|
||||
mutable std::unique_ptr<AnswersAnimation> _answersAnimation;
|
||||
@@ -181,6 +199,8 @@ private:
|
||||
Ui::Animations::Simple _wrongAnswerAnimation;
|
||||
mutable QPoint _lastLinkPoint;
|
||||
|
||||
mutable std::unique_ptr<CloseInformation> _close;
|
||||
|
||||
bool _hasSelected = false;
|
||||
bool _votedFromHere = false;
|
||||
mutable bool _wrongAnswerAnimated = false;
|
||||
|
||||
@@ -37,13 +37,17 @@ namespace {
|
||||
0.625);
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage CacheDiceImage(int index, const QImage &image) {
|
||||
static auto Cache = base::flat_map<int, QImage>();
|
||||
const auto i = Cache.find(index);
|
||||
[[nodiscard]] QImage CacheDiceImage(
|
||||
const QString &emoji,
|
||||
int index,
|
||||
const QImage &image) {
|
||||
static auto Cache = base::flat_map<std::pair<QString, int>, QImage>();
|
||||
const auto key = std::make_pair(emoji, index);
|
||||
const auto i = Cache.find(key);
|
||||
if (i != end(Cache) && i->second.size() == image.size()) {
|
||||
return i->second;
|
||||
}
|
||||
Cache[index] = image;
|
||||
Cache[key] = image;
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -70,12 +74,7 @@ bool Sticker::isEmojiSticker() const {
|
||||
void Sticker::initSize() {
|
||||
_size = _document->dimensions;
|
||||
if (isEmojiSticker() || _diceIndex >= 0) {
|
||||
constexpr auto kIdealStickerSize = 512;
|
||||
const auto zoom = GetEmojiStickerZoom(&_document->session());
|
||||
const auto convert = [&](int size) {
|
||||
return int(size * st::maxStickerSize * zoom / kIdealStickerSize);
|
||||
};
|
||||
_size = QSize(convert(_size.width()), convert(_size.height()));
|
||||
_size = GetAnimatedEmojiSize(&_document->session(), _size);
|
||||
[[maybe_unused]] bool result = readyToDrawLottie();
|
||||
} else {
|
||||
_size = DownscaledSize(
|
||||
@@ -106,6 +105,21 @@ bool Sticker::readyToDrawLottie() {
|
||||
return (_lottie && _lottie->ready());
|
||||
}
|
||||
|
||||
QSize Sticker::GetAnimatedEmojiSize(not_null<Main::Session*> session) {
|
||||
return GetAnimatedEmojiSize(session, { 512, 512 });
|
||||
}
|
||||
|
||||
QSize Sticker::GetAnimatedEmojiSize(
|
||||
not_null<Main::Session*> session,
|
||||
QSize documentSize) {
|
||||
constexpr auto kIdealStickerSize = 512;
|
||||
const auto zoom = GetEmojiStickerZoom(session);
|
||||
const auto convert = [&](int size) {
|
||||
return int(size * st::maxStickerSize * zoom / kIdealStickerSize);
|
||||
};
|
||||
return { convert(documentSize.width()), convert(documentSize.height()) };
|
||||
}
|
||||
|
||||
void Sticker::draw(Painter &p, const QRect &r, bool selected) {
|
||||
if (readyToDrawLottie()) {
|
||||
paintLottie(p, r, selected);
|
||||
@@ -126,7 +140,7 @@ void Sticker::paintLottie(Painter &p, const QRect &r, bool selected) {
|
||||
: Lottie::Animation::FrameInfo();
|
||||
if (_nextLastDiceFrame) {
|
||||
_nextLastDiceFrame = false;
|
||||
_lastDiceFrame = CacheDiceImage(_diceIndex, frame.image);
|
||||
_lastDiceFrame = CacheDiceImage(_diceEmoji, _diceIndex, frame.image);
|
||||
}
|
||||
const auto &image = _lastDiceFrame.isNull()
|
||||
? frame.image
|
||||
@@ -237,7 +251,8 @@ void Sticker::refreshLink() {
|
||||
}
|
||||
}
|
||||
|
||||
void Sticker::setDiceIndex(int index) {
|
||||
void Sticker::setDiceIndex(const QString &emoji, int index) {
|
||||
_diceEmoji = emoji;
|
||||
_diceIndex = index;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/view/media/history_view_media_unwrapped.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Data {
|
||||
struct FileOrigin;
|
||||
} // namespace Data
|
||||
@@ -49,12 +53,18 @@ public:
|
||||
}
|
||||
void refreshLink() override;
|
||||
|
||||
void setDiceIndex(int index);
|
||||
void setDiceIndex(const QString &emoji, int index);
|
||||
[[nodiscard]] bool atTheEnd() const {
|
||||
return _atTheEnd;
|
||||
}
|
||||
[[nodiscard]] bool readyToDrawLottie();
|
||||
|
||||
[[nodiscard]] static QSize GetAnimatedEmojiSize(
|
||||
not_null<Main::Session*> session);
|
||||
[[nodiscard]] static QSize GetAnimatedEmojiSize(
|
||||
not_null<Main::Session*> session,
|
||||
QSize documentSize);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool isEmojiSticker() const;
|
||||
void paintLottie(Painter &p, const QRect &r, bool selected);
|
||||
@@ -71,6 +81,7 @@ private:
|
||||
ClickHandlerPtr _link;
|
||||
QSize _size;
|
||||
QImage _lastDiceFrame;
|
||||
QString _diceEmoji;
|
||||
int _diceIndex = -1;
|
||||
mutable bool _lottieOncePlayed = false;
|
||||
mutable bool _atTheEnd = false;
|
||||
|
||||
@@ -127,7 +127,8 @@ QSize WebPage::countOptimalSize() {
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
_openl = previewOfHiddenUrl
|
||||
_openl = (previewOfHiddenUrl
|
||||
|| UrlClickHandler::IsSuspicious(_data->url))
|
||||
? std::make_shared<HiddenUrlClickHandler>(_data->url)
|
||||
: std::make_shared<UrlClickHandler>(_data->url, true);
|
||||
if (_data->document
|
||||
|
||||
@@ -579,6 +579,9 @@ ListController *CreateAnswerRows(
|
||||
headerWrap->resize(headerWrap->width(), height);
|
||||
}, header->lifetime());
|
||||
|
||||
auto moreTopWidget = object_ptr<Ui::RpWidget>(container);
|
||||
moreTopWidget->resize(0, 0);
|
||||
const auto moreTop = container->add(std::move(moreTopWidget));
|
||||
const auto more = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
|
||||
container,
|
||||
@@ -604,7 +607,7 @@ ListController *CreateAnswerRows(
|
||||
rpl::combine(
|
||||
std::move(visibleTop),
|
||||
headerWrap->geometryValue(),
|
||||
more->topValue()
|
||||
moreTop->topValue()
|
||||
) | rpl::filter([=](int, QRect headerRect, int moreTop) {
|
||||
return moreTop >= headerRect.y() + headerRect.height();
|
||||
}) | rpl::start_with_next([=](
|
||||
|
||||
@@ -341,9 +341,10 @@ void start(not_null<Core::Launcher*> launcher) {
|
||||
|
||||
if (cAlphaVersion()) {
|
||||
workingDirChosen = true;
|
||||
} else {
|
||||
|
||||
#if defined Q_OS_MAC || defined Q_OS_LINUX
|
||||
} else {
|
||||
|
||||
if (!cWorkingDir().isEmpty()) {
|
||||
// This value must come from TelegramForcePortable
|
||||
// or from the "-workdir" command line argument.
|
||||
@@ -362,11 +363,12 @@ void start(not_null<Core::Launcher*> launcher) {
|
||||
#endif // Q_OS_LINUX && !_DEBUG
|
||||
|
||||
#elif defined Q_OS_WINRT // Q_OS_MAC || Q_OS_LINUX
|
||||
} else {
|
||||
|
||||
cForceWorkingDir(psAppDataPath());
|
||||
workingDirChosen = true;
|
||||
|
||||
#elif defined OS_WIN_STORE // Q_OS_MAC || Q_OS_LINUX || Q_OS_WINRT
|
||||
|
||||
#ifdef _DEBUG
|
||||
cForceWorkingDir(cExeDir());
|
||||
#else // _DEBUG
|
||||
@@ -375,7 +377,7 @@ void start(not_null<Core::Launcher*> launcher) {
|
||||
workingDirChosen = true;
|
||||
|
||||
#elif defined Q_OS_WIN
|
||||
} else {
|
||||
|
||||
if (!cWorkingDir().isEmpty()) {
|
||||
// This value must come from TelegramForcePortable
|
||||
// or from the "-workdir" command line argument.
|
||||
@@ -384,6 +386,7 @@ void start(not_null<Core::Launcher*> launcher) {
|
||||
}
|
||||
|
||||
#endif // Q_OS_MAC || Q_OS_LINUX || Q_OS_WINRT || OS_WIN_STORE
|
||||
|
||||
}
|
||||
|
||||
LogsData = new LogsDataFields();
|
||||
|
||||
@@ -57,7 +57,7 @@ Session::Session(
|
||||
, _data(std::make_unique<Data::Session>(this))
|
||||
, _user(_data->processUser(user))
|
||||
, _emojiStickersPack(std::make_unique<Stickers::EmojiPack>(this))
|
||||
, _diceStickersPack(std::make_unique<Stickers::DicePack>(this))
|
||||
, _diceStickersPacks(std::make_unique<Stickers::DicePacks>(this))
|
||||
, _changelogs(Core::Changelogs::Create(this))
|
||||
, _supportHelper(Support::Helper::Create(this)) {
|
||||
Core::App().passcodeLockChanges(
|
||||
|
||||
@@ -46,7 +46,7 @@ class Instance;
|
||||
|
||||
namespace Stickers {
|
||||
class EmojiPack;
|
||||
class DicePack;
|
||||
class DicePacks;
|
||||
} // namespace Stickers;
|
||||
|
||||
namespace Core {
|
||||
@@ -93,8 +93,8 @@ public:
|
||||
[[nodiscard]] Stickers::EmojiPack &emojiStickersPack() const {
|
||||
return *_emojiStickersPack;
|
||||
}
|
||||
[[nodiscard]] Stickers::DicePack &diceStickersPack() const {
|
||||
return *_diceStickersPack;
|
||||
[[nodiscard]] Stickers::DicePacks &diceStickersPacks() const {
|
||||
return *_diceStickersPacks;
|
||||
}
|
||||
|
||||
[[nodiscard]] base::Observable<void> &downloaderTaskFinished();
|
||||
@@ -161,7 +161,7 @@ private:
|
||||
|
||||
// _emojiStickersPack depends on _data.
|
||||
const std::unique_ptr<Stickers::EmojiPack> _emojiStickersPack;
|
||||
const std::unique_ptr<Stickers::DicePack> _diceStickersPack;
|
||||
const std::unique_ptr<Stickers::DicePacks> _diceStickersPacks;
|
||||
|
||||
// _changelogs depends on _data, subscribes on chats loading event.
|
||||
const std::unique_ptr<Core::Changelogs> _changelogs;
|
||||
|
||||
@@ -702,6 +702,7 @@ bool MainWindow::takeThirdSectionFromLayer() {
|
||||
}
|
||||
|
||||
void MainWindow::fixOrder() {
|
||||
if (_passcodeLock) _passcodeLock->raise();
|
||||
if (_layer) _layer->raise();
|
||||
if (_mediaPreview) _mediaPreview->raise();
|
||||
if (_testingThemeWarning) _testingThemeWarning->raise();
|
||||
|
||||
@@ -314,7 +314,8 @@ void Mixer::Track::createStream(AudioMsgId::Type type) {
|
||||
alSource3f(stream.source, AL_POSITION, 0, 0, 0);
|
||||
alSource3f(stream.source, AL_VELOCITY, 0, 0, 0);
|
||||
alSourcei(stream.source, AL_LOOPING, 0);
|
||||
alSourcei(stream.source, AL_DIRECT_CHANNELS_SOFT, 1);
|
||||
alSourcei(stream.source, AL_SOURCE_RELATIVE, 1);
|
||||
alSourcei(stream.source, AL_ROLLOFF_FACTOR, 0);
|
||||
alGenBuffers(3, stream.buffers);
|
||||
#ifndef TDESKTOP_DISABLE_OPENAL_EFFECTS
|
||||
if (speedEffect) {
|
||||
|
||||
@@ -512,7 +512,7 @@ bool VideoTrackObject::tryReadFirstFrame(FFmpeg::Packet &&packet) {
|
||||
}
|
||||
|
||||
bool VideoTrackObject::processFirstFrame() {
|
||||
if (_stream.frame->width * _stream.frame->height >= kMaxFrameArea) {
|
||||
if (_stream.frame->width * _stream.frame->height > kMaxFrameArea) {
|
||||
return false;
|
||||
}
|
||||
auto frame = ConvertFrame(
|
||||
|
||||
@@ -2396,9 +2396,9 @@ void OverlayWidget::initThemePreview() {
|
||||
_themeShare->setClickedCallback([=] {
|
||||
QGuiApplication::clipboard()->setText(
|
||||
Core::App().createInternalLinkFull("addtheme/" + slug));
|
||||
auto config = Ui::Toast::Config();
|
||||
config.text = tr::lng_background_link_copied(tr::now);
|
||||
Ui::Toast::Show(this, config);
|
||||
Ui::Toast::Show(
|
||||
this,
|
||||
tr::lng_background_link_copied(tr::now));
|
||||
});
|
||||
} else {
|
||||
_themeShare.destroy();
|
||||
|
||||
@@ -482,6 +482,27 @@ mtpMsgId SessionPrivate::placeToContainer(
|
||||
return msgId;
|
||||
}
|
||||
|
||||
MTPVector<MTPJSONObjectValue> SessionPrivate::prepareInitParams() {
|
||||
const auto local = QDateTime::currentDateTime();
|
||||
const auto utc = QDateTime(local.date(), local.time(), Qt::UTC);
|
||||
const auto shift = base::unixtime::now() - (TimeId)::time(nullptr);
|
||||
const auto delta = int(utc.toTime_t()) - int(local.toTime_t()) - shift;
|
||||
auto sliced = delta;
|
||||
while (sliced < -12 * 3600) {
|
||||
sliced += 24 * 3600;
|
||||
}
|
||||
while (sliced > 14 * 3600) {
|
||||
sliced -= 24 * 3600;
|
||||
}
|
||||
const auto sign = (sliced < 0) ? -1 : 1;
|
||||
const auto rounded = std::round(std::abs(sliced) / 900.) * 900 * sign;
|
||||
return MTP_vector<MTPJSONObjectValue>(
|
||||
1,
|
||||
MTP_jsonObjectValue(
|
||||
MTP_string("tz_offset"),
|
||||
MTP_jsonNumber(MTP_double(rounded))));
|
||||
}
|
||||
|
||||
void SessionPrivate::tryToSend() {
|
||||
DEBUG_LOG(("MTP Info: tryToSend for dc %1.").arg(_shiftedDcId));
|
||||
if (!_connection) {
|
||||
@@ -612,7 +633,8 @@ void SessionPrivate::tryToSend() {
|
||||
: MTPInputClientProxy();
|
||||
using Flag = MTPInitConnection<SerializedRequest>::Flag;
|
||||
initWrapper = MTPInitConnection<SerializedRequest>(
|
||||
MTP_flags(mtprotoProxy ? Flag::f_proxy : Flag(0)),
|
||||
MTP_flags(Flag::f_params
|
||||
| (mtprotoProxy ? Flag::f_proxy : Flag(0))),
|
||||
MTP_int(ApiId),
|
||||
MTP_string(deviceModel),
|
||||
MTP_string(systemVersion),
|
||||
@@ -621,6 +643,7 @@ void SessionPrivate::tryToSend() {
|
||||
MTP_string(langPackName),
|
||||
MTP_string(cloudLangCode),
|
||||
clientProxyFields,
|
||||
MTP_jsonObject(prepareInitParams()),
|
||||
SerializedRequest());
|
||||
initSizeInInts = (tl::count_length(initWrapper) >> 2) + 2;
|
||||
initSize = initSizeInInts * sizeof(mtpPrime);
|
||||
@@ -2007,9 +2030,9 @@ void SessionPrivate::requestsAcked(const QVector<MTPlong> &ids, bool byResponse)
|
||||
} else {
|
||||
DEBUG_LOG(("Message Info: acked msgId %1 that was prepared to resend, requestId %2").arg(msgId).arg(requestId));
|
||||
}
|
||||
|
||||
|
||||
_ackedIds.emplace(msgId, j->second->requestId);
|
||||
|
||||
|
||||
toSend.erase(j);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ private:
|
||||
[[nodiscard]] uint32 nextRequestSeqNumber(bool needAck);
|
||||
|
||||
[[nodiscard]] bool realDcTypeChanged();
|
||||
[[nodiscard]] MTPVector<MTPJSONObjectValue> prepareInitParams();
|
||||
|
||||
const not_null<Instance*> _instance;
|
||||
const ShiftedDcId _shiftedDcId = 0;
|
||||
|
||||
@@ -1619,7 +1619,9 @@ const style::RoundCheckbox &Link::checkboxStyle() const {
|
||||
Link::LinkEntry::LinkEntry(const QString &url, const QString &text)
|
||||
: text(text)
|
||||
, width(st::normalFont->width(text))
|
||||
, lnk(std::make_shared<UrlClickHandler>(url)) {
|
||||
, lnk(UrlClickHandler::IsSuspicious(url)
|
||||
? std::make_shared<HiddenUrlClickHandler>(url)
|
||||
: std::make_shared<UrlClickHandler>(url)) {
|
||||
}
|
||||
|
||||
} // namespace Layout
|
||||
|
||||
@@ -238,9 +238,13 @@ QIcon TrayIconGen(int counter, bool muted) {
|
||||
bool IsIndicatorApplication() {
|
||||
// Hack for indicator-application, which doesn't handle icons sent across D-Bus:
|
||||
// save the icon to a temp file and set the icon name to that filename.
|
||||
static const auto IndicatorApplication = [&] {
|
||||
static const auto IndicatorApplication = [] {
|
||||
const auto interface = QDBusConnection::sessionBus().interface();
|
||||
|
||||
if (!interface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ubuntuIndicator = interface->isServiceRegistered(
|
||||
qsl("com.canonical.indicator.application"));
|
||||
|
||||
@@ -338,10 +342,15 @@ quint32 djbStringHash(QString string) {
|
||||
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
bool AppMenuSupported() {
|
||||
static const auto Available = QDBusInterface(
|
||||
kAppMenuService.utf16(),
|
||||
kAppMenuObjectPath.utf16(),
|
||||
kAppMenuInterface.utf16()).isValid();
|
||||
static const auto Available = []() -> bool {
|
||||
const auto interface = QDBusConnection::sessionBus().interface();
|
||||
|
||||
if (!interface) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return interface->isServiceRegistered(kAppMenuService.utf16());
|
||||
}();
|
||||
|
||||
return Available;
|
||||
}
|
||||
@@ -451,7 +460,7 @@ void MainWindow::initHook() {
|
||||
|
||||
bool MainWindow::hasTrayIcon() const {
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
return trayIcon || _sniTrayIcon;
|
||||
return trayIcon || (SNIAvailable && _sniTrayIcon);
|
||||
#else
|
||||
return trayIcon;
|
||||
#endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
@@ -545,12 +554,6 @@ void MainWindow::onSNIOwnerChanged(
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sniTrayIcon) {
|
||||
_sniTrayIcon->setContextMenu(0);
|
||||
_sniTrayIcon->deleteLater();
|
||||
}
|
||||
_sniTrayIcon = nullptr;
|
||||
|
||||
if (trayIcon) {
|
||||
trayIcon->setContextMenu(0);
|
||||
trayIcon->deleteLater();
|
||||
|
||||
@@ -130,7 +130,7 @@ bool RunShellCommand(const QByteArray &command) {
|
||||
|
||||
LOG(("Fontconfig version: %1.").arg(version.toString()));
|
||||
if (version < QVersionNumber::fromString("2.13")) {
|
||||
if (qgetenv("TDESKTOP_FORCE_CUSTOM_FONTCONFIG").isEmpty()) {
|
||||
if (!qEnvironmentVariableIsSet("TDESKTOP_FORCE_CUSTOM_FONTCONFIG")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -138,13 +138,24 @@ bool RunShellCommand(const QByteArray &command) {
|
||||
}
|
||||
|
||||
void FallbackFontConfig() {
|
||||
if (BadFontConfigVersion()) {
|
||||
const auto custom = cWorkingDir() + "tdata/fc-custom-1.conf";
|
||||
QFile(":/fc/fc-custom.conf").copy(custom);
|
||||
const auto custom = cWorkingDir() + "tdata/fc-custom-1.conf";
|
||||
|
||||
auto doFallback = [&] {
|
||||
if (QFile(custom).exists()) {
|
||||
LOG(("Custom FONTCONFIG_FILE: ") + custom);
|
||||
qputenv("FONTCONFIG_FILE", QFile::encodeName(custom));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (doFallback()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (BadFontConfigVersion()) {
|
||||
QFile(":/fc/fc-custom.conf").copy(custom);
|
||||
doFallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +707,7 @@ void psAutoStart(bool start, bool silent) {
|
||||
if (InSandbox()) {
|
||||
#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
SandboxAutostart(start, silent);
|
||||
#endif // !DESKTOP_APP_USE_PACKAGED
|
||||
#endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION
|
||||
} else {
|
||||
const auto autostart = [&] {
|
||||
if (InSnap()) {
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "platform/win/notifications_manager_win.h"
|
||||
#include "platform/win/windows_app_user_model_id.h"
|
||||
#include "platform/win/windows_dlls.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "mainwindow.h"
|
||||
#include "mainwidget.h"
|
||||
@@ -71,6 +72,8 @@ using namespace Platform;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kRefreshBadLastUserInputTimeout = 10 * crl::time(1000);
|
||||
|
||||
QStringList _initLogs;
|
||||
|
||||
bool themeInited = false;
|
||||
@@ -336,9 +339,51 @@ QString SingleInstanceLocalServerName(const QString &hash) {
|
||||
std::optional<crl::time> LastUserInputTime() {
|
||||
auto lii = LASTINPUTINFO{ 0 };
|
||||
lii.cbSize = sizeof(LASTINPUTINFO);
|
||||
return GetLastInputInfo(&lii)
|
||||
? std::make_optional(crl::now() + lii.dwTime - GetTickCount())
|
||||
: std::nullopt;
|
||||
if (!GetLastInputInfo(&lii)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto now = crl::now();
|
||||
const auto input = crl::time(lii.dwTime);
|
||||
static auto LastTrackedInput = input;
|
||||
static auto LastTrackedWhen = now;
|
||||
|
||||
const auto ticks32 = crl::time(GetTickCount());
|
||||
const auto ticks64 = crl::time(GetTickCount64());
|
||||
const auto elapsed = std::max(ticks32, ticks64) - input;
|
||||
const auto good = (std::abs(ticks32 - ticks64) <= crl::time(1000))
|
||||
&& (elapsed >= 0);
|
||||
if (good) {
|
||||
LastTrackedInput = input;
|
||||
LastTrackedWhen = now;
|
||||
return (now > elapsed) ? (now - elapsed) : crl::time(0);
|
||||
}
|
||||
|
||||
static auto WaitingDelayed = false;
|
||||
if (!WaitingDelayed) {
|
||||
WaitingDelayed = true;
|
||||
base::call_delayed(kRefreshBadLastUserInputTimeout, [=] {
|
||||
WaitingDelayed = false;
|
||||
[[maybe_unused]] const auto cheked = LastUserInputTime();
|
||||
});
|
||||
}
|
||||
constexpr auto OverrunLimit = std::numeric_limits<DWORD>::max();
|
||||
constexpr auto OverrunThreshold = OverrunLimit / 4;
|
||||
if (LastTrackedInput == input) {
|
||||
return LastTrackedWhen;
|
||||
}
|
||||
const auto guard = gsl::finally([&] {
|
||||
LastTrackedInput = input;
|
||||
LastTrackedWhen = now;
|
||||
});
|
||||
if (input > LastTrackedInput) {
|
||||
const auto add = input - LastTrackedInput;
|
||||
return std::min(LastTrackedWhen + add, now);
|
||||
} else if (crl::time(OverrunLimit) + input - LastTrackedInput
|
||||
< crl::time(OverrunThreshold)) {
|
||||
const auto add = crl::time(OverrunLimit) + input - LastTrackedInput;
|
||||
return std::min(LastTrackedWhen + add, now);
|
||||
}
|
||||
return LastTrackedWhen;
|
||||
}
|
||||
|
||||
} // namespace Platform
|
||||
|
||||
@@ -552,6 +552,7 @@ bool FileLoadTask::CheckForSong(
|
||||
qstr("audio/aac"),
|
||||
qstr("audio/ogg"),
|
||||
qstr("audio/flac"),
|
||||
qstr("audio/opus"),
|
||||
};
|
||||
static const auto extensions = {
|
||||
qstr(".mp3"),
|
||||
@@ -559,6 +560,8 @@ bool FileLoadTask::CheckForSong(
|
||||
qstr(".aac"),
|
||||
qstr(".ogg"),
|
||||
qstr(".flac"),
|
||||
qstr(".opus"),
|
||||
qstr(".oga"),
|
||||
};
|
||||
if (!filepath.isEmpty()
|
||||
&& !CheckMimeOrExtensions(
|
||||
|
||||
@@ -111,68 +111,62 @@ bool _userWorking() {
|
||||
return _manager && !_basePath.isEmpty() && !_userBasePath.isEmpty();
|
||||
}
|
||||
|
||||
enum class FileOption {
|
||||
User = (1 << 0),
|
||||
Safe = (1 << 1),
|
||||
enum class FileOwner {
|
||||
User = (1 << 0),
|
||||
Global = (1 << 1),
|
||||
};
|
||||
using FileOptions = base::flags<FileOption>;
|
||||
inline constexpr auto is_flag_type(FileOption) { return true; };
|
||||
|
||||
bool keyAlreadyUsed(QString &name, FileOptions options = FileOption::User | FileOption::Safe) {
|
||||
[[nodiscard]] bool KeyAlreadyUsed(QString &name) {
|
||||
name += '0';
|
||||
if (QFileInfo(name).exists()) {
|
||||
return true;
|
||||
}
|
||||
if (options & (FileOption::Safe)) {
|
||||
name[name.size() - 1] = '1';
|
||||
if (QFileInfo(name).exists()) {
|
||||
return true;
|
||||
}
|
||||
name[name.size() - 1] = 's';
|
||||
if (QFileInfo(name).exists()) {
|
||||
return true;
|
||||
}
|
||||
name[name.size() - 1] = '1';
|
||||
if (QFileInfo(name).exists()) {
|
||||
return true;
|
||||
}
|
||||
name[name.size() - 1] = 's';
|
||||
if (QFileInfo(name).exists()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FileKey genKey(FileOptions options = FileOption::User | FileOption::Safe) {
|
||||
if (options & FileOption::User) {
|
||||
[[nodiscard]] FileKey GenerateKey(FileOwner owner = FileOwner::User) {
|
||||
if (owner == FileOwner::User) {
|
||||
if (!_userWorking()) return 0;
|
||||
} else {
|
||||
if (!_working()) return 0;
|
||||
}
|
||||
|
||||
FileKey result;
|
||||
QString base = (options & FileOption::User) ? _userBasePath : _basePath, path;
|
||||
QString base = (owner == FileOwner::User) ? _userBasePath : _basePath, path;
|
||||
path.reserve(base.size() + 0x11);
|
||||
path += base;
|
||||
do {
|
||||
result = rand_value<FileKey>();
|
||||
path.resize(base.size());
|
||||
path += toFilePart(result);
|
||||
} while (!result || keyAlreadyUsed(path, options));
|
||||
} while (!result || KeyAlreadyUsed(path));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void clearKey(const FileKey &key, FileOptions options = FileOption::User | FileOption::Safe) {
|
||||
if (options & FileOption::User) {
|
||||
void ClearKey(const FileKey &key, FileOwner owner = FileOwner::User) {
|
||||
if (owner == FileOwner::User) {
|
||||
if (!_userWorking()) return;
|
||||
} else {
|
||||
if (!_working()) return;
|
||||
}
|
||||
|
||||
QString base = (options & FileOption::User) ? _userBasePath : _basePath, name;
|
||||
QString base = (owner == FileOwner::User) ? _userBasePath : _basePath, name;
|
||||
name.reserve(base.size() + 0x11);
|
||||
name.append(base).append(toFilePart(key)).append('0');
|
||||
QFile::remove(name);
|
||||
if (options & FileOption::Safe) {
|
||||
name[name.size() - 1] = '1';
|
||||
QFile::remove(name);
|
||||
name[name.size() - 1] = 's';
|
||||
QFile::remove(name);
|
||||
}
|
||||
name[name.size() - 1] = '1';
|
||||
QFile::remove(name);
|
||||
name[name.size() - 1] = 's';
|
||||
QFile::remove(name);
|
||||
}
|
||||
|
||||
bool _checkStreamStatus(QDataStream &stream) {
|
||||
@@ -251,31 +245,37 @@ struct EncryptedDescriptor {
|
||||
}
|
||||
};
|
||||
|
||||
struct FileWriteDescriptor {
|
||||
FileWriteDescriptor(
|
||||
class FileWriteDescriptor final {
|
||||
public:
|
||||
explicit FileWriteDescriptor(
|
||||
const FileKey &key,
|
||||
FileOptions options = FileOption::User | FileOption::Safe);
|
||||
FileWriteDescriptor(
|
||||
FileOwner owner = FileOwner::User);
|
||||
explicit FileWriteDescriptor(
|
||||
const QString &name,
|
||||
FileOptions options = FileOption::User | FileOption::Safe);
|
||||
FileOwner owner = FileOwner::User);
|
||||
~FileWriteDescriptor();
|
||||
|
||||
void init(const QString &name, FileOptions options);
|
||||
bool writeData(const QByteArray &data);
|
||||
bool writeEncrypted(
|
||||
void writeData(const QByteArray &data);
|
||||
void writeEncrypted(
|
||||
EncryptedDescriptor &data,
|
||||
const MTP::AuthKeyPtr &key = LocalKey);
|
||||
|
||||
private:
|
||||
void init(const QString &name);
|
||||
[[nodiscard]] QString path(char postfix) const;
|
||||
template <typename File>
|
||||
[[nodiscard]] bool open(File &file, char postfix);
|
||||
[[nodiscard]] bool writeHeader(QFileDevice &file);
|
||||
void writeFooter(QFileDevice &file);
|
||||
void finish();
|
||||
|
||||
QFile plainFile;
|
||||
QSaveFile saveFile;
|
||||
not_null<QFileDevice*> file;
|
||||
QDataStream stream;
|
||||
|
||||
QString toDelete;
|
||||
|
||||
HashMd5 md5;
|
||||
int32 dataSize = 0;
|
||||
const FileOwner _owner = FileOwner();
|
||||
QBuffer _buffer;
|
||||
QDataStream _stream;
|
||||
QByteArray _safeData;
|
||||
QString _base;
|
||||
HashMd5 _md5;
|
||||
int _fullSize = 0;
|
||||
|
||||
};
|
||||
|
||||
@@ -302,152 +302,168 @@ struct FileWriteDescriptor {
|
||||
|
||||
FileWriteDescriptor::FileWriteDescriptor(
|
||||
const FileKey &key,
|
||||
FileOptions options)
|
||||
: file((options & FileOption::Safe) ? (QFileDevice*)&saveFile : &plainFile) {
|
||||
init(toFilePart(key), options);
|
||||
FileOwner owner)
|
||||
: FileWriteDescriptor(toFilePart(key), owner) {
|
||||
}
|
||||
|
||||
FileWriteDescriptor::FileWriteDescriptor(
|
||||
const QString &name,
|
||||
FileOptions options)
|
||||
: file((options & FileOption::Safe) ? (QFileDevice*)&saveFile : &plainFile) {
|
||||
init(name, options);
|
||||
FileOwner owner)
|
||||
: _owner(owner) {
|
||||
init(name);
|
||||
}
|
||||
|
||||
FileWriteDescriptor::~FileWriteDescriptor() {
|
||||
finish();
|
||||
}
|
||||
|
||||
void FileWriteDescriptor::init(const QString &name, FileOptions options) {
|
||||
if (options & FileOption::User) {
|
||||
if (!_userWorking()) return;
|
||||
} else {
|
||||
if (!_working()) return;
|
||||
}
|
||||
|
||||
const auto base = ((options & FileOption::User) ? _userBasePath : _basePath) + name;
|
||||
saveFile.setFileName(base + 's');
|
||||
plainFile.setFileName(base + '0');
|
||||
if (options & FileOption::Safe) {
|
||||
toDelete = base;
|
||||
}
|
||||
if (!file->open(QIODevice::WriteOnly)) {
|
||||
LOG(("Storage Error: Could not open '%1' for writing.").arg(file->fileName()));
|
||||
if (!(options & FileOption::Safe)) {
|
||||
return;
|
||||
}
|
||||
file = &plainFile;
|
||||
LOG(("Storage Info: Trying to fallback to '%1'.").arg(file->fileName()));
|
||||
if (!file->open(QIODevice::WriteOnly)) {
|
||||
LOG(("Storage Error: Could not open '%1' for safe writing.").arg(file->fileName()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
file->write(tdfMagic, tdfMagicLen);
|
||||
qint32 version = AppVersion;
|
||||
file->write((const char*)&version, sizeof(version));
|
||||
|
||||
stream.setDevice(file);
|
||||
stream.setVersion(QDataStream::Qt_5_1);
|
||||
QString FileWriteDescriptor::path(char postfix) const {
|
||||
return _base + postfix;
|
||||
}
|
||||
|
||||
bool FileWriteDescriptor::writeData(const QByteArray &data) {
|
||||
if (!file->isOpen()) {
|
||||
template <typename File>
|
||||
bool FileWriteDescriptor::open(File &file, char postfix) {
|
||||
const auto name = path(postfix);
|
||||
file.setFileName(name);
|
||||
if (!writeHeader(file)) {
|
||||
LOG(("Storage Error: Could not open '%1' for writing.").arg(name));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
stream << data;
|
||||
bool FileWriteDescriptor::writeHeader(QFileDevice &file) {
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return false;
|
||||
}
|
||||
file.write(tdfMagic, tdfMagicLen);
|
||||
const auto version = qint32(AppVersion);
|
||||
file.write((const char*)&version, sizeof(version));
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileWriteDescriptor::writeFooter(QFileDevice &file) {
|
||||
file.write((const char*)_md5.result(), 0x10);
|
||||
}
|
||||
|
||||
void FileWriteDescriptor::init(const QString &name) {
|
||||
const auto working = (_owner == FileOwner::User)
|
||||
? _userWorking()
|
||||
: _working();
|
||||
if (!working) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto basePath = (_owner == FileOwner::User)
|
||||
? _userBasePath
|
||||
: _basePath;
|
||||
_base = basePath + name;
|
||||
_buffer.setBuffer(&_safeData);
|
||||
const auto opened = _buffer.open(QIODevice::WriteOnly);
|
||||
Assert(opened);
|
||||
_stream.setDevice(&_buffer);
|
||||
}
|
||||
|
||||
void FileWriteDescriptor::writeData(const QByteArray &data) {
|
||||
if (!_stream.device()) {
|
||||
return;
|
||||
}
|
||||
_stream << data;
|
||||
quint32 len = data.isNull() ? 0xffffffff : data.size();
|
||||
if (QSysInfo::ByteOrder != QSysInfo::BigEndian) {
|
||||
len = qbswap(len);
|
||||
}
|
||||
md5.feed(&len, sizeof(len));
|
||||
md5.feed(data.constData(), data.size());
|
||||
dataSize += sizeof(len) + data.size();
|
||||
|
||||
return true;
|
||||
_md5.feed(&len, sizeof(len));
|
||||
_md5.feed(data.constData(), data.size());
|
||||
_fullSize += sizeof(len) + data.size();
|
||||
}
|
||||
|
||||
bool FileWriteDescriptor::writeEncrypted(
|
||||
void FileWriteDescriptor::writeEncrypted(
|
||||
EncryptedDescriptor &data,
|
||||
const MTP::AuthKeyPtr &key) {
|
||||
return writeData(PrepareEncrypted(data, key));
|
||||
writeData(PrepareEncrypted(data, key));
|
||||
}
|
||||
|
||||
void FileWriteDescriptor::finish() {
|
||||
if (!file->isOpen()) {
|
||||
if (!_stream.device()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stream.setDevice(nullptr);
|
||||
|
||||
md5.feed(&dataSize, sizeof(dataSize));
|
||||
_stream.setDevice(nullptr);
|
||||
_md5.feed(&_fullSize, sizeof(_fullSize));
|
||||
qint32 version = AppVersion;
|
||||
md5.feed(&version, sizeof(version));
|
||||
md5.feed(tdfMagic, tdfMagicLen);
|
||||
file->write((const char*)md5.result(), 0x10);
|
||||
_md5.feed(&version, sizeof(version));
|
||||
_md5.feed(tdfMagic, tdfMagicLen);
|
||||
|
||||
if (saveFile.isOpen()) {
|
||||
if (!saveFile.commit()) {
|
||||
LOG(("Storage Error: Could not commit safe writing in '%1'."
|
||||
).arg(saveFile.fileName()));
|
||||
}
|
||||
} else {
|
||||
plainFile.close();
|
||||
if (!toDelete.isEmpty()) {
|
||||
QFile::remove(toDelete + '1');
|
||||
if (!base::Platform::RenameWithOverwrite(
|
||||
plainFile.fileName(),
|
||||
saveFile.fileName())) {
|
||||
LOG(("Storage Error: Could not rename '%1' to '%2'"
|
||||
).arg(plainFile.fileName()
|
||||
).arg(saveFile.fileName()));
|
||||
}
|
||||
_buffer.close();
|
||||
|
||||
const auto safe = path('s');
|
||||
const auto simple = path('0');
|
||||
const auto backup = path('1');
|
||||
QSaveFile save;
|
||||
if (open(save, 's')) {
|
||||
save.write(_safeData);
|
||||
writeFooter(save);
|
||||
if (save.commit()) {
|
||||
QFile::remove(simple);
|
||||
QFile::remove(backup);
|
||||
return;
|
||||
}
|
||||
LOG(("Storage Error: Could not commit '%1'.").arg(safe));
|
||||
}
|
||||
QFile plain;
|
||||
if (open(plain, '0')) {
|
||||
plain.write(_safeData);
|
||||
writeFooter(plain);
|
||||
base::Platform::FlushFileData(plain);
|
||||
plain.close();
|
||||
|
||||
if (!toDelete.isEmpty()) {
|
||||
QFile::remove(toDelete + '0');
|
||||
QFile::remove(toDelete + '1');
|
||||
QFile::remove(backup);
|
||||
if (base::Platform::RenameWithOverwrite(simple, safe)) {
|
||||
return;
|
||||
}
|
||||
QFile::remove(safe);
|
||||
LOG(("Storage Error: Could not rename '%1' to '%2', removing."
|
||||
).arg(simple
|
||||
).arg(safe));
|
||||
}
|
||||
}
|
||||
|
||||
bool readFile(FileReadDescriptor &result, const QString &name, FileOptions options = FileOption::User | FileOption::Safe) {
|
||||
if (options & FileOption::User) {
|
||||
bool ReadFile(
|
||||
FileReadDescriptor &result,
|
||||
const QString &name,
|
||||
FileOwner owner = FileOwner::User) {
|
||||
if (owner == FileOwner::User) {
|
||||
if (!_userWorking()) return false;
|
||||
} else {
|
||||
if (!_working()) return false;
|
||||
}
|
||||
|
||||
const auto base = ((options & FileOption::User) ? _userBasePath : _basePath) + name;
|
||||
const auto base = ((owner == FileOwner::User) ? _userBasePath : _basePath) + name;
|
||||
|
||||
// detect order of read attempts
|
||||
QString toTry[2];
|
||||
if (options & FileOption::Safe) {
|
||||
const auto modern = base + 's';
|
||||
if (QFileInfo(modern).exists()) {
|
||||
toTry[0] = modern;
|
||||
} else {
|
||||
// Legacy way.
|
||||
toTry[0] = base + '0';
|
||||
QFileInfo toTry0(toTry[0]);
|
||||
if (toTry0.exists()) {
|
||||
toTry[1] = ((options & FileOption::User) ? _userBasePath : _basePath) + name + '1';
|
||||
QFileInfo toTry1(toTry[1]);
|
||||
if (toTry1.exists()) {
|
||||
QDateTime mod0 = toTry0.lastModified(), mod1 = toTry1.lastModified();
|
||||
if (mod0 < mod1) {
|
||||
qSwap(toTry[0], toTry[1]);
|
||||
}
|
||||
} else {
|
||||
toTry[1] = QString();
|
||||
const auto modern = base + 's';
|
||||
if (QFileInfo(modern).exists()) {
|
||||
toTry[0] = modern;
|
||||
} else {
|
||||
// Legacy way.
|
||||
toTry[0] = base + '0';
|
||||
QFileInfo toTry0(toTry[0]);
|
||||
if (toTry0.exists()) {
|
||||
toTry[1] = ((owner == FileOwner::User) ? _userBasePath : _basePath) + name + '1';
|
||||
QFileInfo toTry1(toTry[1]);
|
||||
if (toTry1.exists()) {
|
||||
QDateTime mod0 = toTry0.lastModified(), mod1 = toTry1.lastModified();
|
||||
if (mod0 < mod1) {
|
||||
qSwap(toTry[0], toTry[1]);
|
||||
}
|
||||
} else {
|
||||
toTry[0][toTry[0].size() - 1] = '1';
|
||||
toTry[1] = QString();
|
||||
}
|
||||
} else {
|
||||
toTry[0][toTry[0].size() - 1] = '1';
|
||||
}
|
||||
} else {
|
||||
toTry[0] = base + '0';
|
||||
}
|
||||
for (int32 i = 0; i < 2; ++i) {
|
||||
QString fname(toTry[i]);
|
||||
@@ -555,8 +571,12 @@ bool decryptLocal(EncryptedDescriptor &result, const QByteArray &encrypted, cons
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readEncryptedFile(FileReadDescriptor &result, const QString &name, FileOptions options = FileOption::User | FileOption::Safe, const MTP::AuthKeyPtr &key = LocalKey) {
|
||||
if (!readFile(result, name, options)) {
|
||||
bool ReadEncryptedFile(
|
||||
FileReadDescriptor &result,
|
||||
const QString &name,
|
||||
FileOwner owner = FileOwner::User,
|
||||
const MTP::AuthKeyPtr &key = LocalKey) {
|
||||
if (!ReadFile(result, name, owner)) {
|
||||
return false;
|
||||
}
|
||||
QByteArray encrypted;
|
||||
@@ -585,8 +605,12 @@ bool readEncryptedFile(FileReadDescriptor &result, const QString &name, FileOpti
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readEncryptedFile(FileReadDescriptor &result, const FileKey &fkey, FileOptions options = FileOption::User | FileOption::Safe, const MTP::AuthKeyPtr &key = LocalKey) {
|
||||
return readEncryptedFile(result, toFilePart(fkey), options, key);
|
||||
bool ReadEncryptedFile(
|
||||
FileReadDescriptor &result,
|
||||
const FileKey &fkey,
|
||||
FileOwner owner = FileOwner::User,
|
||||
const MTP::AuthKeyPtr &key = LocalKey) {
|
||||
return ReadEncryptedFile(result, toFilePart(fkey), owner, key);
|
||||
}
|
||||
|
||||
FileKey _dataNameKey = 0;
|
||||
@@ -810,14 +834,14 @@ void _writeLocations(WriteMapWhen when = WriteMapWhen::Soon) {
|
||||
_manager->writingLocations();
|
||||
if (_fileLocations.isEmpty()) {
|
||||
if (_locationsKey) {
|
||||
clearKey(_locationsKey);
|
||||
ClearKey(_locationsKey);
|
||||
_locationsKey = 0;
|
||||
_mapChanged = true;
|
||||
_writeMap();
|
||||
}
|
||||
} else {
|
||||
if (!_locationsKey) {
|
||||
_locationsKey = genKey();
|
||||
_locationsKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -874,8 +898,8 @@ void _writeLocations(WriteMapWhen when = WriteMapWhen::Soon) {
|
||||
|
||||
void _readLocations() {
|
||||
FileReadDescriptor locations;
|
||||
if (!readEncryptedFile(locations, _locationsKey)) {
|
||||
clearKey(_locationsKey);
|
||||
if (!ReadEncryptedFile(locations, _locationsKey)) {
|
||||
ClearKey(_locationsKey);
|
||||
_locationsKey = 0;
|
||||
_writeMap();
|
||||
return;
|
||||
@@ -924,7 +948,7 @@ void _readLocations() {
|
||||
quint64 key;
|
||||
qint32 size;
|
||||
locations.stream >> url >> key >> size;
|
||||
clearKey(key, FileOption::User);
|
||||
ClearKey(key, FileOwner::User);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2136,7 +2160,7 @@ void _writeUserSettings() {
|
||||
LOG(("App Info: writing encrypted user settings..."));
|
||||
|
||||
if (!_userSettingsKey) {
|
||||
_userSettingsKey = genKey();
|
||||
_userSettingsKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -2231,7 +2255,7 @@ void _writeUserSettings() {
|
||||
void _readUserSettings() {
|
||||
ReadSettingsContext context;
|
||||
FileReadDescriptor userSettings;
|
||||
if (!readEncryptedFile(userSettings, _userSettingsKey)) {
|
||||
if (!ReadEncryptedFile(userSettings, _userSettingsKey)) {
|
||||
LOG(("App Info: could not read encrypted user settings..."));
|
||||
|
||||
_readOldUserSettings(true, context);
|
||||
@@ -2262,7 +2286,7 @@ void _readUserSettings() {
|
||||
}
|
||||
|
||||
void _writeMtpData() {
|
||||
FileWriteDescriptor mtp(toFilePart(_dataNameKey), FileOption::Safe);
|
||||
FileWriteDescriptor mtp(toFilePart(_dataNameKey), FileOwner::Global);
|
||||
if (!LocalKey) {
|
||||
LOG(("App Error: localkey not created in _writeMtpData()"));
|
||||
return;
|
||||
@@ -2280,7 +2304,7 @@ void _writeMtpData() {
|
||||
void _readMtpData() {
|
||||
ReadSettingsContext context;
|
||||
FileReadDescriptor mtp;
|
||||
if (!readEncryptedFile(mtp, toFilePart(_dataNameKey), FileOption::Safe)) {
|
||||
if (!ReadEncryptedFile(mtp, toFilePart(_dataNameKey), FileOwner::Global)) {
|
||||
if (LocalKey) {
|
||||
_readOldMtpData(true, context);
|
||||
applyReadContext(std::move(context));
|
||||
@@ -2318,7 +2342,7 @@ ReadMapState _readMap(const QByteArray &pass) {
|
||||
+ '/';
|
||||
|
||||
FileReadDescriptor mapData;
|
||||
if (!readFile(mapData, qsl("map"))) {
|
||||
if (!ReadFile(mapData, qsl("map"))) {
|
||||
return ReadMapFailed;
|
||||
}
|
||||
LOG(("App Info: reading map..."));
|
||||
@@ -2411,7 +2435,7 @@ ReadMapState _readMap(const QByteArray &pass) {
|
||||
} break;
|
||||
case lskReportSpamStatusesOld: {
|
||||
map.stream >> reportSpamStatusesKey;
|
||||
clearKey(reportSpamStatusesKey);
|
||||
ClearKey(reportSpamStatusesKey);
|
||||
} break;
|
||||
case lskTrustedBots: {
|
||||
map.stream >> trustedBotsKey;
|
||||
@@ -2666,7 +2690,7 @@ void start() {
|
||||
|
||||
ReadSettingsContext context;
|
||||
FileReadDescriptor settingsData;
|
||||
if (!readFile(settingsData, cTestMode() ? qsl("settings_test") : qsl("settings"), FileOption::Safe)) {
|
||||
if (!ReadFile(settingsData, cTestMode() ? qsl("settings_test") : qsl("settings"), FileOwner::Global)) {
|
||||
_readOldSettings(true, context);
|
||||
_readOldUserSettings(false, context); // needed further in _readUserSettings
|
||||
_readOldMtpData(false, context); // needed further in _readMtpData
|
||||
@@ -2727,7 +2751,9 @@ void writeSettings() {
|
||||
|
||||
if (!QDir().exists(_basePath)) QDir().mkpath(_basePath);
|
||||
|
||||
FileWriteDescriptor settings(cTestMode() ? qsl("settings_test") : qsl("settings"), FileOption::Safe);
|
||||
FileWriteDescriptor settings(
|
||||
cTestMode() ? qsl("settings_test") : qsl("settings"),
|
||||
FileOwner::Global);
|
||||
if (_settingsSalt.isEmpty() || !SettingsKey) {
|
||||
_settingsSalt.resize(LocalEncryptSaltSize);
|
||||
memset_rand(_settingsSalt.data(), _settingsSalt.size());
|
||||
@@ -3008,7 +3034,7 @@ void writeDrafts(const PeerId &peer, const MessageDraft &localDraft, const Messa
|
||||
if (localDraft.msgId <= 0 && localDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) {
|
||||
auto i = _draftsMap.find(peer);
|
||||
if (i != _draftsMap.cend()) {
|
||||
clearKey(i.value());
|
||||
ClearKey(i.value());
|
||||
_draftsMap.erase(i);
|
||||
_mapChanged = true;
|
||||
_writeMap();
|
||||
@@ -3018,7 +3044,7 @@ void writeDrafts(const PeerId &peer, const MessageDraft &localDraft, const Messa
|
||||
} else {
|
||||
auto i = _draftsMap.constFind(peer);
|
||||
if (i == _draftsMap.cend()) {
|
||||
i = _draftsMap.insert(peer, genKey());
|
||||
i = _draftsMap.insert(peer, GenerateKey());
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -3049,7 +3075,7 @@ void writeDrafts(const PeerId &peer, const MessageDraft &localDraft, const Messa
|
||||
void clearDraftCursors(const PeerId &peer) {
|
||||
DraftsMap::iterator i = _draftCursorsMap.find(peer);
|
||||
if (i != _draftCursorsMap.cend()) {
|
||||
clearKey(i.value());
|
||||
ClearKey(i.value());
|
||||
_draftCursorsMap.erase(i);
|
||||
_mapChanged = true;
|
||||
_writeMap();
|
||||
@@ -3063,7 +3089,7 @@ void _readDraftCursors(const PeerId &peer, MessageCursor &localCursor, MessageCu
|
||||
}
|
||||
|
||||
FileReadDescriptor draft;
|
||||
if (!readEncryptedFile(draft, j.value())) {
|
||||
if (!ReadEncryptedFile(draft, j.value())) {
|
||||
clearDraftCursors(peer);
|
||||
return;
|
||||
}
|
||||
@@ -3097,8 +3123,8 @@ void readDraftsWithCursors(History *h) {
|
||||
return;
|
||||
}
|
||||
FileReadDescriptor draft;
|
||||
if (!readEncryptedFile(draft, j.value())) {
|
||||
clearKey(j.value());
|
||||
if (!ReadEncryptedFile(draft, j.value())) {
|
||||
ClearKey(j.value());
|
||||
_draftsMap.erase(j);
|
||||
clearDraftCursors(peer);
|
||||
return;
|
||||
@@ -3126,7 +3152,7 @@ void readDraftsWithCursors(History *h) {
|
||||
}
|
||||
}
|
||||
if (draftPeer != peer) {
|
||||
clearKey(j.value());
|
||||
ClearKey(j.value());
|
||||
_draftsMap.erase(j);
|
||||
clearDraftCursors(peer);
|
||||
return;
|
||||
@@ -3172,7 +3198,7 @@ void writeDraftCursors(const PeerId &peer, const MessageCursor &msgCursor, const
|
||||
} else {
|
||||
DraftsMap::const_iterator i = _draftCursorsMap.constFind(peer);
|
||||
if (i == _draftCursorsMap.cend()) {
|
||||
i = _draftCursorsMap.insert(peer, genKey());
|
||||
i = _draftCursorsMap.insert(peer, GenerateKey());
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -3453,7 +3479,7 @@ void _writeStickerSets(FileKey &stickersKey, CheckSet checkSet, const Stickers::
|
||||
const auto &sets = Auth().data().stickerSets();
|
||||
if (sets.isEmpty()) {
|
||||
if (stickersKey) {
|
||||
clearKey(stickersKey);
|
||||
ClearKey(stickersKey);
|
||||
stickersKey = 0;
|
||||
_mapChanged = true;
|
||||
}
|
||||
@@ -3505,7 +3531,7 @@ void _writeStickerSets(FileKey &stickersKey, CheckSet checkSet, const Stickers::
|
||||
}
|
||||
if (!setsCount && order.isEmpty()) {
|
||||
if (stickersKey) {
|
||||
clearKey(stickersKey);
|
||||
ClearKey(stickersKey);
|
||||
stickersKey = 0;
|
||||
_mapChanged = true;
|
||||
}
|
||||
@@ -3515,7 +3541,7 @@ void _writeStickerSets(FileKey &stickersKey, CheckSet checkSet, const Stickers::
|
||||
size += sizeof(qint32) + (order.size() * sizeof(quint64));
|
||||
|
||||
if (!stickersKey) {
|
||||
stickersKey = genKey();
|
||||
stickersKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -3541,15 +3567,15 @@ void _writeStickerSets(FileKey &stickersKey, CheckSet checkSet, const Stickers::
|
||||
|
||||
void _readStickerSets(FileKey &stickersKey, Stickers::Order *outOrder = nullptr, MTPDstickerSet::Flags readingFlags = 0) {
|
||||
FileReadDescriptor stickers;
|
||||
if (!readEncryptedFile(stickers, stickersKey)) {
|
||||
clearKey(stickersKey);
|
||||
if (!ReadEncryptedFile(stickers, stickersKey)) {
|
||||
ClearKey(stickersKey);
|
||||
stickersKey = 0;
|
||||
_writeMap();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto failed = [&] {
|
||||
clearKey(stickersKey);
|
||||
ClearKey(stickersKey);
|
||||
stickersKey = 0;
|
||||
};
|
||||
|
||||
@@ -3841,8 +3867,8 @@ void importOldRecentStickers() {
|
||||
if (!_recentStickersKeyOld) return;
|
||||
|
||||
FileReadDescriptor stickers;
|
||||
if (!readEncryptedFile(stickers, _recentStickersKeyOld)) {
|
||||
clearKey(_recentStickersKeyOld);
|
||||
if (!ReadEncryptedFile(stickers, _recentStickersKeyOld)) {
|
||||
ClearKey(_recentStickersKeyOld);
|
||||
_recentStickersKeyOld = 0;
|
||||
_writeMap();
|
||||
return;
|
||||
@@ -3940,7 +3966,7 @@ void importOldRecentStickers() {
|
||||
writeInstalledStickers();
|
||||
writeUserSettings();
|
||||
|
||||
clearKey(_recentStickersKeyOld);
|
||||
ClearKey(_recentStickersKeyOld);
|
||||
_recentStickersKeyOld = 0;
|
||||
_writeMap();
|
||||
}
|
||||
@@ -4061,7 +4087,7 @@ void writeSavedGifs() {
|
||||
auto &saved = Auth().data().savedGifs();
|
||||
if (saved.isEmpty()) {
|
||||
if (_savedGifsKey) {
|
||||
clearKey(_savedGifsKey);
|
||||
ClearKey(_savedGifsKey);
|
||||
_savedGifsKey = 0;
|
||||
_mapChanged = true;
|
||||
}
|
||||
@@ -4073,7 +4099,7 @@ void writeSavedGifs() {
|
||||
}
|
||||
|
||||
if (!_savedGifsKey) {
|
||||
_savedGifsKey = genKey();
|
||||
_savedGifsKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -4091,8 +4117,8 @@ void readSavedGifs() {
|
||||
if (!_savedGifsKey) return;
|
||||
|
||||
FileReadDescriptor gifs;
|
||||
if (!readEncryptedFile(gifs, _savedGifsKey)) {
|
||||
clearKey(_savedGifsKey);
|
||||
if (!ReadEncryptedFile(gifs, _savedGifsKey)) {
|
||||
ClearKey(_savedGifsKey);
|
||||
_savedGifsKey = 0;
|
||||
_writeMap();
|
||||
return;
|
||||
@@ -4100,7 +4126,7 @@ void readSavedGifs() {
|
||||
|
||||
auto &saved = Auth().data().savedGifsRef();
|
||||
const auto failed = [&] {
|
||||
clearKey(_savedGifsKey);
|
||||
ClearKey(_savedGifsKey);
|
||||
_savedGifsKey = 0;
|
||||
saved.clear();
|
||||
};
|
||||
@@ -4168,7 +4194,7 @@ void writeBackground(const Data::WallPaper &paper, const QImage &image) {
|
||||
}
|
||||
}
|
||||
if (!backgroundKey) {
|
||||
backgroundKey = genKey();
|
||||
backgroundKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -4191,9 +4217,9 @@ bool readBackground() {
|
||||
auto &backgroundKey = Window::Theme::IsNightMode()
|
||||
? _backgroundKeyNight
|
||||
: _backgroundKeyDay;
|
||||
if (!readEncryptedFile(bg, backgroundKey)) {
|
||||
if (!ReadEncryptedFile(bg, backgroundKey)) {
|
||||
if (backgroundKey) {
|
||||
clearKey(backgroundKey);
|
||||
ClearKey(backgroundKey);
|
||||
backgroundKey = 0;
|
||||
_mapChanged = true;
|
||||
_writeMap();
|
||||
@@ -4308,7 +4334,7 @@ Window::Theme::Saved readThemeUsingKey(FileKey key) {
|
||||
using namespace Window::Theme;
|
||||
|
||||
FileReadDescriptor theme;
|
||||
if (!readEncryptedFile(theme, key, FileOption::Safe, SettingsKey)) {
|
||||
if (!ReadEncryptedFile(theme, key, FileOwner::Global, SettingsKey)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -4397,7 +4423,7 @@ void writeTheme(const Window::Theme::Saved &saved) {
|
||||
: _themeKeyDay;
|
||||
if (saved.object.content.isEmpty()) {
|
||||
if (themeKey) {
|
||||
clearKey(themeKey);
|
||||
ClearKey(themeKey, FileOwner::Global);
|
||||
themeKey = 0;
|
||||
writeSettings();
|
||||
}
|
||||
@@ -4405,7 +4431,7 @@ void writeTheme(const Window::Theme::Saved &saved) {
|
||||
}
|
||||
|
||||
if (!themeKey) {
|
||||
themeKey = genKey(FileOption::Safe);
|
||||
themeKey = GenerateKey(FileOwner::Global);
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
@@ -4442,7 +4468,7 @@ void writeTheme(const Window::Theme::Saved &saved) {
|
||||
<< cache.background
|
||||
<< quint32(cache.tiled ? 1 : 0);
|
||||
|
||||
FileWriteDescriptor file(themeKey, FileOption::Safe);
|
||||
FileWriteDescriptor file(themeKey, FileOwner::Global);
|
||||
file.writeEncrypted(data, SettingsKey);
|
||||
}
|
||||
|
||||
@@ -4494,7 +4520,7 @@ Window::Theme::Saved readThemeAfterSwitch() {
|
||||
|
||||
void readLangPack() {
|
||||
FileReadDescriptor langpack;
|
||||
if (!_langPackKey || !readEncryptedFile(langpack, _langPackKey, FileOption::Safe, SettingsKey)) {
|
||||
if (!_langPackKey || !ReadEncryptedFile(langpack, _langPackKey, FileOwner::Global, SettingsKey)) {
|
||||
return;
|
||||
}
|
||||
auto data = QByteArray();
|
||||
@@ -4507,21 +4533,21 @@ void readLangPack() {
|
||||
void writeLangPack() {
|
||||
auto langpack = Lang::Current().serialize();
|
||||
if (!_langPackKey) {
|
||||
_langPackKey = genKey(FileOption::Safe);
|
||||
_langPackKey = GenerateKey(FileOwner::Global);
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
EncryptedDescriptor data(Serialize::bytearraySize(langpack));
|
||||
data.stream << langpack;
|
||||
|
||||
FileWriteDescriptor file(_langPackKey, FileOption::Safe);
|
||||
FileWriteDescriptor file(_langPackKey, FileOwner::Global);
|
||||
file.writeEncrypted(data, SettingsKey);
|
||||
}
|
||||
|
||||
void saveRecentLanguages(const std::vector<Lang::Language> &list) {
|
||||
if (list.empty()) {
|
||||
if (_languagesKey) {
|
||||
clearKey(_languagesKey, FileOption::Safe);
|
||||
ClearKey(_languagesKey, FileOwner::Global);
|
||||
_languagesKey = 0;
|
||||
writeSettings();
|
||||
}
|
||||
@@ -4537,7 +4563,7 @@ void saveRecentLanguages(const std::vector<Lang::Language> &list) {
|
||||
+ Serialize::stringSize(language.nativeName);
|
||||
}
|
||||
if (!_languagesKey) {
|
||||
_languagesKey = genKey(FileOption::Safe);
|
||||
_languagesKey = GenerateKey(FileOwner::Global);
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
@@ -4552,7 +4578,7 @@ void saveRecentLanguages(const std::vector<Lang::Language> &list) {
|
||||
<< language.nativeName;
|
||||
}
|
||||
|
||||
FileWriteDescriptor file(_languagesKey, FileOption::Safe);
|
||||
FileWriteDescriptor file(_languagesKey, FileOwner::Global);
|
||||
file.writeEncrypted(data, SettingsKey);
|
||||
}
|
||||
|
||||
@@ -4584,7 +4610,7 @@ void removeRecentLanguage(const QString &id) {
|
||||
|
||||
std::vector<Lang::Language> readRecentLanguages() {
|
||||
FileReadDescriptor languages;
|
||||
if (!_languagesKey || !readEncryptedFile(languages, _languagesKey, FileOption::Safe, SettingsKey)) {
|
||||
if (!_languagesKey || !ReadEncryptedFile(languages, _languagesKey, FileOwner::Global, SettingsKey)) {
|
||||
return {};
|
||||
}
|
||||
qint32 count = 0;
|
||||
@@ -4619,7 +4645,7 @@ Window::Theme::Object ReadThemeContent() {
|
||||
}
|
||||
|
||||
FileReadDescriptor theme;
|
||||
if (!readEncryptedFile(theme, themeKey, FileOption::Safe, SettingsKey)) {
|
||||
if (!ReadEncryptedFile(theme, themeKey, FileOwner::Global, SettingsKey)) {
|
||||
return Object();
|
||||
}
|
||||
|
||||
@@ -4644,14 +4670,14 @@ void writeRecentHashtagsAndBots() {
|
||||
if (write.isEmpty() && search.isEmpty() && bots.isEmpty()) readRecentHashtagsAndBots();
|
||||
if (write.isEmpty() && search.isEmpty() && bots.isEmpty()) {
|
||||
if (_recentHashtagsAndBotsKey) {
|
||||
clearKey(_recentHashtagsAndBotsKey);
|
||||
ClearKey(_recentHashtagsAndBotsKey);
|
||||
_recentHashtagsAndBotsKey = 0;
|
||||
_mapChanged = true;
|
||||
}
|
||||
_writeMap();
|
||||
} else {
|
||||
if (!_recentHashtagsAndBotsKey) {
|
||||
_recentHashtagsAndBotsKey = genKey();
|
||||
_recentHashtagsAndBotsKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -4696,8 +4722,8 @@ void readRecentHashtagsAndBots() {
|
||||
if (!_recentHashtagsAndBotsKey) return;
|
||||
|
||||
FileReadDescriptor hashtags;
|
||||
if (!readEncryptedFile(hashtags, _recentHashtagsAndBotsKey)) {
|
||||
clearKey(_recentHashtagsAndBotsKey);
|
||||
if (!ReadEncryptedFile(hashtags, _recentHashtagsAndBotsKey)) {
|
||||
ClearKey(_recentHashtagsAndBotsKey);
|
||||
_recentHashtagsAndBotsKey = 0;
|
||||
_writeMap();
|
||||
return;
|
||||
@@ -4851,14 +4877,14 @@ void WriteExportSettings(const Export::Settings &settings) {
|
||||
&& settings.availableAt == check.availableAt
|
||||
&& !settings.onlySinglePeer()) {
|
||||
if (_exportSettingsKey) {
|
||||
clearKey(_exportSettingsKey);
|
||||
ClearKey(_exportSettingsKey);
|
||||
_exportSettingsKey = 0;
|
||||
_mapChanged = true;
|
||||
}
|
||||
_writeMap();
|
||||
} else {
|
||||
if (!_exportSettingsKey) {
|
||||
_exportSettingsKey = genKey();
|
||||
_exportSettingsKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -4905,8 +4931,8 @@ void WriteExportSettings(const Export::Settings &settings) {
|
||||
|
||||
Export::Settings ReadExportSettings() {
|
||||
FileReadDescriptor file;
|
||||
if (!readEncryptedFile(file, _exportSettingsKey)) {
|
||||
clearKey(_exportSettingsKey);
|
||||
if (!ReadEncryptedFile(file, _exportSettingsKey)) {
|
||||
ClearKey(_exportSettingsKey);
|
||||
_exportSettingsKey = 0;
|
||||
_writeMap();
|
||||
return Export::Settings();
|
||||
@@ -5006,14 +5032,14 @@ void writeTrustedBots() {
|
||||
|
||||
if (_trustedBots.isEmpty()) {
|
||||
if (_trustedBotsKey) {
|
||||
clearKey(_trustedBotsKey);
|
||||
ClearKey(_trustedBotsKey);
|
||||
_trustedBotsKey = 0;
|
||||
_mapChanged = true;
|
||||
_writeMap();
|
||||
}
|
||||
} else {
|
||||
if (!_trustedBotsKey) {
|
||||
_trustedBotsKey = genKey();
|
||||
_trustedBotsKey = GenerateKey();
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapWhen::Fast);
|
||||
}
|
||||
@@ -5033,8 +5059,8 @@ void readTrustedBots() {
|
||||
if (!_trustedBotsKey) return;
|
||||
|
||||
FileReadDescriptor trusted;
|
||||
if (!readEncryptedFile(trusted, _trustedBotsKey)) {
|
||||
clearKey(_trustedBotsKey);
|
||||
if (!ReadEncryptedFile(trusted, _trustedBotsKey)) {
|
||||
ClearKey(_trustedBotsKey);
|
||||
_trustedBotsKey = 0;
|
||||
_writeMap();
|
||||
return;
|
||||
|
||||
@@ -130,6 +130,11 @@ const auto kIcons = std::vector<FilterIcons>{
|
||||
&st::foldersWorkActive,
|
||||
"\xF0\x9F\x92\xBC"_cs.utf16()
|
||||
},
|
||||
{
|
||||
&st::filtersEdit,
|
||||
&st::filtersEdit,
|
||||
QString()
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -145,6 +150,9 @@ std::optional<FilterIcon> LookupFilterIconByEmoji(const QString &emoji) {
|
||||
auto result = base::flat_map<EmojiPtr, FilterIcon>();
|
||||
auto index = 0;
|
||||
for (const auto &entry : kIcons) {
|
||||
if (entry.emoji.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto emoji = Ui::Emoji::Find(entry.emoji);
|
||||
Assert(emoji != nullptr);
|
||||
result.emplace(emoji, static_cast<FilterIcon>(index++));
|
||||
|
||||
@@ -44,6 +44,8 @@ enum class FilterIcon : uchar {
|
||||
Trade,
|
||||
Travel,
|
||||
Work,
|
||||
|
||||
Edit,
|
||||
};
|
||||
|
||||
struct FilterIcons {
|
||||
|
||||
@@ -26,6 +26,8 @@ filtersCustomActive: icon {{ "filters/filters_custom_active", sideBarIconFgActiv
|
||||
filtersSetup: icon {{ "filters/filters_setup", sideBarIconFg }};
|
||||
filtersSetupActive: icon {{ "filters/filters_setup", sideBarIconFgActive }};
|
||||
|
||||
filtersEdit: icon {{ "filters/filters_edit", sideBarIconFg }};
|
||||
|
||||
foldersCat: icon {{ "filters/folders_cat", sideBarIconFg }};
|
||||
foldersCatActive: icon {{ "filters/folders_cat_active", sideBarIconFgActive }};
|
||||
foldersCrown: icon {{ "filters/folders_crown", sideBarIconFg }};
|
||||
|
||||
@@ -273,9 +273,7 @@ void SeparatePanel::showBox(
|
||||
}
|
||||
|
||||
void SeparatePanel::showToast(const QString &text) {
|
||||
auto toast = Ui::Toast::Config();
|
||||
toast.text = text;
|
||||
Ui::Toast::Show(this, toast);
|
||||
Ui::Toast::Show(this, text);
|
||||
}
|
||||
|
||||
void SeparatePanel::ensureLayerCreated() {
|
||||
|
||||
@@ -106,6 +106,7 @@ void Controller::showRightColumn(object_ptr<TWidget> widget) {
|
||||
void Controller::sideBarChanged() {
|
||||
_widget.setMinimumWidth(_widget.computeMinWidth());
|
||||
_widget.updateControlsGeometry();
|
||||
_widget.fixOrder();
|
||||
}
|
||||
|
||||
void Controller::activate() {
|
||||
|
||||
@@ -198,7 +198,7 @@ void FiltersMenu::setupList() {
|
||||
_container,
|
||||
-1,
|
||||
tr::lng_filters_setup(tr::now),
|
||||
Ui::FilterIcon::Setup);
|
||||
Ui::FilterIcon::Edit);
|
||||
_reorder = std::make_unique<Ui::VerticalLayoutReorder>(_list, &_scroll);
|
||||
|
||||
_reorder->updates(
|
||||
|
||||