Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b8ad171a | ||
|
|
4f6f654e34 | ||
|
|
ed50aa0d8e | ||
|
|
49480001f7 | ||
|
|
4ed6918a5e | ||
|
|
0563e1f878 | ||
|
|
17e8e0a7b0 | ||
|
|
96b2e26f42 | ||
|
|
c0142726f8 | ||
|
|
c52e914060 | ||
|
|
985956e625 | ||
|
|
facbaecf30 | ||
|
|
04c068d8b3 | ||
|
|
980ce9fba3 | ||
|
|
024a35d770 | ||
|
|
ab38ddc21d | ||
|
|
79cc4da626 | ||
|
|
92298316ab | ||
|
|
c9314e5e5e | ||
|
|
eadd952e66 | ||
|
|
fb2924f2d6 | ||
|
|
7dac42b523 | ||
|
|
d2defabd4b | ||
|
|
e0cc3791ff | ||
|
|
6ecc446a8a | ||
|
|
2668619758 | ||
|
|
5eba680483 | ||
|
|
7826d0246d | ||
|
|
189c940710 | ||
|
|
8b2bb722de | ||
|
|
a19e3ca3dc | ||
|
|
647cbc5464 | ||
|
|
131c2e1c56 | ||
|
|
81723a5d19 | ||
|
|
b3eb7858e6 | ||
|
|
4a0efb9114 | ||
|
|
f04b3da76a | ||
|
|
4a8b59b788 | ||
|
|
4f22171dd6 | ||
|
|
10adbecb9c | ||
|
|
a8564b166b | ||
|
|
cf6ca3b1ac | ||
|
|
ac02e2be9e | ||
|
|
5d2ffae215 | ||
|
|
1d120092cf | ||
|
|
2035392564 | ||
|
|
c4897cec0a | ||
|
|
dd462eb8cf | ||
|
|
71e8bda7bb | ||
|
|
ce1ae5ba12 | ||
|
|
6ae15485ad | ||
|
|
7a32d78689 | ||
|
|
cb84e70bdc | ||
|
|
b3925a3bec | ||
|
|
041d8571c2 | ||
|
|
367b487a6c | ||
|
|
57eb4f8234 | ||
|
|
ad4bf9b5c8 | ||
|
|
6ed7615653 | ||
|
|
cab22c07a5 | ||
|
|
ba3862e70f | ||
|
|
4970740739 | ||
|
|
cdb77d46b1 | ||
|
|
b6743feec1 | ||
|
|
fe5242d6d2 | ||
|
|
914e40fb62 | ||
|
|
326342420d | ||
|
|
478f5f671c | ||
|
|
43635f6e4b | ||
|
|
ebe1fa7408 | ||
|
|
5c006002b6 | ||
|
|
e7454e3849 | ||
|
|
3e4866d3b7 | ||
|
|
db564ca486 | ||
|
|
fd76b44dbd | ||
|
|
112dea8594 | ||
|
|
6d775d6f45 | ||
|
|
f7c6876e1b | ||
|
|
8845652f77 | ||
|
|
cd67cb1c62 | ||
|
|
b71f61dec3 | ||
|
|
e4d2a66f45 | ||
|
|
02eea38724 | ||
|
|
358228ce00 | ||
|
|
0089692b52 | ||
|
|
9d6e5f2a5b | ||
|
|
c3b638449a | ||
|
|
2df7e4181f | ||
|
|
b4cb47cf7f | ||
|
|
e4b9900a06 | ||
|
|
5c8a19b7f7 | ||
|
|
620c596200 | ||
|
|
d7ef484aec | ||
|
|
772bd81ea5 | ||
|
|
c8ce5dfa8b | ||
|
|
e64f6f7266 | ||
|
|
21133abe13 | ||
|
|
8b0fcee6a6 | ||
|
|
a768b65295 | ||
|
|
a68d9b4522 | ||
|
|
8a9317f9e1 | ||
|
|
db23485fa2 | ||
|
|
87e4bb1059 | ||
|
|
091b62bed4 | ||
|
|
167a73ef1b | ||
|
|
91a2ec225a | ||
|
|
3a45957ceb | ||
|
|
acaf8e4931 | ||
|
|
e0de4dbc5e | ||
|
|
876c57dcfb | ||
|
|
f980cade39 | ||
|
|
3d18d28dc5 | ||
|
|
e04598835b | ||
|
|
eee3049fdd | ||
|
|
d97dcaec62 | ||
|
|
05f43cabdf |
510
.github/workflows/linux.yml
vendored
@@ -45,71 +45,38 @@ on:
|
||||
jobs:
|
||||
|
||||
linux:
|
||||
name: Ubuntu 14.04
|
||||
name: CentOS 7
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:trusty
|
||||
container:
|
||||
image: docker.pkg.github.com/telegramdesktop/tdesktop/centos_env
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: scl enable devtoolset-8 -- bash --noprofile --norc -eo pipefail {0}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
defines:
|
||||
- ""
|
||||
- "DESKTOP_APP_DISABLE_DBUS_INTEGRATION"
|
||||
- "DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION"
|
||||
- "TDESKTOP_DISABLE_GTK_INTEGRATION"
|
||||
|
||||
env:
|
||||
GIT: "https://github.com"
|
||||
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"
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
MANUAL_CACHING: "6"
|
||||
DOC_PATH: "docs/building-cmake.md"
|
||||
AUTO_CACHING: "1"
|
||||
|
||||
steps:
|
||||
- name: Get repository name.
|
||||
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Disable man for further package installs.
|
||||
- name: Yum install.
|
||||
run: |
|
||||
cfgFile="/etc/dpkg/dpkg.cfg.d/no_man"
|
||||
sudo touch $cfgFile
|
||||
p() {
|
||||
sudo echo "path-exclude=/usr/share/$1/*" >> $cfgFile
|
||||
}
|
||||
|
||||
p man
|
||||
p locale
|
||||
p doc
|
||||
|
||||
- name: Apt install.
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install software-properties-common -y && \
|
||||
sudo add-apt-repository ppa:git-core/ppa -y && \
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install git libexif-dev liblzma-dev libz-dev libssl-dev \
|
||||
libgtk2.0-dev libice-dev libsm-dev libicu-dev libdrm-dev dh-autoreconf \
|
||||
autoconf automake build-essential libxml2-dev libass-dev libfreetype6-dev \
|
||||
libgpac-dev libsdl1.2-dev libtheora-dev libtool libva-dev libvdpau-dev \
|
||||
libvorbis-dev libxcb1-dev libxcb-image0-dev libxcb-shm0-dev \
|
||||
libxcb-screensaver0-dev libjpeg-dev ninja-build \
|
||||
libxcb-xfixes0-dev libxcb-keysyms1-dev libxcb-icccm4-dev libatspi2.0-dev \
|
||||
libxcb-render-util0-dev libxcb-util0-dev libxcb-xkb-dev libxrender-dev \
|
||||
libasound-dev libpulse-dev libxcb-sync0-dev libxcb-randr0-dev libegl1-mesa-dev \
|
||||
libx11-xcb-dev libffi-dev libncurses5-dev pkg-config texi2html bison yasm \
|
||||
zlib1g-dev xutils-dev python-xcbgen chrpath gperf wget -y --force-yes && \
|
||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y && \
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install gcc-8 g++-8 -y && \
|
||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 60 && \
|
||||
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-8 60 && \
|
||||
sudo update-alternatives --config gcc && \
|
||||
sudo add-apt-repository --remove ppa:ubuntu-toolchain-r/test -y
|
||||
yum -y autoremove git
|
||||
yum -y install https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.7-1.x86_64.rpm
|
||||
yum -y install git
|
||||
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v2
|
||||
@@ -118,452 +85,11 @@ jobs:
|
||||
path: ${{ env.REPO_NAME }}
|
||||
|
||||
- name: First set up.
|
||||
shell: bash
|
||||
run: |
|
||||
gcc --version
|
||||
|
||||
gcc --version > CACHE_KEY.txt
|
||||
echo $MANUAL_CACHING >> CACHE_KEY.txt
|
||||
if [ "$AUTO_CACHING" == "1" ]; then
|
||||
thisFile=$REPO_NAME/.github/workflows/linux.yml
|
||||
echo `md5sum $thisFile | cut -c -32` >> CACHE_KEY.txt
|
||||
fi
|
||||
md5cache=$(md5sum CACHE_KEY.txt | cut -c -32)
|
||||
echo "CACHE_KEY=$md5cache" >> $GITHUB_ENV
|
||||
|
||||
mkdir -p Libraries
|
||||
cd Libraries
|
||||
echo "LibrariesPath=`pwd`" >> $GITHUB_ENV
|
||||
|
||||
- name: Patches.
|
||||
run: |
|
||||
echo "Find necessary commit from doc."
|
||||
checkoutCommit=$(grep -A 1 "cd patches" $REPO_NAME/$DOC_PATH | sed -n 2p)
|
||||
cd $LibrariesPath
|
||||
git clone $GIT/desktop-app/patches.git
|
||||
cd patches
|
||||
eval $checkoutCommit
|
||||
|
||||
- name: CMake.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
file=cmake-$CMAKE_VER-Linux-x86_64.sh
|
||||
wget $GIT/Kitware/CMake/releases/download/v$CMAKE_VER/$file
|
||||
sudo mkdir /opt/cmake
|
||||
sudo sh $file --prefix=/opt/cmake --skip-license
|
||||
sudo ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake
|
||||
rm $file
|
||||
|
||||
cmake --version
|
||||
|
||||
- name: MozJPEG.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b v4.0.1-rc2 $GIT/mozilla/mozjpeg.git
|
||||
cd mozjpeg
|
||||
cmake -B build . \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
-DWITH_JPEG8=ON \
|
||||
-DPNG_SUPPORTED=OFF
|
||||
cmake --build build -j$(nproc)
|
||||
sudo cmake --install build
|
||||
cd ..
|
||||
rm -rf mozjpeg
|
||||
|
||||
- name: Opus cache.
|
||||
id: cache-opus
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/opus
|
||||
key: ${{ runner.OS }}-opus-${{ env.CACHE_KEY }}
|
||||
- name: Opus.
|
||||
if: steps.cache-opus.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b v1.3 --depth=1 $GIT/xiph/opus
|
||||
cd opus
|
||||
./autogen.sh
|
||||
./configure
|
||||
make -j$(nproc)
|
||||
- name: Opus install.
|
||||
run: |
|
||||
cd $LibrariesPath/opus
|
||||
sudo make install
|
||||
|
||||
- name: Libva.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone $GIT/intel/libva.git
|
||||
cd libva
|
||||
./autogen.sh --enable-static
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
cd ..
|
||||
rm -rf libva
|
||||
|
||||
- name: Libvdpau.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b libvdpau-1.2 --depth=1 https://gitlab.freedesktop.org/vdpau/libvdpau.git
|
||||
cd libvdpau
|
||||
./autogen.sh --enable-static
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
cd ..
|
||||
rm -rf libvdpau
|
||||
|
||||
- name: FFmpeg cache.
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/ffmpeg-cache
|
||||
key: ${{ runner.OS }}-ffmpeg-${{ env.CACHE_KEY }}
|
||||
- name: FFmpeg build.
|
||||
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone --branch release/3.4 $GIT/FFmpeg/FFmpeg ffmpeg
|
||||
cd ffmpeg
|
||||
./configure \
|
||||
--disable-debug \
|
||||
--disable-programs \
|
||||
--disable-doc \
|
||||
--disable-network \
|
||||
--disable-autodetect \
|
||||
--disable-everything \
|
||||
--disable-alsa \
|
||||
--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_fixed \
|
||||
--enable-decoder=aac_latm \
|
||||
--enable-decoder=aasc \
|
||||
--enable-decoder=alac \
|
||||
--enable-decoder=flac \
|
||||
--enable-decoder=gif \
|
||||
--enable-decoder=h264 \
|
||||
--enable-decoder=h264_vdpau \
|
||||
--enable-decoder=hevc \
|
||||
--enable-decoder=mp1 \
|
||||
--enable-decoder=mp1float \
|
||||
--enable-decoder=mp2 \
|
||||
--enable-decoder=mp2float \
|
||||
--enable-decoder=mp3 \
|
||||
--enable-decoder=mp3adu \
|
||||
--enable-decoder=mp3adufloat \
|
||||
--enable-decoder=mp3float \
|
||||
--enable-decoder=mp3on4 \
|
||||
--enable-decoder=mp3on4float \
|
||||
--enable-decoder=mpeg4 \
|
||||
--enable-decoder=mpeg4_vdpau \
|
||||
--enable-decoder=msmpeg4v2 \
|
||||
--enable-decoder=msmpeg4v3 \
|
||||
--enable-decoder=opus \
|
||||
--enable-decoder=pcm_alaw \
|
||||
--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_s16be \
|
||||
--enable-decoder=pcm_s16be_planar \
|
||||
--enable-decoder=pcm_s16le \
|
||||
--enable-decoder=pcm_s16le_planar \
|
||||
--enable-decoder=pcm_s24be \
|
||||
--enable-decoder=pcm_s24daud \
|
||||
--enable-decoder=pcm_s24le \
|
||||
--enable-decoder=pcm_s24le_planar \
|
||||
--enable-decoder=pcm_s32be \
|
||||
--enable-decoder=pcm_s32le \
|
||||
--enable-decoder=pcm_s32le_planar \
|
||||
--enable-decoder=pcm_s64be \
|
||||
--enable-decoder=pcm_s64le \
|
||||
--enable-decoder=pcm_s8 \
|
||||
--enable-decoder=pcm_s8_planar \
|
||||
--enable-decoder=pcm_u16be \
|
||||
--enable-decoder=pcm_u16le \
|
||||
--enable-decoder=pcm_u24be \
|
||||
--enable-decoder=pcm_u24le \
|
||||
--enable-decoder=pcm_u32be \
|
||||
--enable-decoder=pcm_u32le \
|
||||
--enable-decoder=pcm_u8 \
|
||||
--enable-decoder=pcm_zork \
|
||||
--enable-decoder=vorbis \
|
||||
--enable-decoder=wavpack \
|
||||
--enable-decoder=wmalossless \
|
||||
--enable-decoder=wmapro \
|
||||
--enable-decoder=wmav1 \
|
||||
--enable-decoder=wmav2 \
|
||||
--enable-decoder=wmavoice \
|
||||
--enable-encoder=libopus \
|
||||
--enable-parser=aac \
|
||||
--enable-parser=aac_latm \
|
||||
--enable-parser=flac \
|
||||
--enable-parser=h264 \
|
||||
--enable-parser=hevc \
|
||||
--enable-parser=mpeg4video \
|
||||
--enable-parser=mpegaudio \
|
||||
--enable-parser=opus \
|
||||
--enable-parser=vorbis \
|
||||
--enable-demuxer=aac \
|
||||
--enable-demuxer=flac \
|
||||
--enable-demuxer=gif \
|
||||
--enable-demuxer=h264 \
|
||||
--enable-demuxer=hevc \
|
||||
--enable-demuxer=m4v \
|
||||
--enable-demuxer=mov \
|
||||
--enable-demuxer=mp3 \
|
||||
--enable-demuxer=ogg \
|
||||
--enable-demuxer=wav \
|
||||
--enable-muxer=ogg \
|
||||
--enable-muxer=opus
|
||||
|
||||
make -j$(nproc)
|
||||
sudo make DESTDIR="$LibrariesPath/ffmpeg-cache" install
|
||||
cd ..
|
||||
rm -rf ffmpeg
|
||||
- name: FFmpeg install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
#List of files from cmake/external/ffmpeg/CMakeLists.txt.
|
||||
copyLib() {
|
||||
mkdir -p ffmpeg/$1
|
||||
yes | cp -i ffmpeg-cache/usr/local/lib/$1.a ffmpeg/$1/$1.a
|
||||
}
|
||||
copyLib libavformat
|
||||
copyLib libavcodec
|
||||
copyLib libswresample
|
||||
copyLib libswscale
|
||||
copyLib libavutil
|
||||
|
||||
sudo cp -R ffmpeg-cache/. /
|
||||
|
||||
- name: OpenAL Soft.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b openal-soft-1.20.1 --depth=1 $GIT/kcat/openal-soft.git
|
||||
cd openal-soft/build
|
||||
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 -
|
||||
rm -rf openal-soft
|
||||
|
||||
- name: OpenSSL cache.
|
||||
id: cache-openssl
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/openssl-cache
|
||||
key: ${{ runner.OS }}-${{ env.OPENSSL_VER }}-${{ env.CACHE_KEY }}
|
||||
- name: OpenSSL build.
|
||||
if: steps.cache-openssl.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
opensslDir=openssl_${OPENSSL_VER}
|
||||
git clone -b OpenSSL_${OPENSSL_VER}-stable --depth=1 \
|
||||
$GIT/openssl/openssl $opensslDir
|
||||
cd $opensslDir
|
||||
./config --prefix="$OPENSSL_PREFIX" no-tests
|
||||
make -j$(nproc)
|
||||
sudo make DESTDIR="$LibrariesPath/openssl-cache" install_sw
|
||||
cd ..
|
||||
# rm -rf $opensslDir # Keep this folder for WebRTC.
|
||||
- name: OpenSSL install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
sudo cp -R openssl-cache/. /
|
||||
|
||||
- name: Libwayland.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b 1.18.0 https://gitlab.freedesktop.org/wayland/wayland
|
||||
cd wayland
|
||||
./autogen.sh \
|
||||
--enable-static \
|
||||
--disable-documentation \
|
||||
--disable-dtd-validation
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
cd ..
|
||||
rm -rf wayland
|
||||
|
||||
- name: Libxkbcommon.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone -b xkbcommon-0.8.4 --depth=1 $GIT/xkbcommon/libxkbcommon.git
|
||||
cd libxkbcommon
|
||||
./autogen.sh \
|
||||
--disable-docs \
|
||||
--disable-wayland \
|
||||
--with-xkb-config-root=/usr/share/X11/xkb \
|
||||
--with-x-locale-root=/usr/share/X11/locale
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
cd ..
|
||||
rm -rf libxkbcommon
|
||||
|
||||
- name: Qt 5.12.8 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/qt-cache
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qt*_5_12_8/*') }}
|
||||
- name: Qt 5.12.8 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
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
|
||||
cd qtbase
|
||||
find ../../patches/qtbase_${QT} -type f -print0 | sort -z | xargs -r0 git apply
|
||||
cd ..
|
||||
cd qtwayland
|
||||
find ../../patches/qtwayland_${QT} -type f -print0 | sort -z | xargs -r0 git apply
|
||||
cd ..
|
||||
|
||||
./configure -prefix "$QT_PREFIX" \
|
||||
-release \
|
||||
-opensource \
|
||||
-confirm-license \
|
||||
-qt-zlib \
|
||||
-qt-libpng \
|
||||
-qt-harfbuzz \
|
||||
-qt-pcre \
|
||||
-qt-xcb \
|
||||
-no-icu \
|
||||
-no-gtk \
|
||||
-static \
|
||||
-dbus-runtime \
|
||||
-openssl-linked \
|
||||
-I "$OPENSSL_PREFIX/include" OPENSSL_LIBS="$OPENSSL_PREFIX/lib/libssl.a $OPENSSL_PREFIX/lib/libcrypto.a -ldl -lpthread" \
|
||||
-nomake examples \
|
||||
-nomake tests
|
||||
|
||||
make -j$(nproc)
|
||||
sudo make INSTALL_ROOT="$LibrariesPath/qt-cache" install
|
||||
cd ..
|
||||
rm -rf qt_${QT}
|
||||
- name: Qt 5.12.8 install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
sudo cp -R qt-cache/. /
|
||||
|
||||
- name: Breakpad cache.
|
||||
id: cache-breakpad
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/breakpad-cache
|
||||
key: ${{ runner.OS }}-breakpad-${{ env.CACHE_KEY }}
|
||||
- name: Breakpad clone.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone https://chromium.googlesource.com/breakpad/breakpad
|
||||
cd breakpad
|
||||
git checkout bc8fb886
|
||||
git clone https://chromium.googlesource.com/linux-syscall-support src/third_party/lss
|
||||
cd src/third_party/lss
|
||||
git checkout a91633d1
|
||||
- name: Breakpad build.
|
||||
if: steps.cache-breakpad.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
BreakpadCache=$LibrariesPath/breakpad-cache
|
||||
|
||||
git clone https://chromium.googlesource.com/external/gyp
|
||||
cd gyp
|
||||
git checkout 9f2a7bb1
|
||||
git apply ../patches/gyp.diff
|
||||
cd ..
|
||||
|
||||
cd breakpad
|
||||
./configure
|
||||
make -j$(nproc)
|
||||
sudo make DESTDIR="$BreakpadCache" install
|
||||
cd src
|
||||
rm -r testing
|
||||
git clone $GIT/google/googletest testing
|
||||
cd tools
|
||||
sed -i 's/minidump_upload.m/minidump_upload.cc/' linux/tools_linux.gypi
|
||||
../../../gyp/gyp --depth=. --generator-output=.. -Goutput_dir=../out tools.gyp --format=cmake
|
||||
cd ../../out/Default
|
||||
cmake .
|
||||
make -j$(nproc) dump_syms
|
||||
|
||||
mv dump_syms $BreakpadCache/
|
||||
cd ..
|
||||
rm -rf gyp breakpad
|
||||
- name: Breakpad install.
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
sudo cp -R breakpad-cache/. /
|
||||
mkdir -p breakpad/out/Default/
|
||||
cp breakpad-cache/dump_syms breakpad/out/Default/dump_syms
|
||||
|
||||
- name: WebRTC cache.
|
||||
id: cache-webrtc
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/tg_owt
|
||||
key: ${{ runner.OS }}-webrtc-${{ env.CACHE_KEY }}
|
||||
- name: WebRTC.
|
||||
if: steps.cache-webrtc.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone $GIT/desktop-app/tg_owt.git
|
||||
mkdir -p tg_owt/out/Debug
|
||||
cd tg_owt/out/Debug
|
||||
cmake -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DTG_OWT_SPECIAL_TARGET=linux \
|
||||
-DTG_OWT_LIBJPEG_INCLUDE_PATH=/usr/local/include \
|
||||
-DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_PREFIX/include \
|
||||
-DTG_OWT_OPUS_INCLUDE_PATH=/usr/local/include/opus \
|
||||
-DTG_OWT_FFMPEG_INCLUDE_PATH=/usr/local/include \
|
||||
../..
|
||||
ninja
|
||||
|
||||
# Cleanup.
|
||||
cd $LibrariesPath/tg_owt
|
||||
mv out/Debug/libtg_owt.a libtg_owt.a
|
||||
rm -rf out
|
||||
mkdir -p out/Debug
|
||||
mv libtg_owt.a out/Debug/libtg_owt.a
|
||||
|
||||
rm -rf $LibrariesPath/openssl_${OPENSSL_VER}
|
||||
ln -s $LibrariesPath Libraries
|
||||
|
||||
- name: Telegram Desktop build.
|
||||
if: env.ONLY_CACHE == 'false'
|
||||
run: |
|
||||
cd $REPO_NAME/Telegram
|
||||
|
||||
@@ -577,7 +103,8 @@ jobs:
|
||||
fi
|
||||
|
||||
./configure.sh \
|
||||
-D CMAKE_CXX_FLAGS="-s" \
|
||||
-D CMAKE_C_FLAGS="-Werror -s" \
|
||||
-D CMAKE_CXX_FLAGS="-Werror -s" \
|
||||
-D TDESKTOP_API_TEST=ON \
|
||||
-D DESKTOP_APP_USE_PACKAGED=OFF \
|
||||
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \
|
||||
@@ -587,7 +114,6 @@ jobs:
|
||||
make -j$(nproc)
|
||||
|
||||
- name: Check.
|
||||
if: env.ONLY_CACHE == 'false'
|
||||
run: |
|
||||
filePath="$REPO_NAME/out/Debug/bin/Telegram"
|
||||
if test -f "$filePath"; then
|
||||
|
||||
30
.github/workflows/mac.yml
vendored
@@ -57,9 +57,9 @@ jobs:
|
||||
PREFIX: "/usr/local/macos"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.12"
|
||||
XZ: "xz-5.2.4"
|
||||
QT: "5_12_8"
|
||||
QT: "5_15_1"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
QT_PREFIX: "/usr/local/desktop-app/Qt-5.12.8"
|
||||
QT_PREFIX: "/usr/local/desktop-app/Qt-5.15.1"
|
||||
LIBICONV_VER: "libiconv-1.16"
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
|
||||
git clone $GIT/FFmpeg/FFmpeg.git ffmpeg
|
||||
cd ffmpeg
|
||||
git checkout release/3.4
|
||||
git checkout release/4.2
|
||||
CFLAGS=`freetype-config --cflags`
|
||||
LDFLAGS=`freetype-config --libs`
|
||||
PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig:/usr/X11/lib/pkgconfig
|
||||
@@ -244,7 +244,9 @@ jobs:
|
||||
--extra-cflags="$MIN_MAC $UNGUARDED" \
|
||||
--extra-cxxflags="$MIN_MAC $UNGUARDED" \
|
||||
--extra-ldflags="$MIN_MAC" \
|
||||
--enable-protocol=file --enable-libopus \
|
||||
--x86asmexe=`pwd`/macos_yasm_wrap.sh \
|
||||
--enable-protocol=file \
|
||||
--enable-libopus \
|
||||
--disable-programs \
|
||||
--disable-doc \
|
||||
--disable-network \
|
||||
@@ -364,9 +366,9 @@ jobs:
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
|
||||
git clone $GIT/kcat/openal-soft.git
|
||||
git clone https://github.com/telegramdesktop/openal-soft.git
|
||||
cd openal-soft
|
||||
git checkout openal-soft-1.19.1
|
||||
git checkout fix_mono
|
||||
cd build
|
||||
|
||||
CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake \
|
||||
@@ -417,20 +419,20 @@ jobs:
|
||||
build/gyp_crashpad.py -Dmac_deployment_target=10.10
|
||||
ninja -C out/Debug
|
||||
|
||||
- name: Qt 5.12.8 cache.
|
||||
- name: Qt 5.15.1 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/qt-cache
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_8/*') }}
|
||||
- name: Use cached Qt 5.12.8.
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_15_1/*') }}
|
||||
- name: Use cached Qt 5.15.1.
|
||||
if: steps.cache-qt.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
mv qt-cache Qt-5.12.8
|
||||
mv qt-cache Qt-5.15.1
|
||||
sudo mkdir -p $QT_PREFIX
|
||||
sudo mv -f Qt-5.12.8 "$(dirname "$QT_PREFIX")"/
|
||||
- name: Qt 5.12.8 build.
|
||||
sudo mv -f Qt-5.15.1 "$(dirname "$QT_PREFIX")"/
|
||||
- name: Qt 5.15.1 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd $LibrariesPath
|
||||
@@ -438,7 +440,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.8
|
||||
git checkout v5.15.1
|
||||
git submodule update qtbase
|
||||
git submodule update qtimageformats
|
||||
cd qtbase
|
||||
@@ -513,6 +515,8 @@ jobs:
|
||||
fi
|
||||
|
||||
./configure.sh \
|
||||
-D CMAKE_C_FLAGS="-Werror" \
|
||||
-D CMAKE_CXX_FLAGS="-Werror" \
|
||||
-D TDESKTOP_API_TEST=ON \
|
||||
-D DESKTOP_APP_USE_PACKAGED=OFF \
|
||||
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \
|
||||
|
||||
16
.github/workflows/win.yml
vendored
@@ -59,8 +59,8 @@ jobs:
|
||||
SDK: "10.0.18362.0"
|
||||
VC: "call vcvars32.bat && cd Libraries"
|
||||
GIT: "https://github.com"
|
||||
QT: "5_12_8"
|
||||
QT_VER: "5.12.8"
|
||||
QT: "5_15_1"
|
||||
QT_VER: "5.15.1"
|
||||
OPENSSL_VER: "1_1_1"
|
||||
UPLOAD_ARTIFACT: "false"
|
||||
ONLY_CACHE: "false"
|
||||
@@ -294,20 +294,20 @@ jobs:
|
||||
|
||||
git clone %GIT%/FFmpeg/FFmpeg.git ffmpeg
|
||||
cd ffmpeg
|
||||
git checkout release/3.4
|
||||
git checkout release/4.2
|
||||
set CHERE_INVOKING=enabled_from_arguments
|
||||
set MSYS2_PATH_TYPE=inherit
|
||||
call c:\tools\msys64\usr\bin\bash --login ../../%REPO_NAME%/Telegram/Patches/build_ffmpeg_win.sh
|
||||
call c:\tools\msys64\usr\bin\bash --login ../patches/build_ffmpeg_win.sh
|
||||
|
||||
rmdir /S /Q .git
|
||||
|
||||
- name: Qt 5.12.8 cache.
|
||||
- name: Qt 5.15.1 cache.
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.LibrariesPath }}/Qt-${{ env.QT_VER }}
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_8/*') }}
|
||||
- name: Configure Qt 5.12.8.
|
||||
key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_15_1/*') }}
|
||||
- name: Configure Qt 5.15.1.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
%VC%
|
||||
@@ -344,7 +344,7 @@ jobs:
|
||||
-I "%LibrariesPath%\mozjpeg" ^
|
||||
LIBJPEG_LIBS_DEBUG="%LibrariesPath%\mozjpeg\Debug\jpeg-static.lib" ^
|
||||
LIBJPEG_LIBS_RELEASE="%LibrariesPath%\mozjpeg\Release\jpeg-static.lib"
|
||||
- name: Qt 5.12.8 build.
|
||||
- name: Qt 5.15.1 build.
|
||||
if: steps.cache-qt.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
%VC%
|
||||
|
||||
@@ -34,7 +34,7 @@ Version **1.8.15** was the last that supports older systems
|
||||
|
||||
* Qt 5.12.8, 5.6.2 and 5.3.2 slightly patched ([LGPL](http://doc.qt.io/qt-5/lgpl.html))
|
||||
* OpenSSL 1.1.1 and 1.0.1 ([OpenSSL License](https://www.openssl.org/source/license.html))
|
||||
* WebRTC ([New BSD License](https://github.com/desktop-app/tg_owt/blob/master/src/LICENSE))
|
||||
* WebRTC ([New BSD License](https://github.com/desktop-app/tg_owt/blob/master/LICENSE))
|
||||
* 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))
|
||||
* liblzma ([public domain](http://tukaani.org/xz/))
|
||||
|
||||
@@ -70,7 +70,6 @@ PRIVATE
|
||||
if (LINUX)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
desktop-app::external_materialdecoration
|
||||
desktop-app::external_nimf_qt5
|
||||
desktop-app::external_qt5ct_support
|
||||
desktop-app::external_xcb_screensaver
|
||||
@@ -89,14 +88,22 @@ if (LINUX)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (DESKTOP_APP_USE_PACKAGED AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
|
||||
|
||||
target_include_directories(Telegram
|
||||
if (NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
target_link_libraries(Telegram
|
||||
PRIVATE
|
||||
${WAYLAND_CLIENT_INCLUDE_DIRS}
|
||||
desktop-app::external_materialdecoration
|
||||
)
|
||||
|
||||
if (DESKTOP_APP_USE_PACKAGED
|
||||
AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)
|
||||
|
||||
target_include_directories(Telegram
|
||||
PRIVATE
|
||||
${WAYLAND_CLIENT_INCLUDE_DIRS}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT TDESKTOP_DISABLE_GTK_INTEGRATION)
|
||||
@@ -112,7 +119,7 @@ if (LINUX)
|
||||
PkgConfig::X11
|
||||
)
|
||||
else()
|
||||
pkg_search_module(GTK REQUIRED gtk+-2.0 gtk+-3.0)
|
||||
pkg_search_module(GTK REQUIRED gtk+-3.0 gtk+-2.0)
|
||||
target_include_directories(Telegram PRIVATE ${GTK_INCLUDE_DIRS})
|
||||
target_link_libraries(Telegram PRIVATE X11)
|
||||
endif()
|
||||
@@ -474,6 +481,13 @@ PRIVATE
|
||||
history/admin_log/history_admin_log_section.h
|
||||
# history/feed/history_feed_section.cpp
|
||||
# history/feed/history_feed_section.h
|
||||
history/view/controls/compose_controls_common.h
|
||||
history/view/controls/history_view_compose_controls.cpp
|
||||
history/view/controls/history_view_compose_controls.h
|
||||
history/view/controls/history_view_voice_record_bar.cpp
|
||||
history/view/controls/history_view_voice_record_bar.h
|
||||
history/view/controls/history_view_voice_record_button.cpp
|
||||
history/view/controls/history_view_voice_record_button.h
|
||||
history/view/media/history_view_call.h
|
||||
history/view/media/history_view_call.cpp
|
||||
history/view/media/history_view_contact.h
|
||||
@@ -514,8 +528,6 @@ PRIVATE
|
||||
history/view/media/history_view_theme_document.cpp
|
||||
history/view/media/history_view_web_page.h
|
||||
history/view/media/history_view_web_page.cpp
|
||||
history/view/history_view_compose_controls.cpp
|
||||
history/view/history_view_compose_controls.h
|
||||
history/view/history_view_contact_status.cpp
|
||||
history/view/history_view_contact_status.h
|
||||
history/view/history_view_context_menu.cpp
|
||||
@@ -642,6 +654,8 @@ PRIVATE
|
||||
inline_bots/inline_bot_result.h
|
||||
inline_bots/inline_bot_send_data.cpp
|
||||
inline_bots/inline_bot_send_data.h
|
||||
inline_bots/inline_results_inner.cpp
|
||||
inline_bots/inline_results_inner.h
|
||||
inline_bots/inline_results_widget.cpp
|
||||
inline_bots/inline_results_widget.h
|
||||
intro/intro_code.cpp
|
||||
@@ -796,6 +810,8 @@ PRIVATE
|
||||
platform/linux/linux_gdk_helper.h
|
||||
platform/linux/linux_libs.cpp
|
||||
platform/linux/linux_libs.h
|
||||
platform/linux/linux_wayland_integration.cpp
|
||||
platform/linux/linux_wayland_integration.h
|
||||
platform/linux/linux_xlib_helper.cpp
|
||||
platform/linux/linux_xlib_helper.h
|
||||
platform/linux/file_utilities_linux.cpp
|
||||
@@ -808,6 +824,7 @@ PRIVATE
|
||||
platform/linux/notifications_manager_linux.h
|
||||
platform/linux/specific_linux.cpp
|
||||
platform/linux/specific_linux.h
|
||||
platform/linux/window_title_linux.cpp
|
||||
platform/linux/window_title_linux.h
|
||||
platform/mac/file_utilities_mac.mm
|
||||
platform/mac/file_utilities_mac.h
|
||||
@@ -1081,6 +1098,11 @@ if (NOT LINUX)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (LINUX AND DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
|
||||
remove_target_sources(Telegram ${src_loc} platform/linux/linux_wayland_integration.cpp)
|
||||
nice_target_sources(Telegram ${src_loc} PRIVATE platform/linux/linux_wayland_integration_dummy.cpp)
|
||||
endif()
|
||||
|
||||
if (NOT DESKTOP_APP_USE_PACKAGED)
|
||||
nice_target_sources(Telegram ${src_loc} PRIVATE platform/mac/mac_iconv_helper.c)
|
||||
endif()
|
||||
|
||||
BIN
Telegram/Resources/icons/send_control_record_active.png
Normal file
|
After Width: | Height: | Size: 869 B |
BIN
Telegram/Resources/icons/send_control_record_active@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/send_control_record_active@3x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_body.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_body@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_body@3x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_body_shadow.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_bottom.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_bottom@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_bottom@3x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_top.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_top@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_top@3x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Telegram/Resources/icons/voice_lock/record_lock_top_shadow.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
BIN
Telegram/Resources/icons/voice_lock/voice_arrow.png
Normal file
|
After Width: | Height: | Size: 354 B |
BIN
Telegram/Resources/icons/voice_lock/voice_arrow@2x.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
Telegram/Resources/icons/voice_lock/voice_arrow@3x.png
Normal file
|
After Width: | Height: | Size: 818 B |
@@ -184,6 +184,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_edit_media_album_error" = "This file cannot be saved as a part of an album.";
|
||||
"lng_edit_media_invalid_file" = "Sorry, no way to use this file.";
|
||||
"lng_edit_caption_attach" = "Sorry, you can't attach a new media while you're editing your message.";
|
||||
"lng_edit_caption_voice" = "Sorry, you can't edit your message while you're having an unsent voice message.";
|
||||
|
||||
"lng_intro_about" = "Welcome to the official Telegram Desktop app.\nIt's fast and secure.";
|
||||
"lng_start_msgs" = "START MESSAGING";
|
||||
@@ -1341,6 +1342,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_broadcast_silent_ph" = "Silent broadcast...";
|
||||
"lng_send_anonymous_ph" = "Send anonymously...";
|
||||
"lng_record_cancel" = "Release outside this field to cancel";
|
||||
"lng_record_lock_cancel" = "Click outside of microphone button to cancel";
|
||||
"lng_record_lock_cancel_sure" = "Are you sure you want to stop recording and discard your voice message?";
|
||||
"lng_record_lock_discard" = "Discard";
|
||||
"lng_will_be_notified" = "Members will be notified when you post";
|
||||
"lng_wont_be_notified" = "Members will not be notified when you post";
|
||||
"lng_willbe_history" = "Please select a chat to start messaging";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="2.4.8.0" />
|
||||
Version="2.4.10.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,8,0
|
||||
PRODUCTVERSION 2,4,8,0
|
||||
FILEVERSION 2,4,10,0
|
||||
PRODUCTVERSION 2,4,10,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "2.4.8.0"
|
||||
VALUE "FileVersion", "2.4.10.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.8.0"
|
||||
VALUE "ProductVersion", "2.4.10.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,8,0
|
||||
PRODUCTVERSION 2,4,8,0
|
||||
FILEVERSION 2,4,10,0
|
||||
PRODUCTVERSION 2,4,10,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "2.4.8.0"
|
||||
VALUE "FileVersion", "2.4.10.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.8.0"
|
||||
VALUE "ProductVersion", "2.4.10.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -791,6 +791,10 @@ void Updates::mtpUpdateReceived(const MTPUpdates &updates) {
|
||||
}
|
||||
}
|
||||
|
||||
int32 Updates::pts() const {
|
||||
return _ptsWaiter.current();
|
||||
}
|
||||
|
||||
void Updates::updateOnline() {
|
||||
updateOnline(false);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ public:
|
||||
void applyUpdatesNoPtsCheck(const MTPUpdates &updates);
|
||||
void applyUpdateNoPtsCheck(const MTPUpdate &update);
|
||||
|
||||
[[nodiscard]] int32 pts() const;
|
||||
|
||||
void updateOnline();
|
||||
[[nodiscard]] bool isIdle() const;
|
||||
void checkIdleFinish();
|
||||
|
||||
@@ -2441,16 +2441,14 @@ void ApiWrap::saveCurrentDraftToCloud() {
|
||||
Core::App().saveCurrentDraftsToHistories();
|
||||
|
||||
for (const auto controller : _session->windows()) {
|
||||
if (const auto peer = controller->activeChatCurrent().peer()) {
|
||||
if (const auto history = _session->data().historyLoaded(peer)) {
|
||||
_session->local().writeDrafts(history);
|
||||
if (const auto history = controller->activeChatCurrent().history()) {
|
||||
_session->local().writeDrafts(history);
|
||||
|
||||
const auto localDraft = history->localDraft();
|
||||
const auto cloudDraft = history->cloudDraft();
|
||||
if (!Data::draftsAreEqual(localDraft, cloudDraft)
|
||||
&& !_session->supportMode()) {
|
||||
saveDraftToCloudDelayed(history);
|
||||
}
|
||||
const auto localDraft = history->localDraft();
|
||||
const auto cloudDraft = history->cloudDraft();
|
||||
if (!Data::draftsAreEqual(localDraft, cloudDraft)
|
||||
&& !_session->supportMode()) {
|
||||
saveDraftToCloudDelayed(history);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "chat_helpers/send_context_menu.h" // SendMenu::FillSendMenu
|
||||
#include "chat_helpers/stickers_lottie.h"
|
||||
#include "chat_helpers/message_field.h" // PrepareMentionTag.
|
||||
#include "mainwindow.h"
|
||||
#include "apiwrap.h"
|
||||
#include "main/main_session.h"
|
||||
@@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lottie/lottie_single_player.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
@@ -39,15 +41,105 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
class FieldAutocomplete::Inner final
|
||||
: public Ui::RpWidget
|
||||
, private base::Subscriber {
|
||||
|
||||
public:
|
||||
struct ScrollTo {
|
||||
int top;
|
||||
int bottom;
|
||||
};
|
||||
|
||||
Inner(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<FieldAutocomplete*> parent,
|
||||
not_null<MentionRows*> mrows,
|
||||
not_null<HashtagRows*> hrows,
|
||||
not_null<BotCommandRows*> brows,
|
||||
not_null<StickerRows*> srows);
|
||||
|
||||
void clearSel(bool hidden = false);
|
||||
bool moveSel(int key);
|
||||
bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
|
||||
bool chooseAtIndex(
|
||||
FieldAutocomplete::ChooseMethod method,
|
||||
int index,
|
||||
Api::SendOptions options = Api::SendOptions()) const;
|
||||
|
||||
void setRecentInlineBotsInRows(int32 bots);
|
||||
void rowsUpdated();
|
||||
|
||||
rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
|
||||
rpl::producer<FieldAutocomplete::HashtagChosen> hashtagChosen() const;
|
||||
rpl::producer<FieldAutocomplete::BotCommandChosen>
|
||||
botCommandChosen() const;
|
||||
rpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;
|
||||
rpl::producer<ScrollTo> scrollToRequested() const;
|
||||
|
||||
void onParentGeometryChanged();
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void enterEventHook(QEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
void updateSelectedRow();
|
||||
void setSel(int sel, bool scroll = false);
|
||||
void showPreview();
|
||||
void selectByMouse(QPoint global);
|
||||
|
||||
QSize stickerBoundingBox() const;
|
||||
void setupLottie(StickerSuggestion &suggestion);
|
||||
void repaintSticker(not_null<DocumentData*> document);
|
||||
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<FieldAutocomplete*> _parent;
|
||||
const not_null<MentionRows*> _mrows;
|
||||
const not_null<HashtagRows*> _hrows;
|
||||
const not_null<BotCommandRows*> _brows;
|
||||
const not_null<StickerRows*> _srows;
|
||||
rpl::lifetime _stickersLifetime;
|
||||
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
int _stickersPerRow = 1;
|
||||
int _recentInlineBotsInRows = 0;
|
||||
int _sel = -1;
|
||||
int _down = -1;
|
||||
std::optional<QPoint> _lastMousePosition;
|
||||
bool _mouseSelection = false;
|
||||
|
||||
bool _overDelete = false;
|
||||
|
||||
bool _previewShown = false;
|
||||
|
||||
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
|
||||
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
|
||||
rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
|
||||
rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
|
||||
rpl::event_stream<ScrollTo> _scrollToRequested;
|
||||
|
||||
base::Timer _previewTimer;
|
||||
|
||||
};
|
||||
|
||||
FieldAutocomplete::FieldAutocomplete(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _scroll(this, st::mentionScroll) {
|
||||
_scroll->setGeometry(rect());
|
||||
hide();
|
||||
|
||||
using Inner = internal::FieldAutocompleteInner;
|
||||
_scroll->setGeometry(rect());
|
||||
|
||||
_inner = _scroll->setOwnedWidget(
|
||||
object_ptr<Inner>(
|
||||
@@ -76,6 +168,10 @@ FieldAutocomplete::FieldAutocomplete(
|
||||
&Inner::onParentGeometryChanged);
|
||||
}
|
||||
|
||||
not_null<Window::SessionController*> FieldAutocomplete::controller() const {
|
||||
return _controller;
|
||||
}
|
||||
|
||||
auto FieldAutocomplete::mentionChosen() const
|
||||
-> rpl::producer<FieldAutocomplete::MentionChosen> {
|
||||
return _inner->mentionChosen();
|
||||
@@ -125,9 +221,9 @@ void FieldAutocomplete::showFiltered(
|
||||
if (query.isEmpty()) {
|
||||
_type = Type::Mentions;
|
||||
rowsUpdated(
|
||||
internal::MentionRows(),
|
||||
internal::HashtagRows(),
|
||||
internal::BotCommandRows(),
|
||||
MentionRows(),
|
||||
HashtagRows(),
|
||||
BotCommandRows(),
|
||||
base::take(_srows),
|
||||
false);
|
||||
return;
|
||||
@@ -171,7 +267,7 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) {
|
||||
base::take(_mrows),
|
||||
base::take(_hrows),
|
||||
base::take(_brows),
|
||||
internal::StickerRows(),
|
||||
StickerRows(),
|
||||
false);
|
||||
return;
|
||||
}
|
||||
@@ -203,7 +299,7 @@ inline int indexOfInFirstN(const T &v, const U &elem, int last) {
|
||||
}
|
||||
}
|
||||
|
||||
internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
||||
FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
||||
const auto list = _controller->session().data().stickers().getListByEmoji(
|
||||
_emoji,
|
||||
_stickersSeed
|
||||
@@ -211,7 +307,7 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
||||
auto result = ranges::view::all(
|
||||
list
|
||||
) | ranges::view::transform([](not_null<DocumentData*> sticker) {
|
||||
return internal::StickerSuggestion{
|
||||
return StickerSuggestion{
|
||||
sticker,
|
||||
sticker->createMediaView()
|
||||
};
|
||||
@@ -223,7 +319,7 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
||||
const auto i = ranges::find(
|
||||
result,
|
||||
suggestion.document,
|
||||
&internal::StickerSuggestion::document);
|
||||
&StickerSuggestion::document);
|
||||
if (i != end(result)) {
|
||||
i->animated = std::move(suggestion.animated);
|
||||
}
|
||||
@@ -233,10 +329,10 @@ internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
||||
|
||||
void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
||||
int32 now = base::unixtime::now(), recentInlineBots = 0;
|
||||
internal::MentionRows mrows;
|
||||
internal::HashtagRows hrows;
|
||||
internal::BotCommandRows brows;
|
||||
internal::StickerRows srows;
|
||||
MentionRows mrows;
|
||||
HashtagRows hrows;
|
||||
BotCommandRows brows;
|
||||
StickerRows srows;
|
||||
if (_emoji) {
|
||||
srows = getStickerSuggestions();
|
||||
} else if (_type == Type::Mentions) {
|
||||
@@ -435,10 +531,10 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
||||
}
|
||||
|
||||
void FieldAutocomplete::rowsUpdated(
|
||||
internal::MentionRows &&mrows,
|
||||
internal::HashtagRows &&hrows,
|
||||
internal::BotCommandRows &&brows,
|
||||
internal::StickerRows &&srows,
|
||||
MentionRows &&mrows,
|
||||
HashtagRows &&hrows,
|
||||
BotCommandRows &&brows,
|
||||
StickerRows &&srows,
|
||||
bool resetScroll) {
|
||||
if (mrows.empty() && hrows.empty() && brows.empty() && srows.empty()) {
|
||||
if (!isHidden()) {
|
||||
@@ -620,9 +716,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
|
||||
return QWidget::eventFilter(obj, e);
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
FieldAutocompleteInner::FieldAutocompleteInner(
|
||||
FieldAutocomplete::Inner::Inner(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<FieldAutocomplete*> parent,
|
||||
not_null<MentionRows*> mrows,
|
||||
@@ -642,7 +736,7 @@ FieldAutocompleteInner::FieldAutocompleteInner(
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
|
||||
void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
QRect r(e->rect());
|
||||
@@ -841,11 +935,11 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
|
||||
p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerTop(), width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowFg);
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::resizeEvent(QResizeEvent *e) {
|
||||
void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) {
|
||||
_stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width()));
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) {
|
||||
void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {
|
||||
const auto globalPosition = e->globalPos();
|
||||
if (!_lastMousePosition) {
|
||||
_lastMousePosition = globalPosition;
|
||||
@@ -857,7 +951,7 @@ void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) {
|
||||
selectByMouse(globalPosition);
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::clearSel(bool hidden) {
|
||||
void FieldAutocomplete::Inner::clearSel(bool hidden) {
|
||||
_overDelete = false;
|
||||
_mouseSelection = false;
|
||||
_lastMousePosition = std::nullopt;
|
||||
@@ -868,7 +962,7 @@ void FieldAutocompleteInner::clearSel(bool hidden) {
|
||||
}
|
||||
}
|
||||
|
||||
bool FieldAutocompleteInner::moveSel(int key) {
|
||||
bool FieldAutocomplete::Inner::moveSel(int key) {
|
||||
_mouseSelection = false;
|
||||
_lastMousePosition = std::nullopt;
|
||||
|
||||
@@ -903,12 +997,12 @@ bool FieldAutocompleteInner::moveSel(int key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FieldAutocompleteInner::chooseSelected(
|
||||
bool FieldAutocomplete::Inner::chooseSelected(
|
||||
FieldAutocomplete::ChooseMethod method) const {
|
||||
return chooseAtIndex(method, _sel);
|
||||
}
|
||||
|
||||
bool FieldAutocompleteInner::chooseAtIndex(
|
||||
bool FieldAutocomplete::Inner::chooseAtIndex(
|
||||
FieldAutocomplete::ChooseMethod method,
|
||||
int index,
|
||||
Api::SendOptions options) const {
|
||||
@@ -955,11 +1049,11 @@ bool FieldAutocompleteInner::chooseAtIndex(
|
||||
return false;
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::setRecentInlineBotsInRows(int32 bots) {
|
||||
void FieldAutocomplete::Inner::setRecentInlineBotsInRows(int32 bots) {
|
||||
_recentInlineBotsInRows = bots;
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) {
|
||||
void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {
|
||||
selectByMouse(e->globalPos());
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if (_overDelete && _sel >= 0 && _sel < (_mrows->empty() ? _hrows->size() : _recentInlineBotsInRows)) {
|
||||
@@ -999,7 +1093,7 @@ void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
void FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
_previewTimer.cancel();
|
||||
|
||||
int32 pressed = _down;
|
||||
@@ -1017,7 +1111,7 @@ void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
chooseSelected(FieldAutocomplete::ChooseMethod::ByClick);
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (_sel < 0 || _srows->empty() || _down >= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1040,11 +1134,11 @@ void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::enterEventHook(QEvent *e) {
|
||||
void FieldAutocomplete::Inner::enterEventHook(QEvent *e) {
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::leaveEventHook(QEvent *e) {
|
||||
void FieldAutocomplete::Inner::leaveEventHook(QEvent *e) {
|
||||
setMouseTracking(false);
|
||||
if (_mouseSelection) {
|
||||
setSel(-1);
|
||||
@@ -1053,7 +1147,7 @@ void FieldAutocompleteInner::leaveEventHook(QEvent *e) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::updateSelectedRow() {
|
||||
void FieldAutocomplete::Inner::updateSelectedRow() {
|
||||
if (_sel >= 0) {
|
||||
if (_srows->empty()) {
|
||||
update(0, _sel * st::mentionHeight, width(), st::mentionHeight);
|
||||
@@ -1064,7 +1158,7 @@ void FieldAutocompleteInner::updateSelectedRow() {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::setSel(int sel, bool scroll) {
|
||||
void FieldAutocomplete::Inner::setSel(int sel, bool scroll) {
|
||||
updateSelectedRow();
|
||||
_sel = sel;
|
||||
updateSelectedRow();
|
||||
@@ -1084,13 +1178,13 @@ void FieldAutocompleteInner::setSel(int sel, bool scroll) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::rowsUpdated() {
|
||||
void FieldAutocomplete::Inner::rowsUpdated() {
|
||||
if (_srows->empty()) {
|
||||
_stickersLifetime.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::getLottieRenderer()
|
||||
auto FieldAutocomplete::Inner::getLottieRenderer()
|
||||
-> std::shared_ptr<Lottie::FrameRenderer> {
|
||||
if (auto result = _lottieRenderer.lock()) {
|
||||
return result;
|
||||
@@ -1100,7 +1194,7 @@ auto FieldAutocompleteInner::getLottieRenderer()
|
||||
return result;
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) {
|
||||
void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {
|
||||
const auto document = suggestion.document;
|
||||
suggestion.animated = ChatHelpers::LottiePlayerFromDocument(
|
||||
suggestion.documentMedia.get(),
|
||||
@@ -1115,13 +1209,13 @@ void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) {
|
||||
}, _stickersLifetime);
|
||||
}
|
||||
|
||||
QSize FieldAutocompleteInner::stickerBoundingBox() const {
|
||||
QSize FieldAutocomplete::Inner::stickerBoundingBox() const {
|
||||
return QSize(
|
||||
st::stickerPanSize.width() - st::buttonRadius * 2,
|
||||
st::stickerPanSize.height() - st::buttonRadius * 2);
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::repaintSticker(
|
||||
void FieldAutocomplete::Inner::repaintSticker(
|
||||
not_null<DocumentData*> document) {
|
||||
const auto i = ranges::find(
|
||||
*_srows,
|
||||
@@ -1140,7 +1234,7 @@ void FieldAutocompleteInner::repaintSticker(
|
||||
st::stickerPanSize.height());
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) {
|
||||
void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) {
|
||||
_mouseSelection = true;
|
||||
_lastMousePosition = globalPosition;
|
||||
const auto mouse = mapFromGlobal(globalPosition);
|
||||
@@ -1186,7 +1280,7 @@ void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::onParentGeometryChanged() {
|
||||
void FieldAutocomplete::Inner::onParentGeometryChanged() {
|
||||
const auto globalPosition = QCursor::pos();
|
||||
if (rect().contains(mapFromGlobal(globalPosition))) {
|
||||
setMouseTracking(true);
|
||||
@@ -1196,7 +1290,7 @@ void FieldAutocompleteInner::onParentGeometryChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::showPreview() {
|
||||
void FieldAutocomplete::Inner::showPreview() {
|
||||
if (_down >= 0 && _down < _srows->size()) {
|
||||
if (const auto w = App::wnd()) {
|
||||
w->showMediaPreview(
|
||||
@@ -1207,29 +1301,27 @@ void FieldAutocompleteInner::showPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::mentionChosen() const
|
||||
auto FieldAutocomplete::Inner::mentionChosen() const
|
||||
-> rpl::producer<FieldAutocomplete::MentionChosen> {
|
||||
return _mentionChosen.events();
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::hashtagChosen() const
|
||||
auto FieldAutocomplete::Inner::hashtagChosen() const
|
||||
-> rpl::producer<FieldAutocomplete::HashtagChosen> {
|
||||
return _hashtagChosen.events();
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::botCommandChosen() const
|
||||
auto FieldAutocomplete::Inner::botCommandChosen() const
|
||||
-> rpl::producer<FieldAutocomplete::BotCommandChosen> {
|
||||
return _botCommandChosen.events();
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::stickerChosen() const
|
||||
auto FieldAutocomplete::Inner::stickerChosen() const
|
||||
-> rpl::producer<FieldAutocomplete::StickerChosen> {
|
||||
return _stickerChosen.events();
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::scrollToRequested() const
|
||||
auto FieldAutocomplete::Inner::scrollToRequested() const
|
||||
-> rpl::producer<ScrollTo> {
|
||||
return _scrollToRequested.events();
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
class ScrollArea;
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Lottie {
|
||||
@@ -32,42 +33,15 @@ class DocumentMedia;
|
||||
class CloudImageView;
|
||||
} // namespace Data
|
||||
|
||||
namespace internal {
|
||||
|
||||
struct StickerSuggestion {
|
||||
not_null<DocumentData*> document;
|
||||
std::shared_ptr<Data::DocumentMedia> documentMedia;
|
||||
std::unique_ptr<Lottie::SinglePlayer> animated;
|
||||
};
|
||||
|
||||
struct MentionRow {
|
||||
not_null<UserData*> user;
|
||||
std::shared_ptr<Data::CloudImageView> userpic;
|
||||
};
|
||||
|
||||
struct BotCommandRow {
|
||||
not_null<UserData*> user;
|
||||
not_null<const BotCommand*> command;
|
||||
std::shared_ptr<Data::CloudImageView> userpic;
|
||||
};
|
||||
|
||||
using HashtagRows = std::vector<QString>;
|
||||
using BotCommandRows = std::vector<BotCommandRow>;
|
||||
using StickerRows = std::vector<StickerSuggestion>;
|
||||
using MentionRows = std::vector<MentionRow>;
|
||||
|
||||
class FieldAutocompleteInner;
|
||||
|
||||
} // namespace internal
|
||||
|
||||
class FieldAutocomplete final : public Ui::RpWidget {
|
||||
|
||||
public:
|
||||
FieldAutocomplete(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller);
|
||||
~FieldAutocomplete();
|
||||
|
||||
[[nodiscard]] not_null<Window::SessionController*> controller() const;
|
||||
|
||||
bool clearFilteredBotCommands();
|
||||
void showFiltered(
|
||||
not_null<PeerData*> peer,
|
||||
@@ -140,29 +114,54 @@ protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
class Inner;
|
||||
friend class Inner;
|
||||
|
||||
struct StickerSuggestion {
|
||||
not_null<DocumentData*> document;
|
||||
std::shared_ptr<Data::DocumentMedia> documentMedia;
|
||||
std::unique_ptr<Lottie::SinglePlayer> animated;
|
||||
};
|
||||
|
||||
struct MentionRow {
|
||||
not_null<UserData*> user;
|
||||
std::shared_ptr<Data::CloudImageView> userpic;
|
||||
};
|
||||
|
||||
struct BotCommandRow {
|
||||
not_null<UserData*> user;
|
||||
not_null<const BotCommand*> command;
|
||||
std::shared_ptr<Data::CloudImageView> userpic;
|
||||
};
|
||||
|
||||
using HashtagRows = std::vector<QString>;
|
||||
using BotCommandRows = std::vector<BotCommandRow>;
|
||||
using StickerRows = std::vector<StickerSuggestion>;
|
||||
using MentionRows = std::vector<MentionRow>;
|
||||
|
||||
void animationCallback();
|
||||
void hideFinish();
|
||||
|
||||
void updateFiltered(bool resetScroll = false);
|
||||
void recount(bool resetScroll = false);
|
||||
internal::StickerRows getStickerSuggestions();
|
||||
StickerRows getStickerSuggestions();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
QPixmap _cache;
|
||||
internal::MentionRows _mrows;
|
||||
internal::HashtagRows _hrows;
|
||||
internal::BotCommandRows _brows;
|
||||
internal::StickerRows _srows;
|
||||
MentionRows _mrows;
|
||||
HashtagRows _hrows;
|
||||
BotCommandRows _brows;
|
||||
StickerRows _srows;
|
||||
|
||||
void rowsUpdated(
|
||||
internal::MentionRows &&mrows,
|
||||
internal::HashtagRows &&hrows,
|
||||
internal::BotCommandRows &&brows,
|
||||
internal::StickerRows &&srows,
|
||||
MentionRows &&mrows,
|
||||
HashtagRows &&hrows,
|
||||
BotCommandRows &&brows,
|
||||
StickerRows &&srows,
|
||||
bool resetScroll);
|
||||
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
QPointer<internal::FieldAutocompleteInner> _inner;
|
||||
QPointer<Inner> _inner;
|
||||
|
||||
ChatData *_chat = nullptr;
|
||||
UserData *_user = nullptr;
|
||||
@@ -186,100 +185,4 @@ private:
|
||||
|
||||
Fn<bool(int)> _moderateKeyActivateCallback;
|
||||
|
||||
friend class internal::FieldAutocompleteInner;
|
||||
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
class FieldAutocompleteInner final
|
||||
: public Ui::RpWidget
|
||||
, private base::Subscriber {
|
||||
|
||||
public:
|
||||
struct ScrollTo {
|
||||
int top;
|
||||
int bottom;
|
||||
};
|
||||
|
||||
FieldAutocompleteInner(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<FieldAutocomplete*> parent,
|
||||
not_null<MentionRows*> mrows,
|
||||
not_null<HashtagRows*> hrows,
|
||||
not_null<BotCommandRows*> brows,
|
||||
not_null<StickerRows*> srows);
|
||||
|
||||
void clearSel(bool hidden = false);
|
||||
bool moveSel(int key);
|
||||
bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
|
||||
bool chooseAtIndex(
|
||||
FieldAutocomplete::ChooseMethod method,
|
||||
int index,
|
||||
Api::SendOptions options = Api::SendOptions()) const;
|
||||
|
||||
void setRecentInlineBotsInRows(int32 bots);
|
||||
void rowsUpdated();
|
||||
|
||||
rpl::producer<FieldAutocomplete::MentionChosen> mentionChosen() const;
|
||||
rpl::producer<FieldAutocomplete::HashtagChosen> hashtagChosen() const;
|
||||
rpl::producer<FieldAutocomplete::BotCommandChosen>
|
||||
botCommandChosen() const;
|
||||
rpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;
|
||||
rpl::producer<ScrollTo> scrollToRequested() const;
|
||||
|
||||
void onParentGeometryChanged();
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void enterEventHook(QEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
void updateSelectedRow();
|
||||
void setSel(int sel, bool scroll = false);
|
||||
void showPreview();
|
||||
void selectByMouse(QPoint global);
|
||||
|
||||
QSize stickerBoundingBox() const;
|
||||
void setupLottie(StickerSuggestion &suggestion);
|
||||
void repaintSticker(not_null<DocumentData*> document);
|
||||
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<FieldAutocomplete*> _parent;
|
||||
const not_null<MentionRows*> _mrows;
|
||||
const not_null<HashtagRows*> _hrows;
|
||||
const not_null<BotCommandRows*> _brows;
|
||||
const not_null<StickerRows*> _srows;
|
||||
rpl::lifetime _stickersLifetime;
|
||||
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
int _stickersPerRow = 1;
|
||||
int _recentInlineBotsInRows = 0;
|
||||
int _sel = -1;
|
||||
int _down = -1;
|
||||
std::optional<QPoint> _lastMousePosition;
|
||||
bool _mouseSelection = false;
|
||||
|
||||
bool _overDelete = false;
|
||||
|
||||
bool _previewShown = false;
|
||||
|
||||
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
|
||||
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
|
||||
rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
|
||||
rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
|
||||
rpl::event_stream<ScrollTo> _scrollToRequested;
|
||||
|
||||
base::Timer _previewTimer;
|
||||
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
|
||||
@@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace InlineBots {
|
||||
class Result;
|
||||
struct ResultSelected;
|
||||
} // namespace InlineBots
|
||||
|
||||
namespace Main {
|
||||
@@ -60,11 +60,7 @@ public:
|
||||
not_null<PhotoData*> photo;
|
||||
Api::SendOptions options;
|
||||
};
|
||||
struct InlineChosen {
|
||||
not_null<InlineBots::Result*> result;
|
||||
not_null<UserData*> bot;
|
||||
Api::SendOptions options;
|
||||
};
|
||||
using InlineChosen = InlineBots::ResultSelected;
|
||||
enum class Mode {
|
||||
Full,
|
||||
EmojiOnly
|
||||
|
||||
@@ -29,9 +29,6 @@ enum {
|
||||
LinksOverviewPerPage = 12,
|
||||
MediaOverviewStartPerPage = 5,
|
||||
|
||||
AudioVoiceMsgMaxLength = 100 * 60, // 100 minutes
|
||||
AudioVoiceMsgChannels = 2, // stereo
|
||||
|
||||
PreloadHeightsCount = 3, // when 3 screens to scroll left make a preload request
|
||||
|
||||
SearchPeopleLimit = 5,
|
||||
|
||||
@@ -89,6 +89,12 @@ std::map<int, const char*> BetaLogs() {
|
||||
2004008,
|
||||
"- Upgrade several third party libraries to latest versions.\n"
|
||||
},
|
||||
{
|
||||
2004010,
|
||||
"- Use inline bots and sticker by emoji suggestions in channel comments.\n"
|
||||
|
||||
"- Lock voice message recording, listen to your voice message before sending.\n"
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -180,20 +180,14 @@ auto CashtagClickHandler::getTextEntity() const -> TextEntity {
|
||||
return { EntityType::Cashtag };
|
||||
}
|
||||
|
||||
PeerData *BotCommandClickHandler::_peer = nullptr;
|
||||
UserData *BotCommandClickHandler::_bot = nullptr;
|
||||
void BotCommandClickHandler::onClick(ClickContext context) const {
|
||||
const auto button = context.button;
|
||||
if (button == Qt::LeftButton || button == Qt::MiddleButton) {
|
||||
if (auto peer = peerForCommand()) {
|
||||
if (auto bot = peer->isUser() ? peer->asUser() : botForCommand()) {
|
||||
Ui::showPeerHistory(peer, ShowAtTheEndMsgId);
|
||||
App::sendBotCommand(peer, bot, _cmd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto peer = Ui::getPeerForMouseAction()) { // old way
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
if (const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr) {
|
||||
delegate->elementSendBotCommand(_cmd, my.itemId);
|
||||
return;
|
||||
} else if (auto peer = Ui::getPeerForMouseAction()) { // old way
|
||||
auto bot = peer->isUser() ? peer->asUser() : nullptr;
|
||||
if (!bot) {
|
||||
if (const auto view = App::hoveredLinkItem()) {
|
||||
|
||||
@@ -13,8 +13,18 @@ namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace HistoryView {
|
||||
class ElementDelegate;
|
||||
} // namespace HistoryView
|
||||
|
||||
[[nodiscard]] bool UrlRequiresConfirmation(const QUrl &url);
|
||||
|
||||
struct ClickHandlerContext {
|
||||
FullMsgId itemId;
|
||||
Fn<HistoryView::ElementDelegate*()> elementDelegate;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ClickHandlerContext);
|
||||
|
||||
class HiddenUrlClickHandler : public UrlClickHandler {
|
||||
public:
|
||||
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
|
||||
@@ -165,30 +175,14 @@ public:
|
||||
return _cmd;
|
||||
}
|
||||
|
||||
static void setPeerForCommand(PeerData *peer) {
|
||||
_peer = peer;
|
||||
}
|
||||
static void setBotForCommand(UserData *bot) {
|
||||
_bot = bot;
|
||||
}
|
||||
|
||||
TextEntity getTextEntity() const override;
|
||||
|
||||
protected:
|
||||
QString url() const override {
|
||||
return _cmd;
|
||||
}
|
||||
static PeerData *peerForCommand() {
|
||||
return _peer;
|
||||
}
|
||||
static UserData *botForCommand() {
|
||||
return _bot;
|
||||
}
|
||||
|
||||
private:
|
||||
QString _cmd;
|
||||
|
||||
static PeerData *_peer;
|
||||
static UserData *_bot;
|
||||
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_keys.h"
|
||||
#include "core/update_checker.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "boxes/confirm_phone_box.h"
|
||||
#include "boxes/background_preview_box.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
@@ -76,11 +77,11 @@ bool ShowTheme(
|
||||
if (!controller) {
|
||||
return false;
|
||||
}
|
||||
const auto clickFromMessageId = context.value<FullMsgId>();
|
||||
const auto fromMessageId = context.value<ClickHandlerContext>().itemId;
|
||||
Core::App().hideMediaView();
|
||||
controller->session().data().cloudThemes().resolve(
|
||||
match->captured(1),
|
||||
clickFromMessageId);
|
||||
fromMessageId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -280,7 +281,7 @@ bool ResolveUsername(
|
||||
startToken = gameParam;
|
||||
post = ShowAtGameShareMsgId;
|
||||
}
|
||||
const auto clickFromMessageId = context.value<FullMsgId>();
|
||||
const auto fromMessageId = context.value<ClickHandlerContext>().itemId;
|
||||
using Navigation = Window::SessionNavigation;
|
||||
controller->showPeerByLink(Navigation::PeerByLinkInfo{
|
||||
.usernameOrId = domain,
|
||||
@@ -295,7 +296,7 @@ bool ResolveUsername(
|
||||
}
|
||||
: Navigation::RepliesByLinkInfo{ v::null },
|
||||
.startToken = startToken,
|
||||
.clickFromMessageId = clickFromMessageId,
|
||||
.clickFromMessageId = fromMessageId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -319,7 +320,7 @@ bool ResolvePrivatePost(
|
||||
if (!channelId || !IsServerMsgId(msgId)) {
|
||||
return false;
|
||||
}
|
||||
const auto clickFromMessageId = context.value<FullMsgId>();
|
||||
const auto fromMessageId = context.value<ClickHandlerContext>().itemId;
|
||||
using Navigation = Window::SessionNavigation;
|
||||
controller->showPeerByLink(Navigation::PeerByLinkInfo{
|
||||
.usernameOrId = channelId,
|
||||
@@ -333,7 +334,7 @@ bool ResolvePrivatePost(
|
||||
Navigation::ThreadId{ threadId }
|
||||
}
|
||||
: Navigation::RepliesByLinkInfo{ v::null },
|
||||
.clickFromMessageId = clickFromMessageId,
|
||||
.clickFromMessageId = fromMessageId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ void Sandbox::setupScreenScale() {
|
||||
Sandbox::~Sandbox() = default;
|
||||
|
||||
bool Sandbox::event(QEvent *e) {
|
||||
if (e->type() == QEvent::Close) {
|
||||
if (e->type() == QEvent::Close || e->type() == QEvent::Quit) {
|
||||
App::quit();
|
||||
}
|
||||
return QApplication::event(e);
|
||||
|
||||
@@ -65,7 +65,7 @@ QString UiIntegration::timeFormat() {
|
||||
std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
|
||||
const EntityLinkData &data,
|
||||
const std::any &context) {
|
||||
const auto my = std::any_cast<Context>(&context);
|
||||
const auto my = std::any_cast<MarkedTextContext>(&context);
|
||||
switch (data.type) {
|
||||
case EntityType::Url:
|
||||
return (!data.data.isEmpty()
|
||||
@@ -82,6 +82,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
|
||||
return std::make_shared<BotCommandClickHandler>(data.data);
|
||||
|
||||
case EntityType::Hashtag:
|
||||
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
|
||||
if (my && my->type == HashtagMentionType::Twitter) {
|
||||
return std::make_shared<UrlClickHandler>(
|
||||
(qsl("https://twitter.com/hashtag/")
|
||||
@@ -101,6 +102,7 @@ std::shared_ptr<ClickHandler> UiIntegration::createLinkHandler(
|
||||
return std::make_shared<CashtagClickHandler>(data.data);
|
||||
|
||||
case EntityType::Mention:
|
||||
using HashtagMentionType = MarkedTextContext::HashtagMentionType;
|
||||
if (my && my->type == HashtagMentionType::Twitter) {
|
||||
return std::make_shared<UrlClickHandler>(
|
||||
qsl("https://twitter.com/") + data.data.mid(1),
|
||||
|
||||
@@ -13,20 +13,25 @@ namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace HistoryView {
|
||||
class ElementDelegate;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Core {
|
||||
|
||||
class UiIntegration : public Ui::Integration {
|
||||
public:
|
||||
struct MarkedTextContext {
|
||||
enum class HashtagMentionType : uchar {
|
||||
Telegram,
|
||||
Twitter,
|
||||
Instagram,
|
||||
};
|
||||
struct Context {
|
||||
Main::Session *session = nullptr;
|
||||
HashtagMentionType type = HashtagMentionType::Telegram;
|
||||
};
|
||||
|
||||
Main::Session *session = nullptr;
|
||||
HashtagMentionType type = HashtagMentionType::Telegram;
|
||||
};
|
||||
|
||||
class UiIntegration : public Ui::Integration {
|
||||
public:
|
||||
void postponeCall(FnMut<void()> &&callable) override;
|
||||
void registerLeaveSubscription(not_null<QWidget*> widget) override;
|
||||
void unregisterLeaveSubscription(not_null<QWidget*> widget) override;
|
||||
|
||||
@@ -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 = 2004008;
|
||||
constexpr auto AppVersionStr = "2.4.8";
|
||||
constexpr auto AppVersion = 2004010;
|
||||
constexpr auto AppVersionStr = "2.4.10";
|
||||
constexpr auto AppBetaVersion = true;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -48,6 +48,77 @@ struct Draft {
|
||||
mtpRequestId saveRequestId = 0;
|
||||
};
|
||||
|
||||
class DraftKey {
|
||||
public:
|
||||
[[nodiscard]] static DraftKey None() {
|
||||
return 0;
|
||||
}
|
||||
[[nodiscard]] static DraftKey Local() {
|
||||
return kLocalDraftIndex;
|
||||
}
|
||||
[[nodiscard]] static DraftKey LocalEdit() {
|
||||
return kLocalDraftIndex + kEditDraftShift;
|
||||
}
|
||||
[[nodiscard]] static DraftKey Cloud() {
|
||||
return kCloudDraftIndex;
|
||||
}
|
||||
[[nodiscard]] static DraftKey Scheduled() {
|
||||
return kScheduledDraftIndex;
|
||||
}
|
||||
[[nodiscard]] static DraftKey ScheduledEdit() {
|
||||
return kScheduledDraftIndex + kEditDraftShift;
|
||||
}
|
||||
[[nodiscard]] static DraftKey Replies(MsgId rootId) {
|
||||
return rootId;
|
||||
}
|
||||
[[nodiscard]] static DraftKey RepliesEdit(MsgId rootId) {
|
||||
return rootId + kEditDraftShift;
|
||||
}
|
||||
|
||||
[[nodiscard]] static DraftKey FromSerialized(int32 value) {
|
||||
return value;
|
||||
}
|
||||
[[nodiscard]] int32 serialize() const {
|
||||
return _value;
|
||||
}
|
||||
|
||||
inline bool operator<(const DraftKey &other) const {
|
||||
return _value < other._value;
|
||||
}
|
||||
inline bool operator==(const DraftKey &other) const {
|
||||
return _value == other._value;
|
||||
}
|
||||
inline bool operator>(const DraftKey &other) const {
|
||||
return (other < *this);
|
||||
}
|
||||
inline bool operator<=(const DraftKey &other) const {
|
||||
return !(other < *this);
|
||||
}
|
||||
inline bool operator>=(const DraftKey &other) const {
|
||||
return !(*this < other);
|
||||
}
|
||||
inline bool operator!=(const DraftKey &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
inline explicit operator bool() const {
|
||||
return _value != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
DraftKey(int value) : _value(value) {
|
||||
}
|
||||
|
||||
static constexpr auto kLocalDraftIndex = -1;
|
||||
static constexpr auto kCloudDraftIndex = -2;
|
||||
static constexpr auto kScheduledDraftIndex = -3;
|
||||
static constexpr auto kEditDraftShift = ServerMaxMsgId;
|
||||
|
||||
int _value = 0;
|
||||
|
||||
};
|
||||
|
||||
using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>;
|
||||
|
||||
inline bool draftStringIsEmpty(const QString &text) {
|
||||
for_const (auto ch, text) {
|
||||
if (!ch.isSpace()) {
|
||||
|
||||
@@ -89,7 +89,7 @@ void CheckForSwitchInlineButton(not_null<HistoryItem*> item) {
|
||||
return;
|
||||
}
|
||||
if (const auto user = item->history()->peer->asUser()) {
|
||||
if (!user->isBot() || !user->botInfo->inlineReturnPeerId) {
|
||||
if (!user->isBot() || !user->botInfo->inlineReturnTo.key) {
|
||||
return;
|
||||
}
|
||||
if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
|
||||
|
||||
@@ -434,6 +434,12 @@ inline bool operator==(
|
||||
&& (a.scroll == b.scroll);
|
||||
}
|
||||
|
||||
inline bool operator!=(
|
||||
const MessageCursor &a,
|
||||
const MessageCursor &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
class FileClickHandler : public LeftButtonClickHandler {
|
||||
public:
|
||||
FileClickHandler(
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
|
||||
class BotCommand {
|
||||
public:
|
||||
@@ -34,7 +35,7 @@ struct BotInfo {
|
||||
Ui::Text::String text = { int(st::msgMinWidth) }; // description
|
||||
|
||||
QString startToken, startGroupToken, shareGameShortName;
|
||||
PeerId inlineReturnPeerId = 0;
|
||||
Dialogs::EntryState inlineReturnTo;
|
||||
};
|
||||
|
||||
class UserData : public PeerData {
|
||||
|
||||
@@ -1814,23 +1814,17 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
|
||||
} else {
|
||||
fillArchiveSearchMenu(_menu.get());
|
||||
}
|
||||
} else if (const auto history = row.key.history()) {
|
||||
Window::FillPeerMenu(
|
||||
} else {
|
||||
Window::FillDialogsEntryMenu(
|
||||
_controller,
|
||||
history->peer,
|
||||
_filterId,
|
||||
Dialogs::EntryState{
|
||||
.key = row.key,
|
||||
.section = Dialogs::EntryState::Section::ChatsList,
|
||||
.filterId = _filterId,
|
||||
},
|
||||
[&](const QString &text, Fn<void()> callback) {
|
||||
return _menu->addAction(text, std::move(callback));
|
||||
},
|
||||
Window::PeerMenuSource::ChatsList);
|
||||
} else if (const auto folder = row.key.folder()) {
|
||||
Window::FillFolderMenu(
|
||||
_controller,
|
||||
folder,
|
||||
[&](const QString &text, Fn<void()> callback) {
|
||||
return _menu->addAction(text, std::move(callback));
|
||||
},
|
||||
Window::PeerMenuSource::ChatsList);
|
||||
});
|
||||
}
|
||||
connect(_menu.get(), &QObject::destroyed, [=] {
|
||||
if (_menuRow.key) {
|
||||
|
||||
@@ -109,4 +109,21 @@ inline bool operator>=(const RowDescriptor &a, const RowDescriptor &b) {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
struct EntryState {
|
||||
enum class Section {
|
||||
History,
|
||||
Profile,
|
||||
ChatsList,
|
||||
Scheduled,
|
||||
Pinned,
|
||||
Replies,
|
||||
};
|
||||
|
||||
Key key;
|
||||
Section section = Section::History;
|
||||
FilterId filterId = 0;
|
||||
MsgId rootId = 0;
|
||||
MsgId currentReplyToId = 0;
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
|
||||
@@ -523,8 +523,10 @@ void Widget::refreshFolderTopBar() {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
_folderTopBar->setActiveChat(
|
||||
_openedFolder,
|
||||
HistoryView::TopBarWidget::Section::History,
|
||||
HistoryView::TopBarWidget::ActiveChat{
|
||||
.key = _openedFolder,
|
||||
.section = Dialogs::EntryState::Section::ChatsList,
|
||||
},
|
||||
nullptr);
|
||||
} else {
|
||||
_folderTopBar.destroy();
|
||||
@@ -1784,7 +1786,9 @@ bool Widget::onCancelSearch() {
|
||||
void Widget::onCancelSearchInChat() {
|
||||
cancelSearchRequest();
|
||||
if (_searchInChat) {
|
||||
if (Adaptive::OneColumn() && !controller()->selectingPeer()) {
|
||||
if (Adaptive::OneColumn()
|
||||
&& !controller()->selectingPeer()
|
||||
&& _filter->getLastText().trimmed().isEmpty()) {
|
||||
if (const auto peer = _searchInChat.peer()) {
|
||||
Ui::showPeerHistory(peer, ShowAtUnreadMsgId);
|
||||
//} else if (const auto feed = _searchInChat.feed()) { // #feed
|
||||
|
||||
@@ -171,7 +171,13 @@ void activateBotCommand(
|
||||
}
|
||||
}
|
||||
if (const auto m = CheckMainWidget(&msg->history()->session())) {
|
||||
Window::PeerMenuCreatePoll(m->controller(), msg->history()->peer, chosen, disabled);
|
||||
const auto replyToId = MsgId(0);
|
||||
Window::PeerMenuCreatePoll(
|
||||
m->controller(),
|
||||
msg->history()->peer,
|
||||
replyToId,
|
||||
chosen,
|
||||
disabled);
|
||||
}
|
||||
} break;
|
||||
|
||||
@@ -185,7 +191,7 @@ void activateBotCommand(
|
||||
if (samePeer) {
|
||||
Notify::switchInlineBotButtonReceived(session, QString::fromUtf8(button->data), bot, msg->id);
|
||||
return true;
|
||||
} else if (bot->isBot() && bot->botInfo->inlineReturnPeerId) {
|
||||
} else if (bot->isBot() && bot->botInfo->inlineReturnTo.key) {
|
||||
if (Notify::switchInlineBotButtonReceived(session, QString::fromUtf8(button->data))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -600,6 +600,14 @@ bool InnerWidget::elementShownUnread(not_null<const Element*> view) {
|
||||
return view->data()->unread();
|
||||
}
|
||||
|
||||
void InnerWidget::elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
}
|
||||
|
||||
void InnerWidget::elementHandleViaClick(not_null<UserData*> bot) {
|
||||
}
|
||||
|
||||
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
|
||||
memento->setFilter(std::move(_filter));
|
||||
memento->setAdmins(std::move(_admins));
|
||||
|
||||
@@ -116,6 +116,10 @@ public:
|
||||
not_null<const HistoryView::Element*> view) override;
|
||||
bool elementShownUnread(
|
||||
not_null<const HistoryView::Element*> view) override;
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override;
|
||||
void elementHandleViaClick(not_null<UserData*> bot) override;
|
||||
|
||||
~InnerWidget();
|
||||
|
||||
|
||||
@@ -179,22 +179,22 @@ void History::itemVanished(not_null<HistoryItem*> item) {
|
||||
}
|
||||
}
|
||||
|
||||
void History::setLocalDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
_localDraft = std::move(draft);
|
||||
}
|
||||
|
||||
void History::takeLocalDraft(History *from) {
|
||||
if (auto &draft = from->_localDraft) {
|
||||
if (!draft->textWithTags.text.isEmpty() && !_localDraft) {
|
||||
_localDraft = std::move(draft);
|
||||
|
||||
// Edit and reply to drafts can't migrate.
|
||||
// Cloud drafts do not migrate automatically.
|
||||
_localDraft->msgId = 0;
|
||||
}
|
||||
from->clearLocalDraft();
|
||||
session().api().saveDraftToCloudDelayed(from);
|
||||
void History::takeLocalDraft(not_null<History*> from) {
|
||||
const auto i = from->_drafts.find(Data::DraftKey::Local());
|
||||
if (i == end(from->_drafts)) {
|
||||
return;
|
||||
}
|
||||
auto &draft = i->second;
|
||||
if (!draft->textWithTags.text.isEmpty()
|
||||
&& !_drafts.contains(Data::DraftKey::Local())) {
|
||||
// Edit and reply to drafts can't migrate.
|
||||
// Cloud drafts do not migrate automatically.
|
||||
draft->msgId = 0;
|
||||
|
||||
setLocalDraft(std::move(draft));
|
||||
}
|
||||
from->clearLocalDraft();
|
||||
session().api().saveDraftToCloudDelayed(from);
|
||||
}
|
||||
|
||||
void History::createLocalDraftFromCloud() {
|
||||
@@ -227,9 +227,51 @@ void History::createLocalDraftFromCloud() {
|
||||
}
|
||||
}
|
||||
|
||||
void History::setCloudDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
_cloudDraft = std::move(draft);
|
||||
cloudDraftTextCache.clear();
|
||||
Data::Draft *History::draft(Data::DraftKey key) const {
|
||||
if (!key) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto i = _drafts.find(key);
|
||||
return (i != _drafts.end()) ? i->second.get() : nullptr;
|
||||
}
|
||||
|
||||
void History::setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const auto changingCloudDraft = (key == Data::DraftKey::Cloud());
|
||||
if (changingCloudDraft) {
|
||||
cloudDraftTextCache.clear();
|
||||
}
|
||||
if (draft) {
|
||||
_drafts[key] = std::move(draft);
|
||||
} else if (_drafts.remove(key) && changingCloudDraft) {
|
||||
updateChatListSortPosition();
|
||||
}
|
||||
}
|
||||
|
||||
const Data::HistoryDrafts &History::draftsMap() const {
|
||||
return _drafts;
|
||||
}
|
||||
|
||||
void History::setDraftsMap(Data::HistoryDrafts &&map) {
|
||||
for (auto &[key, draft] : _drafts) {
|
||||
map[key] = std::move(draft);
|
||||
}
|
||||
_drafts = std::move(map);
|
||||
}
|
||||
|
||||
void History::clearDraft(Data::DraftKey key) {
|
||||
setDraft(key, nullptr);
|
||||
}
|
||||
|
||||
void History::clearDrafts() {
|
||||
const auto changingCloudDraft = _drafts.contains(Data::DraftKey::Cloud());
|
||||
_drafts.clear();
|
||||
if (changingCloudDraft) {
|
||||
cloudDraftTextCache.clear();
|
||||
updateChatListSortPosition();
|
||||
}
|
||||
}
|
||||
|
||||
Data::Draft *History::createCloudDraft(const Data::Draft *fromDraft) {
|
||||
@@ -287,22 +329,6 @@ void History::clearSentDraftText(const QString &text) {
|
||||
accumulate_max(_lastSentDraftTime, base::unixtime::now());
|
||||
}
|
||||
|
||||
void History::setEditDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
_editDraft = std::move(draft);
|
||||
}
|
||||
|
||||
void History::clearLocalDraft() {
|
||||
_localDraft = nullptr;
|
||||
}
|
||||
|
||||
void History::clearCloudDraft() {
|
||||
if (_cloudDraft) {
|
||||
_cloudDraft = nullptr;
|
||||
cloudDraftTextCache.clear();
|
||||
updateChatListSortPosition();
|
||||
}
|
||||
}
|
||||
|
||||
void History::applyCloudDraft() {
|
||||
if (session().supportMode()) {
|
||||
updateChatListEntry();
|
||||
@@ -314,10 +340,6 @@ void History::applyCloudDraft() {
|
||||
}
|
||||
}
|
||||
|
||||
void History::clearEditDraft() {
|
||||
_editDraft = nullptr;
|
||||
}
|
||||
|
||||
void History::draftSavedToCloud() {
|
||||
updateChatListEntry();
|
||||
session().local().writeDrafts(this);
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "data/data_types.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_drafts.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "history/view/history_view_send_action.h"
|
||||
#include "base/observer.h"
|
||||
@@ -302,31 +303,48 @@ public:
|
||||
void eraseFromUnreadMentions(MsgId msgId);
|
||||
void addUnreadMentionsSlice(const MTPmessages_Messages &result);
|
||||
|
||||
Data::Draft *draft(Data::DraftKey key) const;
|
||||
void setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft);
|
||||
void clearDraft(Data::DraftKey key);
|
||||
|
||||
[[nodiscard]] const Data::HistoryDrafts &draftsMap() const;
|
||||
void setDraftsMap(Data::HistoryDrafts &&map);
|
||||
|
||||
Data::Draft *localDraft() const {
|
||||
return _localDraft.get();
|
||||
return draft(Data::DraftKey::Local());
|
||||
}
|
||||
Data::Draft *localEditDraft() const {
|
||||
return draft(Data::DraftKey::LocalEdit());
|
||||
}
|
||||
Data::Draft *cloudDraft() const {
|
||||
return _cloudDraft.get();
|
||||
return draft(Data::DraftKey::Cloud());
|
||||
}
|
||||
Data::Draft *editDraft() const {
|
||||
return _editDraft.get();
|
||||
void setLocalDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
setDraft(Data::DraftKey::Local(), std::move(draft));
|
||||
}
|
||||
void setLocalDraft(std::unique_ptr<Data::Draft> &&draft);
|
||||
void takeLocalDraft(History *from);
|
||||
void setCloudDraft(std::unique_ptr<Data::Draft> &&draft);
|
||||
void setLocalEditDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
setDraft(Data::DraftKey::LocalEdit(), std::move(draft));
|
||||
}
|
||||
void setCloudDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
setDraft(Data::DraftKey::Cloud(), std::move(draft));
|
||||
}
|
||||
void clearLocalDraft() {
|
||||
clearDraft(Data::DraftKey::Local());
|
||||
}
|
||||
void clearCloudDraft() {
|
||||
clearDraft(Data::DraftKey::Cloud());
|
||||
}
|
||||
void clearLocalEditDraft() {
|
||||
clearDraft(Data::DraftKey::LocalEdit());
|
||||
}
|
||||
void clearDrafts();
|
||||
Data::Draft *createCloudDraft(const Data::Draft *fromDraft);
|
||||
bool skipCloudDraft(const QString &text, MsgId replyTo, TimeId date) const;
|
||||
void setSentDraftText(const QString &text);
|
||||
void clearSentDraftText(const QString &text);
|
||||
void setEditDraft(std::unique_ptr<Data::Draft> &&draft);
|
||||
void clearLocalDraft();
|
||||
void clearCloudDraft();
|
||||
void takeLocalDraft(not_null<History*> from);
|
||||
void applyCloudDraft();
|
||||
void clearEditDraft();
|
||||
void draftSavedToCloud();
|
||||
Data::Draft *draft() {
|
||||
return _editDraft ? editDraft() : localDraft();
|
||||
}
|
||||
|
||||
const MessageIdsList &forwardDraft() const {
|
||||
return _forwardDraft;
|
||||
@@ -560,8 +578,7 @@ private:
|
||||
};
|
||||
std::unique_ptr<BuildingBlock> _buildingFrontBlock;
|
||||
|
||||
std::unique_ptr<Data::Draft> _localDraft, _cloudDraft;
|
||||
std::unique_ptr<Data::Draft> _editDraft;
|
||||
Data::HistoryDrafts _drafts;
|
||||
std::optional<QString> _lastSentDraftText;
|
||||
TimeId _lastSentDraftTime = 0;
|
||||
MessageIdsList _forwardDraft;
|
||||
|
||||
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <rpl/merge.h>
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/crash_reports.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_message.h"
|
||||
#include "history/view/media/history_view_media.h"
|
||||
@@ -1330,7 +1331,14 @@ void HistoryInner::mouseActionFinish(
|
||||
: FullMsgId();
|
||||
ActivateClickHandler(window(), activated, {
|
||||
button,
|
||||
QVariant::fromValue(pressedItemId)
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.itemId = pressedItemId,
|
||||
.elementDelegate = [weak = Ui::MakeWeak(this)] {
|
||||
return weak
|
||||
? HistoryInner::ElementDelegate().get()
|
||||
: nullptr;
|
||||
},
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2540,6 +2548,28 @@ bool HistoryInner::elementIsGifPaused() {
|
||||
return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
|
||||
}
|
||||
|
||||
void HistoryInner::elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
if (auto peer = Ui::getPeerForMouseAction()) { // old way
|
||||
auto bot = peer->isUser() ? peer->asUser() : nullptr;
|
||||
if (!bot) {
|
||||
if (const auto view = App::hoveredLinkItem()) {
|
||||
// may return nullptr
|
||||
bot = view->data()->fromOriginal()->asUser();
|
||||
}
|
||||
}
|
||||
Ui::showPeerHistory(peer, ShowAtTheEndMsgId);
|
||||
App::sendBotCommand(peer, bot, command);
|
||||
} else {
|
||||
App::insertBotCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryInner::elementHandleViaClick(not_null<UserData*> bot) {
|
||||
App::insertBotCommand('@' + bot->username);
|
||||
}
|
||||
|
||||
auto HistoryInner::getSelectionState() const
|
||||
-> HistoryView::TopBarWidget::SelectedState {
|
||||
auto result = HistoryView::TopBarWidget::SelectedState {};
|
||||
@@ -3434,6 +3464,18 @@ not_null<HistoryView::ElementDelegate*> HistoryInner::ElementDelegate() {
|
||||
bool elementShownUnread(not_null<const Element*> view) override {
|
||||
return view->data()->unread();
|
||||
}
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override {
|
||||
if (Instance) {
|
||||
Instance->elementSendBotCommand(command, context);
|
||||
}
|
||||
}
|
||||
void elementHandleViaClick(not_null<UserData*> bot) override {
|
||||
if (Instance) {
|
||||
Instance->elementHandleViaClick(bot);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static Result result;
|
||||
|
||||
@@ -92,6 +92,10 @@ public:
|
||||
const TextWithEntities &text,
|
||||
Fn<void()> hiddenCallback);
|
||||
bool elementIsGifPaused();
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context);
|
||||
void elementHandleViaClick(not_null<UserData*> bot);
|
||||
|
||||
void updateBotInfo(bool recount = true);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history_message.h"
|
||||
#include "history/view/history_view_service_message.h"
|
||||
#include "history/view/media/history_view_document.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "mainwindow.h"
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
@@ -45,7 +46,8 @@ void HistoryMessageVia::create(
|
||||
bot = owner->user(userId);
|
||||
maxWidth = st::msgServiceNameFont->width(
|
||||
tr::lng_inline_bot_via(tr::now, lt_inline_bot, '@' + bot->username));
|
||||
link = std::make_shared<LambdaClickHandler>([bot = this->bot] {
|
||||
link = std::make_shared<LambdaClickHandler>([bot = this->bot](
|
||||
ClickContext context) {
|
||||
if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) {
|
||||
if (const auto window = App::wnd()) {
|
||||
if (const auto controller = window->sessionController()) {
|
||||
@@ -54,7 +56,12 @@ void HistoryMessageVia::create(
|
||||
}
|
||||
}
|
||||
}
|
||||
App::insertBotCommand('@' + bot->username);
|
||||
const auto my = context.other.value<ClickHandlerContext>();
|
||||
if (const auto delegate = my.elementDelegate ? my.elementDelegate() : nullptr) {
|
||||
delegate->elementHandleViaClick(bot);
|
||||
} else {
|
||||
App::insertBotCommand('@' + bot->username);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/view/media/history_view_media.h" // AddTimestampLinks.
|
||||
#include "chat_helpers/stickers_emoji_pack.h"
|
||||
#include "main/main_session.h"
|
||||
#include "api/api_updates.h"
|
||||
#include "boxes/share_box.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "ui/toast/toast.h"
|
||||
@@ -1083,6 +1084,17 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
|
||||
_fromNameVersion = from ? from->nameVersion : 1;
|
||||
}
|
||||
|
||||
bool HistoryMessage::checkRepliesPts(const MTPMessageReplies &data) const {
|
||||
const auto channel = history()->peer->asChannel();
|
||||
const auto pts = channel
|
||||
? channel->pts()
|
||||
: history()->session().updates().pts();
|
||||
const auto repliesPts = data.match([&](const MTPDmessageReplies &data) {
|
||||
return data.vreplies_pts().v;
|
||||
});
|
||||
return (repliesPts >= pts);
|
||||
}
|
||||
|
||||
void HistoryMessage::setupForwardedComponent(const CreateConfig &config) {
|
||||
const auto forwarded = Get<HistoryMessageForwarded>();
|
||||
if (!forwarded) {
|
||||
@@ -1317,7 +1329,9 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) {
|
||||
setForwardsCount(message.vforwards().value_or(-1));
|
||||
setText(_media ? textWithEntities : EnsureNonEmpty(textWithEntities));
|
||||
if (const auto replies = message.vreplies()) {
|
||||
setReplies(*replies);
|
||||
if (checkRepliesPts(*replies)) {
|
||||
setReplies(*replies);
|
||||
}
|
||||
} else {
|
||||
clearReplies();
|
||||
}
|
||||
@@ -1464,7 +1478,7 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
|
||||
}
|
||||
|
||||
clearIsolatedEmoji();
|
||||
const auto context = Core::UiIntegration::Context{
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = &history()->session()
|
||||
};
|
||||
_text.setMarkedText(
|
||||
|
||||
@@ -236,6 +236,8 @@ private:
|
||||
const TextWithEntities &textWithEntities) const;
|
||||
void reapplyText();
|
||||
|
||||
[[nodiscard]] bool checkRepliesPts(const MTPMessageReplies &data) const;
|
||||
|
||||
QString _timeText;
|
||||
int _timeWidth = 0;
|
||||
|
||||
|
||||
@@ -617,6 +617,11 @@ bool HistoryService::updateDependencyItem() {
|
||||
return HistoryItem::updateDependencyItem();
|
||||
}
|
||||
|
||||
bool HistoryService::needCheck() const {
|
||||
return (GetDependentData() != nullptr)
|
||||
|| Has<HistoryServiceSelfDestruct>();
|
||||
}
|
||||
|
||||
QString HistoryService::inDialogsText(DrawInDialog way) const {
|
||||
return textcmdLink(1, TextUtilities::Clean(notificationText()));
|
||||
}
|
||||
|
||||
@@ -95,9 +95,7 @@ public:
|
||||
|
||||
Storage::SharedMediaTypesMask sharedMediaTypes() const override;
|
||||
|
||||
bool needCheck() const override {
|
||||
return false;
|
||||
}
|
||||
bool needCheck() const override;
|
||||
bool serviceMsg() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,13 +38,9 @@ namespace Layout {
|
||||
class ItemBase;
|
||||
class Widget;
|
||||
} // namespace Layout
|
||||
class Result;
|
||||
struct ResultSelected;
|
||||
} // namespace InlineBots
|
||||
|
||||
namespace Data {
|
||||
struct Draft;
|
||||
} // namespace Data
|
||||
|
||||
namespace Support {
|
||||
class Autocomplete;
|
||||
struct Contact;
|
||||
@@ -94,6 +90,10 @@ class TopBarWidget;
|
||||
class ContactStatus;
|
||||
class Element;
|
||||
class PinnedTracker;
|
||||
namespace Controls {
|
||||
class RecordLock;
|
||||
class VoiceRecordBar;
|
||||
} // namespace Controls
|
||||
} // namespace HistoryView
|
||||
|
||||
class DragArea;
|
||||
@@ -104,10 +104,10 @@ class HistoryInner;
|
||||
struct HistoryMessageMarkupButton;
|
||||
|
||||
class HistoryWidget final : public Window::AbstractSectionWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using FieldHistoryAction = Ui::InputField::HistoryAction;
|
||||
using RecordLock = HistoryView::Controls::RecordLock;
|
||||
using VoiceRecordBar = HistoryView::Controls::VoiceRecordBar;
|
||||
|
||||
HistoryWidget(
|
||||
QWidget *parent,
|
||||
@@ -201,9 +201,6 @@ public:
|
||||
void updatePreview();
|
||||
void previewCancel();
|
||||
|
||||
bool recordingAnimationCallback(crl::time now);
|
||||
void stopRecording(bool send);
|
||||
|
||||
void escape();
|
||||
|
||||
void sendBotCommand(
|
||||
@@ -238,7 +235,11 @@ public:
|
||||
|
||||
void updateFieldSubmitSettings();
|
||||
|
||||
void activate();
|
||||
void setInnerFocus();
|
||||
[[nodiscard]] rpl::producer<> cancelRequests() const {
|
||||
return _cancelRequests.events();
|
||||
}
|
||||
|
||||
void updateNotifyControls();
|
||||
|
||||
@@ -287,40 +288,6 @@ protected:
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
|
||||
signals:
|
||||
void cancelled();
|
||||
|
||||
public slots:
|
||||
void onScroll();
|
||||
|
||||
void activate();
|
||||
void onTextChange();
|
||||
|
||||
void onFieldTabbed();
|
||||
|
||||
void onWindowVisibleChanged();
|
||||
|
||||
void onFieldFocused();
|
||||
void onFieldResize();
|
||||
void onCheckFieldAutocomplete();
|
||||
void onScrollTimer();
|
||||
|
||||
void onDraftSaveDelayed();
|
||||
void onDraftSave(bool delayed = false);
|
||||
void onCloudDraftSave();
|
||||
|
||||
void onUpdateHistoryItems();
|
||||
|
||||
// checks if we are too close to the top or to the bottom
|
||||
// in the scroll area and preloads history if needed
|
||||
void preloadHistoryIfNeeded();
|
||||
|
||||
private slots:
|
||||
void onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method);
|
||||
void onMentionInsert(UserData *user);
|
||||
void onInlineBotCancel();
|
||||
void onMembersDropdownShow();
|
||||
|
||||
private:
|
||||
using TabbedPanel = ChatHelpers::TabbedPanel;
|
||||
using TabbedSelector = ChatHelpers::TabbedSelector;
|
||||
@@ -345,10 +312,38 @@ private:
|
||||
friend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; };
|
||||
|
||||
void initTabbedSelector();
|
||||
void initVoiceRecordBar();
|
||||
void refreshTabbedPanel();
|
||||
void createTabbedPanel();
|
||||
void setTabbedPanel(std::unique_ptr<TabbedPanel> panel);
|
||||
void updateField();
|
||||
void fieldChanged();
|
||||
void fieldTabbed();
|
||||
void fieldFocused();
|
||||
void fieldResized();
|
||||
|
||||
void insertHashtagOrBotCommand(
|
||||
QString str,
|
||||
FieldAutocomplete::ChooseMethod method);
|
||||
void insertMention(UserData *user);
|
||||
void cancelInlineBot();
|
||||
void saveDraft(bool delayed = false);
|
||||
void saveCloudDraft();
|
||||
void saveDraftDelayed();
|
||||
void checkFieldAutocomplete();
|
||||
void showMembersDropdown();
|
||||
void windowIsVisibleChanged();
|
||||
|
||||
// Checks if we are too close to the top or to the bottom
|
||||
// in the scroll area and preloads history if needed.
|
||||
void preloadHistoryIfNeeded();
|
||||
|
||||
void handleScroll();
|
||||
void scrollByTimer();
|
||||
void updateHistoryItemsByTimer();
|
||||
|
||||
[[nodiscard]] Dialogs::EntryState computeDialogsEntryState() const;
|
||||
void refreshTopBarActiveChat();
|
||||
|
||||
void requestMessageData(MsgId msgId);
|
||||
void messageDataReceived(ChannelData *channel, MsgId msgId);
|
||||
@@ -395,11 +390,6 @@ private:
|
||||
|
||||
void animationCallback();
|
||||
void updateOverStates(QPoint pos);
|
||||
void recordDone(QByteArray result, VoiceWaveform waveform, int samples);
|
||||
void recordUpdate(ushort level, int samples);
|
||||
void recordStartCallback();
|
||||
void recordStopCallback(bool active);
|
||||
void recordUpdateCallback(QPoint globalPos);
|
||||
void chooseAttach();
|
||||
void historyDownAnimationFinish();
|
||||
void unreadMentionsAnimationFinish();
|
||||
@@ -457,6 +447,7 @@ private:
|
||||
bool replyToNextMessage();
|
||||
[[nodiscard]] bool showSlowmodeError();
|
||||
|
||||
void hideChildWidgets();
|
||||
void hideSelectorControlsAnimated();
|
||||
int countMembersDropdownHeightMax() const;
|
||||
|
||||
@@ -484,10 +475,7 @@ private:
|
||||
int wasScrollTop,
|
||||
int nowScrollTop);
|
||||
|
||||
void sendInlineResult(
|
||||
not_null<InlineBots::Result*> result,
|
||||
not_null<UserData*> bot,
|
||||
Api::SendOptions options);
|
||||
void sendInlineResult(InlineBots::ResultSelected result);
|
||||
|
||||
void drawField(Painter &p, const QRect &rect);
|
||||
void paintEditHeader(
|
||||
@@ -495,7 +483,6 @@ private:
|
||||
const QRect &rect,
|
||||
int left,
|
||||
int top) const;
|
||||
void drawRecording(Painter &p, float64 recordActive);
|
||||
void drawRestrictedWrite(Painter &p, const QString &error);
|
||||
bool paintShowAnimationFrame();
|
||||
|
||||
@@ -535,8 +522,9 @@ private:
|
||||
// This one is syntetic.
|
||||
void synteticScrollToY(int y);
|
||||
|
||||
void writeDrafts(Data::Draft **localDraft, Data::Draft **editDraft);
|
||||
void writeDrafts(History *history);
|
||||
void writeDrafts();
|
||||
void writeDraftTexts();
|
||||
void writeDraftCursors();
|
||||
void setFieldText(
|
||||
const TextWithTags &textWithTags,
|
||||
TextUpdateEvents events = 0,
|
||||
@@ -566,6 +554,8 @@ private:
|
||||
void inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result);
|
||||
void inlineBotResolveFail(const RPCError &error, const QString &username);
|
||||
|
||||
bool isRecording() const;
|
||||
|
||||
bool isBotStart() const;
|
||||
bool isBlocked() const;
|
||||
bool isJoinChannel() const;
|
||||
@@ -579,6 +569,8 @@ private:
|
||||
void setupScheduledToggle();
|
||||
void refreshScheduledToggle();
|
||||
|
||||
bool kbWasHidden() const;
|
||||
|
||||
MTP::Sender _api;
|
||||
MsgId _replyToId = 0;
|
||||
Ui::Text::String _replyToName;
|
||||
@@ -647,7 +639,7 @@ private:
|
||||
|
||||
int _lastScrollTop = 0; // gifs optimization
|
||||
crl::time _lastScrolled = 0;
|
||||
QTimer _updateHistoryItems;
|
||||
base::Timer _updateHistoryItems;
|
||||
|
||||
crl::time _lastUserScrolled = 0;
|
||||
bool _synteticScrollEvent = false;
|
||||
@@ -673,7 +665,7 @@ private:
|
||||
|
||||
std::unique_ptr<HistoryView::ContactStatus> _contactStatus;
|
||||
|
||||
object_ptr<Ui::SendButton> _send;
|
||||
const std::shared_ptr<Ui::SendButton> _send;
|
||||
object_ptr<Ui::FlatButton> _unblock;
|
||||
object_ptr<Ui::FlatButton> _botStart;
|
||||
object_ptr<Ui::FlatButton> _joinChannel;
|
||||
@@ -685,22 +677,11 @@ private:
|
||||
object_ptr<Ui::IconButton> _botCommandStart;
|
||||
object_ptr<Ui::SilentToggle> _silent = { nullptr };
|
||||
object_ptr<Ui::IconButton> _scheduled = { nullptr };
|
||||
const std::unique_ptr<VoiceRecordBar> _voiceRecordBar;
|
||||
bool _cmdStartShown = false;
|
||||
object_ptr<Ui::InputField> _field;
|
||||
bool _recording = false;
|
||||
bool _inField = false;
|
||||
bool _inReplyEditForward = false;
|
||||
bool _inClickable = false;
|
||||
int _recordingSamples = 0;
|
||||
int _recordCancelWidth;
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||
// This can animate for a very long time (like in music playing),
|
||||
// so it should be a Basic, not a Simple animation.
|
||||
Ui::Animations::Basic _recordingAnimation;
|
||||
anim::value _recordingLevel;
|
||||
|
||||
bool kbWasHidden() const;
|
||||
|
||||
bool _kbShown = false;
|
||||
HistoryItem *_kbReplyTo = nullptr;
|
||||
@@ -708,7 +689,7 @@ private:
|
||||
QPointer<BotKeyboard> _keyboard;
|
||||
|
||||
object_ptr<Ui::InnerDropdown> _membersDropdown = { nullptr };
|
||||
QTimer _membersDropdownShowTimer;
|
||||
base::Timer _membersDropdownShowTimer;
|
||||
|
||||
object_ptr<InlineBots::Layout::Widget> _inlineResults = { nullptr };
|
||||
std::unique_ptr<TabbedPanel> _tabbedPanel;
|
||||
@@ -727,7 +708,7 @@ private:
|
||||
Window::SlideDirection _showDirection;
|
||||
QPixmap _cacheUnder, _cacheOver;
|
||||
|
||||
QTimer _scrollTimer;
|
||||
base::Timer _scrollTimer;
|
||||
int32 _scrollDelta = 0;
|
||||
|
||||
MsgId _highlightedMessageId = 0;
|
||||
@@ -737,7 +718,8 @@ private:
|
||||
|
||||
crl::time _saveDraftStart = 0;
|
||||
bool _saveDraftText = false;
|
||||
QTimer _saveDraftTimer, _saveCloudDraftTimer;
|
||||
base::Timer _saveDraftTimer;
|
||||
base::Timer _saveCloudDraftTimer;
|
||||
|
||||
base::weak_ptr<Ui::Toast::Instance> _topToast;
|
||||
|
||||
@@ -746,4 +728,6 @@ private:
|
||||
|
||||
int _topDelta = 0;
|
||||
|
||||
rpl::event_stream<> _cancelRequests;
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Api {
|
||||
enum class SendProgressType;
|
||||
struct SendOptions;
|
||||
} // namespace Api
|
||||
|
||||
class History;
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
|
||||
struct MessageToEdit {
|
||||
FullMsgId fullId;
|
||||
Api::SendOptions options;
|
||||
TextWithTags textWithTags;
|
||||
};
|
||||
struct VoiceToSend {
|
||||
QByteArray bytes;
|
||||
VoiceWaveform waveform;
|
||||
int duration = 0;
|
||||
Api::SendOptions options;
|
||||
};
|
||||
struct SendActionUpdate {
|
||||
Api::SendProgressType type = Api::SendProgressType();
|
||||
int progress = 0;
|
||||
};
|
||||
|
||||
struct SetHistoryArgs {
|
||||
required<History*> history;
|
||||
Fn<bool()> showSlowmodeError;
|
||||
rpl::producer<int> slowmodeSecondsLeft;
|
||||
rpl::producer<bool> sendDisabledBySlowmode;
|
||||
rpl::producer<std::optional<QString>> writeRestriction;
|
||||
};
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
@@ -10,12 +10,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/required.h"
|
||||
#include "api/api_common.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "history/view/controls/compose_controls_common.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
|
||||
class History;
|
||||
class FieldAutocomplete;
|
||||
|
||||
namespace SendMenu {
|
||||
enum class Type;
|
||||
} // namespace SendMenu
|
||||
|
||||
namespace ChatHelpers {
|
||||
class TabbedPanel;
|
||||
@@ -24,6 +32,8 @@ class TabbedSelector;
|
||||
|
||||
namespace Data {
|
||||
struct MessagePosition;
|
||||
struct Draft;
|
||||
class DraftKey;
|
||||
} // namespace Data
|
||||
|
||||
namespace InlineBots {
|
||||
@@ -55,67 +65,60 @@ enum class SendProgressType;
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
namespace Controls {
|
||||
class VoiceRecordBar;
|
||||
} // namespace Controls
|
||||
|
||||
class FieldHeader;
|
||||
|
||||
class ComposeControls final {
|
||||
public:
|
||||
using FileChosen = ChatHelpers::TabbedSelector::FileChosen;
|
||||
using PhotoChosen = ChatHelpers::TabbedSelector::PhotoChosen;
|
||||
using InlineChosen = ChatHelpers::TabbedSelector::InlineChosen;
|
||||
|
||||
using MessageToEdit = Controls::MessageToEdit;
|
||||
using VoiceToSend = Controls::VoiceToSend;
|
||||
using SendActionUpdate = Controls::SendActionUpdate;
|
||||
using SetHistoryArgs = Controls::SetHistoryArgs;
|
||||
using FieldHistoryAction = Ui::InputField::HistoryAction;
|
||||
|
||||
enum class Mode {
|
||||
Normal,
|
||||
Scheduled,
|
||||
};
|
||||
|
||||
struct MessageToEdit {
|
||||
FullMsgId fullId;
|
||||
Api::SendOptions options;
|
||||
TextWithTags textWithTags;
|
||||
};
|
||||
struct VoiceToSend {
|
||||
QByteArray bytes;
|
||||
VoiceWaveform waveform;
|
||||
int duration = 0;
|
||||
};
|
||||
struct SendActionUpdate {
|
||||
Api::SendProgressType type = Api::SendProgressType();
|
||||
int progress = 0;
|
||||
};
|
||||
|
||||
ComposeControls(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Window::SessionController*> window,
|
||||
Mode mode);
|
||||
Mode mode,
|
||||
SendMenu::Type sendMenuType);
|
||||
~ComposeControls();
|
||||
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
struct SetHistoryArgs {
|
||||
required<History*> history;
|
||||
Fn<bool()> showSlowmodeError;
|
||||
rpl::producer<int> slowmodeSecondsLeft;
|
||||
rpl::producer<bool> sendDisabledBySlowmode;
|
||||
rpl::producer<std::optional<QString>> writeRestriction;
|
||||
};
|
||||
void setHistory(SetHistoryArgs &&args);
|
||||
void setCurrentDialogsEntryState(Dialogs::EntryState state);
|
||||
|
||||
void finishAnimating();
|
||||
|
||||
void move(int x, int y);
|
||||
void resizeToWidth(int width);
|
||||
void setAutocompleteBoundingRect(QRect rect);
|
||||
[[nodiscard]] rpl::producer<int> height() const;
|
||||
[[nodiscard]] int heightCurrent() const;
|
||||
|
||||
bool focus();
|
||||
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
||||
[[nodiscard]] rpl::producer<> sendRequests() const;
|
||||
[[nodiscard]] rpl::producer<Api::SendOptions> sendRequests() const;
|
||||
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
|
||||
[[nodiscard]] rpl::producer<QString> sendCommandRequests() const;
|
||||
[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;
|
||||
[[nodiscard]] rpl::producer<> attachRequests() const;
|
||||
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
|
||||
[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;
|
||||
[[nodiscard]] rpl::producer<Data::MessagePosition> scrollRequests() const;
|
||||
[[nodiscard]] rpl::producer<not_null<QKeyEvent*>> keyEvents() const;
|
||||
[[nodiscard]] auto inlineResultChosen() const
|
||||
-> rpl::producer<ChatHelpers::TabbedSelector::InlineChosen>;
|
||||
[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;
|
||||
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
|
||||
|
||||
using MimeDataHook = Fn<bool(
|
||||
@@ -134,6 +137,7 @@ public:
|
||||
void showForGrab();
|
||||
void showStarted();
|
||||
void showFinished();
|
||||
void raisePanels();
|
||||
|
||||
void editMessage(FullMsgId id);
|
||||
void cancelEditMessage();
|
||||
@@ -141,17 +145,34 @@ public:
|
||||
void replyToMessage(FullMsgId id);
|
||||
void cancelReplyMessage();
|
||||
|
||||
bool handleCancelRequest();
|
||||
|
||||
[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
|
||||
[[nodiscard]] WebPageId webPageId() const;
|
||||
void setText(const TextWithTags &text);
|
||||
void clear();
|
||||
void hidePanelsAnimated();
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
|
||||
[[nodiscard]] bool isLockPresent() const;
|
||||
[[nodiscard]] bool isRecording() const;
|
||||
|
||||
void applyDraft(
|
||||
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
|
||||
|
||||
private:
|
||||
enum class TextUpdateEvent {
|
||||
//SaveDraft = (1 << 0),
|
||||
SaveDraft = (1 << 0),
|
||||
SendTyping = (1 << 1),
|
||||
};
|
||||
enum class DraftType {
|
||||
Normal,
|
||||
Edit,
|
||||
};
|
||||
enum class SendRequestType {
|
||||
Text,
|
||||
Voice,
|
||||
};
|
||||
using TextUpdateEvents = base::flags<TextUpdateEvent>;
|
||||
friend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; };
|
||||
|
||||
@@ -161,6 +182,9 @@ private:
|
||||
void initSendButton();
|
||||
void initWebpageProcess();
|
||||
void initWriteRestriction();
|
||||
void initVoiceRecordBar();
|
||||
void initAutocomplete();
|
||||
void updateSubmitSettings();
|
||||
void updateSendButtonType();
|
||||
void updateHeight();
|
||||
void updateWrappingVisibility();
|
||||
@@ -169,28 +193,62 @@ private:
|
||||
void updateOuterGeometry(QRect rect);
|
||||
void paintBackground(QRect clip);
|
||||
|
||||
[[nodiscard]] auto computeSendButtonType() const;
|
||||
[[nodiscard]] SendMenu::Type sendMenuType() const;
|
||||
[[nodiscard]] SendMenu::Type sendButtonMenuType() const;
|
||||
|
||||
void sendSilent();
|
||||
void sendScheduled();
|
||||
[[nodiscard]] auto sendContentRequests(
|
||||
SendRequestType requestType = SendRequestType::Text) const;
|
||||
|
||||
void orderControls();
|
||||
void checkAutocomplete();
|
||||
void updateStickersByEmoji();
|
||||
void updateFieldPlaceholder();
|
||||
void editMessage(not_null<HistoryItem*> item);
|
||||
|
||||
void escape();
|
||||
void fieldChanged();
|
||||
void fieldTabbed();
|
||||
void toggleTabbedSelectorMode();
|
||||
void createTabbedPanel();
|
||||
void setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);
|
||||
|
||||
void setTextFromEditingMessage(not_null<HistoryItem*> item);
|
||||
|
||||
void recordUpdated(quint16 level, int samples);
|
||||
void recordDone(QByteArray result, VoiceWaveform waveform, int samples);
|
||||
|
||||
bool recordingAnimationCallback(crl::time now);
|
||||
void stopRecording(bool send);
|
||||
|
||||
void recordStartCallback();
|
||||
void recordStopCallback(bool active);
|
||||
void recordUpdateCallback(QPoint globalPos);
|
||||
|
||||
bool showRecordButton() const;
|
||||
void drawRecording(Painter &p, float64 recordActive);
|
||||
void drawRestrictedWrite(Painter &p, const QString &error);
|
||||
void updateOverStates(QPoint pos);
|
||||
bool updateBotCommandShown();
|
||||
|
||||
void cancelInlineBot();
|
||||
void clearInlineBot();
|
||||
void inlineBotChanged();
|
||||
|
||||
// Look in the _field for the inline bot and query string.
|
||||
void updateInlineBotQuery();
|
||||
|
||||
// Request to show results in the emoji panel.
|
||||
void applyInlineBotQuery(UserData *bot, const QString &query);
|
||||
|
||||
void inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result);
|
||||
void inlineBotResolveFail(const RPCError &error, const QString &username);
|
||||
|
||||
[[nodiscard]] Data::DraftKey draftKey(
|
||||
DraftType type = DraftType::Normal) const;
|
||||
[[nodiscard]] Data::DraftKey draftKeyCurrent() const;
|
||||
void saveDraft(bool delayed = false);
|
||||
void saveDraftDelayed();
|
||||
|
||||
void writeDrafts();
|
||||
void writeDraftTexts();
|
||||
void writeDraftCursors();
|
||||
void setFieldText(
|
||||
const TextWithTags &textWithTags,
|
||||
TextUpdateEvents events = 0,
|
||||
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
|
||||
void clearFieldText(
|
||||
TextUpdateEvents events = 0,
|
||||
FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear);
|
||||
void saveFieldToHistoryLocalDraft();
|
||||
|
||||
const not_null<QWidget*> _parent;
|
||||
const not_null<Window::SessionController*> _window;
|
||||
@@ -204,41 +262,51 @@ private:
|
||||
const std::unique_ptr<Ui::RpWidget> _wrap;
|
||||
const std::unique_ptr<Ui::RpWidget> _writeRestricted;
|
||||
|
||||
const not_null<Ui::SendButton*> _send;
|
||||
const std::shared_ptr<Ui::SendButton> _send;
|
||||
const not_null<Ui::IconButton*> _attachToggle;
|
||||
const not_null<Ui::EmojiButton*> _tabbedSelectorToggle;
|
||||
const not_null<Ui::InputField*> _field;
|
||||
const not_null<Ui::IconButton*> _botCommandStart;
|
||||
|
||||
std::unique_ptr<InlineBots::Layout::Widget> _inlineResults;
|
||||
std::unique_ptr<ChatHelpers::TabbedPanel> _tabbedPanel;
|
||||
std::unique_ptr<FieldAutocomplete> _autocomplete;
|
||||
|
||||
friend class FieldHeader;
|
||||
const std::unique_ptr<FieldHeader> _header;
|
||||
const std::unique_ptr<Controls::VoiceRecordBar> _voiceRecordBar;
|
||||
|
||||
const SendMenu::Type _sendMenuType;
|
||||
|
||||
rpl::event_stream<Api::SendOptions> _sendCustomRequests;
|
||||
rpl::event_stream<> _cancelRequests;
|
||||
rpl::event_stream<FileChosen> _fileChosen;
|
||||
rpl::event_stream<PhotoChosen> _photoChosen;
|
||||
rpl::event_stream<ChatHelpers::TabbedSelector::InlineChosen> _inlineResultChosen;
|
||||
rpl::event_stream<InlineChosen> _inlineResultChosen;
|
||||
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
|
||||
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
|
||||
rpl::event_stream<QString> _sendCommandRequests;
|
||||
|
||||
TextWithTags _localSavedText;
|
||||
TextUpdateEvents _textUpdateEvents;
|
||||
TextUpdateEvents _textUpdateEvents = TextUpdateEvents()
|
||||
| TextUpdateEvent::SaveDraft
|
||||
| TextUpdateEvent::SendTyping;
|
||||
Dialogs::EntryState _currentDialogsEntryState;
|
||||
|
||||
bool _recording = false;
|
||||
bool _inField = false;
|
||||
//bool _inReplyEditForward = false;
|
||||
//bool _inClickable = false;
|
||||
int _recordingSamples = 0;
|
||||
int _recordCancelWidth;
|
||||
rpl::lifetime _recordingLifetime;
|
||||
crl::time _saveDraftStart = 0;
|
||||
bool _saveDraftText = false;
|
||||
base::Timer _saveDraftTimer;
|
||||
|
||||
UserData *_inlineBot = nullptr;
|
||||
QString _inlineBotUsername;
|
||||
bool _inlineLookingUpBot = false;
|
||||
mtpRequestId _inlineBotResolveRequestId = 0;
|
||||
bool _isInlineBot = false;
|
||||
bool _botCommandShown = false;
|
||||
|
||||
Fn<void()> _previewCancel;
|
||||
bool _previewCancelled = false;
|
||||
|
||||
rpl::lifetime _uploaderSubscriptions;
|
||||
|
||||
// This can animate for a very long time (like in music playing),
|
||||
// so it should be a Basic, not a Simple animation.
|
||||
Ui::Animations::Basic _recordingAnimation;
|
||||
anim::value _recordingLevel;
|
||||
|
||||
Fn<void()> _raiseEmojiSuggestions;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "base/timer.h"
|
||||
#include "history/view/controls/compose_controls_common.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
struct VoiceData;
|
||||
|
||||
namespace Ui {
|
||||
class SendButton;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
|
||||
class VoiceRecordButton;
|
||||
class ListenWrap;
|
||||
class RecordLock;
|
||||
|
||||
class VoiceRecordBar final : public Ui::RpWidget {
|
||||
public:
|
||||
using SendActionUpdate = Controls::SendActionUpdate;
|
||||
using VoiceToSend = Controls::VoiceToSend;
|
||||
|
||||
VoiceRecordBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Ui::RpWidget*> sectionWidget,
|
||||
not_null<Window::SessionController*> controller,
|
||||
std::shared_ptr<Ui::SendButton> send,
|
||||
int recorderHeight);
|
||||
VoiceRecordBar(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
std::shared_ptr<Ui::SendButton> send,
|
||||
int recorderHeight);
|
||||
~VoiceRecordBar();
|
||||
|
||||
void startRecording();
|
||||
void finishAnimating();
|
||||
void hideFast();
|
||||
|
||||
void orderControls();
|
||||
|
||||
[[nodiscard]] rpl::producer<SendActionUpdate> sendActionUpdates() const;
|
||||
[[nodiscard]] rpl::producer<VoiceToSend> sendVoiceRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> recordingStateChanges() const;
|
||||
[[nodiscard]] rpl::producer<bool> lockShowStarts() const;
|
||||
[[nodiscard]] rpl::producer<> updateSendButtonTypeRequests() const;
|
||||
|
||||
void requestToSendWithOptions(Api::SendOptions options);
|
||||
|
||||
void setLockBottom(rpl::producer<int> &&bottom);
|
||||
void setSendButtonGeometryValue(rpl::producer<QRect> &&geometry);
|
||||
void setEscFilter(Fn<bool()> &&callback);
|
||||
void setStartRecordingFilter(Fn<bool()> &&callback);
|
||||
|
||||
[[nodiscard]] bool isRecording() const;
|
||||
[[nodiscard]] bool isLockPresent() const;
|
||||
[[nodiscard]] bool isListenState() const;
|
||||
|
||||
private:
|
||||
enum class StopType {
|
||||
Cancel,
|
||||
Send,
|
||||
Listen,
|
||||
};
|
||||
|
||||
void init();
|
||||
|
||||
void updateMessageGeometry();
|
||||
void updateLockGeometry();
|
||||
|
||||
void recordUpdated(quint16 level, int samples);
|
||||
|
||||
bool recordingAnimationCallback(crl::time now);
|
||||
|
||||
void stop(bool send);
|
||||
void stopRecording(StopType type);
|
||||
void visibilityAnimate(bool show, Fn<void()> &&callback);
|
||||
|
||||
bool showRecordButton() const;
|
||||
void drawDuration(Painter &p);
|
||||
void drawRedCircle(Painter &p);
|
||||
void drawMessage(Painter &p, float64 recordActive);
|
||||
|
||||
void startRedCircleAnimation();
|
||||
void installClickOutsideFilter();
|
||||
void installListenStateFilter();
|
||||
|
||||
bool isTypeRecord() const;
|
||||
bool hasDuration() const;
|
||||
|
||||
void hideAnimated();
|
||||
void finish();
|
||||
|
||||
void activeAnimate(bool active);
|
||||
float64 showAnimationRatio() const;
|
||||
float64 showListenAnimationRatio() const;
|
||||
float64 activeAnimationRatio() const;
|
||||
|
||||
void computeAndSetLockProgress(QPoint globalPos);
|
||||
|
||||
const not_null<Ui::RpWidget*> _sectionWidget;
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const std::shared_ptr<Ui::SendButton> _send;
|
||||
const std::unique_ptr<RecordLock> _lock;
|
||||
const std::unique_ptr<VoiceRecordButton> _level;
|
||||
std::unique_ptr<ListenWrap> _listen;
|
||||
|
||||
base::Timer _startTimer;
|
||||
|
||||
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
|
||||
rpl::event_stream<VoiceToSend> _sendVoiceRequests;
|
||||
rpl::event_stream<> _listenChanges;
|
||||
|
||||
int _centerY = 0;
|
||||
QRect _redCircleRect;
|
||||
QRect _durationRect;
|
||||
QRect _messageRect;
|
||||
|
||||
Ui::Text::String _message;
|
||||
|
||||
Fn<bool()> _escFilter;
|
||||
Fn<bool()> _startRecordingFilter;
|
||||
|
||||
rpl::variable<bool> _recording = false;
|
||||
rpl::variable<bool> _inField = false;
|
||||
rpl::variable<bool> _lockShowing = false;
|
||||
int _recordingSamples = 0;
|
||||
float64 _redCircleProgress = 0.;
|
||||
|
||||
const style::font &_cancelFont;
|
||||
|
||||
rpl::lifetime _recordingLifetime;
|
||||
|
||||
Ui::Animations::Simple _showLockAnimation;
|
||||
Ui::Animations::Simple _showListenAnimation;
|
||||
Ui::Animations::Simple _activeAnimation;
|
||||
Ui::Animations::Simple _showAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
@@ -0,0 +1,748 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/controls/history_view_voice_record_button.h"
|
||||
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
#include <QMatrix>
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kSegmentsCount = 12;
|
||||
constexpr auto kMajorDegreeOffset = 360 / kSegmentsCount;
|
||||
constexpr auto kSixtyDegrees = 60;
|
||||
|
||||
constexpr auto kEnterIdleAnimationDuration = crl::time(1200);
|
||||
|
||||
constexpr auto kRotationSpeed = 0.36 * 0.1;
|
||||
|
||||
constexpr auto kRandomAdditionFactor = 0.15;
|
||||
|
||||
constexpr auto kIdleRadiusGlobalFactor = 0.56;
|
||||
constexpr auto kIdleRadiusFactor = 0.15 * 0.5;
|
||||
|
||||
constexpr auto kOpacityMajor = 0.30;
|
||||
constexpr auto kOpacityMinor = 0.15;
|
||||
|
||||
constexpr auto kIdleRotationSpeed = 0.2;
|
||||
constexpr auto kIdleRotateDiff = 0.1 * kIdleRotationSpeed;
|
||||
|
||||
constexpr auto kWaveAngle = 0.03;
|
||||
|
||||
constexpr auto kAnimationSpeedMajor = 1.5 - 0.65;
|
||||
constexpr auto kAnimationSpeedMinor = 1.5 - 0.45;
|
||||
constexpr auto kAnimationSpeedCircle = 1.5 - 0.25;
|
||||
|
||||
constexpr auto kAmplitudeDiffFactorMax = 500. - 100.;
|
||||
constexpr auto kAmplitudeDiffFactorMajor = 300. - 100.;
|
||||
constexpr auto kAmplitudeDiffFactorMinor = 400. - 100.;
|
||||
|
||||
constexpr auto kFlingDistanceFactorMajor = 8 * 16;
|
||||
constexpr auto kFlingDistanceFactorMinor = 20 * 16;
|
||||
|
||||
constexpr auto kFlingInAnimationDurationMajor = 200;
|
||||
constexpr auto kFlingInAnimationDurationMinor = 350;
|
||||
constexpr auto kFlingOutAnimationDurationMajor = 220;
|
||||
constexpr auto kFlingOutAnimationDurationMinor = 380;
|
||||
|
||||
constexpr auto kSineWaveSpeedMajor = 0.02 * 0.2;
|
||||
constexpr auto kSineWaveSpeedMinor = 0.026 * 0.2;
|
||||
|
||||
constexpr auto kSmallWaveRadius = 0.55;
|
||||
|
||||
constexpr auto kFlingDistance = 0.50;
|
||||
|
||||
constexpr auto kMinDivider = 100.;
|
||||
|
||||
constexpr auto kMaxAmplitude = 1800.;
|
||||
|
||||
constexpr auto kZeroPoint = QPointF(0, 0);
|
||||
|
||||
template <typename Number>
|
||||
void Normalize(Number &value, Number right) {
|
||||
if (value >= right) {
|
||||
value -= right;
|
||||
}
|
||||
}
|
||||
|
||||
float64 RandomAdditional() {
|
||||
return (rand_value<int>() % 100 / 100.);
|
||||
}
|
||||
|
||||
void PerformAnimation(
|
||||
rpl::producer<crl::time> &&animationTicked,
|
||||
Fn<void(float64)> &&applyValue,
|
||||
Fn<void()> &&finishCallback,
|
||||
float64 duration,
|
||||
float64 from,
|
||||
float64 to,
|
||||
rpl::lifetime &lifetime) {
|
||||
lifetime.destroy();
|
||||
const auto animValue =
|
||||
lifetime.make_state<anim::value>(from, to);
|
||||
const auto animStarted = crl::now();
|
||||
std::move(
|
||||
animationTicked
|
||||
) | rpl::start_with_next([=,
|
||||
applyValue = std::move(applyValue),
|
||||
finishCallback = std::move(finishCallback),
|
||||
&lifetime](crl::time now) mutable {
|
||||
const auto dt = anim::Disabled()
|
||||
? 1.
|
||||
: ((now - animStarted) / duration);
|
||||
if (dt >= 1.) {
|
||||
animValue->finish();
|
||||
applyValue(animValue->current());
|
||||
lifetime.destroy();
|
||||
if (finishCallback) {
|
||||
finishCallback();
|
||||
}
|
||||
} else {
|
||||
animValue->update(dt, anim::linear);
|
||||
applyValue(animValue->current());
|
||||
}
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ContinuousValue {
|
||||
public:
|
||||
ContinuousValue() = default;
|
||||
ContinuousValue(float64 duration) : _duration(duration) {
|
||||
}
|
||||
void start(float64 to, float64 duration) {
|
||||
_to = to;
|
||||
_delta = (_to - _cur) / duration;
|
||||
}
|
||||
void start(float64 to) {
|
||||
start(to, _duration);
|
||||
}
|
||||
void reset() {
|
||||
_to = _cur = _delta = 0.;
|
||||
}
|
||||
|
||||
float64 current() const {
|
||||
return _cur;
|
||||
}
|
||||
float64 to() const {
|
||||
return _to;
|
||||
}
|
||||
float64 delta() const {
|
||||
return _delta;
|
||||
}
|
||||
void update(crl::time dt, Fn<void(float64 &)> &&callback = nullptr) {
|
||||
if (_to != _cur) {
|
||||
_cur += _delta * dt;
|
||||
if ((_to != _cur) && ((_delta > 0) == (_cur > _to))) {
|
||||
_cur = _to;
|
||||
}
|
||||
if (callback) {
|
||||
callback(_cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
float64 _duration = 0.;
|
||||
float64 _to = 0.;
|
||||
|
||||
float64 _cur = 0.;
|
||||
float64 _delta = 0.;
|
||||
|
||||
};
|
||||
|
||||
class CircleBezier final {
|
||||
public:
|
||||
CircleBezier(int n);
|
||||
|
||||
void computeRandomAdditionals();
|
||||
void paintCircle(
|
||||
Painter &p,
|
||||
const QColor &c,
|
||||
float64 radius,
|
||||
float64 cubicBezierFactor,
|
||||
float64 idleStateDiff,
|
||||
float64 radiusDiff,
|
||||
float64 randomFactor);
|
||||
|
||||
private:
|
||||
struct Points {
|
||||
QPointF point;
|
||||
QPointF control;
|
||||
};
|
||||
|
||||
const int _segmentsCount;
|
||||
const float64 _segmentLength;
|
||||
std::vector<float64> _randomAdditionals;
|
||||
|
||||
};
|
||||
|
||||
class Wave final {
|
||||
public:
|
||||
Wave(
|
||||
rpl::producer<crl::time> animationTicked,
|
||||
int n,
|
||||
float64 rotationOffset,
|
||||
float64 amplitudeRadius,
|
||||
float64 amplitudeWaveDiff,
|
||||
float64 fling,
|
||||
int flingDistanceFactor,
|
||||
int flingInAnimationDuration,
|
||||
int flingOutAnimationDuration,
|
||||
float64 amplitudeDiffSpeed,
|
||||
float64 amplitudeDiffFactor,
|
||||
bool isDirectionClockwise);
|
||||
|
||||
void setValue(float64 to);
|
||||
void tick(float64 circleRadius, crl::time dt);
|
||||
void reset();
|
||||
|
||||
void paint(Painter &p, QColor c);
|
||||
|
||||
private:
|
||||
|
||||
void initEnterIdleAnimation(rpl::producer<crl::time> animationTicked);
|
||||
void initFlingAnimation(rpl::producer<crl::time> animationTicked);
|
||||
|
||||
Ui::Animations::Simple _flingAnimation;
|
||||
|
||||
const std::unique_ptr<CircleBezier> _circleBezier;
|
||||
|
||||
const float _rotationOffset;
|
||||
const float64 _idleGlobalRadius;
|
||||
const float64 _amplitudeRadius;
|
||||
const float64 _amplitudeWaveDiff;
|
||||
const float64 _randomAdditions;
|
||||
const float64 _fling;
|
||||
const int _flingDistanceFactor;
|
||||
const int _flingInAnimationDuration;
|
||||
const int _flingOutAnimationDuration;
|
||||
const float64 _amplitudeInAnimationDuration;
|
||||
const float64 _amplitudeOutAnimationDuration;
|
||||
const int _directionClockwise;
|
||||
|
||||
bool _incRandomAdditionals = false;
|
||||
bool _isIdle = true;
|
||||
bool _wasFling = false;
|
||||
float64 _flingRadius = 0.;
|
||||
float64 _idleRadius = 0.;
|
||||
float64 _idleRotation = 0.;
|
||||
float64 _lastRadius = 0.;
|
||||
float64 _rotation = 0.;
|
||||
float64 _sineAngleMax = 0.;
|
||||
float64 _waveAngle = 0.;
|
||||
float64 _waveDiff = 0.;
|
||||
ContinuousValue _levelValue;
|
||||
|
||||
rpl::event_stream<float64> _flingAnimationRequests;
|
||||
rpl::event_stream<> _enterIdleAnimationRequests;
|
||||
rpl::lifetime _animationEnterIdleLifetime;
|
||||
rpl::lifetime _animationFlingLifetime;
|
||||
rpl::lifetime _lifetime;
|
||||
};
|
||||
|
||||
class RecordCircle final {
|
||||
public:
|
||||
RecordCircle(rpl::producer<crl::time> animationTicked);
|
||||
|
||||
void reset();
|
||||
void setAmplitude(float64 value);
|
||||
void paint(Painter &p, QColor c);
|
||||
|
||||
private:
|
||||
|
||||
const std::unique_ptr<Wave> _majorWave;
|
||||
const std::unique_ptr<Wave> _minorWave;
|
||||
|
||||
crl::time _lastUpdateTime = 0;
|
||||
ContinuousValue _levelValue;
|
||||
|
||||
};
|
||||
|
||||
CircleBezier::CircleBezier(int n)
|
||||
: _segmentsCount(n)
|
||||
, _segmentLength((4.0 / 3.0) * std::tan(M_PI / (2 * n)))
|
||||
, _randomAdditionals(n) {
|
||||
}
|
||||
|
||||
void CircleBezier::computeRandomAdditionals() {
|
||||
ranges::generate(_randomAdditionals, RandomAdditional);
|
||||
}
|
||||
|
||||
void CircleBezier::paintCircle(
|
||||
Painter &p,
|
||||
const QColor &c,
|
||||
float64 radius,
|
||||
float64 cubicBezierFactor,
|
||||
float64 idleStateDiff,
|
||||
float64 radiusDiff,
|
||||
float64 randomFactor) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
const auto r1 = radius - idleStateDiff / 2. - radiusDiff / 2.;
|
||||
const auto r2 = radius + radiusDiff / 2. + idleStateDiff / 2.;
|
||||
const auto l = _segmentLength * std::max(r1, r2) * cubicBezierFactor;
|
||||
|
||||
auto m = QMatrix();
|
||||
|
||||
const auto preparePoints = [&](int i, bool isStart) -> Points {
|
||||
Normalize(i, _segmentsCount);
|
||||
const auto randomAddition = randomFactor * _randomAdditionals[i];
|
||||
const auto r = ((i % 2 == 0) ? r1 : r2) + randomAddition;
|
||||
|
||||
m.reset();
|
||||
m.rotate(360. / _segmentsCount * i);
|
||||
const auto sign = isStart ? 1 : -1;
|
||||
|
||||
return {
|
||||
(isStart && i) ? QPointF() : m.map(QPointF(0, -r)),
|
||||
m.map(QPointF(sign * (l + randomAddition * _segmentLength), -r)),
|
||||
};
|
||||
};
|
||||
|
||||
const auto &[startPoint, _] = preparePoints(0, true);
|
||||
|
||||
auto path = QPainterPath();
|
||||
path.moveTo(startPoint);
|
||||
|
||||
for (auto i = 0; i < _segmentsCount; i++) {
|
||||
const auto &[_, startControl] = preparePoints(i, true);
|
||||
const auto &[end, endControl] = preparePoints(i + 1, false);
|
||||
|
||||
path.cubicTo(startControl, endControl, end);
|
||||
}
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
|
||||
auto pen = QPen(Qt::NoPen);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
|
||||
p.setPen(pen);
|
||||
p.fillPath(path, c);
|
||||
p.drawPath(path);
|
||||
}
|
||||
|
||||
Wave::Wave(
|
||||
rpl::producer<crl::time> animationTicked,
|
||||
int n,
|
||||
float64 rotationOffset,
|
||||
float64 amplitudeRadius,
|
||||
float64 amplitudeWaveDiff,
|
||||
float64 fling,
|
||||
int flingDistanceFactor,
|
||||
int flingInAnimationDuration,
|
||||
int flingOutAnimationDuration,
|
||||
float64 amplitudeDiffSpeed,
|
||||
float64 amplitudeDiffFactor,
|
||||
bool isDirectionClockwise)
|
||||
: _circleBezier(std::make_unique<CircleBezier>(n))
|
||||
, _rotationOffset(rotationOffset)
|
||||
, _idleGlobalRadius(st::historyRecordRadiusDiffMin * kIdleRadiusGlobalFactor)
|
||||
, _amplitudeRadius(amplitudeRadius)
|
||||
, _amplitudeWaveDiff(amplitudeWaveDiff)
|
||||
, _randomAdditions(st::historyRecordRandomAddition * kRandomAdditionFactor)
|
||||
, _fling(fling)
|
||||
, _flingDistanceFactor(flingDistanceFactor)
|
||||
, _flingInAnimationDuration(flingInAnimationDuration)
|
||||
, _flingOutAnimationDuration(flingOutAnimationDuration)
|
||||
, _amplitudeInAnimationDuration(kMinDivider
|
||||
+ amplitudeDiffFactor * amplitudeDiffSpeed)
|
||||
, _amplitudeOutAnimationDuration(kMinDivider
|
||||
+ kAmplitudeDiffFactorMax * amplitudeDiffSpeed)
|
||||
, _directionClockwise(isDirectionClockwise ? 1 : -1)
|
||||
, _rotation(rotationOffset) {
|
||||
initEnterIdleAnimation(rpl::duplicate(animationTicked));
|
||||
initFlingAnimation(std::move(animationTicked));
|
||||
}
|
||||
|
||||
void Wave::reset() {
|
||||
_incRandomAdditionals = false;
|
||||
_isIdle = true;
|
||||
_wasFling = false;
|
||||
_flingRadius = 0.;
|
||||
_idleRadius = 0.;
|
||||
_idleRotation = 0.;
|
||||
_lastRadius = 0.;
|
||||
_rotation = 0.;
|
||||
_sineAngleMax = 0.;
|
||||
_waveAngle = 0.;
|
||||
_waveDiff = 0.;
|
||||
_levelValue.reset();
|
||||
}
|
||||
|
||||
void Wave::setValue(float64 to) {
|
||||
const auto duration = (to <= _levelValue.current())
|
||||
? _amplitudeOutAnimationDuration
|
||||
: _amplitudeInAnimationDuration;
|
||||
_levelValue.start(to, duration);
|
||||
|
||||
const auto idle = to < 0.1;
|
||||
if (_isIdle != idle && idle) {
|
||||
_enterIdleAnimationRequests.fire({});
|
||||
}
|
||||
|
||||
_isIdle = idle;
|
||||
|
||||
if (!_isIdle) {
|
||||
_animationEnterIdleLifetime.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void Wave::initEnterIdleAnimation(rpl::producer<crl::time> animationTicked) {
|
||||
_enterIdleAnimationRequests.events(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto &k = kSixtyDegrees;
|
||||
|
||||
const auto rotation = _rotation;
|
||||
const auto rotationTo = std::round(rotation / k) * k
|
||||
+ _rotationOffset;
|
||||
const auto waveDiff = _waveDiff;
|
||||
|
||||
auto applyValue = [=](float64 v) {
|
||||
_rotation = rotationTo + (rotation - rotationTo) * v;
|
||||
_waveDiff = 1. + (waveDiff - 1.) * v;
|
||||
_waveAngle = std::acos(_waveDiff * _directionClockwise);
|
||||
};
|
||||
|
||||
PerformAnimation(
|
||||
rpl::duplicate(animationTicked),
|
||||
std::move(applyValue),
|
||||
nullptr,
|
||||
kEnterIdleAnimationDuration,
|
||||
1,
|
||||
0,
|
||||
_animationEnterIdleLifetime);
|
||||
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Wave::initFlingAnimation(rpl::producer<crl::time> animationTicked) {
|
||||
_flingAnimationRequests.events(
|
||||
) | rpl::start_with_next([=](float64 delta) {
|
||||
|
||||
const auto fling = _fling * 2;
|
||||
const auto flingDistance = delta
|
||||
* _amplitudeRadius
|
||||
* _flingDistanceFactor
|
||||
* fling;
|
||||
|
||||
const auto applyValue = [=](float64 v) {
|
||||
_flingRadius = v;
|
||||
};
|
||||
auto finishCallback = [=] {
|
||||
PerformAnimation(
|
||||
rpl::duplicate(animationTicked),
|
||||
applyValue,
|
||||
nullptr,
|
||||
_flingOutAnimationDuration * fling,
|
||||
flingDistance,
|
||||
0,
|
||||
_animationFlingLifetime);
|
||||
};
|
||||
|
||||
PerformAnimation(
|
||||
rpl::duplicate(animationTicked),
|
||||
applyValue,
|
||||
std::move(finishCallback),
|
||||
_flingInAnimationDuration * fling,
|
||||
_flingRadius,
|
||||
flingDistance,
|
||||
_animationFlingLifetime);
|
||||
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Wave::tick(float64 circleRadius, crl::time dt) {
|
||||
|
||||
auto amplitudeCallback = [&](float64 &value) {
|
||||
if (std::abs(value - _levelValue.to()) * _amplitudeRadius
|
||||
< (st::historyRecordRandomAddition / 2)) {
|
||||
if (!_wasFling) {
|
||||
_flingAnimationRequests.fire_copy(_levelValue.delta());
|
||||
_wasFling = true;
|
||||
}
|
||||
} else {
|
||||
_wasFling = false;
|
||||
}
|
||||
};
|
||||
_levelValue.update(dt, std::move(amplitudeCallback));
|
||||
|
||||
_idleRadius = circleRadius * kIdleRadiusFactor;
|
||||
|
||||
{
|
||||
const auto to = _levelValue.to();
|
||||
const auto delta = (_sineAngleMax - to);
|
||||
if (std::abs(delta) - 0.25 < 0) {
|
||||
_sineAngleMax = to;
|
||||
} else {
|
||||
_sineAngleMax -= 0.25 * ((delta < 0) ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isIdle) {
|
||||
_rotation += dt
|
||||
* (kRotationSpeed * 4. * std::min(_levelValue.current() / .5, 1.)
|
||||
+ kRotationSpeed * 0.5);
|
||||
Normalize(_rotation, 360.);
|
||||
} else {
|
||||
_idleRotation += kIdleRotateDiff * dt;
|
||||
Normalize(_idleRotation, 360.);
|
||||
}
|
||||
|
||||
_lastRadius = circleRadius;
|
||||
|
||||
if (!_isIdle) {
|
||||
_waveAngle += (_amplitudeWaveDiff * _sineAngleMax) * dt;
|
||||
_waveDiff = std::cos(_waveAngle) * _directionClockwise;
|
||||
|
||||
if ((_waveDiff != 0) && ((_waveDiff > 0) == _incRandomAdditionals)) {
|
||||
_circleBezier->computeRandomAdditionals();
|
||||
_incRandomAdditionals = !_incRandomAdditionals;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Wave::paint(Painter &p, QColor c) {
|
||||
const auto amplitude = _levelValue.current();
|
||||
const auto waveAmplitude = std::min(amplitude / .3, 1.);
|
||||
const auto radiusDiff = st::historyRecordRadiusDiffMin
|
||||
+ st::historyRecordRadiusDiff * kWaveAngle * _levelValue.to();
|
||||
|
||||
const auto diffFactor = 0.35 * waveAmplitude * _waveDiff;
|
||||
|
||||
const auto radius = (_lastRadius + _amplitudeRadius * amplitude)
|
||||
+ _idleGlobalRadius
|
||||
+ (_flingRadius * waveAmplitude);
|
||||
|
||||
const auto cubicBezierFactor = 1.
|
||||
+ std::abs(diffFactor) * waveAmplitude
|
||||
+ (1. - waveAmplitude) * kIdleRadiusFactor;
|
||||
|
||||
const auto circleRadiusDiff = std::max(
|
||||
radiusDiff * diffFactor,
|
||||
st::historyRecordLevelMainRadius - radius);
|
||||
|
||||
p.rotate((_rotation + _idleRotation) * _directionClockwise);
|
||||
|
||||
_circleBezier->paintCircle(
|
||||
p,
|
||||
c,
|
||||
radius,
|
||||
cubicBezierFactor,
|
||||
_idleRadius * (1. - waveAmplitude),
|
||||
circleRadiusDiff,
|
||||
waveAmplitude * _waveDiff * _randomAdditions);
|
||||
|
||||
p.rotate(0);
|
||||
}
|
||||
|
||||
RecordCircle::RecordCircle(rpl::producer<crl::time> animationTicked)
|
||||
: _majorWave(std::make_unique<Wave>(
|
||||
rpl::duplicate(animationTicked),
|
||||
kSegmentsCount,
|
||||
kMajorDegreeOffset,
|
||||
st::historyRecordMajorAmplitudeRadius,
|
||||
kSineWaveSpeedMajor,
|
||||
0.,
|
||||
kFlingDistanceFactorMajor,
|
||||
kFlingInAnimationDurationMajor,
|
||||
kFlingOutAnimationDurationMajor,
|
||||
kAnimationSpeedMajor,
|
||||
kAmplitudeDiffFactorMajor,
|
||||
true))
|
||||
, _minorWave(std::make_unique<Wave>(
|
||||
std::move(animationTicked),
|
||||
kSegmentsCount,
|
||||
0,
|
||||
st::historyRecordMinorAmplitudeRadius
|
||||
+ st::historyRecordMinorAmplitudeRadius * kSmallWaveRadius,
|
||||
kSineWaveSpeedMinor,
|
||||
kFlingDistance,
|
||||
kFlingDistanceFactorMinor,
|
||||
kFlingInAnimationDurationMinor,
|
||||
kFlingOutAnimationDurationMinor,
|
||||
kAnimationSpeedMinor,
|
||||
kAmplitudeDiffFactorMinor,
|
||||
false))
|
||||
, _levelValue(kMinDivider
|
||||
+ kAmplitudeDiffFactorMax * kAnimationSpeedCircle) {
|
||||
}
|
||||
|
||||
void RecordCircle::reset() {
|
||||
_majorWave->reset();
|
||||
_minorWave->reset();
|
||||
_levelValue.reset();
|
||||
}
|
||||
|
||||
void RecordCircle::setAmplitude(float64 value) {
|
||||
const auto to = std::min(kMaxAmplitude, value) / kMaxAmplitude;
|
||||
_levelValue.start(to);
|
||||
_majorWave->setValue(to);
|
||||
_minorWave->setValue(to);
|
||||
}
|
||||
|
||||
void RecordCircle::paint(Painter &p, QColor c) {
|
||||
const auto dt = crl::now() - _lastUpdateTime;
|
||||
_levelValue.update(dt);
|
||||
|
||||
const auto &mainRadius = st::historyRecordLevelMainRadiusAmplitude;
|
||||
const auto radius = (st::historyRecordLevelMainRadius
|
||||
+ (anim::Disabled() ? 0 : mainRadius * _levelValue.current()));
|
||||
|
||||
if (!anim::Disabled()) {
|
||||
_majorWave->tick(radius, dt);
|
||||
_minorWave->tick(radius, dt);
|
||||
_lastUpdateTime = crl::now();
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
p.setOpacity(kOpacityMajor);
|
||||
_majorWave->paint(p, c);
|
||||
p.setOpacity(kOpacityMinor);
|
||||
_minorWave->paint(p, c);
|
||||
p.setOpacity(opacity);
|
||||
}
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(c);
|
||||
p.drawEllipse(kZeroPoint, radius, radius);
|
||||
}
|
||||
|
||||
VoiceRecordButton::VoiceRecordButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<> leaveWindowEventProducer)
|
||||
: AbstractButton(parent)
|
||||
, _recordCircle(std::make_unique<RecordCircle>(
|
||||
_recordAnimationTicked.events()))
|
||||
, _center(st::historyRecordLevelMaxRadius)
|
||||
, _recordingAnimation([=](crl::time now) {
|
||||
if (!anim::Disabled()) {
|
||||
update();
|
||||
}
|
||||
_recordAnimationTicked.fire_copy(now);
|
||||
return true;
|
||||
}) {
|
||||
const auto h = st::historyRecordLevelMaxRadius * 2;
|
||||
resize(h, h);
|
||||
std::move(
|
||||
leaveWindowEventProducer
|
||||
) | rpl::start_with_next([=] {
|
||||
_inCircle = false;
|
||||
}, lifetime());
|
||||
init();
|
||||
}
|
||||
|
||||
VoiceRecordButton::~VoiceRecordButton() = default;
|
||||
|
||||
void VoiceRecordButton::requestPaintLevel(quint16 level) {
|
||||
_recordCircle->setAmplitude(level);
|
||||
update();
|
||||
}
|
||||
|
||||
void VoiceRecordButton::init() {
|
||||
const auto hasProgress = [](auto value) { return value != 0.; };
|
||||
|
||||
paintRequest(
|
||||
) | rpl::start_with_next([=](const QRect &clip) {
|
||||
Painter p(this);
|
||||
|
||||
const auto progress = _showProgress.current();
|
||||
const auto complete = (progress == 1.);
|
||||
|
||||
p.translate(_center, _center);
|
||||
if (!complete) {
|
||||
p.scale(progress, progress);
|
||||
}
|
||||
PainterHighQualityEnabler hq(p);
|
||||
const auto color = anim::color(
|
||||
st::historyRecordVoiceFgInactive,
|
||||
st::historyRecordVoiceFgActive,
|
||||
_colorProgress.current());
|
||||
_recordCircle->paint(p, color);
|
||||
p.resetTransform();
|
||||
|
||||
if (!complete) {
|
||||
p.setOpacity(progress);
|
||||
}
|
||||
st::historyRecordVoiceActive.paintInCenter(p, rect());
|
||||
|
||||
}, lifetime());
|
||||
|
||||
rpl::merge(
|
||||
shownValue(),
|
||||
_showProgress.value(
|
||||
) | rpl::map(hasProgress) | rpl::distinct_until_changed()
|
||||
) | rpl::start_with_next([=](bool show) {
|
||||
setVisible(show);
|
||||
setMouseTracking(show);
|
||||
if (!show) {
|
||||
_recordingAnimation.stop();
|
||||
_showProgress = 0.;
|
||||
_recordCircle->reset();
|
||||
} else {
|
||||
if (!_recordingAnimation.animating()) {
|
||||
_recordingAnimation.start();
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
actives(
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](bool active) {
|
||||
setPointerCursor(active);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
rpl::producer<bool> VoiceRecordButton::actives() const {
|
||||
return events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseMove
|
||||
|| e->type() == QEvent::Leave
|
||||
|| e->type() == QEvent::Enter);
|
||||
}) | rpl::map([=](not_null<QEvent*> e) {
|
||||
switch(e->type()) {
|
||||
case QEvent::MouseMove:
|
||||
return inCircle((static_cast<QMouseEvent*>(e.get()))->pos());
|
||||
case QEvent::Leave: return false;
|
||||
case QEvent::Enter: return inCircle(mapFromGlobal(QCursor::pos()));
|
||||
default: return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool VoiceRecordButton::inCircle(const QPoint &localPos) const {
|
||||
const auto &radii = st::historyRecordLevelMaxRadius;
|
||||
const auto dx = std::abs(localPos.x() - _center);
|
||||
if (dx > radii) {
|
||||
return false;
|
||||
}
|
||||
const auto dy = std::abs(localPos.y() - _center);
|
||||
if (dy > radii) {
|
||||
return false;
|
||||
} else if (dx + dy <= radii) {
|
||||
return true;
|
||||
}
|
||||
return ((dx * dx + dy * dy) <= (radii * radii));
|
||||
}
|
||||
|
||||
void VoiceRecordButton::requestPaintProgress(float64 progress) {
|
||||
_showProgress = progress;
|
||||
update();
|
||||
}
|
||||
|
||||
void VoiceRecordButton::requestPaintColor(float64 progress) {
|
||||
_colorProgress = progress;
|
||||
update();
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace HistoryView::Controls {
|
||||
|
||||
class RecordCircle;
|
||||
|
||||
class VoiceRecordButton final : public Ui::AbstractButton {
|
||||
public:
|
||||
VoiceRecordButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<> leaveWindowEventProducer);
|
||||
~VoiceRecordButton();
|
||||
|
||||
void requestPaintColor(float64 progress);
|
||||
void requestPaintProgress(float64 progress);
|
||||
void requestPaintLevel(quint16 level);
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> actives() const;
|
||||
|
||||
[[nodiscard]] bool inCircle(const QPoint &localPos) const;
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
rpl::event_stream<crl::time> _recordAnimationTicked;
|
||||
std::unique_ptr<RecordCircle> _recordCircle;
|
||||
|
||||
const int _center;
|
||||
|
||||
rpl::variable<float64> _showProgress = 0.;
|
||||
rpl::variable<float64> _colorProgress = 0.;
|
||||
rpl::variable<bool> _inCircle = false;
|
||||
|
||||
// This can animate for a very long time (like in music playing),
|
||||
// so it should be a Basic, not a Simple animation.
|
||||
Ui::Animations::Basic _recordingAnimation;
|
||||
};
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
@@ -121,6 +121,14 @@ bool SimpleElementDelegate::elementShownUnread(
|
||||
return view->data()->unread();
|
||||
}
|
||||
|
||||
void SimpleElementDelegate::elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
}
|
||||
|
||||
void SimpleElementDelegate::elementHandleViaClick(not_null<UserData*> bot) {
|
||||
}
|
||||
|
||||
TextSelection UnshiftItemSelection(
|
||||
TextSelection selection,
|
||||
uint16 byLength) {
|
||||
|
||||
@@ -67,6 +67,10 @@ public:
|
||||
virtual bool elementIsGifPaused() = 0;
|
||||
virtual bool elementHideReply(not_null<const Element*> view) = 0;
|
||||
virtual bool elementShownUnread(not_null<const Element*> view) = 0;
|
||||
virtual void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) = 0;
|
||||
virtual void elementHandleViaClick(not_null<UserData*> bot) = 0;
|
||||
|
||||
};
|
||||
|
||||
@@ -99,6 +103,10 @@ public:
|
||||
bool elementIsGifPaused() override;
|
||||
bool elementHideReply(not_null<const Element*> view) override;
|
||||
bool elementShownUnread(not_null<const Element*> view) override;
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override;
|
||||
void elementHandleViaClick(not_null<UserData*> bot) override;
|
||||
|
||||
private:
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
|
||||
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwindow.h"
|
||||
#include "mainwidget.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "apiwrap.h"
|
||||
#include "layout.h"
|
||||
#include "window/window_session_controller.h"
|
||||
@@ -37,6 +38,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
@@ -1290,6 +1294,16 @@ bool ListWidget::elementShownUnread(not_null<const Element*> view) {
|
||||
return _delegate->listElementShownUnread(view);
|
||||
}
|
||||
|
||||
void ListWidget::elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
_delegate->listSendBotCommand(command, context);
|
||||
}
|
||||
|
||||
void ListWidget::elementHandleViaClick(not_null<UserData*> bot) {
|
||||
_delegate->listHandleViaClick(bot);
|
||||
}
|
||||
|
||||
void ListWidget::saveState(not_null<ListMemento*> memento) {
|
||||
memento->setAroundPosition(_aroundPosition);
|
||||
auto state = countScrollState();
|
||||
@@ -2186,7 +2200,14 @@ void ListWidget::mouseActionFinish(
|
||||
mouseActionCancel();
|
||||
ActivateClickHandler(window(), activated, {
|
||||
button,
|
||||
QVariant::fromValue(pressState.itemId)
|
||||
QVariant::fromValue(ClickHandlerContext{
|
||||
.itemId = pressState.itemId,
|
||||
.elementDelegate = [weak = Ui::MakeWeak(this)] {
|
||||
return weak
|
||||
? (ElementDelegate*)weak
|
||||
: nullptr;
|
||||
},
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2786,4 +2807,34 @@ void ConfirmSendNowSelectedItems(not_null<ListWidget*> widget) {
|
||||
[=] { navigation->showBackFromStack(); });
|
||||
}
|
||||
|
||||
QString WrapBotCommandInChat(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
auto result = command;
|
||||
if (const auto item = peer->owner().message(context)) {
|
||||
if (const auto user = item->fromOriginal()->asUser()) {
|
||||
return WrapBotCommandInChat(peer, command, user);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString WrapBotCommandInChat(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &command,
|
||||
not_null<UserData*> bot) {
|
||||
if (!bot->isBot() || bot->username.isEmpty()) {
|
||||
return command;
|
||||
}
|
||||
const auto botStatus = peer->isChat()
|
||||
? peer->asChat()->botStatus
|
||||
: peer->isMegagroup()
|
||||
? peer->asChannel()->mgInfo->botStatus
|
||||
: -1;
|
||||
return ((command.indexOf('@') < 2) && (botStatus == 0 || botStatus == 2))
|
||||
? command + '@' + bot->username
|
||||
: command;
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
||||
|
||||
@@ -87,6 +87,10 @@ public:
|
||||
virtual bool listElementShownUnread(not_null<const Element*> view) = 0;
|
||||
virtual bool listIsGoodForAroundPosition(
|
||||
not_null<const Element*> view) = 0;
|
||||
virtual void listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) = 0;
|
||||
virtual void listHandleViaClick(not_null<UserData*> bot) = 0;
|
||||
|
||||
};
|
||||
|
||||
@@ -233,6 +237,10 @@ public:
|
||||
bool elementIsGifPaused() override;
|
||||
bool elementHideReply(not_null<const Element*> view) override;
|
||||
bool elementShownUnread(not_null<const Element*> view) override;
|
||||
void elementSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override;
|
||||
void elementHandleViaClick(not_null<UserData*> bot) override;
|
||||
|
||||
~ListWidget();
|
||||
|
||||
@@ -556,4 +564,13 @@ void ConfirmDeleteSelectedItems(not_null<ListWidget*> widget);
|
||||
void ConfirmForwardSelectedItems(not_null<ListWidget*> widget);
|
||||
void ConfirmSendNowSelectedItems(not_null<ListWidget*> widget);
|
||||
|
||||
[[nodiscard]] QString WrapBotCommandInChat(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &command,
|
||||
const FullMsgId &context);
|
||||
[[nodiscard]] QString WrapBotCommandInChat(
|
||||
not_null<PeerData*> peer,
|
||||
const QString &command,
|
||||
not_null<UserData*> bot);
|
||||
|
||||
} // namespace HistoryView
|
||||
|
||||
@@ -102,8 +102,10 @@ PinnedWidget::PinnedWidget(
|
||||
st::historyComposeButton))
|
||||
, _scrollDown(_scroll.get(), st::historyToDown) {
|
||||
_topBar->setActiveChat(
|
||||
_history,
|
||||
TopBarWidget::Section::Pinned,
|
||||
TopBarWidget::ActiveChat{
|
||||
.key = _history,
|
||||
.section = Dialogs::EntryState::Section::Pinned,
|
||||
},
|
||||
nullptr);
|
||||
|
||||
_topBar->move(0, 0);
|
||||
@@ -639,6 +641,14 @@ bool PinnedWidget::listIsGoodForAroundPosition(
|
||||
return IsServerMsgId(view->data()->id);
|
||||
}
|
||||
|
||||
void PinnedWidget::listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
}
|
||||
|
||||
void PinnedWidget::listHandleViaClick(not_null<UserData*> bot) {
|
||||
}
|
||||
|
||||
void PinnedWidget::confirmDeleteSelected() {
|
||||
ConfirmDeleteSelectedItems(_inner);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,10 @@ public:
|
||||
bool listElementHideReply(not_null<const Element*> view) override;
|
||||
bool listElementShownUnread(not_null<const Element*> view) override;
|
||||
bool listIsGoodForAroundPosition(not_null<const Element*> view) override;
|
||||
void listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override;
|
||||
void listHandleViaClick(not_null<UserData*> bot) override;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
@@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/history_view_replies_section.h"
|
||||
|
||||
#include "history/view/history_view_compose_controls.h"
|
||||
#include "history/view/controls/history_view_compose_controls.h"
|
||||
#include "history/view/history_view_top_bar_widget.h"
|
||||
#include "history/view/history_view_list_widget.h"
|
||||
#include "history/view/history_view_schedule_box.h"
|
||||
@@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "main/main_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_replies_list.h"
|
||||
#include "data/data_changes.h"
|
||||
@@ -157,7 +158,8 @@ RepliesWidget::RepliesWidget(
|
||||
, _composeControls(std::make_unique<ComposeControls>(
|
||||
this,
|
||||
controller,
|
||||
ComposeControls::Mode::Normal))
|
||||
ComposeControls::Mode::Normal,
|
||||
SendMenu::Type::SilentOnly))
|
||||
, _scroll(std::make_unique<Ui::ScrollArea>(this, st::historyScroll, false))
|
||||
, _scrollDown(_scroll.get(), st::historyToDown)
|
||||
, _readRequestTimer([=] { sendReadTillRequest(); }) {
|
||||
@@ -166,10 +168,7 @@ RepliesWidget::RepliesWidget(
|
||||
|
||||
session().api().requestFullPeer(_history->peer);
|
||||
|
||||
_topBar->setActiveChat(
|
||||
_history,
|
||||
TopBarWidget::Section::Replies,
|
||||
_sendAction.get());
|
||||
refreshTopBarActiveChat();
|
||||
|
||||
_topBar->move(0, 0);
|
||||
_topBar->resizeToWidth(width());
|
||||
@@ -219,7 +218,7 @@ RepliesWidget::RepliesWidget(
|
||||
|
||||
_inner->replyToMessageRequested(
|
||||
) | rpl::start_with_next([=](auto fullId) {
|
||||
_composeControls->replyToMessage(fullId);
|
||||
replyToMessage(fullId);
|
||||
}, _inner->lifetime());
|
||||
|
||||
_composeControls->sendActionUpdates(
|
||||
@@ -253,6 +252,7 @@ RepliesWidget::RepliesWidget(
|
||||
|
||||
setupScrollDownButton();
|
||||
setupComposeControls();
|
||||
orderWidgets();
|
||||
}
|
||||
|
||||
RepliesWidget::~RepliesWidget() {
|
||||
@@ -263,6 +263,17 @@ RepliesWidget::~RepliesWidget() {
|
||||
_history->owner().repliesSendActionPainterRemoved(_history, _rootId);
|
||||
}
|
||||
|
||||
void RepliesWidget::orderWidgets() {
|
||||
if (_topBar) {
|
||||
_topBar->raise();
|
||||
}
|
||||
if (_rootView) {
|
||||
_rootView->raise();
|
||||
}
|
||||
_topBarShadow->raise();
|
||||
_composeControls->raisePanels();
|
||||
}
|
||||
|
||||
void RepliesWidget::sendReadTillRequest() {
|
||||
if (!_root) {
|
||||
_readRequestPending = true;
|
||||
@@ -419,13 +430,21 @@ void RepliesWidget::setupComposeControls() {
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->sendRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
send();
|
||||
) | rpl::start_with_next([=](Api::SendOptions options) {
|
||||
send(options);
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->sendVoiceRequests(
|
||||
) | rpl::start_with_next([=](ComposeControls::VoiceToSend &&data) {
|
||||
sendVoice(data.bytes, data.waveform, data.duration);
|
||||
sendVoice(std::move(data));
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->sendCommandRequests(
|
||||
) | rpl::start_with_next([=](const QString &command) {
|
||||
if (showSlowmodeError()) {
|
||||
return;
|
||||
}
|
||||
listSendBotCommand(command, FullMsgId());
|
||||
}, lifetime());
|
||||
|
||||
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
|
||||
@@ -451,17 +470,17 @@ void RepliesWidget::setupComposeControls() {
|
||||
|
||||
_composeControls->fileChosen(
|
||||
) | rpl::start_with_next([=](Selector::FileChosen chosen) {
|
||||
sendExistingDocument(chosen.document);
|
||||
sendExistingDocument(chosen.document, chosen.options);
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->photoChosen(
|
||||
) | rpl::start_with_next([=](Selector::PhotoChosen chosen) {
|
||||
sendExistingPhoto(chosen.photo);
|
||||
sendExistingPhoto(chosen.photo, chosen.options);
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->inlineResultChosen(
|
||||
) | rpl::start_with_next([=](Selector::InlineChosen chosen) {
|
||||
sendInlineResult(chosen.result, chosen.bot);
|
||||
sendInlineResult(chosen.result, chosen.bot, chosen.options);
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->scrollRequests(
|
||||
@@ -507,6 +526,11 @@ void RepliesWidget::setupComposeControls() {
|
||||
Unexpected("action in MimeData hook.");
|
||||
});
|
||||
|
||||
_composeControls->lockShowStarts(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateScrollDownVisibility();
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->finishAnimating();
|
||||
}
|
||||
|
||||
@@ -688,6 +712,7 @@ void RepliesWidget::sendingFilesConfirmed(
|
||||
}
|
||||
if (_composeControls->replyingToMessage().msg == replyTo) {
|
||||
_composeControls->cancelReplyMessage();
|
||||
refreshTopBarActiveChat();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,13 +879,15 @@ void RepliesWidget::send() {
|
||||
// Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void RepliesWidget::sendVoice(
|
||||
QByteArray bytes,
|
||||
VoiceWaveform waveform,
|
||||
int duration) {
|
||||
void RepliesWidget::sendVoice(ComposeControls::VoiceToSend &&data) {
|
||||
auto action = Api::SendAction(_history);
|
||||
action.replyTo = replyToId();
|
||||
session().api().sendVoiceMessage(bytes, waveform, duration, action);
|
||||
action.options = data.options;
|
||||
session().api().sendVoiceMessage(
|
||||
data.bytes,
|
||||
data.waveform,
|
||||
data.duration,
|
||||
std::move(action));
|
||||
}
|
||||
|
||||
void RepliesWidget::send(Api::SendOptions options) {
|
||||
@@ -1010,14 +1037,6 @@ bool RepliesWidget::sendExistingDocument(
|
||||
message.action.options = options;
|
||||
Api::SendExistingDocument(std::move(message), document);
|
||||
|
||||
//if (_fieldAutocomplete->stickersShown()) {
|
||||
// clearFieldText();
|
||||
// //_saveDraftText = true;
|
||||
// //_saveDraftStart = crl::now();
|
||||
// //onDraftSave();
|
||||
// onCloudDraftSave(); // won't be needed if SendInlineBotResult will clear the cloud draft
|
||||
//}
|
||||
|
||||
_composeControls->cancelReplyMessage();
|
||||
finishSending();
|
||||
return true;
|
||||
@@ -1112,6 +1131,17 @@ SendMenu::Type RepliesWidget::sendMenuType() const {
|
||||
: SendMenu::Type::Scheduled;
|
||||
}
|
||||
|
||||
void RepliesWidget::refreshTopBarActiveChat() {
|
||||
const auto state = Dialogs::EntryState{
|
||||
.key = _history,
|
||||
.section = Dialogs::EntryState::Section::Replies,
|
||||
.rootId = _rootId,
|
||||
.currentReplyToId = _composeControls->replyingToMessage().msg,
|
||||
};
|
||||
_topBar->setActiveChat(state, _sendAction.get());
|
||||
_composeControls->setCurrentDialogsEntryState(state);
|
||||
}
|
||||
|
||||
MsgId RepliesWidget::replyToId() const {
|
||||
const auto custom = _composeControls->replyingToMessage().msg;
|
||||
return custom ? custom : _rootId;
|
||||
@@ -1155,6 +1185,7 @@ void RepliesWidget::finishSending() {
|
||||
//if (_previewData && _previewData->pendingTill) previewCancel();
|
||||
doSetInnerFocus();
|
||||
showAtEnd();
|
||||
refreshTopBarActiveChat();
|
||||
}
|
||||
|
||||
void RepliesWidget::showAtPosition(
|
||||
@@ -1213,6 +1244,9 @@ void RepliesWidget::updateScrollDownVisibility() {
|
||||
}
|
||||
|
||||
const auto scrollDownIsVisible = [&]() -> std::optional<bool> {
|
||||
if (_composeControls->isLockPresent()) {
|
||||
return false;
|
||||
}
|
||||
const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
|
||||
if (top < _scroll->scrollTopMax() || _replyReturn) {
|
||||
return true;
|
||||
@@ -1365,10 +1399,15 @@ bool RepliesWidget::replyToMessage(not_null<HistoryItem*> item) {
|
||||
if (item->history() != _history || item->replyToTop() != _rootId) {
|
||||
return false;
|
||||
}
|
||||
_composeControls->replyToMessage(item->fullId());
|
||||
replyToMessage(item->fullId());
|
||||
return true;
|
||||
}
|
||||
|
||||
void RepliesWidget::replyToMessage(FullMsgId itemId) {
|
||||
_composeControls->replyToMessage(itemId);
|
||||
refreshTopBarActiveChat();
|
||||
}
|
||||
|
||||
void RepliesWidget::saveState(not_null<RepliesMemento*> memento) {
|
||||
memento->setReplies(_replies);
|
||||
memento->setReplyReturns(_replyReturns);
|
||||
@@ -1466,6 +1505,7 @@ void RepliesWidget::updateControlsGeometry() {
|
||||
updateInnerVisibleArea();
|
||||
}
|
||||
_composeControls->move(0, bottom - controlsHeight);
|
||||
_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
|
||||
|
||||
updateScrollDownPosition();
|
||||
}
|
||||
@@ -1590,12 +1630,8 @@ void RepliesWidget::listCancelRequest() {
|
||||
if (_inner && !_inner->getSelectedItems().empty()) {
|
||||
clearSelected();
|
||||
return;
|
||||
}
|
||||
if (_composeControls->isEditingMessage()) {
|
||||
_composeControls->cancelEditMessage();
|
||||
return;
|
||||
} else if (_composeControls->replyingToMessage()) {
|
||||
_composeControls->cancelReplyMessage();
|
||||
} else if (_composeControls->handleCancelRequest()) {
|
||||
refreshTopBarActiveChat();
|
||||
return;
|
||||
}
|
||||
controller()->showBackFromStack();
|
||||
@@ -1738,6 +1774,21 @@ bool RepliesWidget::listIsGoodForAroundPosition(
|
||||
return IsServerMsgId(view->data()->id);
|
||||
}
|
||||
|
||||
void RepliesWidget::listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
const auto text = WrapBotCommandInChat(_history->peer, command, context);
|
||||
auto message = ApiWrap::MessageToSend(_history);
|
||||
message.textWithTags = { text };
|
||||
message.action.replyTo = replyToId();
|
||||
session().api().sendMessage(std::move(message));
|
||||
finishSending();
|
||||
}
|
||||
|
||||
void RepliesWidget::listHandleViaClick(not_null<UserData*> bot) {
|
||||
_composeControls->setText({ '@' + bot->username + ' ' });
|
||||
}
|
||||
|
||||
void RepliesWidget::confirmDeleteSelected() {
|
||||
ConfirmDeleteSelectedItems(_inner);
|
||||
}
|
||||
@@ -1753,7 +1804,7 @@ void RepliesWidget::clearSelected() {
|
||||
void RepliesWidget::setupDragArea() {
|
||||
const auto areas = DragArea::SetupDragAreaToContainer(
|
||||
this,
|
||||
[=](not_null<const QMimeData*> d) { return _history; },
|
||||
[=](auto d) { return _history && !_composeControls->isRecording(); },
|
||||
nullptr,
|
||||
[=] { updateControlsGeometry(); });
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ class RepliesList;
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
namespace Controls {
|
||||
struct VoiceToSend;
|
||||
} // namespace Controls
|
||||
|
||||
class Element;
|
||||
class TopBarWidget;
|
||||
class RepliesMemento;
|
||||
@@ -126,6 +130,10 @@ public:
|
||||
bool listElementHideReply(not_null<const Element*> view) override;
|
||||
bool listElementShownUnread(not_null<const Element*> view) override;
|
||||
bool listIsGoodForAroundPosition(not_null<const Element*> view) override;
|
||||
void listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override;
|
||||
void listHandleViaClick(not_null<UserData*> bot) override;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
@@ -176,7 +184,7 @@ private:
|
||||
|
||||
void send();
|
||||
void send(Api::SendOptions options);
|
||||
void sendVoice(QByteArray bytes, VoiceWaveform waveform, int duration);
|
||||
void sendVoice(Controls::VoiceToSend &&data);
|
||||
void edit(
|
||||
not_null<HistoryItem*> item,
|
||||
Api::SendOptions options,
|
||||
@@ -186,6 +194,7 @@ private:
|
||||
[[nodiscard]] MsgId replyToId() const;
|
||||
[[nodiscard]] HistoryItem *lookupRoot() const;
|
||||
[[nodiscard]] bool computeAreComments() const;
|
||||
void orderWidgets();
|
||||
|
||||
void pushReplyReturn(not_null<HistoryItem*> item);
|
||||
void computeCurrentReplyReturn();
|
||||
@@ -193,6 +202,8 @@ private:
|
||||
void restoreReplyReturns(const std::vector<MsgId> &list);
|
||||
void checkReplyReturns();
|
||||
void recountChatWidth();
|
||||
void replyToMessage(FullMsgId itemId);
|
||||
void refreshTopBarActiveChat();
|
||||
|
||||
void uploadFile(const QByteArray &fileContent, SendMediaType type);
|
||||
bool confirmSendingFiles(
|
||||
|
||||
@@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/history_view_scheduled_section.h"
|
||||
|
||||
#include "history/view/history_view_compose_controls.h"
|
||||
#include "history/view/controls/history_view_compose_controls.h"
|
||||
#include "history/view/history_view_top_bar_widget.h"
|
||||
#include "history/view/history_view_list_widget.h"
|
||||
#include "history/view/history_view_schedule_box.h"
|
||||
@@ -97,12 +97,15 @@ ScheduledWidget::ScheduledWidget(
|
||||
, _composeControls(std::make_unique<ComposeControls>(
|
||||
this,
|
||||
controller,
|
||||
ComposeControls::Mode::Scheduled))
|
||||
ComposeControls::Mode::Scheduled,
|
||||
SendMenu::Type::Disabled))
|
||||
, _scrollDown(_scroll, st::historyToDown) {
|
||||
_topBar->setActiveChat(
|
||||
_history,
|
||||
TopBarWidget::Section::Scheduled,
|
||||
nullptr);
|
||||
const auto state = Dialogs::EntryState{
|
||||
.key = _history,
|
||||
.section = Dialogs::EntryState::Section::Scheduled,
|
||||
};
|
||||
_topBar->setActiveChat(state, nullptr);
|
||||
_composeControls->setCurrentDialogsEntryState(state);
|
||||
|
||||
_topBar->move(0, 0);
|
||||
_topBar->resizeToWidth(width());
|
||||
@@ -180,6 +183,11 @@ void ScheduledWidget::setupComposeControls() {
|
||||
sendVoice(data.bytes, data.waveform, data.duration);
|
||||
}, lifetime());
|
||||
|
||||
_composeControls->sendCommandRequests(
|
||||
) | rpl::start_with_next([=](const QString &command) {
|
||||
listSendBotCommand(command, FullMsgId());
|
||||
}, lifetime());
|
||||
|
||||
const auto saveEditMsgRequestId = lifetime().make_state<mtpRequestId>(0);
|
||||
_composeControls->editRequests(
|
||||
) | rpl::start_with_next([=](auto data) {
|
||||
@@ -253,6 +261,11 @@ void ScheduledWidget::setupComposeControls() {
|
||||
}
|
||||
Unexpected("action in MimeData hook.");
|
||||
});
|
||||
|
||||
_composeControls->lockShowStarts(
|
||||
) | rpl::start_with_next([=] {
|
||||
updateScrollDownVisibility();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void ScheduledWidget::chooseAttach() {
|
||||
@@ -661,14 +674,6 @@ bool ScheduledWidget::sendExistingDocument(
|
||||
message.action.options = options;
|
||||
Api::SendExistingDocument(std::move(message), document);
|
||||
|
||||
//if (_fieldAutocomplete->stickersShown()) {
|
||||
// clearFieldText();
|
||||
// //_saveDraftText = true;
|
||||
// //_saveDraftStart = crl::now();
|
||||
// //onDraftSave();
|
||||
// onCloudDraftSave(); // won't be needed if SendInlineBotResult will clear the cloud draft
|
||||
//}
|
||||
|
||||
_composeControls->hidePanelsAnimated();
|
||||
_composeControls->focus();
|
||||
return true;
|
||||
@@ -820,6 +825,9 @@ void ScheduledWidget::updateScrollDownVisibility() {
|
||||
}
|
||||
|
||||
const auto scrollDownIsVisible = [&]() -> std::optional<bool> {
|
||||
if (_composeControls->isLockPresent()) {
|
||||
return false;
|
||||
}
|
||||
const auto top = _scroll->scrollTop() + st::historyToDownShownAfter;
|
||||
if (top < _scroll->scrollTopMax()) {
|
||||
return true;
|
||||
@@ -971,6 +979,7 @@ void ScheduledWidget::updateControlsGeometry() {
|
||||
updateInnerVisibleArea();
|
||||
}
|
||||
_composeControls->move(0, bottom - controlsHeight);
|
||||
_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
|
||||
|
||||
updateScrollDownPosition();
|
||||
}
|
||||
@@ -1049,9 +1058,7 @@ void ScheduledWidget::listCancelRequest() {
|
||||
if (_inner && !_inner->getSelectedItems().empty()) {
|
||||
clearSelected();
|
||||
return;
|
||||
}
|
||||
if (_composeControls->isEditingMessage()) {
|
||||
_composeControls->cancelEditMessage();
|
||||
} else if (_composeControls->handleCancelRequest()) {
|
||||
return;
|
||||
}
|
||||
controller()->showBackFromStack();
|
||||
@@ -1165,6 +1172,25 @@ bool ScheduledWidget::listIsGoodForAroundPosition(
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScheduledWidget::listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) {
|
||||
const auto callback = [=](Api::SendOptions options) {
|
||||
const auto text = WrapBotCommandInChat(_history->peer, command, context);
|
||||
auto message = ApiWrap::MessageToSend(_history);
|
||||
message.textWithTags = { text };
|
||||
message.action.options = options;
|
||||
session().api().sendMessage(std::move(message));
|
||||
};
|
||||
Ui::show(
|
||||
PrepareScheduleBox(this, sendMenuType(), callback),
|
||||
Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void ScheduledWidget::listHandleViaClick(not_null<UserData*> bot) {
|
||||
_composeControls->setText({ '@' + bot->username + ' ' });
|
||||
}
|
||||
|
||||
void ScheduledWidget::confirmSendNowSelected() {
|
||||
ConfirmSendNowSelectedItems(_inner);
|
||||
}
|
||||
@@ -1180,7 +1206,7 @@ void ScheduledWidget::clearSelected() {
|
||||
void ScheduledWidget::setupDragArea() {
|
||||
const auto areas = DragArea::SetupDragAreaToContainer(
|
||||
this,
|
||||
[=](not_null<const QMimeData*> d) { return _history; },
|
||||
[=](auto d) { return _history && !_composeControls->isRecording(); },
|
||||
nullptr,
|
||||
[=] { updateControlsGeometry(); });
|
||||
|
||||
|
||||
@@ -110,6 +110,10 @@ public:
|
||||
bool listElementHideReply(not_null<const Element*> view) override;
|
||||
bool listElementShownUnread(not_null<const Element*> view) override;
|
||||
bool listIsGoodForAroundPosition(not_null<const Element *> view) override;
|
||||
void listSendBotCommand(
|
||||
const QString &command,
|
||||
const FullMsgId &context) override;
|
||||
void listHandleViaClick(not_null<UserData*> bot) override;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
@@ -115,7 +115,7 @@ TopBarWidget::TopBarWidget(
|
||||
using AnimationUpdate = Data::Session::SendActionAnimationUpdate;
|
||||
session().data().sendActionAnimationUpdated(
|
||||
) | rpl::filter([=](const AnimationUpdate &update) {
|
||||
return (update.history == _activeChat.history());
|
||||
return (update.history == _activeChat.key.history());
|
||||
}) | rpl::start_with_next([=] {
|
||||
update();
|
||||
}, lifetime());
|
||||
@@ -191,13 +191,13 @@ void TopBarWidget::refreshLang() {
|
||||
}
|
||||
|
||||
void TopBarWidget::onSearch() {
|
||||
if (_activeChat) {
|
||||
_controller->content()->searchInChat(_activeChat);
|
||||
if (_activeChat.key) {
|
||||
_controller->content()->searchInChat(_activeChat.key);
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarWidget::onCall() {
|
||||
if (const auto peer = _activeChat.peer()) {
|
||||
if (const auto peer = _activeChat.key.peer()) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
Core::App().calls().startOutgoingCall(user, false);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ void TopBarWidget::onCall() {
|
||||
}
|
||||
|
||||
void TopBarWidget::showMenu() {
|
||||
if (!_activeChat || _menu) {
|
||||
if (!_activeChat.key || _menu) {
|
||||
return;
|
||||
}
|
||||
_menu.create(parentWidget());
|
||||
@@ -228,28 +228,14 @@ void TopBarWidget::showMenu() {
|
||||
}));
|
||||
_menuToggle->installEventFilter(_menu);
|
||||
const auto addAction = [&](
|
||||
const QString & text,
|
||||
const QString &text,
|
||||
Fn<void()> callback) {
|
||||
return _menu->addAction(text, std::move(callback));
|
||||
};
|
||||
if (const auto peer = _activeChat.peer()) {
|
||||
Window::FillPeerMenu(
|
||||
_controller,
|
||||
peer,
|
||||
FilterId(),
|
||||
addAction,
|
||||
(_section == Section::Scheduled)
|
||||
? Window::PeerMenuSource::ScheduledSection
|
||||
: Window::PeerMenuSource::History);
|
||||
} else if (const auto folder = _activeChat.folder()) {
|
||||
Window::FillFolderMenu(
|
||||
_controller,
|
||||
folder,
|
||||
addAction,
|
||||
Window::PeerMenuSource::History);
|
||||
} else {
|
||||
Unexpected("Empty active chat in TopBarWidget::showMenu.");
|
||||
}
|
||||
Window::FillDialogsEntryMenu(
|
||||
_controller,
|
||||
_activeChat,
|
||||
addAction);
|
||||
if (_menu->actions().empty()) {
|
||||
_menu.destroy();
|
||||
} else {
|
||||
@@ -263,13 +249,13 @@ void TopBarWidget::toggleInfoSection() {
|
||||
&& (Core::App().settings().thirdSectionInfoEnabled()
|
||||
|| Core::App().settings().tabbedReplacedWithInfo())) {
|
||||
_controller->closeThirdSection();
|
||||
} else if (_activeChat.peer()) {
|
||||
} else if (_activeChat.key.peer()) {
|
||||
if (_controller->canShowThirdSection()) {
|
||||
Core::App().settings().setThirdSectionInfoEnabled(true);
|
||||
Core::App().saveSettingsDelayed();
|
||||
if (Adaptive::ThreeColumn()) {
|
||||
_controller->showSection(
|
||||
Info::Memento::Default(_activeChat.peer()),
|
||||
Info::Memento::Default(_activeChat.key.peer()),
|
||||
Window::SectionShow().withThirdColumn());
|
||||
} else {
|
||||
_controller->resizeForThirdSection();
|
||||
@@ -323,7 +309,7 @@ void TopBarWidget::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
void TopBarWidget::paintTopBar(Painter &p) {
|
||||
if (!_activeChat) {
|
||||
if (!_activeChat.key) {
|
||||
return;
|
||||
}
|
||||
auto nameleft = _leftTaken;
|
||||
@@ -331,18 +317,18 @@ void TopBarWidget::paintTopBar(Painter &p) {
|
||||
auto statustop = st::topBarHeight - st::topBarArrowPadding.bottom() - st::dialogsTextFont->height;
|
||||
auto availableWidth = width() - _rightTaken - nameleft;
|
||||
|
||||
const auto history = _activeChat.history();
|
||||
const auto folder = _activeChat.folder();
|
||||
const auto history = _activeChat.key.history();
|
||||
const auto folder = _activeChat.key.folder();
|
||||
if (folder
|
||||
|| history->peer->sharedMediaInfo()
|
||||
|| (_section == Section::Scheduled)
|
||||
|| (_section == Section::Pinned)) {
|
||||
|| (_activeChat.section == Section::Scheduled)
|
||||
|| (_activeChat.section == Section::Pinned)) {
|
||||
// #TODO feed name emoji.
|
||||
auto text = (_section == Section::Scheduled)
|
||||
auto text = (_activeChat.section == Section::Scheduled)
|
||||
? ((history && history->peer->isSelf())
|
||||
? tr::lng_reminder_messages(tr::now)
|
||||
: tr::lng_scheduled_messages(tr::now))
|
||||
: (_section == Section::Pinned)
|
||||
: (_activeChat.section == Section::Pinned)
|
||||
? _customTitleText
|
||||
: folder
|
||||
? folder->chatListName()
|
||||
@@ -360,7 +346,7 @@ void TopBarWidget::paintTopBar(Painter &p) {
|
||||
(height() - st::historySavedFont->height) / 2,
|
||||
width(),
|
||||
text);
|
||||
} else if (_section == Section::Replies) {
|
||||
} else if (_activeChat.section == Section::Replies) {
|
||||
p.setPen(st::dialogsNameFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.drawTextLeft(
|
||||
@@ -382,7 +368,7 @@ void TopBarWidget::paintTopBar(Painter &p) {
|
||||
p.setPen(st::historyStatusFg);
|
||||
p.drawTextLeft(nameleft, statustop, width(), _customTitleText);
|
||||
}
|
||||
} else if (const auto history = _activeChat.history()) {
|
||||
} else if (const auto history = _activeChat.key.history()) {
|
||||
const auto peer = history->peer;
|
||||
const auto &text = peer->topBarNameText();
|
||||
const auto badgeStyle = Ui::PeerBadgeStyle{
|
||||
@@ -481,29 +467,30 @@ void TopBarWidget::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
void TopBarWidget::infoClicked() {
|
||||
if (!_activeChat) {
|
||||
const auto key = _activeChat.key;
|
||||
if (!key) {
|
||||
return;
|
||||
} else if (_activeChat.folder()) {
|
||||
} else if (key.folder()) {
|
||||
_controller->closeFolder();
|
||||
//} else if (const auto feed = _activeChat.feed()) { // #feed
|
||||
// _controller->showSection(Info::Memento(
|
||||
// feed,
|
||||
// Info::Section(Info::Section::Type::Profile)));
|
||||
} else if (_activeChat.peer()->isSelf()) {
|
||||
} else if (key.peer()->isSelf()) {
|
||||
_controller->showSection(Info::Memento(
|
||||
_activeChat.peer(),
|
||||
key.peer(),
|
||||
Info::Section(Storage::SharedMediaType::Photo)));
|
||||
} else if (_activeChat.peer()->isRepliesChat()) {
|
||||
} else if (key.peer()->isRepliesChat()) {
|
||||
_controller->showSection(Info::Memento(
|
||||
_activeChat.peer(),
|
||||
key.peer(),
|
||||
Info::Section(Storage::SharedMediaType::Photo)));
|
||||
} else {
|
||||
_controller->showPeerInfo(_activeChat.peer());
|
||||
_controller->showPeerInfo(key.peer());
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarWidget::backClicked() {
|
||||
if (_activeChat.folder()) {
|
||||
if (_activeChat.key.folder()) {
|
||||
_controller->closeFolder();
|
||||
} else {
|
||||
_controller->showBackFromStack();
|
||||
@@ -511,14 +498,14 @@ void TopBarWidget::backClicked() {
|
||||
}
|
||||
|
||||
void TopBarWidget::setActiveChat(
|
||||
Dialogs::Key chat,
|
||||
Section section,
|
||||
ActiveChat activeChat,
|
||||
SendActionPainter *sendAction) {
|
||||
if (_activeChat == chat && _section == section) {
|
||||
if (_activeChat.key == activeChat.key
|
||||
&& _activeChat.section == activeChat.section) {
|
||||
_activeChat = activeChat;
|
||||
return;
|
||||
}
|
||||
_activeChat = chat;
|
||||
_section = section;
|
||||
_activeChat = activeChat;
|
||||
_sendAction = sendAction;
|
||||
_back->clearState();
|
||||
update();
|
||||
@@ -542,7 +529,7 @@ void TopBarWidget::setCustomTitle(const QString &title) {
|
||||
}
|
||||
|
||||
void TopBarWidget::refreshInfoButton() {
|
||||
if (const auto peer = _activeChat.peer()) {
|
||||
if (const auto peer = _activeChat.key.peer()) {
|
||||
auto info = object_ptr<Ui::UserpicButton>(
|
||||
this,
|
||||
_controller,
|
||||
@@ -575,14 +562,14 @@ int TopBarWidget::countSelectedButtonsTop(float64 selectedShown) {
|
||||
}
|
||||
|
||||
void TopBarWidget::updateSearchVisibility() {
|
||||
const auto historyMode = (_section == Section::History);
|
||||
const auto smallDialogsColumn = _activeChat.folder()
|
||||
const auto historyMode = (_activeChat.section == Section::History);
|
||||
const auto smallDialogsColumn = _activeChat.key.folder()
|
||||
&& (width() < _back->width() + _search->width());
|
||||
_search->setVisible(historyMode && !smallDialogsColumn);
|
||||
}
|
||||
|
||||
void TopBarWidget::updateControlsGeometry() {
|
||||
if (!_activeChat) {
|
||||
if (!_activeChat.key) {
|
||||
return;
|
||||
}
|
||||
auto hasSelected = (_selectedCount > 0);
|
||||
@@ -619,7 +606,7 @@ void TopBarWidget::updateControlsGeometry() {
|
||||
if (_back->isHidden()) {
|
||||
_leftTaken = st::topBarArrowPadding.right();
|
||||
} else {
|
||||
const auto smallDialogsColumn = _activeChat.folder()
|
||||
const auto smallDialogsColumn = _activeChat.key.folder()
|
||||
&& (width() < _back->width() + _search->width());
|
||||
_leftTaken = smallDialogsColumn ? (width() - _back->width()) / 2 : 0;
|
||||
_back->moveToLeft(_leftTaken, otherButtonsTop);
|
||||
@@ -664,7 +651,7 @@ void TopBarWidget::setAnimatingMode(bool enabled) {
|
||||
}
|
||||
|
||||
void TopBarWidget::updateControlsVisibility() {
|
||||
if (!_activeChat) {
|
||||
if (!_activeChat.key) {
|
||||
return;
|
||||
} else if (_animatingMode) {
|
||||
hideChildren();
|
||||
@@ -677,7 +664,7 @@ void TopBarWidget::updateControlsVisibility() {
|
||||
|
||||
auto backVisible = Adaptive::OneColumn()
|
||||
|| !_controller->content()->stackIsEmpty()
|
||||
|| _activeChat.folder();
|
||||
|| _activeChat.key.folder();
|
||||
_back->setVisible(backVisible);
|
||||
if (_info) {
|
||||
_info->setVisible(Adaptive::OneColumn());
|
||||
@@ -685,19 +672,22 @@ void TopBarWidget::updateControlsVisibility() {
|
||||
if (_unreadBadge) {
|
||||
_unreadBadge->show();
|
||||
}
|
||||
const auto historyMode = (_section == Section::History);
|
||||
const auto scheduledMode = (_section == Section::Scheduled);
|
||||
const auto showInScheduledMode = (_activeChat.peer()
|
||||
&& _activeChat.peer()->canSendPolls());
|
||||
const auto section = _activeChat.section;
|
||||
const auto historyMode = (section == Section::History);
|
||||
const auto hasPollsMenu = _activeChat.key.peer()
|
||||
&& _activeChat.key.peer()->canSendPolls();
|
||||
const auto hasMenu = !_activeChat.key.folder()
|
||||
&& ((section == Section::Scheduled || section == Section::Replies)
|
||||
? hasPollsMenu
|
||||
: historyMode);
|
||||
updateSearchVisibility();
|
||||
_menuToggle->setVisible(!_activeChat.folder()
|
||||
&& (scheduledMode ? showInScheduledMode : historyMode));
|
||||
_menuToggle->setVisible(hasMenu);
|
||||
_infoToggle->setVisible(historyMode
|
||||
&& !_activeChat.folder()
|
||||
&& !_activeChat.key.folder()
|
||||
&& !Adaptive::OneColumn()
|
||||
&& _controller->canShowThirdSection());
|
||||
const auto callsEnabled = [&] {
|
||||
if (const auto peer = _activeChat.peer()) {
|
||||
if (const auto peer = _activeChat.key.peer()) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
return session().serverConfig().phoneCallsEnabled.current()
|
||||
&& user->hasCalls();
|
||||
@@ -715,7 +705,7 @@ void TopBarWidget::updateControlsVisibility() {
|
||||
|
||||
void TopBarWidget::updateMembersShowArea() {
|
||||
const auto membersShowAreaNeeded = [&] {
|
||||
const auto peer = _activeChat.peer();
|
||||
const auto peer = _activeChat.key.peer();
|
||||
if ((_selectedCount > 0) || !peer) {
|
||||
return false;
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
@@ -801,7 +791,7 @@ void TopBarWidget::updateAdaptiveLayout() {
|
||||
}
|
||||
|
||||
void TopBarWidget::refreshUnreadBadge() {
|
||||
if (!Adaptive::OneColumn() && !_activeChat.folder()) {
|
||||
if (!Adaptive::OneColumn() && !_activeChat.key.folder()) {
|
||||
_unreadBadge.destroy();
|
||||
return;
|
||||
} else if (_unreadBadge) {
|
||||
@@ -830,8 +820,9 @@ void TopBarWidget::refreshUnreadBadge() {
|
||||
void TopBarWidget::updateUnreadBadge() {
|
||||
if (!_unreadBadge) return;
|
||||
|
||||
const auto muted = session().data().unreadBadgeMutedIgnoreOne(_activeChat);
|
||||
const auto counter = session().data().unreadBadgeIgnoreOne(_activeChat);
|
||||
const auto key = _activeChat.key;
|
||||
const auto muted = session().data().unreadBadgeMutedIgnoreOne(key);
|
||||
const auto counter = session().data().unreadBadgeIgnoreOne(key);
|
||||
const auto text = [&] {
|
||||
if (!counter) {
|
||||
return QString();
|
||||
@@ -858,12 +849,15 @@ void TopBarWidget::updateInfoToggleActive() {
|
||||
}
|
||||
|
||||
void TopBarWidget::updateOnlineDisplay() {
|
||||
if (!_activeChat.peer()) return;
|
||||
const auto peer = _activeChat.key.peer();
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text;
|
||||
const auto now = base::unixtime::now();
|
||||
bool titlePeerTextOnline = false;
|
||||
if (const auto user = _activeChat.peer()->asUser()) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
if (session().supportMode()
|
||||
&& !session().supportHelper().infoCurrent(user).text.empty()) {
|
||||
text = QString::fromUtf8("\xe2\x9a\xa0\xef\xb8\x8f check info");
|
||||
@@ -872,7 +866,7 @@ void TopBarWidget::updateOnlineDisplay() {
|
||||
text = Data::OnlineText(user, now);
|
||||
titlePeerTextOnline = Data::OnlineTextActive(user, now);
|
||||
}
|
||||
} else if (const auto chat = _activeChat.peer()->asChat()) {
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
if (!chat->amIn()) {
|
||||
text = tr::lng_chat_status_unaccessible(tr::now);
|
||||
} else if (chat->participants.empty()) {
|
||||
@@ -903,7 +897,7 @@ void TopBarWidget::updateOnlineDisplay() {
|
||||
text = tr::lng_group_status(tr::now);
|
||||
}
|
||||
}
|
||||
} else if (const auto channel = _activeChat.peer()->asChannel()) {
|
||||
} else if (const auto channel = peer->asChannel()) {
|
||||
if (channel->isMegagroup()
|
||||
&& (channel->membersCount() > 0)
|
||||
&& (channel->membersCount()
|
||||
@@ -950,7 +944,10 @@ void TopBarWidget::updateOnlineDisplay() {
|
||||
}
|
||||
|
||||
void TopBarWidget::updateOnlineDisplayTimer() {
|
||||
if (!_activeChat.peer()) return;
|
||||
const auto peer = _activeChat.key.peer();
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = base::unixtime::now();
|
||||
auto minTimeout = crl::time(86400);
|
||||
@@ -958,13 +955,13 @@ void TopBarWidget::updateOnlineDisplayTimer() {
|
||||
auto hisTimeout = Data::OnlineChangeTimeout(user, now);
|
||||
accumulate_min(minTimeout, hisTimeout);
|
||||
};
|
||||
if (const auto user = _activeChat.peer()->asUser()) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
handleUser(user);
|
||||
} else if (auto chat = _activeChat.peer()->asChat()) {
|
||||
} else if (const auto chat = peer->asChat()) {
|
||||
for (const auto user : chat->participants) {
|
||||
handleUser(user);
|
||||
}
|
||||
} else if (_activeChat.peer()->isChannel()) {
|
||||
} else if (peer->isChannel()) {
|
||||
}
|
||||
updateOnlineDisplayIn(minTimeout);
|
||||
}
|
||||
|
||||
@@ -43,12 +43,8 @@ public:
|
||||
int canForwardCount = 0;
|
||||
int canSendNowCount = 0;
|
||||
};
|
||||
enum class Section {
|
||||
History,
|
||||
Scheduled,
|
||||
Pinned,
|
||||
Replies,
|
||||
};
|
||||
using ActiveChat = Dialogs::EntryState;
|
||||
using Section = ActiveChat::Section;
|
||||
|
||||
TopBarWidget(
|
||||
QWidget *parent,
|
||||
@@ -66,8 +62,7 @@ public:
|
||||
void setAnimatingMode(bool enabled);
|
||||
|
||||
void setActiveChat(
|
||||
Dialogs::Key chat,
|
||||
Section section,
|
||||
ActiveChat activeChat,
|
||||
SendActionPainter *sendAction);
|
||||
void setCustomTitle(const QString &title);
|
||||
|
||||
@@ -131,8 +126,7 @@ private:
|
||||
void updateUnreadBadge();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
Dialogs::Key _activeChat;
|
||||
Section _section = Section::History;
|
||||
ActiveChat _activeChat;
|
||||
QString _customTitleText;
|
||||
|
||||
int _selectedCount = 0;
|
||||
|
||||
@@ -61,6 +61,85 @@ constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
void PaintWaveform(
|
||||
Painter &p,
|
||||
const VoiceData *voiceData,
|
||||
int availableWidth,
|
||||
bool selected,
|
||||
bool outbg,
|
||||
float64 progress) {
|
||||
const auto wf = [&]() -> const VoiceWaveform* {
|
||||
if (!voiceData) {
|
||||
return nullptr;
|
||||
}
|
||||
if (voiceData->waveform.isEmpty()) {
|
||||
return nullptr;
|
||||
} else if (voiceData->waveform.at(0) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return &voiceData->waveform;
|
||||
}();
|
||||
|
||||
// Rescale waveform by going in waveform.size * bar_count 1D grid.
|
||||
const auto active = outbg
|
||||
? (selected
|
||||
? st::msgWaveformOutActiveSelected
|
||||
: st::msgWaveformOutActive)
|
||||
: (selected
|
||||
? st::msgWaveformInActiveSelected
|
||||
: st::msgWaveformInActive);
|
||||
const auto inactive = outbg
|
||||
? (selected
|
||||
? st::msgWaveformOutInactiveSelected
|
||||
: st::msgWaveformOutInactive)
|
||||
: (selected
|
||||
? st::msgWaveformInInactiveSelected
|
||||
: st::msgWaveformInInactive);
|
||||
const auto wfSize = wf
|
||||
? wf->size()
|
||||
: ::Media::Player::kWaveformSamplesCount;
|
||||
const auto activeWidth = std::round(availableWidth * progress);
|
||||
|
||||
const auto &barWidth = st::msgWaveformBar;
|
||||
const auto barCount = std::min(
|
||||
availableWidth / (barWidth + st::msgWaveformSkip),
|
||||
wfSize);
|
||||
const auto barNormValue = (wf ? voiceData->wavemax : 0) + 1;
|
||||
const auto maxDelta = st::msgWaveformMax - st::msgWaveformMin;
|
||||
const auto &bottom = st::msgWaveformMax;
|
||||
p.setPen(Qt::NoPen);
|
||||
for (auto i = 0, barLeft = 0, sum = 0, maxValue = 0; i < wfSize; ++i) {
|
||||
const auto value = wf ? wf->at(i) : 0;
|
||||
if (sum + barCount < wfSize) {
|
||||
maxValue = std::max(maxValue, value);
|
||||
sum += barCount;
|
||||
continue;
|
||||
}
|
||||
// Draw bar.
|
||||
sum = sum + barCount - wfSize;
|
||||
if (sum < (barCount + 1) / 2) {
|
||||
maxValue = std::max(maxValue, value);
|
||||
}
|
||||
const auto barValue = ((maxValue * maxDelta) + (barNormValue / 2))
|
||||
/ barNormValue;
|
||||
const auto barHeight = st::msgWaveformMin + barValue;
|
||||
const auto barTop = bottom - barValue;
|
||||
|
||||
if ((barLeft < activeWidth) && (barLeft + barWidth > activeWidth)) {
|
||||
const auto leftWidth = activeWidth - barLeft;
|
||||
const auto rightWidth = barWidth - leftWidth;
|
||||
p.fillRect(barLeft, barTop, leftWidth, barHeight, active);
|
||||
p.fillRect(activeWidth, barTop, rightWidth, barHeight, inactive);
|
||||
} else {
|
||||
const auto &color = (barLeft >= activeWidth) ? inactive : active;
|
||||
p.fillRect(barLeft, barTop, barWidth, barHeight, color);
|
||||
}
|
||||
barLeft += barWidth + st::msgWaveformSkip;
|
||||
|
||||
maxValue = (sum < (barCount + 1) / 2) ? 0 : value;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Document::Document(
|
||||
@@ -418,79 +497,42 @@ void Document::draw(
|
||||
if (const auto voice = Get<HistoryDocumentVoice>()) {
|
||||
ensureDataMediaCreated();
|
||||
|
||||
const VoiceWaveform *wf = nullptr;
|
||||
uchar norm_value = 0;
|
||||
if (const auto voiceData = _data->voice()) {
|
||||
wf = &voiceData->waveform;
|
||||
if (wf->isEmpty()) {
|
||||
wf = nullptr;
|
||||
if (voiceData->waveform.isEmpty()) {
|
||||
if (loaded) {
|
||||
Local::countVoiceWaveform(_dataMedia.get());
|
||||
}
|
||||
} else if (wf->at(0) < 0) {
|
||||
wf = nullptr;
|
||||
} else {
|
||||
norm_value = voiceData->wavemax;
|
||||
}
|
||||
}
|
||||
auto progress = ([voice] {
|
||||
|
||||
const auto progress = [&] {
|
||||
if (!outbg
|
||||
&& !voice->_playback
|
||||
&& _realParent->hasUnreadMediaFlag()) {
|
||||
return 1.;
|
||||
}
|
||||
if (voice->seeking()) {
|
||||
return voice->seekingCurrent();
|
||||
} else if (voice->_playback) {
|
||||
return voice->_playback->progress.current();
|
||||
}
|
||||
return 0.;
|
||||
})();
|
||||
}();
|
||||
if (voice->seeking()) {
|
||||
voiceStatusOverride = Ui::FormatPlayedText(qRound(progress * voice->_lastDurationMs) / 1000, voice->_lastDurationMs / 1000);
|
||||
voiceStatusOverride = Ui::FormatPlayedText(
|
||||
std::round(progress * voice->_lastDurationMs) / 1000,
|
||||
voice->_lastDurationMs / 1000);
|
||||
}
|
||||
|
||||
// rescale waveform by going in waveform.size * bar_count 1D grid
|
||||
auto active = outbg ? (selected ? st::msgWaveformOutActiveSelected : st::msgWaveformOutActive) : (selected ? st::msgWaveformInActiveSelected : st::msgWaveformInActive);
|
||||
auto inactive = outbg ? (selected ? st::msgWaveformOutInactiveSelected : st::msgWaveformOutInactive) : (selected ? st::msgWaveformInInactiveSelected : st::msgWaveformInInactive);
|
||||
auto wf_size = wf ? wf->size() : ::Media::Player::kWaveformSamplesCount;
|
||||
auto availw = namewidth + st::msgWaveformSkip;
|
||||
auto activew = qRound(availw * progress);
|
||||
if (!outbg
|
||||
&& !voice->_playback
|
||||
&& _realParent->hasUnreadMediaFlag()) {
|
||||
activew = availw;
|
||||
}
|
||||
auto bar_count = qMin(availw / (st::msgWaveformBar + st::msgWaveformSkip), wf_size);
|
||||
auto max_value = 0;
|
||||
auto max_delta = st::msgWaveformMax - st::msgWaveformMin;
|
||||
auto bottom = st.padding.top() - topMinus + st::msgWaveformMax;
|
||||
p.setPen(Qt::NoPen);
|
||||
for (auto i = 0, bar_x = 0, sum_i = 0; i < wf_size; ++i) {
|
||||
auto value = wf ? wf->at(i) : 0;
|
||||
if (sum_i + bar_count >= wf_size) { // draw bar
|
||||
sum_i = sum_i + bar_count - wf_size;
|
||||
if (sum_i < (bar_count + 1) / 2) {
|
||||
if (max_value < value) max_value = value;
|
||||
}
|
||||
auto bar_value = ((max_value * max_delta) + ((norm_value + 1) / 2)) / (norm_value + 1);
|
||||
|
||||
if (bar_x >= activew) {
|
||||
p.fillRect(nameleft + bar_x, bottom - bar_value, st::msgWaveformBar, st::msgWaveformMin + bar_value, inactive);
|
||||
} else if (bar_x + st::msgWaveformBar <= activew) {
|
||||
p.fillRect(nameleft + bar_x, bottom - bar_value, st::msgWaveformBar, st::msgWaveformMin + bar_value, active);
|
||||
} else {
|
||||
p.fillRect(nameleft + bar_x, bottom - bar_value, activew - bar_x, st::msgWaveformMin + bar_value, active);
|
||||
p.fillRect(nameleft + activew, bottom - bar_value, st::msgWaveformBar - (activew - bar_x), st::msgWaveformMin + bar_value, inactive);
|
||||
}
|
||||
bar_x += st::msgWaveformBar + st::msgWaveformSkip;
|
||||
|
||||
if (sum_i < (bar_count + 1) / 2) {
|
||||
max_value = 0;
|
||||
} else {
|
||||
max_value = value;
|
||||
}
|
||||
} else {
|
||||
if (max_value < value) max_value = value;
|
||||
|
||||
sum_i += bar_count;
|
||||
}
|
||||
}
|
||||
p.save();
|
||||
p.translate(nameleft, st.padding.top() - topMinus);
|
||||
PaintWaveform(p,
|
||||
_data->voice(),
|
||||
namewidth + st::msgWaveformSkip,
|
||||
selected,
|
||||
outbg,
|
||||
progress);
|
||||
p.restore();
|
||||
} else if (auto named = Get<HistoryDocumentNamed>()) {
|
||||
p.setFont(st::semiboldFont);
|
||||
p.setPen(outbg ? (selected ? st::historyFileNameOutFgSelected : st::historyFileNameOutFg) : (selected ? st::historyFileNameInFgSelected : st::historyFileNameInFg));
|
||||
|
||||
@@ -33,7 +33,7 @@ Game::Game(
|
||||
, _title(st::msgMinWidth - st::webPageLeft)
|
||||
, _description(st::msgMinWidth - st::webPageLeft) {
|
||||
if (!consumed.text.isEmpty()) {
|
||||
const auto context = Core::UiIntegration::Context{
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = &history()->session()
|
||||
};
|
||||
_description.setMarkedText(
|
||||
@@ -418,7 +418,7 @@ void Game::parentTextUpdated() {
|
||||
if (const auto media = _parent->data()->media()) {
|
||||
const auto consumed = media->consumedMessageText();
|
||||
if (!consumed.text.isEmpty()) {
|
||||
const auto context = Core::UiIntegration::Context{
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = &history()->session()
|
||||
};
|
||||
_description.setMarkedText(
|
||||
|
||||
@@ -139,7 +139,7 @@ Ui::Text::String Media::createCaption(
|
||||
- st::msgPadding.left()
|
||||
- st::msgPadding.right();
|
||||
auto result = Ui::Text::String(minResizeWidth);
|
||||
const auto context = Core::UiIntegration::Context{
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = &history()->session()
|
||||
};
|
||||
result.setMarkedText(
|
||||
|
||||
@@ -201,11 +201,12 @@ QSize WebPage::countOptimalSize() {
|
||||
- st::msgPadding.right()
|
||||
- st::webPageLeft);
|
||||
}
|
||||
auto context = Core::UiIntegration::Context();
|
||||
auto context = Core::MarkedTextContext();
|
||||
using MarkedTextContext = Core::MarkedTextContext;
|
||||
if (_data->siteName == qstr("Twitter")) {
|
||||
context.type = Core::UiIntegration::HashtagMentionType::Twitter;
|
||||
context.type = MarkedTextContext::HashtagMentionType::Twitter;
|
||||
} else if (_data->siteName == qstr("Instagram")) {
|
||||
context.type = Core::UiIntegration::HashtagMentionType::Instagram;
|
||||
context.type = MarkedTextContext::HashtagMentionType::Instagram;
|
||||
}
|
||||
_description.setMarkedText(
|
||||
st::webPageDescriptionStyle,
|
||||
|
||||
@@ -575,12 +575,13 @@ void WrapWidget::showTopBarMenu() {
|
||||
return _topBarMenu->addAction(text, std::move(callback));
|
||||
};
|
||||
if (const auto peer = key().peer()) {
|
||||
Window::FillPeerMenu(
|
||||
Window::FillDialogsEntryMenu(
|
||||
_controller->parentController(),
|
||||
peer,
|
||||
FilterId(),
|
||||
addAction,
|
||||
Window::PeerMenuSource::Profile);
|
||||
Dialogs::EntryState{
|
||||
.key = peer->owner().history(peer),
|
||||
.section = Dialogs::EntryState::Section::Profile,
|
||||
},
|
||||
addAction);
|
||||
//} else if (const auto feed = key().feed()) { // #feed
|
||||
// Window::FillFeedMenu(
|
||||
// _controller->parentController(),
|
||||
|
||||
@@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "data/data_cloud_file.h"
|
||||
#include "api/api_common.h"
|
||||
|
||||
class FileLoader;
|
||||
class History;
|
||||
class UserData;
|
||||
|
||||
namespace Data {
|
||||
class LocationPoint;
|
||||
@@ -125,4 +127,10 @@ private:
|
||||
|
||||
};
|
||||
|
||||
struct ResultSelected {
|
||||
not_null<Result*> result;
|
||||
not_null<UserData*> bot;
|
||||
Api::SendOptions options;
|
||||
};
|
||||
|
||||
} // namespace InlineBots
|
||||
|
||||
769
Telegram/SourceFiles/inline_bots/inline_results_inner.cpp
Normal file
@@ -0,0 +1,769 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "inline_bots/inline_results_inner.h"
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "chat_helpers/send_context_menu.h" // SendMenu::FillSendMenu
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "inline_bots/inline_bot_result.h"
|
||||
#include "inline_bots/inline_bot_layout_item.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "mainwindow.h"
|
||||
#include "facades.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace InlineBots {
|
||||
namespace Layout {
|
||||
|
||||
Inner::Inner(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _updateInlineItems([=] { updateInlineItems(); })
|
||||
, _previewTimer([=] { showPreview(); }) {
|
||||
resize(st::emojiPanWidth - st::emojiScroll.width - st::buttonRadius, st::inlineResultsMinHeight);
|
||||
|
||||
setMouseTracking(true);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
_controller->session().downloaderTaskFinished(
|
||||
) | rpl::start_with_next([=] {
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
subscribe(controller->gifPauseLevelChanged(), [this] {
|
||||
if (!_controller->isGifPausedAtLeastFor(Window::GifPauseReason::InlineResults)) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
_controller->session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::Rights
|
||||
) | rpl::filter([=](const Data::PeerUpdate &update) {
|
||||
return (update.peer.get() == _inlineQueryPeer);
|
||||
}) | rpl::start_with_next([=] {
|
||||
auto isRestricted = (_restrictedLabel != nullptr);
|
||||
if (isRestricted != isRestrictedView()) {
|
||||
auto h = countHeight();
|
||||
if (h != height()) resize(width(), h);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Inner::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
_visibleBottom = visibleBottom;
|
||||
if (_visibleTop != visibleTop) {
|
||||
_visibleTop = visibleTop;
|
||||
_lastScrolled = crl::now();
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::checkRestrictedPeer() {
|
||||
if (_inlineQueryPeer) {
|
||||
const auto error = Data::RestrictionError(
|
||||
_inlineQueryPeer,
|
||||
ChatRestriction::f_send_inline);
|
||||
if (error) {
|
||||
if (!_restrictedLabel) {
|
||||
_restrictedLabel.create(this, *error, st::stickersRestrictedLabel);
|
||||
_restrictedLabel->show();
|
||||
_restrictedLabel->move(st::inlineResultsLeft - st::buttonRadius, st::stickerPanPadding);
|
||||
_restrictedLabel->resizeToNaturalWidth(width() - (st::inlineResultsLeft - st::buttonRadius) * 2);
|
||||
if (_switchPmButton) {
|
||||
_switchPmButton->hide();
|
||||
}
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_restrictedLabel) {
|
||||
_restrictedLabel.destroy();
|
||||
if (_switchPmButton) {
|
||||
_switchPmButton->show();
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
bool Inner::isRestrictedView() {
|
||||
checkRestrictedPeer();
|
||||
return (_restrictedLabel != nullptr);
|
||||
}
|
||||
|
||||
int Inner::countHeight() {
|
||||
if (isRestrictedView()) {
|
||||
return st::stickerPanPadding + _restrictedLabel->height() + st::stickerPanPadding;
|
||||
} else if (_rows.isEmpty() && !_switchPmButton) {
|
||||
return st::stickerPanPadding + st::normalFont->height + st::stickerPanPadding;
|
||||
}
|
||||
auto result = st::stickerPanPadding;
|
||||
if (_switchPmButton) {
|
||||
result += _switchPmButton->height() + st::inlineResultsSkip;
|
||||
}
|
||||
for (int i = 0, l = _rows.count(); i < l; ++i) {
|
||||
result += _rows[i].height;
|
||||
}
|
||||
return result + st::stickerPanPadding;
|
||||
}
|
||||
|
||||
QString Inner::tooltipText() const {
|
||||
if (const auto lnk = ClickHandler::getActive()) {
|
||||
return lnk->tooltip();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QPoint Inner::tooltipPos() const {
|
||||
return _lastMousePos;
|
||||
}
|
||||
|
||||
bool Inner::tooltipWindowActive() const {
|
||||
return Ui::AppInFocus() && Ui::InFocusChain(window());
|
||||
}
|
||||
|
||||
rpl::producer<> Inner::inlineRowsCleared() const {
|
||||
return _inlineRowsCleared.events();
|
||||
}
|
||||
|
||||
Inner::~Inner() = default;
|
||||
|
||||
void Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
QRect r = e ? e->rect() : rect();
|
||||
if (r != rect()) {
|
||||
p.setClipRect(r);
|
||||
}
|
||||
p.fillRect(r, st::emojiPanBg);
|
||||
|
||||
paintInlineItems(p, r);
|
||||
}
|
||||
|
||||
void Inner::paintInlineItems(Painter &p, const QRect &r) {
|
||||
if (_restrictedLabel) {
|
||||
return;
|
||||
}
|
||||
if (_rows.isEmpty() && !_switchPmButton) {
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st::noContactsColor);
|
||||
p.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), tr::lng_inline_bot_no_results(tr::now), style::al_center);
|
||||
return;
|
||||
}
|
||||
auto gifPaused = _controller->isGifPausedAtLeastFor(Window::GifPauseReason::InlineResults);
|
||||
InlineBots::Layout::PaintContext context(crl::now(), false, gifPaused, false);
|
||||
|
||||
auto top = st::stickerPanPadding;
|
||||
if (_switchPmButton) {
|
||||
top += _switchPmButton->height() + st::inlineResultsSkip;
|
||||
}
|
||||
|
||||
auto fromx = rtl() ? (width() - r.x() - r.width()) : r.x();
|
||||
auto tox = rtl() ? (width() - r.x()) : (r.x() + r.width());
|
||||
for (auto row = 0, rows = _rows.size(); row != rows; ++row) {
|
||||
auto &inlineRow = _rows[row];
|
||||
if (top >= r.top() + r.height()) break;
|
||||
if (top + inlineRow.height > r.top()) {
|
||||
auto left = st::inlineResultsLeft - st::buttonRadius;
|
||||
if (row == rows - 1) context.lastRow = true;
|
||||
for (int col = 0, cols = inlineRow.items.size(); col < cols; ++col) {
|
||||
if (left >= tox) break;
|
||||
|
||||
auto item = inlineRow.items.at(col);
|
||||
auto w = item->width();
|
||||
if (left + w > fromx) {
|
||||
p.translate(left, top);
|
||||
item->paint(p, r.translated(-left, -top), &context);
|
||||
p.translate(-left, -top);
|
||||
}
|
||||
left += w;
|
||||
if (item->hasRightSkip()) {
|
||||
left += st::inlineResultsSkip;
|
||||
}
|
||||
}
|
||||
}
|
||||
top += inlineRow.height;
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::mousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
_lastMousePos = e->globalPos();
|
||||
updateSelected();
|
||||
|
||||
_pressed = _selected;
|
||||
ClickHandler::pressed();
|
||||
_previewTimer.callOnce(QApplication::startDragTime());
|
||||
}
|
||||
|
||||
void Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
_previewTimer.cancel();
|
||||
|
||||
auto pressed = std::exchange(_pressed, -1);
|
||||
auto activated = ClickHandler::unpressed();
|
||||
|
||||
if (_previewShown) {
|
||||
_previewShown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_lastMousePos = e->globalPos();
|
||||
updateSelected();
|
||||
|
||||
if (_selected < 0 || _selected != pressed || !activated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dynamic_cast<InlineBots::Layout::SendClickHandler*>(activated.get())) {
|
||||
int row = _selected / MatrixRowShift, column = _selected % MatrixRowShift;
|
||||
selectInlineResult(row, column);
|
||||
} else {
|
||||
ActivateClickHandler(window(), activated, e->button());
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::selectInlineResult(int row, int column) {
|
||||
selectInlineResult(row, column, Api::SendOptions());
|
||||
}
|
||||
|
||||
void Inner::selectInlineResult(
|
||||
int row,
|
||||
int column,
|
||||
Api::SendOptions options) {
|
||||
if (row >= _rows.size() || column >= _rows.at(row).items.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto item = _rows[row].items[column];
|
||||
if (const auto inlineResult = item->getResult()) {
|
||||
if (inlineResult->onChoose(item)) {
|
||||
_resultSelectedCallback({
|
||||
.result = inlineResult,
|
||||
.bot = _inlineBot,
|
||||
.options = std::move(options)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::mouseMoveEvent(QMouseEvent *e) {
|
||||
_lastMousePos = e->globalPos();
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
void Inner::leaveEventHook(QEvent *e) {
|
||||
clearSelection();
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
|
||||
void Inner::leaveToChildEvent(QEvent *e, QWidget *child) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
void Inner::enterFromChildEvent(QEvent *e, QWidget *child) {
|
||||
_lastMousePos = QCursor::pos();
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
void Inner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (_selected < 0 || _pressed >= 0) {
|
||||
return;
|
||||
}
|
||||
const auto row = _selected / MatrixRowShift;
|
||||
const auto column = _selected % MatrixRowShift;
|
||||
const auto type = SendMenu::Type::Scheduled;
|
||||
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(this);
|
||||
|
||||
const auto send = [=](Api::SendOptions options) {
|
||||
selectInlineResult(row, column, options);
|
||||
};
|
||||
SendMenu::FillSendMenu(
|
||||
_menu,
|
||||
[&] { return type; },
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send));
|
||||
|
||||
if (!_menu->actions().empty()) {
|
||||
_menu->popup(QCursor::pos());
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::clearSelection() {
|
||||
if (_selected >= 0) {
|
||||
int srow = _selected / MatrixRowShift, scol = _selected % MatrixRowShift;
|
||||
Assert(srow >= 0 && srow < _rows.size() && scol >= 0 && scol < _rows.at(srow).items.size());
|
||||
ClickHandler::clearActive(_rows.at(srow).items.at(scol));
|
||||
setCursor(style::cur_default);
|
||||
}
|
||||
_selected = _pressed = -1;
|
||||
update();
|
||||
}
|
||||
|
||||
void Inner::hideFinished() {
|
||||
clearHeavyData();
|
||||
}
|
||||
|
||||
void Inner::clearHeavyData() {
|
||||
clearInlineRows(false);
|
||||
for (const auto &[result, layout] : _inlineLayouts) {
|
||||
layout->unloadHeavyPart();
|
||||
}
|
||||
}
|
||||
|
||||
bool Inner::inlineRowsAddItem(Result *result, Row &row, int32 &sumWidth) {
|
||||
auto layout = layoutPrepareInlineResult(result, (_rows.size() * MatrixRowShift) + row.items.size());
|
||||
if (!layout) return false;
|
||||
|
||||
layout->preload();
|
||||
if (inlineRowFinalize(row, sumWidth, layout->isFullLine())) {
|
||||
layout->setPosition(_rows.size() * MatrixRowShift);
|
||||
}
|
||||
|
||||
sumWidth += layout->maxWidth();
|
||||
if (!row.items.isEmpty() && row.items.back()->hasRightSkip()) {
|
||||
sumWidth += st::inlineResultsSkip;
|
||||
}
|
||||
|
||||
row.items.push_back(layout);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inner::inlineRowFinalize(Row &row, int32 &sumWidth, bool force) {
|
||||
if (row.items.isEmpty()) return false;
|
||||
|
||||
auto full = (row.items.size() >= kInlineItemsMaxPerRow);
|
||||
auto big = (sumWidth >= st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft);
|
||||
if (full || big || force) {
|
||||
_rows.push_back(layoutInlineRow(row, (full || big) ? sumWidth : 0));
|
||||
row = Row();
|
||||
row.items.reserve(kInlineItemsMaxPerRow);
|
||||
sumWidth = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Inner::inlineBotChanged() {
|
||||
refreshInlineRows(nullptr, nullptr, nullptr, true);
|
||||
}
|
||||
|
||||
void Inner::clearInlineRows(bool resultsDeleted) {
|
||||
if (resultsDeleted) {
|
||||
_selected = _pressed = -1;
|
||||
} else {
|
||||
clearSelection();
|
||||
for_const (auto &row, _rows) {
|
||||
for_const (auto &item, row.items) {
|
||||
item->setPosition(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_rows.clear();
|
||||
}
|
||||
|
||||
ItemBase *Inner::layoutPrepareInlineResult(Result *result, int32 position) {
|
||||
auto it = _inlineLayouts.find(result);
|
||||
if (it == _inlineLayouts.cend()) {
|
||||
if (auto layout = ItemBase::createLayout(this, result, _inlineWithThumb)) {
|
||||
it = _inlineLayouts.emplace(result, std::move(layout)).first;
|
||||
it->second->initDimensions();
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (!it->second->maxWidth()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
it->second->setPosition(position);
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
void Inner::deleteUnusedInlineLayouts() {
|
||||
if (_rows.isEmpty()) { // delete all
|
||||
_inlineLayouts.clear();
|
||||
} else {
|
||||
for (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {
|
||||
if (i->second->position() < 0) {
|
||||
i = _inlineLayouts.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inner::Row &Inner::layoutInlineRow(Row &row, int32 sumWidth) {
|
||||
auto count = int(row.items.size());
|
||||
Assert(count <= kInlineItemsMaxPerRow);
|
||||
|
||||
// enumerate items in the order of growing maxWidth()
|
||||
// for that sort item indices by maxWidth()
|
||||
int indices[kInlineItemsMaxPerRow];
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
indices[i] = i;
|
||||
}
|
||||
std::sort(indices, indices + count, [&row](int a, int b) -> bool {
|
||||
return row.items.at(a)->maxWidth() < row.items.at(b)->maxWidth();
|
||||
});
|
||||
|
||||
row.height = 0;
|
||||
int availw = width() - (st::inlineResultsLeft - st::buttonRadius);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int index = indices[i];
|
||||
int w = sumWidth ? (row.items.at(index)->maxWidth() * availw / sumWidth) : row.items.at(index)->maxWidth();
|
||||
int actualw = qMax(w, int(st::inlineResultsMinWidth));
|
||||
row.height = qMax(row.height, row.items.at(index)->resizeGetHeight(actualw));
|
||||
if (sumWidth) {
|
||||
availw -= actualw;
|
||||
sumWidth -= row.items.at(index)->maxWidth();
|
||||
if (index > 0 && row.items.at(index - 1)->hasRightSkip()) {
|
||||
availw -= st::inlineResultsSkip;
|
||||
sumWidth -= st::inlineResultsSkip;
|
||||
}
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
void Inner::preloadImages() {
|
||||
for (auto row = 0, rows = _rows.size(); row != rows; ++row) {
|
||||
for (auto col = 0, cols = _rows[row].items.size(); col != cols; ++col) {
|
||||
_rows[row].items[col]->preload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::hideInlineRowsPanel() {
|
||||
clearInlineRows(false);
|
||||
}
|
||||
|
||||
void Inner::clearInlineRowsPanel() {
|
||||
clearInlineRows(false);
|
||||
}
|
||||
|
||||
void Inner::refreshSwitchPmButton(const CacheEntry *entry) {
|
||||
if (!entry || entry->switchPmText.isEmpty()) {
|
||||
_switchPmButton.destroy();
|
||||
_switchPmStartToken.clear();
|
||||
} else {
|
||||
if (!_switchPmButton) {
|
||||
_switchPmButton.create(this, nullptr, st::switchPmButton);
|
||||
_switchPmButton->show();
|
||||
_switchPmButton->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
_switchPmButton->addClickHandler([=] { switchPm(); });
|
||||
}
|
||||
_switchPmButton->setText(rpl::single(entry->switchPmText));
|
||||
_switchPmStartToken = entry->switchPmStartToken;
|
||||
const auto buttonTop = st::stickerPanPadding;
|
||||
_switchPmButton->move(st::inlineResultsLeft - st::buttonRadius, buttonTop);
|
||||
if (isRestrictedView()) {
|
||||
_switchPmButton->hide();
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
int Inner::refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *entry, bool resultsDeleted) {
|
||||
_inlineBot = bot;
|
||||
_inlineQueryPeer = queryPeer;
|
||||
refreshSwitchPmButton(entry);
|
||||
auto clearResults = [&] {
|
||||
if (!entry) {
|
||||
return true;
|
||||
}
|
||||
if (entry->results.empty() && entry->switchPmText.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
auto clearResultsResult = clearResults(); // Clang workaround.
|
||||
if (clearResultsResult) {
|
||||
if (resultsDeleted) {
|
||||
clearInlineRows(true);
|
||||
deleteUnusedInlineLayouts();
|
||||
}
|
||||
_inlineRowsCleared.fire({});
|
||||
return 0;
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
|
||||
Assert(_inlineBot != 0);
|
||||
|
||||
auto count = int(entry->results.size());
|
||||
auto from = validateExistingInlineRows(entry->results);
|
||||
auto added = 0;
|
||||
|
||||
if (count) {
|
||||
_rows.reserve(count);
|
||||
auto row = Row();
|
||||
row.items.reserve(kInlineItemsMaxPerRow);
|
||||
auto sumWidth = 0;
|
||||
for (auto i = from; i != count; ++i) {
|
||||
if (inlineRowsAddItem(entry->results[i].get(), row, sumWidth)) {
|
||||
++added;
|
||||
}
|
||||
}
|
||||
inlineRowFinalize(row, sumWidth, true);
|
||||
}
|
||||
|
||||
auto h = countHeight();
|
||||
if (h != height()) resize(width(), h);
|
||||
update();
|
||||
|
||||
_lastMousePos = QCursor::pos();
|
||||
updateSelected();
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
int Inner::validateExistingInlineRows(const Results &results) {
|
||||
int count = results.size(), until = 0, untilrow = 0, untilcol = 0;
|
||||
for (; until < count;) {
|
||||
if (untilrow >= _rows.size() || _rows[untilrow].items[untilcol]->getResult() != results[until].get()) {
|
||||
break;
|
||||
}
|
||||
++until;
|
||||
if (++untilcol == _rows[untilrow].items.size()) {
|
||||
++untilrow;
|
||||
untilcol = 0;
|
||||
}
|
||||
}
|
||||
if (until == count) { // all items are layed out
|
||||
if (untilrow == _rows.size()) { // nothing changed
|
||||
return until;
|
||||
}
|
||||
|
||||
for (int i = untilrow, l = _rows.size(), skip = untilcol; i < l; ++i) {
|
||||
for (int j = 0, s = _rows[i].items.size(); j < s; ++j) {
|
||||
if (skip) {
|
||||
--skip;
|
||||
} else {
|
||||
_rows[i].items[j]->setPosition(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!untilcol) { // all good rows are filled
|
||||
_rows.resize(untilrow);
|
||||
return until;
|
||||
}
|
||||
_rows.resize(untilrow + 1);
|
||||
_rows[untilrow].items.resize(untilcol);
|
||||
_rows[untilrow] = layoutInlineRow(_rows[untilrow]);
|
||||
return until;
|
||||
}
|
||||
if (untilrow && !untilcol) { // remove last row, maybe it is not full
|
||||
--untilrow;
|
||||
untilcol = _rows[untilrow].items.size();
|
||||
}
|
||||
until -= untilcol;
|
||||
|
||||
for (int i = untilrow, l = _rows.size(); i < l; ++i) {
|
||||
for (int j = 0, s = _rows[i].items.size(); j < s; ++j) {
|
||||
_rows[i].items[j]->setPosition(-1);
|
||||
}
|
||||
}
|
||||
_rows.resize(untilrow);
|
||||
|
||||
if (_rows.isEmpty()) {
|
||||
_inlineWithThumb = false;
|
||||
for (int i = until; i < count; ++i) {
|
||||
if (results.at(i)->hasThumbDisplay()) {
|
||||
_inlineWithThumb = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return until;
|
||||
}
|
||||
|
||||
void Inner::inlineItemLayoutChanged(const ItemBase *layout) {
|
||||
if (_selected < 0 || !isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int row = _selected / MatrixRowShift, col = _selected % MatrixRowShift;
|
||||
if (row < _rows.size() && col < _rows.at(row).items.size()) {
|
||||
if (layout == _rows.at(row).items.at(col)) {
|
||||
updateSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::inlineItemRepaint(const ItemBase *layout) {
|
||||
auto ms = crl::now();
|
||||
if (_lastScrolled + 100 <= ms) {
|
||||
update();
|
||||
} else {
|
||||
_updateInlineItems.callOnce(_lastScrolled + 100 - ms);
|
||||
}
|
||||
}
|
||||
|
||||
bool Inner::inlineItemVisible(const ItemBase *layout) {
|
||||
int32 position = layout->position();
|
||||
if (position < 0 || !isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int row = position / MatrixRowShift, col = position % MatrixRowShift;
|
||||
Assert((row < _rows.size()) && (col < _rows[row].items.size()));
|
||||
|
||||
auto &inlineItems = _rows[row].items;
|
||||
int top = st::stickerPanPadding;
|
||||
for (int32 i = 0; i < row; ++i) {
|
||||
top += _rows.at(i).height;
|
||||
}
|
||||
|
||||
return (top < _visibleBottom) && (top + _rows[row].items[col]->height() > _visibleTop);
|
||||
}
|
||||
|
||||
Data::FileOrigin Inner::inlineItemFileOrigin() {
|
||||
return Data::FileOrigin();
|
||||
}
|
||||
|
||||
void Inner::updateSelected() {
|
||||
if (_pressed >= 0 && !_previewShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto newSelected = -1;
|
||||
auto p = mapFromGlobal(_lastMousePos);
|
||||
|
||||
int sx = (rtl() ? width() - p.x() : p.x()) - (st::inlineResultsLeft - st::buttonRadius);
|
||||
int sy = p.y() - st::stickerPanPadding;
|
||||
if (_switchPmButton) {
|
||||
sy -= _switchPmButton->height() + st::inlineResultsSkip;
|
||||
}
|
||||
int row = -1, col = -1, sel = -1;
|
||||
ClickHandlerPtr lnk;
|
||||
ClickHandlerHost *lnkhost = nullptr;
|
||||
HistoryView::CursorState cursor = HistoryView::CursorState::None;
|
||||
if (sy >= 0) {
|
||||
row = 0;
|
||||
for (int rows = _rows.size(); row < rows; ++row) {
|
||||
if (sy < _rows[row].height) {
|
||||
break;
|
||||
}
|
||||
sy -= _rows[row].height;
|
||||
}
|
||||
}
|
||||
if (sx >= 0 && row >= 0 && row < _rows.size()) {
|
||||
auto &inlineItems = _rows[row].items;
|
||||
col = 0;
|
||||
for (int cols = inlineItems.size(); col < cols; ++col) {
|
||||
int width = inlineItems.at(col)->width();
|
||||
if (sx < width) {
|
||||
break;
|
||||
}
|
||||
sx -= width;
|
||||
if (inlineItems.at(col)->hasRightSkip()) {
|
||||
sx -= st::inlineResultsSkip;
|
||||
}
|
||||
}
|
||||
if (col < inlineItems.size()) {
|
||||
sel = row * MatrixRowShift + col;
|
||||
auto result = inlineItems[col]->getState(
|
||||
QPoint(sx, sy),
|
||||
HistoryView::StateRequest());
|
||||
lnk = result.link;
|
||||
cursor = result.cursor;
|
||||
lnkhost = inlineItems[col];
|
||||
} else {
|
||||
row = col = -1;
|
||||
}
|
||||
} else {
|
||||
row = col = -1;
|
||||
}
|
||||
int srow = (_selected >= 0) ? (_selected / MatrixRowShift) : -1;
|
||||
int scol = (_selected >= 0) ? (_selected % MatrixRowShift) : -1;
|
||||
if (_selected != sel) {
|
||||
if (srow >= 0 && scol >= 0) {
|
||||
Assert(srow >= 0 && srow < _rows.size() && scol >= 0 && scol < _rows.at(srow).items.size());
|
||||
_rows[srow].items[scol]->update();
|
||||
}
|
||||
_selected = sel;
|
||||
if (row >= 0 && col >= 0) {
|
||||
Assert(row >= 0 && row < _rows.size() && col >= 0 && col < _rows.at(row).items.size());
|
||||
_rows[row].items[col]->update();
|
||||
}
|
||||
if (_previewShown && _selected >= 0 && _pressed != _selected) {
|
||||
_pressed = _selected;
|
||||
if (row >= 0 && col >= 0) {
|
||||
auto layout = _rows.at(row).items.at(col);
|
||||
if (const auto w = App::wnd()) {
|
||||
if (const auto previewDocument = layout->getPreviewDocument()) {
|
||||
w->showMediaPreview(
|
||||
Data::FileOrigin(),
|
||||
previewDocument);
|
||||
} else if (auto previewPhoto = layout->getPreviewPhoto()) {
|
||||
w->showMediaPreview(Data::FileOrigin(), previewPhoto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ClickHandler::setActive(lnk, lnkhost)) {
|
||||
setCursor(lnk ? style::cur_pointer : style::cur_default);
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
if (lnk) {
|
||||
Ui::Tooltip::Show(1000, this);
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::showPreview() {
|
||||
if (_pressed < 0) return;
|
||||
|
||||
int row = _pressed / MatrixRowShift, col = _pressed % MatrixRowShift;
|
||||
if (row < _rows.size() && col < _rows.at(row).items.size()) {
|
||||
auto layout = _rows.at(row).items.at(col);
|
||||
if (const auto w = App::wnd()) {
|
||||
if (const auto previewDocument = layout->getPreviewDocument()) {
|
||||
_previewShown = w->showMediaPreview(Data::FileOrigin(), previewDocument);
|
||||
} else if (const auto previewPhoto = layout->getPreviewPhoto()) {
|
||||
_previewShown = w->showMediaPreview(Data::FileOrigin(), previewPhoto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::updateInlineItems() {
|
||||
auto ms = crl::now();
|
||||
if (_lastScrolled + 100 <= ms) {
|
||||
update();
|
||||
} else {
|
||||
_updateInlineItems.callOnce(_lastScrolled + 100 - ms);
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::switchPm() {
|
||||
if (_inlineBot && _inlineBot->isBot()) {
|
||||
_inlineBot->botInfo->startToken = _switchPmStartToken;
|
||||
_inlineBot->botInfo->inlineReturnTo = _currentDialogsEntryState;
|
||||
Ui::showPeerHistory(_inlineBot, ShowAndStartBotMsgId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Layout
|
||||
} // namespace InlineBots
|
||||
186
Telegram/SourceFiles/inline_bots/inline_results_inner.h
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/panel_animation.h"
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "base/timer.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "inline_bots/inline_bot_layout_item.h"
|
||||
|
||||
namespace Api {
|
||||
struct SendOptions;
|
||||
} // namespace Api
|
||||
|
||||
namespace Ui {
|
||||
class ScrollArea;
|
||||
class IconButton;
|
||||
class LinkButton;
|
||||
class RoundButton;
|
||||
class FlatLabel;
|
||||
class RippleAnimation;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace InlineBots {
|
||||
class Result;
|
||||
struct ResultSelected;
|
||||
} // namespace InlineBots
|
||||
|
||||
namespace InlineBots {
|
||||
namespace Layout {
|
||||
|
||||
constexpr int kInlineItemsMaxPerRow = 5;
|
||||
|
||||
class ItemBase;
|
||||
using Results = std::vector<std::unique_ptr<Result>>;
|
||||
|
||||
struct CacheEntry {
|
||||
QString nextOffset;
|
||||
QString switchPmText, switchPmStartToken;
|
||||
Results results;
|
||||
};
|
||||
|
||||
class Inner
|
||||
: public Ui::RpWidget
|
||||
, public Ui::AbstractTooltipShower
|
||||
, public Context
|
||||
, private base::Subscriber {
|
||||
|
||||
public:
|
||||
Inner(QWidget *parent, not_null<Window::SessionController*> controller);
|
||||
|
||||
void hideFinished();
|
||||
|
||||
void clearSelection();
|
||||
|
||||
int refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *results, bool resultsDeleted);
|
||||
void inlineBotChanged();
|
||||
void hideInlineRowsPanel();
|
||||
void clearInlineRowsPanel();
|
||||
|
||||
void preloadImages();
|
||||
|
||||
void inlineItemLayoutChanged(const ItemBase *layout) override;
|
||||
void inlineItemRepaint(const ItemBase *layout) override;
|
||||
bool inlineItemVisible(const ItemBase *layout) override;
|
||||
Data::FileOrigin inlineItemFileOrigin() override;
|
||||
|
||||
int countHeight();
|
||||
|
||||
void setResultSelectedCallback(Fn<void(ResultSelected)> callback) {
|
||||
_resultSelectedCallback = std::move(callback);
|
||||
}
|
||||
void setCurrentDialogsEntryState(Dialogs::EntryState state) {
|
||||
_currentDialogsEntryState = state;
|
||||
}
|
||||
|
||||
// Ui::AbstractTooltipShower interface.
|
||||
QString tooltipText() const override;
|
||||
QPoint tooltipPos() const override;
|
||||
bool tooltipWindowActive() const override;
|
||||
|
||||
rpl::producer<> inlineRowsCleared() const;
|
||||
|
||||
~Inner();
|
||||
|
||||
protected:
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void leaveToChildEvent(QEvent *e, QWidget *child) override;
|
||||
void enterFromChildEvent(QEvent *e, QWidget *child) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
private:
|
||||
static constexpr bool kRefreshIconsScrollAnimation = true;
|
||||
static constexpr bool kRefreshIconsNoAnimation = false;
|
||||
|
||||
struct Row {
|
||||
int height = 0;
|
||||
QVector<ItemBase*> items;
|
||||
};
|
||||
|
||||
void switchPm();
|
||||
|
||||
void updateSelected();
|
||||
void checkRestrictedPeer();
|
||||
bool isRestrictedView();
|
||||
void clearHeavyData();
|
||||
|
||||
void paintInlineItems(Painter &p, const QRect &r);
|
||||
|
||||
void refreshSwitchPmButton(const CacheEntry *entry);
|
||||
|
||||
void showPreview();
|
||||
void updateInlineItems();
|
||||
void clearInlineRows(bool resultsDeleted);
|
||||
ItemBase *layoutPrepareInlineResult(Result *result, int32 position);
|
||||
|
||||
bool inlineRowsAddItem(Result *result, Row &row, int32 &sumWidth);
|
||||
bool inlineRowFinalize(Row &row, int32 &sumWidth, bool force = false);
|
||||
|
||||
Row &layoutInlineRow(Row &row, int32 sumWidth = 0);
|
||||
void deleteUnusedInlineLayouts();
|
||||
|
||||
int validateExistingInlineRows(const Results &results);
|
||||
void selectInlineResult(int row, int column);
|
||||
void selectInlineResult(int row, int column, Api::SendOptions options);
|
||||
|
||||
not_null<Window::SessionController*> _controller;
|
||||
|
||||
int _visibleTop = 0;
|
||||
int _visibleBottom = 0;
|
||||
|
||||
UserData *_inlineBot = nullptr;
|
||||
PeerData *_inlineQueryPeer = nullptr;
|
||||
crl::time _lastScrolled = 0;
|
||||
base::Timer _updateInlineItems;
|
||||
bool _inlineWithThumb = false;
|
||||
|
||||
object_ptr<Ui::RoundButton> _switchPmButton = { nullptr };
|
||||
QString _switchPmStartToken;
|
||||
Dialogs::EntryState _currentDialogsEntryState;
|
||||
|
||||
object_ptr<Ui::FlatLabel> _restrictedLabel = { nullptr };
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
QVector<Row> _rows;
|
||||
|
||||
std::map<Result*, std::unique_ptr<ItemBase>> _inlineLayouts;
|
||||
|
||||
rpl::event_stream<> _inlineRowsCleared;
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
QPoint _lastMousePos;
|
||||
|
||||
base::Timer _previewTimer;
|
||||
bool _previewShown = false;
|
||||
|
||||
Fn<void(ResultSelected)> _resultSelectedCallback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Layout
|
||||
} // namespace InlineBots
|
||||
@@ -7,783 +7,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "inline_bots/inline_results_widget.h"
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "chat_helpers/send_context_menu.h" // SendMenu::FillSendMenu
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "inline_bots/inline_bot_result.h"
|
||||
#include "inline_bots/inline_bot_layout_item.h"
|
||||
#include "dialogs/dialogs_layout.h"
|
||||
#include "storage/localstorage.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "mainwindow.h"
|
||||
#include "mainwidget.h"
|
||||
#include "inline_bots/inline_results_inner.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "facades.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace InlineBots {
|
||||
namespace Layout {
|
||||
namespace internal {
|
||||
namespace {
|
||||
|
||||
constexpr auto kInlineBotRequestDelay = 400;
|
||||
|
||||
} // namespace
|
||||
|
||||
Inner::Inner(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _updateInlineItems([=] { updateInlineItems(); })
|
||||
, _previewTimer([=] { showPreview(); }) {
|
||||
resize(st::emojiPanWidth - st::emojiScroll.width - st::buttonRadius, st::inlineResultsMinHeight);
|
||||
|
||||
setMouseTracking(true);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
_controller->session().downloaderTaskFinished(
|
||||
) | rpl::start_with_next([=] {
|
||||
update();
|
||||
}, lifetime());
|
||||
|
||||
subscribe(controller->gifPauseLevelChanged(), [this] {
|
||||
if (!_controller->isGifPausedAtLeastFor(Window::GifPauseReason::InlineResults)) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
_controller->session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::Rights
|
||||
) | rpl::filter([=](const Data::PeerUpdate &update) {
|
||||
return (update.peer.get() == _inlineQueryPeer);
|
||||
}) | rpl::start_with_next([=] {
|
||||
auto isRestricted = (_restrictedLabel != nullptr);
|
||||
if (isRestricted != isRestrictedView()) {
|
||||
auto h = countHeight();
|
||||
if (h != height()) resize(width(), h);
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Inner::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
_visibleBottom = visibleBottom;
|
||||
if (_visibleTop != visibleTop) {
|
||||
_visibleTop = visibleTop;
|
||||
_lastScrolled = crl::now();
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::checkRestrictedPeer() {
|
||||
if (_inlineQueryPeer) {
|
||||
const auto error = Data::RestrictionError(
|
||||
_inlineQueryPeer,
|
||||
ChatRestriction::f_send_inline);
|
||||
if (error) {
|
||||
if (!_restrictedLabel) {
|
||||
_restrictedLabel.create(this, *error, st::stickersRestrictedLabel);
|
||||
_restrictedLabel->show();
|
||||
_restrictedLabel->move(st::inlineResultsLeft - st::buttonRadius, st::stickerPanPadding);
|
||||
_restrictedLabel->resizeToNaturalWidth(width() - (st::inlineResultsLeft - st::buttonRadius) * 2);
|
||||
if (_switchPmButton) {
|
||||
_switchPmButton->hide();
|
||||
}
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_restrictedLabel) {
|
||||
_restrictedLabel.destroy();
|
||||
if (_switchPmButton) {
|
||||
_switchPmButton->show();
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
bool Inner::isRestrictedView() {
|
||||
checkRestrictedPeer();
|
||||
return (_restrictedLabel != nullptr);
|
||||
}
|
||||
|
||||
int Inner::countHeight() {
|
||||
if (isRestrictedView()) {
|
||||
return st::stickerPanPadding + _restrictedLabel->height() + st::stickerPanPadding;
|
||||
} else if (_rows.isEmpty() && !_switchPmButton) {
|
||||
return st::stickerPanPadding + st::normalFont->height + st::stickerPanPadding;
|
||||
}
|
||||
auto result = st::stickerPanPadding;
|
||||
if (_switchPmButton) {
|
||||
result += _switchPmButton->height() + st::inlineResultsSkip;
|
||||
}
|
||||
for (int i = 0, l = _rows.count(); i < l; ++i) {
|
||||
result += _rows[i].height;
|
||||
}
|
||||
return result + st::stickerPanPadding;
|
||||
}
|
||||
|
||||
QString Inner::tooltipText() const {
|
||||
if (const auto lnk = ClickHandler::getActive()) {
|
||||
return lnk->tooltip();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QPoint Inner::tooltipPos() const {
|
||||
return _lastMousePos;
|
||||
}
|
||||
|
||||
bool Inner::tooltipWindowActive() const {
|
||||
return Ui::AppInFocus() && Ui::InFocusChain(window());
|
||||
}
|
||||
|
||||
rpl::producer<> Inner::inlineRowsCleared() const {
|
||||
return _inlineRowsCleared.events();
|
||||
}
|
||||
|
||||
Inner::~Inner() = default;
|
||||
|
||||
void Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
QRect r = e ? e->rect() : rect();
|
||||
if (r != rect()) {
|
||||
p.setClipRect(r);
|
||||
}
|
||||
p.fillRect(r, st::emojiPanBg);
|
||||
|
||||
paintInlineItems(p, r);
|
||||
}
|
||||
|
||||
void Inner::paintInlineItems(Painter &p, const QRect &r) {
|
||||
if (_restrictedLabel) {
|
||||
return;
|
||||
}
|
||||
if (_rows.isEmpty() && !_switchPmButton) {
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st::noContactsColor);
|
||||
p.drawText(QRect(0, 0, width(), (height() / 3) * 2 + st::normalFont->height), tr::lng_inline_bot_no_results(tr::now), style::al_center);
|
||||
return;
|
||||
}
|
||||
auto gifPaused = _controller->isGifPausedAtLeastFor(Window::GifPauseReason::InlineResults);
|
||||
InlineBots::Layout::PaintContext context(crl::now(), false, gifPaused, false);
|
||||
|
||||
auto top = st::stickerPanPadding;
|
||||
if (_switchPmButton) {
|
||||
top += _switchPmButton->height() + st::inlineResultsSkip;
|
||||
}
|
||||
|
||||
auto fromx = rtl() ? (width() - r.x() - r.width()) : r.x();
|
||||
auto tox = rtl() ? (width() - r.x()) : (r.x() + r.width());
|
||||
for (auto row = 0, rows = _rows.size(); row != rows; ++row) {
|
||||
auto &inlineRow = _rows[row];
|
||||
if (top >= r.top() + r.height()) break;
|
||||
if (top + inlineRow.height > r.top()) {
|
||||
auto left = st::inlineResultsLeft - st::buttonRadius;
|
||||
if (row == rows - 1) context.lastRow = true;
|
||||
for (int col = 0, cols = inlineRow.items.size(); col < cols; ++col) {
|
||||
if (left >= tox) break;
|
||||
|
||||
auto item = inlineRow.items.at(col);
|
||||
auto w = item->width();
|
||||
if (left + w > fromx) {
|
||||
p.translate(left, top);
|
||||
item->paint(p, r.translated(-left, -top), &context);
|
||||
p.translate(-left, -top);
|
||||
}
|
||||
left += w;
|
||||
if (item->hasRightSkip()) {
|
||||
left += st::inlineResultsSkip;
|
||||
}
|
||||
}
|
||||
}
|
||||
top += inlineRow.height;
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::mousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
_lastMousePos = e->globalPos();
|
||||
updateSelected();
|
||||
|
||||
_pressed = _selected;
|
||||
ClickHandler::pressed();
|
||||
_previewTimer.callOnce(QApplication::startDragTime());
|
||||
}
|
||||
|
||||
void Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
_previewTimer.cancel();
|
||||
|
||||
auto pressed = std::exchange(_pressed, -1);
|
||||
auto activated = ClickHandler::unpressed();
|
||||
|
||||
if (_previewShown) {
|
||||
_previewShown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_lastMousePos = e->globalPos();
|
||||
updateSelected();
|
||||
|
||||
if (_selected < 0 || _selected != pressed || !activated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dynamic_cast<InlineBots::Layout::SendClickHandler*>(activated.get())) {
|
||||
int row = _selected / MatrixRowShift, column = _selected % MatrixRowShift;
|
||||
selectInlineResult(row, column);
|
||||
} else {
|
||||
ActivateClickHandler(window(), activated, e->button());
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::selectInlineResult(int row, int column) {
|
||||
selectInlineResult(row, column, Api::SendOptions());
|
||||
}
|
||||
|
||||
void Inner::selectInlineResult(
|
||||
int row,
|
||||
int column,
|
||||
Api::SendOptions options) {
|
||||
if (row >= _rows.size() || column >= _rows.at(row).items.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto item = _rows[row].items[column];
|
||||
if (const auto inlineResult = item->getResult()) {
|
||||
if (inlineResult->onChoose(item)) {
|
||||
_resultSelectedCallback(
|
||||
inlineResult,
|
||||
_inlineBot,
|
||||
std::move(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::mouseMoveEvent(QMouseEvent *e) {
|
||||
_lastMousePos = e->globalPos();
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
void Inner::leaveEventHook(QEvent *e) {
|
||||
clearSelection();
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
|
||||
void Inner::leaveToChildEvent(QEvent *e, QWidget *child) {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
void Inner::enterFromChildEvent(QEvent *e, QWidget *child) {
|
||||
_lastMousePos = QCursor::pos();
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
void Inner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (_selected < 0 || _pressed >= 0) {
|
||||
return;
|
||||
}
|
||||
const auto row = _selected / MatrixRowShift;
|
||||
const auto column = _selected % MatrixRowShift;
|
||||
const auto type = SendMenu::Type::Scheduled;
|
||||
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(this);
|
||||
|
||||
const auto send = [=](Api::SendOptions options) {
|
||||
selectInlineResult(row, column, options);
|
||||
};
|
||||
SendMenu::FillSendMenu(
|
||||
_menu,
|
||||
[&] { return type; },
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send));
|
||||
|
||||
if (!_menu->actions().empty()) {
|
||||
_menu->popup(QCursor::pos());
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::clearSelection() {
|
||||
if (_selected >= 0) {
|
||||
int srow = _selected / MatrixRowShift, scol = _selected % MatrixRowShift;
|
||||
Assert(srow >= 0 && srow < _rows.size() && scol >= 0 && scol < _rows.at(srow).items.size());
|
||||
ClickHandler::clearActive(_rows.at(srow).items.at(scol));
|
||||
setCursor(style::cur_default);
|
||||
}
|
||||
_selected = _pressed = -1;
|
||||
update();
|
||||
}
|
||||
|
||||
void Inner::hideFinished() {
|
||||
clearHeavyData();
|
||||
}
|
||||
|
||||
void Inner::clearHeavyData() {
|
||||
clearInlineRows(false);
|
||||
for (const auto &[result, layout] : _inlineLayouts) {
|
||||
layout->unloadHeavyPart();
|
||||
}
|
||||
}
|
||||
|
||||
bool Inner::inlineRowsAddItem(Result *result, Row &row, int32 &sumWidth) {
|
||||
auto layout = layoutPrepareInlineResult(result, (_rows.size() * MatrixRowShift) + row.items.size());
|
||||
if (!layout) return false;
|
||||
|
||||
layout->preload();
|
||||
if (inlineRowFinalize(row, sumWidth, layout->isFullLine())) {
|
||||
layout->setPosition(_rows.size() * MatrixRowShift);
|
||||
}
|
||||
|
||||
sumWidth += layout->maxWidth();
|
||||
if (!row.items.isEmpty() && row.items.back()->hasRightSkip()) {
|
||||
sumWidth += st::inlineResultsSkip;
|
||||
}
|
||||
|
||||
row.items.push_back(layout);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inner::inlineRowFinalize(Row &row, int32 &sumWidth, bool force) {
|
||||
if (row.items.isEmpty()) return false;
|
||||
|
||||
auto full = (row.items.size() >= kInlineItemsMaxPerRow);
|
||||
auto big = (sumWidth >= st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft);
|
||||
if (full || big || force) {
|
||||
_rows.push_back(layoutInlineRow(row, (full || big) ? sumWidth : 0));
|
||||
row = Row();
|
||||
row.items.reserve(kInlineItemsMaxPerRow);
|
||||
sumWidth = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Inner::inlineBotChanged() {
|
||||
refreshInlineRows(nullptr, nullptr, nullptr, true);
|
||||
}
|
||||
|
||||
void Inner::clearInlineRows(bool resultsDeleted) {
|
||||
if (resultsDeleted) {
|
||||
_selected = _pressed = -1;
|
||||
} else {
|
||||
clearSelection();
|
||||
for_const (auto &row, _rows) {
|
||||
for_const (auto &item, row.items) {
|
||||
item->setPosition(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_rows.clear();
|
||||
}
|
||||
|
||||
ItemBase *Inner::layoutPrepareInlineResult(Result *result, int32 position) {
|
||||
auto it = _inlineLayouts.find(result);
|
||||
if (it == _inlineLayouts.cend()) {
|
||||
if (auto layout = ItemBase::createLayout(this, result, _inlineWithThumb)) {
|
||||
it = _inlineLayouts.emplace(result, std::move(layout)).first;
|
||||
it->second->initDimensions();
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (!it->second->maxWidth()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
it->second->setPosition(position);
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
void Inner::deleteUnusedInlineLayouts() {
|
||||
if (_rows.isEmpty()) { // delete all
|
||||
_inlineLayouts.clear();
|
||||
} else {
|
||||
for (auto i = _inlineLayouts.begin(); i != _inlineLayouts.cend();) {
|
||||
if (i->second->position() < 0) {
|
||||
i = _inlineLayouts.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inner::Row &Inner::layoutInlineRow(Row &row, int32 sumWidth) {
|
||||
auto count = int(row.items.size());
|
||||
Assert(count <= kInlineItemsMaxPerRow);
|
||||
|
||||
// enumerate items in the order of growing maxWidth()
|
||||
// for that sort item indices by maxWidth()
|
||||
int indices[kInlineItemsMaxPerRow];
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
indices[i] = i;
|
||||
}
|
||||
std::sort(indices, indices + count, [&row](int a, int b) -> bool {
|
||||
return row.items.at(a)->maxWidth() < row.items.at(b)->maxWidth();
|
||||
});
|
||||
|
||||
row.height = 0;
|
||||
int availw = width() - (st::inlineResultsLeft - st::buttonRadius);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int index = indices[i];
|
||||
int w = sumWidth ? (row.items.at(index)->maxWidth() * availw / sumWidth) : row.items.at(index)->maxWidth();
|
||||
int actualw = qMax(w, int(st::inlineResultsMinWidth));
|
||||
row.height = qMax(row.height, row.items.at(index)->resizeGetHeight(actualw));
|
||||
if (sumWidth) {
|
||||
availw -= actualw;
|
||||
sumWidth -= row.items.at(index)->maxWidth();
|
||||
if (index > 0 && row.items.at(index - 1)->hasRightSkip()) {
|
||||
availw -= st::inlineResultsSkip;
|
||||
sumWidth -= st::inlineResultsSkip;
|
||||
}
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
void Inner::preloadImages() {
|
||||
for (auto row = 0, rows = _rows.size(); row != rows; ++row) {
|
||||
for (auto col = 0, cols = _rows[row].items.size(); col != cols; ++col) {
|
||||
_rows[row].items[col]->preload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::hideInlineRowsPanel() {
|
||||
clearInlineRows(false);
|
||||
}
|
||||
|
||||
void Inner::clearInlineRowsPanel() {
|
||||
clearInlineRows(false);
|
||||
}
|
||||
|
||||
void Inner::refreshSwitchPmButton(const CacheEntry *entry) {
|
||||
if (!entry || entry->switchPmText.isEmpty()) {
|
||||
_switchPmButton.destroy();
|
||||
_switchPmStartToken.clear();
|
||||
} else {
|
||||
if (!_switchPmButton) {
|
||||
_switchPmButton.create(this, nullptr, st::switchPmButton);
|
||||
_switchPmButton->show();
|
||||
_switchPmButton->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
_switchPmButton->addClickHandler([=] { onSwitchPm(); });
|
||||
}
|
||||
_switchPmButton->setText(rpl::single(entry->switchPmText));
|
||||
_switchPmStartToken = entry->switchPmStartToken;
|
||||
const auto buttonTop = st::stickerPanPadding;
|
||||
_switchPmButton->move(st::inlineResultsLeft - st::buttonRadius, buttonTop);
|
||||
if (isRestrictedView()) {
|
||||
_switchPmButton->hide();
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
int Inner::refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *entry, bool resultsDeleted) {
|
||||
_inlineBot = bot;
|
||||
_inlineQueryPeer = queryPeer;
|
||||
refreshSwitchPmButton(entry);
|
||||
auto clearResults = [&] {
|
||||
if (!entry) {
|
||||
return true;
|
||||
}
|
||||
if (entry->results.empty() && entry->switchPmText.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
auto clearResultsResult = clearResults(); // Clang workaround.
|
||||
if (clearResultsResult) {
|
||||
if (resultsDeleted) {
|
||||
clearInlineRows(true);
|
||||
deleteUnusedInlineLayouts();
|
||||
}
|
||||
_inlineRowsCleared.fire({});
|
||||
return 0;
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
|
||||
Assert(_inlineBot != 0);
|
||||
|
||||
auto count = int(entry->results.size());
|
||||
auto from = validateExistingInlineRows(entry->results);
|
||||
auto added = 0;
|
||||
|
||||
if (count) {
|
||||
_rows.reserve(count);
|
||||
auto row = Row();
|
||||
row.items.reserve(kInlineItemsMaxPerRow);
|
||||
auto sumWidth = 0;
|
||||
for (auto i = from; i != count; ++i) {
|
||||
if (inlineRowsAddItem(entry->results[i].get(), row, sumWidth)) {
|
||||
++added;
|
||||
}
|
||||
}
|
||||
inlineRowFinalize(row, sumWidth, true);
|
||||
}
|
||||
|
||||
auto h = countHeight();
|
||||
if (h != height()) resize(width(), h);
|
||||
update();
|
||||
|
||||
_lastMousePos = QCursor::pos();
|
||||
updateSelected();
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
int Inner::validateExistingInlineRows(const Results &results) {
|
||||
int count = results.size(), until = 0, untilrow = 0, untilcol = 0;
|
||||
for (; until < count;) {
|
||||
if (untilrow >= _rows.size() || _rows[untilrow].items[untilcol]->getResult() != results[until].get()) {
|
||||
break;
|
||||
}
|
||||
++until;
|
||||
if (++untilcol == _rows[untilrow].items.size()) {
|
||||
++untilrow;
|
||||
untilcol = 0;
|
||||
}
|
||||
}
|
||||
if (until == count) { // all items are layed out
|
||||
if (untilrow == _rows.size()) { // nothing changed
|
||||
return until;
|
||||
}
|
||||
|
||||
for (int i = untilrow, l = _rows.size(), skip = untilcol; i < l; ++i) {
|
||||
for (int j = 0, s = _rows[i].items.size(); j < s; ++j) {
|
||||
if (skip) {
|
||||
--skip;
|
||||
} else {
|
||||
_rows[i].items[j]->setPosition(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!untilcol) { // all good rows are filled
|
||||
_rows.resize(untilrow);
|
||||
return until;
|
||||
}
|
||||
_rows.resize(untilrow + 1);
|
||||
_rows[untilrow].items.resize(untilcol);
|
||||
_rows[untilrow] = layoutInlineRow(_rows[untilrow]);
|
||||
return until;
|
||||
}
|
||||
if (untilrow && !untilcol) { // remove last row, maybe it is not full
|
||||
--untilrow;
|
||||
untilcol = _rows[untilrow].items.size();
|
||||
}
|
||||
until -= untilcol;
|
||||
|
||||
for (int i = untilrow, l = _rows.size(); i < l; ++i) {
|
||||
for (int j = 0, s = _rows[i].items.size(); j < s; ++j) {
|
||||
_rows[i].items[j]->setPosition(-1);
|
||||
}
|
||||
}
|
||||
_rows.resize(untilrow);
|
||||
|
||||
if (_rows.isEmpty()) {
|
||||
_inlineWithThumb = false;
|
||||
for (int i = until; i < count; ++i) {
|
||||
if (results.at(i)->hasThumbDisplay()) {
|
||||
_inlineWithThumb = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return until;
|
||||
}
|
||||
|
||||
void Inner::inlineItemLayoutChanged(const ItemBase *layout) {
|
||||
if (_selected < 0 || !isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int row = _selected / MatrixRowShift, col = _selected % MatrixRowShift;
|
||||
if (row < _rows.size() && col < _rows.at(row).items.size()) {
|
||||
if (layout == _rows.at(row).items.at(col)) {
|
||||
updateSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::inlineItemRepaint(const ItemBase *layout) {
|
||||
auto ms = crl::now();
|
||||
if (_lastScrolled + 100 <= ms) {
|
||||
update();
|
||||
} else {
|
||||
_updateInlineItems.callOnce(_lastScrolled + 100 - ms);
|
||||
}
|
||||
}
|
||||
|
||||
bool Inner::inlineItemVisible(const ItemBase *layout) {
|
||||
int32 position = layout->position();
|
||||
if (position < 0 || !isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int row = position / MatrixRowShift, col = position % MatrixRowShift;
|
||||
Assert((row < _rows.size()) && (col < _rows[row].items.size()));
|
||||
|
||||
auto &inlineItems = _rows[row].items;
|
||||
int top = st::stickerPanPadding;
|
||||
for (int32 i = 0; i < row; ++i) {
|
||||
top += _rows.at(i).height;
|
||||
}
|
||||
|
||||
return (top < _visibleBottom) && (top + _rows[row].items[col]->height() > _visibleTop);
|
||||
}
|
||||
|
||||
Data::FileOrigin Inner::inlineItemFileOrigin() {
|
||||
return Data::FileOrigin();
|
||||
}
|
||||
|
||||
void Inner::updateSelected() {
|
||||
if (_pressed >= 0 && !_previewShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto newSelected = -1;
|
||||
auto p = mapFromGlobal(_lastMousePos);
|
||||
|
||||
int sx = (rtl() ? width() - p.x() : p.x()) - (st::inlineResultsLeft - st::buttonRadius);
|
||||
int sy = p.y() - st::stickerPanPadding;
|
||||
if (_switchPmButton) {
|
||||
sy -= _switchPmButton->height() + st::inlineResultsSkip;
|
||||
}
|
||||
int row = -1, col = -1, sel = -1;
|
||||
ClickHandlerPtr lnk;
|
||||
ClickHandlerHost *lnkhost = nullptr;
|
||||
HistoryView::CursorState cursor = HistoryView::CursorState::None;
|
||||
if (sy >= 0) {
|
||||
row = 0;
|
||||
for (int rows = _rows.size(); row < rows; ++row) {
|
||||
if (sy < _rows[row].height) {
|
||||
break;
|
||||
}
|
||||
sy -= _rows[row].height;
|
||||
}
|
||||
}
|
||||
if (sx >= 0 && row >= 0 && row < _rows.size()) {
|
||||
auto &inlineItems = _rows[row].items;
|
||||
col = 0;
|
||||
for (int cols = inlineItems.size(); col < cols; ++col) {
|
||||
int width = inlineItems.at(col)->width();
|
||||
if (sx < width) {
|
||||
break;
|
||||
}
|
||||
sx -= width;
|
||||
if (inlineItems.at(col)->hasRightSkip()) {
|
||||
sx -= st::inlineResultsSkip;
|
||||
}
|
||||
}
|
||||
if (col < inlineItems.size()) {
|
||||
sel = row * MatrixRowShift + col;
|
||||
auto result = inlineItems[col]->getState(
|
||||
QPoint(sx, sy),
|
||||
HistoryView::StateRequest());
|
||||
lnk = result.link;
|
||||
cursor = result.cursor;
|
||||
lnkhost = inlineItems[col];
|
||||
} else {
|
||||
row = col = -1;
|
||||
}
|
||||
} else {
|
||||
row = col = -1;
|
||||
}
|
||||
int srow = (_selected >= 0) ? (_selected / MatrixRowShift) : -1;
|
||||
int scol = (_selected >= 0) ? (_selected % MatrixRowShift) : -1;
|
||||
if (_selected != sel) {
|
||||
if (srow >= 0 && scol >= 0) {
|
||||
Assert(srow >= 0 && srow < _rows.size() && scol >= 0 && scol < _rows.at(srow).items.size());
|
||||
_rows[srow].items[scol]->update();
|
||||
}
|
||||
_selected = sel;
|
||||
if (row >= 0 && col >= 0) {
|
||||
Assert(row >= 0 && row < _rows.size() && col >= 0 && col < _rows.at(row).items.size());
|
||||
_rows[row].items[col]->update();
|
||||
}
|
||||
if (_previewShown && _selected >= 0 && _pressed != _selected) {
|
||||
_pressed = _selected;
|
||||
if (row >= 0 && col >= 0) {
|
||||
auto layout = _rows.at(row).items.at(col);
|
||||
if (const auto w = App::wnd()) {
|
||||
if (const auto previewDocument = layout->getPreviewDocument()) {
|
||||
w->showMediaPreview(
|
||||
Data::FileOrigin(),
|
||||
previewDocument);
|
||||
} else if (auto previewPhoto = layout->getPreviewPhoto()) {
|
||||
w->showMediaPreview(Data::FileOrigin(), previewPhoto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ClickHandler::setActive(lnk, lnkhost)) {
|
||||
setCursor(lnk ? style::cur_pointer : style::cur_default);
|
||||
Ui::Tooltip::Hide();
|
||||
}
|
||||
if (lnk) {
|
||||
Ui::Tooltip::Show(1000, this);
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::showPreview() {
|
||||
if (_pressed < 0) return;
|
||||
|
||||
int row = _pressed / MatrixRowShift, col = _pressed % MatrixRowShift;
|
||||
if (row < _rows.size() && col < _rows.at(row).items.size()) {
|
||||
auto layout = _rows.at(row).items.at(col);
|
||||
if (const auto w = App::wnd()) {
|
||||
if (const auto previewDocument = layout->getPreviewDocument()) {
|
||||
_previewShown = w->showMediaPreview(Data::FileOrigin(), previewDocument);
|
||||
} else if (const auto previewPhoto = layout->getPreviewPhoto()) {
|
||||
_previewShown = w->showMediaPreview(Data::FileOrigin(), previewPhoto);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::updateInlineItems() {
|
||||
auto ms = crl::now();
|
||||
if (_lastScrolled + 100 <= ms) {
|
||||
update();
|
||||
} else {
|
||||
_updateInlineItems.callOnce(_lastScrolled + 100 - ms);
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::onSwitchPm() {
|
||||
if (_inlineBot && _inlineBot->isBot()) {
|
||||
_inlineBot->botInfo->startToken = _switchPmStartToken;
|
||||
Ui::showPeerHistory(_inlineBot, ShowAndStartBotMsgId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
Widget::Widget(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
@@ -801,7 +44,7 @@ Widget::Widget(
|
||||
_scroll->resize(st::emojiPanWidth - st::buttonRadius, _contentHeight);
|
||||
|
||||
_scroll->move(verticalRect().topLeft());
|
||||
_inner = _scroll->setOwnedWidget(object_ptr<internal::Inner>(this, controller));
|
||||
_inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, controller));
|
||||
|
||||
_inner->moveToLeft(0, 0, _scroll->width());
|
||||
|
||||
@@ -990,6 +233,14 @@ QImage Widget::grabForPanelAnimation() {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Widget::setResultSelectedCallback(Fn<void(ResultSelected)> callback) {
|
||||
_inner->setResultSelectedCallback(std::move(callback));
|
||||
}
|
||||
|
||||
void Widget::setCurrentDialogsEntryState(Dialogs::EntryState state) {
|
||||
_inner->setCurrentDialogsEntryState(state);
|
||||
}
|
||||
|
||||
void Widget::hideAnimated() {
|
||||
if (isHidden()) return;
|
||||
if (_hiding) return;
|
||||
@@ -1103,7 +354,7 @@ void Widget::inlineResultsDone(const MTPmessages_BotResults &result) {
|
||||
if (it == _inlineCache.cend()) {
|
||||
it = _inlineCache.emplace(
|
||||
_inlineQuery,
|
||||
std::make_unique<internal::CacheEntry>()).first;
|
||||
std::make_unique<CacheEntry>()).first;
|
||||
}
|
||||
auto entry = it->second.get();
|
||||
entry->nextOffset = qs(d.vnext_offset().value_or_empty());
|
||||
@@ -1163,7 +414,7 @@ void Widget::queryInlineBot(UserData *bot, PeerData *peer, QString query) {
|
||||
showInlineRows(true);
|
||||
} else {
|
||||
_inlineNextQuery = query;
|
||||
_inlineRequestTimer.callOnce(internal::kInlineBotRequestDelay);
|
||||
_inlineRequestTimer.callOnce(kInlineBotRequestDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1199,7 +450,7 @@ void Widget::onInlineRequest() {
|
||||
|
||||
bool Widget::refreshInlineRows(int *added) {
|
||||
auto it = _inlineCache.find(_inlineQuery);
|
||||
const internal::CacheEntry *entry = nullptr;
|
||||
const CacheEntry *entry = nullptr;
|
||||
if (it != _inlineCache.cend()) {
|
||||
if (!it->second->results.empty() || !it->second->switchPmText.isEmpty()) {
|
||||
entry = it->second.get();
|
||||
|
||||
@@ -30,161 +30,29 @@ class RippleAnimation;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
struct EntryState;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace InlineBots {
|
||||
|
||||
class Result;
|
||||
struct ResultSelected;
|
||||
} // namespace InlineBots
|
||||
|
||||
namespace InlineBots {
|
||||
namespace Layout {
|
||||
|
||||
class ItemBase;
|
||||
|
||||
namespace internal {
|
||||
|
||||
constexpr int kInlineItemsMaxPerRow = 5;
|
||||
|
||||
using Results = std::vector<std::unique_ptr<Result>>;
|
||||
using ResultSelected = Fn<void(Result *, UserData *, Api::SendOptions)>;
|
||||
|
||||
struct CacheEntry {
|
||||
QString nextOffset;
|
||||
QString switchPmText, switchPmStartToken;
|
||||
Results results;
|
||||
};
|
||||
|
||||
class Inner
|
||||
: public Ui::RpWidget
|
||||
, public Ui::AbstractTooltipShower
|
||||
, public Context
|
||||
, private base::Subscriber {
|
||||
|
||||
public:
|
||||
Inner(QWidget *parent, not_null<Window::SessionController*> controller);
|
||||
|
||||
void hideFinished();
|
||||
|
||||
void clearSelection();
|
||||
|
||||
int refreshInlineRows(PeerData *queryPeer, UserData *bot, const CacheEntry *results, bool resultsDeleted);
|
||||
void inlineBotChanged();
|
||||
void hideInlineRowsPanel();
|
||||
void clearInlineRowsPanel();
|
||||
|
||||
void preloadImages();
|
||||
|
||||
void inlineItemLayoutChanged(const ItemBase *layout) override;
|
||||
void inlineItemRepaint(const ItemBase *layout) override;
|
||||
bool inlineItemVisible(const ItemBase *layout) override;
|
||||
Data::FileOrigin inlineItemFileOrigin() override;
|
||||
|
||||
int countHeight();
|
||||
|
||||
void setResultSelectedCallback(ResultSelected callback) {
|
||||
_resultSelectedCallback = std::move(callback);
|
||||
}
|
||||
|
||||
// Ui::AbstractTooltipShower interface.
|
||||
QString tooltipText() const override;
|
||||
QPoint tooltipPos() const override;
|
||||
bool tooltipWindowActive() const override;
|
||||
|
||||
rpl::producer<> inlineRowsCleared() const;
|
||||
|
||||
~Inner();
|
||||
|
||||
protected:
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void leaveToChildEvent(QEvent *e, QWidget *child) override;
|
||||
void enterFromChildEvent(QEvent *e, QWidget *child) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
private:
|
||||
static constexpr bool kRefreshIconsScrollAnimation = true;
|
||||
static constexpr bool kRefreshIconsNoAnimation = false;
|
||||
|
||||
struct Row {
|
||||
int height = 0;
|
||||
QVector<ItemBase*> items;
|
||||
};
|
||||
|
||||
void onSwitchPm();
|
||||
|
||||
void updateSelected();
|
||||
void checkRestrictedPeer();
|
||||
bool isRestrictedView();
|
||||
void clearHeavyData();
|
||||
|
||||
void paintInlineItems(Painter &p, const QRect &r);
|
||||
|
||||
void refreshSwitchPmButton(const CacheEntry *entry);
|
||||
|
||||
void showPreview();
|
||||
void updateInlineItems();
|
||||
void clearInlineRows(bool resultsDeleted);
|
||||
ItemBase *layoutPrepareInlineResult(Result *result, int32 position);
|
||||
|
||||
bool inlineRowsAddItem(Result *result, Row &row, int32 &sumWidth);
|
||||
bool inlineRowFinalize(Row &row, int32 &sumWidth, bool force = false);
|
||||
|
||||
Row &layoutInlineRow(Row &row, int32 sumWidth = 0);
|
||||
void deleteUnusedInlineLayouts();
|
||||
|
||||
int validateExistingInlineRows(const Results &results);
|
||||
void selectInlineResult(int row, int column);
|
||||
void selectInlineResult(int row, int column, Api::SendOptions options);
|
||||
|
||||
not_null<Window::SessionController*> _controller;
|
||||
|
||||
int _visibleTop = 0;
|
||||
int _visibleBottom = 0;
|
||||
|
||||
UserData *_inlineBot = nullptr;
|
||||
PeerData *_inlineQueryPeer = nullptr;
|
||||
crl::time _lastScrolled = 0;
|
||||
base::Timer _updateInlineItems;
|
||||
bool _inlineWithThumb = false;
|
||||
|
||||
object_ptr<Ui::RoundButton> _switchPmButton = { nullptr };
|
||||
QString _switchPmStartToken;
|
||||
|
||||
object_ptr<Ui::FlatLabel> _restrictedLabel = { nullptr };
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
QVector<Row> _rows;
|
||||
|
||||
std::map<Result*, std::unique_ptr<ItemBase>> _inlineLayouts;
|
||||
|
||||
rpl::event_stream<> _inlineRowsCleared;
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
QPoint _lastMousePos;
|
||||
|
||||
base::Timer _previewTimer;
|
||||
bool _previewShown = false;
|
||||
|
||||
ResultSelected _resultSelectedCallback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
struct CacheEntry;
|
||||
class Inner;
|
||||
|
||||
class Widget : public Ui::RpWidget {
|
||||
|
||||
public:
|
||||
Widget(QWidget *parent, not_null<Window::SessionController*> controller);
|
||||
~Widget();
|
||||
|
||||
void moveBottom(int bottom);
|
||||
|
||||
@@ -201,16 +69,13 @@ public:
|
||||
void showAnimated();
|
||||
void hideAnimated();
|
||||
|
||||
void setResultSelectedCallback(internal::ResultSelected callback) {
|
||||
_inner->setResultSelectedCallback(std::move(callback));
|
||||
}
|
||||
void setResultSelectedCallback(Fn<void(ResultSelected)> callback);
|
||||
void setCurrentDialogsEntryState(Dialogs::EntryState state);
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> requesting() const {
|
||||
return _requesting.events();
|
||||
}
|
||||
|
||||
~Widget();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
@@ -273,9 +138,9 @@ private:
|
||||
bool _inPanelGrab = false;
|
||||
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
QPointer<internal::Inner> _inner;
|
||||
QPointer<Inner> _inner;
|
||||
|
||||
std::map<QString, std::unique_ptr<internal::CacheEntry>> _inlineCache;
|
||||
std::map<QString, std::unique_ptr<CacheEntry>> _inlineCache;
|
||||
base::Timer _inlineRequestTimer;
|
||||
|
||||
UserData *_inlineBot = nullptr;
|
||||
|
||||
@@ -244,7 +244,11 @@ MainWidget::MainWidget(
|
||||
setupConnectingWidget();
|
||||
|
||||
connect(_dialogs, SIGNAL(cancelled()), this, SLOT(dialogsCancelled()));
|
||||
connect(_history, &HistoryWidget::cancelled, [=] { handleHistoryBack(); });
|
||||
|
||||
_history->cancelRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
handleHistoryBack();
|
||||
}, lifetime());
|
||||
|
||||
Core::App().calls().currentCallValue(
|
||||
) | rpl::start_with_next([=](Calls::Call *call) {
|
||||
@@ -479,7 +483,7 @@ void MainWidget::floatPlayerClosed(FullMsgId itemId) {
|
||||
const auto voiceData = Media::Player::instance()->current(
|
||||
AudioMsgId::Type::Voice);
|
||||
if (voiceData.contextId() == itemId) {
|
||||
_player->entity()->stopAndClose();
|
||||
stopAndClosePlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,7 +533,7 @@ bool MainWidget::shareUrl(
|
||||
auto history = peer->owner().history(peer);
|
||||
history->setLocalDraft(
|
||||
std::make_unique<Data::Draft>(textWithTags, 0, cursor, false));
|
||||
history->clearEditDraft();
|
||||
history->clearLocalEditDraft();
|
||||
if (_history->peer() == peer) {
|
||||
_history->applyDraft();
|
||||
} else {
|
||||
@@ -558,7 +562,7 @@ bool MainWidget::inlineSwitchChosen(PeerId peerId, const QString &botAndQuery) {
|
||||
TextWithTags textWithTags = { botAndQuery, TextWithTags::Tags() };
|
||||
MessageCursor cursor = { botAndQuery.size(), botAndQuery.size(), QFIXED_MAX };
|
||||
h->setLocalDraft(std::make_unique<Data::Draft>(textWithTags, 0, cursor, false));
|
||||
h->clearEditDraft();
|
||||
h->clearLocalEditDraft();
|
||||
const auto opened = _history->peer() && (_history->peer() == peer);
|
||||
if (opened) {
|
||||
_history->applyDraft();
|
||||
@@ -819,15 +823,6 @@ crl::time MainWidget::highlightStartTime(not_null<const HistoryItem*> item) cons
|
||||
return _history->highlightStartTime(item);
|
||||
}
|
||||
|
||||
MsgId MainWidget::currentReplyToIdFor(not_null<History*> history) const {
|
||||
if (_history->history() == history) {
|
||||
return _history->replyToId();
|
||||
} else if (const auto localDraft = history->localDraft()) {
|
||||
return localDraft->msgId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MainWidget::sendBotCommand(
|
||||
not_null<PeerData*> peer,
|
||||
UserData *bot,
|
||||
@@ -887,6 +882,12 @@ void MainWidget::closeBothPlayers() {
|
||||
Shortcuts::ToggleMediaShortcuts(false);
|
||||
}
|
||||
|
||||
void MainWidget::stopAndClosePlayer() {
|
||||
if (_player) {
|
||||
_player->entity()->stopAndClose();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::createPlayer() {
|
||||
if (!_player) {
|
||||
_player.create(
|
||||
|
||||
@@ -175,8 +175,6 @@ public:
|
||||
// While HistoryInner is not HistoryView::ListWidget.
|
||||
crl::time highlightStartTime(not_null<const HistoryItem*> item) const;
|
||||
|
||||
MsgId currentReplyToIdFor(not_null<History*> history) const;
|
||||
|
||||
void sendBotCommand(
|
||||
not_null<PeerData*> peer,
|
||||
UserData *bot,
|
||||
@@ -222,6 +220,7 @@ public:
|
||||
using FloatDelegate::floatPlayerAreaUpdated;
|
||||
|
||||
void closeBothPlayers();
|
||||
void stopAndClosePlayer();
|
||||
|
||||
public slots:
|
||||
void inlineResultLoadProgress(FileLoader *loader);
|
||||
|
||||