diff --git a/.github/workflows/issue_closer.yml b/.github/workflows/issue_closer.yml index 44782fa45..d3784944f 100644 --- a/.github/workflows/issue_closer.yml +++ b/.github/workflows/issue_closer.yml @@ -8,132 +8,7 @@ jobs: comment: runs-on: ubuntu-latest steps: - - name: Get the latest version. - run: | - tag=$(git ls-remote --tags git://github.com/$GITHUB_REPOSITORY | cut -f 2 | tail -n1) - echo $tag - echo "LATEST_TAG=$tag" >> $GITHUB_ENV - - - name: Get the latest macOS version. - shell: python - run: | - import subprocess; - from xml.dom import minidom; - - url = "https://osx.telegram.org/updates/versions.xml"; - subprocess.check_call("wget %s" % url, shell=True); - - xmldoc = minidom.parse('versions.xml'); - itemlist = xmldoc.getElementsByTagName('enclosure'); - ver = itemlist[0].attributes['sparkle:shortVersionString'].value; - print(ver); - - open(os.environ['GITHUB_ENV'], "a").write("LATEST_MACOS=" + ver); - - - name: Check a version from an issue. - uses: actions/github-script@0.4.0 + - name: Process an issue. + uses: desktop-app/action_issue_closer@master with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - let errorStr = "Version not found."; - - function maxIndexOf(str, i) { - let index = str.indexOf(i); - return (index == -1) ? Number.MAX_SAFE_INTEGER : index; - } - - let item1 = "Version of Telegram Desktop"; - let item2 = "Installation source"; - let item3 = "Used theme"; - let item4 = "
"; - let body = context.payload.issue.body; - - console.log("Body of issue:\n" + body); - let index1 = body.indexOf(item1); - let index2 = Math.min( - Math.min( - maxIndexOf(body, item2), - maxIndexOf(body, item3)), - maxIndexOf(body, item4)); - - console.log("Index 1: " + index1); - console.log("Index 2: " + index2); - - if (index1 == -1) { - console.log(errorStr); - return; - } - - function parseVersion(str) { - let pattern = /[0-9]\.[0-9][0-9.]{0,}/g; - return str.match(pattern); - } - function firstNum(version) { - return version[0].split(".")[0]; - } - - let issueVer = parseVersion(body.substring(index1 + item1.length, index2)); - - if (issueVer == undefined) { - console.log(errorStr); - return; - } - console.log("Version from issue: " + issueVer[0]); - - let latestVer = parseVersion(process.env.LATEST_TAG); - - if (latestVer == undefined) { - console.log(errorStr); - return; - } - console.log("Version from tags: " + latestVer[0]); - - let issueNum = firstNum(issueVer); - let latestNum = firstNum(latestVer); - - let macos_ver = process.env.LATEST_MACOS; - console.log("Telegram for MacOS version from website: " + macos_ver); - - if (issueNum <= latestNum && issueNum < macos_ver) { - console.log("Seems the version of this issue is fine!"); - return; - } - if (issueNum > macos_ver) { - let message = `Seems like it's neither the Telegram Desktop\ - nor the Telegram for macOS version. - `; - console.log(message); - return; - } - - let message = ` - Sorry, but according to the version you specify in this issue, \ - you are using the [Telegram for macOS](https://macos.telegram.org), \ - not the [Telegram Desktop](https://desktop.telegram.org). - You can report your issue to [the group](https://t.me/macswift) \ - or to [the repository of Telegram for macOS](https://github.com/overtake/TelegramSwift). - - **If I made a mistake and closed your issue wrongly, please reopen it. Thanks!** - `; - - let params = { - owner: context.issue.owner, - repo: context.issue.repo, - issue_number: context.issue.number - }; - - github.issues.createComment({ - ...params, - body: message - }); - - github.issues.addLabels({ - ...params, - labels: ['TG macOS Swift'] - }); - - github.issues.update({ - ...params, - state: 'closed' - }); - + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index b9cc4e0bd..cf4cc1b11 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -47,77 +47,38 @@ on: jobs: linux: - name: Ubuntu 14.04 + name: CentOS 7 if: > !(github.event_name == 'push' && contains(github.event.head_commit.message, '[skip ci]')) && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) runs-on: ubuntu-latest - container: ubuntu:trusty + container: + image: docker.pkg.github.com/kotatogram/kotatogram-desktop/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: "7" - 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. - 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 - - name: Clone. uses: actions/checkout@v2 with: @@ -125,452 +86,11 @@ jobs: path: ${{ env.REPO_NAME }} - name: First set up. - shell: bash run: | gcc --version + ln -s $LibrariesPath Libraries - 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} - - - name: Telegram Desktop build. - if: env.ONLY_CACHE == 'false' + - name: Kotatogram Desktop build. run: | cd $REPO_NAME/Telegram @@ -584,7 +104,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 \ @@ -594,7 +116,6 @@ jobs: make -j$(nproc) - name: Check. - if: env.ONLY_CACHE == 'false' run: | filePath="$REPO_NAME/out/Debug/bin/Kotatogram" if test -f "$filePath"; then diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index b74ae209c..3b15adfa0 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -64,9 +64,9 @@ jobs: PREFIX: "/usr/local/macos" MACOSX_DEPLOYMENT_TARGET: "10.12" XZ: "xz-5.2.4" - QT: "5_12_8" + QT: "5_15_2" OPENSSL_VER: "1_1_1" - QT_PREFIX: "/usr/local/desktop-app/Qt-5.12.8" + QT_PREFIX: "/usr/local/desktop-app/Qt-5.15.2" LIBICONV_VER: "libiconv-1.16" UPLOAD_ARTIFACT: "false" ONLY_CACHE: "false" @@ -242,7 +242,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 @@ -251,7 +251,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 \ @@ -373,7 +375,7 @@ jobs: git clone $GIT/kcat/openal-soft.git cd openal-soft - git checkout openal-soft-1.19.1 + git checkout 3970252da9 cd build CFLAGS="$UNGUARDED" CPPFLAGS="$UNGUARDED" cmake \ @@ -424,20 +426,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.2 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_2/*') }} + - name: Use cached Qt 5.15.2. if: steps.cache-qt.outputs.cache-hit == 'true' run: | cd $LibrariesPath - mv qt-cache Qt-5.12.8 + mv qt-cache Qt-5.15.2 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.2 "$(dirname "$QT_PREFIX")"/ + - name: Qt 5.15.2 build. if: steps.cache-qt.outputs.cache-hit != 'true' run: | cd $LibrariesPath @@ -445,7 +447,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.2 git submodule update qtbase git submodule update qtimageformats cd qtbase @@ -486,7 +488,7 @@ jobs: run: | cd $LibrariesPath - git clone $GIT/desktop-app/tg_owt.git + git clone --recursive $GIT/desktop-app/tg_owt.git mkdir -p tg_owt/out/Debug cd tg_owt/out/Debug cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug \ @@ -520,6 +522,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 \ diff --git a/.github/workflows/user_agent_updater.yml b/.github/workflows/user_agent_updater.yml index eee5c8b2c..a19b3271f 100644 --- a/.github/workflows/user_agent_updater.yml +++ b/.github/workflows/user_agent_updater.yml @@ -44,12 +44,14 @@ jobs: git remote set-url origin $url - name: Delete branch. + env: + ref: ${{ github.event.pull_request.head.ref }} if: | env.isPull == '1' && github.event.action == 'closed' - && startsWith(github.head_ref, env.headBranchPrefix) + && startsWith(env.ref, env.headBranchPrefix) run: | - git push origin --delete ${{ github.head_ref }} + git push origin --delete $ref - name: Write a new version of Google Chrome to the user-agent for DNS. if: env.isPull == '0' diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 519ed4194..50eb07f59 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -66,8 +66,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_2" + QT_VER: "5.15.2" OPENSSL_VER: "1_1_1" UPLOAD_ARTIFACT: "true" ONLY_CACHE: "false" @@ -218,16 +218,14 @@ jobs: run: | %VC% - git clone %GIT%/telegramdesktop/openal-soft.git - cd openal-soft - git checkout fix_capture - cd build + git clone -b openal-soft-1.21.0 --depth=1 %GIT%/kcat/openal-soft.git + cd openal-soft\build cmake .. ^ -G "Visual Studio 16 2019" ^ -A Win32 ^ -D LIBTYPE:STRING=STATIC ^ -D FORCE_STATIC_VCRT=ON ^ - -D ALSOFT_BACKEND_WASAPI=OFF + -D ALSOFT_BACKEND_DSOUND=OFF msbuild -m OpenAL.vcxproj /property:Configuration=Debug @@ -301,20 +299,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.2 cache. id: cache-qt uses: actions/cache@v2 with: path: ${{ env.LibrariesPath }}/Qt-${{ env.QT_VER }} - key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_12_8/*') }} - - name: Configure Qt 5.12.8. + key: ${{ runner.OS }}-qt-${{ env.CACHE_KEY }}-${{ hashFiles('**/qtbase_5_15_2/*') }} + - name: Configure Qt 5.15.2. if: steps.cache-qt.outputs.cache-hit != 'true' run: | %VC% @@ -351,7 +349,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.2 build. if: steps.cache-qt.outputs.cache-hit != 'true' run: | %VC% @@ -374,7 +372,7 @@ jobs: run: | %VC% - git clone %GIT%/desktop-app/tg_owt.git + git clone --recursive %GIT%/desktop-app/tg_owt.git mkdir tg_owt\out\Debug cd tg_owt\out\Debug cmake -G Ninja ^ diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 67429e11f..b6d46629c 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -33,6 +33,7 @@ include(cmake/td_mtproto.cmake) include(cmake/td_lang.cmake) include(cmake/td_scheme.cmake) include(cmake/td_ui.cmake) +include(cmake/generate_appdata_changelog.cmake) set_target_properties(Telegram PROPERTIES AUTOMOC ON AUTORCC ON) @@ -59,6 +60,7 @@ PRIVATE desktop-app::external_rlottie desktop-app::external_zlib desktop-app::external_minizip + desktop-app::external_qt_static_plugins desktop-app::external_qt desktop-app::external_qr_code_generator desktop-app::external_crash_reports @@ -70,9 +72,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 desktop-app::external_xcb desktop-app::external_glib @@ -83,13 +82,12 @@ if (LINUX) PRIVATE desktop-app::external_statusnotifieritem desktop-app::external_dbusmenu_qt - desktop-app::external_fcitx_qt5 - desktop-app::external_fcitx5_qt5 - desktop-app::external_hime_qt ) endif() - if (DESKTOP_APP_USE_PACKAGED AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0) + if (DESKTOP_APP_USE_PACKAGED + AND NOT DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION + AND Qt5WaylandClient_VERSION VERSION_LESS 5.13.0) find_package(PkgConfig REQUIRED) pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client) @@ -112,7 +110,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() @@ -227,6 +225,8 @@ PRIVATE boxes/peer_list_box.h boxes/peer_list_controllers.cpp boxes/peer_list_controllers.h + boxes/peer_lists_box.cpp + boxes/peer_lists_box.h boxes/passcode_box.cpp boxes/passcode_box.h boxes/photo_crop_box.cpp @@ -257,6 +257,14 @@ PRIVATE calls/calls_box_controller.h calls/calls_call.cpp calls/calls_call.h + calls/calls_group_call.cpp + calls/calls_group_call.h + calls/calls_group_members.cpp + calls/calls_group_members.h + calls/calls_group_panel.cpp + calls/calls_group_panel.h + calls/calls_group_settings.cpp + calls/calls_group_settings.h calls/calls_emoji_fingerprint.cpp calls/calls_emoji_fingerprint.h calls/calls_instance.cpp @@ -378,6 +386,8 @@ PRIVATE data/data_file_origin.h data/data_flags.h data/data_game.h + data/data_group_call.cpp + data/data_group_call.h data/data_groups.cpp data/data_groups.h data/data_histories.cpp @@ -474,6 +484,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 +531,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 @@ -524,6 +539,8 @@ PRIVATE history/view/history_view_cursor_state.h history/view/history_view_element.cpp history/view/history_view_element.h + history/view/history_view_group_call_tracker.cpp + history/view/history_view_group_call_tracker.h history/view/history_view_list_widget.cpp history/view/history_view_list_widget.h history/view/history_view_message.cpp @@ -642,6 +659,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 @@ -811,6 +830,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 @@ -823,6 +844,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 @@ -1083,7 +1105,6 @@ PRIVATE mainwidget.h mainwindow.cpp mainwindow.h - qt_static_plugins.cpp settings.cpp settings.h stdafx.h @@ -1096,6 +1117,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() @@ -1305,6 +1331,7 @@ endif() if (LINUX AND DESKTOP_APP_USE_PACKAGED) include(GNUInstallDirs) configure_file("../lib/xdg/kotatogramdesktop.appdata.xml.in" "${CMAKE_CURRENT_BINARY_DIR}/kotatogramdesktop.appdata.xml" @ONLY) + generate_appdata_changelog(Telegram "${CMAKE_SOURCE_DIR}/changelog.txt" "${CMAKE_CURRENT_BINARY_DIR}/kotatogramdesktop.appdata.xml") install(TARGETS Telegram RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION "${CMAKE_INSTALL_BINDIR}") install(FILES "Resources/art/icon16.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps" RENAME "kotatogram.png") install(FILES "Resources/art/icon32.png" DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps" RENAME "kotatogram.png") diff --git a/Telegram/Resources/icons/call_answer.png b/Telegram/Resources/icons/calls/call_answer.png similarity index 100% rename from Telegram/Resources/icons/call_answer.png rename to Telegram/Resources/icons/calls/call_answer.png diff --git a/Telegram/Resources/icons/call_answer@2x.png b/Telegram/Resources/icons/calls/call_answer@2x.png similarity index 100% rename from Telegram/Resources/icons/call_answer@2x.png rename to Telegram/Resources/icons/calls/call_answer@2x.png diff --git a/Telegram/Resources/icons/call_answer@3x.png b/Telegram/Resources/icons/calls/call_answer@3x.png similarity index 100% rename from Telegram/Resources/icons/call_answer@3x.png rename to Telegram/Resources/icons/calls/call_answer@3x.png diff --git a/Telegram/Resources/icons/call_arrow_in.png b/Telegram/Resources/icons/calls/call_arrow_in.png similarity index 100% rename from Telegram/Resources/icons/call_arrow_in.png rename to Telegram/Resources/icons/calls/call_arrow_in.png diff --git a/Telegram/Resources/icons/call_arrow_in@2x.png b/Telegram/Resources/icons/calls/call_arrow_in@2x.png similarity index 100% rename from Telegram/Resources/icons/call_arrow_in@2x.png rename to Telegram/Resources/icons/calls/call_arrow_in@2x.png diff --git a/Telegram/Resources/icons/call_arrow_in@3x.png b/Telegram/Resources/icons/calls/call_arrow_in@3x.png similarity index 100% rename from Telegram/Resources/icons/call_arrow_in@3x.png rename to Telegram/Resources/icons/calls/call_arrow_in@3x.png diff --git a/Telegram/Resources/icons/call_arrow_out.png b/Telegram/Resources/icons/calls/call_arrow_out.png similarity index 100% rename from Telegram/Resources/icons/call_arrow_out.png rename to Telegram/Resources/icons/calls/call_arrow_out.png diff --git a/Telegram/Resources/icons/call_arrow_out@2x.png b/Telegram/Resources/icons/calls/call_arrow_out@2x.png similarity index 100% rename from Telegram/Resources/icons/call_arrow_out@2x.png rename to Telegram/Resources/icons/calls/call_arrow_out@2x.png diff --git a/Telegram/Resources/icons/call_arrow_out@3x.png b/Telegram/Resources/icons/calls/call_arrow_out@3x.png similarity index 100% rename from Telegram/Resources/icons/call_arrow_out@3x.png rename to Telegram/Resources/icons/calls/call_arrow_out@3x.png diff --git a/Telegram/Resources/icons/call_camera_active.png b/Telegram/Resources/icons/calls/call_camera_active.png similarity index 100% rename from Telegram/Resources/icons/call_camera_active.png rename to Telegram/Resources/icons/calls/call_camera_active.png diff --git a/Telegram/Resources/icons/call_camera_active@2x.png b/Telegram/Resources/icons/calls/call_camera_active@2x.png similarity index 100% rename from Telegram/Resources/icons/call_camera_active@2x.png rename to Telegram/Resources/icons/calls/call_camera_active@2x.png diff --git a/Telegram/Resources/icons/call_camera_active@3x.png b/Telegram/Resources/icons/calls/call_camera_active@3x.png similarity index 100% rename from Telegram/Resources/icons/call_camera_active@3x.png rename to Telegram/Resources/icons/calls/call_camera_active@3x.png diff --git a/Telegram/Resources/icons/call_camera_muted.png b/Telegram/Resources/icons/calls/call_camera_muted.png similarity index 100% rename from Telegram/Resources/icons/call_camera_muted.png rename to Telegram/Resources/icons/calls/call_camera_muted.png diff --git a/Telegram/Resources/icons/call_camera_muted@2x.png b/Telegram/Resources/icons/calls/call_camera_muted@2x.png similarity index 100% rename from Telegram/Resources/icons/call_camera_muted@2x.png rename to Telegram/Resources/icons/calls/call_camera_muted@2x.png diff --git a/Telegram/Resources/icons/call_camera_muted@3x.png b/Telegram/Resources/icons/calls/call_camera_muted@3x.png similarity index 100% rename from Telegram/Resources/icons/call_camera_muted@3x.png rename to Telegram/Resources/icons/calls/call_camera_muted@3x.png diff --git a/Telegram/Resources/icons/call_cancel.png b/Telegram/Resources/icons/calls/call_cancel.png similarity index 100% rename from Telegram/Resources/icons/call_cancel.png rename to Telegram/Resources/icons/calls/call_cancel.png diff --git a/Telegram/Resources/icons/call_cancel@2x.png b/Telegram/Resources/icons/calls/call_cancel@2x.png similarity index 100% rename from Telegram/Resources/icons/call_cancel@2x.png rename to Telegram/Resources/icons/calls/call_cancel@2x.png diff --git a/Telegram/Resources/icons/call_cancel@3x.png b/Telegram/Resources/icons/calls/call_cancel@3x.png similarity index 100% rename from Telegram/Resources/icons/call_cancel@3x.png rename to Telegram/Resources/icons/calls/call_cancel@3x.png diff --git a/Telegram/Resources/icons/call_discard.png b/Telegram/Resources/icons/calls/call_discard.png similarity index 100% rename from Telegram/Resources/icons/call_discard.png rename to Telegram/Resources/icons/calls/call_discard.png diff --git a/Telegram/Resources/icons/call_discard@2x.png b/Telegram/Resources/icons/calls/call_discard@2x.png similarity index 100% rename from Telegram/Resources/icons/call_discard@2x.png rename to Telegram/Resources/icons/calls/call_discard@2x.png diff --git a/Telegram/Resources/icons/call_discard@3x.png b/Telegram/Resources/icons/calls/call_discard@3x.png similarity index 100% rename from Telegram/Resources/icons/call_discard@3x.png rename to Telegram/Resources/icons/calls/call_discard@3x.png diff --git a/Telegram/Resources/icons/call_rating.png b/Telegram/Resources/icons/calls/call_rating.png similarity index 100% rename from Telegram/Resources/icons/call_rating.png rename to Telegram/Resources/icons/calls/call_rating.png diff --git a/Telegram/Resources/icons/call_rating@2x.png b/Telegram/Resources/icons/calls/call_rating@2x.png similarity index 100% rename from Telegram/Resources/icons/call_rating@2x.png rename to Telegram/Resources/icons/calls/call_rating@2x.png diff --git a/Telegram/Resources/icons/call_rating@3x.png b/Telegram/Resources/icons/calls/call_rating@3x.png similarity index 100% rename from Telegram/Resources/icons/call_rating@3x.png rename to Telegram/Resources/icons/calls/call_rating@3x.png diff --git a/Telegram/Resources/icons/call_rating_filled.png b/Telegram/Resources/icons/calls/call_rating_filled.png similarity index 100% rename from Telegram/Resources/icons/call_rating_filled.png rename to Telegram/Resources/icons/calls/call_rating_filled.png diff --git a/Telegram/Resources/icons/call_rating_filled@2x.png b/Telegram/Resources/icons/calls/call_rating_filled@2x.png similarity index 100% rename from Telegram/Resources/icons/call_rating_filled@2x.png rename to Telegram/Resources/icons/calls/call_rating_filled@2x.png diff --git a/Telegram/Resources/icons/call_rating_filled@3x.png b/Telegram/Resources/icons/calls/call_rating_filled@3x.png similarity index 100% rename from Telegram/Resources/icons/call_rating_filled@3x.png rename to Telegram/Resources/icons/calls/call_rating_filled@3x.png diff --git a/Telegram/Resources/icons/call_record_active.png b/Telegram/Resources/icons/calls/call_record_active.png similarity index 100% rename from Telegram/Resources/icons/call_record_active.png rename to Telegram/Resources/icons/calls/call_record_active.png diff --git a/Telegram/Resources/icons/call_record_active@2x.png b/Telegram/Resources/icons/calls/call_record_active@2x.png similarity index 100% rename from Telegram/Resources/icons/call_record_active@2x.png rename to Telegram/Resources/icons/calls/call_record_active@2x.png diff --git a/Telegram/Resources/icons/call_record_active@3x.png b/Telegram/Resources/icons/calls/call_record_active@3x.png similarity index 100% rename from Telegram/Resources/icons/call_record_active@3x.png rename to Telegram/Resources/icons/calls/call_record_active@3x.png diff --git a/Telegram/Resources/icons/call_record_muted.png b/Telegram/Resources/icons/calls/call_record_muted.png similarity index 100% rename from Telegram/Resources/icons/call_record_muted.png rename to Telegram/Resources/icons/calls/call_record_muted.png diff --git a/Telegram/Resources/icons/call_record_muted@2x.png b/Telegram/Resources/icons/calls/call_record_muted@2x.png similarity index 100% rename from Telegram/Resources/icons/call_record_muted@2x.png rename to Telegram/Resources/icons/calls/call_record_muted@2x.png diff --git a/Telegram/Resources/icons/call_record_muted@3x.png b/Telegram/Resources/icons/calls/call_record_muted@3x.png similarity index 100% rename from Telegram/Resources/icons/call_record_muted@3x.png rename to Telegram/Resources/icons/calls/call_record_muted@3x.png diff --git a/Telegram/Resources/icons/calls/call_settings.png b/Telegram/Resources/icons/calls/call_settings.png new file mode 100644 index 000000000..f16037f3c Binary files /dev/null and b/Telegram/Resources/icons/calls/call_settings.png differ diff --git a/Telegram/Resources/icons/calls/call_settings@2x.png b/Telegram/Resources/icons/calls/call_settings@2x.png new file mode 100644 index 000000000..21d18e24f Binary files /dev/null and b/Telegram/Resources/icons/calls/call_settings@2x.png differ diff --git a/Telegram/Resources/icons/calls/call_settings@3x.png b/Telegram/Resources/icons/calls/call_settings@3x.png new file mode 100644 index 000000000..d86c793a0 Binary files /dev/null and b/Telegram/Resources/icons/calls/call_settings@3x.png differ diff --git a/Telegram/Resources/icons/call_shadow_left.png b/Telegram/Resources/icons/calls/call_shadow_left.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_left.png rename to Telegram/Resources/icons/calls/call_shadow_left.png diff --git a/Telegram/Resources/icons/call_shadow_left@2x.png b/Telegram/Resources/icons/calls/call_shadow_left@2x.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_left@2x.png rename to Telegram/Resources/icons/calls/call_shadow_left@2x.png diff --git a/Telegram/Resources/icons/call_shadow_left@3x.png b/Telegram/Resources/icons/calls/call_shadow_left@3x.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_left@3x.png rename to Telegram/Resources/icons/calls/call_shadow_left@3x.png diff --git a/Telegram/Resources/icons/call_shadow_top.png b/Telegram/Resources/icons/calls/call_shadow_top.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_top.png rename to Telegram/Resources/icons/calls/call_shadow_top.png diff --git a/Telegram/Resources/icons/call_shadow_top@2x.png b/Telegram/Resources/icons/calls/call_shadow_top@2x.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_top@2x.png rename to Telegram/Resources/icons/calls/call_shadow_top@2x.png diff --git a/Telegram/Resources/icons/call_shadow_top@3x.png b/Telegram/Resources/icons/calls/call_shadow_top@3x.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_top@3x.png rename to Telegram/Resources/icons/calls/call_shadow_top@3x.png diff --git a/Telegram/Resources/icons/call_shadow_top_left.png b/Telegram/Resources/icons/calls/call_shadow_top_left.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_top_left.png rename to Telegram/Resources/icons/calls/call_shadow_top_left.png diff --git a/Telegram/Resources/icons/call_shadow_top_left@2x.png b/Telegram/Resources/icons/calls/call_shadow_top_left@2x.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_top_left@2x.png rename to Telegram/Resources/icons/calls/call_shadow_top_left@2x.png diff --git a/Telegram/Resources/icons/call_shadow_top_left@3x.png b/Telegram/Resources/icons/calls/call_shadow_top_left@3x.png similarity index 100% rename from Telegram/Resources/icons/call_shadow_top_left@3x.png rename to Telegram/Resources/icons/calls/call_shadow_top_left@3x.png diff --git a/Telegram/Resources/icons/calls_close_main.png b/Telegram/Resources/icons/calls/calls_close_main.png similarity index 100% rename from Telegram/Resources/icons/calls_close_main.png rename to Telegram/Resources/icons/calls/calls_close_main.png diff --git a/Telegram/Resources/icons/calls_close_main@2x.png b/Telegram/Resources/icons/calls/calls_close_main@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_close_main@2x.png rename to Telegram/Resources/icons/calls/calls_close_main@2x.png diff --git a/Telegram/Resources/icons/calls_close_main@3x.png b/Telegram/Resources/icons/calls/calls_close_main@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_close_main@3x.png rename to Telegram/Resources/icons/calls/calls_close_main@3x.png diff --git a/Telegram/Resources/icons/calls_close_shadow.png b/Telegram/Resources/icons/calls/calls_close_shadow.png similarity index 100% rename from Telegram/Resources/icons/calls_close_shadow.png rename to Telegram/Resources/icons/calls/calls_close_shadow.png diff --git a/Telegram/Resources/icons/calls_close_shadow@2x.png b/Telegram/Resources/icons/calls/calls_close_shadow@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_close_shadow@2x.png rename to Telegram/Resources/icons/calls/calls_close_shadow@2x.png diff --git a/Telegram/Resources/icons/calls_close_shadow@3x.png b/Telegram/Resources/icons/calls/calls_close_shadow@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_close_shadow@3x.png rename to Telegram/Resources/icons/calls/calls_close_shadow@3x.png diff --git a/Telegram/Resources/icons/calls_maximize_main.png b/Telegram/Resources/icons/calls/calls_maximize_main.png similarity index 100% rename from Telegram/Resources/icons/calls_maximize_main.png rename to Telegram/Resources/icons/calls/calls_maximize_main.png diff --git a/Telegram/Resources/icons/calls_maximize_main@2x.png b/Telegram/Resources/icons/calls/calls_maximize_main@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_maximize_main@2x.png rename to Telegram/Resources/icons/calls/calls_maximize_main@2x.png diff --git a/Telegram/Resources/icons/calls_maximize_main@3x.png b/Telegram/Resources/icons/calls/calls_maximize_main@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_maximize_main@3x.png rename to Telegram/Resources/icons/calls/calls_maximize_main@3x.png diff --git a/Telegram/Resources/icons/calls_maximize_shadow.png b/Telegram/Resources/icons/calls/calls_maximize_shadow.png similarity index 100% rename from Telegram/Resources/icons/calls_maximize_shadow.png rename to Telegram/Resources/icons/calls/calls_maximize_shadow.png diff --git a/Telegram/Resources/icons/calls_maximize_shadow@2x.png b/Telegram/Resources/icons/calls/calls_maximize_shadow@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_maximize_shadow@2x.png rename to Telegram/Resources/icons/calls/calls_maximize_shadow@2x.png diff --git a/Telegram/Resources/icons/calls_maximize_shadow@3x.png b/Telegram/Resources/icons/calls/calls_maximize_shadow@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_maximize_shadow@3x.png rename to Telegram/Resources/icons/calls/calls_maximize_shadow@3x.png diff --git a/Telegram/Resources/icons/calls_minimize_main.png b/Telegram/Resources/icons/calls/calls_minimize_main.png similarity index 100% rename from Telegram/Resources/icons/calls_minimize_main.png rename to Telegram/Resources/icons/calls/calls_minimize_main.png diff --git a/Telegram/Resources/icons/calls_minimize_main@2x.png b/Telegram/Resources/icons/calls/calls_minimize_main@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_minimize_main@2x.png rename to Telegram/Resources/icons/calls/calls_minimize_main@2x.png diff --git a/Telegram/Resources/icons/calls_minimize_main@3x.png b/Telegram/Resources/icons/calls/calls_minimize_main@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_minimize_main@3x.png rename to Telegram/Resources/icons/calls/calls_minimize_main@3x.png diff --git a/Telegram/Resources/icons/calls_minimize_shadow.png b/Telegram/Resources/icons/calls/calls_minimize_shadow.png similarity index 100% rename from Telegram/Resources/icons/calls_minimize_shadow.png rename to Telegram/Resources/icons/calls/calls_minimize_shadow.png diff --git a/Telegram/Resources/icons/calls_minimize_shadow@2x.png b/Telegram/Resources/icons/calls/calls_minimize_shadow@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_minimize_shadow@2x.png rename to Telegram/Resources/icons/calls/calls_minimize_shadow@2x.png diff --git a/Telegram/Resources/icons/calls_minimize_shadow@3x.png b/Telegram/Resources/icons/calls/calls_minimize_shadow@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_minimize_shadow@3x.png rename to Telegram/Resources/icons/calls/calls_minimize_shadow@3x.png diff --git a/Telegram/Resources/icons/calls_mute_tooltip.png b/Telegram/Resources/icons/calls/calls_mute_tooltip.png similarity index 100% rename from Telegram/Resources/icons/calls_mute_tooltip.png rename to Telegram/Resources/icons/calls/calls_mute_tooltip.png diff --git a/Telegram/Resources/icons/calls_mute_tooltip@2x.png b/Telegram/Resources/icons/calls/calls_mute_tooltip@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_mute_tooltip@2x.png rename to Telegram/Resources/icons/calls/calls_mute_tooltip@2x.png diff --git a/Telegram/Resources/icons/calls_mute_tooltip@3x.png b/Telegram/Resources/icons/calls/calls_mute_tooltip@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_mute_tooltip@3x.png rename to Telegram/Resources/icons/calls/calls_mute_tooltip@3x.png diff --git a/Telegram/Resources/icons/calls_mute_userpic.png b/Telegram/Resources/icons/calls/calls_mute_userpic.png similarity index 100% rename from Telegram/Resources/icons/calls_mute_userpic.png rename to Telegram/Resources/icons/calls/calls_mute_userpic.png diff --git a/Telegram/Resources/icons/calls_mute_userpic@2x.png b/Telegram/Resources/icons/calls/calls_mute_userpic@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_mute_userpic@2x.png rename to Telegram/Resources/icons/calls/calls_mute_userpic@2x.png diff --git a/Telegram/Resources/icons/calls_mute_userpic@3x.png b/Telegram/Resources/icons/calls/calls_mute_userpic@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_mute_userpic@3x.png rename to Telegram/Resources/icons/calls/calls_mute_userpic@3x.png diff --git a/Telegram/Resources/icons/calls_restore_main.png b/Telegram/Resources/icons/calls/calls_restore_main.png similarity index 100% rename from Telegram/Resources/icons/calls_restore_main.png rename to Telegram/Resources/icons/calls/calls_restore_main.png diff --git a/Telegram/Resources/icons/calls_restore_main@2x.png b/Telegram/Resources/icons/calls/calls_restore_main@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_restore_main@2x.png rename to Telegram/Resources/icons/calls/calls_restore_main@2x.png diff --git a/Telegram/Resources/icons/calls_restore_main@3x.png b/Telegram/Resources/icons/calls/calls_restore_main@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_restore_main@3x.png rename to Telegram/Resources/icons/calls/calls_restore_main@3x.png diff --git a/Telegram/Resources/icons/calls_restore_shadow.png b/Telegram/Resources/icons/calls/calls_restore_shadow.png similarity index 100% rename from Telegram/Resources/icons/calls_restore_shadow.png rename to Telegram/Resources/icons/calls/calls_restore_shadow.png diff --git a/Telegram/Resources/icons/calls_restore_shadow@2x.png b/Telegram/Resources/icons/calls/calls_restore_shadow@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_restore_shadow@2x.png rename to Telegram/Resources/icons/calls/calls_restore_shadow@2x.png diff --git a/Telegram/Resources/icons/calls_restore_shadow@3x.png b/Telegram/Resources/icons/calls/calls_restore_shadow@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_restore_shadow@3x.png rename to Telegram/Resources/icons/calls/calls_restore_shadow@3x.png diff --git a/Telegram/Resources/icons/calls_shadow_controls.png b/Telegram/Resources/icons/calls/calls_shadow_controls.png similarity index 100% rename from Telegram/Resources/icons/calls_shadow_controls.png rename to Telegram/Resources/icons/calls/calls_shadow_controls.png diff --git a/Telegram/Resources/icons/calls_shadow_controls@2x.png b/Telegram/Resources/icons/calls/calls_shadow_controls@2x.png similarity index 100% rename from Telegram/Resources/icons/calls_shadow_controls@2x.png rename to Telegram/Resources/icons/calls/calls_shadow_controls@2x.png diff --git a/Telegram/Resources/icons/calls_shadow_controls@3x.png b/Telegram/Resources/icons/calls/calls_shadow_controls@3x.png similarity index 100% rename from Telegram/Resources/icons/calls_shadow_controls@3x.png rename to Telegram/Resources/icons/calls/calls_shadow_controls@3x.png diff --git a/Telegram/Resources/icons/calls/group_calls_invited.png b/Telegram/Resources/icons/calls/group_calls_invited.png new file mode 100644 index 000000000..cddca3ac9 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_invited.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_invited@2x.png b/Telegram/Resources/icons/calls/group_calls_invited@2x.png new file mode 100644 index 000000000..921e358d0 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_invited@2x.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_invited@3x.png b/Telegram/Resources/icons/calls/group_calls_invited@3x.png new file mode 100644 index 000000000..e70c2acad Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_invited@3x.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_muted.png b/Telegram/Resources/icons/calls/group_calls_muted.png new file mode 100644 index 000000000..1c63be9cd Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_muted.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_muted@2x.png b/Telegram/Resources/icons/calls/group_calls_muted@2x.png new file mode 100644 index 000000000..231ce293f Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_muted@2x.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_muted@3x.png b/Telegram/Resources/icons/calls/group_calls_muted@3x.png new file mode 100644 index 000000000..7eaf73940 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_muted@3x.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_unmuted.png b/Telegram/Resources/icons/calls/group_calls_unmuted.png new file mode 100644 index 000000000..213b54d8a Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_unmuted.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_unmuted@2x.png b/Telegram/Resources/icons/calls/group_calls_unmuted@2x.png new file mode 100644 index 000000000..a3f4b226a Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_unmuted@2x.png differ diff --git a/Telegram/Resources/icons/calls/group_calls_unmuted@3x.png b/Telegram/Resources/icons/calls/group_calls_unmuted@3x.png new file mode 100644 index 000000000..e351c3996 Binary files /dev/null and b/Telegram/Resources/icons/calls/group_calls_unmuted@3x.png differ diff --git a/Telegram/Resources/icons/send_control_record_active.png b/Telegram/Resources/icons/send_control_record_active.png new file mode 100644 index 000000000..af937a201 Binary files /dev/null and b/Telegram/Resources/icons/send_control_record_active.png differ diff --git a/Telegram/Resources/icons/send_control_record_active@2x.png b/Telegram/Resources/icons/send_control_record_active@2x.png new file mode 100644 index 000000000..f854549ac Binary files /dev/null and b/Telegram/Resources/icons/send_control_record_active@2x.png differ diff --git a/Telegram/Resources/icons/send_control_record_active@3x.png b/Telegram/Resources/icons/send_control_record_active@3x.png new file mode 100644 index 000000000..1406aadc5 Binary files /dev/null and b/Telegram/Resources/icons/send_control_record_active@3x.png differ diff --git a/Telegram/Resources/icons/title_button_close.png b/Telegram/Resources/icons/title_button_close.png deleted file mode 100644 index 22cfe520c..000000000 Binary files a/Telegram/Resources/icons/title_button_close.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_close@2x.png b/Telegram/Resources/icons/title_button_close@2x.png deleted file mode 100644 index e43b4d8a7..000000000 Binary files a/Telegram/Resources/icons/title_button_close@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_close@3x.png b/Telegram/Resources/icons/title_button_close@3x.png deleted file mode 100644 index 18c8496e1..000000000 Binary files a/Telegram/Resources/icons/title_button_close@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_maximize.png b/Telegram/Resources/icons/title_button_maximize.png deleted file mode 100644 index 2759b3f41..000000000 Binary files a/Telegram/Resources/icons/title_button_maximize.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_maximize@2x.png b/Telegram/Resources/icons/title_button_maximize@2x.png deleted file mode 100644 index 00daaae19..000000000 Binary files a/Telegram/Resources/icons/title_button_maximize@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_maximize@3x.png b/Telegram/Resources/icons/title_button_maximize@3x.png deleted file mode 100644 index 4d8073fd5..000000000 Binary files a/Telegram/Resources/icons/title_button_maximize@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_minimize.png b/Telegram/Resources/icons/title_button_minimize.png deleted file mode 100644 index 85934ddde..000000000 Binary files a/Telegram/Resources/icons/title_button_minimize.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_minimize@2x.png b/Telegram/Resources/icons/title_button_minimize@2x.png deleted file mode 100644 index d70a49734..000000000 Binary files a/Telegram/Resources/icons/title_button_minimize@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_minimize@3x.png b/Telegram/Resources/icons/title_button_minimize@3x.png deleted file mode 100644 index 93e36c271..000000000 Binary files a/Telegram/Resources/icons/title_button_minimize@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_restore.png b/Telegram/Resources/icons/title_button_restore.png deleted file mode 100644 index c9f276b51..000000000 Binary files a/Telegram/Resources/icons/title_button_restore.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_restore@2x.png b/Telegram/Resources/icons/title_button_restore@2x.png deleted file mode 100644 index 63c3e1b1e..000000000 Binary files a/Telegram/Resources/icons/title_button_restore@2x.png and /dev/null differ diff --git a/Telegram/Resources/icons/title_button_restore@3x.png b/Telegram/Resources/icons/title_button_restore@3x.png deleted file mode 100644 index 44b93232b..000000000 Binary files a/Telegram/Resources/icons/title_button_restore@3x.png and /dev/null differ diff --git a/Telegram/Resources/icons/top_bar_group_call.png b/Telegram/Resources/icons/top_bar_group_call.png new file mode 100644 index 000000000..b084f255b Binary files /dev/null and b/Telegram/Resources/icons/top_bar_group_call.png differ diff --git a/Telegram/Resources/icons/top_bar_group_call@2x.png b/Telegram/Resources/icons/top_bar_group_call@2x.png new file mode 100644 index 000000000..12534f224 Binary files /dev/null and b/Telegram/Resources/icons/top_bar_group_call@2x.png differ diff --git a/Telegram/Resources/icons/top_bar_group_call@3x.png b/Telegram/Resources/icons/top_bar_group_call@3x.png new file mode 100644 index 000000000..08df3a7fa Binary files /dev/null and b/Telegram/Resources/icons/top_bar_group_call@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_body.png b/Telegram/Resources/icons/voice_lock/record_lock_body.png new file mode 100644 index 000000000..e6e25e290 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_body.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_body@2x.png b/Telegram/Resources/icons/voice_lock/record_lock_body@2x.png new file mode 100644 index 000000000..2ac328fef Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_body@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_body@3x.png b/Telegram/Resources/icons/voice_lock/record_lock_body@3x.png new file mode 100644 index 000000000..0f12933eb Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_body@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_body_shadow.png b/Telegram/Resources/icons/voice_lock/record_lock_body_shadow.png new file mode 100644 index 000000000..884d7a81c Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_body_shadow.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_body_shadow@2x.png b/Telegram/Resources/icons/voice_lock/record_lock_body_shadow@2x.png new file mode 100644 index 000000000..f2173f10d Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_body_shadow@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_body_shadow@3x.png b/Telegram/Resources/icons/voice_lock/record_lock_body_shadow@3x.png new file mode 100644 index 000000000..cfefa592c Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_body_shadow@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_bottom.png b/Telegram/Resources/icons/voice_lock/record_lock_bottom.png new file mode 100644 index 000000000..882e925e6 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_bottom.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_bottom@2x.png b/Telegram/Resources/icons/voice_lock/record_lock_bottom@2x.png new file mode 100644 index 000000000..bad5a987a Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_bottom@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_bottom@3x.png b/Telegram/Resources/icons/voice_lock/record_lock_bottom@3x.png new file mode 100644 index 000000000..ce162a65d Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_bottom@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow.png b/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow.png new file mode 100644 index 000000000..b675e122c Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow@2x.png b/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow@2x.png new file mode 100644 index 000000000..073d169d5 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow@3x.png b/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow@3x.png new file mode 100644 index 000000000..3da360933 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_bottom_shadow@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_top.png b/Telegram/Resources/icons/voice_lock/record_lock_top.png new file mode 100644 index 000000000..198615edf Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_top.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_top@2x.png b/Telegram/Resources/icons/voice_lock/record_lock_top@2x.png new file mode 100644 index 000000000..06ce004e2 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_top@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_top@3x.png b/Telegram/Resources/icons/voice_lock/record_lock_top@3x.png new file mode 100644 index 000000000..00998824e Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_top@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_top_shadow.png b/Telegram/Resources/icons/voice_lock/record_lock_top_shadow.png new file mode 100644 index 000000000..69bf37e51 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_top_shadow.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_top_shadow@2x.png b/Telegram/Resources/icons/voice_lock/record_lock_top_shadow@2x.png new file mode 100644 index 000000000..f35d3240c Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_top_shadow@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/record_lock_top_shadow@3x.png b/Telegram/Resources/icons/voice_lock/record_lock_top_shadow@3x.png new file mode 100644 index 000000000..96660d2d2 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/record_lock_top_shadow@3x.png differ diff --git a/Telegram/Resources/icons/voice_lock/voice_arrow.png b/Telegram/Resources/icons/voice_lock/voice_arrow.png new file mode 100644 index 000000000..e42af30b0 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/voice_arrow.png differ diff --git a/Telegram/Resources/icons/voice_lock/voice_arrow@2x.png b/Telegram/Resources/icons/voice_lock/voice_arrow@2x.png new file mode 100644 index 000000000..d8196558a Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/voice_arrow@2x.png differ diff --git a/Telegram/Resources/icons/voice_lock/voice_arrow@3x.png b/Telegram/Resources/icons/voice_lock/voice_arrow@3x.png new file mode 100644 index 000000000..83b3cfb88 Binary files /dev/null and b/Telegram/Resources/icons/voice_lock/voice_arrow@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3b04c89c7..b82d366e5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; @@ -1061,6 +1062,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_duration_minsec_seconds#other" = "{count} sec"; "lng_duration_minutes_seconds" = "{minutes_count} {seconds_count}"; +"lng_action_invite_user" = "{from} invited {user} to {chat}"; +"lng_action_invite_users_many" = "{from} invited {users} to {chat}"; +"lng_action_invite_user_chat" = "the voice chat"; +"lng_action_invite_users_and_one" = "{accumulated}, {user}"; +"lng_action_invite_users_and_last" = "{accumulated} and {user}"; +"lng_action_group_call_started" = "{from} started {chat}"; +"lng_action_group_call_started_chat" = "a voice chat"; +"lng_action_group_call_finished" = "Voice chat finished ({duration})"; "lng_action_add_user" = "{from} added {user}"; "lng_action_add_users_many" = "{from} added {users}"; "lng_action_add_users_and_one" = "{accumulated}, {user}"; @@ -1135,13 +1144,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_migrate_body" = "To add more members, you can upgrade your group to a supergroup."; "lng_profile_migrate_learn_more" = "Learn more »"; "lng_profile_migrate_button" = "Upgrade to supergroup"; -"lng_profile_convert_title" = "Convert to supergroup"; -"lng_profile_convert_feature1" = "— New members see the full message history"; -"lng_profile_convert_feature2" = "— Messages are deleted for all members"; -"lng_profile_convert_feature3" = "— Admins can pin important messages"; -"lng_profile_convert_feature4" = "— Creator can set a public link for the group"; -"lng_profile_convert_warning" = "{bold_start}Note:{bold_end} This action can not be undone"; -"lng_profile_convert_confirm" = "Convert"; "lng_profile_add_more_after_create" = "You will be able to add more members after you create the group."; "lng_channel_not_accessible" = "Sorry, this channel is not accessible."; @@ -1341,6 +1343,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_sure" = "Are you sure you want to stop recording and discard your voice message?"; +"lng_record_listen_cancel_sure" = "Are you sure you want to discard your recorded 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"; @@ -1469,6 +1474,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_game_tag" = "Game"; "lng_context_view_profile" = "View profile"; +"lng_context_send_message" = "Send message"; "lng_context_view_group" = "View group info"; "lng_context_view_channel" = "View channel info"; //"lng_context_view_feed_info" = "View feed info"; @@ -1493,6 +1499,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_add_to_group" = "Add to group"; "lng_context_copy_link" = "Copy Link"; +"lng_context_copy_message_link" = "Copy Message Link"; "lng_context_copy_post_link" = "Copy Post Link"; "lng_context_copy_email" = "Copy Email Address"; "lng_context_copy_hashtag" = "Copy Hashtag"; @@ -1772,6 +1779,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_call_error_audio_io" = "There seems to be a problem with audio playback on your computer. Please make sure that your computer's speakers and microphone are working and try again."; "lng_call_bar_hangup" = "End call"; +"lng_call_leave_to_other_sure" = "Do you want to end your active call and join a voice chat in this group?"; "lng_call_box_title" = "Calls"; "lng_call_box_about" = "You haven't made any Telegram calls yet."; @@ -1808,6 +1816,67 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_call_microphone_off" = "{user}'s microphone is off"; +"lng_group_call_title" = "Voice Chat"; +"lng_group_call_active" = "speaking"; +"lng_group_call_inactive" = "listening"; +"lng_group_call_settings" = "Settings"; +"lng_group_call_unmute" = "Unmute"; +"lng_group_call_unmute_sub" = "or hold spacebar to talk"; +"lng_group_call_you_are_live" = "You are Live"; +"lng_group_call_force_muted" = "Muted by admin"; +"lng_group_call_force_muted_sub" = "You are in Listen Only mode"; +"lng_group_call_connecting" = "Connecting..."; +"lng_group_call_leave" = "Leave"; +"lng_group_call_leave_title" = "Leave voice chat"; +"lng_group_call_leave_sure" = "Are you sure you want to leave this voice chat?"; +"lng_group_call_leave_to_other_sure" = "Do you want to leave your active voice chat and join a voice chat in this group?"; +"lng_group_call_create_sure" = "Do you really want to start a voice chat in this group?"; +"lng_group_call_also_end" = "End voice chat"; +"lng_group_call_settings_title" = "Settings"; +"lng_group_call_invite" = "Invite Member"; +"lng_group_call_invited_status" = "invited"; +"lng_group_call_invite_title" = "Invite members"; +"lng_group_call_invite_button" = "Invite"; +"lng_group_call_add_to_group_one" = "{user} isn't a member of «{group}» yet. Add them to the group?"; +"lng_group_call_add_to_group_some" = "Some of those users aren't members of «{group}» yet. Add them to the group?"; +"lng_group_call_add_to_group_all" = "Those users aren't members of «{group}» yet. Add them to the group?"; +"lng_group_call_invite_members" = "Group members"; +"lng_group_call_invite_search_results" = "Search results"; +"lng_group_call_new_muted" = "Mute new members"; +"lng_group_call_speakers" = "Speakers"; +"lng_group_call_microphone" = "Microphone"; +"lng_group_call_push_to_talk" = "Push to Talk Shortcut"; +"lng_group_call_ptt_shortcut" = "Edit Shortcut"; +"lng_group_call_ptt_recording" = "Stop Recording"; +"lng_group_call_ptt_delay_ms" = "{amount} ms"; +"lng_group_call_ptt_delay_s" = "{amount}s"; +"lng_group_call_ptt_delay" = "Push to Talk release delay: {delay}"; +"lng_group_call_share" = "Share Invite Link"; +"lng_group_call_end" = "End Voice Chat"; +"lng_group_call_join" = "Join"; +"lng_group_call_invite_done_user" = "You invited {user} to the voice chat."; +"lng_group_call_invite_done_many#one" = "You invited **{count} member** to the voice chat."; +"lng_group_call_invite_done_many#other" = "You invited **{count} members** to the voice chat."; +"lng_group_call_no_members" = "click to join"; +"lng_group_call_members#one" = "{count} participant"; +"lng_group_call_members#other" = "{count} participants"; +"lng_group_call_no_anonymous" = "Sorry, anonymous group admins can't join voice chats."; +"lng_group_call_too_many" = "Sorry, there are too many members in this voice chat. Please try again later."; +"lng_group_call_context_mute" = "Mute"; +"lng_group_call_context_unmute" = "Allow to speak"; +"lng_group_call_duration_days#one" = "{count} day"; +"lng_group_call_duration_days#other" = "{count} days"; +"lng_group_call_duration_hours#one" = "{count} hour"; +"lng_group_call_duration_hours#other" = "{count} hours"; +"lng_group_call_duration_minutes#one" = "{count} minute"; +"lng_group_call_duration_minutes#other" = "{count} minutes"; +"lng_group_call_duration_seconds#one" = "{count} second"; +"lng_group_call_duration_seconds#other" = "{count} seconds"; +"lng_group_call_mac_access" = "Telegram Desktop does not have access to system wide keyboard input required for Push to Talk."; +"lng_group_call_mac_input" = "Please allow **Input Monitoring** for Telegram in Privacy Settings."; +"lng_group_call_mac_accessibility" = "Please allow **Accessibility** for Telegram in Privacy Settings.\n\nApp restart may be required."; +"lng_group_call_mac_settings" = "Open Settings"; + "lng_no_mic_permission" = "Telegram needs access to your microphone so that you can make calls and record voice messages."; "lng_player_message_today" = "Today at {time}"; @@ -1860,6 +1929,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_group_invite_link" = "Invite users via link"; "lng_rights_group_invite" = "Add users"; "lng_rights_group_pin" = "Pin messages"; +"lng_rights_group_manage_calls" = "Manage voice chats"; "lng_rights_group_delete" = "Delete messages"; "lng_rights_group_anonymous" = "Remain Anonymous"; "lng_rights_add_admins" = "Add new admins"; @@ -1942,6 +2012,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_filter_messages_deleted" = "Deleted messages"; "lng_admin_log_filter_messages_edited" = "Edited messages"; "lng_admin_log_filter_messages_pinned" = "Pinned messages"; +"lng_admin_log_filter_voice_chats" = "Voice chat"; "lng_admin_log_filter_members_removed" = "Leaving members"; "lng_admin_log_filter_all_admins" = "All users and admins"; "lng_admin_log_about" = "What is this?"; @@ -2007,6 +2078,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_removed_location_chat" = "{from} removed the group location"; "lng_admin_log_changed_slow_mode" = "{from} changed slow mode to {duration}"; "lng_admin_log_removed_slow_mode" = "{from} disabled slow mode"; +"lng_admin_log_started_group_call" = "{from} started a new voice chat"; +"lng_admin_log_discarded_group_call" = "{from} discarded a voice chat"; +"lng_admin_log_muted_participant" = "{from} muted {user} in a voice chat"; +"lng_admin_log_unmuted_participant" = "{from} unmuted {user} in a voice chat"; +"lng_admin_log_allowed_unmute_self" = "{from} allowed new voice chat members to speak"; +"lng_admin_log_disallowed_unmute_self" = "{from} started muting new voice chat members"; "lng_admin_log_user_with_username" = "{name} ({mention})"; "lng_admin_log_restricted_forever" = "indefinitely"; "lng_admin_log_restricted_until" = "until {date}"; @@ -2024,6 +2101,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_admin_invite_users" = "Add members"; "lng_admin_log_admin_invite_link" = "Invite users via link"; "lng_admin_log_admin_pin_messages" = "Pin messages"; +"lng_admin_log_admin_manage_calls" = "Manage voice chats"; "lng_admin_log_admin_add_admins" = "Add new admins"; // #feed diff --git a/Telegram/Resources/qrc/telegram/sounds.qrc b/Telegram/Resources/qrc/telegram/sounds.qrc index 6135e0050..69d1427e5 100644 --- a/Telegram/Resources/qrc/telegram/sounds.qrc +++ b/Telegram/Resources/qrc/telegram/sounds.qrc @@ -6,5 +6,8 @@ ../../sounds/call_end.mp3 ../../sounds/call_incoming.mp3 ../../sounds/call_outgoing.mp3 + ../../sounds/group_call_start.mp3 + ../../sounds/group_call_connect.mp3 + ../../sounds/group_call_end.mp3 diff --git a/Telegram/Resources/sounds/group_call_connect.mp3 b/Telegram/Resources/sounds/group_call_connect.mp3 new file mode 100644 index 000000000..4e0f60b4f Binary files /dev/null and b/Telegram/Resources/sounds/group_call_connect.mp3 differ diff --git a/Telegram/Resources/sounds/group_call_end.mp3 b/Telegram/Resources/sounds/group_call_end.mp3 new file mode 100644 index 000000000..c03390958 Binary files /dev/null and b/Telegram/Resources/sounds/group_call_end.mp3 differ diff --git a/Telegram/Resources/sounds/group_call_start.mp3 b/Telegram/Resources/sounds/group_call_start.mp3 new file mode 100644 index 000000000..527959c9d Binary files /dev/null and b/Telegram/Resources/sounds/group_call_start.mp3 differ diff --git a/Telegram/Resources/tl/api.tl b/Telegram/Resources/tl/api.tl index 06f21cc28..ce5735679 100644 --- a/Telegram/Resources/tl/api.tl +++ b/Telegram/Resources/tl/api.tl @@ -63,7 +63,7 @@ inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMe inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia; inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia; +inputMediaDocument#33473058 flags:# id:InputDocument ttl_seconds:flags.0?int query:flags.1?string = InputMedia; inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia; @@ -122,13 +122,13 @@ userStatusLastWeek#7bf09fc = UserStatus; userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#9ba2d800 id:int = Chat; -chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; +chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#7328bdb id:int title:string = Chat; -channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; +channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; -chatFull#1b7c9db3 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull; -channelFull#f0e6672a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int = ChatFull; +chatFull#dc8c181 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall = ChatFull; +channelFull#ef3a6acd flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall = ChatFull; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant; @@ -182,6 +182,8 @@ messageActionSecureValuesSentMe#1b287353 values:Vector credentials: messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; messageActionContactSignUp#f3f25f76 = MessageAction; messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction; +messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction; +messageActionInviteToGroupCall#76b9f11a call:InputGroupCall users:Vector = MessageAction; dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -194,6 +196,7 @@ photoSize#77bfb61b type:string location:FileLocation w:int h:int size:int = Phot photoCachedSize#e9a734fa type:string location:FileLocation w:int h:int bytes:bytes = PhotoSize; photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; photoSizeProgressive#5aa86a51 type:string location:FileLocation w:int h:int sizes:Vector = PhotoSize; +photoPathSize#d8214d41 type:string bytes:bytes = PhotoSize; geoPointEmpty#1117dd5f = GeoPoint; geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint; @@ -312,7 +315,7 @@ updateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update; updateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true order:Vector = Update; updateStickerSets#43ae3dec = Update; updateSavedGifs#9375341e = Update; -updateBotInlineQuery#54826690 flags:# query_id:long user_id:int query:string geo:flags.0?GeoPoint offset:string = Update; +updateBotInlineQuery#3f2038db flags:# query_id:long user_id:int query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update; updateBotInlineSend#e48f964 flags:# user_id:int query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update; updateBotCallbackQuery#e73547e1 flags:# query_id:long user_id:int peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; @@ -362,6 +365,9 @@ updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; updateChannelUserTyping#ff2abe9f flags:# channel_id:int top_msg_id:flags.0?int user_id:int action:SendMessageAction = Update; updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; updatePinnedChannelMessages#8588878b flags:# pinned:flags.0?true channel_id:int messages:Vector pts:int pts_count:int = Update; +updateChat#1330a196 chat_id:int = Update; +updateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector version:int = Update; +updateGroupCall#a45eb99b chat_id:int call:GroupCall = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -448,6 +454,7 @@ sendMessageChooseContactAction#628cbc6f = SendMessageAction; sendMessageGamePlayAction#dd6a8f48 = SendMessageAction; sendMessageRecordRoundAction#88f27fbc = SendMessageAction; sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; +speakingInGroupCallAction#d92c2285 = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -541,7 +548,7 @@ inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; -stickerSet#eeb46f27 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumb:flags.4?PhotoSize thumb_dc_id:flags.4?int count:int hash:int = StickerSet; +stickerSet#40e237a8 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int count:int hash:int = StickerSet; messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; @@ -873,12 +880,17 @@ channelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEve channelAdminLogEventActionChangeLinkedChat#a26f881b prev_value:int new_value:int = ChannelAdminLogEventAction; channelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction; channelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction; +channelAdminLogEventActionStartGroupCall#23209745 call:InputGroupCall = ChannelAdminLogEventAction; +channelAdminLogEventActionDiscardGroupCall#db9f9140 call:InputGroupCall = ChannelAdminLogEventAction; +channelAdminLogEventActionParticipantMute#f92424d2 participant:GroupCallParticipant = ChannelAdminLogEventAction; +channelAdminLogEventActionParticipantUnmute#e64429c0 participant:GroupCallParticipant = ChannelAdminLogEventAction; +channelAdminLogEventActionToggleGroupCallSetting#56d6a247 join_muted:Bool = ChannelAdminLogEventAction; channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent; channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; -channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true = ChannelAdminLogEventsFilter; +channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true = ChannelAdminLogEventsFilter; popularContact#5ce14175 client_id:long importers:int = PopularContact; @@ -902,6 +914,7 @@ account.webAuthorizations#ed56c9fc authorizations:Vector users inputMessageID#a676a322 id:int = InputMessage; inputMessageReplyTo#bad88395 id:int = InputMessage; inputMessagePinned#86872538 = InputMessage; +inputMessageCallbackQuery#acfa1a7e id:int query_id:long = InputMessage; inputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer; inputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer; @@ -1035,7 +1048,7 @@ chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true = ChatAdminRights; +chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true = ChatAdminRights; chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true until_date:int = ChatBannedRights; @@ -1177,6 +1190,23 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; +groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; +groupCall#55903081 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true id:long access_hash:long participants_count:int params:flags.0?DataJSON version:int = GroupCall; + +inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; + +groupCallParticipant#56b087c9 flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true user_id:int date:int active_date:flags.3?int source:int = GroupCallParticipant; + +phone.groupCall#66ab0bfc call:GroupCall participants:Vector participants_next_offset:string users:Vector = phone.GroupCall; + +phone.groupParticipants#9cfeb92d count:int participants:Vector next_offset:string users:Vector version:int = phone.GroupParticipants; + +inlineQueryPeerTypeSameBotPM#3081ed9d = InlineQueryPeerType; +inlineQueryPeerTypePM#833c0fac = InlineQueryPeerType; +inlineQueryPeerTypeChat#d766c50a = InlineQueryPeerType; +inlineQueryPeerTypeMegagroup#5ec4be43 = InlineQueryPeerType; +inlineQueryPeerTypeBroadcast#6334ee9a = InlineQueryPeerType; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1531,6 +1561,16 @@ phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall durati phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates; phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; +phone.createGroupCall#bd3dabe0 peer:InputPeer random_id:int = Updates; +phone.joinGroupCall#5f9c8e62 flags:# muted:flags.0?true call:InputGroupCall params:DataJSON = Updates; +phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; +phone.editGroupCallMember#63146ae4 flags:# muted:flags.0?true call:InputGroupCall user_id:InputUser = Updates; +phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; +phone.discardGroupCall#7a777135 call:InputGroupCall = Updates; +phone.toggleGroupCallSettings#74bbb43d flags:# call:InputGroupCall join_muted:flags.0?Bool = Updates; +phone.getGroupCall#c7cb017 call:InputGroupCall = phone.GroupCall; +phone.getGroupParticipants#c9f1d285 call:InputGroupCall ids:Vector sources:Vector offset:string limit:int = phone.GroupParticipants; +phone.checkGroupCall#b74a7bea call:InputGroupCall source:int = Bool; langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; @@ -1547,4 +1587,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 120 +// LAYER 122 diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index de007958c..93884509d 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -9,7 +9,7 @@ + Version="2.5.1.0" /> Telegram Desktop Telegram FZ-LLC diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 3f9b139bd..599fa1a7e 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 2,4,7,0 - PRODUCTVERSION 2,4,7,0 + FILEVERSION 2,5,1,0 + PRODUCTVERSION 2,5,1,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.7.0" + VALUE "FileVersion", "2.5.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2020" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "2.4.7.0" + VALUE "ProductVersion", "2.5.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index ca046b74e..d91d26525 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 2,4,7,0 - PRODUCTVERSION 2,4,7,0 + FILEVERSION 2,5,1,0 + PRODUCTVERSION 2,5,1,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.7.0" + VALUE "FileVersion", "2.5.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2020" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "2.4.7.0" + VALUE "ProductVersion", "2.5.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_authorizations.cpp b/Telegram/SourceFiles/api/api_authorizations.cpp index f1c988476..ea8bef14a 100644 --- a/Telegram/SourceFiles/api/api_authorizations.cpp +++ b/Telegram/SourceFiles/api/api_authorizations.cpp @@ -125,13 +125,20 @@ void Authorizations::requestTerminate( Fn &&done, Fn &&fail, std::optional hash) { - auto request = hash - ? MTPaccount_ResetAuthorization(MTP_long(*hash)) - : MTPaccount_ResetAuthorization(); - _api.request(std::move(request)) - .done(std::move(done)) - .fail(std::move(fail)) - .send(); + const auto send = [&](auto request) { + _api.request( + std::move(request) + ).done( + std::move(done) + ).fail( + std::move(fail) + ).send(); + }; + if (hash) { + send(MTPaccount_ResetAuthorization(MTP_long(*hash))); + } else { + send(MTPauth_ResetAuthorizations()); + } } Authorizations::List Authorizations::list() const { diff --git a/Telegram/SourceFiles/api/api_send_progress.cpp b/Telegram/SourceFiles/api/api_send_progress.cpp index 6def1f9cd..c3436f7ab 100644 --- a/Telegram/SourceFiles/api/api_send_progress.cpp +++ b/Telegram/SourceFiles/api/api_send_progress.cpp @@ -19,7 +19,8 @@ namespace Api { namespace { constexpr auto kCancelTypingActionTimeout = crl::time(5000); -constexpr auto kSetMyActionForMs = 10 * crl::time(1000); +constexpr auto kSendMySpeakingInterval = 3 * crl::time(1000); +constexpr auto kSendMyTypingInterval = 5 * crl::time(1000); constexpr auto kSendTypingsToOfflineFor = TimeId(30); } // namespace @@ -82,12 +83,15 @@ bool SendProgressManager::updated(const Key &key, bool doing) { const auto now = crl::now(); const auto i = _updated.find(key); if (doing) { + const auto sendEach = (key.type == SendProgressType::Speaking) + ? kSendMySpeakingInterval + : kSendMyTypingInterval; if (i == end(_updated)) { - _updated.emplace(key, now + kSetMyActionForMs); - } else if (i->second > now + (kSetMyActionForMs / 2)) { + _updated.emplace(key, now + 2 * sendEach); + } else if (i->second > now + sendEach) { return false; } else { - i->second = now + kSetMyActionForMs; + i->second = now + 2 * sendEach; } } else { if (i == end(_updated)) { @@ -121,6 +125,7 @@ void SendProgressManager::send(const Key &key, int progress) { case Type::ChooseLocation: return MTP_sendMessageGeoLocationAction(); case Type::ChooseContact: return MTP_sendMessageChooseContactAction(); case Type::PlayGame: return MTP_sendMessageGamePlayAction(); + case Type::Speaking: return MTP_speakingInGroupCallAction(); default: return MTP_sendMessageTypingAction(); } }(); diff --git a/Telegram/SourceFiles/api/api_send_progress.h b/Telegram/SourceFiles/api/api_send_progress.h index 22bed6ac1..511e25cc1 100644 --- a/Telegram/SourceFiles/api/api_send_progress.h +++ b/Telegram/SourceFiles/api/api_send_progress.h @@ -31,6 +31,7 @@ enum class SendProgressType { ChooseLocation, ChooseContact, PlayGame, + Speaking, }; struct SendProgress { diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 4ad6bc49a..851decd8a 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -89,8 +89,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; @@ -193,7 +192,8 @@ void SendExistingDocument( return MTP_inputMediaDocument( MTP_flags(0), document->mtpInput(), - MTPint()); + MTPint(), // ttl_seconds + MTPstring()); // query }; SendExistingMedia( std::move(message), @@ -277,8 +277,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; @@ -419,7 +418,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; diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 4b853a1e2..188e06a22 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat_filters.h" #include "data/data_cloud_themes.h" +#include "data/data_group_call.h" #include "data/data_drafts.h" #include "data/data_histories.h" #include "data/data_folder.h" @@ -232,6 +233,27 @@ Updates::Updates(not_null session) )).done([=](const MTPupdates_State &result) { stateDone(result); }).send(); + + using namespace rpl::mappers; + base::ObservableViewer( + api().fullPeerUpdated() + ) | rpl::filter([](not_null peer) { + return peer->isChat() || peer->isMegagroup(); + }) | rpl::start_with_next([=](not_null peer) { + if (const auto users = _pendingSpeakingCallMembers.take(peer)) { + if (const auto call = peer->groupCall()) { + for (const auto [userId, when] : *users) { + call->applyActiveUpdate( + userId, + Data::LastSpokeTimes{ + .anything = when, + .voice = when + }, + peer->owner().userLoaded(userId)); + } + } + } + }, _lifetime); } Main::Session &Updates::session() const { @@ -791,6 +813,10 @@ void Updates::mtpUpdateReceived(const MTPUpdates &updates) { } } +int32 Updates::pts() const { + return _ptsWaiter.current(); +} + void Updates::updateOnline() { updateOnline(false); } @@ -886,6 +912,58 @@ bool Updates::isQuitPrevent() { updateOnline(); return true; } +void Updates::handleSendActionUpdate( + PeerId peerId, + MsgId rootId, + UserId userId, + const MTPSendMessageAction &action) { + const auto history = session().data().historyLoaded(peerId); + if (!history) { + return; + } + const auto peer = history->peer; + const auto user = (userId == session().userId()) + ? session().user().get() + : session().data().userLoaded(userId); + const auto isSpeakingInCall = (action.type() + == mtpc_speakingInGroupCallAction); + if (isSpeakingInCall) { + if (!peer->isChat() && !peer->isChannel()) { + return; + } + const auto call = peer->groupCall(); + const auto now = crl::now(); + if (call) { + call->applyActiveUpdate( + userId, + Data::LastSpokeTimes{ .anything = now, .voice = now }, + user); + } else { + const auto chat = peer->asChat(); + const auto channel = peer->asChannel(); + const auto active = chat + ? (chat->flags() & MTPDchat::Flag::f_call_active) + : (channel->flags() & MTPDchannel::Flag::f_call_active); + if (active) { + _pendingSpeakingCallMembers.emplace( + peer).first->second[userId] = now; + session().api().requestFullPeer(peer); + } + } + } + if (!user || user->isSelf()) { + return; + } + const auto when = requestingDifference() + ? 0 + : base::unixtime::now(); + session().data().registerSendAction( + history, + rootId, + user, + action, + when); +} void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { switch (updates.type()) { @@ -1576,57 +1654,29 @@ void Updates::feedUpdate(const MTPUpdate &update) { case mtpc_updateUserTyping: { auto &d = update.c_updateUserTyping(); - const auto userId = peerFromUser(d.vuser_id()); - const auto history = session().data().historyLoaded(userId); - const auto user = session().data().userLoaded(d.vuser_id().v); - if (history && user) { - const auto when = requestingDifference() ? 0 : base::unixtime::now(); - session().data().registerSendAction( - history, - MsgId(), - user, - d.vaction(), - when); - } + handleSendActionUpdate( + peerFromUser(d.vuser_id()), + 0, + d.vuser_id().v, + d.vaction()); } break; case mtpc_updateChatUserTyping: { auto &d = update.c_updateChatUserTyping(); - const auto history = session().data().historyLoaded( - peerFromChat(d.vchat_id())); - const auto user = (d.vuser_id().v == session().userId()) - ? nullptr - : session().data().userLoaded(d.vuser_id().v); - if (history && user) { - const auto when = requestingDifference() ? 0 : base::unixtime::now(); - session().data().registerSendAction( - history, - MsgId(), - user, - d.vaction(), - when); - } + handleSendActionUpdate( + peerFromChat(d.vchat_id()), + 0, + d.vuser_id().v, + d.vaction()); } break; case mtpc_updateChannelUserTyping: { const auto &d = update.c_updateChannelUserTyping(); - const auto history = session().data().historyLoaded( - peerFromChannel(d.vchannel_id())); - const auto user = (d.vuser_id().v == session().userId()) - ? nullptr - : session().data().userLoaded(d.vuser_id().v); - if (history && user) { - const auto when = requestingDifference() - ? 0 - : base::unixtime::now(); - const auto rootId = d.vtop_msg_id().value_or_empty(); - session().data().registerSendAction( - history, - rootId, - user, - d.vaction(), - when); - } + handleSendActionUpdate( + peerFromChannel(d.vchannel_id()), + d.vtop_msg_id().value_or_empty(), + d.vuser_id().v, + d.vaction()); } break; case mtpc_updateChatParticipants: { @@ -1792,7 +1842,9 @@ void Updates::feedUpdate(const MTPUpdate &update) { } break; case mtpc_updatePhoneCall: - case mtpc_updatePhoneCallSignalingData: { + case mtpc_updatePhoneCallSignalingData: + case mtpc_updateGroupCallParticipants: + case mtpc_updateGroupCall: { Core::App().calls().handleUpdate(&session(), update); } break; diff --git a/Telegram/SourceFiles/api/api_updates.h b/Telegram/SourceFiles/api/api_updates.h index 98bd13d60..e6ca8502c 100644 --- a/Telegram/SourceFiles/api/api_updates.h +++ b/Telegram/SourceFiles/api/api_updates.h @@ -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(); @@ -120,6 +122,12 @@ private: base::flat_map, crl::time> &whenMap, crl::time &curTime); + void handleSendActionUpdate( + PeerId peerId, + MsgId rootId, + UserId userId, + const MTPSendMessageAction &action); + const not_null _session; int32 _updatesDate = 0; @@ -158,6 +166,9 @@ private: bool _handlingChannelDifference = false; base::flat_map _activeChats; + base::flat_map< + not_null, + base::flat_map> _pendingSpeakingCallMembers; mtpRequestId _onlineRequest = 0; base::Timer _idleFinishTimer; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 7c540db2c..6a8870b11 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "base/openssl_help.h" #include "base/unixtime.h" +#include "base/qt_adapters.h" #include "base/call_delayed.h" #include "lang/lang_keys.h" #include "mainwindow.h" @@ -2440,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); } } } @@ -3277,7 +3276,7 @@ void ApiWrap::requestMessageAfterDate( // So we request a message with offset_date = desired_date - 1 and add_offset = -1. // This should give us the first message with date >= desired_date. auto offsetId = 0; - auto offsetDate = static_cast(QDateTime(date).toTime_t()) - 1; + auto offsetDate = static_cast(base::QDateToDateTime(date).toTime_t()) - 1; auto addOffset = -1; auto limit = 1; auto maxId = 0; @@ -3365,7 +3364,7 @@ void ApiWrap::jumpToHistoryDate(not_null peer, const QDate &date) { // const QDate &date, // Callback &&callback) { // const auto offsetId = 0; -// const auto offsetDate = static_cast(QDateTime(date).toTime_t()); +// const auto offsetDate = static_cast(base::QDateToDateTime(date).toTime_t()); // const auto addOffset = -2; // const auto limit = 1; // const auto hash = 0; @@ -3413,7 +3412,7 @@ void ApiWrap::jumpToHistoryDate(not_null peer, const QDate &date) { // requestMessageAfterDate(feed, date, [=](Data::MessagePosition result) { // Ui::hideLayer(); // App::wnd()->sessionController()->showSection( -// HistoryFeed::Memento(feed, result)); +// std::make_shared(feed, result)); // }); //} @@ -3459,7 +3458,8 @@ void ApiWrap::checkForUnreadMentions( void ApiWrap::addChatParticipants( not_null peer, - const std::vector> &users) { + const std::vector> &users, + Fn done) { if (const auto chat = peer->asChat()) { for (const auto user : users) { request(MTPmessages_AddChatUser( @@ -3468,8 +3468,10 @@ void ApiWrap::addChatParticipants( MTP_int(kForwardMessagesOnAdd) )).done([=](const MTPUpdates &result) { applyUpdates(result); + if (done) done(true); }).fail([=](const RPCError &error) { ShowAddParticipantsError(error.type(), peer, { 1, user }); + if (done) done(false); }).afterDelay(crl::time(5)).send(); } } else if (const auto channel = peer->asChannel()) { @@ -3481,14 +3483,17 @@ void ApiWrap::addChatParticipants( auto list = QVector(); list.reserve(qMin(int(users.size()), int(kMaxUsersPerInvite))); const auto send = [&] { + const auto callback = base::take(done); request(MTPchannels_InviteToChannel( channel->inputChannel, MTP_vector(list) )).done([=](const MTPUpdates &result) { applyUpdates(result); requestParticipantsCountDelayed(channel); + if (callback) callback(true); }).fail([=](const RPCError &error) { ShowAddParticipantsError(error.type(), peer, users); + if (callback) callback(false); }).afterDelay(crl::time(5)).send(); }; for (const auto user : users) { @@ -3956,8 +3961,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(); @@ -4013,6 +4017,7 @@ void ApiWrap::forwardMessages( ++shared->requestsLeft; } const auto requestType = Data::Histories::RequestType::Send; + const auto idsCopy = localIds; histories.sendRequest(history, requestType, [=](Fn finish) { history->sendRequestId = request(MTPmessages_ForwardMessages( MTP_flags(sendFlags), @@ -4027,9 +4032,9 @@ void ApiWrap::forwardMessages( shared->callback(); } finish(); - }).fail([=, ids = localIds](const RPCError &error) { - if (ids) { - for (const auto &[randomId, itemId] : *ids) { + }).fail([=](const RPCError &error) { + if (idsCopy) { + for (const auto &[randomId, itemId] : *idsCopy) { sendMessageFail(error, peer, randomId, itemId); } } else { @@ -4474,11 +4479,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( @@ -4696,8 +4697,7 @@ void ApiWrap::sendMessage( 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; @@ -4843,8 +4843,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; @@ -4972,7 +4971,8 @@ void ApiWrap::uploadAlbumMedia( fields.vid(), fields.vaccess_hash(), fields.vfile_reference()), - MTP_int(data.vttl_seconds().value_or_empty())); + MTP_int(data.vttl_seconds().value_or_empty()), + MTPstring()); // query sendAlbumWithUploaded(item, groupId, media); } break; } @@ -5010,7 +5010,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() @@ -5117,7 +5117,7 @@ void ApiWrap::sendAlbumIfReady(not_null 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 @@ -5155,10 +5155,6 @@ void ApiWrap::sendAlbumIfReady(not_null 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); } @@ -5588,8 +5584,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; } @@ -5612,6 +5607,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 { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 920064100..6dc68ce61 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -366,7 +366,8 @@ public: Fn callbackNotModified = nullptr); void addChatParticipants( not_null peer, - const std::vector> &users); + const std::vector> &users, + Fn done = nullptr); rpl::producer sendActions() const { return _sendActions.events(); diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index c0419a5ef..599edfc10 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -632,7 +632,7 @@ void GroupInfoBox::submit() { not_null box) { auto create = [box, title, weak] { if (weak) { - auto rows = box->peerListCollectSelectedRows(); + auto rows = box->collectSelectedRows(); if (!rows.empty()) { weak->createGroup(box, title, rows); } @@ -643,7 +643,8 @@ void GroupInfoBox::submit() { }; Ui::show( Box( - std::make_unique(_navigation), + std::make_unique( + &_navigation->session()), std::move(initBox)), Ui::LayerOption::KeepOther); } diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 73a71d6a7..3b97332ba 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -153,75 +153,6 @@ contactsAboutFg: windowSubTextFgOver; contactsAboutTop: 60px; contactsAboutBottom: 19px; -contactsSearchField: InputField(defaultInputField) { - textBg: transparent; - textMargins: margins(2px, 7px, 2px, 0px); - - placeholderFg: placeholderFg; - placeholderFgActive: placeholderFgActive; - placeholderFgError: placeholderFgActive; - placeholderMargins: margins(2px, 0px, 2px, 0px); - placeholderScale: 0.; - placeholderFont: normalFont; - - border: 0px; - borderActive: 0px; - - heightMin: 32px; - - font: normalFont; -} -contactsSearchCancel: CrossButton { - width: 44px; - height: 44px; - - cross: CrossAnimation { - size: 36px; - skip: 12px; - stroke: 2px; - minScale: 0.3; - } - crossFg: boxTitleCloseFg; - crossFgOver: boxTitleCloseFgOver; - crossPosition: point(4px, 4px); - - duration: 150; - loadingPeriod: 1000; - ripple: RippleAnimation(defaultRippleAnimation) { - color: windowBgOver; - } -} -contactsMultiSelect: MultiSelect { - bg: boxSearchBg; - padding: margins(8px, 6px, 8px, 6px); - maxHeight: 104px; - scroll: ScrollArea(defaultSolidScroll) { - deltat: 3px; - deltab: 3px; - round: 1px; - width: 8px; - deltax: 3px; - hiding: 1000; - } - - item: defaultMultiSelectItem; - itemSkip: 8px; - - field: contactsSearchField; - fieldMinWidth: 42px; - fieldIcon: boxFieldSearchIcon; - fieldIconSkip: 36px; - - fieldCancel: contactsSearchCancel; - fieldCancelSkip: 40px; -} -contactsPhotoCheckIcon: defaultPeerListCheckIcon; -contactsPhotoCheck: defaultPeerListCheck; -contactsPhotoCheckbox: defaultPeerListCheckbox; -contactsPhotoDisabledCheckFg: menuIconFg; -contactsNameCheckedFg: windowActiveTextFg; -contactsRipple: defaultRippleAnimation; - contactsMarginTop: 4px; contactsMarginBottom: 4px; membersMarginTop: 10px; @@ -242,7 +173,7 @@ peerListBox: PeerList(defaultPeerList) { button: OutlineButton(defaultPeerListButton) { textBg: contactsBg; textBgOver: contactsBgOver; - ripple: contactsRipple; + ripple: defaultRippleAnimation; } statusFg: contactsStatusFg; statusFgOver: contactsStatusFgOver; @@ -283,7 +214,7 @@ localStorageLimitMargin: margins(22px, 5px, 20px, 10px); shareRowsTop: 12px; shareRowHeight: 108px; sharePhotoTop: 6px; -sharePhotoCheckbox: RoundImageCheckbox(contactsPhotoCheckbox) { +sharePhotoCheckbox: RoundImageCheckbox(defaultPeerListCheckbox) { imageRadius: 28px; imageSmallRadius: 24px; } @@ -446,7 +377,7 @@ autoDownloadLimitPadding: margins(22px, 8px, 22px, 8px); confirmCaptionArea: InputField(defaultInputField) { textMargins: margins(1px, 26px, 31px, 4px); - heightMax: 150px; + heightMax: 158px; } confirmBg: windowBgOver; confirmMaxHeight: 245px; @@ -698,10 +629,10 @@ muteChatTitle: FlatLabel(boxLabel) { } muteChatTitleLeft: 50px; -groupStickersRemove: contactsSearchCancel; +groupStickersRemove: defaultMultiSelectSearchCancel; groupStickersRemovePosition: point(6px, 6px); groupStickersFieldPadding: margins(8px, 6px, 8px, 6px); -groupStickersField: InputField(contactsSearchField) { +groupStickersField: InputField(defaultMultiSelectSearchField) { placeholderFont: boxTextFont; font: boxTextFont; placeholderMargins: margins(0px, 0px, 0px, 0px); @@ -961,7 +892,7 @@ pollResultsVotesCount: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } pollResultsHeaderPadding: margins(22px, 22px, 22px, 8px); -pollResultsShowMore: SettingsButton { +pollResultsShowMore: SettingsButton(defaultSettingsButton) { textFg: lightButtonFg; textFgOver: lightButtonFgOver; textBg: windowBg; diff --git a/Telegram/SourceFiles/boxes/confirm_box.cpp b/Telegram/SourceFiles/boxes/confirm_box.cpp index b28aebf0a..7479e0536 100644 --- a/Telegram/SourceFiles/boxes/confirm_box.cpp +++ b/Telegram/SourceFiles/boxes/confirm_box.cpp @@ -801,7 +801,10 @@ void DeleteMessagesBox::resizeEvent(QResizeEvent *e) { void DeleteMessagesBox::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { - deleteAndClear(); + // Don't make the clearing history so easy. + if (!_wipeHistoryPeer) { + deleteAndClear(); + } } else { BoxContent::keyPressEvent(e); } diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 2ac6a8fb7..07d0dd0c5 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -754,11 +754,13 @@ CreatePollBox::CreatePollBox( not_null controller, PollData::Flags chosen, PollData::Flags disabled, - Api::SendType sendType) + Api::SendType sendType, + SendMenu::Type sendMenuType) : _controller(controller) , _chosen(chosen) , _disabled(disabled) -, _sendType(sendType) { +, _sendType(sendType) +, _sendMenuType(sendMenuType) { } rpl::producer CreatePollBox::submitRequests() const { @@ -1101,26 +1103,23 @@ object_ptr CreatePollBox::setupContent() { }, lifetime()); const auto isNormal = (_sendType == Api::SendType::Normal); - const auto isScheduled = (_sendType == Api::SendType::Scheduled); const auto submit = addButton( isNormal ? tr::lng_polls_create_button() : tr::lng_schedule_button(), [=] { isNormal ? send({}) : sendScheduled(); }); - if (isNormal || isScheduled) { - const auto sendMenuType = [=] { - collectError(); - return (*error || isScheduled) - ? SendMenu::Type::Disabled - : SendMenu::Type::Scheduled; - }; - SendMenu::SetupMenuAndShortcuts( - submit.data(), - sendMenuType, - sendSilent, - sendScheduled); - } + const auto sendMenuType = [=] { + collectError(); + return (*error) + ? SendMenu::Type::Disabled + : _sendMenuType; + }; + SendMenu::SetupMenuAndShortcuts( + submit.data(), + sendMenuType, + sendSilent, + sendScheduled); addButton(tr::lng_cancel(), [=] { closeBox(); }); return result; diff --git a/Telegram/SourceFiles/boxes/create_poll_box.h b/Telegram/SourceFiles/boxes/create_poll_box.h index f88de5418..362213987 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.h +++ b/Telegram/SourceFiles/boxes/create_poll_box.h @@ -22,6 +22,10 @@ namespace Window { class SessionController; } // namespace Window +namespace SendMenu { +enum class Type; +} // namespace SendMenu + class CreatePollBox : public Ui::BoxContent { public: struct Result { @@ -34,7 +38,8 @@ public: not_null controller, PollData::Flags chosen, PollData::Flags disabled, - Api::SendType sendType); + Api::SendType sendType, + SendMenu::Type sendMenuType); [[nodiscard]] rpl::producer submitRequests() const; void submitFailed(const QString &error); @@ -66,6 +71,7 @@ private: const PollData::Flags _chosen = PollData::Flags(); const PollData::Flags _disabled = PollData::Flags(); const Api::SendType _sendType = Api::SendType(); + const SendMenu::Type _sendMenuType; Fn _setInnerFocus; Fn()> _dataIsValidValue; rpl::event_stream _submitRequests; diff --git a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp index 5bf50e809..e8caa8404 100644 --- a/Telegram/SourceFiles/boxes/dictionaries_manager.cpp +++ b/Telegram/SourceFiles/boxes/dictionaries_manager.cpp @@ -91,7 +91,7 @@ QString StateDescription(const DictState &state) { auto CreateMultiSelect(QWidget *parent) { const auto result = Ui::CreateChild( parent, - st::contactsMultiSelect, + st::defaultMultiSelect, tr::lng_participant_filter()); result->resizeToWidth(st::boxWidth); diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 3be617c73..453840772 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -33,38 +33,36 @@ namespace { class PrivacyExceptionsBoxController : public ChatsListBoxController { public: PrivacyExceptionsBoxController( - not_null navigation, + not_null session, rpl::producer title, const std::vector> &selected); Main::Session &session() const override; void rowClicked(not_null row) override; - std::vector> getResult() const; - protected: void prepareViewHook() override; std::unique_ptr createRow(not_null history) override; private: - not_null _navigation; + const not_null _session; rpl::producer _title; std::vector> _selected; }; PrivacyExceptionsBoxController::PrivacyExceptionsBoxController( - not_null navigation, + not_null session, rpl::producer title, const std::vector> &selected) -: ChatsListBoxController(navigation) -, _navigation(navigation) +: ChatsListBoxController(session) +, _session(session) , _title(std::move(title)) , _selected(selected) { } Main::Session &PrivacyExceptionsBoxController::session() const { - return _navigation->session(); + return *_session; } void PrivacyExceptionsBoxController::prepareViewHook() { @@ -72,10 +70,6 @@ void PrivacyExceptionsBoxController::prepareViewHook() { delegate()->peerListAddSelectedPeers(_selected); } -std::vector> PrivacyExceptionsBoxController::getResult() const { - return delegate()->peerListCollectSelectedRows(); -} - void PrivacyExceptionsBoxController::rowClicked(not_null row) { const auto peer = row->peer(); @@ -146,13 +140,13 @@ void EditPrivacyBox::editExceptions( Exception exception, Fn done) { auto controller = std::make_unique( - _window, + &_window->session(), _controller->exceptionBoxTitle(exception), exceptions(exception)); auto initBox = [=, controller = controller.get()]( not_null box) { box->addButton(tr::lng_settings_save(), crl::guard(this, [=] { - exceptions(exception) = controller->getResult(); + exceptions(exception) = box->collectSelectedRows(); const auto type = [&] { switch (exception) { case Exception::Always: return Exception::Never; diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index 247d3d683..e47b7179f 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -419,7 +419,7 @@ void EditExceptions( const auto include = (options & Flag::Contacts) != Flags(0); const auto rules = data->current(); auto controller = std::make_unique( - window, + &window->session(), (include ? tr::lng_filters_include_title() : tr::lng_filters_exclude_title()), @@ -431,7 +431,7 @@ void EditExceptions( auto initBox = [=](not_null box) { box->setCloseByOutsideClick(false); box->addButton(tr::lng_settings_save(), crl::guard(context, [=] { - const auto peers = box->peerListCollectSelectedRows(); + const auto peers = box->collectSelectedRows(); const auto rules = data->current(); auto &&histories = ranges::view::all( peers diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp index 08969b38a..ccb9ac677 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.cpp @@ -74,7 +74,6 @@ public: void peerListSetAdditionalTitle(rpl::producer title) override; bool peerListIsRowChecked(not_null row) override; int peerListSelectedRowsCount() override; - std::vector> peerListCollectSelectedRows() override; void peerListScrollToTop() override; void peerListAddSelectedPeerInBunch( not_null peer) override; @@ -215,11 +214,6 @@ int TypeDelegate::peerListSelectedRowsCount() { return 0; } -auto TypeDelegate::peerListCollectSelectedRows() --> std::vector> { - return {}; -} - void TypeDelegate::peerListScrollToTop() { } @@ -392,14 +386,14 @@ void PaintFilterChatsTypeIcon( } EditFilterChatsListController::EditFilterChatsListController( - not_null navigation, + not_null session, rpl::producer title, Flags options, Flags selected, const base::flat_set> &peers, bool isLocal) -: ChatsListBoxController(navigation) -, _navigation(navigation) +: ChatsListBoxController(session) +, _session(session) , _title(std::move(title)) , _peers(peers) , _options(options) @@ -408,7 +402,7 @@ EditFilterChatsListController::EditFilterChatsListController( } Main::Session &EditFilterChatsListController::session() const { - return _navigation->session(); + return *_session; } void EditFilterChatsListController::rowClicked(not_null row) { @@ -470,10 +464,10 @@ object_ptr EditFilterChatsListController::prepareTypesList() { &session(), _options, _selected); + controller->setStyleOverrides(&st::windowFilterSmallList); const auto content = result->add(object_ptr( container, - controller, - st::windowFilterSmallList)); + controller)); delegate->setContent(content); controller->setDelegate(delegate); for (const auto flag : kAllTypes) { diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h index 4dc934419..1932ecd30 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_chats_list.h @@ -41,7 +41,7 @@ public: using Flags = Data::ChatFilter::Flags; EditFilterChatsListController( - not_null navigation, + not_null session, rpl::producer title, Flags options, Flags selected, @@ -65,7 +65,7 @@ private: void updateTitle(); - const not_null _navigation; + const not_null _session; rpl::producer _title; base::flat_set> _peers; Flags _options; diff --git a/Telegram/SourceFiles/boxes/language_box.cpp b/Telegram/SourceFiles/boxes/language_box.cpp index 53ba3f53e..80d7dc7f4 100644 --- a/Telegram/SourceFiles/boxes/language_box.cpp +++ b/Telegram/SourceFiles/boxes/language_box.cpp @@ -880,7 +880,7 @@ void Content::setupContent( const auto inner = wrap->entity(); inner->add(object_ptr( inner, - st::boxVerticalMargin)); + st::defaultBox.margin.top())); const auto rows = inner->add(object_ptr( inner, list, @@ -888,7 +888,7 @@ void Content::setupContent( areOfficial)); inner->add(object_ptr( inner, - st::boxVerticalMargin)); + st::defaultBox.margin.top())); rows->isEmpty() | rpl::start_with_next([=](bool empty) { wrap->toggle(!empty, anim::type::instant); @@ -1153,7 +1153,7 @@ void LanguageBox::setInnerFocus() { not_null LanguageBox::createMultiSelect() { const auto result = Ui::CreateChild( this, - st::contactsMultiSelect, + st::defaultMultiSelect, tr::lng_participant_filter()); result->resizeToWidth(st::boxWidth); result->moveToLeft(0, 0); diff --git a/Telegram/SourceFiles/boxes/local_storage_box.cpp b/Telegram/SourceFiles/boxes/local_storage_box.cpp index 5bf122579..4f1e56dd4 100644 --- a/Telegram/SourceFiles/boxes/local_storage_box.cpp +++ b/Telegram/SourceFiles/boxes/local_storage_box.cpp @@ -370,7 +370,7 @@ void LocalStorageBox::clearByTag(uint16 tag) { void LocalStorageBox::setupControls() { const auto container = setInnerWidget( object_ptr(this), - st::contactsMultiSelect.scroll); + st::defaultMultiSelect.scroll); const auto createRow = [&]( uint16 tag, Fn title, diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 148ed0cbc..33b43d854 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -70,7 +70,9 @@ void PeerListBox::createMultiSelect() { auto entity = object_ptr( this, - st::contactsMultiSelect, + (_controller->selectSt() + ? *_controller->selectSt() + : st::defaultMultiSelect), tr::lng_participant_filter()); _select.create(this, std::move(entity)); _select->heightValue( @@ -120,8 +122,7 @@ void PeerListBox::prepare() { setContent(setInnerWidget( object_ptr( this, - _controller.get(), - st::peerListBox), + _controller.get()), st::boxScroll)); content()->resizeToWidth(_controller->contentWidth()); @@ -181,8 +182,12 @@ void PeerListBox::resizeEvent(QResizeEvent *e) { void PeerListBox::paintEvent(QPaintEvent *e) { Painter p(this); - for (auto rect : e->region().rects()) { - p.fillRect(rect, st::contactsBg); + + const auto &bg = (_controller->listSt() + ? *_controller->listSt() + : st::peerListBox).bg; + for (const auto rect : e->region()) { + p.fillRect(rect, bg); } } @@ -251,6 +256,14 @@ PeerListController::PeerListController(std::unique_ptr } } +const style::PeerList &PeerListController::computeListSt() const { + return _listSt ? *_listSt : st::peerListBox; +} + +const style::MultiSelect &PeerListController::computeSelectSt() const { + return _selectSt ? *_selectSt : st::defaultMultiSelect; +} + bool PeerListController::hasComplexSearch() const { return (_searchController != nullptr); } @@ -282,7 +295,8 @@ void PeerListController::setDescriptionText(const QString &text) { if (text.isEmpty()) { setDescription(nullptr); } else { - setDescription(object_ptr(nullptr, text, st::membersAbout)); + const auto &st = _listSt ? *_listSt : st::peerListBox; + setDescription(object_ptr(nullptr, text, computeListSt().about)); } } @@ -356,17 +370,20 @@ void PeerListBox::addSelectItem( createMultiSelect(); _select->hide(anim::type::instant); } + const auto &activeBg = (_controller->selectSt() + ? *_controller->selectSt() + : st::defaultMultiSelect).item.textActiveBg; if (animated == anim::type::instant) { _select->entity()->addItemInBunch( itemId, text, - st::activeButtonBg, + activeBg, std::move(paintUserpic)); } else { _select->entity()->addItem( itemId, text, - st::activeButtonBg, + activeBg, std::move(paintUserpic)); } } @@ -385,7 +402,7 @@ int PeerListBox::peerListSelectedRowsCount() { return _select ? _select->entity()->getItemsCount() : 0; } -auto PeerListBox::peerListCollectSelectedRows() +auto PeerListBox::collectSelectedRows() -> std::vector> { auto result = std::vector>(); auto items = _select @@ -429,9 +446,9 @@ bool PeerListRow::checked() const { return _checkbox && _checkbox->checked(); } -void PeerListRow::setCustomStatus(const QString &status) { +void PeerListRow::setCustomStatus(const QString &status, bool active) { setStatusText(status); - _statusType = StatusType::Custom; + _statusType = active ? StatusType::CustomActive : StatusType::Custom; _statusValidTill = 0; } @@ -441,7 +458,10 @@ void PeerListRow::clearCustomStatus() { } void PeerListRow::refreshStatus() { - if (!_initialized || special() || _statusType == StatusType::Custom) { + if (!_initialized + || special() + || _statusType == StatusType::Custom + || _statusType == StatusType::CustomActive) { return; } _statusType = StatusType::LastSeen; @@ -627,7 +647,8 @@ void PeerListRow::paintStatusText( int availableWidth, int outerWidth, bool selected) { - auto statusHasOnlineColor = (_statusType == PeerListRow::StatusType::Online); + auto statusHasOnlineColor = (_statusType == PeerListRow::StatusType::Online) + || (_statusType == PeerListRow::StatusType::CustomActive); p.setFont(st::contactsStatusFont); p.setPen(statusHasOnlineColor ? st.statusFgActive : (selected ? st.statusFgOver : st.statusFg)); _status.drawLeftElided(p, x, y, availableWidth, outerWidth); @@ -695,21 +716,21 @@ void PeerListRow::paintDisabledCheckUserpic( int x, int y, int outerWidth) const { - auto userpicRadius = st::contactsPhotoCheckbox.imageSmallRadius; - auto userpicShift = st::contactsPhotoCheckbox.imageRadius - userpicRadius; - auto userpicDiameter = st::contactsPhotoCheckbox.imageRadius * 2; + auto userpicRadius = st.checkbox.imageSmallRadius; + auto userpicShift = st.checkbox.imageRadius - userpicRadius; + auto userpicDiameter = st.checkbox.imageRadius * 2; auto userpicLeft = x + userpicShift; auto userpicTop = y + userpicShift; auto userpicEllipse = style::rtlrect(x, y, userpicDiameter, userpicDiameter, outerWidth); - auto userpicBorderPen = st::contactsPhotoDisabledCheckFg->p; - userpicBorderPen.setWidth(st::contactsPhotoCheckbox.selectWidth); + auto userpicBorderPen = st.disabledCheckFg->p; + userpicBorderPen.setWidth(st.checkbox.selectWidth); - auto iconDiameter = st::contactsPhotoCheckbox.check.size; - auto iconLeft = x + userpicDiameter + st::contactsPhotoCheckbox.selectWidth - iconDiameter; - auto iconTop = y + userpicDiameter + st::contactsPhotoCheckbox.selectWidth - iconDiameter; + auto iconDiameter = st.checkbox.check.size; + auto iconLeft = x + userpicDiameter + st.checkbox.selectWidth - iconDiameter; + auto iconTop = y + userpicDiameter + st.checkbox.selectWidth - iconDiameter; auto iconEllipse = style::rtlrect(iconLeft, iconTop, iconDiameter, iconDiameter, outerWidth); - auto iconBorderPen = st::contactsPhotoCheckbox.check.border->p; - iconBorderPen.setWidth(st::contactsPhotoCheckbox.selectWidth); + auto iconBorderPen = st.checkbox.check.border->p; + iconBorderPen.setWidth(st.checkbox.selectWidth); if (_isSavedMessagesChat) { Ui::EmptyUserpic::PaintSavedMessages(p, userpicLeft, userpicTop, outerWidth, userpicRadius * 2); @@ -748,11 +769,11 @@ void PeerListRow::paintDisabledCheckUserpic( } p.setPen(iconBorderPen); - p.setBrush(st::contactsPhotoDisabledCheckFg); + p.setBrush(st.disabledCheckFg); p.drawEllipse(iconEllipse); } - st::contactsPhotoCheckbox.check.check.paint(p, iconEllipse.topLeft(), outerWidth); + st.checkbox.check.check.paint(p, iconEllipse.topLeft(), outerWidth); } void PeerListRow::setStatusText(const QString &text) { @@ -789,10 +810,9 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) { PeerListContent::PeerListContent( QWidget *parent, - not_null controller, - const style::PeerList &st) + not_null controller) : RpWidget(parent) -, _st(st) +, _st(controller->computeListSt()) , _controller(controller) , _rowHeight(_st.item.height) { _controller->session().downloaderTaskFinished( @@ -1076,6 +1096,18 @@ void PeerListContent::setAboveWidget(object_ptr widget) { } } +void PeerListContent::setAboveSearchWidget(object_ptr widget) { + _aboveSearchWidget = std::move(widget); + if (_aboveSearchWidget) { + _aboveSearchWidget->setParent(this); + } +} + +void PeerListContent::setHideEmpty(bool hide) { + _hideEmpty = hide; + resizeToWidth(width()); +} + void PeerListContent::setBelowWidget(object_ptr widget) { _belowWidget = std::move(widget); if (_belowWidget) { @@ -1084,6 +1116,9 @@ void PeerListContent::setBelowWidget(object_ptr widget) { } int PeerListContent::labelHeight() const { + if (_hideEmpty && !shownRowsCount()) { + return 0; + } auto computeLabelHeight = [](auto &label) { if (!label) { return 0; @@ -1176,34 +1211,45 @@ void PeerListContent::paintEvent(QPaintEvent *e) { } int PeerListContent::resizeGetHeight(int newWidth) { + const auto rowsCount = shownRowsCount(); + const auto hideAll = !rowsCount && _hideEmpty; _aboveHeight = 0; if (_aboveWidget) { _aboveWidget->resizeToWidth(newWidth); _aboveWidget->moveToLeft(0, 0, newWidth); - if (showingSearch()) { + if (hideAll || showingSearch()) { _aboveWidget->hide(); } else { _aboveWidget->show(); _aboveHeight = _aboveWidget->height(); } } - const auto rowsCount = shownRowsCount(); + if (_aboveSearchWidget) { + _aboveSearchWidget->resizeToWidth(newWidth); + _aboveSearchWidget->moveToLeft(0, 0, newWidth); + if (hideAll || !showingSearch()) { + _aboveSearchWidget->hide(); + } else { + _aboveSearchWidget->show(); + _aboveHeight = _aboveSearchWidget->height(); + } + } const auto labelTop = rowsTop() + qMax(1, shownRowsCount()) * _rowHeight; const auto labelWidth = newWidth - 2 * st::contactsPadding.left(); if (_description) { _description->resizeToWidth(labelWidth); _description->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth); - _description->setVisible(!showingSearch()); + _description->setVisible(!hideAll && !showingSearch()); } if (_searchNoResults) { _searchNoResults->resizeToWidth(labelWidth); _searchNoResults->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth); - _searchNoResults->setVisible(showingSearch() && _filterResults.empty() && !_controller->isSearchLoading()); + _searchNoResults->setVisible(!hideAll && showingSearch() && _filterResults.empty() && !_controller->isSearchLoading()); } if (_searchLoading) { _searchLoading->resizeToWidth(labelWidth); _searchLoading->moveToLeft(st::contactsPadding.left(), labelTop + st::membersAboutLimitPadding.top(), newWidth); - _searchLoading->setVisible(showingSearch() && _filterResults.empty() && _controller->isSearchLoading()); + _searchLoading->setVisible(!hideAll && showingSearch() && _filterResults.empty() && _controller->isSearchLoading()); } const auto label = labelHeight(); const auto belowTop = (label > 0 || rowsCount > 0) @@ -1213,7 +1259,7 @@ int PeerListContent::resizeGetHeight(int newWidth) { if (_belowWidget) { _belowWidget->resizeToWidth(newWidth); _belowWidget->moveToLeft(0, belowTop, newWidth); - if (showingSearch()) { + if (hideAll || showingSearch()) { _belowWidget->hide(); } else { _belowWidget->show(); @@ -1260,7 +1306,7 @@ void PeerListContent::mousePressEvent(QMouseEvent *e) { updateRow(row, hint); }; if (_selected.action) { - auto actionRect = getActionRect(row, _selected.index); + auto actionRect = getActiveActionRect(row, _selected.index); if (!actionRect.isEmpty()) { auto point = mapFromGlobal(QCursor::pos()) - actionRect.topLeft(); row->addActionRipple(point, std::move(updateCallback)); @@ -1323,7 +1369,11 @@ void PeerListContent::contextMenuEvent(QContextMenuEvent *e) { })); _contextMenu->popup(e->globalPos()); e->accept(); + } else { + setContexted(Selected()); } + } else { + setContexted(Selected()); } } @@ -1412,7 +1462,7 @@ crl::time PeerListContent::paintRow( selected); } auto nameCheckedRatio = row->disabled() ? 0. : row->checkedRatio(); - p.setPen(anim::pen(st::contactsNameFg, st::contactsNameCheckedFg, nameCheckedRatio)); + p.setPen(anim::pen(_st.item.nameFg, _st.item.nameFgChecked, nameCheckedRatio)); name.drawLeftElided(p, namex, _st.item.namePosition.y(), namew, width()); if (!actionSize.isEmpty()) { @@ -1460,15 +1510,18 @@ crl::time PeerListContent::paintRow( return (refreshStatusAt - ms); } -void PeerListContent::selectSkip(int direction) { - if (_pressed.index.value >= 0) { - return; +PeerListContent::SkipResult PeerListContent::selectSkip(int direction) { + if (hasPressed()) { + return { _selected.index.value, _selected.index.value }; } _mouseSelection = false; _lastMousePosition = std::nullopt; auto newSelectedIndex = _selected.index.value + direction; + auto result = SkipResult(); + result.shouldMoveTo = newSelectedIndex; + auto rowsCount = shownRowsCount(); auto index = 0; auto firstEnabled = -1, lastEnabled = -1; @@ -1524,14 +1577,36 @@ void PeerListContent::selectSkip(int direction) { } update(); + + _selectedIndex = _selected.index.value; + result.reallyMovedTo = _selected.index.value; + return result; } void PeerListContent::selectSkipPage(int height, int direction) { auto rowsToSkip = height / _rowHeight; - if (!rowsToSkip) return; + if (!rowsToSkip) { + return; + } selectSkip(rowsToSkip * direction); } +rpl::producer PeerListContent::selectedIndexValue() const { + return _selectedIndex.value(); +} + +bool PeerListContent::hasSelection() const { + return _selected.index.value >= 0; +} + +bool PeerListContent::hasPressed() const { + return _pressed.index.value >= 0; +} + +void PeerListContent::clearSelection() { + setSelected(Selected()); +} + void PeerListContent::loadProfilePhotos() { if (_visibleTop >= _visibleBottom) return; @@ -1678,14 +1753,17 @@ void PeerListContent::setSearchQuery( clearSearchRows(); } -void PeerListContent::submitted() { +bool PeerListContent::submitted() { if (const auto row = getRow(_selected.index)) { _controller->rowClicked(row); + return true; } else if (showingSearch()) { if (const auto row = getRow(RowIndex(0))) { _controller->rowClicked(row); + return true; } } + return false; } void PeerListContent::visibleTopBottomUpdated( @@ -1699,11 +1777,14 @@ void PeerListContent::visibleTopBottomUpdated( void PeerListContent::setSelected(Selected selected) { updateRow(_selected.index); - if (_selected != selected) { - _selected = selected; - updateRow(_selected.index); - setCursor(_selected.action ? style::cur_pointer : style::cur_default); + if (_selected == selected) { + return; } + _selected = selected; + updateRow(_selected.index); + setCursor(_selected.action ? style::cur_pointer : style::cur_default); + + _selectedIndex = _selected.index.value; } void PeerListContent::setContexted(Selected contexted) { @@ -1752,7 +1833,7 @@ void PeerListContent::selectByMouse(QPoint globalPosition) { if (row->disabled()) { selected = Selected(); } else { - if (row->hasAction() && getActionRect(row, selected.index).contains(point)) { + if (row->hasAction() && getActiveActionRect(row, selected.index).contains(point)) { selected.action = true; } } @@ -1760,9 +1841,9 @@ void PeerListContent::selectByMouse(QPoint globalPosition) { setSelected(selected); } -QRect PeerListContent::getActionRect(not_null row, RowIndex index) const { +QRect PeerListContent::getActiveActionRect(not_null row, RowIndex index) const { auto actionSize = row->actionSize(); - if (actionSize.isEmpty()) { + if (actionSize.isEmpty() || row->actionDisabled()) { return QRect(); } auto actionMargins = row->actionMargins(); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 8ffa8dc11..acd1a4715 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace style { struct PeerList; struct PeerListItem; +struct MultiSelect; } // namespace style namespace Main { @@ -88,7 +89,7 @@ public: [[nodiscard]] virtual auto generatePaintUserpicCallback() -> PaintRoundImageCallback; - void setCustomStatus(const QString &status); + void setCustomStatus(const QString &status, bool active = false); void clearCustomStatus(); // Box interface. @@ -113,6 +114,9 @@ public: virtual QSize placeholderSize() const { return QSize(); } + virtual bool actionDisabled() const { + return false; + } virtual QMargins actionMargins() const { return QMargins(); } @@ -138,6 +142,7 @@ public: Online, LastSeen, Custom, + CustomActive, }; virtual void refreshStatus(); crl::time refreshStatusTime() const; @@ -262,10 +267,12 @@ class PeerListDelegate { public: virtual void peerListSetTitle(rpl::producer title) = 0; virtual void peerListSetAdditionalTitle(rpl::producer title) = 0; + virtual void peerListSetHideEmpty(bool hide) = 0; virtual void peerListSetDescription(object_ptr description) = 0; virtual void peerListSetSearchLoading(object_ptr loading) = 0; virtual void peerListSetSearchNoResults(object_ptr noResults) = 0; virtual void peerListSetAboveWidget(object_ptr aboveWidget) = 0; + virtual void peerListSetAboveSearchWidget(object_ptr aboveWidget) = 0; virtual void peerListSetBelowWidget(object_ptr belowWidget) = 0; virtual void peerListSetSearchMode(PeerListSearchMode mode) = 0; virtual void peerListAppendRow(std::unique_ptr row) = 0; @@ -307,7 +314,6 @@ public: } virtual int peerListSelectedRowsCount() = 0; - virtual std::vector> peerListCollectSelectedRows() = 0; virtual std::unique_ptr peerListSaveState() const = 0; virtual void peerListRestoreState( std::unique_ptr state) = 0; @@ -380,6 +386,21 @@ public: prepare(); } + void setStyleOverrides( + const style::PeerList *listSt, + const style::MultiSelect *selectSt = nullptr) { + _listSt = listSt; + _selectSt = selectSt; + } + const style::PeerList *listSt() const { + return _listSt; + } + const style::MultiSelect *selectSt() const { + return _selectSt; + } + const style::PeerList &computeListSt() const; + const style::MultiSelect &computeSelectSt() const; + virtual void prepare() = 0; virtual void rowClicked(not_null row) = 0; virtual Main::Session &session() const = 0; @@ -466,6 +487,9 @@ private: PeerListDelegate *_delegate = nullptr; std::unique_ptr _searchController = nullptr; + const style::PeerList *_listSt = nullptr; + const style::MultiSelect *_selectSt = nullptr; + rpl::lifetime _lifetime; }; @@ -487,16 +511,22 @@ class PeerListContent public: PeerListContent( QWidget *parent, - not_null controller, - const style::PeerList &st); + not_null controller); - void selectSkip(int direction); + struct SkipResult { + int shouldMoveTo = 0; + int reallyMovedTo = 0; + }; + SkipResult selectSkip(int direction); void selectSkipPage(int height, int direction); + [[nodiscard]] rpl::producer selectedIndexValue() const; + [[nodiscard]] bool hasSelection() const; + [[nodiscard]] bool hasPressed() const; void clearSelection(); void searchQueryChanged(QString query); - void submitted(); + bool submitted(); // Interface for the controller. void appendRow(std::unique_ptr row); @@ -516,7 +546,9 @@ public: void setSearchLoading(object_ptr loading); void setSearchNoResults(object_ptr noResults); void setAboveWidget(object_ptr widget); + void setAboveSearchWidget(object_ptr widget); void setBelowWidget(object_ptr width); + void setHideEmpty(bool hide); void refreshRows(); void setSearchMode(PeerListSearchMode mode); @@ -619,7 +651,7 @@ private: int getRowTop(RowIndex row) const; PeerListRow *getRow(RowIndex element); RowIndex findRowIndex(not_null row, RowIndex hint = RowIndex()); - QRect getActionRect(not_null row, RowIndex index) const; + QRect getActiveActionRect(not_null row, RowIndex index) const; crl::time paintRow(Painter &p, crl::time ms, RowIndex index); @@ -658,6 +690,7 @@ private: Selected _selected; Selected _pressed; Selected _contexted; + rpl::variable _selectedIndex = -1; bool _mouseSelection = false; std::optional _lastMousePosition; Qt::MouseButton _pressButton = Qt::LeftButton; @@ -676,7 +709,9 @@ private: int _aboveHeight = 0; int _belowHeight = 0; + bool _hideEmpty = false; object_ptr _aboveWidget = { nullptr }; + object_ptr _aboveSearchWidget = { nullptr }; object_ptr _belowWidget = { nullptr }; object_ptr _description = { nullptr }; object_ptr _searchNoResults = { nullptr }; @@ -694,6 +729,9 @@ public: _content = content; } + void peerListSetHideEmpty(bool hide) override { + _content->setHideEmpty(hide); + } void peerListAppendRow( std::unique_ptr row) override { _content->appendRow(std::move(row)); @@ -758,6 +796,9 @@ public: void peerListSetAboveWidget(object_ptr aboveWidget) override { _content->setAboveWidget(std::move(aboveWidget)); } + void peerListSetAboveSearchWidget(object_ptr aboveWidget) override { + _content->setAboveSearchWidget(std::move(aboveWidget)); + } void peerListSetBelowWidget(object_ptr belowWidget) override { _content->setBelowWidget(std::move(belowWidget)); } @@ -769,7 +810,7 @@ public: _content->reorderRows([&]( auto &&begin, auto &&end) { - std::sort(begin, end, [&](auto &&a, auto &&b) { + std::stable_sort(begin, end, [&](auto &&a, auto &&b) { return compare(*a, *b); }); }); @@ -815,6 +856,8 @@ public: std::unique_ptr controller, Fn)> init); + [[nodiscard]] std::vector> collectSelectedRows(); + void peerListSetTitle(rpl::producer title) override { setTitle(std::move(title)); } @@ -831,7 +874,6 @@ public: anim::type animated) override; bool peerListIsRowChecked(not_null row) override; int peerListSelectedRowsCount() override; - std::vector> peerListCollectSelectedRows() override; void peerListScrollToTop() override; protected: diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index cbd16625c..de5ffe31a 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "history/history.h" #include "dialogs/dialogs_main_list.h" -#include "window/window_session_controller.h" +#include "window/window_session_controller.h" // onShowAddContact() #include "facades.h" #include "styles/style_boxes.h" #include "styles/style_profile.h" @@ -115,7 +115,8 @@ object_ptr PrepareContactsBox( [=] { controller->widget()->onShowAddContact(); }); }; return Box( - std::make_unique(controller), + std::make_unique( + &sessionController->session()), std::move(delegate)); } @@ -184,9 +185,9 @@ void PeerListRowWithLink::paintAction( } PeerListGlobalSearchController::PeerListGlobalSearchController( - not_null navigation) -: _navigation(navigation) -, _api(&_navigation->session().mtp()) { + not_null session) +: _session(session) +, _api(&session->mtp()) { _timer.setCallback([this] { searchOnServer(); }); } @@ -235,8 +236,8 @@ void PeerListGlobalSearchController::searchDone( auto &contacts = result.c_contacts_found(); auto query = _query; if (requestId) { - _navigation->session().data().processUsers(contacts.vusers()); - _navigation->session().data().processChats(contacts.vchats()); + _session->data().processUsers(contacts.vusers()); + _session->data().processChats(contacts.vchats()); auto it = _queries.find(requestId); if (it != _queries.cend()) { query = it->second; @@ -246,7 +247,7 @@ void PeerListGlobalSearchController::searchDone( } const auto feedList = [&](const MTPVector &list) { for (const auto &mtpPeer : list.v) { - const auto peer = _navigation->session().data().peerLoaded( + const auto peer = _session->data().peerLoaded( peerFromMTP(mtpPeer)); if (peer) { delegate()->peerListSearchAddRow(peer); @@ -271,9 +272,9 @@ ChatsListBoxController::Row::Row(not_null history) } ChatsListBoxController::ChatsListBoxController( - not_null navigation) + not_null session) : ChatsListBoxController( - std::make_unique(navigation)) { + std::make_unique(session)) { } ChatsListBoxController::ChatsListBoxController( @@ -379,21 +380,21 @@ bool ChatsListBoxController::appendRow(not_null history) { } ContactsBoxController::ContactsBoxController( - not_null navigation) -: PeerListController( - std::make_unique(navigation)) -, _navigation(navigation) { + not_null session) +: ContactsBoxController( + session, + std::make_unique(session)) { } ContactsBoxController::ContactsBoxController( - not_null navigation, + not_null session, std::unique_ptr searchController) : PeerListController(std::move(searchController)) -, _navigation(navigation) { +, _session(session) { } Main::Session &ContactsBoxController::session() const { - return _navigation->session(); + return *_session; } void ContactsBoxController::prepare() { @@ -460,26 +461,24 @@ bool ContactsBoxController::appendRow(not_null user) { return false; } -std::unique_ptr ContactsBoxController::createRow(not_null user) { +std::unique_ptr ContactsBoxController::createRow( + not_null user) { return std::make_unique(user); } -void AddBotToGroupBoxController::Start( - not_null navigation, - not_null bot) { +void AddBotToGroupBoxController::Start(not_null bot) { auto initBox = [=](not_null box) { box->addButton(tr::lng_cancel(), [box] { box->closeBox(); }); }; Ui::show(Box( - std::make_unique(navigation, bot), + std::make_unique(bot), std::move(initBox))); } AddBotToGroupBoxController::AddBotToGroupBoxController( - not_null navigation, not_null bot) : ChatsListBoxController(SharingBotGame(bot) - ? std::make_unique(navigation) + ? std::make_unique(&bot->session()) : nullptr) , _bot(bot) { } @@ -597,15 +596,15 @@ void AddBotToGroupBoxController::prepareViewHook() { } ChooseRecipientBoxController::ChooseRecipientBoxController( - not_null navigation, + not_null session, FnMut)> callback) -: ChatsListBoxController(navigation) -, _navigation(navigation) +: ChatsListBoxController(session) +, _session(session) , _callback(std::move(callback)) { } Main::Session &ChooseRecipientBoxController::session() const { - return _navigation->session(); + return *_session; } void ChooseRecipientBoxController::prepareViewHook() { diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 60b7d076b..f05fe3e75 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -32,7 +32,6 @@ class History; namespace Window { class SessionController; -class SessionNavigation; } // namespace Window [[nodiscard]] object_ptr PrepareContactsBox( @@ -71,8 +70,7 @@ private: class PeerListGlobalSearchController : public PeerListSearchController { public: - PeerListGlobalSearchController( - not_null navigation); + explicit PeerListGlobalSearchController(not_null session); void searchQuery(const QString &query) override; bool isLoading() override; @@ -85,7 +83,7 @@ private: void searchOnServer(); void searchDone(const MTPcontacts_Found &result, mtpRequestId requestId); - const not_null _navigation; + const not_null _session; MTP::Sender _api; base::Timer _timer; QString _query; @@ -110,7 +108,7 @@ public: }; - ChatsListBoxController(not_null navigation); + ChatsListBoxController(not_null session); ChatsListBoxController( std::unique_ptr searchController); @@ -133,15 +131,15 @@ private: class ContactsBoxController : public PeerListController { public: + explicit ContactsBoxController(not_null session); ContactsBoxController( - not_null navigation); - ContactsBoxController( - not_null navigation, + not_null session, std::unique_ptr searchController); - Main::Session &session() const override; + [[nodiscard]] Main::Session &session() const override; void prepare() override final; - std::unique_ptr createSearchRow(not_null peer) override final; + [[nodiscard]] std::unique_ptr createSearchRow( + not_null peer) override final; void rowClicked(not_null row) override; protected: @@ -156,7 +154,7 @@ private: void checkForEmptyRows(); bool appendRow(not_null user); - const not_null _navigation; + const not_null _session; }; @@ -164,13 +162,9 @@ class AddBotToGroupBoxController : public ChatsListBoxController , public base::has_weak_ptr { public: - static void Start( - not_null navigation, - not_null bot); + static void Start(not_null bot); - AddBotToGroupBoxController( - not_null navigation, - not_null bot); + explicit AddBotToGroupBoxController(not_null bot); Main::Session &session() const override; void rowClicked(not_null row) override; @@ -192,7 +186,7 @@ private: void shareBotGame(not_null chat); void addBotToGroup(not_null chat); - not_null _bot; + const not_null _bot; }; @@ -201,7 +195,7 @@ class ChooseRecipientBoxController , public base::has_weak_ptr { public: ChooseRecipientBoxController( - not_null navigation, + not_null session, FnMut)> callback); Main::Session &session() const override; @@ -216,7 +210,7 @@ protected: std::unique_ptr createRow(not_null history) override; private: - const not_null _navigation; + const not_null _session; FnMut)> _callback; }; diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.cpp b/Telegram/SourceFiles/boxes/peer_lists_box.cpp new file mode 100644 index 000000000..7ea2c91b4 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peer_lists_box.cpp @@ -0,0 +1,429 @@ +/* +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 "boxes/peer_lists_box.h" + +#include "lang/lang_keys.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/widgets/multi_select.h" +#include "ui/widgets/scroll_area.h" +#include "main/main_session.h" +#include "data/data_session.h" +#include "data/data_peer.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +PeerListsBox::PeerListsBox( + QWidget*, + std::vector> controllers, + Fn)> init) +: _lists(makeLists(std::move(controllers))) +, _init(std::move(init)) { + Expects(!_lists.empty()); +} + +auto PeerListsBox::collectSelectedRows() +-> std::vector> { + auto result = std::vector>(); + auto items = _select + ? _select->entity()->getItems() + : QVector(); + if (!items.empty()) { + result.reserve(items.size()); + const auto session = &firstController()->session(); + for (const auto itemId : items) { + const auto foreign = [&] { + for (const auto &list : _lists) { + if (list.controller->isForeignRow(itemId)) { + return true; + } + } + return false; + }(); + if (!foreign) { + result.push_back(session->data().peer(itemId)); + } + } + } + return result; +} + + +PeerListsBox::List PeerListsBox::makeList( + std::unique_ptr controller) { + auto delegate = std::make_unique(this, controller.get()); + return { + std::move(controller), + std::move(delegate), + }; +} + +std::vector PeerListsBox::makeLists( + std::vector> controllers) { + auto result = std::vector(); + result.reserve(controllers.size()); + for (auto &controller : controllers) { + result.push_back(makeList(std::move(controller))); + } + return result; +} + +not_null PeerListsBox::firstController() const { + return _lists.front().controller.get(); +} + +void PeerListsBox::createMultiSelect() { + Expects(_select == nullptr); + + auto entity = object_ptr( + this, + (firstController()->selectSt() + ? *firstController()->selectSt() + : st::defaultMultiSelect), + tr::lng_participant_filter()); + _select.create(this, std::move(entity)); + _select->heightValue( + ) | rpl::start_with_next( + [this] { updateScrollSkips(); }, + lifetime()); + _select->entity()->setSubmittedCallback([=](Qt::KeyboardModifiers) { + for (const auto &list : _lists) { + if (list.content->submitted()) { + break; + } + } + }); + _select->entity()->setQueryChangedCallback([=](const QString &query) { + searchQueryChanged(query); + }); + _select->entity()->setItemRemovedCallback([=](uint64 itemId) { + for (const auto &list : _lists) { + if (list.controller->handleDeselectForeignRow(itemId)) { + return; + } + } + const auto session = &firstController()->session(); + if (const auto peer = session->data().peerLoaded(itemId)) { + const auto id = peer->id; + for (const auto &list : _lists) { + if (const auto row = list.delegate->peerListFindRow(id)) { + list.content->changeCheckState( + row, + false, + anim::type::normal); + update(); + } + list.controller->itemDeselectedHook(peer); + } + } + }); + _select->resizeToWidth(firstController()->contentWidth()); + _select->moveToLeft(0, 0); +} + +int PeerListsBox::getTopScrollSkip() const { + auto result = 0; + if (_select && !_select->isHidden()) { + result += _select->height(); + } + return result; +} + +void PeerListsBox::updateScrollSkips() { + // If we show / hide the search field scroll top is fixed. + // If we resize search field by bubbles scroll bottom is fixed. + setInnerTopSkip(getTopScrollSkip(), _scrollBottomFixed); + if (!_select->animating()) { + _scrollBottomFixed = true; + } +} + +void PeerListsBox::prepare() { + auto rows = setInnerWidget( + object_ptr(this), + st::boxScroll); + for (auto &list : _lists) { + const auto content = rows->add(object_ptr( + rows, + list.controller.get())); + list.content = content; + list.delegate->setContent(content); + list.controller->setDelegate(list.delegate.get()); + + content->scrollToRequests( + ) | rpl::start_with_next([=](Ui::ScrollToRequest request) { + const auto skip = content->y(); + onScrollToY( + skip + request.ymin, + (request.ymax >= 0) ? (skip + request.ymax) : request.ymax); + }, lifetime()); + + content->selectedIndexValue( + ) | rpl::filter([=](int index) { + return (index >= 0); + }) | rpl::start_with_next([=] { + for (const auto &list : _lists) { + if (list.content && list.content != content) { + list.content->clearSelection(); + } + } + }, lifetime()); + } + rows->resizeToWidth(firstController()->contentWidth()); + + setDimensions(firstController()->contentWidth(), st::boxMaxListHeight); + if (_select) { + _select->finishAnimating(); + Ui::SendPendingMoveResizeEvents(_select); + _scrollBottomFixed = true; + onScrollToY(0); + } + + if (_init) { + _init(this); + } +} + +void PeerListsBox::keyPressEvent(QKeyEvent *e) { + const auto skipRows = [&](int rows) { + if (rows == 0) { + return; + } + for (const auto &list : _lists) { + if (list.content->hasPressed()) { + return; + } + } + const auto from = begin(_lists), till = end(_lists); + auto i = from; + for (; i != till; ++i) { + if (i->content->hasSelection()) { + break; + } + } + if (i == till && rows < 0) { + return; + } + if (rows > 0) { + if (i == till) { + i = from; + } + for (; i != till; ++i) { + const auto result = i->content->selectSkip(rows); + if (result.shouldMoveTo - result.reallyMovedTo >= rows) { + continue; + } else if (result.reallyMovedTo >= result.shouldMoveTo) { + return; + } else { + rows = result.shouldMoveTo - result.reallyMovedTo; + } + } + } else { + for (++i; i != from;) { + const auto result = (--i)->content->selectSkip(rows); + if (result.shouldMoveTo - result.reallyMovedTo <= rows) { + continue; + } else if (result.reallyMovedTo <= result.shouldMoveTo) { + return; + } else { + rows = result.shouldMoveTo - result.reallyMovedTo; + } + } + } + }; + const auto rowsInPage = [&] { + const auto rowHeight = firstController()->computeListSt().item.height; + return height() / rowHeight; + }; + if (e->key() == Qt::Key_Down) { + skipRows(1); + } else if (e->key() == Qt::Key_Up) { + skipRows(-1); + } else if (e->key() == Qt::Key_PageDown) { + skipRows(rowsInPage()); + } else if (e->key() == Qt::Key_PageUp) { + skipRows(-rowsInPage()); + } else if (e->key() == Qt::Key_Escape && _select && !_select->entity()->getQuery().isEmpty()) { + _select->entity()->clearQuery(); + } else { + BoxContent::keyPressEvent(e); + } +} + +void PeerListsBox::searchQueryChanged(const QString &query) { + onScrollToY(0); + for (const auto &list : _lists) { + list.content->searchQueryChanged(query); + } +} + +void PeerListsBox::resizeEvent(QResizeEvent *e) { + BoxContent::resizeEvent(e); + + if (_select) { + _select->resizeToWidth(width()); + _select->moveToLeft(0, 0); + + updateScrollSkips(); + } + + for (const auto &list : _lists) { + list.content->resizeToWidth(width()); + } +} + +void PeerListsBox::paintEvent(QPaintEvent *e) { + Painter p(this); + + const auto &bg = (firstController()->listSt() + ? *firstController()->listSt() + : st::peerListBox).bg; + for (const auto rect : e->region()) { + p.fillRect(rect, bg); + } +} + +void PeerListsBox::setInnerFocus() { + if (!_select || !_select->toggled()) { + _lists.front().content->setFocus(); + } else { + _select->entity()->setInnerFocus(); + } +} + +PeerListsBox::Delegate::Delegate( + not_null box, + not_null controller) +: _box(box) +, _controller(controller) { +} + +void PeerListsBox::Delegate::peerListSetTitle(rpl::producer title) { +} + +void PeerListsBox::Delegate::peerListSetAdditionalTitle( + rpl::producer title) { +} + +void PeerListsBox::Delegate::peerListSetRowChecked( + not_null row, + bool checked) { + if (checked) { + _box->addSelectItem(row, anim::type::normal); + PeerListContentDelegate::peerListSetRowChecked(row, checked); + peerListUpdateRow(row); + + // This call deletes row from _searchRows. + _box->_select->entity()->clearQuery(); + } else { + // The itemRemovedCallback will call changeCheckState() here. + _box->_select->entity()->removeItem(row->id()); + peerListUpdateRow(row); + } +} + +void PeerListsBox::Delegate::peerListSetForeignRowChecked( + not_null row, + bool checked, + anim::type animated) { + if (checked) { + _box->addSelectItem(row, animated); + + // This call deletes row from _searchRows. + _box->_select->entity()->clearQuery(); + } else { + // The itemRemovedCallback will call changeCheckState() here. + _box->_select->entity()->removeItem(row->id()); + } +} + +void PeerListsBox::Delegate::peerListScrollToTop() { + _box->onScrollToY(0); +} + +void PeerListsBox::Delegate::peerListSetSearchMode(PeerListSearchMode mode) { + PeerListContentDelegate::peerListSetSearchMode(mode); + _box->setSearchMode(mode); +} + +void PeerListsBox::setSearchMode(PeerListSearchMode mode) { + auto selectVisible = (mode != PeerListSearchMode::Disabled); + if (selectVisible && !_select) { + createMultiSelect(); + _select->toggle(!selectVisible, anim::type::instant); + } + if (_select) { + _select->toggle(selectVisible, anim::type::normal); + _scrollBottomFixed = false; + setInnerFocus(); + } +} + +void PeerListsBox::Delegate::peerListFinishSelectedRowsBunch() { + Expects(_box->_select != nullptr); + + _box->_select->entity()->finishItemsBunch(); +} + +bool PeerListsBox::Delegate::peerListIsRowChecked( + not_null row) { + return _box->_select + ? _box->_select->entity()->hasItem(row->id()) + : false; +} + +int PeerListsBox::Delegate::peerListSelectedRowsCount() { + return _box->_select ? _box->_select->entity()->getItemsCount() : 0; +} + +void PeerListsBox::addSelectItem( + not_null peer, + anim::type animated) { + addSelectItem( + peer->id, + peer->shortName(), + PaintUserpicCallback(peer, false), + animated); +} + +void PeerListsBox::addSelectItem( + not_null row, + anim::type animated) { + addSelectItem( + row->id(), + row->generateShortName(), + row->generatePaintUserpicCallback(), + animated); +} + +void PeerListsBox::addSelectItem( + uint64 itemId, + const QString &text, + Ui::MultiSelect::PaintRoundImage paintUserpic, + anim::type animated) { + if (!_select) { + createMultiSelect(); + _select->hide(anim::type::instant); + } + const auto &activeBg = (firstController()->selectSt() + ? *firstController()->selectSt() + : st::defaultMultiSelect).item.textActiveBg; + if (animated == anim::type::instant) { + _select->entity()->addItemInBunch( + itemId, + text, + activeBg, + std::move(paintUserpic)); + } else { + _select->entity()->addItem( + itemId, + text, + activeBg, + std::move(paintUserpic)); + } +} diff --git a/Telegram/SourceFiles/boxes/peer_lists_box.h b/Telegram/SourceFiles/boxes/peer_lists_box.h new file mode 100644 index 000000000..09e8e92e2 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peer_lists_box.h @@ -0,0 +1,101 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "boxes/peer_list_box.h" + +class PeerListsBox : public Ui::BoxContent { +public: + PeerListsBox( + QWidget*, + std::vector> controllers, + Fn)> init); + + [[nodiscard]] std::vector> collectSelectedRows(); + +protected: + void prepare() override; + void setInnerFocus() override; + + void keyPressEvent(QKeyEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + +private: + class Delegate final : public PeerListContentDelegate { + public: + Delegate( + not_null box, + not_null controller); + + void peerListSetTitle(rpl::producer title) override; + void peerListSetAdditionalTitle(rpl::producer title) override; + void peerListSetSearchMode(PeerListSearchMode mode) override; + void peerListSetRowChecked( + not_null row, + bool checked) override; + void peerListSetForeignRowChecked( + not_null row, + bool checked, + anim::type animated) override; + bool peerListIsRowChecked(not_null row) override; + int peerListSelectedRowsCount() override; + void peerListScrollToTop() override; + + void peerListAddSelectedPeerInBunch(not_null peer) override { + _box->addSelectItem(peer, anim::type::instant); + } + void peerListAddSelectedRowInBunch(not_null row) override { + _box->addSelectItem(row, anim::type::instant); + } + void peerListFinishSelectedRowsBunch() override; + + private: + const not_null _box; + const not_null _controller; + + }; + struct List { + std::unique_ptr controller; + std::unique_ptr delegate; + PeerListContent *content = nullptr; + }; + + friend class Delegate; + + [[nodiscard]] List makeList( + std::unique_ptr controller); + [[nodiscard]] std::vector makeLists( + std::vector> controllers); + + [[nodiscard]] not_null firstController() const; + + void addSelectItem( + not_null peer, + anim::type animated); + void addSelectItem( + not_null row, + anim::type animated); + void addSelectItem( + uint64 itemId, + const QString &text, + PaintRoundImageCallback paintUserpic, + anim::type animated); + void setSearchMode(PeerListSearchMode mode); + void createMultiSelect(); + int getTopScrollSkip() const; + void updateScrollSkips(); + void searchQueryChanged(const QString &query); + + object_ptr> _select = { nullptr }; + + std::vector _lists; + Fn _init; + bool _scrollBottomFixed = false; + +}; diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index dda179e00..9ac503df3 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -51,28 +51,21 @@ base::flat_set> GetAlreadyInFromPeer(PeerData *peer) { } // namespace AddParticipantsBoxController::AddParticipantsBoxController( - not_null navigation) -: ContactsBoxController( - navigation, - std::make_unique(navigation)) { + not_null session) +: ContactsBoxController(session) { } AddParticipantsBoxController::AddParticipantsBoxController( - not_null navigation, not_null peer) : AddParticipantsBoxController( - navigation, peer, GetAlreadyInFromPeer(peer)) { } AddParticipantsBoxController::AddParticipantsBoxController( - not_null navigation, not_null peer, base::flat_set> &&alreadyIn) -: ContactsBoxController( - navigation, - std::make_unique(navigation)) +: ContactsBoxController(&peer->session()) , _peer(peer) , _alreadyIn(std::move(alreadyIn)) { subscribeToMigration(); @@ -179,7 +172,7 @@ bool AddParticipantsBoxController::inviteSelectedUsers( not_null box) const { Expects(_peer != nullptr); - const auto rows = box->peerListCollectSelectedRows(); + const auto rows = box->collectSelectedRows(); const auto users = ranges::view::all( rows ) | ranges::view::transform([](not_null peer) { @@ -198,9 +191,7 @@ bool AddParticipantsBoxController::inviteSelectedUsers( void AddParticipantsBoxController::Start( not_null navigation, not_null chat) { - auto controller = std::make_unique( - navigation, - chat); + auto controller = std::make_unique(chat); const auto weak = controller.get(); auto initBox = [=](not_null box) { box->addButton(tr::lng_participant_invite(), [=] { @@ -223,7 +214,6 @@ void AddParticipantsBoxController::Start( base::flat_set> &&alreadyIn, bool justCreated) { auto controller = std::make_unique( - navigation, channel, std::move(alreadyIn)); const auto weak = controller.get(); diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.h b/Telegram/SourceFiles/boxes/peers/add_participants_box.h index 122491c6d..7bba41119 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.h @@ -27,16 +27,16 @@ public: not_null channel, base::flat_set> &&alreadyIn); - explicit AddParticipantsBoxController( - not_null navigation); + explicit AddParticipantsBoxController(not_null session); + explicit AddParticipantsBoxController(not_null peer); AddParticipantsBoxController( - not_null navigation, - not_null peer); - AddParticipantsBoxController( - not_null navigation, not_null peer, base::flat_set> &&alreadyIn); + [[nodiscard]] not_null peer() const { + return _peer; + } + void rowClicked(not_null row) override; void itemDeselectedHook(not_null peer) override; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index c5dcbedb6..863fd2183 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "core/core_cloud_password.h" #include "base/unixtime.h" +#include "base/qt_adapters.h" #include "apiwrap.h" #include "main/main_session.h" #include "styles/style_layers.h" @@ -233,7 +234,8 @@ MTPChatAdminRights EditAdminBox::Defaults(not_null peer) { | Flag::f_delete_messages | Flag::f_ban_users | Flag::f_invite_users - | Flag::f_pin_messages) + | Flag::f_pin_messages + | Flag::f_manage_call) : (Flag::f_change_info | Flag::f_post_messages | Flag::f_edit_messages @@ -712,7 +714,7 @@ void EditRestrictedBox::showRestrictUntil() { highlighted, [this](const QDate &date) { setRestrictUntil( - static_cast(QDateTime(date).toTime_t())); + static_cast(base::QDateToDateTime(date).toTime_t())); }), Ui::LayerOption::KeepOther); _restrictUntilBox->setMaxDate( diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index fe972ddb5..a0ab9e949 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -760,6 +760,14 @@ ParticipantsBoxController::ParticipantsBoxController( not_null navigation, not_null peer, Role role) +: ParticipantsBoxController(CreateTag(), navigation, peer, role) { +} + +ParticipantsBoxController::ParticipantsBoxController( + CreateTag, + Window::SessionNavigation *navigation, + not_null peer, + Role role) : PeerListController(CreateSearchController(peer, role, &_additional)) , _navigation(navigation) , _peer(peer) @@ -797,8 +805,8 @@ void ParticipantsBoxController::setupListChangeViewers() { delegate()->peerListPartitionRows([&](const PeerListRow &row) { return (row.peer() == user); }); - } else { - delegate()->peerListPrependRow(createRow(user)); + } else if (auto row = createRow(user)) { + delegate()->peerListPrependRow(std::move(row)); delegate()->peerListRefreshRows(); if (_onlineSorter) { _onlineSorter->sort(); @@ -931,6 +939,8 @@ void ParticipantsBoxController::addNewItem() { } void ParticipantsBoxController::addNewParticipants() { + Expects(_navigation != nullptr); + const auto chat = _peer->asChat(); const auto channel = _peer->asChannel(); if (chat) { @@ -1361,9 +1371,12 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() { return false; } + auto added = false; _additional.fillFromPeer(); for (const auto user : info->lastParticipants) { - appendRow(user); + if (appendRow(user)) { + added = true; + } // // Don't count lastParticipants in _offset, because we don't know @@ -1375,7 +1388,7 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() { if (_onlineSorter) { _onlineSorter->sort(); } - return true; + return added; } void ParticipantsBoxController::rowClicked(not_null row) { @@ -1388,6 +1401,7 @@ void ParticipantsBoxController::rowClicked(not_null row) { && (_peer->isChat() || _peer->isMegagroup())) { showRestricted(user); } else { + Assert(_navigation != nullptr); _navigation->showPeerInfo(user); } } @@ -1415,9 +1429,11 @@ base::unique_qptr ParticipantsBoxController::rowContextMenu( const auto channel = _peer->asChannel(); const auto user = row->peer()->asUser(); auto result = base::make_unique_q(parent); - result->addAction( - tr::lng_context_view_profile(tr::now), - crl::guard(this, [=] { _navigation->showPeerInfo(user); })); + if (_navigation) { + result->addAction( + tr::lng_context_view_profile(tr::now), + crl::guard(this, [=] { _navigation->showPeerInfo(user); })); + } result->addAction( tr::ktg_context_show_messages_from(tr::now), crl::guard(this, [=] { App::searchByHashtag(QString(), _peer, user); })); @@ -1765,16 +1781,18 @@ bool ParticipantsBoxController::appendRow(not_null user) { if (delegate()->peerListFindRow(user->id)) { recomputeTypeFor(user); return false; + } else if (auto row = createRow(user)) { + delegate()->peerListAppendRow(std::move(row)); + if (_role != Role::Kicked) { + setDescriptionText(QString()); + } + return true; } - delegate()->peerListAppendRow(createRow(user)); - if (_role != Role::Kicked) { - setDescriptionText(QString()); - } - return true; + return false; } bool ParticipantsBoxController::prependRow(not_null user) { - if (auto row = delegate()->peerListFindRow(user->id)) { + if (const auto row = delegate()->peerListFindRow(user->id)) { recomputeTypeFor(user); refreshCustomStatus(row); if (_role == Role::Admins) { @@ -1782,12 +1800,14 @@ bool ParticipantsBoxController::prependRow(not_null user) { delegate()->peerListPrependRowFromSearchResult(row); } return false; + } else if (auto row = createRow(user)) { + delegate()->peerListPrependRow(std::move(row)); + if (_role != Role::Kicked) { + setDescriptionText(QString()); + } + return true; } - delegate()->peerListPrependRow(createRow(user)); - if (_role != Role::Kicked) { - setDescriptionText(QString()); - } - return true; + return false; } bool ParticipantsBoxController::removeRow(not_null user) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 65d8994fa..dfcb1ae6d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -135,7 +135,6 @@ private: // Viewing admins, banned or restricted users list with search. class ParticipantsBoxController : public PeerListController - , private base::Subscriber , public base::has_weak_ptr { public: using Role = ParticipantsRole; @@ -171,6 +170,16 @@ public: rpl::producer onlineCountValue() const override; protected: + // Allow child controllers not providing navigation. + // This is their responsibility to override all methods that use it. + struct CreateTag { + }; + ParticipantsBoxController( + CreateTag, + Window::SessionNavigation *navigation, + not_null peer, + Role role); + virtual std::unique_ptr createRow( not_null user) const; @@ -237,7 +246,9 @@ private: void subscribeToCreatorChange(not_null channel); void fullListRefresh(); - not_null _navigation; + // It may be nullptr in subclasses of this controller. + Window::SessionNavigation *_navigation = nullptr; + not_null _peer; MTP::Sender _api; Role _role = Role::Admins; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index ef554c482..0cc50501d 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -1000,13 +1000,15 @@ void Controller::fillManageSection() { st::infoIconBlacklist); } if (hasRecentActions) { + auto callback = [=] { + _navigation->showSection( + std::make_shared(channel)); + }; AddButtonWithCount( _controls.buttonsLayout, tr::lng_manage_peer_recent_actions(), rpl::single(QString()), //Empty count. - [=] { - _navigation->showSection(AdminLog::SectionMemento(channel)); - }, + std::move(callback), st::infoIconRecentActions); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index f1b75ba07..166d4a2c8 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -138,6 +138,7 @@ std::vector> AdminRightLabels( ? tr::lng_rights_group_invite_link(tr::now) : tr::lng_rights_group_invite(tr::now) }, { Flag::f_pin_messages, tr::lng_rights_group_pin(tr::now) }, + { Flag::f_manage_call, tr::lng_rights_group_manage_calls(tr::now) }, { Flag::f_anonymous, tr::lng_rights_group_anonymous(tr::now) }, { Flag::f_add_admins, tr::lng_rights_add_admins(tr::now) }, }; diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index e7d1f80be..041ae00e5 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -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() { diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index 0efa9b961..9f447dcfa 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.cpp +++ b/Telegram/SourceFiles/boxes/sessions_box.cpp @@ -292,7 +292,7 @@ void SessionsContent::terminateAll() { auto callback = [=] { const auto reset = crl::guard(weak, [=] { _authorizations->cancelCurrentRequest(); - shortPollSessions(); + _authorizations->reload(); }); _authorizations->requestTerminate( [=](const MTPBool &result) { reset(); }, diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index f407a3b57..eb6e4f4c5 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -179,7 +179,7 @@ ShareBox::ShareBox( , _hasMediaMessages(hasMedia) , _select( this, - st::contactsMultiSelect, + st::defaultMultiSelect, tr::lng_participant_filter()) , _comment( this, diff --git a/Telegram/SourceFiles/boxes/single_choice_box.cpp b/Telegram/SourceFiles/boxes/single_choice_box.cpp index 882b093a1..ea2cf9a10 100644 --- a/Telegram/SourceFiles/boxes/single_choice_box.cpp +++ b/Telegram/SourceFiles/boxes/single_choice_box.cpp @@ -21,11 +21,15 @@ SingleChoiceBox::SingleChoiceBox( rpl::producer title, const std::vector &optionTexts, int initialSelection, - Fn callback) + Fn callback, + const style::Checkbox *st, + const style::Radio *radioSt) : _title(std::move(title)) , _optionTexts(optionTexts) , _initialSelection(initialSelection) -, _callback(callback) { +, _callback(callback) +, _st(st ? *st : st::defaultBoxCheckbox) +, _radioSt(radioSt ? *radioSt : st::defaultRadio) { } void SingleChoiceBox::prepare() { @@ -47,7 +51,8 @@ void SingleChoiceBox::prepare() { group, i, text, - st::defaultBoxCheckbox), + _st, + _radioSt), QMargins( st::boxPadding.left() + st::boxOptionListPadding.left(), 0, diff --git a/Telegram/SourceFiles/boxes/single_choice_box.h b/Telegram/SourceFiles/boxes/single_choice_box.h index a5c93f5a6..2a83c8e29 100644 --- a/Telegram/SourceFiles/boxes/single_choice_box.h +++ b/Telegram/SourceFiles/boxes/single_choice_box.h @@ -14,6 +14,10 @@ namespace Ui { class Radiobutton; } // namespace Ui +namespace style { +struct Checkbox; +} // namespace style + class SingleChoiceBox : public Ui::BoxContent { public: SingleChoiceBox( @@ -21,7 +25,9 @@ public: rpl::producer title, const std::vector &optionTexts, int initialSelection, - Fn callback); + Fn callback, + const style::Checkbox *st = nullptr, + const style::Radio *radioSt = nullptr); protected: void prepare() override; @@ -31,6 +37,8 @@ private: std::vector _optionTexts; int _initialSelection = 0; Fn _callback; + const style::Checkbox &_st; + const style::Radio &_radioSt; }; diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index ad69cce2e..10b8e783f 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -393,14 +393,20 @@ void StickerSetBox::Inner::gotSet(const MTPmessages_StickerSet &set) { _setHash = set.vhash().v; _setFlags = set.vflags().v; _setInstallDate = set.vinstalled_date().value_or(0); - if (const auto thumb = set.vthumb()) { - _setThumbnail = Images::FromPhotoSize( - &_controller->session(), - set, - *thumb); - } else { - _setThumbnail = ImageWithLocation(); - } + _setThumbnail = [&] { + if (const auto thumbs = set.vthumbs()) { + for (const auto &thumb : thumbs->v) { + const auto result = Images::FromPhotoSize( + &_controller->session(), + set, + thumb); + if (result.location.valid()) { + return result; + } + } + } + return ImageWithLocation(); + }(); const auto &sets = _controller->session().data().stickers().sets(); const auto it = sets.find(_setId); if (it != sets.cend()) { @@ -656,8 +662,8 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) { QSize StickerSetBox::Inner::boundingBoxSize() const { return QSize( - st::stickersSize.width() - st::buttonRadius * 2, - st::stickersSize.height() - st::buttonRadius * 2); + st::stickersSize.width() - st::roundRadiusSmall * 2, + st::stickersSize.height() - st::roundRadiusSmall * 2); } void StickerSetBox::Inner::visibleTopBottomUpdated( @@ -743,7 +749,7 @@ void StickerSetBox::Inner::paintSticker( w = std::max(size.width(), 1); h = std::max(size.height(), 1); } else { - auto coef = qMin((st::stickersSize.width() - st::buttonRadius * 2) / float64(document->dimensions.width()), (st::stickersSize.height() - st::buttonRadius * 2) / float64(document->dimensions.height())); + auto coef = qMin((st::stickersSize.width() - st::roundRadiusSmall * 2) / float64(document->dimensions.width()), (st::stickersSize.height() - st::roundRadiusSmall * 2) / float64(document->dimensions.height())); if (coef > 1) coef = 1; w = std::max(qRound(coef * document->dimensions.width()), 1); h = std::max(qRound(coef * document->dimensions.height()), 1); diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 5ed95a24b..6ee3d8930 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -1369,7 +1369,7 @@ void StickersBox::Inner::setActionDown(int newActionDown) { if (_section == Section::Installed) { if (row->removed) { auto rippleSize = QSize(_undoWidth - st::stickersUndoRemove.width, st::stickersUndoRemove.height); - auto rippleMask = Ui::RippleAnimation::roundRectMask(rippleSize, st::buttonRadius); + auto rippleMask = Ui::RippleAnimation::roundRectMask(rippleSize, st::roundRadiusSmall); ensureRipple(st::stickersUndoRemove.ripple, std::move(rippleMask), removeButton); } else { auto rippleSize = st::stickersRemove.rippleAreaSize; @@ -1378,7 +1378,7 @@ void StickersBox::Inner::setActionDown(int newActionDown) { } } else if (!row->installed || row->archived || row->removed) { auto rippleSize = QSize(_addWidth - st::stickersTrendingAdd.width, st::stickersTrendingAdd.height); - auto rippleMask = Ui::RippleAnimation::roundRectMask(rippleSize, st::buttonRadius); + auto rippleMask = Ui::RippleAnimation::roundRectMask(rippleSize, st::roundRadiusSmall); ensureRipple(st::stickersTrendingAdd.ripple, std::move(rippleMask), removeButton); } } @@ -1436,7 +1436,7 @@ void StickersBox::Inner::setPressed(SelectedRow pressed) { auto &set = _rows[pressedIndex]; auto rippleMask = Ui::RippleAnimation::rectMask(QSize(width(), _rowHeight)); if (!set->ripple) { - set->ripple = std::make_unique(st::contactsRipple, std::move(rippleMask), [this, pressedIndex] { + set->ripple = std::make_unique(st::defaultRippleAnimation, std::move(rippleMask), [this, pressedIndex] { update(0, _itemsTop + pressedIndex * _rowHeight, width(), _rowHeight); }); } diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index ae044b7c9..d1921073f 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL using "ui/basic.style"; using "ui/widgets/widgets.style"; +using "ui/layers/layers.style"; using "window/window.style"; CallSignalBars { @@ -22,14 +23,14 @@ CallSignalBars { callRadius: 6px; callShadow: Shadow { - left: icon {{ "call_shadow_left", windowShadowFg }}; - topLeft: icon {{ "call_shadow_top_left", windowShadowFg }}; - top: icon {{ "call_shadow_top", windowShadowFg }}; - topRight: icon {{ "call_shadow_top_left-flip_horizontal", windowShadowFg }}; - right: icon {{ "call_shadow_left-flip_horizontal", windowShadowFg }}; - bottomRight: icon {{ "call_shadow_top_left-flip_vertical-flip_horizontal", windowShadowFg }}; - bottom: icon {{ "call_shadow_top-flip_vertical", windowShadowFg }}; - bottomLeft: icon {{ "call_shadow_top_left-flip_vertical", windowShadowFg }}; + left: icon {{ "calls/call_shadow_left", windowShadowFg }}; + topLeft: icon {{ "calls/call_shadow_top_left", windowShadowFg }}; + top: icon {{ "calls/call_shadow_top", windowShadowFg }}; + topRight: icon {{ "calls/call_shadow_top_left-flip_horizontal", windowShadowFg }}; + right: icon {{ "calls/call_shadow_left-flip_horizontal", windowShadowFg }}; + bottomRight: icon {{ "calls/call_shadow_top_left-flip_vertical-flip_horizontal", windowShadowFg }}; + bottom: icon {{ "calls/call_shadow_top-flip_vertical", windowShadowFg }}; + bottomLeft: icon {{ "calls/call_shadow_top_left-flip_vertical", windowShadowFg }}; extend: margins(9px, 8px, 9px, 10px); fallback: windowShadowFgFallback; } @@ -72,7 +73,7 @@ callBodyWithPreview: CallBodyLayout { muteSize: 0px; mutePosition: point(90px, 84px); } -callMutedPeerIcon: icon {{ "calls_mute_userpic", callIconFg }}; +callMutedPeerIcon: icon {{ "calls/calls_mute_userpic", callIconFg }}; callOutgoingPreviewMin: size(360px, 120px); callOutgoingPreview: size(540px, 180px); // default, for height == callHeight. @@ -89,7 +90,7 @@ callSignalBarsPadding: margins(8px, 9px, 11px, 5px); callFingerprintTop: 8px; callFingerprintBottom: -16px; -callTooltipMutedIcon: icon{{ "calls_mute_tooltip", videoPlayIconFg }}; +callTooltipMutedIcon: icon{{ "calls/calls_mute_tooltip", videoPlayIconFg }}; callTooltipMutedIconPosition: point(10px, 5px); callTooltipPadding: margins(41px, 7px, 15px, 8px); @@ -114,20 +115,22 @@ callButtonLabel: FlatLabel(defaultFlatLabel) { callAnswer: CallButton { button: IconButton(callButton) { - icon: icon {{ "call_answer", callIconFg }}; + icon: icon {{ "calls/call_answer", callIconFg }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callAnswerRipple; } } bg: callAnswerBg; + bgSize: 44px; + bgPosition: point(12px, 12px); angle: 135.; outerRadius: 12px; outerBg: callAnswerBgOuter; label: callButtonLabel; } -callHangup: CallButton { +callHangup: CallButton(callAnswer) { button: IconButton(callButton) { - icon: icon {{ "call_discard", callIconFg }}; + icon: icon {{ "calls/call_discard", callIconFg }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callHangupRipple; } @@ -136,9 +139,9 @@ callHangup: CallButton { outerBg: callHangupBg; label: callButtonLabel; } -callCancel: CallButton { +callCancel: CallButton(callAnswer) { button: IconButton(callButton) { - icon: icon {{ "call_cancel", callIconFgActive }}; + icon: icon {{ "calls/call_cancel", callIconFgActive }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callIconActiveRipple; } @@ -147,9 +150,9 @@ callCancel: CallButton { outerBg: callIconBgActive; label: callButtonLabel; } -callMicrophoneMute: CallButton { +callMicrophoneMute: CallButton(callAnswer) { button: IconButton(callButton) { - icon: icon {{ "call_record_active", callIconFg }}; + icon: icon {{ "calls/call_record_active", callIconFg }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callMuteRipple; } @@ -160,7 +163,7 @@ callMicrophoneMute: CallButton { } callMicrophoneUnmute: CallButton(callMicrophoneMute) { button: IconButton(callButton) { - icon: icon {{ "call_record_muted", callIconFgActive }}; + icon: icon {{ "calls/call_record_muted", callIconFgActive }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callIconActiveRipple; } @@ -169,7 +172,7 @@ callMicrophoneUnmute: CallButton(callMicrophoneMute) { } callCameraMute: CallButton(callMicrophoneMute) { button: IconButton(callButton) { - icon: icon {{ "call_camera_active", callIconFg }}; + icon: icon {{ "calls/call_camera_active", callIconFg }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callMuteRipple; } @@ -177,7 +180,7 @@ callCameraMute: CallButton(callMicrophoneMute) { } callCameraUnmute: CallButton(callMicrophoneUnmute) { button: IconButton(callButton) { - icon: icon {{ "call_camera_muted", callIconFgActive }}; + icon: icon {{ "calls/call_camera_muted", callIconFgActive }}; ripple: RippleAnimation(defaultRippleAnimation) { color: callIconActiveRipple; } @@ -223,7 +226,7 @@ callBarMuteToggle: IconButton { width: 41px; height: 38px; - icon: icon {{ "call_record_active", callBarFg }}; + icon: icon {{ "calls/call_record_active", callBarFg }}; iconPosition: point(3px, 2px); ripple: RippleAnimation(defaultRippleAnimation) { @@ -232,11 +235,11 @@ callBarMuteToggle: IconButton { rippleAreaPosition: point(5px, 3px); rippleAreaSize: 32px; } -callBarUnmuteIcon: icon {{ "call_record_muted", callBarFg }}; +callBarUnmuteIcon: icon {{ "calls/call_record_muted", callBarFg }}; callBarRightSkip: 12px; callBarSkip: 10px; callBarHangup: IconButton(callBarMuteToggle) { - icon: icon {{ "call_discard", callBarFg }}; + icon: icon {{ "calls/call_discard", callBarFg }}; iconPosition: point(3px, 1px); } callBarLabel: LabelSimple(defaultLabelSimple) { @@ -254,16 +257,16 @@ callBarInfoLabel: FlatLabel(defaultFlatLabel) { callBarLabelTop: 10px; callArrowPosition: point(-2px, 1px); -callArrowIn: icon {{ "call_arrow_in", callArrowFg }}; -callArrowOut: icon {{ "call_arrow_out", callArrowFg }}; -callArrowMissed: icon {{ "call_arrow_in", callArrowMissedFg }}; +callArrowIn: icon {{ "calls/call_arrow_in", callArrowFg }}; +callArrowOut: icon {{ "calls/call_arrow_out", callArrowFg }}; +callArrowMissed: icon {{ "calls/call_arrow_in", callArrowMissedFg }}; callArrowSkip: 4px; callReDial: IconButton { width: 40px; height: 56px; - icon: icon {{ "call_answer", menuIconFg }}; - iconOver: icon {{ "call_answer", menuIconFgOver }}; + icon: icon {{ "calls/call_answer", menuIconFg }}; + iconOver: icon {{ "calls/call_answer", menuIconFgOver }}; iconPosition: point(-1px, -1px); ripple: defaultRippleAnimation; @@ -272,8 +275,8 @@ callReDial: IconButton { } callCameraReDial: IconButton(callReDial) { - icon: icon {{ "call_camera_active", menuIconFg }}; - iconOver: icon {{ "call_camera_active", menuIconFgOver }}; + icon: icon {{ "calls/call_camera_active", menuIconFg }}; + iconOver: icon {{ "calls/call_camera_active", menuIconFgOver }}; } callRatingPadding: margins(24px, 12px, 24px, 0px); @@ -281,7 +284,7 @@ callRatingStar: IconButton { width: 36px; height: 36px; - icon: icon {{ "call_rating", windowSubTextFg }}; + icon: icon {{ "calls/call_rating", windowSubTextFg }}; iconPosition: point(-1px, -1px); ripple: RippleAnimation(defaultRippleAnimation) { @@ -290,7 +293,7 @@ callRatingStar: IconButton { rippleAreaPosition: point(0px, 0px); rippleAreaSize: 36px; } -callRatingStarFilled: icon {{ "call_rating_filled", lightButtonFg }}; +callRatingStarFilled: icon {{ "calls/call_rating_filled", lightButtonFg }}; callRatingStarTop: 4px; callRatingComment: InputField(defaultInputField) { textMargins: margins(1px, 26px, 1px, 4px); @@ -327,43 +330,43 @@ callTitleButton: IconButton { iconPosition: point(0px, 0px); } callTitleMinimizeIcon: icon { - { "calls_minimize_shadow", windowShadowFg }, - { "calls_minimize_main", callNameFg }, + { "calls/calls_minimize_shadow", windowShadowFg }, + { "calls/calls_minimize_main", callNameFg }, }; callTitleMinimizeIconOver: icon { { size(34px, 30px), callBgButton }, { size(34px, 30px), callMuteRipple }, - { "calls_minimize_shadow", windowShadowFg }, - { "calls_minimize_main", callNameFg }, + { "calls/calls_minimize_shadow", windowShadowFg }, + { "calls/calls_minimize_main", callNameFg }, }; callTitleMaximizeIcon: icon { - { "calls_maximize_shadow", windowShadowFg }, - { "calls_maximize_main", callNameFg }, + { "calls/calls_maximize_shadow", windowShadowFg }, + { "calls/calls_maximize_main", callNameFg }, }; callTitleMaximizeIconOver: icon { { size(34px, 30px), callBgButton }, { size(34px, 30px), callMuteRipple }, - { "calls_maximize_shadow", windowShadowFg }, - { "calls_maximize_main", callNameFg }, + { "calls/calls_maximize_shadow", windowShadowFg }, + { "calls/calls_maximize_main", callNameFg }, }; callTitleRestoreIcon: icon { - { "calls_restore_shadow", windowShadowFg }, - { "calls_restore_main", callNameFg }, + { "calls/calls_restore_shadow", windowShadowFg }, + { "calls/calls_restore_main", callNameFg }, }; callTitleRestoreIconOver: icon { { size(34px, 30px), callBgButton }, { size(34px, 30px), callMuteRipple }, - { "calls_restore_shadow", windowShadowFg }, - { "calls_restore_main", callNameFg }, + { "calls/calls_restore_shadow", windowShadowFg }, + { "calls/calls_restore_main", callNameFg }, }; callTitleCloseIcon: icon { - { "calls_close_shadow", windowShadowFg }, - { "calls_close_main", callNameFg }, + { "calls/calls_close_shadow", windowShadowFg }, + { "calls/calls_close_main", callNameFg }, }; callTitleCloseIconOver: icon { { size(34px, 30px), titleButtonCloseBgOver }, - { "calls_close_shadow", windowShadowFg }, - { "calls_close_main", titleButtonCloseFgOver }, + { "calls/calls_close_shadow", windowShadowFg }, + { "calls/calls_close_main", titleButtonCloseFgOver }, }; callTitle: WindowTitle(defaultWindowTitle) { height: 0px; @@ -394,8 +397,387 @@ callTitle: WindowTitle(defaultWindowTitle) { closeIconActive: callTitleCloseIcon; closeIconActiveOver: callTitleCloseIconOver; } -callTitleShadow: icon {{ "calls_shadow_controls", windowShadowFg }}; +callTitleShadow: icon {{ "calls/calls_shadow_controls", windowShadowFg }}; callErrorToast: Toast(defaultToast) { minWidth: 240px; } + +groupCallWidth: 380px; +groupCallHeight: 580px; + +groupCallRipple: RippleAnimation(defaultRippleAnimation) { + color: groupCallMembersBgRipple; +} + +groupCallMenu: Menu(defaultMenu) { + itemBg: groupCallMenuBg; + itemBgOver: groupCallMenuBgOver; + itemFg: groupCallMembersFg; + itemFgOver: groupCallMembersFg; + itemFgDisabled: groupCallMemberNotJoinedStatus; + itemFgShortcut: groupCallMemberNotJoinedStatus; + itemFgShortcutOver: groupCallMemberNotJoinedStatus; + itemFgShortcutDisabled: groupCallMemberNotJoinedStatus; + + separatorFg: groupCallMemberNotJoinedStatus; + + arrow: icon {{ "dropdown_submenu_arrow", groupCallMemberNotJoinedStatus }}; + + ripple: RippleAnimation(defaultRippleAnimation) { + color: groupCallMenuBgRipple; + } +} +groupCallMenuShadow: Shadow(defaultEmptyShadow) { + fallback: groupCallMenuBg; +} +groupCallPanelAnimation: PanelAnimation(defaultPanelAnimation) { + fadeBg: groupCallMenuBg; + shadow: groupCallMenuShadow; +} +groupCallPopupMenu: PopupMenu(defaultPopupMenu) { + shadow: groupCallMenuShadow; + menu: groupCallMenu; + animation: groupCallPanelAnimation; +} + +groupCallMembersListItem: PeerListItem(defaultPeerListItem) { + button: OutlineButton(defaultPeerListButton) { + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; + + textFg: groupCallMemberInactiveStatus; + textFgOver: groupCallMemberInactiveStatus; + + font: normalFont; + padding: margins(11px, 5px, 11px, 5px); + + ripple: groupCallRipple; + } + disabledCheckFg: groupCallMemberNotJoinedStatus; + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + selectFg: groupCallActiveFg; + check: RoundCheckbox(defaultPeerListCheck) { + border: groupCallMembersBg; + bgActive: groupCallActiveFg; + check: icon {{ "default_checkbox_check", groupCallMembersFg, point(3px, 6px) }}; + } + } + height: 52px; + photoPosition: point(12px, 6px); + namePosition: point(63px, 7px); + statusPosition: point(63px, 26px); + photoSize: 40px; + nameFg: groupCallMembersFg; + nameFgChecked: groupCallMembersFg; + statusFg: groupCallMemberInactiveStatus; + statusFgOver: groupCallMemberInactiveStatus; + statusFgActive: groupCallMemberActiveStatus; +} +groupCallMembersList: PeerList(defaultPeerList) { + bg: groupCallMembersBg; + about: FlatLabel(defaultPeerListAbout) { + textFg: groupCallMemberNotJoinedStatus; + } + item: groupCallMembersListItem; +} +groupCallInviteDividerLabel: FlatLabel(defaultFlatLabel) { + textFg: groupCallMemberNotJoinedStatus; +} +groupCallInviteDividerPadding: margins(17px, 7px, 17px, 7px); + +groupCallInviteMembersList: PeerList(groupCallMembersList) { + padding: margins(0px, 10px, 0px, 10px); + item: PeerListItem(groupCallMembersListItem) { + statusFg: groupCallMemberNotJoinedStatus; + statusFgOver: groupCallMemberNotJoinedStatus; + statusFgActive: groupCallMemberInactiveStatus; + } +} +groupCallMultiSelect: MultiSelect(defaultMultiSelect) { + bg: groupCallMembersBg; + item: MultiSelectItem(defaultMultiSelectItem) { + textBg: groupCallMembersBgRipple; + textFg: groupCallMembersFg; + textActiveBg: groupCallActiveFg; + textActiveFg: groupCallMembersFg; + deleteFg: groupCallMembersFg; + } + field: InputField(defaultMultiSelectSearchField) { + textFg: groupCallMembersFg; + placeholderFg: groupCallMemberNotJoinedStatus; + placeholderFgActive: groupCallMemberNotJoinedStatus; + placeholderFgError: groupCallMemberNotJoinedStatus; + menu: groupCallPopupMenu; + } + fieldIcon: icon {{ "box_search", groupCallMemberNotJoinedStatus, point(10px, 9px) }}; + fieldCancel: CrossButton(defaultMultiSelectSearchCancel) { + crossFg: groupCallMemberNotJoinedStatus; + crossFgOver: groupCallMemberNotJoinedStatus; + ripple: groupCallRipple; + } +} + +groupCallMembersTop: 62px; +groupCallTitleTop: 14px; +groupCallSubtitleTop: 33px; + +groupCallMembersMargin: margins(16px, 16px, 16px, 28px); +groupCallAddMember: SettingsButton(defaultSettingsButton) { + textFg: groupCallMemberNotJoinedStatus; + textFgOver: groupCallMemberNotJoinedStatus; + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; + + font: semiboldFont; + + height: 22px; + padding: margins(63px, 17px, 22px, 11px); + + ripple: groupCallRipple; +} +groupCallAddMemberIcon: icon {{ "info_add_member", groupCallMemberInactiveIcon, point(0px, 3px) }}; +groupCallAddMemberIconLeft: 16px; +groupCallSubtitleLabel: FlatLabel(defaultFlatLabel) { + maxHeight: 18px; + textFg: groupCallMemberNotJoinedStatus; +} +groupCallTitleLabel: FlatLabel(groupCallSubtitleLabel) { + textFg: groupCallMembersFg; + style: TextStyle(defaultTextStyle) { + font: font(semibold 14px); + linkFont: font(semibold 14px); + linkFontOver: font(semibold 14px); + } +} +groupCallAddButtonPosition: point(10px, 7px); +groupCallMembersWidthMax: 360px; + +groupCallActiveButton: IconButton { + width: 36px; + height: 52px; + + icon: icon {{ "calls/group_calls_unmuted", groupCallMemberInactiveIcon }}; + iconOver: icon {{ "calls/group_calls_unmuted", groupCallMemberInactiveIcon }}; + iconPosition: point(-1px, -1px); + + ripple: groupCallRipple; + rippleAreaPosition: point(0px, 8px); + rippleAreaSize: 36px; +} +groupCallMemberButtonSkip: 10px; + +groupCallMemberInactiveCrossLine: CrossLineAnimation { + fg: groupCallMemberInactiveIcon; + icon: icon {{ "calls/group_calls_unmuted", groupCallMemberInactiveIcon }}; + startPosition: point(5px, 2px); + endPosition: point(20px, 17px); + stroke: 2px; +} +groupCallMemberColoredCrossLine: CrossLineAnimation(groupCallMemberInactiveCrossLine) { + fg: groupCallMemberMutedIcon; + icon: icon {{ "calls/group_calls_unmuted", groupCallMemberActiveIcon }}; +} +groupCallMemberInvited: icon {{ "calls/group_calls_invited", groupCallMemberInactiveIcon }}; +groupCallMemberInvitedPosition: point(2px, 12px); + +groupCallSettings: CallButton(callMicrophoneMute) { + button: IconButton(callButton) { + iconPosition: point(-1px, 22px); + icon: icon {{ "calls/call_settings", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callMuteRipple; + } + } +} +groupCallHangup: CallButton(callHangup) { + button: IconButton(callButton) { + icon: icon {{ "calls/call_discard", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: groupCallLeaveBgRipple; + } + } + bg: groupCallLeaveBg; + outerBg: groupCallLeaveBg; + label: callButtonLabel; +} +groupCallButtonSkip: 43px; +groupCallButtonBottomSkip: 145px; +groupCallMuteBottomSkip: 160px; + +groupCallTopBarUserpicSize: 28px; +groupCallTopBarUserpicShift: 8px; +groupCallTopBarUserpicStroke: 2px; +groupCallTopBarJoin: RoundButton(defaultActiveButton) { + width: -26px; + height: 26px; + textTop: 4px; +} +groupCallBox: Box(defaultBox) { + button: RoundButton(defaultBoxButton) { + textFg: groupCallActiveFg; + textFgOver: groupCallActiveFg; + numbersTextFg: groupCallActiveFg; + numbersTextFgOver: groupCallActiveFg; + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; + + ripple: groupCallRipple; + } + margin: margins(0px, 56px, 0px, 10px); + bg: groupCallMembersBg; + title: FlatLabel(boxTitle) { + textFg: groupCallMembersFg; + } + titleAdditionalFg: groupCallMemberNotJoinedStatus; +} +groupCallLayerBox: Box(groupCallBox) { + buttonPadding: margins(8px, 8px, 8px, 8px); +} +groupCallLevelMeter: LevelMeter(defaultLevelMeter) { + height: 18px; + lineWidth: 3px; + lineSpacing: 5px; + lineCount: 44; + activeFg: groupCallActiveFg; + inactiveFg: groupCallMemberNotJoinedStatus; +} +groupCallCheckboxIcon: icon {{ "default_checkbox_check", groupCallMembersFg, point(4px, 7px) }}; +groupCallCheck: Check(defaultCheck) { + untoggledFg: groupCallMemberNotJoinedStatus; + toggledFg: groupCallActiveFg; + icon: groupCallCheckboxIcon; +} +groupCallRadio: Radio(defaultRadio) { + untoggledFg: groupCallMemberNotJoinedStatus; + toggledFg: groupCallActiveFg; +} +groupCallCheckbox: Checkbox(defaultBoxCheckbox) { + textFg: groupCallMembersFg; + textFgActive: groupCallMembersFg; + rippleBg: groupCallMembersBgRipple; + rippleBgActive: groupCallMembersBgRipple; +} + +groupCallSettingsToggle: Toggle(defaultToggle) { + toggledBg: groupCallMembersBg; + toggledFg: groupCallActiveFg; + untoggledBg: groupCallMembersBg; + untoggledFg: groupCallMemberNotJoinedStatus; +} +groupCallSettingsButton: SettingsButton(defaultSettingsButton) { + textFg: groupCallMembersFg; + textFgOver: groupCallMembersFg; + textBg: groupCallMembersBg; + textBgOver: groupCallMembersBgOver; + rightLabel: FlatLabel(defaultSettingsRightLabel) { + textFg: groupCallActiveFg; + } + toggle: groupCallSettingsToggle; + toggleOver: groupCallSettingsToggle; + ripple: groupCallRipple; +} +groupCallSettingsAttentionButton: SettingsButton(groupCallSettingsButton) { + textFg: attentionButtonFg; + textFgOver: attentionButtonFgOver; +} +groupCallBoxLabel: FlatLabel(boxLabel) { + textFg: groupCallMembersFg; +} + +groupCallRowBlobMinRadius: 27px; +groupCallRowBlobMaxRadius: 29px; + +groupCallDelayLabel: LabelSimple(defaultLabelSimple) { + textFg: groupCallMembersFg; + font: boxTextFont; +} +groupCallDelayLabelMargin: margins(22px, 10px, 20px, 5px); +groupCallDelaySlider: MediaSlider(defaultContinuousSlider) { + seekSize: size(15px, 15px); + activeFg: groupCallActiveFg; + inactiveFg: groupCallMemberNotJoinedStatus; + activeFgOver: groupCallActiveFg; + inactiveFgOver: groupCallMemberNotJoinedStatus; + activeFgDisabled: groupCallActiveFg; + inactiveFgDisabled: groupCallMemberNotJoinedStatus; + receivedTillFg: groupCallMemberNotJoinedStatus; +} +groupCallDelayMargin: margins(22px, 5px, 20px, 10px); + +groupCallTitleButton: IconButton { + width: 24px; + height: 21px; + iconPosition: point(0px, 0px); +} +groupCallTitleMinimizeIcon: icon { + { "title_button_minimize", groupCallMemberNotJoinedStatus, point(4px, 4px) }, +}; +groupCallTitleMinimizeIconOver: icon { + { size(24px, 21px), groupCallMembersBgOver }, + { "title_button_minimize", groupCallMembersFg, point(4px, 4px) }, +}; +groupCallTitleMaximizeIcon: icon { + { "title_button_maximize", groupCallMemberNotJoinedStatus, point(4px, 4px) }, +}; +groupCallTitleMaximizeIconOver: icon { + { size(24px, 21px), groupCallMembersBgOver }, + { "title_button_maximize", groupCallMembersFg, point(4px, 4px) }, +}; +groupCallTitleRestoreIcon: icon { + { "title_button_restore", groupCallMemberNotJoinedStatus, point(4px, 4px) }, +}; +groupCallTitleRestoreIconOver: icon { + { size(24px, 21px), groupCallMembersBgOver }, + { "title_button_restore", groupCallMembersFg, point(4px, 4px) }, +}; +groupCallTitleCloseIcon: icon { + { "title_button_close", groupCallMemberNotJoinedStatus, point(4px, 4px) }, +}; +groupCallTitleCloseIconOver: icon { + { size(24px, 21px), titleButtonCloseBgOver }, + { "title_button_close", titleButtonCloseFgOver, point(4px, 4px) }, +}; +groupCallTitle: WindowTitle(defaultWindowTitle) { + height: 0px; + bg: transparent; + bgActive: transparent; + fg: transparent; + fgActive: transparent; + minimize: IconButton(groupCallTitleButton) { + icon: groupCallTitleMinimizeIcon; + iconOver: groupCallTitleMinimizeIconOver; + } + minimizeIconActive: groupCallTitleMinimizeIcon; + minimizeIconActiveOver: groupCallTitleMinimizeIconOver; + maximize: IconButton(groupCallTitleButton) { + icon: groupCallTitleMaximizeIcon; + iconOver: groupCallTitleMaximizeIconOver; + } + maximizeIconActive: groupCallTitleMaximizeIcon; + maximizeIconActiveOver: groupCallTitleMaximizeIconOver; + restoreIcon: groupCallTitleRestoreIcon; + restoreIconOver: groupCallTitleRestoreIconOver; + restoreIconActive: groupCallTitleRestoreIcon; + restoreIconActiveOver: groupCallTitleRestoreIconOver; + close: IconButton(groupCallTitleButton) { + icon: groupCallTitleCloseIcon; + iconOver: groupCallTitleCloseIconOver; + } + closeIconActive: groupCallTitleCloseIcon; + closeIconActiveOver: groupCallTitleCloseIconOver; +} + +groupCallMajorBlobIdleRadius: 2px; +groupCallMajorBlobMaxRadius: 4px; + +groupCallMinorBlobIdleRadius: 3px; +groupCallMinorBlobMaxRadius: 12px; + +callTopBarMuteCrossLine: CrossLineAnimation { + fg: callBarFg; + icon: icon {{ "calls/call_record_active", callBarFg }}; + startPosition: point(11px, 8px); + endPosition: point(26px, 23px); + stroke: 2px; +} diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 4ba508eca..e78dbd8e9 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -288,10 +288,19 @@ void Call::startIncoming() { }).send(); } +void Call::switchVideoOutgoing() { + const auto video = _videoOutgoing->state() == Webrtc::VideoState::Active; + _delegate->callRequestPermissionsOrFail(crl::guard(this, [=] { + videoOutgoing()->setState(StartVideoState(!video)); + }), true); + +} + void Call::answer() { - _delegate->requestPermissionsOrFail(crl::guard(this, [=] { + const auto video = _videoOutgoing->state() == Webrtc::VideoState::Active; + _delegate->callRequestPermissionsOrFail(crl::guard(this, [=] { actuallyAnswer(); - })); + }), video); } void Call::actuallyAnswer() { @@ -776,11 +785,11 @@ void Call::createAndStartController(const MTPDphoneCall &call) { auto callLogPath = callLogFolder + qsl("/last_call_log.txt"); auto callLogNative = QDir::toNativeSeparators(callLogPath); #ifdef Q_OS_WIN - descriptor.config.logPath = callLogNative.toStdWString(); + descriptor.config.logPath.data = callLogNative.toStdWString(); #else // Q_OS_WIN const auto callLogUtf = QFile::encodeName(callLogNative); - descriptor.config.logPath.resize(callLogUtf.size()); - ranges::copy(callLogUtf, descriptor.config.logPath.begin()); + descriptor.config.logPath.data.resize(callLogUtf.size()); + ranges::copy(callLogUtf, descriptor.config.logPath.data.begin()); #endif // Q_OS_WIN QFile(callLogPath).remove(); QDir().mkpath(callLogFolder); @@ -942,20 +951,20 @@ void Call::setState(State state) { _startTime = crl::now(); break; case State::ExchangingKeys: - _delegate->playSound(Delegate::Sound::Connecting); + _delegate->callPlaySound(Delegate::CallSound::Connecting); break; case State::Ended: - _delegate->playSound(Delegate::Sound::Ended); + _delegate->callPlaySound(Delegate::CallSound::Ended); [[fallthrough]]; case State::EndedByOtherDevice: _delegate->callFinished(this); break; case State::Failed: - _delegate->playSound(Delegate::Sound::Ended); + _delegate->callPlaySound(Delegate::CallSound::Ended); _delegate->callFailed(this); break; case State::Busy: - _delegate->playSound(Delegate::Sound::Busy); + _delegate->callPlaySound(Delegate::CallSound::Busy); break; } } @@ -978,15 +987,15 @@ void Call::setCurrentVideoDevice(const QString &deviceId) { } } -void Call::setAudioVolume(bool input, float level) { - if (_instance) { - if (input) { - _instance->setInputVolume(level); - } else { - _instance->setOutputVolume(level); - } - } -} +//void Call::setAudioVolume(bool input, float level) { +// if (_instance) { +// if (input) { +// _instance->setInputVolume(level); +// } else { +// _instance->setOutputVolume(level); +// } +// } +//} void Call::setAudioDuckingEnabled(bool enabled) { if (_instance) { @@ -1027,7 +1036,12 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) { || (_videoOutgoing->state() != Webrtc::VideoState::Inactive)) ? MTPphone_DiscardCall::Flag::f_video : MTPphone_DiscardCall::Flag(0); - _api.request(MTPphone_DiscardCall( + + // We want to discard request still being sent and processed even if + // the call is already destroyed. + const auto session = &_user->session(); + const auto weak = base::make_weak(this); + session->api().request(MTPphone_DiscardCall( // We send 'discard' here. MTP_flags(flags), MTP_inputPhoneCall( MTP_long(_id), @@ -1038,11 +1052,11 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) { )).done([=](const MTPUpdates &result) { // Here 'this' could be destroyed by updates, so we set Ended after // updates being handled, but in a guarded way. - crl::on_main(this, [=] { setState(finalState); }); - _user->session().api().applyUpdates(result); - }).fail([this, finalState](const RPCError &error) { + crl::on_main(weak, [=] { setState(finalState); }); + session->api().applyUpdates(result); + }).fail(crl::guard(weak, [this, finalState](const RPCError &error) { setState(finalState); - }).send(); + })).send(); } void Call::setStateQueued(State state) { diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 139a037ad..e4dcf2d65 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -62,13 +62,16 @@ public: virtual void callFailed(not_null call) = 0; virtual void callRedial(not_null call) = 0; - enum class Sound { + enum class CallSound { Connecting, Busy, Ended, }; - virtual void playSound(Sound sound) = 0; - virtual void requestPermissionsOrFail(Fn onSuccess) = 0; + virtual void callPlaySound(CallSound sound) = 0; + virtual void callRequestPermissionsOrFail( + Fn onSuccess, + bool video) = 0; + virtual auto getVideoCapture() -> std::shared_ptr = 0; @@ -165,6 +168,7 @@ public: crl::time getDurationMs() const; float64 getWaitingSoundPeakValue() const; + void switchVideoOutgoing(); void answer(); void hangup(); void redial(); @@ -176,7 +180,7 @@ public: void setCurrentAudioDevice(bool input, const QString &deviceId); void setCurrentVideoDevice(const QString &deviceId); - void setAudioVolume(bool input, float level); + //void setAudioVolume(bool input, float level); void setAudioDuckingEnabled(bool enabled); [[nodiscard]] rpl::lifetime &lifetime() { @@ -191,6 +195,7 @@ private: Ended, Failed, }; + void handleRequestError(const RPCError &error); void handleControllerError(const QString &error); void finish( diff --git a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp index bd95df7a5..61ec38db5 100644 --- a/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp +++ b/Telegram/SourceFiles/calls/calls_emoji_fingerprint.cpp @@ -253,7 +253,7 @@ object_ptr CreateFingerprintAndSignalBars( fullBarsSize.width(), height); const auto bigRadius = height / 2; - const auto smallRadius = st::buttonRadius; + const auto smallRadius = st::roundRadiusSmall; const auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::callBgButton); diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp new file mode 100644 index 000000000..2a68b411e --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -0,0 +1,979 @@ +/* +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 "calls/calls_group_call.h" + +#include "main/main_session.h" +#include "api/api_send_progress.h" +#include "apiwrap.h" +#include "lang/lang_keys.h" +#include "lang/lang_hardcoded.h" +#include "boxes/peers/edit_participants_box.h" // SubscribeToMigration. +#include "ui/toasts/common_toasts.h" +#include "base/unixtime.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_changes.h" +#include "data/data_user.h" +#include "data/data_chat.h" +#include "data/data_channel.h" +#include "data/data_group_call.h" +#include "data/data_session.h" +#include "base/global_shortcuts.h" +#include "webrtc/webrtc_media_devices.h" + +#include + +#include +#include +#include + +namespace tgcalls { +class GroupInstanceImpl; +} // namespace tgcalls + +namespace Calls { +namespace { + +constexpr auto kMaxInvitePerSlice = 10; +constexpr auto kCheckLastSpokeInterval = crl::time(1000); +constexpr auto kCheckJoinedTimeout = 4 * crl::time(1000); +constexpr auto kUpdateSendActionEach = crl::time(500); +constexpr auto kPlayConnectingEach = crl::time(1056) + 2 * crl::time(1000); + +[[nodiscard]] std::unique_ptr CreateMediaDevices() { + const auto &settings = Core::App().settings(); + return Webrtc::CreateMediaDevices( + settings.callInputDeviceId(), + settings.callOutputDeviceId(), + settings.callVideoInputDeviceId()); +} + +} // namespace + +GroupCall::GroupCall( + not_null delegate, + not_null peer, + const MTPInputGroupCall &inputCall) +: _delegate(delegate) +, _peer(peer) +, _history(peer->owner().history(peer)) +, _api(&peer->session().mtp()) +, _lastSpokeCheckTimer([=] { checkLastSpoke(); }) +, _checkJoinedTimer([=] { checkJoined(); }) +, _pushToTalkCancelTimer([=] { pushToTalkCancel(); }) +, _connectingSoundTimer([=] { playConnectingSoundOnce(); }) +, _mediaDevices(CreateMediaDevices()) { + _muted.value( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=](MuteState previous, MuteState state) { + if (_instance) { + updateInstanceMuteState(); + } + if (_mySsrc) { + maybeSendMutedUpdate(previous); + } + }, _lifetime); + + checkGlobalShortcutAvailability(); + + const auto id = inputCall.c_inputGroupCall().vid().v; + if (id) { + if (const auto call = _peer->groupCall(); call && call->id() == id) { + if (!_peer->canManageGroupCall() && call->joinMuted()) { + _muted = MuteState::ForceMuted; + } + } + _state = State::Joining; + join(inputCall); + } else { + start(); + } + + _mediaDevices->audioInputId( + ) | rpl::start_with_next([=](QString id) { + _audioInputId = id; + if (_instance) { + _instance->setAudioInputDevice(id.toStdString()); + } + }, _lifetime); + + _mediaDevices->audioOutputId( + ) | rpl::start_with_next([=](QString id) { + _audioOutputId = id; + if (_instance) { + _instance->setAudioOutputDevice(id.toStdString()); + } + }, _lifetime); +} + +GroupCall::~GroupCall() { + destroyController(); +} + +void GroupCall::checkGlobalShortcutAvailability() { + auto &settings = Core::App().settings(); + if (!settings.groupCallPushToTalk()) { + return; + } else if (!base::GlobalShortcutsAllowed()) { + settings.setGroupCallPushToTalk(false); + Core::App().saveSettingsDelayed(); + } +} + +void GroupCall::setState(State state) { + if (_state.current() == State::Failed) { + return; + } else if (_state.current() == State::FailedHangingUp + && state != State::Failed) { + return; + } + if (_state.current() == state) { + return; + } + _state = state; + + if (state == State::Joined) { + stopConnectingSound(); + if (!_hadJoinedState) { + _hadJoinedState = true; + applyGlobalShortcutChanges(); + _delegate->groupCallPlaySound(Delegate::GroupCallSound::Started); + } + if (const auto call = _peer->groupCall(); call && call->id() == _id) { + call->setInCall(); + } + } else if (state == State::Connecting || state == State::Joining) { + if (_hadJoinedState) { + playConnectingSound(); + } + } else { + stopConnectingSound(); + } + + if (false + || state == State::Ended + || state == State::Failed) { + // Destroy controller before destroying Call Panel, + // so that the panel hide animation is smooth. + destroyController(); + } + switch (state) { + case State::HangingUp: + case State::FailedHangingUp: + _delegate->groupCallPlaySound(Delegate::GroupCallSound::Ended); + break; + case State::Ended: + _delegate->groupCallFinished(this); + break; + case State::Failed: + _delegate->groupCallFailed(this); + break; + case State::Connecting: + if (!_checkJoinedTimer.isActive()) { + _checkJoinedTimer.callOnce(kCheckJoinedTimeout); + } + break; + } +} + +void GroupCall::playConnectingSound() { + if (_connectingSoundTimer.isActive()) { + return; + } + playConnectingSoundOnce(); + _connectingSoundTimer.callEach(kPlayConnectingEach); +} + +void GroupCall::stopConnectingSound() { + _connectingSoundTimer.cancel(); +} + +void GroupCall::playConnectingSoundOnce() { + _delegate->groupCallPlaySound(Delegate::GroupCallSound::Connecting); +} + +void GroupCall::start() { + _createRequestId = _api.request(MTPphone_CreateGroupCall( + _peer->input, + MTP_int(rand_value()) + )).done([=](const MTPUpdates &result) { + _acceptFields = true; + _peer->session().api().applyUpdates(result); + _acceptFields = false; + }).fail([=](const RPCError &error) { + LOG(("Call Error: Could not create, error: %1" + ).arg(error.type())); + hangup(); + if (error.type() == u"GROUPCALL_ANONYMOUS_FORBIDDEN"_q) { + Ui::ShowMultilineToast({ + .text = tr::lng_group_call_no_anonymous(tr::now), + }); + } + }).send(); +} + +void GroupCall::join(const MTPInputGroupCall &inputCall) { + setState(State::Joining); + if (const auto chat = _peer->asChat()) { + chat->setGroupCall(inputCall); + } else if (const auto group = _peer->asMegagroup()) { + group->setGroupCall(inputCall); + } else { + Unexpected("Peer type in GroupCall::join."); + } + + inputCall.match([&](const MTPDinputGroupCall &data) { + _id = data.vid().v; + _accessHash = data.vaccess_hash().v; + rejoin(); + }); + + using Update = Data::GroupCall::ParticipantUpdate; + _peer->groupCall()->participantUpdated( + ) | rpl::filter([=](const Update &update) { + return (_instance != nullptr) && !update.now; + }) | rpl::start_with_next([=](const Update &update) { + Expects(update.was.has_value()); + + _instance->removeSsrcs({ update.was->ssrc }); + }, _lifetime); + + SubscribeToMigration(_peer, _lifetime, [=](not_null group) { + _peer = group; + }); +} + +void GroupCall::rejoin() { + if (state() != State::Joining + && state() != State::Joined + && state() != State::Connecting) { + return; + } + + _mySsrc = 0; + setState(State::Joining); + createAndStartController(); + applySelfInCallLocally(); + LOG(("Call Info: Requesting join payload.")); + + const auto weak = base::make_weak(this); + _instance->emitJoinPayload([=](tgcalls::GroupJoinPayload payload) { + crl::on_main(weak, [=, payload = std::move(payload)]{ + auto fingerprints = QJsonArray(); + for (const auto print : payload.fingerprints) { + auto object = QJsonObject(); + object.insert("hash", QString::fromStdString(print.hash)); + object.insert("setup", QString::fromStdString(print.setup)); + object.insert( + "fingerprint", + QString::fromStdString(print.fingerprint)); + fingerprints.push_back(object); + } + + auto root = QJsonObject(); + const auto ssrc = payload.ssrc; + root.insert("ufrag", QString::fromStdString(payload.ufrag)); + root.insert("pwd", QString::fromStdString(payload.pwd)); + root.insert("fingerprints", fingerprints); + root.insert("ssrc", double(payload.ssrc)); + + LOG(("Call Info: Join payload received, joining with ssrc: %1." + ).arg(ssrc)); + + const auto json = QJsonDocument(root).toJson( + QJsonDocument::Compact); + const auto wasMuteState = muted(); + _api.request(MTPphone_JoinGroupCall( + MTP_flags((wasMuteState != MuteState::Active) + ? MTPphone_JoinGroupCall::Flag::f_muted + : MTPphone_JoinGroupCall::Flag(0)), + inputCall(), + MTP_dataJSON(MTP_bytes(json)) + )).done([=](const MTPUpdates &updates) { + _mySsrc = ssrc; + setState(_instanceConnected + ? State::Joined + : State::Connecting); + applySelfInCallLocally(); + maybeSendMutedUpdate(wasMuteState); + _peer->session().api().applyUpdates(updates); + }).fail([=](const RPCError &error) { + const auto type = error.type(); + LOG(("Call Error: Could not join, error: %1").arg(type)); + + if (type == u"GROUPCALL_SSRC_DUPLICATE_MUCH") { + rejoin(); + return; + } + + hangup(); + Ui::ShowMultilineToast({ + .text = (type == u"GROUPCALL_ANONYMOUS_FORBIDDEN"_q + ? tr::lng_group_call_no_anonymous(tr::now) + : type == u"GROUPCALL_PARTICIPANTS_TOO_MUCH"_q + ? tr::lng_group_call_too_many(tr::now) + : type == u"GROUPCALL_FORBIDDEN"_q + ? tr::lng_group_not_accessible(tr::now) + : Lang::Hard::ServerError()), + }); + }).send(); + }); + }); +} + +void GroupCall::applySelfInCallLocally() { + const auto call = _peer->groupCall(); + if (!call || call->id() != _id) { + return; + } + using Flag = MTPDgroupCallParticipant::Flag; + const auto &participants = call->participants(); + const auto self = _peer->session().user(); + const auto i = ranges::find( + participants, + self, + &Data::GroupCall::Participant::user); + const auto date = (i != end(participants)) + ? i->date + : base::unixtime::now(); + const auto lastActive = (i != end(participants)) + ? i->lastActive + : TimeId(0); + const auto canSelfUnmute = (muted() != MuteState::ForceMuted); + const auto flags = (canSelfUnmute ? Flag::f_can_self_unmute : Flag(0)) + | (lastActive ? Flag::f_active_date : Flag(0)) + | (_mySsrc ? Flag(0) : Flag::f_left) + | ((muted() != MuteState::Active) ? Flag::f_muted : Flag(0)); + call->applyUpdateChecked( + MTP_updateGroupCallParticipants( + inputCall(), + MTP_vector( + 1, + MTP_groupCallParticipant( + MTP_flags(flags), + MTP_int(self->bareId()), + MTP_int(date), + MTP_int(lastActive), + MTP_int(_mySsrc))), + MTP_int(0)).c_updateGroupCallParticipants()); +} + +void GroupCall::hangup() { + finish(FinishType::Ended); +} + +void GroupCall::discard() { + if (!_id) { + _api.request(_createRequestId).cancel(); + hangup(); + return; + } + _api.request(MTPphone_DiscardGroupCall( + inputCall() + )).done([=](const MTPUpdates &result) { + // Here 'this' could be destroyed by updates, so we set Ended after + // updates being handled, but in a guarded way. + crl::on_main(this, [=] { hangup(); }); + _peer->session().api().applyUpdates(result); + }).fail([=](const RPCError &error) { + hangup(); + }).send(); +} + +void GroupCall::finish(FinishType type) { + Expects(type != FinishType::None); + + const auto finalState = (type == FinishType::Ended) + ? State::Ended + : State::Failed; + const auto hangupState = (type == FinishType::Ended) + ? State::HangingUp + : State::FailedHangingUp; + const auto state = _state.current(); + if (state == State::HangingUp + || state == State::FailedHangingUp + || state == State::Ended + || state == State::Failed) { + return; + } + if (!_mySsrc) { + setState(finalState); + return; + } + + setState(hangupState); + + // We want to leave request still being sent and processed even if + // the call is already destroyed. + const auto session = &_peer->session(); + const auto weak = base::make_weak(this); + session->api().request(MTPphone_LeaveGroupCall( + inputCall(), + MTP_int(_mySsrc) + )).done([=](const MTPUpdates &result) { + // Here 'this' could be destroyed by updates, so we set Ended after + // updates being handled, but in a guarded way. + crl::on_main(weak, [=] { setState(finalState); }); + session->api().applyUpdates(result); + }).fail(crl::guard(weak, [=](const RPCError &error) { + setState(finalState); + })).send(); +} + +void GroupCall::setMuted(MuteState mute) { + const auto set = [=] { + const auto wasMuted = (muted() == MuteState::Muted) + || (muted() == MuteState::PushToTalk); + _muted = mute; + const auto nowMuted = (muted() == MuteState::Muted) + || (muted() == MuteState::PushToTalk); + if (wasMuted != nowMuted) { + applySelfInCallLocally(); + } + }; + if (mute == MuteState::Active || mute == MuteState::PushToTalk) { + _delegate->groupCallRequestPermissionsOrFail(crl::guard(this, set)); + } else { + set(); + } +} + +void GroupCall::handleUpdate(const MTPGroupCall &call) { + return call.match([&](const MTPDgroupCall &data) { + if (_acceptFields) { + if (!_instance && !_id) { + join(MTP_inputGroupCall(data.vid(), data.vaccess_hash())); + } + return; + } else if (_id != data.vid().v + || _accessHash != data.vaccess_hash().v + || !_instance) { + return; + } + if (const auto params = data.vparams()) { + params->match([&](const MTPDdataJSON &data) { + auto error = QJsonParseError{ 0, QJsonParseError::NoError }; + const auto document = QJsonDocument::fromJson( + data.vdata().v, + &error); + if (error.error != QJsonParseError::NoError) { + LOG(("API Error: " + "Failed to parse group call params, error: %1." + ).arg(error.errorString())); + return; + } else if (!document.isObject()) { + LOG(("API Error: " + "Not an object received in group call params.")); + return; + } + const auto readString = []( + const QJsonObject &object, + const char *key) { + return object.value(key).toString().toStdString(); + }; + const auto root = document.object().value("transport").toObject(); + auto payload = tgcalls::GroupJoinResponsePayload(); + payload.ufrag = readString(root, "ufrag"); + payload.pwd = readString(root, "pwd"); + const auto prints = root.value("fingerprints").toArray(); + const auto candidates = root.value("candidates").toArray(); + for (const auto &print : prints) { + const auto object = print.toObject(); + payload.fingerprints.push_back(tgcalls::GroupJoinPayloadFingerprint{ + .hash = readString(object, "hash"), + .setup = readString(object, "setup"), + .fingerprint = readString(object, "fingerprint"), + }); + } + for (const auto &candidate : candidates) { + const auto object = candidate.toObject(); + payload.candidates.push_back(tgcalls::GroupJoinResponseCandidate{ + .port = readString(object, "port"), + .protocol = readString(object, "protocol"), + .network = readString(object, "network"), + .generation = readString(object, "generation"), + .id = readString(object, "id"), + .component = readString(object, "component"), + .foundation = readString(object, "foundation"), + .priority = readString(object, "priority"), + .ip = readString(object, "ip"), + .type = readString(object, "type"), + .tcpType = readString(object, "tcpType"), + .relAddr = readString(object, "relAddr"), + .relPort = readString(object, "relPort"), + }); + } + _instance->setJoinResponsePayload(payload); + }); + } + }, [&](const MTPDgroupCallDiscarded &data) { + if (data.vid().v == _id) { + _mySsrc = 0; + hangup(); + } + }); +} + +void GroupCall::handleUpdate(const MTPDupdateGroupCallParticipants &data) { + const auto state = _state.current(); + if (state != State::Joined && state != State::Connecting) { + return; + } + + const auto self = _peer->session().userId(); + for (const auto &participant : data.vparticipants().v) { + participant.match([&](const MTPDgroupCallParticipant &data) { + if (data.vuser_id().v != self) { + return; + } + if (data.is_left() && data.vsource().v == _mySsrc) { + // I was removed from the call, rejoin. + LOG(("Call Info: Rejoin after got 'left' with my ssrc.")); + setState(State::Joining); + rejoin(); + } else if (!data.is_left() && data.vsource().v != _mySsrc) { + // I joined from another device, hangup. + LOG(("Call Info: Hangup after '!left' with ssrc %1, my %2." + ).arg(data.vsource().v + ).arg(_mySsrc)); + _mySsrc = 0; + hangup(); + } + if (data.is_muted() && !data.is_can_self_unmute()) { + setMuted(MuteState::ForceMuted); + } else if (muted() == MuteState::ForceMuted) { + setMuted(MuteState::Muted); + } + }); + } +} + +void GroupCall::createAndStartController() { + const auto &settings = Core::App().settings(); + + const auto weak = base::make_weak(this); + const auto myLevel = std::make_shared(); + tgcalls::GroupInstanceDescriptor descriptor = { + .config = tgcalls::GroupConfig{ + }, + .networkStateUpdated = [=](bool connected) { + crl::on_main(weak, [=] { setInstanceConnected(connected); }); + }, + .audioLevelsUpdated = [=](const tgcalls::GroupLevelsUpdate &data) { + const auto &updates = data.updates; + if (updates.empty()) { + return; + } else if (updates.size() == 1 && !updates.front().ssrc) { + const auto &value = updates.front().value; + // Don't send many 0 while we're muted. + if (myLevel->level == value.level + && myLevel->voice == value.voice) { + return; + } + *myLevel = updates.front().value; + } + crl::on_main(weak, [=] { audioLevelsUpdated(data); }); + }, + .initialInputDeviceId = _audioInputId.toStdString(), + .initialOutputDeviceId = _audioOutputId.toStdString(), + }; + if (Logs::DebugEnabled()) { + auto callLogFolder = cWorkingDir() + qsl("DebugLogs"); + auto callLogPath = callLogFolder + qsl("/last_group_call_log.txt"); + auto callLogNative = QDir::toNativeSeparators(callLogPath); +#ifdef Q_OS_WIN + descriptor.config.logPath.data = callLogNative.toStdWString(); +#else // Q_OS_WIN + const auto callLogUtf = QFile::encodeName(callLogNative); + descriptor.config.logPath.data.resize(callLogUtf.size()); + ranges::copy(callLogUtf, descriptor.config.logPath.data.begin()); +#endif // Q_OS_WIN + QFile(callLogPath).remove(); + QDir().mkpath(callLogFolder); + } + + LOG(("Call Info: Creating group instance")); + _instance = std::make_unique( + std::move(descriptor)); + + updateInstanceMuteState(); + + //raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled()); +} + +void GroupCall::updateInstanceMuteState() { + Expects(_instance != nullptr); + + const auto state = muted(); + _instance->setIsMuted(state != MuteState::Active + && state != MuteState::PushToTalk); +} + +void GroupCall::audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data) { + Expects(!data.updates.empty()); + + auto check = false; + auto checkNow = false; + const auto now = crl::now(); + for (const auto &[ssrcOrZero, value] : data.updates) { + const auto ssrc = ssrcOrZero ? ssrcOrZero : _mySsrc; + const auto level = value.level; + const auto voice = value.voice; + const auto self = (ssrc == _mySsrc); + _levelUpdates.fire(LevelUpdate{ + .ssrc = ssrc, + .value = level, + .voice = voice, + .self = self + }); + if (level <= kSpeakLevelThreshold) { + continue; + } + if (self + && voice + && (!_lastSendProgressUpdate + || _lastSendProgressUpdate + kUpdateSendActionEach < now)) { + _lastSendProgressUpdate = now; + _peer->session().sendProgressManager().update( + _history, + Api::SendProgressType::Speaking); + } + + check = true; + const auto i = _lastSpoke.find(ssrc); + if (i == _lastSpoke.end()) { + _lastSpoke.emplace(ssrc, Data::LastSpokeTimes{ + .anything = now, + .voice = voice ? now : 0, + }); + checkNow = true; + } else { + if ((i->second.anything + kCheckLastSpokeInterval / 3 <= now) + || (voice + && i->second.voice + kCheckLastSpokeInterval / 3 <= now)) { + checkNow = true; + } + i->second.anything = now; + if (voice) { + i->second.voice = now; + } + } + } + if (checkNow) { + checkLastSpoke(); + } else if (check && !_lastSpokeCheckTimer.isActive()) { + _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 2); + } +} + +void GroupCall::checkLastSpoke() { + const auto real = _peer->groupCall(); + if (!real || real->id() != _id) { + return; + } + + auto hasRecent = false; + const auto now = crl::now(); + auto list = base::take(_lastSpoke); + for (auto i = list.begin(); i != list.end();) { + const auto [ssrc, when] = *i; + if (when.anything + kCheckLastSpokeInterval >= now) { + hasRecent = true; + ++i; + } else { + i = list.erase(i); + } + real->applyLastSpoke(ssrc, when, now); + } + _lastSpoke = std::move(list); + + if (!hasRecent) { + _lastSpokeCheckTimer.cancel(); + } else if (!_lastSpokeCheckTimer.isActive()) { + _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 3); + } +} + +void GroupCall::checkJoined() { + if (state() != State::Connecting || !_id || !_mySsrc) { + return; + } + _api.request(MTPphone_CheckGroupCall( + inputCall(), + MTP_int(_mySsrc) + )).done([=](const MTPBool &result) { + if (!mtpIsTrue(result)) { + LOG(("Call Info: Rejoin after FALSE in checkGroupCall.")); + rejoin(); + } else if (state() == State::Connecting) { + _checkJoinedTimer.callOnce(kCheckJoinedTimeout); + } + }).fail([=](const RPCError &error) { + LOG(("Call Info: Rejoin after error '%1' in checkGroupCall." + ).arg(error.type())); + rejoin(); + }).send(); +} + +void GroupCall::setInstanceConnected(bool connected) { + if (_instanceConnected == connected) { + return; + } + _instanceConnected = connected; + if (state() == State::Connecting && connected) { + setState(State::Joined); + } else if (state() == State::Joined && !connected) { + setState(State::Connecting); + } +} + +void GroupCall::maybeSendMutedUpdate(MuteState previous) { + // Send only Active <-> !Active changes. + const auto now = muted(); + const auto wasActive = (previous == MuteState::Active); + const auto nowActive = (now == MuteState::Active); + if (now == MuteState::ForceMuted + || previous == MuteState::ForceMuted + || (nowActive == wasActive)) { + return; + } + sendMutedUpdate(); +} + +void GroupCall::sendMutedUpdate() { + _api.request(_updateMuteRequestId).cancel(); + _updateMuteRequestId = _api.request(MTPphone_EditGroupCallMember( + MTP_flags((muted() != MuteState::Active) + ? MTPphone_EditGroupCallMember::Flag::f_muted + : MTPphone_EditGroupCallMember::Flag(0)), + inputCall(), + MTP_inputUserSelf() + )).done([=](const MTPUpdates &result) { + _updateMuteRequestId = 0; + _peer->session().api().applyUpdates(result); + }).fail([=](const RPCError &error) { + _updateMuteRequestId = 0; + if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { + LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." + ).arg(error.type())); + rejoin(); + } + }).send(); +} + +rpl::producer GroupCall::connectingValue() const { + using namespace rpl::mappers; + return _state.value() | rpl::map( + _1 == State::Creating + || _1 == State::Joining + || _1 == State::Connecting + ) | rpl::distinct_until_changed(); +} + +void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) { + if (input) { + _mediaDevices->switchToAudioInput(deviceId); + } else { + _mediaDevices->switchToAudioOutput(deviceId); + } +} + +void GroupCall::toggleMute(not_null user, bool mute) { + if (!_id) { + return; + } + _api.request(MTPphone_EditGroupCallMember( + MTP_flags(mute + ? MTPphone_EditGroupCallMember::Flag::f_muted + : MTPphone_EditGroupCallMember::Flag(0)), + inputCall(), + user->inputUser + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).fail([=](const RPCError &error) { + if (error.type() == u"GROUPCALL_FORBIDDEN"_q) { + LOG(("Call Info: Rejoin after error '%1' in editGroupCallMember." + ).arg(error.type())); + rejoin(); + } + }).send(); +} + +std::variant> GroupCall::inviteUsers( + const std::vector> &users) { + const auto real = _peer->groupCall(); + if (!real || real->id() != _id) { + return 0; + } + const auto owner = &_peer->owner(); + const auto &invited = owner->invitedToCallUsers(_id); + const auto &participants = real->participants(); + auto &&toInvite = users | ranges::view::filter([&]( + not_null user) { + return !invited.contains(user) && !ranges::contains( + participants, + user, + &Data::GroupCall::Participant::user); + }); + + auto count = 0; + auto slice = QVector(); + auto result = std::variant>(0); + slice.reserve(kMaxInvitePerSlice); + const auto sendSlice = [&] { + count += slice.size(); + _api.request(MTPphone_InviteToGroupCall( + inputCall(), + MTP_vector(slice) + )).done([=](const MTPUpdates &result) { + _peer->session().api().applyUpdates(result); + }).send(); + slice.clear(); + }; + for (const auto user : users) { + if (!count && slice.empty()) { + result = user; + } + owner->registerInvitedToCallUser(_id, _peer, user); + slice.push_back(user->inputUser); + if (slice.size() == kMaxInvitePerSlice) { + sendSlice(); + } + } + if (count != 0 || slice.size() != 1) { + result = int(count + slice.size()); + } + if (!slice.empty()) { + sendSlice(); + } + return result; +} + +auto GroupCall::ensureGlobalShortcutManager() +-> std::shared_ptr { + if (!_shortcutManager) { + _shortcutManager = base::CreateGlobalShortcutManager(); + } + return _shortcutManager; +} + +void GroupCall::applyGlobalShortcutChanges() { + auto &settings = Core::App().settings(); + if (!settings.groupCallPushToTalk() + || settings.groupCallPushToTalkShortcut().isEmpty() + || !base::GlobalShortcutsAvailable() + || !base::GlobalShortcutsAllowed()) { + _shortcutManager = nullptr; + _pushToTalk = nullptr; + return; + } + ensureGlobalShortcutManager(); + const auto shortcut = _shortcutManager->shortcutFromSerialized( + settings.groupCallPushToTalkShortcut()); + if (!shortcut) { + settings.setGroupCallPushToTalkShortcut(QByteArray()); + settings.setGroupCallPushToTalk(false); + Core::App().saveSettingsDelayed(); + _shortcutManager = nullptr; + _pushToTalk = nullptr; + return; + } + if (_pushToTalk) { + if (shortcut->serialize() == _pushToTalk->serialize()) { + return; + } + _shortcutManager->stopWatching(_pushToTalk); + } + _pushToTalk = shortcut; + _shortcutManager->startWatching(_pushToTalk, [=](bool pressed) { + pushToTalk( + pressed, + Core::App().settings().groupCallPushToTalkDelay()); + }); +} + +void GroupCall::pushToTalk(bool pressed, crl::time delay) { + if (muted() == MuteState::ForceMuted + || muted() == MuteState::Active) { + return; + } else if (pressed) { + _pushToTalkCancelTimer.cancel(); + setMuted(MuteState::PushToTalk); + } else if (delay) { + _pushToTalkCancelTimer.callOnce(delay); + } else { + pushToTalkCancel(); + } +} + +void GroupCall::pushToTalkCancel() { + _pushToTalkCancelTimer.cancel(); + if (muted() == MuteState::PushToTalk) { + setMuted(MuteState::Muted); + } +} + +//void GroupCall::setAudioVolume(bool input, float level) { +// if (_instance) { +// if (input) { +// _instance->setInputVolume(level); +// } else { +// _instance->setOutputVolume(level); +// } +// } +//} + +void GroupCall::setAudioDuckingEnabled(bool enabled) { + if (_instance) { + //_instance->setAudioOutputDuckingEnabled(enabled); + } +} + +void GroupCall::handleRequestError(const RPCError &error) { + //if (error.type() == qstr("USER_PRIVACY_RESTRICTED")) { + // Ui::show(Box(tr::lng_call_error_not_available(tr::now, lt_user, _user->name))); + //} else if (error.type() == qstr("PARTICIPANT_VERSION_OUTDATED")) { + // Ui::show(Box(tr::lng_call_error_outdated(tr::now, lt_user, _user->name))); + //} else if (error.type() == qstr("CALL_PROTOCOL_LAYER_INVALID")) { + // Ui::show(Box(Lang::Hard::CallErrorIncompatible().replace("{user}", _user->name))); + //} + //finish(FinishType::Failed); +} + +void GroupCall::handleControllerError(const QString &error) { + if (error == u"ERROR_INCOMPATIBLE"_q) { + //Ui::show(Box( + // Lang::Hard::CallErrorIncompatible().replace( + // "{user}", + // _user->name))); + } else if (error == u"ERROR_AUDIO_IO"_q) { + //Ui::show(Box(tr::lng_call_error_audio_io(tr::now))); + } + //finish(FinishType::Failed); +} + +MTPInputGroupCall GroupCall::inputCall() const { + Expects(_id != 0); + + return MTP_inputGroupCall( + MTP_long(_id), + MTP_long(_accessHash)); +} + +void GroupCall::destroyController() { + if (_instance) { + //_instance->stop([](tgcalls::FinalState) { + //}); + + DEBUG_LOG(("Call Info: Destroying call controller..")); + _instance.reset(); + DEBUG_LOG(("Call Info: Call controller destroyed.")); + } +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h new file mode 100644 index 000000000..8b6907d2c --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_call.h @@ -0,0 +1,221 @@ +/* +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 "base/weak_ptr.h" +#include "base/timer.h" +#include "base/bytes.h" +#include "mtproto/sender.h" +#include "mtproto/mtproto_auth_key.h" + +class History; + +namespace tgcalls { +class GroupInstanceImpl; +struct GroupLevelsUpdate; +} // namespace tgcalls + +namespace base { +class GlobalShortcutManager; +class GlobalShortcutValue; +} // namespace base + +namespace Webrtc { +class MediaDevices; +} // namespace Webrtc + +namespace Data { +struct LastSpokeTimes; +} // namespace Data + +namespace Calls { + +enum class MuteState { + Active, + PushToTalk, + Muted, + ForceMuted, +}; + +[[nodiscard]] inline auto MapPushToTalkToActive() { + return rpl::map([=](MuteState state) { + return (state == MuteState::PushToTalk) ? MuteState::Active : state; + }); +} + +struct LevelUpdate { + uint32 ssrc = 0; + float value = 0.; + bool voice = false; + bool self = false; +}; + +class GroupCall final : public base::has_weak_ptr { +public: + class Delegate { + public: + virtual ~Delegate() = default; + + virtual void groupCallFinished(not_null call) = 0; + virtual void groupCallFailed(not_null call) = 0; + virtual void groupCallRequestPermissionsOrFail( + Fn onSuccess) = 0; + + enum class GroupCallSound { + Started, + Connecting, + Ended, + }; + virtual void groupCallPlaySound(GroupCallSound sound) = 0; + }; + + using GlobalShortcutManager = base::GlobalShortcutManager; + + GroupCall( + not_null delegate, + not_null peer, + const MTPInputGroupCall &inputCall); + ~GroupCall(); + + [[nodiscard]] uint64 id() const { + return _id; + } + [[nodiscard]] not_null peer() const { + return _peer; + } + + void start(); + void hangup(); + void discard(); + void join(const MTPInputGroupCall &inputCall); + void handleUpdate(const MTPGroupCall &call); + void handleUpdate(const MTPDupdateGroupCallParticipants &data); + + void setMuted(MuteState mute); + [[nodiscard]] MuteState muted() const { + return _muted.current(); + } + [[nodiscard]] rpl::producer mutedValue() const { + return _muted.value(); + } + + enum State { + Creating, + Joining, + Connecting, + Joined, + FailedHangingUp, + Failed, + HangingUp, + Ended, + }; + [[nodiscard]] State state() const { + return _state.current(); + } + [[nodiscard]] rpl::producer stateValue() const { + return _state.value(); + } + [[nodiscard]] rpl::producer connectingValue() const; + + [[nodiscard]] rpl::producer levelUpdates() const { + return _levelUpdates.events(); + } + static constexpr auto kSpeakLevelThreshold = 0.2; + + void setCurrentAudioDevice(bool input, const QString &deviceId); + //void setAudioVolume(bool input, float level); + void setAudioDuckingEnabled(bool enabled); + + void toggleMute(not_null user, bool mute); + std::variant> inviteUsers( + const std::vector> &users); + + std::shared_ptr ensureGlobalShortcutManager(); + void applyGlobalShortcutChanges(); + + void pushToTalk(bool pressed, crl::time delay); + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + using GlobalShortcutValue = base::GlobalShortcutValue; + + enum class FinishType { + None, + Ended, + Failed, + }; + + void handleRequestError(const RPCError &error); + void handleControllerError(const QString &error); + void createAndStartController(); + void destroyController(); + + void setState(State state); + void finish(FinishType type); + void maybeSendMutedUpdate(MuteState previous); + void sendMutedUpdate(); + void updateInstanceMuteState(); + void applySelfInCallLocally(); + void rejoin(); + + void audioLevelsUpdated(const tgcalls::GroupLevelsUpdate &data); + void setInstanceConnected(bool connected); + void checkLastSpoke(); + void pushToTalkCancel(); + + void checkGlobalShortcutAvailability(); + void checkJoined(); + + void playConnectingSound(); + void stopConnectingSound(); + void playConnectingSoundOnce(); + + [[nodiscard]] MTPInputGroupCall inputCall() const; + + const not_null _delegate; + not_null _peer; // Can change in legacy group migration. + not_null _history; // Can change in legacy group migration. + MTP::Sender _api; + rpl::variable _state = State::Creating; + bool _instanceConnected = false; + + rpl::variable _muted = MuteState::Muted; + bool _acceptFields = false; + + uint64 _id = 0; + uint64 _accessHash = 0; + uint32 _mySsrc = 0; + mtpRequestId _createRequestId = 0; + mtpRequestId _updateMuteRequestId = 0; + + std::unique_ptr _instance; + rpl::event_stream _levelUpdates; + base::flat_map _lastSpoke; + base::Timer _lastSpokeCheckTimer; + base::Timer _checkJoinedTimer; + + crl::time _lastSendProgressUpdate = 0; + + std::shared_ptr _shortcutManager; + std::shared_ptr _pushToTalk; + base::Timer _pushToTalkCancelTimer; + base::Timer _connectingSoundTimer; + bool _hadJoinedState = false; + + std::unique_ptr _mediaDevices; + QString _audioInputId; + QString _audioOutputId; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_members.cpp b/Telegram/SourceFiles/calls/calls_group_members.cpp new file mode 100644 index 000000000..089bb5c50 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_members.cpp @@ -0,0 +1,1443 @@ +/* +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 "calls/calls_group_members.h" + +#include "calls/calls_group_call.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_user.h" +#include "data/data_changes.h" +#include "data/data_group_call.h" +#include "data/data_peer_values.h" // Data::CanWriteValue. +#include "data/data_session.h" // Data::Session::invitedToCallUsers. +#include "settings/settings_common.h" // Settings::CreateButton. +#include "ui/paint/blobs.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/popup_menu.h" +#include "ui/text/text_utilities.h" +#include "ui/effects/ripple_animation.h" +#include "ui/effects/cross_line.h" +#include "core/application.h" // Core::App().domain, Core::App().activeWindow. +#include "main/main_domain.h" // Core::App().domain().activate. +#include "main/main_session.h" +#include "base/timer.h" +#include "boxes/peers/edit_participants_box.h" // SubscribeToMigration. +#include "lang/lang_keys.h" +#include "window/window_controller.h" // Controller::sessionController. +#include "window/window_session_controller.h" +#include "styles/style_calls.h" + +namespace Calls { +namespace { + +constexpr auto kBlobsEnterDuration = crl::time(250); +constexpr auto kLevelDuration = 100. + 500. * 0.23; +constexpr auto kBlobScale = 0.605; +constexpr auto kMinorBlobFactor = 0.9f; +constexpr auto kUserpicMinScale = 0.8; +constexpr auto kMaxLevel = 1.; +constexpr auto kWideScale = 5; + +auto RowBlobs() -> std::array { + return { { + { + .segmentsCount = 6, + .minScale = kBlobScale * kMinorBlobFactor, + .minRadius = st::groupCallRowBlobMinRadius * kMinorBlobFactor, + .maxRadius = st::groupCallRowBlobMaxRadius * kMinorBlobFactor, + .speedScale = 1., + .alpha = .5, + }, + { + .segmentsCount = 8, + .minScale = kBlobScale, + .minRadius = (float)st::groupCallRowBlobMinRadius, + .maxRadius = (float)st::groupCallRowBlobMaxRadius, + .speedScale = 1., + .alpha = .2, + }, + } }; +} + +class Row; + +class RowDelegate { +public: + virtual bool rowCanMuteMembers() = 0; + virtual void rowUpdateRow(not_null row) = 0; + virtual void rowPaintIcon( + Painter &p, + QRect rect, + float64 speaking, + float64 active, + float64 muted) = 0; +}; + +class Row final : public PeerListRow { +public: + Row(not_null delegate, not_null user); + + enum class State { + Active, + Inactive, + Muted, + Invited, + }; + + void setSkipLevelUpdate(bool value); + void updateState(const Data::GroupCall::Participant *participant); + void updateLevel(float level); + void updateBlobAnimation(crl::time now); + [[nodiscard]] State state() const { + return _state; + } + [[nodiscard]] uint32 ssrc() const { + return _ssrc; + } + [[nodiscard]] bool sounding() const { + return _sounding; + } + [[nodiscard]] bool speaking() const { + return _speaking; + } + + void addActionRipple(QPoint point, Fn updateCallback) override; + void stopLastActionRipple() override; + + int nameIconWidth() const override { + return 0; + } + QSize actionSize() const override { + return QSize( + st::groupCallActiveButton.width, + st::groupCallActiveButton.height); + } + bool actionDisabled() const override { + return peer()->isSelf() + || (_state == State::Invited) + || !_delegate->rowCanMuteMembers(); + } + QMargins actionMargins() const override { + return QMargins( + 0, + 0, + st::groupCallMemberButtonSkip, + 0); + } + void paintAction( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) override; + + auto generatePaintUserpicCallback() -> PaintRoundImageCallback override; + + void paintStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected) override; + +private: + struct BlobsAnimation { + BlobsAnimation( + std::vector blobDatas, + float levelDuration, + float maxLevel) + : blobs(std::move(blobDatas), levelDuration, maxLevel) { + style::PaletteChanged( + ) | rpl::start_with_next([=] { + userpicCache = QImage(); + }, lifetime); + } + + Ui::Paint::Blobs blobs; + crl::time lastTime = 0; + crl::time lastSoundingUpdateTime = 0; + float64 enter = 0.; + + QImage userpicCache; + InMemoryKey userpicKey; + + rpl::lifetime lifetime; + }; + void refreshStatus() override; + void setSounding(bool sounding); + void setSpeaking(bool speaking); + void setState(State state); + void setSsrc(uint32 ssrc); + + void ensureUserpicCache( + std::shared_ptr &view, + int size); + + const not_null _delegate; + State _state = State::Inactive; + std::unique_ptr _actionRipple; + std::unique_ptr _blobsAnimation; + Ui::Animations::Simple _speakingAnimation; // For gray-red/green icon. + Ui::Animations::Simple _mutedAnimation; // For gray/red icon. + Ui::Animations::Simple _activeAnimation; // For icon cross animation. + uint32 _ssrc = 0; + bool _sounding = false; + bool _speaking = false; + bool _skipLevelUpdate = false; + +}; + +class MembersController final + : public PeerListController + , public RowDelegate + , public base::has_weak_ptr { +public: + MembersController( + not_null call, + not_null menuParent); + ~MembersController(); + + using MuteRequest = GroupMembers::MuteRequest; + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void rowActionClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + void loadMoreRows() override; + + [[nodiscard]] rpl::producer fullCountValue() const { + return _fullCount.value(); + } + [[nodiscard]] rpl::producer toggleMuteRequests() const; + [[nodiscard]] auto kickMemberRequests() const + -> rpl::producer>; + + bool rowCanMuteMembers() override; + void rowUpdateRow(not_null row) override; + void rowPaintIcon( + Painter &p, + QRect rect, + float64 speaking, + float64 active, + float64 muted) override; + +private: + [[nodiscard]] std::unique_ptr createSelfRow(); + [[nodiscard]] std::unique_ptr createRow( + const Data::GroupCall::Participant &participant); + [[nodiscard]] std::unique_ptr createInvitedRow( + not_null user); + + void prepareRows(not_null real); + //void repaintByTimer(); + + void setupListChangeViewers(not_null call); + void subscribeToChanges(not_null real); + void updateRow( + const std::optional &was, + const Data::GroupCall::Participant &now); + void updateRow( + not_null row, + const Data::GroupCall::Participant *participant); + void removeRow(not_null row); + void updateRowLevel(not_null row, float level); + void checkSpeakingRowPosition(not_null row); + Row *findRow(not_null user) const; + + [[nodiscard]] Data::GroupCall *resolvedRealCall() const; + void appendInvitedUsers(); + + const base::weak_ptr _call; + not_null _peer; + + // Use only resolvedRealCall() method, not this value directly. + Data::GroupCall *_realCallRawValue = nullptr; + uint64 _realId = 0; + bool _prepared = false; + + rpl::event_stream _toggleMuteRequests; + rpl::event_stream> _kickMemberRequests; + rpl::variable _fullCount = 1; + + not_null _menuParent; + base::unique_qptr _menu; + base::flat_set> _menuCheckRowsAfterHidden; + + base::flat_map> _soundingRowBySsrc; + Ui::Animations::Basic _soundingAnimation; + + crl::time _soundingAnimationHideLastTime = 0; + bool _skipRowLevelUpdate = false; + + Ui::CrossLineAnimation _inactiveCrossLine; + Ui::CrossLineAnimation _coloredCrossLine; + + rpl::lifetime _lifetime; + +}; + +Row::Row(not_null delegate, not_null user) +: PeerListRow(user) +, _delegate(delegate) { + refreshStatus(); +} + +void Row::setSkipLevelUpdate(bool value) { + _skipLevelUpdate = value; +} + +void Row::updateState(const Data::GroupCall::Participant *participant) { + setSsrc(participant ? participant->ssrc : 0); + if (!participant) { + setState(State::Invited); + setSounding(false); + setSpeaking(false); + } else if (!participant->muted + || (participant->sounding && participant->ssrc != 0)) { + setState(State::Active); + setSounding(participant->sounding && participant->ssrc != 0); + setSpeaking(participant->speaking && participant->ssrc != 0); + } else if (participant->canSelfUnmute) { + setState(State::Inactive); + setSounding(false); + setSpeaking(false); + } else { + setState(State::Muted); + setSounding(false); + setSpeaking(false); + } +} + +void Row::setSpeaking(bool speaking) { + if (_speaking == speaking) { + return; + } + _speaking = speaking; + _speakingAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + _speaking ? 0. : 1., + _speaking ? 1. : 0., + st::widgetFadeDuration); +} + +void Row::setSounding(bool sounding) { + if (_sounding == sounding) { + return; + } + _sounding = sounding; + if (!_sounding) { + _blobsAnimation = nullptr; + } else if (!_blobsAnimation) { + _blobsAnimation = std::make_unique( + RowBlobs() | ranges::to_vector, + kLevelDuration, + kMaxLevel); + _blobsAnimation->lastTime = crl::now(); + updateLevel(GroupCall::kSpeakLevelThreshold); + } + refreshStatus(); +} + +void Row::setState(State state) { + if (_state == state) { + return; + } + const auto wasActive = (_state == State::Active); + const auto wasMuted = (_state == State::Muted); + _state = state; + const auto nowActive = (_state == State::Active); + const auto nowMuted = (_state == State::Muted); + if (nowActive != wasActive) { + _activeAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + nowActive ? 0. : 1., + nowActive ? 1. : 0., + st::widgetFadeDuration); + } + if (nowMuted != wasMuted) { + _mutedAnimation.start( + [=] { _delegate->rowUpdateRow(this); }, + nowMuted ? 0. : 1., + nowMuted ? 1. : 0., + st::widgetFadeDuration); + } +} + +void Row::setSsrc(uint32 ssrc) { + _ssrc = ssrc; +} + +void Row::updateLevel(float level) { + Expects(_blobsAnimation != nullptr); + + if (_skipLevelUpdate) { + return; + } + + if (level >= GroupCall::kSpeakLevelThreshold) { + _blobsAnimation->lastSoundingUpdateTime = crl::now(); + } + _blobsAnimation->blobs.setLevel(level); +} + +void Row::updateBlobAnimation(crl::time now) { + Expects(_blobsAnimation != nullptr); + + const auto soundingFinishesAt = _blobsAnimation->lastSoundingUpdateTime + + Data::GroupCall::kSoundStatusKeptFor; + const auto soundingStartsFinishing = soundingFinishesAt + - kBlobsEnterDuration; + const auto soundingFinishes = (soundingStartsFinishing < now); + if (soundingFinishes) { + _blobsAnimation->enter = std::clamp( + (soundingFinishesAt - now) / float64(kBlobsEnterDuration), + 0., + 1.); + } else if (_blobsAnimation->enter < 1.) { + _blobsAnimation->enter = std::clamp( + (_blobsAnimation->enter + + ((now - _blobsAnimation->lastTime) + / float64(kBlobsEnterDuration))), + 0., + 1.); + } + _blobsAnimation->blobs.updateLevel(now - _blobsAnimation->lastTime); + _blobsAnimation->lastTime = now; +} + +void Row::ensureUserpicCache( + std::shared_ptr &view, + int size) { + Expects(_blobsAnimation != nullptr); + + const auto user = peer(); + const auto key = user->userpicUniqueKey(view); + const auto full = QSize(size, size) * kWideScale * cIntRetinaFactor(); + auto &cache = _blobsAnimation->userpicCache; + if (cache.isNull()) { + cache = QImage(full, QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(cRetinaFactor()); + } else if (_blobsAnimation->userpicKey == key + && cache.size() == full) { + return; + } + _blobsAnimation->userpicKey = key; + cache.fill(Qt::transparent); + { + Painter p(&cache); + const auto skip = (kWideScale - 1) / 2 * size; + user->paintUserpicLeft(p, view, skip, skip, kWideScale * size, size); + } +} + +auto Row::generatePaintUserpicCallback() -> PaintRoundImageCallback { + auto userpic = ensureUserpicView(); + return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { + if (_blobsAnimation) { + const auto shift = QPointF(x + size / 2., y + size / 2.); + auto hq = PainterHighQualityEnabler(p); + p.translate(shift); + const auto brush = anim::brush( + st::groupCallMemberInactiveStatus, + st::groupCallMemberActiveStatus, + _speakingAnimation.value(_speaking ? 1. : 0.)); + _blobsAnimation->blobs.paint(p, brush); + p.translate(-shift); + p.setOpacity(1.); + + const auto enter = _blobsAnimation->enter; + const auto &minScale = kUserpicMinScale; + const auto scaleUserpic = minScale + + (1. - minScale) * _blobsAnimation->blobs.currentLevel(); + const auto scale = scaleUserpic * enter + 1. * (1. - enter); + if (scale == 1.) { + peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size); + } else { + ensureUserpicCache(userpic, size); + + PainterHighQualityEnabler hq(p); + + auto target = QRect( + x + (1 - kWideScale) / 2 * size, + y + (1 - kWideScale) / 2 * size, + kWideScale * size, + kWideScale * size); + auto shrink = anim::interpolate( + (1 - kWideScale) / 2 * size, + 0, + scale); + auto margins = QMargins(shrink, shrink, shrink, shrink); + p.drawImage( + target.marginsAdded(margins), + _blobsAnimation->userpicCache); + } + } else { + peer()->paintUserpicLeft(p, userpic, x, y, outerWidth, size); + } + }; +} + +void Row::paintStatusText( + Painter &p, + const style::PeerListItem &st, + int x, + int y, + int availableWidth, + int outerWidth, + bool selected) { + if (_state != State::Invited) { + PeerListRow::paintStatusText( + p, + st, + x, + y, + availableWidth, + outerWidth, + selected); + return; + } + p.setFont(st::normalFont); + p.setPen(st::groupCallMemberNotJoinedStatus); + p.drawTextLeft( + x, + y, + outerWidth, + (peer()->isSelf() + ? tr::lng_status_connecting(tr::now) + : tr::lng_group_call_invited_status(tr::now))); +} + +void Row::paintAction( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + auto size = actionSize(); + const auto iconRect = style::rtlrect( + x, + y, + size.width(), + size.height(), + outerWidth); + if (_state == State::Invited) { + _actionRipple = nullptr; + st::groupCallMemberInvited.paint( + p, + QPoint(x, y) + st::groupCallMemberInvitedPosition, + outerWidth); + return; + } + if (_actionRipple) { + _actionRipple->paint( + p, + x + st::groupCallActiveButton.rippleAreaPosition.x(), + y + st::groupCallActiveButton.rippleAreaPosition.y(), + outerWidth); + if (_actionRipple->empty()) { + _actionRipple.reset(); + } + } + const auto speaking = _speakingAnimation.value(_speaking ? 1. : 0.); + const auto active = _activeAnimation.value( + (_state == State::Active) ? 1. : 0.); + const auto muted = _mutedAnimation.value( + (_state == State::Muted) ? 1. : 0.); + _delegate->rowPaintIcon(p, iconRect, speaking, active, muted); +} + +void Row::refreshStatus() { + setCustomStatus( + (_speaking + ? tr::lng_group_call_active(tr::now) + : tr::lng_group_call_inactive(tr::now)), + _speaking); +} + +void Row::addActionRipple(QPoint point, Fn updateCallback) { + if (!_actionRipple) { + auto mask = Ui::RippleAnimation::ellipseMask(QSize( + st::groupCallActiveButton.rippleAreaSize, + st::groupCallActiveButton.rippleAreaSize)); + _actionRipple = std::make_unique( + st::groupCallActiveButton.ripple, + std::move(mask), + std::move(updateCallback)); + } + _actionRipple->add(point - st::groupCallActiveButton.rippleAreaPosition); +} + +void Row::stopLastActionRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + +MembersController::MembersController( + not_null call, + not_null menuParent) +: _call(call) +, _peer(call->peer()) +, _menuParent(menuParent) +, _inactiveCrossLine(st::groupCallMemberInactiveCrossLine) +, _coloredCrossLine(st::groupCallMemberColoredCrossLine) { + setupListChangeViewers(call); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _inactiveCrossLine.invalidate(); + _coloredCrossLine.invalidate(); + }, _lifetime); + + rpl::combine( + rpl::single(anim::Disabled()) | rpl::then(anim::Disables()), + Core::App().appDeactivatedValue() + ) | rpl::start_with_next([=](bool animDisabled, bool deactivated) { + const auto hide = !(!animDisabled && !deactivated); + + if (!(hide && _soundingAnimationHideLastTime)) { + _soundingAnimationHideLastTime = hide ? crl::now() : 0; + } + for (const auto [_, row] : _soundingRowBySsrc) { + if (hide) { + updateRowLevel(row, 0.); + } + row->setSkipLevelUpdate(hide); + } + if (!hide && !_soundingAnimation.animating()) { + _soundingAnimation.start(); + } + _skipRowLevelUpdate = hide; + }, _lifetime); + + _soundingAnimation.init([=](crl::time now) { + if (const auto &last = _soundingAnimationHideLastTime; (last > 0) + && (now - last >= kBlobsEnterDuration)) { + _soundingAnimation.stop(); + return false; + } + for (const auto [ssrc, row] : _soundingRowBySsrc) { + row->updateBlobAnimation(now); + delegate()->peerListUpdateRow(row); + } + return true; + }); +} + +MembersController::~MembersController() { + if (_menu) { + _menu->setDestroyedCallback(nullptr); + _menu = nullptr; + } +} + +void MembersController::setupListChangeViewers(not_null call) { + const auto peer = call->peer(); + peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::GroupCall + ) | rpl::map([=] { + return peer->groupCall(); + }) | rpl::filter([=](Data::GroupCall *real) { + const auto call = _call.get(); + return call && real && (real->id() == call->id()); + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](not_null real) { + subscribeToChanges(real); + }, _lifetime); + + call->stateValue( + ) | rpl::start_with_next([=] { + const auto call = _call.get(); + const auto real = peer->groupCall(); + if (call && real && (real->id() == call->id())) { + //updateRow(channel->session().user()); + } + }, _lifetime); + + call->levelUpdates( + ) | rpl::start_with_next([=](const LevelUpdate &update) { + const auto i = _soundingRowBySsrc.find(update.ssrc); + if (i != end(_soundingRowBySsrc)) { + updateRowLevel(i->second, update.value); + } + }, _lifetime); +} + +void MembersController::subscribeToChanges(not_null real) { + _realCallRawValue = real; + _realId = real->id(); + + _fullCount = real->fullCountValue( + ) | rpl::map([](int value) { + return std::max(value, 1); + }); + + real->participantsSliceAdded( + ) | rpl::start_with_next([=] { + prepareRows(real); + }, _lifetime); + + using Update = Data::GroupCall::ParticipantUpdate; + real->participantUpdated( + ) | rpl::start_with_next([=](const Update &update) { + Expects(update.was.has_value() || update.now.has_value()); + + const auto user = update.was ? update.was->user : update.now->user; + if (!update.now) { + if (const auto row = findRow(user)) { + const auto owner = &user->owner(); + if (user->isSelf()) { + updateRow(row, nullptr); + } else { + removeRow(row); + delegate()->peerListRefreshRows(); + } + } + } else { + updateRow(update.was, *update.now); + } + }, _lifetime); + + if (_prepared) { + appendInvitedUsers(); + } +} + +void MembersController::appendInvitedUsers() { + for (const auto user : _peer->owner().invitedToCallUsers(_realId)) { + if (auto row = createInvitedRow(user)) { + delegate()->peerListAppendRow(std::move(row)); + } + } + delegate()->peerListRefreshRows(); + + using Invite = Data::Session::InviteToCall; + _peer->owner().invitesToCalls( + ) | rpl::filter([=](const Invite &invite) { + return (invite.id == _realId); + }) | rpl::start_with_next([=](const Invite &invite) { + if (auto row = createInvitedRow(invite.user)) { + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListRefreshRows(); + } + }, _lifetime); +} + +void MembersController::updateRow( + const std::optional &was, + const Data::GroupCall::Participant &now) { + if (const auto row = findRow(now.user)) { + if (now.speaking && (!was || !was->speaking)) { + checkSpeakingRowPosition(row); + } + updateRow(row, &now); + } else if (auto row = createRow(now)) { + if (row->speaking()) { + delegate()->peerListPrependRow(std::move(row)); + } else { + delegate()->peerListAppendRow(std::move(row)); + } + delegate()->peerListRefreshRows(); + } +} + +void MembersController::checkSpeakingRowPosition(not_null row) { + if (_menu) { + // Don't reorder rows while we show the popup menu. + _menuCheckRowsAfterHidden.emplace(row->peer()); + return; + } + // Check if there are non-speaking rows above this one. + const auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count; ++i) { + const auto above = delegate()->peerListRowAt(i); + if (above == row) { + // All rows above are speaking. + return; + } else if (!static_cast(above.get())->speaking()) { + break; + } + } + // Someone started speaking and has a non-speaking row above him. Sort. + const auto proj = [&](const PeerListRow &other) { + if (&other == row.get()) { + // Bring this new one to the top. + return 0; + } else if (static_cast(other).speaking()) { + // Bring all the speaking ones below him. + return 1; + } else { + return 2; + } + }; + delegate()->peerListSortRows([&]( + const PeerListRow &a, + const PeerListRow &b) { + return proj(a) < proj(b); + }); +} + +void MembersController::updateRow( + not_null row, + const Data::GroupCall::Participant *participant) { + const auto wasSounding = row->sounding(); + const auto wasSsrc = row->ssrc(); + row->setSkipLevelUpdate(_skipRowLevelUpdate); + row->updateState(participant); + const auto nowSounding = row->sounding(); + const auto nowSsrc = row->ssrc(); + + const auto wasNoSounding = _soundingRowBySsrc.empty(); + if (wasSsrc == nowSsrc) { + if (nowSounding != wasSounding) { + if (nowSounding) { + _soundingRowBySsrc.emplace(nowSsrc, row); + } else { + _soundingRowBySsrc.remove(nowSsrc); + } + } + } else { + _soundingRowBySsrc.remove(wasSsrc); + if (nowSounding) { + Assert(nowSsrc != 0); + _soundingRowBySsrc.emplace(nowSsrc, row); + } + } + const auto nowNoSounding = _soundingRowBySsrc.empty(); + if (wasNoSounding && !nowNoSounding) { + _soundingAnimation.start(); + } else if (nowNoSounding && !wasNoSounding) { + _soundingAnimation.stop(); + } + + delegate()->peerListUpdateRow(row); +} + +void MembersController::removeRow(not_null row) { + _soundingRowBySsrc.remove(row->ssrc()); + delegate()->peerListRemoveRow(row); +} + +void MembersController::updateRowLevel( + not_null row, + float level) { + if (_skipRowLevelUpdate) { + return; + } + row->updateLevel(level); +} + +Row *MembersController::findRow(not_null user) const { + return static_cast(delegate()->peerListFindRow(user->id)); +} + +Data::GroupCall *MembersController::resolvedRealCall() const { + return (_realCallRawValue + && (_peer->groupCall() == _realCallRawValue) + && (_realCallRawValue->id() == _realId)) + ? _realCallRawValue + : nullptr; +} + +Main::Session &MembersController::session() const { + return _call->peer()->session(); +} + +void MembersController::prepare() { + delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled); + //delegate()->peerListSetTitle(std::move(title)); + setDescriptionText(tr::lng_contacts_loading(tr::now)); + setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now)); + + const auto call = _call.get(); + if (const auto real = _peer->groupCall(); + real && call && real->id() == call->id()) { + prepareRows(real); + } else if (auto row = createSelfRow()) { + delegate()->peerListAppendRow(std::move(row)); + delegate()->peerListRefreshRows(); + } + + loadMoreRows(); + if (_realId) { + appendInvitedUsers(); + } + _prepared = true; +} + +void MembersController::prepareRows(not_null real) { + auto foundSelf = false; + auto changed = false; + const auto &participants = real->participants(); + auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count;) { + auto row = delegate()->peerListRowAt(i); + auto user = row->peer()->asUser(); + if (user->isSelf()) { + foundSelf = true; + ++i; + continue; + } + const auto contains = ranges::contains( + participants, + not_null{ user }, + &Data::GroupCall::Participant::user); + if (contains) { + ++i; + } else { + changed = true; + removeRow(static_cast(row.get())); + --count; + } + } + if (!foundSelf) { + const auto self = _peer->session().user(); + const auto i = ranges::find( + participants, + _peer->session().user(), + &Data::GroupCall::Participant::user); + auto row = (i != end(participants)) ? createRow(*i) : createSelfRow(); + if (row) { + changed = true; + delegate()->peerListAppendRow(std::move(row)); + } + } + for (const auto &participant : participants) { + if (auto row = createRow(participant)) { + changed = true; + delegate()->peerListAppendRow(std::move(row)); + } + } + if (changed) { + delegate()->peerListRefreshRows(); + } +} + +void MembersController::loadMoreRows() { + if (const auto real = _peer->groupCall()) { + real->requestParticipants(); + } +} + +auto MembersController::toggleMuteRequests() const +-> rpl::producer { + return _toggleMuteRequests.events(); +} + +bool MembersController::rowCanMuteMembers() { + return _peer->canManageGroupCall(); +} + +void MembersController::rowUpdateRow(not_null row) { + delegate()->peerListUpdateRow(row); +} + +void MembersController::rowPaintIcon( + Painter &p, + QRect rect, + float64 speaking, + float64 active, + float64 muted) { + const auto &greenIcon = st::groupCallMemberColoredCrossLine.icon; + const auto left = rect.x() + (rect.width() - greenIcon.width()) / 2; + const auto top = rect.y() + (rect.height() - greenIcon.height()) / 2; + if (speaking == 1.) { + // Just green icon, no cross, no coloring. + greenIcon.paintInCenter(p, rect); + return; + } else if (speaking == 0.) { + if (active == 1.) { + // Just gray icon, no cross, no coloring. + st::groupCallMemberInactiveCrossLine.icon.paintInCenter(p, rect); + return; + } else if (active == 0.) { + if (muted == 1.) { + // Red crossed icon, colorized once, cached as last frame. + _coloredCrossLine.paint( + p, + left, + top, + 1., + st::groupCallMemberMutedIcon->c); + return; + } else if (muted == 0.) { + // Gray crossed icon, no coloring, cached as last frame. + _inactiveCrossLine.paint(p, left, top, 1.); + return; + } + } + } + const auto activeInactiveColor = anim::color( + st::groupCallMemberInactiveIcon, + st::groupCallMemberActiveIcon, + speaking); + const auto iconColor = anim::color( + activeInactiveColor, + st::groupCallMemberMutedIcon, + muted); + + // Don't use caching of the last frame, because 'muted' may animate color. + const auto crossProgress = std::min(1. - active, 0.9999); + _inactiveCrossLine.paint(p, left, top, crossProgress, iconColor); +} + +auto MembersController::kickMemberRequests() const +-> rpl::producer>{ + return _kickMemberRequests.events(); +} + +void MembersController::rowClicked(not_null row) { + if (_menu) { + _menu->setDestroyedCallback(nullptr); + _menu->deleteLater(); + _menu = nullptr; + } + _menu = rowContextMenu(_menuParent, row); + if (const auto raw = _menu.get()) { + raw->setDestroyedCallback([=] { + if (_menu && _menu.get() != raw) { + return; + } + auto saved = base::take(_menu); + for (const auto peer : base::take(_menuCheckRowsAfterHidden)) { + if (const auto row = findRow(peer->asUser())) { + if (row->speaking()) { + checkSpeakingRowPosition(row); + } + } + } + _menu = std::move(saved); + }); + raw->popup(QCursor::pos()); + } +} + +void MembersController::rowActionClicked( + not_null row) { + rowClicked(row); +} + +base::unique_qptr MembersController::rowContextMenu( + QWidget *parent, + not_null row) { + Expects(row->peer()->isUser()); + + if (row->peer()->isSelf()) { + return nullptr; + } + const auto real = static_cast(row.get()); + const auto user = row->peer()->asUser(); + auto result = base::make_unique_q( + parent, + st::groupCallPopupMenu); + + const auto muteState = real->state(); + const auto admin = [&] { + if (const auto chat = _peer->asChat()) { + return chat->admins.contains(user) + || (chat->creator == user->bareId()); + } else if (const auto group = _peer->asMegagroup()) { + if (const auto mgInfo = group->mgInfo.get()) { + if (mgInfo->creator == user) { + return true; + } + const auto i = mgInfo->lastAdmins.find(user); + if (i == mgInfo->lastAdmins.end()) { + return false; + } + const auto &rights = i->second.rights; + return rights.c_chatAdminRights().is_manage_call(); + } + } + return false; + }(); + const auto mute = admin + ? (muteState == Row::State::Active) + : (muteState != Row::State::Muted); + const auto toggleMute = crl::guard(this, [=] { + _toggleMuteRequests.fire(MuteRequest{ + .user = user, + .mute = mute, + }); + }); + + const auto session = &user->session(); + const auto getCurrentWindow = [=]() -> Window::SessionController* { + if (const auto window = Core::App().activeWindow()) { + if (const auto controller = window->sessionController()) { + if (&controller->session() == session) { + return controller; + } + } + } + return nullptr; + }; + const auto getWindow = [=] { + if (const auto current = getCurrentWindow()) { + return current; + } else if (&Core::App().domain().active() != &session->account()) { + Core::App().domain().activate(&session->account()); + } + return getCurrentWindow(); + }; + const auto performOnMainWindow = [=](auto callback) { + if (const auto window = getWindow()) { + if (_menu) { + _menu->discardParentReActivate(); + + // We must hide PopupMenu before we activate the MainWindow, + // otherwise we set focus in field inside MainWindow and then + // PopupMenu::hide activates back the group call panel :( + _menu = nullptr; + } + callback(window); + window->widget()->activate(); + } + }; + const auto showProfile = [=] { + performOnMainWindow([=](not_null window) { + window->showPeerInfo(user); + }); + }; + const auto showHistory = [=] { + performOnMainWindow([=](not_null window) { + window->showPeerHistory( + user, + Window::SectionShow::Way::Forward); + }); + }; + const auto removeFromGroup = crl::guard(this, [=] { + _kickMemberRequests.fire_copy(user); + }); + + if (_peer->canManageGroupCall() && (!admin || mute)) { + result->addAction( + (mute + ? tr::lng_group_call_context_mute(tr::now) + : tr::lng_group_call_context_unmute(tr::now)), + toggleMute); + } + result->addAction( + tr::lng_context_view_profile(tr::now), + showProfile); + result->addAction( + tr::lng_context_send_message(tr::now), + showHistory); + const auto canKick = [&] { + if (static_cast(row.get())->state() == Row::State::Invited) { + return false; + } else if (const auto chat = _peer->asChat()) { + return chat->amCreator() + || (chat->canBanMembers() && !chat->admins.contains(user)); + } else if (const auto group = _peer->asMegagroup()) { + return group->canRestrictUser(user); + } + return false; + }(); + if (canKick) { + result->addAction( + tr::lng_context_remove_from_group(tr::now), + removeFromGroup); + } + return result; +} + +std::unique_ptr MembersController::createSelfRow() { + const auto self = _peer->session().user(); + auto result = std::make_unique(this, self); + updateRow(result.get(), nullptr); + return result; +} + +std::unique_ptr MembersController::createRow( + const Data::GroupCall::Participant &participant) { + auto result = std::make_unique(this, participant.user); + updateRow(result.get(), &participant); + return result; +} + +std::unique_ptr MembersController::createInvitedRow( + not_null user) { + if (findRow(user)) { + return nullptr; + } + auto result = std::make_unique(this, user); + updateRow(result.get(), nullptr); + return result; +} + +} // namespace + +GroupMembers::GroupMembers( + not_null parent, + not_null call) +: RpWidget(parent) +, _call(call) +, _scroll(this, st::defaultSolidScroll) +, _listController(std::make_unique(call, parent)) { + setupAddMember(call); + setupList(); + setContent(_list); + setupFakeRoundCorners(); + _listController->setDelegate(static_cast(this)); +} + +auto GroupMembers::toggleMuteRequests() const +-> rpl::producer { + return static_cast( + _listController.get())->toggleMuteRequests(); +} + +auto GroupMembers::kickMemberRequests() const +-> rpl::producer> { + return static_cast( + _listController.get())->kickMemberRequests(); +} + +int GroupMembers::desiredHeight() const { + const auto top = _addMember ? _addMember->height() : 0; + auto count = [&] { + if (const auto call = _call.get()) { + if (const auto real = call->peer()->groupCall()) { + if (call->id() == real->id()) { + return real->fullCount(); + } + } + } + return 0; + }(); + const auto use = std::max(count, _list->fullRowsCount()); + return top + + (use * st::groupCallMembersList.item.height) + + (use ? st::lineWidth : 0); +} + +rpl::producer GroupMembers::desiredHeightValue() const { + const auto controller = static_cast( + _listController.get()); + return rpl::combine( + heightValue(), + _addMemberButton.value(), + controller->fullCountValue() + ) | rpl::map([=] { + return desiredHeight(); + }); +} + +void GroupMembers::setupAddMember(not_null call) { + using namespace rpl::mappers; + + _canAddMembers = Data::CanWriteValue(call->peer().get()); + SubscribeToMigration( + call->peer(), + lifetime(), + [=](not_null channel) { + _canAddMembers = Data::CanWriteValue(channel.get()); + }); + + _canAddMembers.value( + ) | rpl::start_with_next([=](bool can) { + if (!can) { + _addMemberButton = nullptr; + _addMember.destroy(); + updateControlsGeometry(); + return; + } + _addMember = Settings::CreateButton( + this, + tr::lng_group_call_invite(), + st::groupCallAddMember, + &st::groupCallAddMemberIcon, + st::groupCallAddMemberIconLeft); + _addMember->show(); + + _addMember->addClickHandler([=] { // TODO throttle(ripple duration) + _addMemberRequests.fire({}); + }); + _addMemberButton = _addMember.data(); + + resizeToList(); + }, lifetime()); +} + +rpl::producer GroupMembers::fullCountValue() const { + return static_cast( + _listController.get())->fullCountValue(); +} + +//tr::lng_chat_status_members( +// lt_count_decimal, +// controller->fullCountValue() | tr::to_count(), +// Ui::Text::Upper +//), + +void GroupMembers::setupList() { + _listController->setStyleOverrides(&st::groupCallMembersList); + _list = _scroll->setOwnedWidget(object_ptr( + this, + _listController.get())); + + _list->heightValue( + ) | rpl::start_with_next([=] { + resizeToList(); + }, _list->lifetime()); + + updateControlsGeometry(); +} + +void GroupMembers::resizeEvent(QResizeEvent *e) { + updateControlsGeometry(); +} + +void GroupMembers::resizeToList() { + if (!_list) { + return; + } + const auto listHeight = _list->height(); + const auto newHeight = (listHeight > 0) + ? ((_addMember ? _addMember->height() : 0) + + listHeight + + st::lineWidth) + : 0; + if (height() == newHeight) { + updateControlsGeometry(); + } else { + resize(width(), newHeight); + } +} + +void GroupMembers::updateControlsGeometry() { + if (!_list) { + return; + } + auto topSkip = 0; + if (_addMember) { + _addMember->resizeToWidth(width()); + _addMember->move(0, 0); + topSkip = _addMember->height(); + } + _scroll->setGeometry(0, topSkip, width(), height() - topSkip); + _list->resizeToWidth(width()); +} + +void GroupMembers::setupFakeRoundCorners() { + const auto size = st::roundRadiusLarge; + const auto full = 3 * size; + const auto imagePartSize = size * cIntRetinaFactor(); + const auto imageSize = full * cIntRetinaFactor(); + const auto image = std::make_shared( + QImage(imageSize, imageSize, QImage::Format_ARGB32_Premultiplied)); + image->setDevicePixelRatio(cRetinaFactor()); + + const auto refreshImage = [=] { + image->fill(st::groupCallBg->c); + { + QPainter p(image.get()); + PainterHighQualityEnabler hq(p); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setPen(Qt::NoPen); + p.setBrush(Qt::transparent); + p.drawRoundedRect(0, 0, full, full, size, size); + } + }; + + const auto create = [&](QPoint imagePartOrigin) { + const auto result = Ui::CreateChild(this); + result->show(); + result->resize(size, size); + result->setAttribute(Qt::WA_TransparentForMouseEvents); + result->paintRequest( + ) | rpl::start_with_next([=] { + QPainter(result).drawImage( + result->rect(), + *image, + QRect(imagePartOrigin, QSize(imagePartSize, imagePartSize))); + }, result->lifetime()); + result->raise(); + return result; + }; + const auto shift = imageSize - imagePartSize; + const auto topleft = create({ 0, 0 }); + const auto topright = create({ shift, 0 }); + const auto bottomleft = create({ 0, shift }); + const auto bottomright = create({ shift, shift }); + + sizeValue( + ) | rpl::start_with_next([=](QSize size) { + topleft->move(0, 0); + topright->move(size.width() - topright->width(), 0); + bottomleft->move(0, size.height() - bottomleft->height()); + bottomright->move( + size.width() - bottomright->width(), + size.height() - bottomright->height()); + }, lifetime()); + + refreshImage(); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + refreshImage(); + topleft->update(); + topright->update(); + bottomleft->update(); + bottomright->update(); + }, lifetime()); +} + +void GroupMembers::visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) { + setChildVisibleTopBottom(_list, visibleTop, visibleBottom); +} + +void GroupMembers::peerListSetTitle(rpl::producer title) { +} + +void GroupMembers::peerListSetAdditionalTitle(rpl::producer title) { +} + +void GroupMembers::peerListSetHideEmpty(bool hide) { +} + +bool GroupMembers::peerListIsRowChecked(not_null row) { + return false; +} + +void GroupMembers::peerListScrollToTop() { +} + +int GroupMembers::peerListSelectedRowsCount() { + return 0; +} + +void GroupMembers::peerListAddSelectedPeerInBunch(not_null peer) { + Unexpected("Item selection in Calls::GroupMembers."); +} + +void GroupMembers::peerListAddSelectedRowInBunch(not_null row) { + Unexpected("Item selection in Calls::GroupMembers."); +} + +void GroupMembers::peerListFinishSelectedRowsBunch() { +} + +void GroupMembers::peerListSetDescription( + object_ptr description) { + description.destroy(); +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_members.h b/Telegram/SourceFiles/calls/calls_group_members.h new file mode 100644 index 000000000..e255d2ee4 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_members.h @@ -0,0 +1,91 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "boxes/peer_list_box.h" + +namespace Ui { +class ScrollArea; +class SettingsButton; +} // namespace Ui + +namespace Data { +class GroupCall; +} // namespace Data + +namespace Calls { + +class GroupCall; + +class GroupMembers final + : public Ui::RpWidget + , private PeerListContentDelegate { +public: + GroupMembers( + not_null parent, + not_null call); + + struct MuteRequest { + not_null user; + bool mute = false; + }; + + [[nodiscard]] int desiredHeight() const; + [[nodiscard]] rpl::producer desiredHeightValue() const override; + [[nodiscard]] rpl::producer fullCountValue() const; + [[nodiscard]] rpl::producer toggleMuteRequests() const; + [[nodiscard]] auto kickMemberRequests() const + -> rpl::producer>; + [[nodiscard]] rpl::producer<> addMembersRequests() const { + return _addMemberRequests.events(); + } + +private: + using ListWidget = PeerListContent; + + void visibleTopBottomUpdated( + int visibleTop, + int visibleBottom) override; + + void resizeEvent(QResizeEvent *e) override; + + // PeerListContentDelegate interface. + void peerListSetTitle(rpl::producer title) override; + void peerListSetAdditionalTitle(rpl::producer title) override; + void peerListSetHideEmpty(bool hide) override; + bool peerListIsRowChecked(not_null row) override; + int peerListSelectedRowsCount() override; + void peerListScrollToTop() override; + void peerListAddSelectedPeerInBunch( + not_null peer) override; + void peerListAddSelectedRowInBunch( + not_null row) override; + void peerListFinishSelectedRowsBunch() override; + void peerListSetDescription( + object_ptr description) override; + + void setupAddMember(not_null call); + void resizeToList(); + void setupList(); + void setupFakeRoundCorners(); + + void updateControlsGeometry(); + + const base::weak_ptr _call; + object_ptr _scroll; + std::unique_ptr _listController; + object_ptr _addMember = { nullptr }; + rpl::variable _addMemberButton = nullptr; + ListWidget *_list = { nullptr }; + rpl::event_stream<> _addMemberRequests; + + rpl::variable _canAddMembers; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp new file mode 100644 index 000000000..a6e260bb2 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_panel.cpp @@ -0,0 +1,892 @@ +/* +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 "calls/calls_group_panel.h" + +#include "calls/calls_group_members.h" +#include "calls/calls_group_settings.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/window.h" +#include "ui/widgets/call_button.h" +#include "ui/widgets/call_mute_button.h" +#include "ui/widgets/checkbox.h" +#include "ui/layers/layer_manager.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "info/profile/info_profile_values.h" // Info::Profile::Value. +#include "core/application.h" +#include "lang/lang_keys.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_user.h" +#include "data/data_group_call.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "base/event_filter.h" +#include "boxes/peers/edit_participants_box.h" +#include "boxes/peers/add_participants_box.h" +#include "boxes/peer_lists_box.h" +#include "boxes/confirm_box.h" +#include "app.h" +#include "apiwrap.h" // api().kickParticipant. +#include "styles/style_calls.h" +#include "styles/style_layers.h" + +#ifdef Q_OS_WIN +#include "ui/platform/win/ui_window_title_win.h" +#endif // Q_OS_WIN + +#include +#include +#include + +namespace Calls { +namespace { + +constexpr auto kSpacePushToTalkDelay = crl::time(250); + +class InviteController final : public ParticipantsBoxController { +public: + InviteController( + not_null peer, + base::flat_set> alreadyIn); + + void prepare() override; + + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + + void itemDeselectedHook(not_null peer) override; + + [[nodiscard]] auto peersWithRows() const + -> not_null>*>; + [[nodiscard]] rpl::producer> rowAdded() const; + + [[nodiscard]] bool hasRowFor(not_null peer) const; + +private: + [[nodiscard]] bool isAlreadyIn(not_null user) const; + + std::unique_ptr createRow( + not_null user) const override; + + not_null _peer; + const base::flat_set> _alreadyIn; + mutable base::flat_set> _inGroup; + rpl::event_stream> _rowAdded; + +}; + +class InviteContactsController final : public AddParticipantsBoxController { +public: + InviteContactsController( + not_null peer, + base::flat_set> alreadyIn, + not_null>*> inGroup, + rpl::producer> discoveredInGroup); + +private: + void prepareViewHook() override; + + std::unique_ptr createRow( + not_null user) override; + + const not_null>*> _inGroup; + rpl::producer> _discoveredInGroup; + + rpl::lifetime _lifetime; + +}; + +[[nodiscard]] object_ptr CreateSectionSubtitle( + QWidget *parent, + rpl::producer text) { + auto result = object_ptr( + parent, + st::searchedBarHeight); + + const auto raw = result.data(); + raw->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(raw); + p.fillRect(clip, st::groupCallMembersBgOver); + }, raw->lifetime()); + + const auto label = Ui::CreateChild( + raw, + std::move(text), + st::groupCallBoxLabel); + raw->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto padding = st::groupCallInviteDividerPadding; + const auto available = width - padding.left() - padding.right(); + label->resizeToNaturalWidth(available); + label->moveToLeft(padding.left(), padding.top(), width); + }, label->lifetime()); + + return result; +} + +InviteController::InviteController( + not_null peer, + base::flat_set> alreadyIn) +: ParticipantsBoxController(CreateTag{}, nullptr, peer, Role::Members) +, _peer(peer) +, _alreadyIn(std::move(alreadyIn)) { + SubscribeToMigration( + _peer, + lifetime(), + [=](not_null channel) { _peer = channel; }); +} + +void InviteController::prepare() { + delegate()->peerListSetHideEmpty(true); + ParticipantsBoxController::prepare(); + delegate()->peerListSetAboveWidget(CreateSectionSubtitle( + nullptr, + tr::lng_group_call_invite_members())); + delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle( + nullptr, + tr::lng_group_call_invite_members())); +} + +void InviteController::rowClicked(not_null row) { + delegate()->peerListSetRowChecked(row, !row->checked()); +} + +base::unique_qptr InviteController::rowContextMenu( + QWidget *parent, + not_null row) { + return nullptr; +} + +void InviteController::itemDeselectedHook(not_null peer) { +} + +bool InviteController::hasRowFor(not_null peer) const { + return (delegate()->peerListFindRow(peer->id) != nullptr); +} + +bool InviteController::isAlreadyIn(not_null user) const { + return _alreadyIn.contains(user); +} + +std::unique_ptr InviteController::createRow( + not_null user) const { + if (user->isSelf() || user->isBot()) { + return nullptr; + } + auto result = std::make_unique(user); + _rowAdded.fire_copy(user); + _inGroup.emplace(user); + if (isAlreadyIn(user)) { + result->setDisabledState(PeerListRow::State::DisabledChecked); + } + return result; +} + +auto InviteController::peersWithRows() const +-> not_null>*> { + return &_inGroup; +} + +rpl::producer> InviteController::rowAdded() const { + return _rowAdded.events(); +} + +InviteContactsController::InviteContactsController( + not_null peer, + base::flat_set> alreadyIn, + not_null>*> inGroup, + rpl::producer> discoveredInGroup) +: AddParticipantsBoxController(peer, std::move(alreadyIn)) +, _inGroup(inGroup) +, _discoveredInGroup(std::move(discoveredInGroup)) { +} + +void InviteContactsController::prepareViewHook() { + AddParticipantsBoxController::prepareViewHook(); + + delegate()->peerListSetAboveWidget(CreateSectionSubtitle( + nullptr, + tr::lng_contacts_header())); + delegate()->peerListSetAboveSearchWidget(CreateSectionSubtitle( + nullptr, + tr::lng_group_call_invite_search_results())); + + std::move( + _discoveredInGroup + ) | rpl::start_with_next([=](not_null user) { + if (auto row = delegate()->peerListFindRow(user->id)) { + delegate()->peerListRemoveRow(row); + } + }, _lifetime); +} + +std::unique_ptr InviteContactsController::createRow( + not_null user) { + return _inGroup->contains(user) + ? nullptr + : AddParticipantsBoxController::createRow(user); +} + +} // namespace + +void LeaveGroupCallBox( + not_null box, + not_null call, + bool discardChecked, + BoxContext context) { + box->setTitle(tr::lng_group_call_leave_title()); + const auto inCall = (context == BoxContext::GroupCallPanel); + box->addRow(object_ptr( + box.get(), + tr::lng_group_call_leave_sure(), + (inCall ? st::groupCallBoxLabel : st::boxLabel))); + const auto discard = call->peer()->canManageGroupCall() + ? box->addRow(object_ptr( + box.get(), + tr::lng_group_call_end(), + discardChecked, + (inCall ? st::groupCallCheckbox : st::defaultBoxCheckbox), + (inCall ? st::groupCallCheck : st::defaultCheck)), + style::margins( + st::boxRowPadding.left(), + st::boxRowPadding.left(), + st::boxRowPadding.right(), + st::boxRowPadding.bottom())) + : nullptr; + const auto weak = base::make_weak(call.get()); + box->addButton(tr::lng_group_call_leave(), [=] { + const auto discardCall = (discard && discard->checked()); + box->closeBox(); + + if (!weak) { + return; + } else if (discardCall) { + call->discard(); + } else { + call->hangup(); + } + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +void GroupCallConfirmBox( + not_null box, + const QString &text, + rpl::producer button, + Fn callback) { + box->addRow( + object_ptr( + box.get(), + text, + st::groupCallBoxLabel), + st::boxPadding); + box->addButton(std::move(button), callback); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} + +GroupPanel::GroupPanel(not_null call) +: _call(call) +, _peer(call->peer()) +, _window(std::make_unique(Core::App().getModalParent())) +, _layerBg(std::make_unique(_window->body())) +#ifdef Q_OS_WIN +, _controls(std::make_unique( + _window.get(), + st::groupCallTitle)) +#endif // Q_OS_WIN +, _members(widget(), call) +, _settings(widget(), st::groupCallSettings) +, _mute(std::make_unique( + widget(), + Core::App().appDeactivatedValue(), + Ui::CallMuteButtonState{ + .text = tr::lng_group_call_connecting(tr::now), + .type = Ui::CallMuteButtonType::Connecting, + })) +, _hangup(widget(), st::groupCallHangup) { + _layerBg->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox); + _settings->setColorOverrides(_mute->colorOverrides()); + _layerBg->setHideByBackgroundClick(true); + + SubscribeToMigration( + _peer, + _window->lifetime(), + [=](not_null channel) { migrate(channel); }); + + initWindow(); + initWidget(); + initControls(); + initLayout(); + showAndActivate(); +} + +GroupPanel::~GroupPanel() = default; + +bool GroupPanel::isActive() const { + return _window->isActiveWindow() + && _window->isVisible() + && !(_window->windowState() & Qt::WindowMinimized); +} + +void GroupPanel::minimize() { + _window->setWindowState(_window->windowState() | Qt::WindowMinimized); +} + +void GroupPanel::close() { + _window->close(); +} + +void GroupPanel::showAndActivate() { + if (_window->isHidden()) { + _window->show(); + } + const auto state = _window->windowState(); + if (state & Qt::WindowMinimized) { + _window->setWindowState(state & ~Qt::WindowMinimized); + } + _window->raise(); + _window->activateWindow(); + _window->setFocus(); +} + +void GroupPanel::migrate(not_null channel) { + _peer = channel; + _peerLifetime.destroy(); + subscribeToPeerChanges(); + _title.destroy(); + refreshTitle(); +} + +void GroupPanel::subscribeToPeerChanges() { + Info::Profile::NameValue( + _peer + ) | rpl::start_with_next([=](const TextWithEntities &name) { + _window->setTitle(name.text); + }, _peerLifetime); +} + +void GroupPanel::initWindow() { + _window->setAttribute(Qt::WA_OpaquePaintEvent); + _window->setAttribute(Qt::WA_NoSystemBackground); + _window->setWindowIcon( + QIcon(QPixmap::fromImage(Image::Empty()->original(), Qt::ColorOnly))); + _window->setTitleStyle(st::groupCallTitle); + + subscribeToPeerChanges(); + + base::install_event_filter(_window.get(), [=](not_null e) { + if (e->type() == QEvent::Close && handleClose()) { + e->ignore(); + return base::EventFilterResult::Cancel; + } else if (e->type() == QEvent::KeyPress + || e->type() == QEvent::KeyRelease) { + if (static_cast(e.get())->key() == Qt::Key_Space) { + if (_call) { + _call->pushToTalk( + e->type() == QEvent::KeyPress, + kSpacePushToTalkDelay); + } + } + } + return base::EventFilterResult::Continue; + }); + + _window->setBodyTitleArea([=](QPoint widgetPoint) { + using Flag = Ui::WindowTitleHitTestFlag; + const auto titleRect = QRect( + 0, + 0, + widget()->width(), + computeMembersListTop()); + return titleRect.contains(widgetPoint) + ? (Flag::Move | Flag::Maximize) + : Flag::None; + }); +} + +void GroupPanel::initWidget() { + widget()->setMouseTracking(true); + + widget()->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + paint(clip); + }, widget()->lifetime()); + + widget()->sizeValue( + ) | rpl::skip(1) | rpl::start_with_next([=] { + updateControlsGeometry(); + + // title geometry depends on _controls->geometry, + // which is not updated here yet. + crl::on_main(widget(), [=] { refreshTitle(); }); + }, widget()->lifetime()); +} + +void GroupPanel::endCall() { + if (!_call) { + return; + } else if (!_call->peer()->canManageGroupCall()) { + _call->hangup(); + return; + } + _layerBg->showBox(Box( + LeaveGroupCallBox, + _call, + false, + BoxContext::GroupCallPanel)); +} + +void GroupPanel::initControls() { + _mute->clicks( + ) | rpl::filter([=](Qt::MouseButton button) { + return (button == Qt::LeftButton) && (_call != nullptr); + }) | rpl::start_with_next([=] { + if (_call->muted() == MuteState::ForceMuted) { + _mute->shake(); + } else { + _call->setMuted((_call->muted() == MuteState::Muted) + ? MuteState::Active + : MuteState::Muted); + } + }, _mute->lifetime()); + + _hangup->setClickedCallback([=] { endCall(); }); + _settings->setClickedCallback([=] { + if (_call) { + _layerBg->showBox(Box(GroupCallSettingsBox, _call)); + } + }); + + _settings->setText(tr::lng_menu_settings()); + _hangup->setText(tr::lng_group_call_leave()); + + _members->desiredHeightValue( + ) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _members->lifetime()); + + initWithCall(_call); +} + +void GroupPanel::initWithCall(GroupCall *call) { + _callLifetime.destroy(); + _call = call; + if (!_call) { + return; + } + + _peer = _call->peer(); + + call->stateValue( + ) | rpl::filter([](State state) { + return (state == State::HangingUp) + || (state == State::Ended) + || (state == State::FailedHangingUp) + || (state == State::Failed); + }) | rpl::start_with_next([=] { + closeBeforeDestroy(); + }, _callLifetime); + + call->levelUpdates( + ) | rpl::filter([=](const LevelUpdate &update) { + return update.self; + }) | rpl::start_with_next([=](const LevelUpdate &update) { + _mute->setLevel(update.value); + }, _callLifetime); + + _members->toggleMuteRequests( + ) | rpl::start_with_next([=](GroupMembers::MuteRequest request) { + if (_call) { + _call->toggleMute(request.user, request.mute); + } + }, _callLifetime); + + _members->kickMemberRequests( + ) | rpl::start_with_next([=](not_null user) { + kickMember(user); + }, _callLifetime); + + _members->addMembersRequests( + ) | rpl::start_with_next([=] { + if (_call) { + addMembers(); + } + }, _callLifetime); + + rpl::combine( + _call->mutedValue() | MapPushToTalkToActive(), + _call->connectingValue() + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](MuteState mute, bool connecting) { + _mute->setState(Ui::CallMuteButtonState{ + .text = (connecting + ? tr::lng_group_call_connecting(tr::now) + : mute == MuteState::ForceMuted + ? tr::lng_group_call_force_muted(tr::now) + : mute == MuteState::Muted + ? tr::lng_group_call_unmute(tr::now) + : tr::lng_group_call_you_are_live(tr::now)), + .subtext = (connecting + ? QString() + : mute == MuteState::ForceMuted + ? tr::lng_group_call_force_muted_sub(tr::now) + : mute == MuteState::Muted + ? tr::lng_group_call_unmute_sub(tr::now) + : QString()), + .type = (connecting + ? Ui::CallMuteButtonType::Connecting + : mute == MuteState::ForceMuted + ? Ui::CallMuteButtonType::ForceMuted + : mute == MuteState::Muted + ? Ui::CallMuteButtonType::Muted + : Ui::CallMuteButtonType::Active), + }); + }, _callLifetime); +} + +void GroupPanel::addMembers() { + const auto real = _peer->groupCall(); + if (!_call || !real || real->id() != _call->id()) { + return; + } + auto alreadyIn = _peer->owner().invitedToCallUsers(real->id()); + for (const auto &participant : real->participants()) { + alreadyIn.emplace(participant.user); + } + alreadyIn.emplace(_peer->session().user()); + auto controller = std::make_unique( + _peer, + alreadyIn); + controller->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + + auto contactsController = std::make_unique( + _peer, + std::move(alreadyIn), + controller->peersWithRows(), + controller->rowAdded()); + contactsController->setStyleOverrides( + &st::groupCallInviteMembersList, + &st::groupCallMultiSelect); + + const auto weak = base::make_weak(_call); + const auto invite = [=](const std::vector> &users) { + const auto call = weak.get(); + if (!call) { + return; + } + const auto result = call->inviteUsers(users); + if (const auto user = std::get_if>(&result)) { + Ui::Toast::Show( + widget(), + Ui::Toast::Config{ + .text = tr::lng_group_call_invite_done_user( + tr::now, + lt_user, + Ui::Text::Bold((*user)->firstName), + Ui::Text::WithEntities), + .st = &st::defaultToast, + }); + } else if (const auto count = std::get_if(&result)) { + if (*count > 0) { + Ui::Toast::Show( + widget(), + Ui::Toast::Config{ + .text = tr::lng_group_call_invite_done_many( + tr::now, + lt_count, + *count, + Ui::Text::RichLangValue), + .st = &st::defaultToast, + }); + } + } else { + Unexpected("Result in GroupCall::inviteUsers."); + } + }; + const auto inviteWithAdd = [=]( + const std::vector> &users, + const std::vector> &nonMembers, + Fn finish) { + _peer->session().api().addChatParticipants( + _peer, + nonMembers, + [=](bool) { invite(users); finish(); }); + }; + const auto inviteWithConfirmation = [=]( + const std::vector> &users, + const std::vector> &nonMembers, + Fn finish) { + if (nonMembers.empty()) { + invite(users); + finish(); + return; + } + const auto name = _peer->name; + const auto text = (nonMembers.size() == 1) + ? tr::lng_group_call_add_to_group_one( + tr::now, + lt_user, + nonMembers.front()->shortName(), + lt_group, + name) + : (nonMembers.size() < users.size()) + ? tr::lng_group_call_add_to_group_some(tr::now, lt_group, name) + : tr::lng_group_call_add_to_group_all(tr::now, lt_group, name); + const auto shared = std::make_shared>(); + const auto finishWithConfirm = [=] { + if (*shared) { + (*shared)->closeBox(); + } + finish(); + }; + auto box = Box( + GroupCallConfirmBox, + text, + tr::lng_participant_invite(), + [=] { inviteWithAdd(users, nonMembers, finishWithConfirm); }); + *shared = box.data(); + _layerBg->showBox(std::move(box)); + }; + auto initBox = [=, controller = controller.get()]( + not_null box) { + box->setTitle(tr::lng_group_call_invite_title()); + box->addButton(tr::lng_group_call_invite_button(), [=] { + const auto rows = box->collectSelectedRows(); + + const auto users = ranges::view::all( + rows + ) | ranges::view::transform([](not_null peer) { + return not_null(peer->asUser()); + }) | ranges::to_vector; + + const auto nonMembers = ranges::view::all( + users + ) | ranges::view::filter([&](not_null user) { + return !controller->hasRowFor(user); + }) | ranges::to_vector; + + const auto finish = [box = Ui::MakeWeak(box)]() { + if (box) { + box->closeBox(); + } + }; + inviteWithConfirmation(users, nonMembers, finish); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + }; + + auto controllers = std::vector>(); + controllers.push_back(std::move(controller)); + controllers.push_back(std::move(contactsController)); + _layerBg->showBox(Box(std::move(controllers), initBox)); +} + +void GroupPanel::kickMember(not_null user) { + _layerBg->showBox(Box([=](not_null box) { + box->addRow( + object_ptr( + box.get(), + tr::lng_profile_sure_kick( + tr::now, + lt_user, + user->firstName), + st::groupCallBoxLabel), + style::margins( + st::boxRowPadding.left(), + st::boxPadding.top(), + st::boxRowPadding.right(), + st::boxPadding.bottom())); + box->addButton(tr::lng_box_remove(), [=] { + box->closeBox(); + kickMemberSure(user); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + })); +} + +void GroupPanel::kickMemberSure(not_null user) { + if (const auto chat = _peer->asChat()) { + chat->session().api().kickParticipant(chat, user); + } else if (const auto channel = _peer->asChannel()) { + const auto currentRestrictedRights = [&]() -> MTPChatBannedRights { + const auto it = channel->mgInfo->lastRestricted.find(user); + return (it != channel->mgInfo->lastRestricted.cend()) + ? it->second.rights + : MTP_chatBannedRights(MTP_flags(0), MTP_int(0)); + }(); + + channel->session().api().kickParticipant( + channel, + user, + currentRestrictedRights); + } +} + +void GroupPanel::initLayout() { + initGeometry(); + +#ifdef Q_OS_WIN + _controls->raise(); +#endif // Q_OS_WIN +} + +void GroupPanel::showControls() { + Expects(_call != nullptr); + + widget()->showChildren(); +} + +void GroupPanel::closeBeforeDestroy() { + _window->close(); + initWithCall(nullptr); +} + +void GroupPanel::initGeometry() { + const auto center = Core::App().getPointForCallPanelCenter(); + const auto rect = QRect(0, 0, st::groupCallWidth, st::groupCallHeight); + _window->setGeometry(rect.translated(center - rect.center())); + _window->setMinimumSize(rect.size()); + _window->show(); + updateControlsGeometry(); +} + +int GroupPanel::computeMembersListTop() const { + if (computeTitleRect().has_value()) { + return st::groupCallMembersTop; + } + return st::groupCallMembersTop + - (st::groupCallSubtitleTop - st::groupCallTitleTop); +} + +std::optional GroupPanel::computeTitleRect() const { +#ifdef Q_OS_WIN + const auto controls = _controls->geometry(); + return QRect(0, 0, controls.x(), controls.height()); +#elif defined Q_OS_MAC // Q_OS_WIN + return QRect(70, 0, widget()->width() - 70, 28); +#else // Q_OS_WIN || Q_OS_MAC + return std::nullopt; +#endif // Q_OS_WIN || Q_OS_MAC +} + +void GroupPanel::updateControlsGeometry() { + if (widget()->size().isEmpty()) { + return; + } + const auto desiredHeight = _members->desiredHeight(); + const auto membersWidthAvailable = widget()->width() + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right(); + const auto membersWidthMin = st::groupCallWidth + - st::groupCallMembersMargin.left() + - st::groupCallMembersMargin.right(); + const auto membersWidth = std::clamp( + membersWidthAvailable, + membersWidthMin, + st::groupCallMembersWidthMax); + const auto muteTop = widget()->height() - st::groupCallMuteBottomSkip; + const auto buttonsTop = widget()->height() - st::groupCallButtonBottomSkip; + const auto membersTop = computeMembersListTop(); + const auto availableHeight = muteTop + - membersTop + - st::groupCallMembersMargin.bottom(); + _members->setGeometry( + (widget()->width() - membersWidth) / 2, + membersTop, + membersWidth, + std::min(desiredHeight, availableHeight)); + const auto muteSize = _mute->innerSize().width(); + const auto fullWidth = muteSize + + 2 * _settings->width() + + 2 * st::groupCallButtonSkip; + _mute->moveInner({ (widget()->width() - muteSize) / 2, muteTop }); + _settings->moveToLeft((widget()->width() - fullWidth) / 2, buttonsTop); + _hangup->moveToRight((widget()->width() - fullWidth) / 2, buttonsTop); + refreshTitle(); +} + +void GroupPanel::refreshTitle() { + if (const auto titleRect = computeTitleRect()) { + if (!_title) { + _title.create( + widget(), + Info::Profile::NameValue(_peer), + st::groupCallTitleLabel); + _title->show(); + _title->setAttribute(Qt::WA_TransparentForMouseEvents); + } + const auto best = _title->naturalWidth(); + const auto from = (widget()->width() - best) / 2; + const auto top = st::groupCallTitleTop; + const auto left = titleRect->x(); + if (from >= left && from + best <= left + titleRect->width()) { + _title->resizeToWidth(best); + _title->moveToLeft(from, top); + } else if (titleRect->width() < best) { + _title->resizeToWidth(titleRect->width()); + _title->moveToLeft(left, top); + } else if (from < left) { + _title->resizeToWidth(best); + _title->moveToLeft(left, top); + } else { + _title->resizeToWidth(best); + _title->moveToLeft(left + titleRect->width() - best, top); + } + } else if (_title) { + _title.destroy(); + } + if (!_subtitle) { + _subtitle.create( + widget(), + tr::lng_group_call_members( + lt_count_decimal, + _members->fullCountValue() | tr::to_count()), + st::groupCallSubtitleLabel); + _subtitle->show(); + _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + } + const auto middle = _title + ? (_title->x() + _title->width() / 2) + : (widget()->width() / 2); + const auto top = _title + ? st::groupCallSubtitleTop + : st::groupCallTitleTop; + _subtitle->moveToLeft( + (widget()->width() - _subtitle->width()) / 2, + top); +} + +void GroupPanel::paint(QRect clip) { + Painter p(widget()); + + auto region = QRegion(clip); + for (const auto rect : region) { + p.fillRect(rect, st::groupCallBg); + } +} + +bool GroupPanel::handleClose() { + if (_call) { + _window->hide(); + return true; + } + return false; +} + +not_null GroupPanel::widget() const { + return _window->body(); +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h new file mode 100644 index 000000000..32cd06caa --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_panel.h @@ -0,0 +1,131 @@ +/* +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 "base/weak_ptr.h" +#include "base/timer.h" +#include "base/object_ptr.h" +#include "calls/calls_group_call.h" +#include "ui/effects/animations.h" +#include "ui/rp_widget.h" + +class Image; + +namespace Data { +class PhotoMedia; +class CloudImageView; +} // namespace Data + +namespace Ui { +class CallButton; +class CallMuteButton; +class IconButton; +class FlatLabel; +template +class FadeWrap; +template +class PaddingWrap; +class Window; +class ScrollArea; +class GenericBox; +class LayerManager; +namespace Platform { +class TitleControls; +} // namespace Platform +} // namespace Ui + +namespace style { +struct CallSignalBars; +struct CallBodyLayout; +} // namespace style + +namespace Calls { + +class Userpic; +class SignalBars; + +class GroupMembers; + +enum class BoxContext { + GroupCallPanel, + MainWindow, +}; + +void LeaveGroupCallBox( + not_null box, + not_null call, + bool discardChecked, + BoxContext context); + +class GroupPanel final { +public: + GroupPanel(not_null call); + ~GroupPanel(); + + [[nodiscard]] bool isActive() const; + void minimize(); + void close(); + void showAndActivate(); + void closeBeforeDestroy(); + +private: + using State = GroupCall::State; + + [[nodiscard]] not_null widget() const; + + void paint(QRect clip); + + void initWindow(); + void initWidget(); + void initControls(); + void initWithCall(GroupCall *call); + void initLayout(); + void initGeometry(); + + bool handleClose(); + + void updateControlsGeometry(); + void showControls(); + + void endCall(); + + void addMembers(); + void kickMember(not_null user); + void kickMemberSure(not_null user); + [[nodiscard]] int computeMembersListTop() const; + [[nodiscard]] std::optional computeTitleRect() const; + void refreshTitle(); + + void migrate(not_null channel); + void subscribeToPeerChanges(); + + GroupCall *_call = nullptr; + not_null _peer; + + const std::unique_ptr _window; + const std::unique_ptr _layerBg; + +#ifdef Q_OS_WIN + std::unique_ptr _controls; +#endif // Q_OS_WIN + + rpl::lifetime _callLifetime; + + object_ptr _title = { nullptr }; + object_ptr _subtitle = { nullptr }; + object_ptr _members; + + object_ptr _settings; + std::unique_ptr _mute; + object_ptr _hangup; + + rpl::lifetime _peerLifetime; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_group_settings.cpp b/Telegram/SourceFiles/calls/calls_group_settings.cpp new file mode 100644 index 000000000..0a9414506 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_group_settings.cpp @@ -0,0 +1,502 @@ +/* +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 "calls/calls_group_settings.h" + +#include "calls/calls_group_call.h" +#include "calls/calls_group_panel.h" // LeaveGroupCallBox. +#include "calls/calls_instance.h" +#include "ui/widgets/level_meter.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "lang/lang_keys.h" +#include "base/timer_rpl.h" +#include "base/event_filter.h" +#include "base/global_shortcuts.h" +#include "base/platform/base_platform_info.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_group_call.h" +#include "core/application.h" +#include "boxes/single_choice_box.h" +#include "webrtc/webrtc_audio_input_tester.h" +#include "webrtc/webrtc_media_devices.h" +#include "settings/settings_common.h" +#include "settings/settings_calls.h" +#include "main/main_session.h" +#include "apiwrap.h" +#include "styles/style_layers.h" +#include "styles/style_calls.h" +#include "styles/style_settings.h" + +#include + +namespace Calls { +namespace { + +constexpr auto kDelaysCount = 201; +constexpr auto kCheckAccessibilityInterval = crl::time(500); + +void SaveCallJoinMuted( + not_null peer, + uint64 callId, + bool joinMuted) { + const auto call = peer->groupCall(); + if (!call + || call->id() != callId + || !peer->canManageGroupCall() + || !call->canChangeJoinMuted() + || call->joinMuted() == joinMuted) { + return; + } + call->setJoinMutedLocally(joinMuted); + peer->session().api().request(MTPphone_ToggleGroupCallSettings( + MTP_flags(MTPphone_ToggleGroupCallSettings::Flag::f_join_muted), + call->input(), + MTP_bool(joinMuted) + )).send(); +} + +[[nodiscard]] crl::time DelayByIndex(int index) { + return index * crl::time(10); +} + +[[nodiscard]] QString FormatDelay(crl::time delay) { + return (delay < crl::time(1000)) + ? tr::lng_group_call_ptt_delay_ms( + tr::now, + lt_amount, + QString::number(delay)) + : tr::lng_group_call_ptt_delay_s( + tr::now, + lt_amount, + QString::number(delay / 1000., 'f', 2)); +} + +} // namespace + +void GroupCallSettingsBox( + not_null box, + not_null call) { + using namespace Settings; + + const auto weakCall = base::make_weak(call.get()); + const auto weakBox = Ui::MakeWeak(box); + + struct State { + rpl::event_stream outputNameStream; + rpl::event_stream inputNameStream; + std::unique_ptr micTester; + Ui::LevelMeter *micTestLevel = nullptr; + float micLevel = 0.; + Ui::Animations::Simple micLevelAnimation; + base::Timer levelUpdateTimer; + bool generatingLink = false; + }; + const auto state = box->lifetime().make_state(); + + const auto peer = call->peer(); + const auto real = peer->groupCall(); + const auto id = call->id(); + const auto goodReal = (real && real->id() == id); + + const auto layout = box->verticalLayout(); + const auto &settings = Core::App().settings(); + + const auto joinMuted = goodReal ? real->joinMuted() : false; + const auto canChangeJoinMuted = (goodReal && real->canChangeJoinMuted()); + const auto addCheck = (peer->canManageGroupCall() && canChangeJoinMuted); + if (addCheck) { + AddSkip(layout); + } + const auto muteJoined = addCheck + ? AddButton( + layout, + tr::lng_group_call_new_muted(), + st::groupCallSettingsButton)->toggleOn(rpl::single(joinMuted)) + : nullptr; + if (addCheck) { + AddSkip(layout); + } + + AddButtonWithLabel( + layout, + tr::lng_group_call_speakers(), + rpl::single( + CurrentAudioOutputName() + ) | rpl::then( + state->outputNameStream.events() + ), + st::groupCallSettingsButton + )->addClickHandler([=] { + box->getDelegate()->show(ChooseAudioOutputBox(crl::guard(box, [=]( + const QString &id, + const QString &name) { + state->outputNameStream.fire_copy(name); + }), &st::groupCallCheckbox, &st::groupCallRadio)); + }); + + AddButtonWithLabel( + layout, + tr::lng_group_call_microphone(), + rpl::single( + CurrentAudioInputName() + ) | rpl::then( + state->inputNameStream.events() + ), + st::groupCallSettingsButton + )->addClickHandler([=] { + box->getDelegate()->show(ChooseAudioInputBox(crl::guard(box, [=]( + const QString &id, + const QString &name) { + state->inputNameStream.fire_copy(name); + if (state->micTester) { + state->micTester->setDeviceId(id); + } + }), &st::groupCallCheckbox, &st::groupCallRadio)); + }); + + state->micTestLevel = box->addRow( + object_ptr( + box.get(), + st::groupCallLevelMeter), + st::settingsLevelMeterPadding); + state->micTestLevel->resize(QSize(0, st::defaultLevelMeter.height)); + + state->levelUpdateTimer.setCallback([=] { + const auto was = state->micLevel; + state->micLevel = state->micTester->getAndResetLevel(); + state->micLevelAnimation.start([=] { + state->micTestLevel->setValue( + state->micLevelAnimation.value(state->micLevel)); + }, was, state->micLevel, kMicTestAnimationDuration); + }); + + AddSkip(layout); + //AddDivider(layout); + //AddSkip(layout); + + using GlobalShortcut = base::GlobalShortcut; + struct PushToTalkState { + rpl::variable recordText = tr::lng_group_call_ptt_shortcut(); + rpl::variable shortcutText; + rpl::event_stream pushToTalkToggles; + std::shared_ptr manager; + GlobalShortcut shortcut; + crl::time delay = 0; + bool recording = false; + }; + if (base::GlobalShortcutsAvailable()) { + const auto state = box->lifetime().make_state(); + if (!base::GlobalShortcutsAllowed()) { + Core::App().settings().setGroupCallPushToTalk(false); + } + const auto tryFillFromManager = [=] { + state->shortcut = state->manager + ? state->manager->shortcutFromSerialized( + Core::App().settings().groupCallPushToTalkShortcut()) + : nullptr; + state->shortcutText = state->shortcut + ? state->shortcut->toDisplayString() + : QString(); + }; + state->manager = settings.groupCallPushToTalk() + ? call->ensureGlobalShortcutManager() + : nullptr; + tryFillFromManager(); + + state->delay = settings.groupCallPushToTalkDelay(); + const auto pushToTalk = AddButton( + layout, + tr::lng_group_call_push_to_talk(), + st::groupCallSettingsButton + )->toggleOn(rpl::single( + settings.groupCallPushToTalk() + ) | rpl::then(state->pushToTalkToggles.events())); + const auto pushToTalkWrap = layout->add( + object_ptr>( + layout, + object_ptr(layout))); + const auto pushToTalkInner = pushToTalkWrap->entity(); + const auto recording = pushToTalkInner->add( + object_ptr