Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc48afac1c | ||
|
|
0a0dcb9054 | ||
|
|
b1b01385d0 | ||
|
|
a2e4403b28 | ||
|
|
e1017380ec | ||
|
|
a6e4ac679c | ||
|
|
f75fb33c29 | ||
|
|
cbaca6382e | ||
|
|
1758f0fd8f | ||
|
|
e29ee439cf | ||
|
|
51f960442e | ||
|
|
fff2ee2758 | ||
|
|
b1ed15447b | ||
|
|
a086afb152 | ||
|
|
639e6d8e28 | ||
|
|
00504b61cd | ||
|
|
1affb8172f | ||
|
|
c98a3825a5 | ||
|
|
c3b0e6c503 | ||
|
|
76a7cc9229 | ||
|
|
b2047c9558 | ||
|
|
4a73bb7872 | ||
|
|
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 |
511
.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,9 @@ jobs:
|
||||
fi
|
||||
|
||||
./configure.sh \
|
||||
-D CMAKE_CXX_FLAGS="-s" \
|
||||
-D CMAKE_C_FLAGS="-Werror" \
|
||||
-D CMAKE_CXX_FLAGS="-Werror" \
|
||||
-D CMAKE_EXE_LINKER_FLAGS="-s" \
|
||||
-D TDESKTOP_API_TEST=ON \
|
||||
-D DESKTOP_APP_USE_PACKAGED=OFF \
|
||||
-D DESKTOP_APP_DISABLE_CRASH_REPORTS=OFF \
|
||||
@@ -587,7 +115,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 circle 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.9.0" />
|
||||
Version="2.4.11.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,9,0
|
||||
PRODUCTVERSION 2,4,9,0
|
||||
FILEVERSION 2,4,11,0
|
||||
PRODUCTVERSION 2,4,11,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.9.0"
|
||||
VALUE "FileVersion", "2.4.11.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.9.0"
|
||||
VALUE "ProductVersion", "2.4.11.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 2,4,9,0
|
||||
PRODUCTVERSION 2,4,9,0
|
||||
FILEVERSION 2,4,11,0
|
||||
PRODUCTVERSION 2,4,11,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.9.0"
|
||||
VALUE "FileVersion", "2.4.11.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2020"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "2.4.9.0"
|
||||
VALUE "ProductVersion", "2.4.11.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -86,8 +86,7 @@ void SendExistingMedia(
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to_msg_id;
|
||||
}
|
||||
const auto anonymousPost = peer->amAnonymous();
|
||||
const auto silentPost = message.action.options.silent
|
||||
|| (peer->isBroadcast() && session->data().notifySilentPosts(peer));
|
||||
const auto silentPost = ShouldSendSilent(peer, message.action.options);
|
||||
InnerFillMessagePostFlags(message.action.options, peer, flags);
|
||||
if (silentPost) {
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
|
||||
@@ -261,8 +260,7 @@ bool SendDice(Api::MessageToSend &message) {
|
||||
}
|
||||
const auto replyHeader = NewMessageReplyHeader(message.action);
|
||||
const auto anonymousPost = peer->amAnonymous();
|
||||
const auto silentPost = message.action.options.silent
|
||||
|| (peer->isBroadcast() && session->data().notifySilentPosts(peer));
|
||||
const auto silentPost = ShouldSendSilent(peer, message.action.options);
|
||||
InnerFillMessagePostFlags(message.action.options, peer, flags);
|
||||
if (silentPost) {
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
|
||||
@@ -403,7 +401,7 @@ void SendConfirmedFile(
|
||||
}
|
||||
const auto replyHeader = NewMessageReplyHeader(action);
|
||||
const auto anonymousPost = peer->amAnonymous();
|
||||
const auto silentPost = file->to.options.silent;
|
||||
const auto silentPost = ShouldSendSilent(peer, file->to.options);
|
||||
Api::FillMessagePostFlags(action, peer, flags);
|
||||
if (silentPost) {
|
||||
flags |= MTPDmessage::Flag::f_silent;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3957,8 +3955,7 @@ void ApiWrap::forwardMessages(
|
||||
histories.readInbox(history);
|
||||
|
||||
const auto anonymousPost = peer->amAnonymous();
|
||||
const auto silentPost = action.options.silent
|
||||
|| (peer->isBroadcast() && _session->data().notifySilentPosts(peer));
|
||||
const auto silentPost = ShouldSendSilent(peer, action.options);
|
||||
|
||||
auto flags = MTPDmessage::Flags(0);
|
||||
auto clientFlags = MTPDmessage_ClientFlags();
|
||||
@@ -4157,11 +4154,7 @@ void ApiWrap::sendSharedContact(
|
||||
MTP_string(firstName),
|
||||
MTP_string(lastName),
|
||||
MTP_string(vcard));
|
||||
auto options = action.options;
|
||||
if (_session->data().notifySilentPosts(peer)) {
|
||||
options.silent = true;
|
||||
}
|
||||
sendMedia(item, media, options);
|
||||
sendMedia(item, media, action.options);
|
||||
|
||||
_session->data().sendHistoryChangeNotifications();
|
||||
_session->changes().historyUpdated(
|
||||
@@ -4376,8 +4369,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
|
||||
flags |= MTPDmessage::Flag::f_media;
|
||||
}
|
||||
const auto anonymousPost = peer->amAnonymous();
|
||||
const auto silentPost = action.options.silent
|
||||
|| (peer->isBroadcast() && _session->data().notifySilentPosts(peer));
|
||||
const auto silentPost = ShouldSendSilent(peer, action.options);
|
||||
FillMessagePostFlags(action, peer, flags);
|
||||
if (silentPost) {
|
||||
sendFlags |= MTPmessages_SendMessage::Flag::f_silent;
|
||||
@@ -4518,8 +4510,7 @@ void ApiWrap::sendInlineResult(
|
||||
sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_reply_to_msg_id;
|
||||
}
|
||||
const auto anonymousPost = peer->amAnonymous();
|
||||
const auto silentPost = action.options.silent
|
||||
|| (peer->isBroadcast() && _session->data().notifySilentPosts(peer));
|
||||
const auto silentPost = ShouldSendSilent(peer, action.options);
|
||||
FillMessagePostFlags(action, peer, flags);
|
||||
if (silentPost) {
|
||||
sendFlags |= MTPmessages_SendInlineBotResult::Flag::f_silent;
|
||||
@@ -4685,7 +4676,7 @@ void ApiWrap::sendMediaWithRandomId(
|
||||
| (replyTo
|
||||
? MTPmessages_SendMedia::Flag::f_reply_to_msg_id
|
||||
: MTPmessages_SendMedia::Flag(0))
|
||||
| (options.silent
|
||||
| (ShouldSendSilent(history->peer, options)
|
||||
? MTPmessages_SendMedia::Flag::f_silent
|
||||
: MTPmessages_SendMedia::Flag(0))
|
||||
| (!sentEntities.v.isEmpty()
|
||||
@@ -4792,7 +4783,7 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
|
||||
| (replyTo
|
||||
? MTPmessages_SendMultiMedia::Flag::f_reply_to_msg_id
|
||||
: MTPmessages_SendMultiMedia::Flag(0))
|
||||
| (album->options.silent
|
||||
| (ShouldSendSilent(history->peer, album->options)
|
||||
? MTPmessages_SendMultiMedia::Flag::f_silent
|
||||
: MTPmessages_SendMultiMedia::Flag(0))
|
||||
| (album->options.scheduled
|
||||
@@ -4830,10 +4821,6 @@ void ApiWrap::sendAlbumIfReady(not_null<SendingAlbum*> album) {
|
||||
|
||||
FileLoadTo ApiWrap::fileLoadTaskOptions(const SendAction &action) const {
|
||||
const auto peer = action.history->peer;
|
||||
auto options = action.options;
|
||||
if (_session->data().notifySilentPosts(peer)) {
|
||||
options.silent = true;
|
||||
}
|
||||
return FileLoadTo(peer->id, action.options, action.replyTo);
|
||||
}
|
||||
|
||||
@@ -5263,8 +5250,7 @@ void ApiWrap::createPoll(
|
||||
history->clearLocalDraft();
|
||||
history->clearCloudDraft();
|
||||
}
|
||||
const auto silentPost = action.options.silent
|
||||
|| (peer->isBroadcast() && _session->data().notifySilentPosts(peer));
|
||||
const auto silentPost = ShouldSendSilent(peer, action.options);
|
||||
if (silentPost) {
|
||||
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
|
||||
}
|
||||
@@ -5287,6 +5273,11 @@ void ApiWrap::createPoll(
|
||||
MTP_int(action.options.scheduled)
|
||||
)).done([=](const MTPUpdates &result) mutable {
|
||||
applyUpdates(result);
|
||||
_session->changes().historyUpdated(
|
||||
history,
|
||||
(action.options.scheduled
|
||||
? Data::HistoryUpdate::Flag::ScheduledSent
|
||||
: Data::HistoryUpdate::Flag::MessageSent));
|
||||
done();
|
||||
finish();
|
||||
}).fail([=](const RPCError &error) mutable {
|
||||
|
||||
@@ -446,7 +446,7 @@ autoDownloadLimitPadding: margins(22px, 8px, 22px, 8px);
|
||||
|
||||
confirmCaptionArea: InputField(defaultInputField) {
|
||||
textMargins: margins(1px, 26px, 31px, 4px);
|
||||
heightMax: 78px;
|
||||
heightMax: 158px;
|
||||
}
|
||||
confirmBg: windowBgOver;
|
||||
confirmMaxHeight: 245px;
|
||||
|
||||
@@ -280,8 +280,9 @@ void SendFilesBox::enqueueNextPrepare() {
|
||||
}
|
||||
while (!_list.filesToProcess.empty()
|
||||
&& _list.filesToProcess.front().information) {
|
||||
addFile(std::move(_list.filesToProcess.front()));
|
||||
auto file = std::move(_list.filesToProcess.front());
|
||||
_list.filesToProcess.pop_front();
|
||||
addFile(std::move(file));
|
||||
}
|
||||
if (_list.filesToProcess.empty()) {
|
||||
return;
|
||||
@@ -329,7 +330,11 @@ void SendFilesBox::setupShadows() {
|
||||
}
|
||||
|
||||
void SendFilesBox::prepare() {
|
||||
_send = addButton(tr::lng_send_button(), [=] { send({}); });
|
||||
_send = addButton(
|
||||
(_sendType == Api::SendType::Normal
|
||||
? tr::lng_send_button()
|
||||
: tr::lng_create_group_next()),
|
||||
[=] { send({}); });
|
||||
if (_sendType == Api::SendType::Normal) {
|
||||
SendMenu::SetupMenuAndShortcuts(
|
||||
_send,
|
||||
@@ -638,9 +643,6 @@ void SendFilesBox::setupSendWayControls() {
|
||||
}
|
||||
|
||||
void SendFilesBox::updateSendWayControlsVisibility() {
|
||||
if (_sendLimit == SendLimit::One) {
|
||||
return;
|
||||
}
|
||||
const auto onlyOne = (_sendLimit == SendLimit::One);
|
||||
_groupFiles->setVisible(_list.hasGroupOption(onlyOne));
|
||||
_sendImagesAsPhotos->setVisible(
|
||||
@@ -817,10 +819,13 @@ void SendFilesBox::addPreparedAsyncFile(Ui::PreparedFile &&file) {
|
||||
}
|
||||
|
||||
void SendFilesBox::addFile(Ui::PreparedFile &&file) {
|
||||
// canBeSentInSlowmode checks for non empty filesToProcess.
|
||||
auto saved = base::take(_list.filesToProcess);
|
||||
_list.files.push_back(std::move(file));
|
||||
if (_sendLimit == SendLimit::One && !_list.canBeSentInSlowmode()) {
|
||||
_list.files.pop_back();
|
||||
}
|
||||
_list.filesToProcess = std::move(saved);
|
||||
}
|
||||
|
||||
void SendFilesBox::refreshTitleText() {
|
||||
|
||||
@@ -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,108 @@ 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 setSendMenuType(Fn<SendMenu::Type()> &&callback);
|
||||
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;
|
||||
|
||||
Fn<SendMenu::Type()> _sendMenuType;
|
||||
|
||||
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 +171,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 +224,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 +270,7 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) {
|
||||
base::take(_mrows),
|
||||
base::take(_hrows),
|
||||
base::take(_brows),
|
||||
internal::StickerRows(),
|
||||
StickerRows(),
|
||||
false);
|
||||
return;
|
||||
}
|
||||
@@ -203,7 +302,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 +310,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 +322,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 +332,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 +534,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()) {
|
||||
@@ -590,6 +689,10 @@ bool FieldAutocomplete::chooseSelected(ChooseMethod method) const {
|
||||
return _inner->chooseSelected(method);
|
||||
}
|
||||
|
||||
void FieldAutocomplete::setSendMenuType(Fn<SendMenu::Type()> &&callback) {
|
||||
_inner->setSendMenuType(std::move(callback));
|
||||
}
|
||||
|
||||
bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
|
||||
auto hidden = isHidden();
|
||||
auto moderate = Core::App().settings().moderateModeEnabled();
|
||||
@@ -620,9 +723,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 +743,7 @@ FieldAutocompleteInner::FieldAutocompleteInner(
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
|
||||
void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
QRect r(e->rect());
|
||||
@@ -841,11 +942,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 +958,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 +969,7 @@ void FieldAutocompleteInner::clearSel(bool hidden) {
|
||||
}
|
||||
}
|
||||
|
||||
bool FieldAutocompleteInner::moveSel(int key) {
|
||||
bool FieldAutocomplete::Inner::moveSel(int key) {
|
||||
_mouseSelection = false;
|
||||
_lastMousePosition = std::nullopt;
|
||||
|
||||
@@ -903,12 +1004,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 +1056,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 +1100,7 @@ void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
void FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
_previewTimer.cancel();
|
||||
|
||||
int32 pressed = _down;
|
||||
@@ -1017,12 +1118,14 @@ 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;
|
||||
}
|
||||
const auto index = _sel;
|
||||
const auto type = SendMenu::Type::Scheduled;
|
||||
const auto type = _sendMenuType
|
||||
? _sendMenuType()
|
||||
: SendMenu::Type::Disabled;
|
||||
const auto method = FieldAutocomplete::ChooseMethod::ByClick;
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(this);
|
||||
|
||||
@@ -1031,7 +1134,7 @@ void FieldAutocompleteInner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
};
|
||||
SendMenu::FillSendMenu(
|
||||
_menu,
|
||||
[&] { return type; },
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send));
|
||||
|
||||
@@ -1040,11 +1143,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 +1156,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 +1167,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 +1187,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 +1203,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 +1218,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 +1243,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 +1289,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 +1299,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 +1310,32 @@ void FieldAutocompleteInner::showPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::mentionChosen() const
|
||||
void FieldAutocomplete::Inner::setSendMenuType(
|
||||
Fn<SendMenu::Type()> &&callback) {
|
||||
_sendMenuType = std::move(callback);
|
||||
}
|
||||
|
||||
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,20 @@ class DocumentMedia;
|
||||
class CloudImageView;
|
||||
} // namespace Data
|
||||
|
||||
namespace internal {
|
||||
namespace SendMenu {
|
||||
enum class Type;
|
||||
} // namespace SendMenu
|
||||
|
||||
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,
|
||||
@@ -124,6 +103,7 @@ public:
|
||||
void setModerateKeyActivateCallback(Fn<bool(int)> callback) {
|
||||
_moderateKeyActivateCallback = std::move(callback);
|
||||
}
|
||||
void setSendMenuType(Fn<SendMenu::Type()> &&callback);
|
||||
|
||||
void hideFast();
|
||||
|
||||
@@ -140,29 +120,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 +191,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
|
||||
|
||||
@@ -377,7 +377,7 @@ void GifsListWidget::fillContextMenu(
|
||||
};
|
||||
SendMenu::FillSendMenu(
|
||||
menu,
|
||||
[&] { return type; },
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send));
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ Fn<void()> DefaultScheduleCallback(
|
||||
|
||||
FillMenuResult FillSendMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
Fn<Type()> type,
|
||||
Type type,
|
||||
Fn<void()> silent,
|
||||
Fn<void()> schedule) {
|
||||
if (!silent && !schedule) {
|
||||
return FillMenuResult::None;
|
||||
}
|
||||
const auto now = type();
|
||||
const auto now = type;
|
||||
if (now == Type::Disabled
|
||||
|| (!silent && now == Type::SilentOnly)) {
|
||||
return FillMenuResult::None;
|
||||
@@ -76,7 +76,7 @@ void SetupMenuAndShortcuts(
|
||||
const auto menu = std::make_shared<base::unique_qptr<Ui::PopupMenu>>();
|
||||
const auto showMenu = [=] {
|
||||
*menu = base::make_unique_q<Ui::PopupMenu>(button);
|
||||
const auto result = FillSendMenu(*menu, type, silent, schedule);
|
||||
const auto result = FillSendMenu(*menu, type(), silent, schedule);
|
||||
const auto success = (result == FillMenuResult::Success);
|
||||
if (success) {
|
||||
(*menu)->popup(QCursor::pos());
|
||||
|
||||
@@ -40,7 +40,7 @@ Fn<void()> DefaultScheduleCallback(
|
||||
|
||||
FillMenuResult FillSendMenu(
|
||||
not_null<Ui::PopupMenu*> menu,
|
||||
Fn<Type()> type,
|
||||
Type type,
|
||||
Fn<void()> silent,
|
||||
Fn<void()> schedule);
|
||||
|
||||
|
||||
@@ -2077,7 +2077,7 @@ void StickersListWidget::fillContextMenu(
|
||||
};
|
||||
SendMenu::FillSendMenu(
|
||||
menu,
|
||||
[&] { return type; },
|
||||
type,
|
||||
SendMenu::DefaultSilentCallback(send),
|
||||
SendMenu::DefaultScheduleCallback(this, type, send));
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -113,7 +109,7 @@ public:
|
||||
_beforeHidingCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void setSendMenuType(Fn<SendMenu::Type()> callback) {
|
||||
void setSendMenuType(Fn<SendMenu::Type()> &&callback) {
|
||||
_sendMenuType = std::move(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,22 @@ 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"
|
||||
},
|
||||
{
|
||||
2004011,
|
||||
"- Improve locked voice message recording.\n"
|
||||
|
||||
"- Fix main window closing to tray on Windows.\n"
|
||||
|
||||
"- Fix crash in bot command sending.\n"
|
||||
|
||||
"- Fix adding additional photos when sending an album to a group with enabled slow mode.\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 = 2004009;
|
||||
constexpr auto AppVersionStr = "2.4.9";
|
||||
constexpr auto AppVersion = 2004011;
|
||||
constexpr auto AppVersionStr = "2.4.11";
|
||||
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,8 @@ 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 "main/main_session_settings.h"
|
||||
#include "api/api_updates.h"
|
||||
#include "boxes/share_box.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "ui/toast/toast.h"
|
||||
@@ -276,11 +278,8 @@ void FastShareMessage(not_null<HistoryItem*> item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sendFlags = MTPmessages_ForwardMessages::Flag(0)
|
||||
const auto commonSendFlags = MTPmessages_ForwardMessages::Flag(0)
|
||||
| MTPmessages_ForwardMessages::Flag::f_with_my_score
|
||||
| (options.silent
|
||||
? MTPmessages_ForwardMessages::Flag::f_silent
|
||||
: MTPmessages_ForwardMessages::Flag(0))
|
||||
| (options.scheduled
|
||||
? MTPmessages_ForwardMessages::Flag::f_schedule_date
|
||||
: MTPmessages_ForwardMessages::Flag(0));
|
||||
@@ -310,13 +309,17 @@ void FastShareMessage(not_null<HistoryItem*> item) {
|
||||
}
|
||||
histories.sendRequest(history, requestType, [=](Fn<void()> finish) {
|
||||
auto &api = history->session().api();
|
||||
const auto sendFlags = commonSendFlags
|
||||
| (ShouldSendSilent(peer, options)
|
||||
? MTPmessages_ForwardMessages::Flag::f_silent
|
||||
: MTPmessages_ForwardMessages::Flag(0));
|
||||
history->sendRequestId = api.request(MTPmessages_ForwardMessages(
|
||||
MTP_flags(sendFlags),
|
||||
data->peer->input,
|
||||
MTP_vector<MTPint>(msgIds),
|
||||
MTP_vector<MTPlong>(generateRandom()),
|
||||
peer->input,
|
||||
MTP_int(options.scheduled)
|
||||
MTP_flags(sendFlags),
|
||||
data->peer->input,
|
||||
MTP_vector<MTPint>(msgIds),
|
||||
MTP_vector<MTPlong>(generateRandom()),
|
||||
peer->input,
|
||||
MTP_int(options.scheduled)
|
||||
)).done([=](const MTPUpdates &updates, mtpRequestId requestId) {
|
||||
history->session().api().applyUpdates(updates);
|
||||
data->requests.remove(requestId);
|
||||
@@ -374,6 +377,15 @@ MTPDmessage::Flags NewMessageFlags(not_null<PeerData*> peer) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ShouldSendSilent(
|
||||
not_null<PeerData*> peer,
|
||||
const Api::SendOptions &options) {
|
||||
return options.silent
|
||||
|| (peer->isBroadcast() && peer->owner().notifySilentPosts(peer))
|
||||
|| (peer->session().supportMode()
|
||||
&& peer->session().settings().supportAllSilent());
|
||||
}
|
||||
|
||||
MsgId LookupReplyToTop(not_null<History*> history, MsgId replyToId) {
|
||||
const auto &owner = history->owner();
|
||||
if (const auto item = owner.message(history->channelId(), replyToId)) {
|
||||
@@ -1083,6 +1095,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 +1340,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 +1489,7 @@ void HistoryMessage::setText(const TextWithEntities &textWithEntities) {
|
||||
}
|
||||
|
||||
clearIsolatedEmoji();
|
||||
const auto context = Core::UiIntegration::Context{
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = &history()->session()
|
||||
};
|
||||
_text.setMarkedText(
|
||||
|
||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Api {
|
||||
struct SendAction;
|
||||
struct SendOptions;
|
||||
} // namespace Api
|
||||
|
||||
namespace HistoryView {
|
||||
@@ -24,6 +25,9 @@ struct HistoryMessageViews;
|
||||
[[nodiscard]] Fn<void(ChannelData*, MsgId)> HistoryDependentItemCallback(
|
||||
not_null<HistoryItem*> item);
|
||||
[[nodiscard]] MTPDmessage::Flags NewMessageFlags(not_null<PeerData*> peer);
|
||||
[[nodiscard]] bool ShouldSendSilent(
|
||||
not_null<PeerData*> peer,
|
||||
const Api::SendOptions &options);
|
||||
[[nodiscard]] MTPDmessage_ClientFlags NewMessageClientFlags();
|
||||
[[nodiscard]] MsgId LookupReplyToTop(
|
||||
not_null<History*> history,
|
||||
@@ -236,6 +240,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()));
|
||||
}
|
||||
@@ -813,8 +818,9 @@ void HistoryService::updateDependentText() {
|
||||
|
||||
setServiceText(text);
|
||||
history()->owner().requestItemResize(this);
|
||||
if (history()->textCachedFor == this) {
|
||||
history()->textCachedFor = nullptr;
|
||||
const auto inDialogsHistory = history()->migrateToOrMe();
|
||||
if (inDialogsHistory->textCachedFor == this) {
|
||||
inDialogsHistory->textCachedFor = nullptr;
|
||||
}
|
||||
//if (const auto feed = history()->peer->feed()) { // #TODO archive
|
||||
// if (feed->textCachedFor == this) {
|
||||
|
||||
@@ -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,35 @@ 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();
|
||||
void clearListenState();
|
||||
|
||||
[[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 +183,9 @@ private:
|
||||
void initSendButton();
|
||||
void initWebpageProcess();
|
||||
void initWriteRestriction();
|
||||
void initVoiceRecordBar();
|
||||
void initAutocomplete();
|
||||
void updateSubmitSettings();
|
||||
void updateSendButtonType();
|
||||
void updateHeight();
|
||||
void updateWrappingVisibility();
|
||||
@@ -169,28 +194,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 +263,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,156 @@
|
||||
/*
|
||||
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 hideAnimated();
|
||||
void hideFast();
|
||||
void clearListenState();
|
||||
|
||||
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 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,789 @@
|
||||
/*
|
||||
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);
|
||||
|
||||
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.; };
|
||||
|
||||
const auto stateChangedAnimation =
|
||||
lifetime().make_state<Ui::Animations::Simple>();
|
||||
const auto currentState = lifetime().make_state<Type>(_state.current());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Paint icon.
|
||||
{
|
||||
const auto stateProgress = stateChangedAnimation->value(0.);
|
||||
const auto scale = (std::cos(M_PI * 2 * stateProgress) + 1.) * .5;
|
||||
p.translate(_center, _center);
|
||||
if (scale < 1.) {
|
||||
p.scale(scale, scale);
|
||||
}
|
||||
const auto state = *currentState;
|
||||
const auto icon = (state == Type::Send)
|
||||
? st::historySendIcon
|
||||
: st::historyRecordVoiceActive;
|
||||
const auto position = (state == Type::Send)
|
||||
? st::historyRecordSendIconPosition
|
||||
: QPoint(0, 0);
|
||||
icon.paint(
|
||||
p,
|
||||
-icon.width() / 2 + position.x(),
|
||||
-icon.height() / 2 + position.y(),
|
||||
0,
|
||||
st::historyRecordVoiceFgActiveIcon->c);
|
||||
}
|
||||
}, 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();
|
||||
_state = Type::Record;
|
||||
} else {
|
||||
if (!_recordingAnimation.animating()) {
|
||||
_recordingAnimation.start();
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
actives(
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](bool active) {
|
||||
setPointerCursor(active);
|
||||
}, lifetime());
|
||||
|
||||
_state.changes(
|
||||
) | rpl::start_with_next([=](Type newState) {
|
||||
const auto to = 1.;
|
||||
auto callback = [=](float64 value) {
|
||||
if (value >= (to * .5)) {
|
||||
*currentState = newState;
|
||||
}
|
||||
update();
|
||||
};
|
||||
const auto duration = st::historyRecordVoiceDuration * 2;
|
||||
stateChangedAnimation->start(std::move(callback), 0., to, duration);
|
||||
}, 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();
|
||||
}
|
||||
|
||||
void VoiceRecordButton::setType(Type state) {
|
||||
_state = state;
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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();
|
||||
|
||||
enum class Type {
|
||||
Send,
|
||||
Record,
|
||||
};
|
||||
|
||||
void setType(Type state);
|
||||
|
||||
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;
|
||||
rpl::variable<Type> _state = Type::Record;
|
||||
|
||||
// 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,19 @@ 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));
|
||||
|
||||
_composeControls->cancelReplyMessage();
|
||||
_composeControls->clearListenState();
|
||||
finishSending();
|
||||
}
|
||||
|
||||
void RepliesWidget::send(Api::SendOptions options) {
|
||||
@@ -1010,14 +1041,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 +1135,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 +1189,7 @@ void RepliesWidget::finishSending() {
|
||||
//if (_previewData && _previewData->pendingTill) previewCancel();
|
||||
doSetInnerFocus();
|
||||
showAtEnd();
|
||||
refreshTopBarActiveChat();
|
||||
}
|
||||
|
||||
void RepliesWidget::showAtPosition(
|
||||
@@ -1213,6 +1248,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 +1403,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 +1509,7 @@ void RepliesWidget::updateControlsGeometry() {
|
||||
updateInnerVisibleArea();
|
||||
}
|
||||
_composeControls->move(0, bottom - controlsHeight);
|
||||
_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
|
||||
|
||||
updateScrollDownPosition();
|
||||
}
|
||||
@@ -1590,12 +1634,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 +1778,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 +1808,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() {
|
||||
@@ -560,6 +573,7 @@ void ScheduledWidget::sendVoice(
|
||||
auto action = Api::SendAction(_history);
|
||||
action.options = options;
|
||||
session().api().sendVoiceMessage(bytes, waveform, duration, action);
|
||||
_composeControls->clearListenState();
|
||||
}
|
||||
|
||||
void ScheduledWidget::edit(
|
||||
@@ -661,14 +675,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 +826,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 +980,7 @@ void ScheduledWidget::updateControlsGeometry() {
|
||||
updateInnerVisibleArea();
|
||||
}
|
||||
_composeControls->move(0, bottom - controlsHeight);
|
||||
_composeControls->setAutocompleteBoundingRect(_scroll->geometry());
|
||||
|
||||
updateScrollDownPosition();
|
||||
}
|
||||
@@ -1049,9 +1059,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 +1173,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 +1207,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(),
|
||||
|
||||