From 1d2df636521227bb19193cd323631a18ba9378b3 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 20 May 2025 12:33:03 +0000 Subject: [PATCH 001/310] Use Python 3.12 in Docker --- Telegram/build/docker/centos_env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 9c57f1cd1c..aa6878496e 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -19,7 +19,7 @@ ENV PKG_CONFIG_PATH /opt/rh/{{ TOOLSET }}/root/usr/lib64/pkgconfig:/opt/rh/{{ TO RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ - python3.11-pip python3.11-devel gperf flex bison clang clang-tools-extra \ + python3.12-pip python3.12-devel gperf flex bison clang clang-tools-extra \ lld nasm yasm file which perl-open perl-XML-Parser perl-IPC-Cmd \ xorg-x11-util-macros {{ TOOLSET }}-gcc {{ TOOLSET }}-gcc-c++ \ {{ TOOLSET }}-binutils {{ TOOLSET }}-gdb {{ TOOLSET }}-libasan-devel \ From 426cc2798e5c32f21caf368d1f3c33010c08f89b Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 20 May 2025 18:27:10 +0400 Subject: [PATCH 002/310] Revert "Use Python 3.12 in Docker" This reverts commit 1d2df636521227bb19193cd323631a18ba9378b3. --- Telegram/build/docker/centos_env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index aa6878496e..9c57f1cd1c 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -19,7 +19,7 @@ ENV PKG_CONFIG_PATH /opt/rh/{{ TOOLSET }}/root/usr/lib64/pkgconfig:/opt/rh/{{ TO RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ - python3.12-pip python3.12-devel gperf flex bison clang clang-tools-extra \ + python3.11-pip python3.11-devel gperf flex bison clang clang-tools-extra \ lld nasm yasm file which perl-open perl-XML-Parser perl-IPC-Cmd \ xorg-x11-util-macros {{ TOOLSET }}-gcc {{ TOOLSET }}-gcc-c++ \ {{ TOOLSET }}-binutils {{ TOOLSET }}-gdb {{ TOOLSET }}-libasan-devel \ From 88ce676c46bb9806765bd0f4a8c700a60802d1b8 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 20 May 2025 18:28:09 +0400 Subject: [PATCH 003/310] Use Python 3.11 explicitly in Docker --- Telegram/build/docker/centos_env/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 9c57f1cd1c..2afc6b1e62 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -29,6 +29,7 @@ RUN dnf -y install epel-release \ glib2-devel at-spi2-core-devel gtk3-devel boost1.78-devel fmt-devel \ && dnf clean all +RUN alternatives --set python3 /usr/bin/python3.11 RUN python3 -m pip install meson ninja RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc RUN echo set debuginfod enabled on > /opt/rh/{{ TOOLSET }}/root/etc/gdbinit.d/00-debuginfod.gdb From ebe45f73a0eec683094bb20c5b5437579249e6e4 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 May 2025 21:55:27 +0000 Subject: [PATCH 004/310] Shorten GIT_UPDATE_M4 --- Telegram/build/docker/centos_env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 2afc6b1e62..73b88c637a 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,6 +1,6 @@ {%- set GIT = "https://github.com" -%} {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} -{%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git config -f .gitmodules submodule.m4.shallow true && git submodule init && git submodule update" -%} +{%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git submodule update --init --recursive --depth=1" -%} {%- set TOOLSET = "gcc-toolset-12" -%} {%- set QT = "6.9.0" -%} {%- set QT_TAG = "v" ~ QT -%} From ff8292b863e8ede7f8f3eeb3e316785b176d110f Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 24 May 2025 07:41:50 +0000 Subject: [PATCH 005/310] Set build directory for libde265 in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 73b88c637a..452d64c068 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -156,13 +156,13 @@ RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \ FROM builder AS libde265 RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \ && cd libde265 \ - && cmake -GNinja . \ + && cmake -GNinja -B build . \ -DCMAKE_BUILD_TYPE=None \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_DECODER=OFF \ -DENABLE_SDL=OFF \ - && cmake --build . --parallel \ - && DESTDIR="{{ LibrariesPath }}/libde265-cache" cmake --install . \ + && cmake --build build --parallel \ + && DESTDIR="{{ LibrariesPath }}/libde265-cache" cmake --install build \ && cd .. \ && rm -rf libde265 From ae451894368e5aba8e2fb999d0805da6900831be Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 May 2025 21:31:40 +0000 Subject: [PATCH 006/310] Set cmake generator, build type and parallel level globally in Dockerfile --- .github/workflows/linux.yml | 2 +- Telegram/build/docker/centos_env/Dockerfile | 87 +++++++++------------ Telegram/build/docker/centos_env/build.sh | 2 +- docs/building-linux.md | 2 +- 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 228ef623c6..d297754a1e 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -104,7 +104,7 @@ jobs: docker run --rm \ -u $(id -u) \ -v $PWD:/usr/src/tdesktop \ - -e CONFIG=Debug \ + -e CMAKE_CONFIG_TYPE=Debug \ $IMAGE_TAG \ /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \ -D CMAKE_CONFIGURATION_TYPES=Debug \ diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 452d64c068..6bc8a80699 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -42,6 +42,11 @@ ENV NM gcc-nm ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS ENV CXXFLAGS $CFLAGS +ENV CMAKE_GENERATOR Ninja +ENV CMAKE_BUILD_TYPE None +ENV CMAKE_CONFIG_TYPE Release +ENV CMAKE_BUILD_PARALLEL_LEVEL '' + FROM builder AS patches RUN git init patches \ && cd patches \ @@ -53,10 +58,8 @@ RUN git init patches \ FROM builder AS zlib RUN git clone -b v1.3.1 --depth=1 {{ GIT }}/madler/zlib.git \ && cd zlib \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ - -DZLIB_BUILD_EXAMPLES=OFF \ - && cmake --build build --parallel \ + && cmake -B build . -DZLIB_BUILD_EXAMPLES=OFF \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/zlib-cache" cmake --install build \ && cd .. \ && rm -rf zlib @@ -64,8 +67,8 @@ RUN git clone -b v1.3.1 --depth=1 {{ GIT }}/madler/zlib.git \ FROM builder AS xz RUN git clone -b v5.8.1 --depth=1 {{ GIT }}/tukaani-project/xz.git \ && cd xz \ - && cmake -GNinja -B build . -DCMAKE_BUILD_TYPE=None \ - && cmake --build build --parallel \ + && cmake -B build . \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/xz-cache" cmake --install build \ && cd .. \ && rm -rf xz @@ -73,13 +76,12 @@ RUN git clone -b v5.8.1 --depth=1 {{ GIT }}/tukaani-project/xz.git \ FROM builder AS protobuf RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules {{ GIT }}/protocolbuffers/protobuf.git \ && cd protobuf \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -Dprotobuf_BUILD_TESTS=OFF \ -Dprotobuf_BUILD_PROTOBUF_BINARIES=ON \ -Dprotobuf_BUILD_LIBPROTOC=ON \ -Dprotobuf_WITH_ZLIB=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/protobuf-cache" cmake --install build \ && cd .. \ && rm -rf protobuf @@ -98,11 +100,10 @@ RUN git clone -b lcms2.15 --depth=1 {{ GIT }}/mm2/Little-CMS.git \ FROM builder AS brotli RUN git clone -b v1.1.0 --depth=1 {{ GIT }}/google/brotli.git \ && cd brotli \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DBROTLI_DISABLE_TESTS=ON \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/brotli-cache" cmake --install build \ && cd .. \ && rm -rf brotli @@ -110,12 +111,11 @@ RUN git clone -b v1.1.0 --depth=1 {{ GIT }}/google/brotli.git \ FROM builder AS highway RUN git clone -b 1.0.7 --depth=1 {{ GIT }}/google/highway.git \ && cd highway \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_TESTING=OFF \ -DHWY_ENABLE_CONTRIB=OFF \ -DHWY_ENABLE_EXAMPLES=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/highway-cache" cmake --install build \ && cd .. \ && rm -rf highway @@ -123,8 +123,8 @@ RUN git clone -b 1.0.7 --depth=1 {{ GIT }}/google/highway.git \ FROM builder AS opus RUN git clone -b v1.5.2 --depth=1 {{ GIT }}/xiph/opus.git \ && cd opus \ - && cmake -GNinja -B build . -DCMAKE_BUILD_TYPE=None \ - && cmake --build build --parallel \ + && cmake -B build . \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/opus-cache" cmake --install build \ && cd .. \ && rm -rf opus @@ -156,12 +156,12 @@ RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \ FROM builder AS libde265 RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \ && cd libde265 \ - && cmake -GNinja -B build . \ + && cmake -B build . \ -DCMAKE_BUILD_TYPE=None \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_DECODER=OFF \ -DENABLE_SDL=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/libde265-cache" cmake --install build \ && cd .. \ && rm -rf libde265 @@ -189,8 +189,7 @@ RUN git init libvpx \ FROM builder AS libwebp RUN git clone -b chrome-m116-5845 --depth=1 {{ GIT }}/webmproject/libwebp.git \ && cd libwebp \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DWEBP_BUILD_ANIM_UTILS=OFF \ -DWEBP_BUILD_CWEBP=OFF \ -DWEBP_BUILD_DWEBP=OFF \ @@ -200,7 +199,7 @@ RUN git clone -b chrome-m116-5845 --depth=1 {{ GIT }}/webmproject/libwebp.git \ -DWEBP_BUILD_WEBPMUX=OFF \ -DWEBP_BUILD_WEBPINFO=OFF \ -DWEBP_BUILD_EXTRAS=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/libwebp-cache" cmake --install build \ && cd .. \ && rm -rf libwebp @@ -210,11 +209,10 @@ COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / RUN git clone -b v1.0.4 --depth=1 {{ GIT }}/AOMediaCodec/libavif.git \ && cd libavif \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DAVIF_CODEC_DAV1D=ON \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/libavif-cache" cmake --install build \ && cd .. \ && rm -rf libavif @@ -224,8 +222,7 @@ COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / RUN git clone -b v1.18.2 --depth=1 {{ GIT }}/strukturag/libheif.git \ && cd libheif \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ -DENABLE_PLUGIN_LOADING=OFF \ @@ -238,7 +235,7 @@ RUN git clone -b v1.18.2 --depth=1 {{ GIT }}/strukturag/libheif.git \ -DWITH_SvtEnc_PLUGIN=OFF \ -DWITH_DAV1D=OFF \ -DWITH_EXAMPLES=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/libheif-cache" cmake --install build \ && cd .. \ && rm -rf libheif @@ -251,8 +248,7 @@ COPY --link --from=highway {{ LibrariesPath }}/highway-cache / RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ && cd libjxl \ && git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ -DJPEGXL_ENABLE_DEVTOOLS=OFF \ @@ -266,7 +262,7 @@ RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ -DJPEGXL_ENABLE_SJPEG=OFF \ -DJPEGXL_ENABLE_OPENEXR=OFF \ -DJPEGXL_ENABLE_SKCMS=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && export DESTDIR="{{ LibrariesPath }}/libjxl-cache" \ && cmake --install build \ && cp build/lib/libjpegli-static.a $DESTDIR/usr/local/lib64/libjpeg.a \ @@ -277,8 +273,8 @@ RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ FROM builder AS rnnoise RUN git clone -b master --depth=1 {{ GIT }}/desktop-app/rnnoise.git \ && cd rnnoise \ - && cmake -GNinja -B build . -DCMAKE_BUILD_TYPE=None \ - && cmake --build build --parallel \ + && cmake -B build . \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/rnnoise-cache" cmake --install build \ && cd .. \ && rm -rf rnnoise @@ -636,13 +632,12 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / RUN git clone -b 1.24.1 --depth=1 {{ GIT }}/kcat/openal-soft.git \ && cd openal-soft \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DLIBTYPE:STRING=STATIC \ -DALSOFT_EXAMPLES=OFF \ -DALSOFT_UTILS=OFF \ -DALSOFT_INSTALL_CONFIG=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/openal-cache" cmake --install build \ && cd .. \ && rm -rf openal-soft @@ -726,8 +721,7 @@ RUN git clone -b {{ QT_TAG }} --depth=1 {{ GIT }}/qt/qt5.git \ && cd ../qtwayland \ && find ../../patches/qtwayland_{{ QT }} -type f -print0 | sort -z | xargs -r0 git apply \ && cd .. \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DQT_GENERATE_SBOM=OFF \ -DINPUT_libpng=qt \ @@ -737,7 +731,7 @@ RUN git clone -b {{ QT_TAG }} --depth=1 {{ GIT }}/qt/qt5.git \ -DFEATURE_xcb_sm=OFF \ -DINPUT_dbus=runtime \ -DINPUT_openssl=linked \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/qt-cache" cmake --install build \ && cd .. \ && rm -rf qt5 @@ -786,25 +780,24 @@ RUN git init tg_owt \ WORKDIR tg_owt FROM webrtc AS webrtc_release -RUN cmake --build out --config Release --parallel \ +RUN cmake --build out --config Release \ && find out -mindepth 1 -maxdepth 1 ! -name Release -exec rm -rf {} \; {%- if DEBUG %} FROM webrtc AS webrtc_debug -RUN cmake --build out --config Debug --parallel \ +RUN cmake --build out --config Debug \ && find out -mindepth 1 -maxdepth 1 ! -name Debug -exec rm -rf {} \; {%- endif %} FROM builder AS ada RUN git clone -b v3.2.2 --depth=1 {{ GIT }}/ada-url/ada.git \ && cd ada \ - && cmake -GNinja -B build . \ - -D CMAKE_BUILD_TYPE=None \ + && cmake -B build . \ -D ADA_TESTING=OFF \ -D ADA_TOOLS=OFF \ -D ADA_INCLUDE_URL_PATTERN=OFF \ - && cmake --build build --parallel \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/ada-cache" cmake --install build \ && cd .. \ && rm -rf ada @@ -819,10 +812,8 @@ RUN git init tde2e \ && git remote add origin {{ GIT }}/tdlib/td.git \ && git fetch --depth=1 origin 51743dfd01dff6179e2d8f7095729caa4e2222e9 \ && git reset --hard FETCH_HEAD \ - && cmake -GNinja -B build . \ - -DCMAKE_BUILD_TYPE=NONE \ - -DTD_E2E_ONLY=ON \ - && cmake --build build --parallel \ + && cmake -B build . -DTD_E2E_ONLY=ON \ + && cmake --build build \ && DESTDIR="{{ LibrariesPath }}/tde2e-cache" cmake --install build \ && cd .. \ && rm -rf tde2e diff --git a/Telegram/build/docker/centos_env/build.sh b/Telegram/build/docker/centos_env/build.sh index e7a34e6aef..6368e631b9 100755 --- a/Telegram/build/docker/centos_env/build.sh +++ b/Telegram/build/docker/centos_env/build.sh @@ -3,4 +3,4 @@ set -e cd Telegram ./configure.sh "$@" -cmake --build ../out --config "${CONFIG:-Release}" --parallel +cmake --build ../out diff --git a/docs/building-linux.md b/docs/building-linux.md index fbdf253557..2f6d41e37a 100644 --- a/docs/building-linux.md +++ b/docs/building-linux.md @@ -32,7 +32,7 @@ Or, to create a debug build, run (also using [your **api_id** and **api_hash**]( docker run --rm -it \ -u $(id -u) \ -v "$PWD:/usr/src/tdesktop" \ - -e CONFIG=Debug \ + -e CMAKE_CONFIG_TYPE=Debug \ tdesktop:centos_env \ /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \ -D TDESKTOP_API_ID=YOUR_API_ID \ From f0cfbacb4fa407a3205ac845de228ddb0422a9b4 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 May 2025 21:40:30 +0000 Subject: [PATCH 007/310] Install Qt to global prefix in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 2 +- Telegram/build/qt_version.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 6bc8a80699..b64a617cf4 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -722,6 +722,7 @@ RUN git clone -b {{ QT_TAG }} --depth=1 {{ GIT }}/qt/qt5.git \ && find ../../patches/qtwayland_{{ QT }} -type f -print0 | sort -z | xargs -r0 git apply \ && cd .. \ && cmake -B build . \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ -DBUILD_SHARED_LIBS=OFF \ -DQT_GENERATE_SBOM=OFF \ -DINPUT_libpng=qt \ @@ -868,7 +869,6 @@ COPY --link --from=ada {{ LibrariesPath }}/ada-cache / COPY --link --from=tde2e {{ LibrariesPath }}/tde2e-cache / WORKDIR ../tdesktop -ENV QT {{ QT }} ENV BOOST_INCLUDEDIR /usr/include/boost1.78 ENV BOOST_LIBRARYDIR /usr/lib64/boost1.78 diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py index df7bb2a92c..6e5cc0f50f 100644 --- a/Telegram/build/qt_version.py +++ b/Telegram/build/qt_version.py @@ -5,10 +5,9 @@ def resolve(arch): os.environ['QT'] = '6.2.12' elif sys.platform == 'win32': if arch == 'arm' or 'qt6' in sys.argv: + print('Choosing Qt 6.') os.environ['QT'] = '6.9.0' - elif os.environ.get('QT') is None: + else: + print('Choosing Qt 5.') os.environ['QT'] = '5.15.15' - elif os.environ.get('QT') is None: - return False - print('Choosing Qt ' + os.environ.get('QT')) return True From 231a583bf72511f587ea856b86ac0e0d3634b1eb Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 May 2025 21:48:11 +0000 Subject: [PATCH 008/310] Build tg_owt in packaged mode in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 38 ++++----------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index b64a617cf4..c23f714d51 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -4,7 +4,6 @@ {%- set TOOLSET = "gcc-toolset-12" -%} {%- set QT = "6.9.0" -%} {%- set QT_TAG = "v" ~ QT -%} -{%- set CFLAGS_DEBUG = "$CFLAGS -O0 -fno-lto -U_FORTIFY_SOURCE" -%} {%- set LibrariesPath = "/usr/src/Libraries" -%} # syntax=docker/dockerfile:1 @@ -764,32 +763,11 @@ RUN git init tg_owt \ && git fetch --depth=1 origin c4192e8e2e10ccb72704daa79fa108becfa57b01 \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ - && rm -rf .git \ - && env -u CFLAGS -u CXXFLAGS cmake -G"Ninja Multi-Config" -B out . \ - -DCMAKE_C_FLAGS_RELEASE="$CFLAGS" \ - -DCMAKE_C_FLAGS_DEBUG="{{ CFLAGS_DEBUG }}" \ - -DCMAKE_CXX_FLAGS_RELEASE="$CXXFLAGS" \ - -DCMAKE_CXX_FLAGS_DEBUG="{{ CFLAGS_DEBUG }}" \ - -DTG_OWT_SPECIAL_TARGET=linux \ - -DTG_OWT_LIBJPEG_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_OPENSSL_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_OPUS_INCLUDE_PATH=/usr/local/include/opus \ - -DTG_OWT_LIBVPX_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_OPENH264_INCLUDE_PATH=/usr/local/include \ - -DTG_OWT_FFMPEG_INCLUDE_PATH=/usr/local/include - -WORKDIR tg_owt - -FROM webrtc AS webrtc_release -RUN cmake --build out --config Release \ - && find out -mindepth 1 -maxdepth 1 ! -name Release -exec rm -rf {} \; - -{%- if DEBUG %} - -FROM webrtc AS webrtc_debug -RUN cmake --build out --config Debug \ - && find out -mindepth 1 -maxdepth 1 ! -name Debug -exec rm -rf {} \; -{%- endif %} + && cmake -B build . -DTG_OWT_DLOPEN_PIPEWIRE=ON \ + && cmake --build build \ + && DESTDIR="{{ LibrariesPath }}/webrtc-cache" cmake --install build \ + && cd .. \ + && rm -rf tg_owt FROM builder AS ada RUN git clone -b v3.2.2 --depth=1 {{ GIT }}/ada-url/ada.git \ @@ -860,11 +838,7 @@ COPY --link --from=glib {{ LibrariesPath }}/glib-cache / COPY --link --from=gobject-introspection {{ LibrariesPath }}/gobject-introspection-cache / COPY --link --from=qt {{ LibrariesPath }}/qt-cache / COPY --link --from=breakpad {{ LibrariesPath }}/breakpad-cache / -COPY --link --from=webrtc {{ LibrariesPath }}/tg_owt tg_owt -COPY --link --from=webrtc_release {{ LibrariesPath }}/tg_owt/out/Release tg_owt/out/Release -{%- if DEBUG %} -COPY --link --from=webrtc_debug {{ LibrariesPath }}/tg_owt/out/Debug tg_owt/out/Debug -{%- endif %} +COPY --link --from=webrtc {{ LibrariesPath }}/webrtc-cache / COPY --link --from=ada {{ LibrariesPath }}/ada-cache / COPY --link --from=tde2e {{ LibrariesPath }}/tde2e-cache / From 579b358f8b83bca60ead84ed36f03d4eaa5375d3 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 May 2025 21:51:28 +0000 Subject: [PATCH 009/310] Use system gobject-introspection in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 32 ++++----------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index c23f714d51..1fb658d03a 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -25,7 +25,8 @@ RUN dnf -y install epel-release \ libffi-devel fontconfig-devel freetype-devel libX11-devel wayland-devel \ alsa-lib-devel pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel \ mesa-libgbm-devel libdrm-devel vulkan-devel libva-devel libvdpau-devel \ - glib2-devel at-spi2-core-devel gtk3-devel boost1.78-devel fmt-devel \ + glib2-devel gobject-introspection-devel at-spi2-core-devel gtk3-devel \ + boost1.78-devel fmt-devel \ && dnf clean all RUN alternatives --set python3 /usr/bin/python3.11 @@ -672,30 +673,6 @@ RUN git clone -b xkbcommon-1.6.0 --depth=1 {{ GIT }}/xkbcommon/libxkbcommon.git && cd .. \ && rm -rf libxkbcommon -FROM builder AS glib -RUN git clone -b 2.78.1 --depth=1 {{ GIT }}/GNOME/glib.git \ - && cd glib \ - && meson build \ - --buildtype=plain \ - --default-library=both \ - -Dtests=false \ - -Dmm-common:use-network=true \ - && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/glib-cache" meson install -C build \ - && cd .. \ - && rm -rf glib - -FROM builder AS gobject-introspection -COPY --link --from=glib {{ LibrariesPath }}/glib-cache / - -RUN git clone -b 1.78.1 --depth=1 {{ GIT }}/GNOME/gobject-introspection.git \ - && cd gobject-introspection \ - && meson build --buildtype=plain \ - && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/gobject-introspection-cache" meson install -C build \ - && cd .. \ - && rm -rf gobject-introspection - FROM patches AS qt COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / @@ -834,14 +811,15 @@ COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / COPY --link --from=openal {{ LibrariesPath }}/openal-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -COPY --link --from=glib {{ LibrariesPath }}/glib-cache / -COPY --link --from=gobject-introspection {{ LibrariesPath }}/gobject-introspection-cache / COPY --link --from=qt {{ LibrariesPath }}/qt-cache / COPY --link --from=breakpad {{ LibrariesPath }}/breakpad-cache / COPY --link --from=webrtc {{ LibrariesPath }}/webrtc-cache / COPY --link --from=ada {{ LibrariesPath }}/ada-cache / COPY --link --from=tde2e {{ LibrariesPath }}/tde2e-cache / +COPY --link --from=patches {{ LibrariesPath }}/patches patches +RUN patch -p1 -d /usr/lib64/gobject-introspection -i $PWD/patches/gobject-introspection.patch && rm -rf patches + WORKDIR ../tdesktop ENV BOOST_INCLUDEDIR /usr/include/boost1.78 ENV BOOST_LIBRARYDIR /usr/lib64/boost1.78 From 2f003d416a816557b86d510cc7281dd5c6fca3a7 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Thu, 22 May 2025 01:39:49 +0400 Subject: [PATCH 010/310] Update OpenAL to 1.24.3 on Linux --- Telegram/build/docker/centos_env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 1fb658d03a..64f2714d0f 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -630,7 +630,7 @@ RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ FROM builder AS openal COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / -RUN git clone -b 1.24.1 --depth=1 {{ GIT }}/kcat/openal-soft.git \ +RUN git clone -b 1.24.3 --depth=1 {{ GIT }}/kcat/openal-soft.git \ && cd openal-soft \ && cmake -B build . \ -DLIBTYPE:STRING=STATIC \ From 2535b6e08cd15bdbefab26d632c5ad3a64fa4fd1 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 May 2025 21:46:22 +0000 Subject: [PATCH 011/310] Update GCC to 14 on Linux --- .github/workflows/linux.yml | 4 ++-- Telegram/SourceFiles/_other/updater_linux.cpp | 1 + Telegram/build/docker/centos_env/Dockerfile | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index d297754a1e..5d7e0e60df 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -108,8 +108,8 @@ jobs: $IMAGE_TAG \ /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \ -D CMAKE_CONFIGURATION_TYPES=Debug \ - -D CMAKE_C_FLAGS_DEBUG="-O0 -U_FORTIFY_SOURCE" \ - -D CMAKE_CXX_FLAGS_DEBUG="-O0 -U_FORTIFY_SOURCE" \ + -D CMAKE_C_FLAGS_DEBUG="-O0" \ + -D CMAKE_CXX_FLAGS_DEBUG="-O0" \ -D CMAKE_EXE_LINKER_FLAGS="-s" \ -D CMAKE_COMPILE_WARNING_AS_ERROR=ON \ -D TDESKTOP_API_TEST=ON \ diff --git a/Telegram/SourceFiles/_other/updater_linux.cpp b/Telegram/SourceFiles/_other/updater_linux.cpp index 58bc10efad..67b74453e1 100644 --- a/Telegram/SourceFiles/_other/updater_linux.cpp +++ b/Telegram/SourceFiles/_other/updater_linux.cpp @@ -5,6 +5,7 @@ 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 */ +#define _GLIBCXX_USE_CXX11_ABI 0 #include #include #include diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 64f2714d0f..4bbfe1f809 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,7 +1,7 @@ {%- set GIT = "https://github.com" -%} {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} {%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git submodule update --init --recursive --depth=1" -%} -{%- set TOOLSET = "gcc-toolset-12" -%} +{%- set TOOLSET = "gcc-toolset-14" -%} {%- set QT = "6.9.0" -%} {%- set QT_TAG = "v" ~ QT -%} {%- set LibrariesPath = "/usr/src/Libraries" -%} @@ -26,7 +26,7 @@ RUN dnf -y install epel-release \ alsa-lib-devel pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel \ mesa-libgbm-devel libdrm-devel vulkan-devel libva-devel libvdpau-devel \ glib2-devel gobject-introspection-devel at-spi2-core-devel gtk3-devel \ - boost1.78-devel fmt-devel \ + boost1.78-devel \ && dnf clean all RUN alternatives --set python3 /usr/bin/python3.11 @@ -39,7 +39,7 @@ WORKDIR {{ LibrariesPath }} ENV AR gcc-ar ENV RANLIB gcc-ranlib ENV NM gcc-nm -ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fstack-protector-strong -fstack-clash-protection -fcf-protection -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS +ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened ENV CXXFLAGS $CFLAGS ENV CMAKE_GENERATOR Ninja From ab6375ef2a6e70d1ab463747b22956ce62546ab8 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sun, 25 May 2025 09:16:08 +0000 Subject: [PATCH 012/310] Update submodules --- Telegram/build/docker/centos_env/Dockerfile | 2 +- Telegram/lib_base | 2 +- cmake | 2 +- snap/snapcraft.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 4bbfe1f809..ffffe3517a 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -51,7 +51,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin {{ GIT }}/desktop-app/patches.git \ - && git fetch --depth=1 origin 7119a74e3f9b782f3cc29bf52fc78f2e8b0ca352 \ + && git fetch --depth=1 origin 22989737aea515bf6a94d74a65490d37409831bc \ && git reset --hard FETCH_HEAD \ && rm -rf .git diff --git a/Telegram/lib_base b/Telegram/lib_base index b4f913beb8..402034cba6 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit b4f913beb8fba75046b3b6b329658624bf7e934d +Subproject commit 402034cba675220647c5e2041f38cf9d977d496e diff --git a/cmake b/cmake index 50c3edca14..1e09ee81ee 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 50c3edca148cee2bbb1ce41a7c19c9d0b20c5c48 +Subproject commit 1e09ee81ee7cdfa848ee4902934a271f4b71265f diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 4fda801821..49478c7036 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -170,7 +170,7 @@ parts: patches: source: https://github.com/desktop-app/patches.git source-depth: 1 - source-commit: 7119a74e3f9b782f3cc29bf52fc78f2e8b0ca352 + source-commit: 22989737aea515bf6a94d74a65490d37409831bc plugin: dump override-pull: | craftctl default From bf4442ecf5f4a21636d39df3b88ca4e1c0d75ec8 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sun, 1 Jun 2025 02:41:51 +0400 Subject: [PATCH 013/310] CMAKE_CONFIG_TYPE doesn't seem to work This partially reverts commit ae451894368e5aba8e2fb999d0805da6900831be. --- .github/workflows/linux.yml | 2 +- Telegram/build/docker/centos_env/Dockerfile | 1 - Telegram/build/docker/centos_env/build.sh | 2 +- docs/building-linux.md | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 5d7e0e60df..0421fe6b31 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -104,7 +104,7 @@ jobs: docker run --rm \ -u $(id -u) \ -v $PWD:/usr/src/tdesktop \ - -e CMAKE_CONFIG_TYPE=Debug \ + -e CONFIG=Debug \ $IMAGE_TAG \ /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \ -D CMAKE_CONFIGURATION_TYPES=Debug \ diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index ffffe3517a..03c13b03fb 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -44,7 +44,6 @@ ENV CXXFLAGS $CFLAGS ENV CMAKE_GENERATOR Ninja ENV CMAKE_BUILD_TYPE None -ENV CMAKE_CONFIG_TYPE Release ENV CMAKE_BUILD_PARALLEL_LEVEL '' FROM builder AS patches diff --git a/Telegram/build/docker/centos_env/build.sh b/Telegram/build/docker/centos_env/build.sh index 6368e631b9..dfa475955f 100755 --- a/Telegram/build/docker/centos_env/build.sh +++ b/Telegram/build/docker/centos_env/build.sh @@ -3,4 +3,4 @@ set -e cd Telegram ./configure.sh "$@" -cmake --build ../out +cmake --build ../out --config "${CONFIG:-Release}" diff --git a/docs/building-linux.md b/docs/building-linux.md index 2f6d41e37a..fbdf253557 100644 --- a/docs/building-linux.md +++ b/docs/building-linux.md @@ -32,7 +32,7 @@ Or, to create a debug build, run (also using [your **api_id** and **api_hash**]( docker run --rm -it \ -u $(id -u) \ -v "$PWD:/usr/src/tdesktop" \ - -e CMAKE_CONFIG_TYPE=Debug \ + -e CONFIG=Debug \ tdesktop:centos_env \ /usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \ -D TDESKTOP_API_ID=YOUR_API_ID \ From e3e2a477c15c9e7dfbd5b2f018aac96114ada520 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 2 Jun 2025 04:37:48 +0000 Subject: [PATCH 014/310] Proper check for multi-config generator --- Telegram/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1cc9694c1b..2a86806efa 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1910,8 +1910,9 @@ PRIVATE G_LOG_DOMAIN="Telegram" ) +get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if (APPLE - OR "${CMAKE_GENERATOR}" STREQUAL "Ninja Multi-Config" + OR is_multi_config OR NOT CMAKE_EXECUTABLE_SUFFIX STREQUAL "" OR NOT "${output_name}" STREQUAL "Telegram") set(output_folder ${CMAKE_BINARY_DIR}) From 845fddf5f235e10d4510af4db00c0304326e8a67 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sat, 31 May 2025 21:17:05 +0400 Subject: [PATCH 015/310] Use enable_language --- CMakeLists.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 36e54053d7..85c326fec6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,19 +12,19 @@ include(cmake/validate_special_target.cmake) include(cmake/version.cmake) desktop_app_parse_version(Telegram/build/version) -set(project_langs C CXX) -if (APPLE) - list(APPEND project_langs OBJC OBJCXX) -elseif (LINUX) - list(APPEND project_langs ASM) -endif() - project(Telegram - LANGUAGES ${project_langs} + LANGUAGES C CXX VERSION ${desktop_app_version_cmake} DESCRIPTION "Official Telegram Desktop messenger" HOMEPAGE_URL "https://desktop.telegram.org" ) + +if (APPLE) + enable_language(OBJC OBJCXX) +elseif (LINUX) + enable_language(ASM) +endif() + set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Telegram) get_filename_component(third_party_loc "Telegram/ThirdParty" REALPATH) From 28b54fac37694acdd69435efdc42308067408a31 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:16:53 +0000 Subject: [PATCH 016/310] Revert GIT_UPDATE_M4 in Dockerfile This partially reverts commit 9461095c88c4f3e3188427ba4e6054eafdb29041. --- Telegram/build/docker/centos_env/Dockerfile | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 03c13b03fb..d3fe1f3718 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,6 +1,5 @@ {%- set GIT = "https://github.com" -%} {%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} -{%- set GIT_UPDATE_M4 = "git submodule set-url m4 https://gitlab.freedesktop.org/xorg/util/xcb-util-m4 && git submodule update --init --recursive --depth=1" -%} {%- set TOOLSET = "gcc-toolset-14" -%} {%- set QT = "6.9.0" -%} {%- set QT_TAG = "v" ~ QT -%} @@ -299,9 +298,8 @@ RUN git clone -b libxcb-1.16 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb.git \ && rm -rf libxcb FROM builder AS xcb-wm -RUN git clone -b xcb-util-wm-0.4.2 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-wm.git \ +RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-wm.git \ && cd libxcb-wm \ - && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-wm-cache" install \ @@ -309,9 +307,8 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-wm.git && rm -rf libxcb-wm FROM builder AS xcb-util -RUN git clone -b xcb-util-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-util.git \ +RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-util.git \ && cd libxcb-util \ - && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-util-cache" install \ @@ -321,9 +318,8 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-util.git FROM builder AS xcb-image COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -RUN git clone -b xcb-util-image-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-image.git \ +RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-image.git \ && cd libxcb-image \ - && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-image-cache" install \ @@ -331,9 +327,8 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-ima && rm -rf libxcb-image FROM builder AS xcb-keysyms -RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \ +RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \ && cd libxcb-keysyms \ - && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-keysyms-cache" install \ @@ -341,9 +336,8 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-k && rm -rf libxcb-keysyms FROM builder AS xcb-render-util -RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \ +RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \ && cd libxcb-render-util \ - && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-render-util-cache" install \ @@ -355,9 +349,8 @@ COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \ +RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \ && cd libxcb-cursor \ - && {{ GIT_UPDATE_M4 }} \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-cursor-cache" install \ From c1028e7408138ba1f9e831f65b6c800b0be73eb2 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:19:27 +0000 Subject: [PATCH 017/310] Remove GIT_FREEDESKTOP variable from Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 35 ++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index d3fe1f3718..4b4d255dbd 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,5 +1,4 @@ {%- set GIT = "https://github.com" -%} -{%- set GIT_FREEDESKTOP = GIT ~ "/gitlab-freedesktop-mirrors" -%} {%- set TOOLSET = "gcc-toolset-14" -%} {%- set QT = "6.9.0" -%} {%- set QT_TAG = "v" ~ QT -%} @@ -278,7 +277,7 @@ RUN git clone -b master --depth=1 {{ GIT }}/desktop-app/rnnoise.git \ && rm -rf rnnoise FROM builder AS xcb-proto -RUN git clone -b xcb-proto-1.16.0 --depth=1 {{ GIT_FREEDESKTOP }}/xcbproto.git \ +RUN git clone -b xcb-proto-1.16.0 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/xcbproto.git \ && cd xcbproto \ && ./autogen.sh \ && make -j$(nproc) \ @@ -289,7 +288,7 @@ RUN git clone -b xcb-proto-1.16.0 --depth=1 {{ GIT_FREEDESKTOP }}/xcbproto.git \ FROM builder AS xcb COPY --link --from=xcb-proto {{ LibrariesPath }}/xcb-proto-cache / -RUN git clone -b libxcb-1.16 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb.git \ +RUN git clone -b libxcb-1.16 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxcb.git \ && cd libxcb \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -298,7 +297,7 @@ RUN git clone -b libxcb-1.16 --depth=1 {{ GIT_FREEDESKTOP }}/libxcb.git \ && rm -rf libxcb FROM builder AS xcb-wm -RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-wm.git \ +RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-wm.git \ && cd libxcb-wm \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -307,7 +306,7 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ && rm -rf libxcb-wm FROM builder AS xcb-util -RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-util.git \ +RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-util.git \ && cd libxcb-util \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -318,7 +317,7 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GI FROM builder AS xcb-image COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-image.git \ +RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-image.git \ && cd libxcb-image \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -327,7 +326,7 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules && rm -rf libxcb-image FROM builder AS xcb-keysyms -RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-keysyms.git \ +RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ && cd libxcb-keysyms \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -336,7 +335,7 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul && rm -rf libxcb-keysyms FROM builder AS xcb-render-util -RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-render-util.git \ +RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-render-util.git \ && cd libxcb-render-util \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -349,7 +348,7 @@ COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules {{ GIT_FREEDESKTOP }}/libxcb-cursor.git \ +RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-cursor.git \ && cd libxcb-cursor \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -358,7 +357,7 @@ RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodule && rm -rf libxcb-cursor FROM builder AS libXext -RUN git clone -b libXext-1.3.5 --depth=1 {{ GIT_FREEDESKTOP }}/libxext.git \ +RUN git clone -b libXext-1.3.5 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxext.git \ && cd libxext \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -367,7 +366,7 @@ RUN git clone -b libXext-1.3.5 --depth=1 {{ GIT_FREEDESKTOP }}/libxext.git \ && rm -rf libxext FROM builder AS libXtst -RUN git clone -b libXtst-1.2.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxtst.git \ +RUN git clone -b libXtst-1.2.4 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxtst.git \ && cd libxtst \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -376,7 +375,7 @@ RUN git clone -b libXtst-1.2.4 --depth=1 {{ GIT_FREEDESKTOP }}/libxtst.git \ && rm -rf libxtst FROM builder AS libXfixes -RUN git clone -b libXfixes-5.0.3 --depth=1 {{ GIT_FREEDESKTOP }}/libxfixes.git \ +RUN git clone -b libXfixes-5.0.3 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxfixes.git \ && cd libxfixes \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -387,7 +386,7 @@ RUN git clone -b libXfixes-5.0.3 --depth=1 {{ GIT_FREEDESKTOP }}/libxfixes.git \ FROM builder AS libXv COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / -RUN git clone -b libXv-1.0.12 --depth=1 {{ GIT_FREEDESKTOP }}/libxv.git \ +RUN git clone -b libXv-1.0.12 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxv.git \ && cd libxv \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -396,7 +395,7 @@ RUN git clone -b libXv-1.0.12 --depth=1 {{ GIT_FREEDESKTOP }}/libxv.git \ && rm -rf libxv FROM builder AS libXrandr -RUN git clone -b libXrandr-1.5.3 --depth=1 {{ GIT_FREEDESKTOP }}/libxrandr.git \ +RUN git clone -b libXrandr-1.5.3 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxrandr.git \ && cd libxrandr \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -405,7 +404,7 @@ RUN git clone -b libXrandr-1.5.3 --depth=1 {{ GIT_FREEDESKTOP }}/libxrandr.git \ && rm -rf libxrandr FROM builder AS libXrender -RUN git clone -b libXrender-0.9.11 --depth=1 {{ GIT_FREEDESKTOP }}/libxrender.git \ +RUN git clone -b libXrender-0.9.11 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxrender.git \ && cd libxrender \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -414,7 +413,7 @@ RUN git clone -b libXrender-0.9.11 --depth=1 {{ GIT_FREEDESKTOP }}/libxrender.gi && rm -rf libxrender FROM builder AS libXdamage -RUN git clone -b libXdamage-1.1.6 --depth=1 {{ GIT_FREEDESKTOP }}/libxdamage.git \ +RUN git clone -b libXdamage-1.1.6 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxdamage.git \ && cd libxdamage \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -423,7 +422,7 @@ RUN git clone -b libXdamage-1.1.6 --depth=1 {{ GIT_FREEDESKTOP }}/libxdamage.git && rm -rf libxdamage FROM builder AS libXcomposite -RUN git clone -b libXcomposite-0.4.6 --depth=1 {{ GIT_FREEDESKTOP }}/libxcomposite.git \ +RUN git clone -b libXcomposite-0.4.6 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxcomposite.git \ && cd libxcomposite \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -432,7 +431,7 @@ RUN git clone -b libXcomposite-0.4.6 --depth=1 {{ GIT_FREEDESKTOP }}/libxcomposi && rm -rf libxcomposite FROM builder AS wayland -RUN git clone -b 1.19.0 --depth=1 {{ GIT_FREEDESKTOP }}/wayland.git \ +RUN git clone -b 1.19.0 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/wayland.git \ && cd wayland \ && sed -i "/subdir('tests')/d" meson.build \ && meson build \ From 2d000e826bb91e65b647a3cc5c8c68973901da68 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:19:58 +0000 Subject: [PATCH 018/310] Remove GIT variable from Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 89 ++++++++++----------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 4b4d255dbd..5936aa5240 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,4 +1,3 @@ -{%- set GIT = "https://github.com" -%} {%- set TOOLSET = "gcc-toolset-14" -%} {%- set QT = "6.9.0" -%} {%- set QT_TAG = "v" ~ QT -%} @@ -47,13 +46,13 @@ ENV CMAKE_BUILD_PARALLEL_LEVEL '' FROM builder AS patches RUN git init patches \ && cd patches \ - && git remote add origin {{ GIT }}/desktop-app/patches.git \ + && git remote add origin https://github.com/desktop-app/patches.git \ && git fetch --depth=1 origin 22989737aea515bf6a94d74a65490d37409831bc \ && git reset --hard FETCH_HEAD \ && rm -rf .git FROM builder AS zlib -RUN git clone -b v1.3.1 --depth=1 {{ GIT }}/madler/zlib.git \ +RUN git clone -b v1.3.1 --depth=1 https://github.com/madler/zlib.git \ && cd zlib \ && cmake -B build . -DZLIB_BUILD_EXAMPLES=OFF \ && cmake --build build \ @@ -62,7 +61,7 @@ RUN git clone -b v1.3.1 --depth=1 {{ GIT }}/madler/zlib.git \ && rm -rf zlib FROM builder AS xz -RUN git clone -b v5.8.1 --depth=1 {{ GIT }}/tukaani-project/xz.git \ +RUN git clone -b v5.8.1 --depth=1 https://github.com/tukaani-project/xz.git \ && cd xz \ && cmake -B build . \ && cmake --build build \ @@ -71,7 +70,7 @@ RUN git clone -b v5.8.1 --depth=1 {{ GIT }}/tukaani-project/xz.git \ && rm -rf xz FROM builder AS protobuf -RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules {{ GIT }}/protocolbuffers/protobuf.git \ +RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules https://github.com/protocolbuffers/protobuf.git \ && cd protobuf \ && cmake -B build . \ -Dprotobuf_BUILD_TESTS=OFF \ @@ -84,7 +83,7 @@ RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules {{ GIT }}/prot && rm -rf protobuf FROM builder AS lcms2 -RUN git clone -b lcms2.15 --depth=1 {{ GIT }}/mm2/Little-CMS.git \ +RUN git clone -b lcms2.15 --depth=1 https://github.com/mm2/Little-CMS.git \ && cd Little-CMS \ && meson build \ --buildtype=plain \ @@ -95,7 +94,7 @@ RUN git clone -b lcms2.15 --depth=1 {{ GIT }}/mm2/Little-CMS.git \ && rm -rf Little-CMS FROM builder AS brotli -RUN git clone -b v1.1.0 --depth=1 {{ GIT }}/google/brotli.git \ +RUN git clone -b v1.1.0 --depth=1 https://github.com/google/brotli.git \ && cd brotli \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ @@ -106,7 +105,7 @@ RUN git clone -b v1.1.0 --depth=1 {{ GIT }}/google/brotli.git \ && rm -rf brotli FROM builder AS highway -RUN git clone -b 1.0.7 --depth=1 {{ GIT }}/google/highway.git \ +RUN git clone -b 1.0.7 --depth=1 https://github.com/google/highway.git \ && cd highway \ && cmake -B build . \ -DBUILD_TESTING=OFF \ @@ -118,7 +117,7 @@ RUN git clone -b 1.0.7 --depth=1 {{ GIT }}/google/highway.git \ && rm -rf highway FROM builder AS opus -RUN git clone -b v1.5.2 --depth=1 {{ GIT }}/xiph/opus.git \ +RUN git clone -b v1.5.2 --depth=1 https://github.com/xiph/opus.git \ && cd opus \ && cmake -B build . \ && cmake --build build \ @@ -127,7 +126,7 @@ RUN git clone -b v1.5.2 --depth=1 {{ GIT }}/xiph/opus.git \ && rm -rf opus FROM builder AS dav1d -RUN git clone -b 1.4.1 --depth=1 {{ GIT }}/videolan/dav1d.git \ +RUN git clone -b 1.4.1 --depth=1 https://github.com/videolan/dav1d.git \ && cd dav1d \ && meson build \ --buildtype=plain \ @@ -140,7 +139,7 @@ RUN git clone -b 1.4.1 --depth=1 {{ GIT }}/videolan/dav1d.git \ && rm -rf dav1d FROM builder AS openh264 -RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \ +RUN git clone -b v2.4.1 --depth=1 https://github.com/cisco/openh264.git \ && cd openh264 \ && meson build \ --buildtype=plain \ @@ -151,7 +150,7 @@ RUN git clone -b v2.4.1 --depth=1 {{ GIT }}/cisco/openh264.git \ && rm -rf openh264 FROM builder AS libde265 -RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \ +RUN git clone -b v1.0.15 --depth=1 https://github.com/strukturag/libde265.git \ && cd libde265 \ && cmake -B build . \ -DCMAKE_BUILD_TYPE=None \ @@ -166,7 +165,7 @@ RUN git clone -b v1.0.15 --depth=1 {{ GIT }}/strukturag/libde265.git \ FROM builder AS libvpx RUN git init libvpx \ && cd libvpx \ - && git remote add origin {{ GIT }}/webmproject/libvpx.git \ + && git remote add origin https://github.com/webmproject/libvpx.git \ && git fetch --depth=1 origin 12f3a2ac603e8f10742105519e0cd03c3b8f71dd \ && git reset --hard FETCH_HEAD \ && CFLAGS="$CFLAGS -fno-lto" CXXFLAGS="$CXXFLAGS -fno-lto" ./configure \ @@ -184,7 +183,7 @@ RUN git init libvpx \ && rm -rf libvpx FROM builder AS libwebp -RUN git clone -b chrome-m116-5845 --depth=1 {{ GIT }}/webmproject/libwebp.git \ +RUN git clone -b chrome-m116-5845 --depth=1 https://github.com/webmproject/libwebp.git \ && cd libwebp \ && cmake -B build . \ -DWEBP_BUILD_ANIM_UTILS=OFF \ @@ -204,7 +203,7 @@ RUN git clone -b chrome-m116-5845 --depth=1 {{ GIT }}/webmproject/libwebp.git \ FROM builder AS libavif COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -RUN git clone -b v1.0.4 --depth=1 {{ GIT }}/AOMediaCodec/libavif.git \ +RUN git clone -b v1.0.4 --depth=1 https://github.com/AOMediaCodec/libavif.git \ && cd libavif \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ @@ -217,7 +216,7 @@ RUN git clone -b v1.0.4 --depth=1 {{ GIT }}/AOMediaCodec/libavif.git \ FROM builder AS libheif COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / -RUN git clone -b v1.18.2 --depth=1 {{ GIT }}/strukturag/libheif.git \ +RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ && cd libheif \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ @@ -242,7 +241,7 @@ COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / COPY --link --from=highway {{ LibrariesPath }}/highway-cache / -RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ +RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ && cd libjxl \ && git submodule update --init --recursive --depth=1 third_party/libjpeg-turbo \ && cmake -B build . \ @@ -268,7 +267,7 @@ RUN git clone -b v0.11.1 --depth=1 {{ GIT }}/libjxl/libjxl.git \ && rm -rf libjxl FROM builder AS rnnoise -RUN git clone -b master --depth=1 {{ GIT }}/desktop-app/rnnoise.git \ +RUN git clone -b master --depth=1 https://github.com/desktop-app/rnnoise.git \ && cd rnnoise \ && cmake -B build . \ && cmake --build build \ @@ -277,7 +276,7 @@ RUN git clone -b master --depth=1 {{ GIT }}/desktop-app/rnnoise.git \ && rm -rf rnnoise FROM builder AS xcb-proto -RUN git clone -b xcb-proto-1.16.0 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/xcbproto.git \ +RUN git clone -b xcb-proto-1.16.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/xcbproto.git \ && cd xcbproto \ && ./autogen.sh \ && make -j$(nproc) \ @@ -288,7 +287,7 @@ RUN git clone -b xcb-proto-1.16.0 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors FROM builder AS xcb COPY --link --from=xcb-proto {{ LibrariesPath }}/xcb-proto-cache / -RUN git clone -b libxcb-1.16 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxcb.git \ +RUN git clone -b libxcb-1.16 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcb.git \ && cd libxcb \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -297,7 +296,7 @@ RUN git clone -b libxcb-1.16 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libx && rm -rf libxcb FROM builder AS xcb-wm -RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-wm.git \ +RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-wm.git \ && cd libxcb-wm \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -306,7 +305,7 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules {{ && rm -rf libxcb-wm FROM builder AS xcb-util -RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-util.git \ +RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-util.git \ && cd libxcb-util \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -317,7 +316,7 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules {{ GI FROM builder AS xcb-image COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-image.git \ +RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \ && cd libxcb-image \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -326,7 +325,7 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules && rm -rf libxcb-image FROM builder AS xcb-keysyms -RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ +RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ && cd libxcb-keysyms \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -335,7 +334,7 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul && rm -rf libxcb-keysyms FROM builder AS xcb-render-util -RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-render-util.git \ +RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-render-util.git \ && cd libxcb-render-util \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -348,7 +347,7 @@ COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules {{ GIT }}/gitlab-freedesktop-mirrors/libxcb-cursor.git \ +RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \ && cd libxcb-cursor \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -357,7 +356,7 @@ RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodule && rm -rf libxcb-cursor FROM builder AS libXext -RUN git clone -b libXext-1.3.5 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxext.git \ +RUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxext.git \ && cd libxext \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -366,7 +365,7 @@ RUN git clone -b libXext-1.3.5 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/li && rm -rf libxext FROM builder AS libXtst -RUN git clone -b libXtst-1.2.4 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxtst.git \ +RUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxtst.git \ && cd libxtst \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -375,7 +374,7 @@ RUN git clone -b libXtst-1.2.4 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/li && rm -rf libxtst FROM builder AS libXfixes -RUN git clone -b libXfixes-5.0.3 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxfixes.git \ +RUN git clone -b libXfixes-5.0.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxfixes.git \ && cd libxfixes \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -386,7 +385,7 @@ RUN git clone -b libXfixes-5.0.3 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/ FROM builder AS libXv COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / -RUN git clone -b libXv-1.0.12 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxv.git \ +RUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxv.git \ && cd libxv \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -395,7 +394,7 @@ RUN git clone -b libXv-1.0.12 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/lib && rm -rf libxv FROM builder AS libXrandr -RUN git clone -b libXrandr-1.5.3 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxrandr.git \ +RUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrandr.git \ && cd libxrandr \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -404,7 +403,7 @@ RUN git clone -b libXrandr-1.5.3 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/ && rm -rf libxrandr FROM builder AS libXrender -RUN git clone -b libXrender-0.9.11 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxrender.git \ +RUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrender.git \ && cd libxrender \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -413,7 +412,7 @@ RUN git clone -b libXrender-0.9.11 --depth=1 {{ GIT }}/gitlab-freedesktop-mirror && rm -rf libxrender FROM builder AS libXdamage -RUN git clone -b libXdamage-1.1.6 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxdamage.git \ +RUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxdamage.git \ && cd libxdamage \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -422,7 +421,7 @@ RUN git clone -b libXdamage-1.1.6 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors && rm -rf libxdamage FROM builder AS libXcomposite -RUN git clone -b libXcomposite-0.4.6 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/libxcomposite.git \ +RUN git clone -b libXcomposite-0.4.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcomposite.git \ && cd libxcomposite \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ @@ -431,7 +430,7 @@ RUN git clone -b libXcomposite-0.4.6 --depth=1 {{ GIT }}/gitlab-freedesktop-mirr && rm -rf libxcomposite FROM builder AS wayland -RUN git clone -b 1.19.0 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/wayland.git \ +RUN git clone -b 1.19.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/wayland.git \ && cd wayland \ && sed -i "/subdir('tests')/d" meson.build \ && meson build \ @@ -448,7 +447,7 @@ RUN git clone -b 1.19.0 --depth=1 {{ GIT }}/gitlab-freedesktop-mirrors/wayland.g && rm -rf wayland FROM builder AS nv-codec-headers -RUN git clone -b n12.1.14.0 --depth=1 {{ GIT }}/FFmpeg/nv-codec-headers.git \ +RUN git clone -b n12.1.14.0 --depth=1 https://github.com/FFmpeg/nv-codec-headers.git \ && DESTDIR="{{ LibrariesPath }}/nv-codec-headers-cache" make -C nv-codec-headers install \ && rm -rf nv-codec-headers @@ -461,7 +460,7 @@ COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / COPY --link --from=libXv {{ LibrariesPath }}/libXv-cache / COPY --link --from=nv-codec-headers {{ LibrariesPath }}/nv-codec-headers-cache / -RUN git clone -b n6.1.1 --depth=1 {{ GIT }}/FFmpeg/FFmpeg.git \ +RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ && cd FFmpeg \ && ./configure \ --extra-cflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ @@ -605,7 +604,7 @@ RUN git clone -b n6.1.1 --depth=1 {{ GIT }}/FFmpeg/FFmpeg.git \ && rm -rf ffmpeg FROM builder AS pipewire -RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ +RUN git clone -b 0.3.62 --depth=1 https://github.com/PipeWire/pipewire.git \ && cd pipewire \ && meson build \ --buildtype=plain \ @@ -621,7 +620,7 @@ RUN git clone -b 0.3.62 --depth=1 {{ GIT }}/PipeWire/pipewire.git \ FROM builder AS openal COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / -RUN git clone -b 1.24.3 --depth=1 {{ GIT }}/kcat/openal-soft.git \ +RUN git clone -b 1.24.3 --depth=1 https://github.com/kcat/openal-soft.git \ && cd openal-soft \ && cmake -B build . \ -DLIBTYPE:STRING=STATIC \ @@ -634,7 +633,7 @@ RUN git clone -b 1.24.3 --depth=1 {{ GIT }}/kcat/openal-soft.git \ && rm -rf openal-soft FROM builder AS openssl -RUN git clone -b openssl-3.2.1 --depth=1 {{ GIT }}/openssl/openssl.git \ +RUN git clone -b openssl-3.2.1 --depth=1 https://github.com/openssl/openssl.git \ && cd openssl \ && ./config \ --openssldir=/etc/ssl \ @@ -648,7 +647,7 @@ RUN git clone -b openssl-3.2.1 --depth=1 {{ GIT }}/openssl/openssl.git \ FROM builder AS xkbcommon COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / -RUN git clone -b xkbcommon-1.6.0 --depth=1 {{ GIT }}/xkbcommon/libxkbcommon.git \ +RUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbcommon.git \ && cd libxkbcommon \ && meson build \ --buildtype=plain \ @@ -680,7 +679,7 @@ COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -RUN git clone -b {{ QT_TAG }} --depth=1 {{ GIT }}/qt/qt5.git \ +RUN git clone -b {{ QT_TAG }} --depth=1 https://github.com/qt/qt5.git \ && cd qt5 \ && git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \ && cd qtbase \ @@ -727,7 +726,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / # Shallow clone on a specific commit. RUN git init tg_owt \ && cd tg_owt \ - && git remote add origin {{ GIT }}/desktop-app/tg_owt.git \ + && git remote add origin https://github.com/desktop-app/tg_owt.git \ && git fetch --depth=1 origin c4192e8e2e10ccb72704daa79fa108becfa57b01 \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ @@ -738,7 +737,7 @@ RUN git init tg_owt \ && rm -rf tg_owt FROM builder AS ada -RUN git clone -b v3.2.2 --depth=1 {{ GIT }}/ada-url/ada.git \ +RUN git clone -b v3.2.2 --depth=1 https://github.com/ada-url/ada.git \ && cd ada \ && cmake -B build . \ -D ADA_TESTING=OFF \ @@ -756,7 +755,7 @@ COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / # Shallow clone on a specific commit. RUN git init tde2e \ && cd tde2e \ - && git remote add origin {{ GIT }}/tdlib/td.git \ + && git remote add origin https://github.com/tdlib/td.git \ && git fetch --depth=1 origin 51743dfd01dff6179e2d8f7095729caa4e2222e9 \ && git reset --hard FETCH_HEAD \ && cmake -B build . -DTD_E2E_ONLY=ON \ From dd6a4931e51ee7803fb7116c170c44755316e306 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:21:05 +0000 Subject: [PATCH 019/310] Make QT variable local to the Docker layer --- Telegram/build/docker/centos_env/Dockerfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 5936aa5240..e8c4db2154 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,6 +1,4 @@ {%- set TOOLSET = "gcc-toolset-14" -%} -{%- set QT = "6.9.0" -%} -{%- set QT_TAG = "v" ~ QT -%} {%- set LibrariesPath = "/usr/src/Libraries" -%} # syntax=docker/dockerfile:1 @@ -679,13 +677,14 @@ COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -RUN git clone -b {{ QT_TAG }} --depth=1 https://github.com/qt/qt5.git \ +ENV QT 6.9.0 +RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ && cd qt5 \ && git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \ && cd qtbase \ - && find ../../patches/qtbase_{{ QT }} -type f -print0 | sort -z | xargs -r0 git apply \ + && find ../../patches/qtbase_$QT -type f -print0 | sort -z | xargs -r0 git apply \ && cd ../qtwayland \ - && find ../../patches/qtwayland_{{ QT }} -type f -print0 | sort -z | xargs -r0 git apply \ + && find ../../patches/qtwayland_$QT -type f -print0 | sort -z | xargs -r0 git apply \ && cd .. \ && cmake -B build . \ -DCMAKE_INSTALL_PREFIX=/usr/local \ From 8e37e66706b2b3932b3ce2dc87a2f7f7c7da7f56 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:22:34 +0000 Subject: [PATCH 020/310] Make TOOLSET variable not dependent on jinja in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index e8c4db2154..ca071a06f9 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,33 +1,32 @@ -{%- set TOOLSET = "gcc-toolset-14" -%} {%- set LibrariesPath = "/usr/src/Libraries" -%} # syntax=docker/dockerfile:1 FROM rockylinux:8 AS builder ENV LANG C.UTF-8 -ENV PATH /opt/rh/{{ TOOLSET }}/root/usr/bin:$PATH -ENV LIBRARY_PATH /opt/rh/{{ TOOLSET }}/root/usr/lib64:/opt/rh/{{ TOOLSET }}/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib +ENV TOOLSET gcc-toolset-14 +ENV PATH /opt/rh/$TOOLSET/root/usr/bin:$PATH +ENV LIBRARY_PATH /opt/rh/$TOOLSET/root/usr/lib64:/opt/rh/$TOOLSET/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib ENV LD_LIBRARY_PATH $LIBRARY_PATH -ENV PKG_CONFIG_PATH /opt/rh/{{ TOOLSET }}/root/usr/lib64/pkgconfig:/opt/rh/{{ TOOLSET }}/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig +ENV PKG_CONFIG_PATH /opt/rh/$TOOLSET/root/usr/lib64/pkgconfig:/opt/rh/$TOOLSET/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ python3.11-pip python3.11-devel gperf flex bison clang clang-tools-extra \ lld nasm yasm file which perl-open perl-XML-Parser perl-IPC-Cmd \ - xorg-x11-util-macros {{ TOOLSET }}-gcc {{ TOOLSET }}-gcc-c++ \ - {{ TOOLSET }}-binutils {{ TOOLSET }}-gdb {{ TOOLSET }}-libasan-devel \ - libffi-devel fontconfig-devel freetype-devel libX11-devel wayland-devel \ - alsa-lib-devel pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel \ - mesa-libgbm-devel libdrm-devel vulkan-devel libva-devel libvdpau-devel \ - glib2-devel gobject-introspection-devel at-spi2-core-devel gtk3-devel \ - boost1.78-devel \ + xorg-x11-util-macros $TOOLSET-gcc $TOOLSET-gcc-c++ $TOOLSET-binutils \ + $TOOLSET-gdb $TOOLSET-libasan-devel libffi-devel fontconfig-devel \ + freetype-devel libX11-devel wayland-devel alsa-lib-devel \ + pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel mesa-libgbm-devel \ + libdrm-devel vulkan-devel libva-devel libvdpau-devel glib2-devel \ + gobject-introspection-devel at-spi2-core-devel gtk3-devel boost1.78-devel \ && dnf clean all RUN alternatives --set python3 /usr/bin/python3.11 RUN python3 -m pip install meson ninja RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc -RUN echo set debuginfod enabled on > /opt/rh/{{ TOOLSET }}/root/etc/gdbinit.d/00-debuginfod.gdb +RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user WORKDIR {{ LibrariesPath }} From a6157a34bc0ab059a1e579f2321e8d3f1ecd0b0d Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:25:07 +0000 Subject: [PATCH 021/310] Update variable syntax in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index ca071a06f9..90f2a6ba11 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -3,12 +3,12 @@ # syntax=docker/dockerfile:1 FROM rockylinux:8 AS builder -ENV LANG C.UTF-8 -ENV TOOLSET gcc-toolset-14 -ENV PATH /opt/rh/$TOOLSET/root/usr/bin:$PATH -ENV LIBRARY_PATH /opt/rh/$TOOLSET/root/usr/lib64:/opt/rh/$TOOLSET/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib -ENV LD_LIBRARY_PATH $LIBRARY_PATH -ENV PKG_CONFIG_PATH /opt/rh/$TOOLSET/root/usr/lib64/pkgconfig:/opt/rh/$TOOLSET/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig +ENV LANG=C.UTF-8 +ENV TOOLSET=gcc-toolset-14 +ENV PATH=/opt/rh/$TOOLSET/root/usr/bin:$PATH +ENV LIBRARY_PATH=/opt/rh/$TOOLSET/root/usr/lib64:/opt/rh/$TOOLSET/root/usr/lib:/usr/local/lib64:/usr/local/lib:/lib64:/lib:/usr/lib64:/usr/lib +ENV LD_LIBRARY_PATH=$LIBRARY_PATH +ENV PKG_CONFIG_PATH=/opt/rh/$TOOLSET/root/usr/lib64/pkgconfig:/opt/rh/$TOOLSET/root/usr/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ @@ -30,15 +30,15 @@ RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debu RUN adduser user WORKDIR {{ LibrariesPath }} -ENV AR gcc-ar -ENV RANLIB gcc-ranlib -ENV NM gcc-nm -ENV CFLAGS {% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened -ENV CXXFLAGS $CFLAGS +ENV AR=gcc-ar +ENV RANLIB=gcc-ranlib +ENV NM=gcc-nm +ENV CFLAGS='{% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' +ENV CXXFLAGS=$CFLAGS -ENV CMAKE_GENERATOR Ninja -ENV CMAKE_BUILD_TYPE None -ENV CMAKE_BUILD_PARALLEL_LEVEL '' +ENV CMAKE_GENERATOR=Ninja +ENV CMAKE_BUILD_TYPE=None +ENV CMAKE_BUILD_PARALLEL_LEVEL= FROM builder AS patches RUN git init patches \ @@ -676,7 +676,7 @@ COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -ENV QT 6.9.0 +ENV QT=6.9.0 RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ && cd qt5 \ && git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \ @@ -809,8 +809,8 @@ COPY --link --from=patches {{ LibrariesPath }}/patches patches RUN patch -p1 -d /usr/lib64/gobject-introspection -i $PWD/patches/gobject-introspection.patch && rm -rf patches WORKDIR ../tdesktop -ENV BOOST_INCLUDEDIR /usr/include/boost1.78 -ENV BOOST_LIBRARYDIR /usr/lib64/boost1.78 +ENV BOOST_INCLUDEDIR=/usr/include/boost1.78 +ENV BOOST_LIBRARYDIR=/usr/lib64/boost1.78 USER user VOLUME [ "/usr/src/tdesktop" ] From ecf1fa2bbd95077e3b13c2645a257cbf1bae6f14 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 23:50:51 +0000 Subject: [PATCH 022/310] Get rid of lib prefixes in Docker layers --- Telegram/build/docker/centos_env/Dockerfile | 104 ++++++++++---------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 90f2a6ba11..8272915b6f 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -146,7 +146,7 @@ RUN git clone -b v2.4.1 --depth=1 https://github.com/cisco/openh264.git \ && cd .. \ && rm -rf openh264 -FROM builder AS libde265 +FROM builder AS de265 RUN git clone -b v1.0.15 --depth=1 https://github.com/strukturag/libde265.git \ && cd libde265 \ && cmake -B build . \ @@ -155,11 +155,11 @@ RUN git clone -b v1.0.15 --depth=1 https://github.com/strukturag/libde265.git \ -DENABLE_DECODER=OFF \ -DENABLE_SDL=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/libde265-cache" cmake --install build \ + && DESTDIR="{{ LibrariesPath }}/de265-cache" cmake --install build \ && cd .. \ && rm -rf libde265 -FROM builder AS libvpx +FROM builder AS vpx RUN git init libvpx \ && cd libvpx \ && git remote add origin https://github.com/webmproject/libvpx.git \ @@ -175,11 +175,11 @@ RUN git init libvpx \ --enable-webm-io \ --size-limit=4096x4096 \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libvpx-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/vpx-cache" install \ && cd .. \ && rm -rf libvpx -FROM builder AS libwebp +FROM builder AS webp RUN git clone -b chrome-m116-5845 --depth=1 https://github.com/webmproject/libwebp.git \ && cd libwebp \ && cmake -B build . \ @@ -193,11 +193,11 @@ RUN git clone -b chrome-m116-5845 --depth=1 https://github.com/webmproject/libwe -DWEBP_BUILD_WEBPINFO=OFF \ -DWEBP_BUILD_EXTRAS=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/libwebp-cache" cmake --install build \ + && DESTDIR="{{ LibrariesPath }}/webp-cache" cmake --install build \ && cd .. \ && rm -rf libwebp -FROM builder AS libavif +FROM builder AS avif COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / RUN git clone -b v1.0.4 --depth=1 https://github.com/AOMediaCodec/libavif.git \ @@ -206,12 +206,12 @@ RUN git clone -b v1.0.4 --depth=1 https://github.com/AOMediaCodec/libavif.git \ -DBUILD_SHARED_LIBS=OFF \ -DAVIF_CODEC_DAV1D=ON \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/libavif-cache" cmake --install build \ + && DESTDIR="{{ LibrariesPath }}/avif-cache" cmake --install build \ && cd .. \ && rm -rf libavif -FROM builder AS libheif -COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / +FROM builder AS heif +COPY --link --from=de265 {{ LibrariesPath }}/de265-cache / RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ && cd libheif \ @@ -229,11 +229,11 @@ RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ -DWITH_DAV1D=OFF \ -DWITH_EXAMPLES=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/libheif-cache" cmake --install build \ + && DESTDIR="{{ LibrariesPath }}/heif-cache" cmake --install build \ && cd .. \ && rm -rf libheif -FROM builder AS libjxl +FROM builder AS jxl COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / COPY --link --from=highway {{ LibrariesPath }}/highway-cache / @@ -256,7 +256,7 @@ RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ -DJPEGXL_ENABLE_OPENEXR=OFF \ -DJPEGXL_ENABLE_SKCMS=OFF \ && cmake --build build \ - && export DESTDIR="{{ LibrariesPath }}/libjxl-cache" \ + && export DESTDIR="{{ LibrariesPath }}/jxl-cache" \ && cmake --install build \ && cp build/lib/libjpegli-static.a $DESTDIR/usr/local/lib64/libjpeg.a \ && ar rcs $DESTDIR/usr/local/lib64/libjpeg.a build/lib/CMakeFiles/jpegli-libjpeg-obj.dir/jpegli/libjpeg_wrapper.cc.o \ @@ -352,77 +352,77 @@ RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodule && cd .. \ && rm -rf libxcb-cursor -FROM builder AS libXext +FROM builder AS xext RUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxext.git \ && cd libxext \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXext-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xext-cache" install \ && cd .. \ && rm -rf libxext -FROM builder AS libXtst +FROM builder AS xtst RUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxtst.git \ && cd libxtst \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXtst-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xtst-cache" install \ && cd .. \ && rm -rf libxtst -FROM builder AS libXfixes +FROM builder AS xfixes RUN git clone -b libXfixes-5.0.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxfixes.git \ && cd libxfixes \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXfixes-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xfixes-cache" install \ && cd .. \ && rm -rf libxfixes -FROM builder AS libXv -COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / +FROM builder AS xv +COPY --link --from=xext {{ LibrariesPath }}/xext-cache / RUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxv.git \ && cd libxv \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXv-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xv-cache" install \ && cd .. \ && rm -rf libxv -FROM builder AS libXrandr +FROM builder AS xrandr RUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrandr.git \ && cd libxrandr \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXrandr-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xrandr-cache" install \ && cd .. \ && rm -rf libxrandr -FROM builder AS libXrender +FROM builder AS xrender RUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrender.git \ && cd libxrender \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXrender-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xrender-cache" install \ && cd .. \ && rm -rf libxrender -FROM builder AS libXdamage +FROM builder AS xdamage RUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxdamage.git \ && cd libxdamage \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXdamage-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xdamage-cache" install \ && cd .. \ && rm -rf libxdamage -FROM builder AS libXcomposite +FROM builder AS xcomposite RUN git clone -b libXcomposite-0.4.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcomposite.git \ && cd libxcomposite \ && ./autogen.sh --enable-static \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/libXcomposite-cache" install \ + && make DESTDIR="{{ LibrariesPath }}/xcomposite-cache" install \ && cd .. \ && rm -rf libxcomposite @@ -452,9 +452,9 @@ FROM builder AS ffmpeg COPY --link --from=opus {{ LibrariesPath }}/opus-cache / COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / -COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / -COPY --link --from=libXv {{ LibrariesPath }}/libXv-cache / +COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / +COPY --link --from=xext {{ LibrariesPath }}/xext-cache / +COPY --link --from=xv {{ LibrariesPath }}/xv-cache / COPY --link --from=nv-codec-headers {{ LibrariesPath }}/nv-codec-headers-cache / RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ @@ -663,8 +663,8 @@ RUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbco FROM patches AS qt COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=libwebp {{ LibrariesPath }}/libwebp-cache / -COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / +COPY --link --from=webp {{ LibrariesPath }}/webp-cache / +COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / COPY --link --from=xcb-wm {{ LibrariesPath }}/xcb-wm-cache / COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / @@ -714,11 +714,11 @@ RUN git clone -b v2024.02.16 --depth=1 https://chromium.googlesource.com/breakpa FROM builder AS webrtc COPY --link --from=opus {{ LibrariesPath }}/opus-cache / COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / -COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / +COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / +COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=libXtst {{ LibrariesPath }}/libXtst-cache / +COPY --link --from=xtst {{ LibrariesPath }}/xtst-cache / COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / # Shallow clone on a specific commit. @@ -772,12 +772,12 @@ COPY --link --from=highway {{ LibrariesPath }}/highway-cache / COPY --link --from=opus {{ LibrariesPath }}/opus-cache / COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=libde265 {{ LibrariesPath }}/libde265-cache / -COPY --link --from=libvpx {{ LibrariesPath }}/libvpx-cache / -COPY --link --from=libwebp {{ LibrariesPath }}/libwebp-cache / -COPY --link --from=libavif {{ LibrariesPath }}/libavif-cache / -COPY --link --from=libheif {{ LibrariesPath }}/libheif-cache / -COPY --link --from=libjxl {{ LibrariesPath }}/libjxl-cache / +COPY --link --from=de265 {{ LibrariesPath }}/de265-cache / +COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / +COPY --link --from=webp {{ LibrariesPath }}/webp-cache / +COPY --link --from=avif {{ LibrariesPath }}/avif-cache / +COPY --link --from=heif {{ LibrariesPath }}/heif-cache / +COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / COPY --link --from=rnnoise {{ LibrariesPath }}/rnnoise-cache / COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / COPY --link --from=xcb-wm {{ LibrariesPath }}/xcb-wm-cache / @@ -786,14 +786,14 @@ COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / COPY --link --from=xcb-keysyms {{ LibrariesPath }}/xcb-keysyms-cache / COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / COPY --link --from=xcb-cursor {{ LibrariesPath }}/xcb-cursor-cache / -COPY --link --from=libXext {{ LibrariesPath }}/libXext-cache / -COPY --link --from=libXfixes {{ LibrariesPath }}/libXfixes-cache / -COPY --link --from=libXv {{ LibrariesPath }}/libXv-cache / -COPY --link --from=libXtst {{ LibrariesPath }}/libXtst-cache / -COPY --link --from=libXrandr {{ LibrariesPath }}/libXrandr-cache / -COPY --link --from=libXrender {{ LibrariesPath }}/libXrender-cache / -COPY --link --from=libXdamage {{ LibrariesPath }}/libXdamage-cache / -COPY --link --from=libXcomposite {{ LibrariesPath }}/libXcomposite-cache / +COPY --link --from=xext {{ LibrariesPath }}/xext-cache / +COPY --link --from=xfixes {{ LibrariesPath }}/xfixes-cache / +COPY --link --from=xv {{ LibrariesPath }}/xv-cache / +COPY --link --from=xtst {{ LibrariesPath }}/xtst-cache / +COPY --link --from=xrandr {{ LibrariesPath }}/xrandr-cache / +COPY --link --from=xrender {{ LibrariesPath }}/xrender-cache / +COPY --link --from=xdamage {{ LibrariesPath }}/xdamage-cache / +COPY --link --from=xcomposite {{ LibrariesPath }}/xcomposite-cache / COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / COPY --link --from=openal {{ LibrariesPath }}/openal-cache / From 7e418a16aee02509257bf89459d3d2c45db2a072 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sun, 1 Jun 2025 06:29:08 +0000 Subject: [PATCH 023/310] Fix packaged conditions in lib_ffmpeg and Packer --- Telegram/CMakeLists.txt | 4 ++++ Telegram/SourceFiles/_other/packer.cpp | 2 +- Telegram/SourceFiles/_other/packer.h | 2 +- Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp | 14 +++++++------- Telegram/cmake/lib_ffmpeg.cmake | 4 ++++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2a86806efa..82dbdfd5b3 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1989,6 +1989,10 @@ if (NOT DESKTOP_APP_DISABLE_AUTOUPDATE AND NOT build_macstore AND NOT build_wins desktop-app::external_openssl ) + if (DESKTOP_APP_USE_PACKAGED) + target_compile_definitions(Packer PRIVATE PACKER_USE_PACKAGED) + endif() + set_target_properties(Packer PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_folder}) endif() elseif (build_winstore) diff --git a/Telegram/SourceFiles/_other/packer.cpp b/Telegram/SourceFiles/_other/packer.cpp index e563e16753..4df8b23800 100644 --- a/Telegram/SourceFiles/_other/packer.cpp +++ b/Telegram/SourceFiles/_other/packer.cpp @@ -283,7 +283,7 @@ int main(int argc, char *argv[]) cout << "Compression start, size: " << resultSize << "\n"; QByteArray compressed, resultCheck; -#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win +#if defined Q_OS_WIN && !defined PACKER_USE_PACKAGED // use Lzma SDK for win const int32 hSigLen = 128, hShaLen = 20, hPropsLen = LZMA_PROPS_SIZE, hOriginalSizeLen = sizeof(int32), hSize = hSigLen + hShaLen + hPropsLen + hOriginalSizeLen; // header compressed.resize(hSize + resultSize + 1024 * 1024); // rsa signature + sha1 + lzma props + max compressed size diff --git a/Telegram/SourceFiles/_other/packer.h b/Telegram/SourceFiles/_other/packer.h index 4e5fbfc7ac..2c200eefd7 100644 --- a/Telegram/SourceFiles/_other/packer.h +++ b/Telegram/SourceFiles/_other/packer.h @@ -27,7 +27,7 @@ extern "C" { #include } // extern "C" -#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win +#if defined Q_OS_WIN && !defined PACKER_USE_PACKAGED // use Lzma SDK for win #include #else #include diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 3811cbc860..5c489596e5 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -10,10 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/algorithm.h" #include "logs.h" -#if !defined TDESKTOP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC +#ifdef LIB_FFMPEG_USE_IMPLIB #include "base/platform/linux/base_linux_library.h" #include -#endif // !TDESKTOP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC +#endif // LIB_FFMPEG_USE_IMPLIB #include @@ -91,7 +91,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { #endif // LIB_FFMPEG_USE_QT_PRIVATE_API } -#if !defined TDESKTOP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC +#ifdef LIB_FFMPEG_USE_IMPLIB [[nodiscard]] auto CheckHwLibs() { auto list = std::deque{ AV_PIX_FMT_CUDA, @@ -117,7 +117,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return list; } -#endif // !TDESKTOP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC +#endif // LIB_FFMPEG_USE_IMPLIB [[nodiscard]] bool InitHw(AVCodecContext *context, AVHWDeviceType type) { AVCodecContext *parent = static_cast(context->opaque); @@ -160,9 +160,9 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return false; }; -#if !defined TDESKTOP_USE_PACKAGED && !defined Q_OS_WIN && !defined Q_OS_MAC +#ifdef LIB_FFMPEG_USE_IMPLIB static const auto list = CheckHwLibs(); -#else // !TDESKTOP_USE_PACKAGED && !Q_OS_WIN && !Q_OS_MAC +#else // LIB_FFMPEG_USE_IMPLIB const auto list = std::array{ #ifdef Q_OS_WIN AV_PIX_FMT_D3D11, @@ -176,7 +176,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { AV_PIX_FMT_CUDA, #endif // Q_OS_WIN || Q_OS_MAC }; -#endif // TDESKTOP_USE_PACKAGED || Q_OS_WIN || Q_OS_MAC +#endif // LIB_FFMPEG_USE_IMPLIB for (const auto format : list) { if (!has(format)) { continue; diff --git a/Telegram/cmake/lib_ffmpeg.cmake b/Telegram/cmake/lib_ffmpeg.cmake index 0da37cb096..c48a900b99 100644 --- a/Telegram/cmake/lib_ffmpeg.cmake +++ b/Telegram/cmake/lib_ffmpeg.cmake @@ -29,6 +29,10 @@ PUBLIC desktop-app::external_ffmpeg ) +if (LINUX AND NOT DESKTOP_APP_USE_PACKAGED) + target_compile_definitions(lib_ffmpeg PRIVATE LIB_FFMPEG_USE_IMPLIB) +endif() + if (DESKTOP_APP_SPECIAL_TARGET) target_compile_definitions(lib_ffmpeg PRIVATE LIB_FFMPEG_USE_QT_PRIVATE_API) endif() From a64cfe661af4e2a57793c41c3c757e1ad62dfcc1 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Thu, 29 May 2025 17:45:10 +0000 Subject: [PATCH 024/310] Add missing deps to webrtc Docker layer --- Telegram/build/docker/centos_env/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 8272915b6f..794945e4d9 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -712,13 +712,21 @@ RUN git clone -b v2024.02.16 --depth=1 https://chromium.googlesource.com/breakpa && rm -rf breakpad FROM builder AS webrtc +COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / COPY --link --from=opus {{ LibrariesPath }}/opus-cache / COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / +COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / +COPY --link --from=xext {{ LibrariesPath }}/xext-cache / +COPY --link --from=xfixes {{ LibrariesPath }}/xfixes-cache / COPY --link --from=xtst {{ LibrariesPath }}/xtst-cache / +COPY --link --from=xrandr {{ LibrariesPath }}/xrandr-cache / +COPY --link --from=xrender {{ LibrariesPath }}/xrender-cache / +COPY --link --from=xdamage {{ LibrariesPath }}/xdamage-cache / +COPY --link --from=xcomposite {{ LibrariesPath }}/xcomposite-cache / COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / # Shallow clone on a specific commit. From 4a9dd43598e52992d7db0fcad42725c543b4edd7 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 3 Jun 2025 05:39:52 +0000 Subject: [PATCH 025/310] Update tg_owt --- Telegram/build/docker/centos_env/Dockerfile | 2 +- Telegram/build/prepare/prepare.py | 2 +- snap/snapcraft.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 794945e4d9..41279ae260 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -733,7 +733,7 @@ COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / RUN git init tg_owt \ && cd tg_owt \ && git remote add origin https://github.com/desktop-app/tg_owt.git \ - && git fetch --depth=1 origin c4192e8e2e10ccb72704daa79fa108becfa57b01 \ + && git fetch --depth=1 origin 62321fd7128ab2650b459d4195781af8185e46b5 \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ && cmake -B build . -DTG_OWT_DLOPEN_PIPEWIRE=ON \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 42ab88f734..19a9f219db 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1746,7 +1746,7 @@ win: stage('tg_owt', """ git clone https://github.com/desktop-app/tg_owt.git cd tg_owt - git checkout c4192e8 + git checkout 62321fd git submodule update --init --recursive win: SET MOZJPEG_PATH=$LIBS_DIR/mozjpeg diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 49478c7036..7391949903 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -501,7 +501,7 @@ parts: webrtc: source: https://github.com/desktop-app/tg_owt.git source-depth: 1 - source-commit: c4192e8e2e10ccb72704daa79fa108becfa57b01 + source-commit: 62321fd7128ab2650b459d4195781af8185e46b5 plugin: cmake build-environment: - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s From dda587dc6fb76ad247f0403629b6da6caf2cefae Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sun, 1 Jun 2025 23:06:12 +0000 Subject: [PATCH 026/310] DESKTOP_APP_USE_PACKAGED_LAZY -> DESKTOP_APP_DISABLE_QT_PLUGINS --- .github/workflows/mac_packaged.yml | 1 - cmake | 2 +- snap/snapcraft.yaml | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 58de05abe8..7dcae8d6d8 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -168,7 +168,6 @@ jobs: -DCMAKE_CXX_FLAGS_DEBUG="" \ -DCMAKE_EXE_LINKER_FLAGS="-s" \ -DTDESKTOP_API_TEST=ON \ - -DDESKTOP_APP_USE_PACKAGED_LAZY=ON \ $DEFINE cmake --build build --parallel diff --git a/cmake b/cmake index 1e09ee81ee..833d1e2b3a 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 1e09ee81ee7cdfa848ee4902934a271f4b71265f +Subproject commit 833d1e2b3a61ae5a8c61c3e86e78858e4ce84cb4 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 7391949903..24fe70706e 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -130,7 +130,6 @@ parts: - -DCMAKE_PREFIX_PATH=$CRAFT_STAGE/usr - -DTDESKTOP_API_ID=611335 - -DTDESKTOP_API_HASH=d524b414d21f4d37f08684c1df41ac9c - - -DDESKTOP_APP_USE_PACKAGED_LAZY=ON override-pull: | craftctl default From 108b116b06a340e2a6623b9c153e36c5c61f8c72 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 30 May 2025 23:19:17 +0000 Subject: [PATCH 027/310] Use lld when building without LTO in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 41279ae260..222bc3b77f 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -35,6 +35,7 @@ ENV RANLIB=gcc-ranlib ENV NM=gcc-nm ENV CFLAGS='{% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' ENV CXXFLAGS=$CFLAGS +ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %}' ENV CMAKE_GENERATOR=Ninja ENV CMAKE_BUILD_TYPE=None From edc84731ac58cb591c26fefa81c106bde6e755f7 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Thu, 29 May 2025 07:41:20 +0000 Subject: [PATCH 028/310] Change debug cmake flags according to Dockerfile options --- Telegram/build/docker/centos_env/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 222bc3b77f..fc7b16d9a5 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -25,6 +25,7 @@ RUN dnf -y install epel-release \ RUN alternatives --set python3 /usr/bin/python3.11 RUN python3 -m pip install meson ninja +RUN sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/")/ -O0 {% if LTO %}-fno-lto -fno-use-linker-plugin -fuse-ld=lld{% endif %}")/' /usr/share/cmake/Modules/Compiler/GNU.cmake RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user From 73649128f3a993e08d481a1fbdb3cb31ece9609d Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 3 Jun 2025 05:54:16 +0000 Subject: [PATCH 029/310] Update cmake_helpers --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index 833d1e2b3a..9223f910e3 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 833d1e2b3a61ae5a8c61c3e86e78858e4ce84cb4 +Subproject commit 9223f910e3c90f44ac06bfe846baa932c1926c3a From 5f0e9538cf0e7d376ed1ee2644a17ebe29e7520e Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 30 May 2025 19:14:18 +0000 Subject: [PATCH 030/310] Move Implib to Dockerfile --- CMakeLists.txt | 3 -- Telegram/build/docker/centos_env/Dockerfile | 33 ++++++++++++++++++++- cmake | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85c326fec6..315bdc525d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,8 +21,6 @@ project(Telegram if (APPLE) enable_language(OBJC OBJCXX) -elseif (LINUX) - enable_language(ASM) endif() set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT Telegram) @@ -39,7 +37,6 @@ include(cmake/variables.cmake) include(cmake/nice_target_sources.cmake) include(cmake/target_compile_options_if_exists.cmake) include(cmake/target_link_frameworks.cmake) -include(cmake/target_link_optional_libraries.cmake) include(cmake/target_link_options_if_exists.cmake) include(cmake/target_link_static_libraries.cmake) include(cmake/init_target.cmake) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index fc7b16d9a5..f91661c60c 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -36,12 +36,43 @@ ENV RANLIB=gcc-ranlib ENV NM=gcc-nm ENV CFLAGS='{% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' ENV CXXFLAGS=$CFLAGS -ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %}' +ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %} -pthread -ldl -Wl,--as-needed -Wl,-z,muldefs' ENV CMAKE_GENERATOR=Ninja ENV CMAKE_BUILD_TYPE=None ENV CMAKE_BUILD_PARALLEL_LEVEL= +RUN git clone --depth=1 https://github.com/yugr/Implib.so.git \ + && mkdir Implib.so/build \ + && cd Implib.so/build \ + && implib() { \ + LIBFILE=$(basename $1); \ + LIBNAME=$(basename $1 .so); \ + ../implib-gen.py -q $1; \ + gcc $CFLAGS -c -o $LIBFILE.tramp.o $LIBFILE.tramp.S; \ + gcc $CFLAGS -c -o $LIBFILE.init.o $LIBFILE.init.c; \ + ar rcs /usr/local/lib64/$LIBNAME.a $LIBFILE.tramp.o $LIBFILE.init.o; \ + } \ + && implib /usr/lib64/libgtk-3.so \ + && implib /usr/lib64/libgdk-3.so \ + && implib /usr/lib64/libgdk_pixbuf-2.0.so \ + && implib /usr/lib64/libpango-1.0.so \ + && implib /usr/lib64/libvdpau.so \ + && implib /usr/lib64/libva-x11.so \ + && implib /usr/lib64/libva-drm.so \ + && implib /usr/lib64/libva.so \ + && implib /usr/lib64/libEGL.so \ + && implib /usr/lib64/libGL.so \ + && implib /usr/lib64/libdrm.so \ + && implib /usr/lib64/libwayland-egl.so \ + && implib /usr/lib64/libwayland-cursor.so \ + && implib /usr/lib64/libwayland-client.so \ + && implib /usr/lib64/libwayland-server.so \ + && implib /usr/lib64/libX11-xcb.so \ + && implib /usr/lib64/libxcb.so \ + && cd ../.. \ + && rm -rf Implib.so + FROM builder AS patches RUN git init patches \ && cd patches \ diff --git a/cmake b/cmake index 9223f910e3..72e005977d 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 9223f910e3c90f44ac06bfe846baa932c1926c3a +Subproject commit 72e005977d80a12af5ca9028d666437319c46693 From e4f59f1ec43cf4d5b8d20b0e5a11973da70b1ba7 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Thu, 29 May 2025 08:53:48 +0000 Subject: [PATCH 031/310] Build only static libraries in Dockerfile --- CMakeLists.txt | 1 - Telegram/build/docker/centos_env/Dockerfile | 72 +++++++++++++-------- cmake | 2 +- snap/snapcraft.yaml | 2 +- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 315bdc525d..fc7b239b03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,6 @@ include(cmake/nice_target_sources.cmake) include(cmake/target_compile_options_if_exists.cmake) include(cmake/target_link_frameworks.cmake) include(cmake/target_link_options_if_exists.cmake) -include(cmake/target_link_static_libraries.cmake) include(cmake/init_target.cmake) include(cmake/generate_target.cmake) include(cmake/nuget.cmake) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index f91661c60c..b01869f9e3 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -19,13 +19,22 @@ RUN dnf -y install epel-release \ $TOOLSET-gdb $TOOLSET-libasan-devel libffi-devel fontconfig-devel \ freetype-devel libX11-devel wayland-devel alsa-lib-devel \ pulseaudio-libs-devel mesa-libGL-devel mesa-libEGL-devel mesa-libgbm-devel \ - libdrm-devel vulkan-devel libva-devel libvdpau-devel glib2-devel \ - gobject-introspection-devel at-spi2-core-devel gtk3-devel boost1.78-devel \ + libdrm-devel vulkan-devel libva-devel libvdpau-devel libselinux-devel \ + libmount-devel systemd-devel glib2-devel gobject-introspection-devel \ + at-spi2-core-devel gtk3-devel boost1.78-devel \ && dnf clean all RUN alternatives --set python3 /usr/bin/python3.11 RUN python3 -m pip install meson ninja +RUN cat < /usr/local/bin/pkg-config && chmod +x /usr/local/bin/pkg-config +#!/bin/sh +for i in "\$@"; do + [ "\$i" = "--version" ] && exec /usr/bin/pkg-config "\$i" +done +exec /usr/bin/pkg-config --static "\$@" +EOF RUN sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/")/ -O0 {% if LTO %}-fno-lto -fno-use-linker-plugin -fuse-ld=lld{% endif %}")/' /usr/share/cmake/Modules/Compiler/GNU.cmake +RUN sed -i 's/NO_DEFAULT_PATH//g; s/PKG_CONFIG_ALLOW_SYSTEM_LIBS/PKG_CONFIG_IS_DUMB/g' /usr/share/cmake/Modules/FindPkgConfig.cmake RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user @@ -36,7 +45,7 @@ ENV RANLIB=gcc-ranlib ENV NM=gcc-nm ENV CFLAGS='{% if DEBUG %}-g{% endif %} -O3 {% if LTO %}-flto=auto -ffat-lto-objects{% endif %} -pipe -fPIC -fno-strict-aliasing -fexceptions -fasynchronous-unwind-tables -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fhardened -Wno-hardened' ENV CXXFLAGS=$CFLAGS -ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %} -pthread -ldl -Wl,--as-needed -Wl,-z,muldefs' +ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %} -static-libstdc++ -static-libgcc -static-libasan -pthread -ldl -Wl,--as-needed -Wl,-z,muldefs' ENV CMAKE_GENERATOR=Ninja ENV CMAKE_BUILD_TYPE=None @@ -77,7 +86,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin https://github.com/desktop-app/patches.git \ - && git fetch --depth=1 origin 22989737aea515bf6a94d74a65490d37409831bc \ + && git fetch --depth=1 origin 65c6e9f8e88f37396e935dd6e6e494f512a96f99 \ && git reset --hard FETCH_HEAD \ && rm -rf .git @@ -86,7 +95,9 @@ RUN git clone -b v1.3.1 --depth=1 https://github.com/madler/zlib.git \ && cd zlib \ && cmake -B build . -DZLIB_BUILD_EXAMPLES=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/zlib-cache" cmake --install build \ + && export DESTDIR="{{ LibrariesPath }}/zlib-cache" \ + && cmake --install build \ + && rm $DESTDIR/usr/local/lib/libz.so* \ && cd .. \ && rm -rf zlib @@ -117,7 +128,7 @@ RUN git clone -b lcms2.15 --depth=1 https://github.com/mm2/Little-CMS.git \ && cd Little-CMS \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ && meson compile -C build \ && DESTDIR="{{ LibrariesPath }}/lcms2-cache" meson install -C build \ && cd .. \ @@ -160,7 +171,7 @@ RUN git clone -b 1.4.1 --depth=1 https://github.com/videolan/dav1d.git \ && cd dav1d \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ -Denable_tools=false \ -Denable_tests=false \ && meson compile -C build \ @@ -173,7 +184,7 @@ RUN git clone -b v2.4.1 --depth=1 https://github.com/cisco/openh264.git \ && cd openh264 \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ && meson compile -C build \ && DESTDIR="{{ LibrariesPath }}/openh264-cache" meson install -C build \ && cd .. \ @@ -291,8 +302,11 @@ RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ && cmake --build build \ && export DESTDIR="{{ LibrariesPath }}/jxl-cache" \ && cmake --install build \ + && rm $DESTDIR/usr/local/lib64/libjpeg.so* \ && cp build/lib/libjpegli-static.a $DESTDIR/usr/local/lib64/libjpeg.a \ - && ar rcs $DESTDIR/usr/local/lib64/libjpeg.a build/lib/CMakeFiles/jpegli-libjpeg-obj.dir/jpegli/libjpeg_wrapper.cc.o \ + && mkdir build/hwy \ + && ar --output=build/hwy x /usr/local/lib64/libhwy.a \ + && ar rcs $DESTDIR/usr/local/lib64/libjpeg.a build/lib/CMakeFiles/jpegli-libjpeg-obj.dir/jpegli/libjpeg_wrapper.cc.o build/hwy/* \ && cd .. \ && rm -rf libjxl @@ -319,16 +333,18 @@ COPY --link --from=xcb-proto {{ LibrariesPath }}/xcb-proto-cache / RUN git clone -b libxcb-1.16 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcb.git \ && cd libxcb \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-cache" install \ + && export DESTDIR="{{ LibrariesPath }}/xcb-cache" \ + && make install \ + && rm $DESTDIR/usr/local/lib/{libxcb.{,l}a,pkgconfig/xcb.pc} \ && cd .. \ && rm -rf libxcb FROM builder AS xcb-wm RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-wm.git \ && cd libxcb-wm \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-wm-cache" install \ && cd .. \ @@ -337,7 +353,7 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules ht FROM builder AS xcb-util RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-util.git \ && cd libxcb-util \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-util-cache" install \ && cd .. \ @@ -348,7 +364,7 @@ COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \ && cd libxcb-image \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-image-cache" install \ && cd .. \ @@ -357,7 +373,7 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules FROM builder AS xcb-keysyms RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ && cd libxcb-keysyms \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-keysyms-cache" install \ && cd .. \ @@ -366,7 +382,7 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul FROM builder AS xcb-render-util RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-render-util.git \ && cd libxcb-render-util \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-render-util-cache" install \ && cd .. \ @@ -379,7 +395,7 @@ COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \ && cd libxcb-cursor \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcb-cursor-cache" install \ && cd .. \ @@ -388,7 +404,7 @@ RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodule FROM builder AS xext RUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxext.git \ && cd libxext \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xext-cache" install \ && cd .. \ @@ -397,7 +413,7 @@ RUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-m FROM builder AS xtst RUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxtst.git \ && cd libxtst \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xtst-cache" install \ && cd .. \ @@ -406,7 +422,7 @@ RUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-m FROM builder AS xfixes RUN git clone -b libXfixes-5.0.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxfixes.git \ && cd libxfixes \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xfixes-cache" install \ && cd .. \ @@ -417,7 +433,7 @@ COPY --link --from=xext {{ LibrariesPath }}/xext-cache / RUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxv.git \ && cd libxv \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xv-cache" install \ && cd .. \ @@ -426,7 +442,7 @@ RUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mi FROM builder AS xrandr RUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrandr.git \ && cd libxrandr \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xrandr-cache" install \ && cd .. \ @@ -435,7 +451,7 @@ RUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop FROM builder AS xrender RUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxrender.git \ && cd libxrender \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xrender-cache" install \ && cd .. \ @@ -444,7 +460,7 @@ RUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedeskt FROM builder AS xdamage RUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxdamage.git \ && cd libxdamage \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xdamage-cache" install \ && cd .. \ @@ -453,7 +469,7 @@ RUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedeskto FROM builder AS xcomposite RUN git clone -b libXcomposite-0.4.6 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcomposite.git \ && cd libxcomposite \ - && ./autogen.sh --enable-static \ + && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR="{{ LibrariesPath }}/xcomposite-cache" install \ && cd .. \ @@ -465,7 +481,7 @@ RUN git clone -b 1.19.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/ && sed -i "/subdir('tests')/d" meson.build \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ -Ddocumentation=false \ -Ddtd_validation=false \ -Dicon_directory=/usr/share/icons \ @@ -495,7 +511,6 @@ RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ && ./configure \ --extra-cflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ --extra-cxxflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ - --pkg-config-flags=--static \ --disable-debug \ --disable-programs \ --disable-doc \ @@ -667,6 +682,7 @@ RUN git clone -b openssl-3.2.1 --depth=1 https://github.com/openssl/openssl.git && cd openssl \ && ./config \ --openssldir=/etc/ssl \ + no-shared \ no-tests \ no-dso \ && make -j$(nproc) \ @@ -681,7 +697,7 @@ RUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbco && cd libxkbcommon \ && meson build \ --buildtype=plain \ - --default-library=both \ + --default-library=static \ -Denable-docs=false \ -Denable-wayland=false \ -Denable-xkbregistry=false \ diff --git a/cmake b/cmake index 72e005977d..fd6f14f2de 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 72e005977d80a12af5ca9028d666437319c46693 +Subproject commit fd6f14f2deb9cc67c144c529e3267b67f99ba624 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 24fe70706e..129cb43c13 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -169,7 +169,7 @@ parts: patches: source: https://github.com/desktop-app/patches.git source-depth: 1 - source-commit: 22989737aea515bf6a94d74a65490d37409831bc + source-commit: 65c6e9f8e88f37396e935dd6e6e494f512a96f99 plugin: dump override-pull: | craftctl default From 7246c3f304f0dd1f9e3487d29abba7d8889c27f0 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 30 May 2025 19:27:23 +0000 Subject: [PATCH 032/310] Set cmake OpenGL default to legacy in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index b01869f9e3..8efb38acb8 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -35,6 +35,7 @@ exec /usr/bin/pkg-config --static "\$@" EOF RUN sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/")/ -O0 {% if LTO %}-fno-lto -fno-use-linker-plugin -fuse-ld=lld{% endif %}")/' /usr/share/cmake/Modules/Compiler/GNU.cmake RUN sed -i 's/NO_DEFAULT_PATH//g; s/PKG_CONFIG_ALLOW_SYSTEM_LIBS/PKG_CONFIG_IS_DUMB/g' /usr/share/cmake/Modules/FindPkgConfig.cmake +RUN sed -i 's/set(OpenGL_GL_PREFERENCE "")/set(OpenGL_GL_PREFERENCE "LEGACY")/' /usr/share/cmake/Modules/FindOpenGL.cmake RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user From 15c817dd1594dcf0b228fbbad16477d8f6c7a859 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 3 Jun 2025 14:59:02 +0400 Subject: [PATCH 033/310] Update Qt 6.9.0 -> 6.9.1 --- Telegram/build/docker/centos_env/Dockerfile | 4 ++-- Telegram/build/prepare/prepare.py | 2 +- Telegram/build/qt_version.py | 2 +- snap/snapcraft.yaml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 8efb38acb8..3c715c1f86 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -87,7 +87,7 @@ FROM builder AS patches RUN git init patches \ && cd patches \ && git remote add origin https://github.com/desktop-app/patches.git \ - && git fetch --depth=1 origin 65c6e9f8e88f37396e935dd6e6e494f512a96f99 \ + && git fetch --depth=1 origin a405719f0963abf7cb93354a390617c0f0d90f17 \ && git reset --hard FETCH_HEAD \ && rm -rf .git @@ -726,7 +726,7 @@ COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -ENV QT=6.9.0 +ENV QT=6.9.1 RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ && cd qt5 \ && git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 19a9f219db..c2f05aa426 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -456,7 +456,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 7119a74e3f + git checkout a405719f0963abf7cb93354a390617c0f0d90f17 """) stage('msys64', """ diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py index 6e5cc0f50f..3257df851e 100644 --- a/Telegram/build/qt_version.py +++ b/Telegram/build/qt_version.py @@ -6,7 +6,7 @@ def resolve(arch): elif sys.platform == 'win32': if arch == 'arm' or 'qt6' in sys.argv: print('Choosing Qt 6.') - os.environ['QT'] = '6.9.0' + os.environ['QT'] = '6.9.1' else: print('Choosing Qt 5.') os.environ['QT'] = '5.15.15' diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 129cb43c13..8053e8d982 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -169,7 +169,7 @@ parts: patches: source: https://github.com/desktop-app/patches.git source-depth: 1 - source-commit: 65c6e9f8e88f37396e935dd6e6e494f512a96f99 + source-commit: a405719f0963abf7cb93354a390617c0f0d90f17 plugin: dump override-pull: | craftctl default @@ -404,7 +404,7 @@ parts: - mesa-vulkan-drivers - xkb-data override-pull: | - QT=6.9.0 + QT=6.9.1 git clone -b v${QT} --depth=1 https://github.com/qt/qt5.git . git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools From 56ff5808a3d766f892bc3c3305afb106b629ef6f Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 30 May 2025 15:23:18 +0000 Subject: [PATCH 034/310] Unify packaged/non-packaged binary name --- Telegram/CMakeLists.txt | 6 +----- Telegram/SourceFiles/platform/linux/specific_linux.cpp | 2 +- lib/xdg/org.telegram.desktop.desktop | 6 +++--- lib/xdg/org.telegram.desktop.metainfo.xml | 2 +- lib/xdg/org.telegram.desktop.service | 2 +- snap/snapcraft.yaml | 1 + 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 82dbdfd5b3..50dcfec3e6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1864,11 +1864,7 @@ else() set(bundle_identifier "com.tdesktop.Telegram") endif() set(bundle_entitlements "Telegram.entitlements") - if (LINUX AND DESKTOP_APP_USE_PACKAGED) - set(output_name "telegram-desktop") - else() - set(output_name "Telegram") - endif() + set(output_name "Telegram") endif() if (CMAKE_GENERATOR STREQUAL Xcode) diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index dcbefc2ae5..1710805359 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -769,7 +769,7 @@ bool OpenSystemSettings(SystemSettingsType type) { } void NewVersionLaunched(int oldVersion) { - if (oldVersion <= 4001001 && cAutoStart()) { + if (oldVersion <= 5014003 && cAutoStart()) { AutostartToggle(true); } } diff --git a/lib/xdg/org.telegram.desktop.desktop b/lib/xdg/org.telegram.desktop.desktop index 365516b4dd..80cbd8a980 100644 --- a/lib/xdg/org.telegram.desktop.desktop +++ b/lib/xdg/org.telegram.desktop.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Name=Telegram Comment=New era of messaging -TryExec=telegram-desktop -Exec=telegram-desktop -- %u +TryExec=Telegram +Exec=Telegram -- %u Icon=org.telegram.desktop Terminal=false StartupWMClass=TelegramDesktop @@ -17,6 +17,6 @@ X-GNOME-UsesNotifications=true X-GNOME-SingleWindow=true [Desktop Action quit] -Exec=telegram-desktop -quit +Exec=Telegram -quit Name=Quit Telegram Icon=application-exit diff --git a/lib/xdg/org.telegram.desktop.metainfo.xml b/lib/xdg/org.telegram.desktop.metainfo.xml index bc93de9015..c388432ade 100644 --- a/lib/xdg/org.telegram.desktop.metainfo.xml +++ b/lib/xdg/org.telegram.desktop.metainfo.xml @@ -106,7 +106,7 @@ org.telegram.desktop.desktop - telegram-desktop + Telegram org.telegram.desktop x-scheme-handler/tg x-scheme-handler/tonsite diff --git a/lib/xdg/org.telegram.desktop.service b/lib/xdg/org.telegram.desktop.service index 525cac208c..8935a4fa83 100644 --- a/lib/xdg/org.telegram.desktop.service +++ b/lib/xdg/org.telegram.desktop.service @@ -1,3 +1,3 @@ [D-BUS Service] Name=org.telegram.desktop -Exec=@CMAKE_INSTALL_FULL_BINDIR@/telegram-desktop +Exec=@CMAKE_INSTALL_FULL_BINDIR@/Telegram diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 8053e8d982..70bf6cb179 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -146,6 +146,7 @@ parts: craftctl set version="$version" override-build: | craftctl default + mv "$CRAFT_PART_INSTALL"/usr/bin/{Telegram,telegram-desktop} APP_ID=org.telegram.desktop sed -i "s/^Icon=$APP_ID$/Icon=snap.telegram-desktop./g" "$CRAFT_PART_INSTALL/usr/share/applications/$APP_ID.desktop" From 3896f0995c83943e42968bf2c19f84325450218b Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sun, 1 Jun 2025 21:31:43 +0000 Subject: [PATCH 035/310] Runtime Implib detection --- .../SourceFiles/ffmpeg/ffmpeg_utility.cpp | 45 +++++++++++-------- Telegram/cmake/lib_ffmpeg.cmake | 4 -- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp index 5c489596e5..9ccdb15592 100644 --- a/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp +++ b/Telegram/SourceFiles/ffmpeg/ffmpeg_utility.cpp @@ -10,10 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/algorithm.h" #include "logs.h" -#ifdef LIB_FFMPEG_USE_IMPLIB +#if !defined Q_OS_WIN && !defined Q_OS_MAC #include "base/platform/linux/base_linux_library.h" #include -#endif // LIB_FFMPEG_USE_IMPLIB +#endif // !Q_OS_WIN && !Q_OS_MAC #include @@ -26,6 +26,16 @@ extern "C" { #include } // extern "C" +#if !defined Q_OS_WIN && !defined Q_OS_MAC +extern "C" { +void _libvdpau_so_tramp_resolve_all(void) __attribute__((weak)); +void _libva_drm_so_tramp_resolve_all(void) __attribute__((weak)); +void _libva_x11_so_tramp_resolve_all(void) __attribute__((weak)); +void _libva_so_tramp_resolve_all(void) __attribute__((weak)); +void _libdrm_so_tramp_resolve_all(void) __attribute__((weak)); +} // extern "C" +#endif // !Q_OS_WIN && !Q_OS_MAC + namespace FFmpeg { namespace { @@ -91,23 +101,24 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { #endif // LIB_FFMPEG_USE_QT_PRIVATE_API } -#ifdef LIB_FFMPEG_USE_IMPLIB +#if !defined Q_OS_WIN && !defined Q_OS_MAC [[nodiscard]] auto CheckHwLibs() { auto list = std::deque{ AV_PIX_FMT_CUDA, }; - if (base::Platform::LoadLibrary("libvdpau.so.1")) { + if (!_libvdpau_so_tramp_resolve_all + || base::Platform::LoadLibrary("libvdpau.so.1")) { list.push_front(AV_PIX_FMT_VDPAU); } if ([&] { const auto list = std::array{ - "libva-drm.so.2", - "libva-x11.so.2", - "libva.so.2", - "libdrm.so.2", + std::make_pair(_libva_drm_so_tramp_resolve_all, "libva-drm.so.2"), + std::make_pair(_libva_x11_so_tramp_resolve_all, "libva-x11.so.2"), + std::make_pair(_libva_so_tramp_resolve_all, "libva.so.2"), + std::make_pair(_libdrm_so_tramp_resolve_all, "libdrm.so.2"), }; - for (const auto lib : list) { - if (!base::Platform::LoadLibrary(lib)) { + for (const auto &lib : list) { + if (lib.first && !base::Platform::LoadLibrary(lib.second)) { return false; } } @@ -117,7 +128,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return list; } -#endif // LIB_FFMPEG_USE_IMPLIB +#endif // !Q_OS_WIN && !Q_OS_MAC [[nodiscard]] bool InitHw(AVCodecContext *context, AVHWDeviceType type) { AVCodecContext *parent = static_cast(context->opaque); @@ -160,9 +171,7 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { } return false; }; -#ifdef LIB_FFMPEG_USE_IMPLIB - static const auto list = CheckHwLibs(); -#else // LIB_FFMPEG_USE_IMPLIB +#if defined Q_OS_WIN || defined Q_OS_MAC const auto list = std::array{ #ifdef Q_OS_WIN AV_PIX_FMT_D3D11, @@ -170,13 +179,11 @@ void PremultiplyLine(uchar *dst, const uchar *src, int intsCount) { AV_PIX_FMT_CUDA, #elif defined Q_OS_MAC // Q_OS_WIN AV_PIX_FMT_VIDEOTOOLBOX, -#else // Q_OS_WIN || Q_OS_MAC - AV_PIX_FMT_VAAPI, - AV_PIX_FMT_VDPAU, - AV_PIX_FMT_CUDA, #endif // Q_OS_WIN || Q_OS_MAC }; -#endif // LIB_FFMPEG_USE_IMPLIB +#else // Q_OS_WIN || Q_OS_MAC + static const auto list = CheckHwLibs(); +#endif // !Q_OS_WIN && !Q_OS_MAC for (const auto format : list) { if (!has(format)) { continue; diff --git a/Telegram/cmake/lib_ffmpeg.cmake b/Telegram/cmake/lib_ffmpeg.cmake index c48a900b99..0da37cb096 100644 --- a/Telegram/cmake/lib_ffmpeg.cmake +++ b/Telegram/cmake/lib_ffmpeg.cmake @@ -29,10 +29,6 @@ PUBLIC desktop-app::external_ffmpeg ) -if (LINUX AND NOT DESKTOP_APP_USE_PACKAGED) - target_compile_definitions(lib_ffmpeg PRIVATE LIB_FFMPEG_USE_IMPLIB) -endif() - if (DESKTOP_APP_SPECIAL_TARGET) target_compile_definitions(lib_ffmpeg PRIVATE LIB_FFMPEG_USE_QT_PRIVATE_API) endif() From f456071c086cca80d91623816177bdd5292818eb Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 28 May 2025 15:23:16 +0000 Subject: [PATCH 036/310] Switch Dockerfile to packaged mode --- Telegram/build/docker/centos_env/Dockerfile | 298 ++++++++++---------- 1 file changed, 150 insertions(+), 148 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 3c715c1f86..816a6df5b1 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -1,5 +1,3 @@ -{%- set LibrariesPath = "/usr/src/Libraries" -%} - # syntax=docker/dockerfile:1 FROM rockylinux:8 AS builder @@ -14,7 +12,7 @@ RUN dnf -y install epel-release \ && dnf config-manager --set-enabled powertools \ && dnf -y install cmake autoconf automake libtool pkgconfig make patch git \ python3.11-pip python3.11-devel gperf flex bison clang clang-tools-extra \ - lld nasm yasm file which perl-open perl-XML-Parser perl-IPC-Cmd \ + lld nasm yasm file which wget perl-open perl-XML-Parser perl-IPC-Cmd \ xorg-x11-util-macros $TOOLSET-gcc $TOOLSET-gcc-c++ $TOOLSET-binutils \ $TOOLSET-gdb $TOOLSET-libasan-devel libffi-devel fontconfig-devel \ freetype-devel libX11-devel wayland-devel alsa-lib-devel \ @@ -40,7 +38,7 @@ RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user -WORKDIR {{ LibrariesPath }} +WORKDIR /usr/src ENV AR=gcc-ar ENV RANLIB=gcc-ranlib ENV NM=gcc-nm @@ -96,7 +94,7 @@ RUN git clone -b v1.3.1 --depth=1 https://github.com/madler/zlib.git \ && cd zlib \ && cmake -B build . -DZLIB_BUILD_EXAMPLES=OFF \ && cmake --build build \ - && export DESTDIR="{{ LibrariesPath }}/zlib-cache" \ + && export DESTDIR=/usr/src/zlib-cache \ && cmake --install build \ && rm $DESTDIR/usr/local/lib/libz.so* \ && cd .. \ @@ -107,7 +105,7 @@ RUN git clone -b v5.8.1 --depth=1 https://github.com/tukaani-project/xz.git \ && cd xz \ && cmake -B build . \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/xz-cache" cmake --install build \ + && DESTDIR=/usr/src/xz-cache cmake --install build \ && cd .. \ && rm -rf xz @@ -120,7 +118,7 @@ RUN git clone -b v30.2 --depth=1 --recursive --shallow-submodules https://github -Dprotobuf_BUILD_LIBPROTOC=ON \ -Dprotobuf_WITH_ZLIB=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/protobuf-cache" cmake --install build \ + && DESTDIR=/usr/src/protobuf-cache cmake --install build \ && cd .. \ && rm -rf protobuf @@ -131,7 +129,7 @@ RUN git clone -b lcms2.15 --depth=1 https://github.com/mm2/Little-CMS.git \ --buildtype=plain \ --default-library=static \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/lcms2-cache" meson install -C build \ + && DESTDIR=/usr/src/lcms2-cache meson install -C build \ && cd .. \ && rm -rf Little-CMS @@ -142,7 +140,7 @@ RUN git clone -b v1.1.0 --depth=1 https://github.com/google/brotli.git \ -DBUILD_SHARED_LIBS=OFF \ -DBROTLI_DISABLE_TESTS=ON \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/brotli-cache" cmake --install build \ + && DESTDIR=/usr/src/brotli-cache cmake --install build \ && cd .. \ && rm -rf brotli @@ -154,7 +152,7 @@ RUN git clone -b 1.0.7 --depth=1 https://github.com/google/highway.git \ -DHWY_ENABLE_CONTRIB=OFF \ -DHWY_ENABLE_EXAMPLES=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/highway-cache" cmake --install build \ + && DESTDIR=/usr/src/highway-cache cmake --install build \ && cd .. \ && rm -rf highway @@ -163,7 +161,7 @@ RUN git clone -b v1.5.2 --depth=1 https://github.com/xiph/opus.git \ && cd opus \ && cmake -B build . \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/opus-cache" cmake --install build \ + && DESTDIR=/usr/src/opus-cache cmake --install build \ && cd .. \ && rm -rf opus @@ -176,7 +174,7 @@ RUN git clone -b 1.4.1 --depth=1 https://github.com/videolan/dav1d.git \ -Denable_tools=false \ -Denable_tests=false \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/dav1d-cache" meson install -C build \ + && DESTDIR=/usr/src/dav1d-cache meson install -C build \ && cd .. \ && rm -rf dav1d @@ -187,7 +185,7 @@ RUN git clone -b v2.4.1 --depth=1 https://github.com/cisco/openh264.git \ --buildtype=plain \ --default-library=static \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/openh264-cache" meson install -C build \ + && DESTDIR=/usr/src/openh264-cache meson install -C build \ && cd .. \ && rm -rf openh264 @@ -200,7 +198,7 @@ RUN git clone -b v1.0.15 --depth=1 https://github.com/strukturag/libde265.git \ -DENABLE_DECODER=OFF \ -DENABLE_SDL=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/de265-cache" cmake --install build \ + && DESTDIR=/usr/src/de265-cache cmake --install build \ && cd .. \ && rm -rf libde265 @@ -220,7 +218,7 @@ RUN git init libvpx \ --enable-webm-io \ --size-limit=4096x4096 \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/vpx-cache" install \ + && make DESTDIR=/usr/src/vpx-cache install \ && cd .. \ && rm -rf libvpx @@ -238,25 +236,26 @@ RUN git clone -b chrome-m116-5845 --depth=1 https://github.com/webmproject/libwe -DWEBP_BUILD_WEBPINFO=OFF \ -DWEBP_BUILD_EXTRAS=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/webp-cache" cmake --install build \ + && DESTDIR=/usr/src/webp-cache cmake --install build \ && cd .. \ && rm -rf libwebp FROM builder AS avif -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / RUN git clone -b v1.0.4 --depth=1 https://github.com/AOMediaCodec/libavif.git \ && cd libavif \ + && sed -i 's/BUILD_SHARED_LIBS OR VCPKG_TARGET_TRIPLET/TRUE/' CMakeLists.txt \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ -DAVIF_CODEC_DAV1D=ON \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/avif-cache" cmake --install build \ + && DESTDIR=/usr/src/avif-cache cmake --install build \ && cd .. \ && rm -rf libavif FROM builder AS heif -COPY --link --from=de265 {{ LibrariesPath }}/de265-cache / +COPY --link --from=de265 /usr/src/de265-cache / RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ && cd libheif \ @@ -274,14 +273,14 @@ RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ -DWITH_DAV1D=OFF \ -DWITH_EXAMPLES=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/heif-cache" cmake --install build \ + && DESTDIR=/usr/src/heif-cache cmake --install build \ && cd .. \ && rm -rf libheif FROM builder AS jxl -COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / -COPY --link --from=highway {{ LibrariesPath }}/highway-cache / +COPY --link --from=lcms2 /usr/src/lcms2-cache / +COPY --link --from=brotli /usr/src/brotli-cache / +COPY --link --from=highway /usr/src/highway-cache / RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ && cd libjxl \ @@ -301,7 +300,7 @@ RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ -DJPEGXL_ENABLE_OPENEXR=OFF \ -DJPEGXL_ENABLE_SKCMS=OFF \ && cmake --build build \ - && export DESTDIR="{{ LibrariesPath }}/jxl-cache" \ + && export DESTDIR=/usr/src/jxl-cache \ && cmake --install build \ && rm $DESTDIR/usr/local/lib64/libjpeg.so* \ && cp build/lib/libjpegli-static.a $DESTDIR/usr/local/lib64/libjpeg.a \ @@ -312,11 +311,12 @@ RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ && rm -rf libjxl FROM builder AS rnnoise -RUN git clone -b master --depth=1 https://github.com/desktop-app/rnnoise.git \ +RUN git clone -b v0.2 --depth=1 https://github.com/xiph/rnnoise.git \ && cd rnnoise \ - && cmake -B build . \ - && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/rnnoise-cache" cmake --install build \ + && ./autogen.sh \ + && ./configure --enable-static --disable-shared \ + && make -j$(nproc) \ + && make DESTDIR=/usr/src/rnnoise-cache install \ && cd .. \ && rm -rf rnnoise @@ -325,18 +325,18 @@ RUN git clone -b xcb-proto-1.16.0 --depth=1 https://github.com/gitlab-freedeskto && cd xcbproto \ && ./autogen.sh \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-proto-cache" install \ + && make DESTDIR=/usr/src/xcb-proto-cache install \ && cd .. \ && rm -rf xcbproto FROM builder AS xcb -COPY --link --from=xcb-proto {{ LibrariesPath }}/xcb-proto-cache / +COPY --link --from=xcb-proto /usr/src/xcb-proto-cache / RUN git clone -b libxcb-1.16 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxcb.git \ && cd libxcb \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && export DESTDIR="{{ LibrariesPath }}/xcb-cache" \ + && export DESTDIR=/usr/src/xcb-cache \ && make install \ && rm $DESTDIR/usr/local/lib/{libxcb.{,l}a,pkgconfig/xcb.pc} \ && cd .. \ @@ -347,7 +347,7 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules ht && cd libxcb-wm \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-wm-cache" install \ + && make DESTDIR=/usr/src/xcb-wm-cache install \ && cd .. \ && rm -rf libxcb-wm @@ -356,18 +356,18 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules https && cd libxcb-util \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-util-cache" install \ + && make DESTDIR=/usr/src/xcb-util-cache install \ && cd .. \ && rm -rf libxcb-util FROM builder AS xcb-image -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \ && cd libxcb-image \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-image-cache" install \ + && make DESTDIR=/usr/src/xcb-image-cache install \ && cd .. \ && rm -rf libxcb-image @@ -376,7 +376,7 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul && cd libxcb-keysyms \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-keysyms-cache" install \ + && make DESTDIR=/usr/src/xcb-keysyms-cache install \ && cd .. \ && rm -rf libxcb-keysyms @@ -385,20 +385,20 @@ RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-subm && cd libxcb-render-util \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-render-util-cache" install \ + && make DESTDIR=/usr/src/xcb-render-util-cache install \ && cd .. \ && rm -rf libxcb-render-util FROM builder AS xcb-cursor -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / -COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / +COPY --link --from=xcb-image /usr/src/xcb-image-cache / +COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \ && cd libxcb-cursor \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcb-cursor-cache" install \ + && make DESTDIR=/usr/src/xcb-cursor-cache install \ && cd .. \ && rm -rf libxcb-cursor @@ -407,7 +407,7 @@ RUN git clone -b libXext-1.3.5 --depth=1 https://github.com/gitlab-freedesktop-m && cd libxext \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xext-cache" install \ + && make DESTDIR=/usr/src/xext-cache install \ && cd .. \ && rm -rf libxext @@ -416,7 +416,7 @@ RUN git clone -b libXtst-1.2.4 --depth=1 https://github.com/gitlab-freedesktop-m && cd libxtst \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xtst-cache" install \ + && make DESTDIR=/usr/src/xtst-cache install \ && cd .. \ && rm -rf libxtst @@ -425,18 +425,18 @@ RUN git clone -b libXfixes-5.0.3 --depth=1 https://github.com/gitlab-freedesktop && cd libxfixes \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xfixes-cache" install \ + && make DESTDIR=/usr/src/xfixes-cache install \ && cd .. \ && rm -rf libxfixes FROM builder AS xv -COPY --link --from=xext {{ LibrariesPath }}/xext-cache / +COPY --link --from=xext /usr/src/xext-cache / RUN git clone -b libXv-1.0.12 --depth=1 https://github.com/gitlab-freedesktop-mirrors/libxv.git \ && cd libxv \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xv-cache" install \ + && make DESTDIR=/usr/src/xv-cache install \ && cd .. \ && rm -rf libxv @@ -445,7 +445,7 @@ RUN git clone -b libXrandr-1.5.3 --depth=1 https://github.com/gitlab-freedesktop && cd libxrandr \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xrandr-cache" install \ + && make DESTDIR=/usr/src/xrandr-cache install \ && cd .. \ && rm -rf libxrandr @@ -454,7 +454,7 @@ RUN git clone -b libXrender-0.9.11 --depth=1 https://github.com/gitlab-freedeskt && cd libxrender \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xrender-cache" install \ + && make DESTDIR=/usr/src/xrender-cache install \ && cd .. \ && rm -rf libxrender @@ -463,7 +463,7 @@ RUN git clone -b libXdamage-1.1.6 --depth=1 https://github.com/gitlab-freedeskto && cd libxdamage \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xdamage-cache" install \ + && make DESTDIR=/usr/src/xdamage-cache install \ && cd .. \ && rm -rf libxdamage @@ -472,7 +472,7 @@ RUN git clone -b libXcomposite-0.4.6 --depth=1 https://github.com/gitlab-freedes && cd libxcomposite \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/xcomposite-cache" install \ + && make DESTDIR=/usr/src/xcomposite-cache install \ && cd .. \ && rm -rf libxcomposite @@ -487,25 +487,25 @@ RUN git clone -b 1.19.0 --depth=1 https://github.com/gitlab-freedesktop-mirrors/ -Ddtd_validation=false \ -Dicon_directory=/usr/share/icons \ && meson compile -C build src/wayland-scanner \ - && mkdir -p "{{ LibrariesPath }}/wayland-cache/usr/local/bin" "{{ LibrariesPath }}/wayland-cache/usr/local/lib64/pkgconfig" \ - && cp build/src/wayland-scanner "{{ LibrariesPath }}/wayland-cache/usr/local/bin" \ - && sed 's@bindir=${prefix}/bin@bindir=${prefix}/local/bin@;s/1.21.0/1.19.0/' /usr/lib64/pkgconfig/wayland-scanner.pc > "{{ LibrariesPath }}/wayland-cache/usr/local/lib64/pkgconfig/wayland-scanner.pc" \ + && mkdir -p "/usr/src/wayland-cache/usr/local/bin" "/usr/src/wayland-cache/usr/local/lib64/pkgconfig" \ + && cp build/src/wayland-scanner "/usr/src/wayland-cache/usr/local/bin" \ + && sed 's@bindir=${prefix}/bin@bindir=${prefix}/local/bin@;s/1.21.0/1.19.0/' /usr/lib64/pkgconfig/wayland-scanner.pc > "/usr/src/wayland-cache/usr/local/lib64/pkgconfig/wayland-scanner.pc" \ && cd .. \ && rm -rf wayland FROM builder AS nv-codec-headers RUN git clone -b n12.1.14.0 --depth=1 https://github.com/FFmpeg/nv-codec-headers.git \ - && DESTDIR="{{ LibrariesPath }}/nv-codec-headers-cache" make -C nv-codec-headers install \ + && DESTDIR=/usr/src/nv-codec-headers-cache make -C nv-codec-headers install \ && rm -rf nv-codec-headers FROM builder AS ffmpeg -COPY --link --from=opus {{ LibrariesPath }}/opus-cache / -COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / -COPY --link --from=xext {{ LibrariesPath }}/xext-cache / -COPY --link --from=xv {{ LibrariesPath }}/xv-cache / -COPY --link --from=nv-codec-headers {{ LibrariesPath }}/nv-codec-headers-cache / +COPY --link --from=opus /usr/src/opus-cache / +COPY --link --from=openh264 /usr/src/openh264-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / +COPY --link --from=vpx /usr/src/vpx-cache / +COPY --link --from=xext /usr/src/xext-cache / +COPY --link --from=xv /usr/src/xv-cache / +COPY --link --from=nv-codec-headers /usr/src/nv-codec-headers-cache / RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ && cd FFmpeg \ @@ -645,7 +645,7 @@ RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ --enable-muxer=opus \ --enable-muxer=wav \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/ffmpeg-cache" install \ + && make DESTDIR=/usr/src/ffmpeg-cache install \ && cd .. \ && rm -rf ffmpeg @@ -659,12 +659,12 @@ RUN git clone -b 0.3.62 --depth=1 https://github.com/PipeWire/pipewire.git \ -Dsession-managers=media-session \ -Dspa-plugins=disabled \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/pipewire-cache" meson install -C build \ + && DESTDIR=/usr/src/pipewire-cache meson install -C build \ && cd .. \ && rm -rf pipewire FROM builder AS openal -COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / +COPY --link --from=pipewire /usr/src/pipewire-cache / RUN git clone -b 1.24.3 --depth=1 https://github.com/kcat/openal-soft.git \ && cd openal-soft \ @@ -674,7 +674,7 @@ RUN git clone -b 1.24.3 --depth=1 https://github.com/kcat/openal-soft.git \ -DALSOFT_UTILS=OFF \ -DALSOFT_INSTALL_CONFIG=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/openal-cache" cmake --install build \ + && DESTDIR=/usr/src/openal-cache cmake --install build \ && cd .. \ && rm -rf openal-soft @@ -687,12 +687,12 @@ RUN git clone -b openssl-3.2.1 --depth=1 https://github.com/openssl/openssl.git no-tests \ no-dso \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/openssl-cache" install_sw \ + && make DESTDIR=/usr/src/openssl-cache install_sw \ && cd .. \ && rm -rf openssl FROM builder AS xkbcommon -COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / +COPY --link --from=xcb /usr/src/xcb-cache / RUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbcommon.git \ && cd libxkbcommon \ @@ -706,25 +706,25 @@ RUN git clone -b xkbcommon-1.6.0 --depth=1 https://github.com/xkbcommon/libxkbco -Dxkb-config-extra-path=/etc/xkb \ -Dx-locale-root=/usr/share/X11/locale \ && meson compile -C build \ - && DESTDIR="{{ LibrariesPath }}/xkbcommon-cache" meson install -C build \ + && DESTDIR=/usr/src/xkbcommon-cache meson install -C build \ && cd .. \ && rm -rf libxkbcommon FROM patches AS qt -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=webp {{ LibrariesPath }}/webp-cache / -COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / -COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / -COPY --link --from=xcb-wm {{ LibrariesPath }}/xcb-wm-cache / -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / -COPY --link --from=xcb-keysyms {{ LibrariesPath }}/xcb-keysyms-cache / -COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -COPY --link --from=xcb-cursor {{ LibrariesPath }}/xcb-cursor-cache / -COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=lcms2 /usr/src/lcms2-cache / +COPY --link --from=webp /usr/src/webp-cache / +COPY --link --from=jxl /usr/src/jxl-cache / +COPY --link --from=xcb /usr/src/xcb-cache / +COPY --link --from=xcb-wm /usr/src/xcb-wm-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / +COPY --link --from=xcb-image /usr/src/xcb-image-cache / +COPY --link --from=xcb-keysyms /usr/src/xcb-keysyms-cache / +COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / +COPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache / +COPY --link --from=wayland /usr/src/wayland-cache / +COPY --link --from=openssl /usr/src/openssl-cache / +COPY --link --from=xkbcommon /usr/src/xkbcommon-cache / ENV QT=6.9.1 RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ @@ -739,15 +739,17 @@ RUN git clone -b v$QT --depth=1 https://github.com/qt/qt5.git \ -DCMAKE_INSTALL_PREFIX=/usr/local \ -DBUILD_SHARED_LIBS=OFF \ -DQT_GENERATE_SBOM=OFF \ + -DQT_QPA_PLATFORMS="wayland;xcb" \ -DINPUT_libpng=qt \ -DINPUT_harfbuzz=qt \ -DINPUT_pcre=qt \ -DFEATURE_icu=OFF \ -DFEATURE_xcb_sm=OFF \ + -DFEATURE_eglfs=OFF \ -DINPUT_dbus=runtime \ -DINPUT_openssl=linked \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/qt-cache" cmake --install build \ + && DESTDIR=/usr/src/qt-cache cmake --install build \ && cd .. \ && rm -rf qt5 @@ -757,27 +759,27 @@ RUN git clone -b v2024.02.16 --depth=1 https://chromium.googlesource.com/breakpa && git clone -b v2024.02.01 --depth=1 https://chromium.googlesource.com/linux-syscall-support.git src/third_party/lss \ && CFLAGS="$CFLAGS -fno-lto" CXXFLAGS="$CXXFLAGS -fno-lto" ./configure \ && make -j$(nproc) \ - && make DESTDIR="{{ LibrariesPath }}/breakpad-cache" install \ + && make DESTDIR=/usr/src/breakpad-cache install \ && cd .. \ && rm -rf breakpad FROM builder AS webrtc -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=opus {{ LibrariesPath }}/opus-cache / -COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / -COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / -COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=xext {{ LibrariesPath }}/xext-cache / -COPY --link --from=xfixes {{ LibrariesPath }}/xfixes-cache / -COPY --link --from=xtst {{ LibrariesPath }}/xtst-cache / -COPY --link --from=xrandr {{ LibrariesPath }}/xrandr-cache / -COPY --link --from=xrender {{ LibrariesPath }}/xrender-cache / -COPY --link --from=xdamage {{ LibrariesPath }}/xdamage-cache / -COPY --link --from=xcomposite {{ LibrariesPath }}/xcomposite-cache / -COPY --link --from=pipewire {{ LibrariesPath }}/pipewire-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=opus /usr/src/opus-cache / +COPY --link --from=openh264 /usr/src/openh264-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / +COPY --link --from=vpx /usr/src/vpx-cache / +COPY --link --from=jxl /usr/src/jxl-cache / +COPY --link --from=ffmpeg /usr/src/ffmpeg-cache / +COPY --link --from=openssl /usr/src/openssl-cache / +COPY --link --from=xext /usr/src/xext-cache / +COPY --link --from=xfixes /usr/src/xfixes-cache / +COPY --link --from=xtst /usr/src/xtst-cache / +COPY --link --from=xrandr /usr/src/xrandr-cache / +COPY --link --from=xrender /usr/src/xrender-cache / +COPY --link --from=xdamage /usr/src/xdamage-cache / +COPY --link --from=xcomposite /usr/src/xcomposite-cache / +COPY --link --from=pipewire /usr/src/pipewire-cache / # Shallow clone on a specific commit. RUN git init tg_owt \ @@ -788,7 +790,7 @@ RUN git init tg_owt \ && git submodule update --init --recursive --depth=1 \ && cmake -B build . -DTG_OWT_DLOPEN_PIPEWIRE=ON \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/webrtc-cache" cmake --install build \ + && DESTDIR=/usr/src/webrtc-cache cmake --install build \ && cd .. \ && rm -rf tg_owt @@ -800,13 +802,13 @@ RUN git clone -b v3.2.2 --depth=1 https://github.com/ada-url/ada.git \ -D ADA_TOOLS=OFF \ -D ADA_INCLUDE_URL_PATTERN=OFF \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/ada-cache" cmake --install build \ + && DESTDIR=/usr/src/ada-cache cmake --install build \ && cd .. \ && rm -rf ada FROM builder AS tde2e -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=openssl /usr/src/openssl-cache / # Shallow clone on a specific commit. RUN git init tde2e \ @@ -816,57 +818,57 @@ RUN git init tde2e \ && git reset --hard FETCH_HEAD \ && cmake -B build . -DTD_E2E_ONLY=ON \ && cmake --build build \ - && DESTDIR="{{ LibrariesPath }}/tde2e-cache" cmake --install build \ + && DESTDIR=/usr/src/tde2e-cache cmake --install build \ && cd .. \ && rm -rf tde2e FROM builder -COPY --link --from=zlib {{ LibrariesPath }}/zlib-cache / -COPY --link --from=xz {{ LibrariesPath }}/xz-cache / -COPY --link --from=protobuf {{ LibrariesPath }}/protobuf-cache / -COPY --link --from=lcms2 {{ LibrariesPath }}/lcms2-cache / -COPY --link --from=brotli {{ LibrariesPath }}/brotli-cache / -COPY --link --from=highway {{ LibrariesPath }}/highway-cache / -COPY --link --from=opus {{ LibrariesPath }}/opus-cache / -COPY --link --from=dav1d {{ LibrariesPath }}/dav1d-cache / -COPY --link --from=openh264 {{ LibrariesPath }}/openh264-cache / -COPY --link --from=de265 {{ LibrariesPath }}/de265-cache / -COPY --link --from=vpx {{ LibrariesPath }}/vpx-cache / -COPY --link --from=webp {{ LibrariesPath }}/webp-cache / -COPY --link --from=avif {{ LibrariesPath }}/avif-cache / -COPY --link --from=heif {{ LibrariesPath }}/heif-cache / -COPY --link --from=jxl {{ LibrariesPath }}/jxl-cache / -COPY --link --from=rnnoise {{ LibrariesPath }}/rnnoise-cache / -COPY --link --from=xcb {{ LibrariesPath }}/xcb-cache / -COPY --link --from=xcb-wm {{ LibrariesPath }}/xcb-wm-cache / -COPY --link --from=xcb-util {{ LibrariesPath }}/xcb-util-cache / -COPY --link --from=xcb-image {{ LibrariesPath }}/xcb-image-cache / -COPY --link --from=xcb-keysyms {{ LibrariesPath }}/xcb-keysyms-cache / -COPY --link --from=xcb-render-util {{ LibrariesPath }}/xcb-render-util-cache / -COPY --link --from=xcb-cursor {{ LibrariesPath }}/xcb-cursor-cache / -COPY --link --from=xext {{ LibrariesPath }}/xext-cache / -COPY --link --from=xfixes {{ LibrariesPath }}/xfixes-cache / -COPY --link --from=xv {{ LibrariesPath }}/xv-cache / -COPY --link --from=xtst {{ LibrariesPath }}/xtst-cache / -COPY --link --from=xrandr {{ LibrariesPath }}/xrandr-cache / -COPY --link --from=xrender {{ LibrariesPath }}/xrender-cache / -COPY --link --from=xdamage {{ LibrariesPath }}/xdamage-cache / -COPY --link --from=xcomposite {{ LibrariesPath }}/xcomposite-cache / -COPY --link --from=wayland {{ LibrariesPath }}/wayland-cache / -COPY --link --from=ffmpeg {{ LibrariesPath }}/ffmpeg-cache / -COPY --link --from=openal {{ LibrariesPath }}/openal-cache / -COPY --link --from=openssl {{ LibrariesPath }}/openssl-cache / -COPY --link --from=xkbcommon {{ LibrariesPath }}/xkbcommon-cache / -COPY --link --from=qt {{ LibrariesPath }}/qt-cache / -COPY --link --from=breakpad {{ LibrariesPath }}/breakpad-cache / -COPY --link --from=webrtc {{ LibrariesPath }}/webrtc-cache / -COPY --link --from=ada {{ LibrariesPath }}/ada-cache / -COPY --link --from=tde2e {{ LibrariesPath }}/tde2e-cache / +COPY --link --from=zlib /usr/src/zlib-cache / +COPY --link --from=xz /usr/src/xz-cache / +COPY --link --from=protobuf /usr/src/protobuf-cache / +COPY --link --from=lcms2 /usr/src/lcms2-cache / +COPY --link --from=brotli /usr/src/brotli-cache / +COPY --link --from=highway /usr/src/highway-cache / +COPY --link --from=opus /usr/src/opus-cache / +COPY --link --from=dav1d /usr/src/dav1d-cache / +COPY --link --from=openh264 /usr/src/openh264-cache / +COPY --link --from=de265 /usr/src/de265-cache / +COPY --link --from=vpx /usr/src/vpx-cache / +COPY --link --from=webp /usr/src/webp-cache / +COPY --link --from=avif /usr/src/avif-cache / +COPY --link --from=heif /usr/src/heif-cache / +COPY --link --from=jxl /usr/src/jxl-cache / +COPY --link --from=rnnoise /usr/src/rnnoise-cache / +COPY --link --from=xcb /usr/src/xcb-cache / +COPY --link --from=xcb-wm /usr/src/xcb-wm-cache / +COPY --link --from=xcb-util /usr/src/xcb-util-cache / +COPY --link --from=xcb-image /usr/src/xcb-image-cache / +COPY --link --from=xcb-keysyms /usr/src/xcb-keysyms-cache / +COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / +COPY --link --from=xcb-cursor /usr/src/xcb-cursor-cache / +COPY --link --from=xext /usr/src/xext-cache / +COPY --link --from=xfixes /usr/src/xfixes-cache / +COPY --link --from=xv /usr/src/xv-cache / +COPY --link --from=xtst /usr/src/xtst-cache / +COPY --link --from=xrandr /usr/src/xrandr-cache / +COPY --link --from=xrender /usr/src/xrender-cache / +COPY --link --from=xdamage /usr/src/xdamage-cache / +COPY --link --from=xcomposite /usr/src/xcomposite-cache / +COPY --link --from=wayland /usr/src/wayland-cache / +COPY --link --from=ffmpeg /usr/src/ffmpeg-cache / +COPY --link --from=openal /usr/src/openal-cache / +COPY --link --from=openssl /usr/src/openssl-cache / +COPY --link --from=xkbcommon /usr/src/xkbcommon-cache / +COPY --link --from=qt /usr/src/qt-cache / +COPY --link --from=breakpad /usr/src/breakpad-cache / +COPY --link --from=webrtc /usr/src/webrtc-cache / +COPY --link --from=ada /usr/src/ada-cache / +COPY --link --from=tde2e /usr/src/tde2e-cache / -COPY --link --from=patches {{ LibrariesPath }}/patches patches +COPY --link --from=patches /usr/src/patches patches RUN patch -p1 -d /usr/lib64/gobject-introspection -i $PWD/patches/gobject-introspection.patch && rm -rf patches -WORKDIR ../tdesktop +WORKDIR /usr/src/tdesktop ENV BOOST_INCLUDEDIR=/usr/include/boost1.78 ENV BOOST_LIBRARYDIR=/usr/lib64/boost1.78 From a532067a933776cc3932565f5d2d1467e7f01020 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 20 May 2025 22:20:19 +0300 Subject: [PATCH 037/310] Fixed dismissing of custom pending suggestions. --- Telegram/SourceFiles/data/components/promo_suggestions.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/components/promo_suggestions.cpp b/Telegram/SourceFiles/data/components/promo_suggestions.cpp index fd6613815c..99ec5f5b2f 100644 --- a/Telegram/SourceFiles/data/components/promo_suggestions.cpp +++ b/Telegram/SourceFiles/data/components/promo_suggestions.cpp @@ -237,7 +237,9 @@ void PromoSuggestions::invalidate() { } std::optional PromoSuggestions::custom() const { - return _custom; + return (_custom && !_dismissedSuggestions.contains(_custom->suggestion)) + ? _custom + : std::nullopt; } void PromoSuggestions::requestContactBirthdays(Fn done, bool force) { From b0125e81652d75a3c035ebbd3defc5d50921e15b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 20 May 2025 23:56:40 +0300 Subject: [PATCH 038/310] Slightly improved display of numbers approaching zero in stats charts. --- Telegram/SourceFiles/statistics/chart_rulers_data.cpp | 2 +- Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp index 4f9471c8ce..6241901804 100644 --- a/Telegram/SourceFiles/statistics/chart_rulers_data.cpp +++ b/Telegram/SourceFiles/statistics/chart_rulers_data.cpp @@ -22,7 +22,7 @@ constexpr auto kStep = 5.; } [[nodiscard]] QString Format(ChartValue absoluteValue) { - constexpr auto kTooMuch = ChartValue(10'000); + static constexpr auto kTooMuch = ChartValue(10'000); return (absoluteValue >= kTooMuch) ? Lang::FormatCountToShort(absoluteValue).string : QString::number(absoluteValue); diff --git a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp index 8a94d10844..fc8d98a653 100644 --- a/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp +++ b/Telegram/SourceFiles/statistics/view/chart_rulers_view.cpp @@ -20,8 +20,11 @@ namespace Statistic { namespace { [[nodiscard]] QString FormatF(float64 absoluteValue) { - constexpr auto kTooMuch = int(10'000); - return (absoluteValue >= kTooMuch) + static constexpr auto kTooMuch = int(10'000); + static constexpr auto kTooSmall = 1e-9; + return (std::abs(absoluteValue) <= kTooSmall) + ? u"0"_q + : (absoluteValue >= kTooMuch) ? Lang::FormatCountToShort(absoluteValue).string : QString::number(absoluteValue); } From 0e44de2fe33f342cb8eabde868d9714cdb47214a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 21 May 2025 09:16:31 +0300 Subject: [PATCH 039/310] Slightly improved style of exception button in notifications settings. --- Telegram/SourceFiles/ui/menu_icons.style | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 48c2b0d8fa..c54c168f81 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -233,7 +233,7 @@ menuIconCancelAttention: icon {{ "menu/cancel", menuIconAttentionColor }}; menuIconBlockAttention: icon {{ "menu/block", menuIconAttentionColor }}; menuIconBlockSettings: icon {{ "menu/block", windowBgActive }}; -menuIconInviteSettings: icon {{ "menu/invite", windowBgActive }}; +menuIconInviteSettings: icon {{ "menu/invite", lightButtonFg }}; playerSpeedSlow: icon {{ "player/speed/audiospeed_menu_0.5", menuIconColor }}; playerSpeedSlowActive: icon {{ "player/speed/audiospeed_menu_0.5", mediaPlayerActiveFg }}; From 5b9e24f3f4ba34662a29b858c775afd0f2a88f3c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 May 2025 15:44:09 +0300 Subject: [PATCH 040/310] Slightly improved box for writing captions to be more generic. --- .../boxes/send_gif_with_caption_box.cpp | 25 ++++++++++++------- .../boxes/send_gif_with_caption_box.h | 6 +++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index b271028766..310f8fbd8e 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -226,9 +226,8 @@ namespace { } // namespace -void SendGifWithCaptionBox( +void CaptionBox( not_null box, - not_null document, not_null peer, const SendMenu::Details &details, Fn done) { @@ -237,17 +236,10 @@ void SendGifWithCaptionBox( if (!controller) { return; } - box->setTitle(tr::lng_send_gif_with_caption()); box->setWidth(st::boxWidth); box->getDelegate()->setStyle(st::sendGifBox); const auto container = box->verticalLayout(); - [[maybe_unused]] const auto gifWidget = AddGifWidget( - container, - document, - st::boxWidth); - - Ui::AddSkip(container); const auto input = AddInputField(box, controller); box->setFocusCallback([=] { @@ -339,4 +331,19 @@ void SendGifWithCaptionBox( ) | rpl::start_with_next([=] { send({}); }, input->lifetime()); } +void SendGifWithCaptionBox( + not_null box, + not_null document, + not_null peer, + const SendMenu::Details &details, + Fn done) { + box->setTitle(tr::lng_send_gif_with_caption()); + [[maybe_unused]] const auto gifWidget = AddGifWidget( + box->verticalLayout(), + document, + st::boxWidth); + Ui::AddSkip(box->verticalLayout()); + CaptionBox(box, peer, details, std::move(done)); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h index 0247cf5e3d..74f6594b59 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h @@ -22,6 +22,12 @@ namespace Ui { class GenericBox; +void CaptionBox( + not_null box, + not_null peer, + const SendMenu::Details &details, + Fn done); + void SendGifWithCaptionBox( not_null box, not_null document, From 5ac373d4aa42b04b71fbc1897e3ef102db61dd57 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 May 2025 16:22:09 +0300 Subject: [PATCH 041/310] Added simple box to edit caption of single file while it's uploading. --- Telegram/Resources/langs/lang.strings | 2 + .../boxes/send_gif_with_caption_box.cpp | 45 ++++++++++++++++--- .../boxes/send_gif_with_caption_box.h | 10 +++-- .../history/history_inner_widget.cpp | 23 +++++++++- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 89211b5e01..71e56d6269 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4197,6 +4197,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_pin_msg" = "Pin"; "lng_context_unpin_msg" = "Unpin"; "lng_context_cancel_upload" = "Cancel Upload"; +"lng_context_upload_edit_caption" = "Edit Caption"; +"lng_context_upload_edit_caption_error" = "Sorry, the file is already uploaded."; "lng_context_copy_selected" = "Copy Selected Text"; "lng_context_copy_selected_items" = "Copy Selected as Text"; "lng_context_forward_selected" = "Forward Selected"; diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index 310f8fbd8e..88dc350c11 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -24,7 +24,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" +#include "history/history.h" +#include "history/history_item.h" #include "history/view/controls/history_view_characters_limit.h" +#include "history/view/history_view_message.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "media/clip/media_clip_reader.h" @@ -32,10 +35,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/emoji_button.h" #include "ui/controls/emoji_button_factory.h" #include "ui/layers/generic_box.h" -#include "ui/widgets/fields/input_field.h" #include "ui/rect.h" +#include "ui/text/text_entity.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" @@ -224,10 +228,9 @@ namespace { return input; } -} // namespace - void CaptionBox( not_null box, + rpl::producer confirmText, not_null peer, const SendMenu::Details &details, Fn done) { @@ -310,7 +313,7 @@ void CaptionBox( done(std::move(options), input->getTextWithTags()); }; const auto confirm = box->addButton( - tr::lng_send_button(), + std::move(confirmText), [=] { send({}); }); SendMenu::SetupMenuAndShortcuts( confirm, @@ -331,6 +334,8 @@ void CaptionBox( ) | rpl::start_with_next([=] { send({}); }, input->lifetime()); } +} // namespace + void SendGifWithCaptionBox( not_null box, not_null document, @@ -343,7 +348,37 @@ void SendGifWithCaptionBox( document, st::boxWidth); Ui::AddSkip(box->verticalLayout()); - CaptionBox(box, peer, details, std::move(done)); + CaptionBox(box, tr::lng_send_button(), peer, details, std::move(done)); +} + +void EditCaptionBox( + not_null box, + not_null view) { + const auto window = Core::App().findWindow(box); + Assert(window != nullptr); + const auto controller = window->sessionController(); + Assert(controller != nullptr); + box->setTitle(tr::lng_context_upload_edit_caption()); + + const auto item = view->data(); + const auto peer = item->history()->peer; + + auto done = [=](Api::SendOptions, TextWithTags textWithTags) { + if (item->isUploading()) { + item->setText({ + base::take(textWithTags.text), + TextUtilities::ConvertTextTagsToEntities( + base::take(textWithTags.tags)), + }); + peer->owner().requestViewResize(view); + box->closeBox(); + } else { + controller->showToast( + tr::lng_context_upload_edit_caption_error(tr::now)); + } + }; + + CaptionBox(box, tr::lng_settings_save(), peer, {}, std::move(done)); } } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h index 74f6594b59..1730c86714 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h @@ -18,15 +18,17 @@ namespace SendMenu { struct Details; } // namespace SendMenu +namespace HistoryView { +class Element; +} // namespace HistoryView + namespace Ui { class GenericBox; -void CaptionBox( +void EditCaptionBox( not_null box, - not_null peer, - const SendMenu::Details &details, - Fn done); + not_null view); void SendGifWithCaptionBox( not_null box, diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 1edd5850c0..4946e4929e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" -#include "boxes/moderate_messages_box.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions.h" @@ -54,7 +53,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/statistics/info_statistics_widget.h" #include "boxes/about_sponsored_box.h" #include "boxes/delete_messages_box.h" +#include "boxes/moderate_messages_box.h" #include "boxes/report_messages_box.h" +#include "boxes/send_gif_with_caption_box.h" #include "boxes/star_gift_box.h" // ShowStarGiftBox #include "boxes/sticker_set_box.h" #include "boxes/translate_box.h" @@ -2718,6 +2719,16 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (item->canDelete()) { const auto callback = [=] { deleteItem(itemId); }; if (item->isUploading()) { + _menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { + if (const auto view = viewByItem(item)) { + controller->uiShow()->show(Box( + Ui::EditCaptionBox, + view)); + } + }, + &st::menuIconEdit); _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( @@ -2962,6 +2973,16 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { deleteAsGroup(itemId); }; if (item->isUploading()) { + _menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { + if (const auto view = viewByItem(item)) { + controller->uiShow()->show(Box( + Ui::EditCaptionBox, + view)); + } + }, + &st::menuIconEdit); _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( From adc1ee71a9b1bc9549deb2c6aa6252ea149591ce Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 May 2025 17:25:32 +0300 Subject: [PATCH 042/310] Slightly improved box to edit caption of grouped file. --- .../boxes/send_gif_with_caption_box.cpp | 26 +++++++++++++++---- Telegram/SourceFiles/data/data_groups.cpp | 2 +- .../SourceFiles/storage/localimageloader.cpp | 7 +++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index 88dc350c11..1eaf9f6c4f 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" +#include "data/data_groups.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.h" #include "data/data_session.h" @@ -231,6 +232,7 @@ namespace { void CaptionBox( not_null box, rpl::producer confirmText, + TextWithTags initialText, not_null peer, const SendMenu::Details &details, Fn done) { @@ -249,6 +251,7 @@ void CaptionBox( input->setFocus(); }); + input->setTextWithTags(std::move(initialText)); input->setSubmitSettings(Core::App().settings().sendSubmitWay()); InitMessageField(controller, input, [=](not_null) { return true; @@ -341,14 +344,14 @@ void SendGifWithCaptionBox( not_null document, not_null peer, const SendMenu::Details &details, - Fn done) { + Fn c) { box->setTitle(tr::lng_send_gif_with_caption()); [[maybe_unused]] const auto gifWidget = AddGifWidget( box->verticalLayout(), document, st::boxWidth); Ui::AddSkip(box->verticalLayout()); - CaptionBox(box, tr::lng_send_button(), peer, details, std::move(done)); + CaptionBox(box, tr::lng_send_button(), {}, peer, details, std::move(c)); } void EditCaptionBox( @@ -363,14 +366,18 @@ void EditCaptionBox( const auto item = view->data(); const auto peer = item->history()->peer; + using namespace TextUtilities; + auto done = [=](Api::SendOptions, TextWithTags textWithTags) { if (item->isUploading()) { item->setText({ base::take(textWithTags.text), - TextUtilities::ConvertTextTagsToEntities( - base::take(textWithTags.tags)), + ConvertTextTagsToEntities(base::take(textWithTags.tags)), }); peer->owner().requestViewResize(view); + if (item->groupId()) { + peer->owner().groups().refreshMessage(item, true); + } box->closeBox(); } else { controller->showToast( @@ -378,7 +385,16 @@ void EditCaptionBox( } }; - CaptionBox(box, tr::lng_settings_save(), peer, {}, std::move(done)); + CaptionBox( + box, + tr::lng_settings_save(), + TextWithTags{ + .text = item->originalText().text, + .tags = ConvertEntitiesToTextTags(item->originalText().entities), + }, + peer, + {}, + std::move(done)); } } // namespace Ui diff --git a/Telegram/SourceFiles/data/data_groups.cpp b/Telegram/SourceFiles/data/data_groups.cpp index 15bb0d820e..7af7050f14 100644 --- a/Telegram/SourceFiles/data/data_groups.cpp +++ b/Telegram/SourceFiles/data/data_groups.cpp @@ -84,7 +84,7 @@ void Groups::refreshMessage( _data->requestItemViewRefresh(item); return; } - if (!item->isRegular() && !item->isScheduled()) { + if (!item->isRegular() && !item->isScheduled() && !item->isUploading()) { return; } const auto groupId = item->groupId(); diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 476a1521b9..8603cd7c30 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -404,12 +404,15 @@ void SendingAlbum::removeItem(not_null item) { Assert(i != end(items)); items.erase(i); if (moveCaption) { - const auto caption = item->originalText(); + auto caption = item->originalText(); const auto firstId = items.front().msgId; if (const auto first = item->history()->owner().message(firstId)) { // We don't need to finishEdition() here, because the whole // album will be rebuilt after one item was removed from it. - first->setText(caption); + auto firstCaption = first->originalText(); + first->setText(firstCaption.text.isEmpty() + ? std::move(caption) + : firstCaption.append('\n').append(std::move(caption))); refreshMediaCaption(first); } } From 81b432140c4db05edfe1c682be00d8ae1c4d9bf7 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 May 2025 18:08:01 +0300 Subject: [PATCH 043/310] Added ability to edit caption from box even when file is uploaded. --- Telegram/Resources/langs/lang.strings | 1 - .../boxes/send_gif_with_caption_box.cpp | 64 +++++++++++++------ .../history/history_inner_widget.cpp | 42 ++++++------ .../history/history_inner_widget.h | 1 + 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 71e56d6269..6c8d3e117a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4198,7 +4198,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_unpin_msg" = "Unpin"; "lng_context_cancel_upload" = "Cancel Upload"; "lng_context_upload_edit_caption" = "Edit Caption"; -"lng_context_upload_edit_caption_error" = "Sorry, the file is already uploaded."; "lng_context_copy_selected" = "Copy Selected Text"; "lng_context_copy_selected_items" = "Copy Selected as Text"; "lng_context_forward_selected" = "Forward Selected"; diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index 1eaf9f6c4f..b94f000768 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/send_gif_with_caption_box.h" +#include "api/api_editing.h" #include "base/event_filter.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/field_autocomplete.h" @@ -357,34 +358,59 @@ void SendGifWithCaptionBox( void EditCaptionBox( not_null box, not_null view) { - const auto window = Core::App().findWindow(box); - Assert(window != nullptr); - const auto controller = window->sessionController(); - Assert(controller != nullptr); - box->setTitle(tr::lng_context_upload_edit_caption()); - - const auto item = view->data(); - const auto peer = item->history()->peer; - using namespace TextUtilities; - auto done = [=](Api::SendOptions, TextWithTags textWithTags) { + box->setTitle(tr::lng_context_upload_edit_caption()); + + const auto data = &view->data()->history()->peer->owner(); + + struct State { + FullMsgId fullId; + }; + const auto state = box->lifetime().make_state(); + state->fullId = view->data()->fullId(); + + data->itemIdChanged( + ) | rpl::start_with_next([=](Data::Session::IdChange event) { + if (event.oldId == state->fullId.msg) { + state->fullId = event.newId; + } + }, box->lifetime()); + + auto done = [=, show = box->uiShow()]( + Api::SendOptions, + TextWithTags textWithTags) { + const auto item = data->message(state->fullId); + if (!item) { + show->showToast(tr::lng_message_not_found(tr::now)); + return; + } + if (!(item->media() && item->media()->allowsEditCaption())) { + show->showToast(tr::lng_edit_error(tr::now)); + return; + } + auto text = TextWithEntities{ + base::take(textWithTags.text), + ConvertTextTagsToEntities(base::take(textWithTags.tags)), + }; if (item->isUploading()) { - item->setText({ - base::take(textWithTags.text), - ConvertTextTagsToEntities(base::take(textWithTags.tags)), - }); - peer->owner().requestViewResize(view); + item->setText(std::move(text)); + data->requestViewResize(view); if (item->groupId()) { - peer->owner().groups().refreshMessage(item, true); + data->groups().refreshMessage(item, true); } box->closeBox(); } else { - controller->showToast( - tr::lng_context_upload_edit_caption_error(tr::now)); + Api::EditCaption( + item, + std::move(text), + { .invertCaption = item->invertMedia() }, + [=] { box->closeBox(); }, + [=](const QString &e) { box->uiShow()->showToast(e); }); } }; + const auto item = view->data(); CaptionBox( box, tr::lng_settings_save(), @@ -392,7 +418,7 @@ void EditCaptionBox( .text = item->originalText().text, .tags = ConvertEntitiesToTextTags(item->originalText().entities), }, - peer, + item->history()->peer, {}, std::move(done)); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 4946e4929e..dd6031275a 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2719,16 +2719,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (item->canDelete()) { const auto callback = [=] { deleteItem(itemId); }; if (item->isUploading()) { - _menu->addAction( - tr::lng_context_upload_edit_caption(tr::now), - [=] { - if (const auto view = viewByItem(item)) { - controller->uiShow()->show(Box( - Ui::EditCaptionBox, - view)); - } - }, - &st::menuIconEdit); + if (item->media() + && item->media()->allowsEditCaption()) { + _menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { editCaptionUploadLayer(item); }, + &st::menuIconEdit); + } _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( @@ -2973,16 +2970,13 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { deleteAsGroup(itemId); }; if (item->isUploading()) { - _menu->addAction( - tr::lng_context_upload_edit_caption(tr::now), - [=] { - if (const auto view = viewByItem(item)) { - controller->uiShow()->show(Box( - Ui::EditCaptionBox, - view)); - } - }, - &st::menuIconEdit); + if (item->media() + && item->media()->allowsEditCaption()) { + _menu->addAction( + tr::lng_context_upload_edit_caption(tr::now), + [=] { editCaptionUploadLayer(item); }, + &st::menuIconEdit); + } _menu->addAction(tr::lng_context_cancel_upload(tr::now), callback, &st::menuIconCancel); } else { _menu->addAction(Ui::DeleteMessageContextAction( @@ -3119,6 +3113,14 @@ void HistoryInner::copySelectedText() { } } +void HistoryInner::editCaptionUploadLayer(not_null item) { + if (const auto view = viewByItem(item)) { + if (item->isUploading()) { + _controller->uiShow()->show(Box(Ui::EditCaptionBox, view)); + } + } +} + void HistoryInner::savePhotoToFile(not_null photo) { const auto media = photo->activeMediaView(); if (photo->isNull() || !media || !media->loaded()) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 33dd734966..8d180cf670 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -418,6 +418,7 @@ private: void blockSenderItem(FullMsgId itemId); void blockSenderAsGroup(FullMsgId itemId); void copySelectedText(); + void editCaptionUploadLayer(not_null item); [[nodiscard]] auto reactionButtonParameters( not_null view, From 5f8d662d678618a3c0e6954066f6492428c084c6 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 May 2025 19:05:40 +0300 Subject: [PATCH 044/310] Slightly improved of forward declarations in history item. --- Telegram/SourceFiles/history/history_item.cpp | 4 --- Telegram/SourceFiles/history/history_item.h | 28 ------------------- 2 files changed, 32 deletions(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 7f22efb0b4..0b7571674c 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_premium.h" #include "api/api_sensitive_content.h" #include "lang/lang_keys.h" -#include "mainwidget.h" #include "calls/calls_instance.h" // Core::App().calls().joinGroupCall. #include "history/view/history_view_item_preview.h" #include "history/view/history_view_message.h" @@ -28,8 +27,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits_graphics.h" // ShowRefundInfoBox. #include "storage/file_upload.h" #include "storage/storage_shared_media.h" -#include "main/main_account.h" -#include "main/main_domain.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "menu/menu_ttl_validator.h" @@ -72,7 +69,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_non_panel_process.h" // ProcessNonPanelPaymentFormFactory. #include "platform/platform_notifications_manager.h" #include "spellcheck/spellcheck_highlight_syntax.h" -#include "styles/style_dialogs.h" namespace { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index e8153f2d78..fcfd616382 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -22,8 +22,6 @@ struct HistoryMessageMarkupData; struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; struct HistoryMessageForwarded; -struct HistoryMessageSavedMediaData; -struct HistoryMessageFactcheck; struct HistoryServiceDependentData; enum class HistorySelfDestructType; struct PreparedServiceText; @@ -31,10 +29,6 @@ struct MessageFactcheck; class ReplyKeyboard; struct LanguageId; -namespace Api { -struct SendOptions; -} // namespace Api - namespace base { template class enum_mask; @@ -45,15 +39,6 @@ enum class SharedMediaType : signed char; using SharedMediaTypesMask = base::enum_mask; } // namespace Storage -namespace Ui { -class RippleAnimation; -} // namespace Ui - -namespace style { -struct BotKeyboardButton; -struct RippleAnimation; -} // namespace style - namespace Data { struct MessagePosition; struct RecentReaction; @@ -71,24 +56,11 @@ struct PaidReactionSend; struct SendError; } // namespace Data -namespace Main { -class Session; -} // namespace Main - -namespace Window { -class SessionController; -} // namespace Window - namespace HistoryUnreadThings { enum class AddType; } // namespace HistoryUnreadThings namespace HistoryView { -struct TextState; -struct StateRequest; -enum class CursorState : char; -enum class PointState : char; -enum class Context : char; class ElementDelegate; class Element; class Message; From 727acca217d093298ea596bce2bcc93407d886d5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 27 May 2025 15:31:39 +0300 Subject: [PATCH 045/310] Updated Qt to 5.15.17 on Windows. --- Telegram/build/prepare/prepare.py | 1 - Telegram/build/qt_version.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index c2f05aa426..a859afa93d 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1557,7 +1557,6 @@ release: depends:patches/qtbase_""" + qt + """/*.patch cd qtbase win: - git revert --no-edit 6ad56dce34 setlocal enabledelayedexpansion for /r %%i in (..\\..\\patches\\qtbase_%QT%\\*) do ( git apply %%i -v diff --git a/Telegram/build/qt_version.py b/Telegram/build/qt_version.py index 3257df851e..e551364ce3 100644 --- a/Telegram/build/qt_version.py +++ b/Telegram/build/qt_version.py @@ -9,5 +9,5 @@ def resolve(arch): os.environ['QT'] = '6.9.1' else: print('Choosing Qt 5.') - os.environ['QT'] = '5.15.15' + os.environ['QT'] = '5.15.17' return True From 1ae3122c205f4e5e48839db831c8e6baf8f86d38 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 28 May 2025 13:33:09 +0300 Subject: [PATCH 046/310] Added support of suggestion to validate cloud password to settings. --- Telegram/CMakeLists.txt | 2 + .../animations/cloud_password/validate.tgs | Bin 0 -> 15226 bytes Telegram/Resources/langs/lang.strings | 8 + .../Resources/qrc/telegram/animations.qrc | 1 + .../data/components/promo_suggestions.cpp | 5 + .../data/components/promo_suggestions.h | 2 + .../settings_cloud_password_common.h | 1 + .../settings_cloud_password_input.cpp | 155 ++++++++++++++---- .../settings_cloud_password_input.h | 1 + .../settings_cloud_password_validate_icon.cpp | 74 +++++++++ .../settings_cloud_password_validate_icon.h | 27 +++ .../SourceFiles/settings/settings_main.cpp | 78 ++++++++- 12 files changed, 324 insertions(+), 30 deletions(-) create mode 100644 Telegram/Resources/animations/cloud_password/validate.tgs create mode 100644 Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp create mode 100644 Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 50dcfec3e6..92ce8f31a3 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1446,6 +1446,8 @@ PRIVATE settings/cloud_password/settings_cloud_password_start.h settings/cloud_password/settings_cloud_password_step.cpp settings/cloud_password/settings_cloud_password_step.h + settings/cloud_password/settings_cloud_password_validate_icon.cpp + settings/cloud_password/settings_cloud_password_validate_icon.h settings/settings_active_sessions.cpp settings/settings_active_sessions.h settings/settings_advanced.cpp diff --git a/Telegram/Resources/animations/cloud_password/validate.tgs b/Telegram/Resources/animations/cloud_password/validate.tgs new file mode 100644 index 0000000000000000000000000000000000000000..fd5161d885486ba3a015d7792a1f06aff63f542a GIT binary patch literal 15226 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../../animations/cloud_password/password_input.tgs ../../animations/cloud_password/hint.tgs ../../animations/cloud_password/email.tgs + ../../animations/cloud_password/validate.tgs ../../animations/ttl.tgs ../../animations/discussion.tgs ../../animations/stats.tgs diff --git a/Telegram/SourceFiles/data/components/promo_suggestions.cpp b/Telegram/SourceFiles/data/components/promo_suggestions.cpp index 99ec5f5b2f..33c0b860a5 100644 --- a/Telegram/SourceFiles/data/components/promo_suggestions.cpp +++ b/Telegram/SourceFiles/data/components/promo_suggestions.cpp @@ -306,4 +306,9 @@ std::optional PromoSuggestions::knownBirthdaysToday() const { return _contactBirthdaysToday; } +QString PromoSuggestions::SugValidatePassword() { + static const auto key = u"VALIDATE_PASSWORD"_q; + return key; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/promo_suggestions.h b/Telegram/SourceFiles/data/components/promo_suggestions.h index 33f943a958..e57c1162cf 100644 --- a/Telegram/SourceFiles/data/components/promo_suggestions.h +++ b/Telegram/SourceFiles/data/components/promo_suggestions.h @@ -51,6 +51,8 @@ public: [[nodiscard]] auto knownBirthdaysToday() const -> std::optional>; + [[nodiscard]] static QString SugValidatePassword(); + private: void setTopPromoted( History *promoted, diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h index 586e58a9dd..a87c1aaed5 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_common.h @@ -34,6 +34,7 @@ struct StepData { QString email; int unconfirmedEmailLengthCode; bool setOnlyRecoveryEmail = false; + bool suggestionValidate = false; struct ProcessRecover { bool setNewPassword = false; diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp index 0064dcbda7..7719fc2ddd 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.cpp @@ -12,20 +12,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "base/unixtime.h" #include "core/core_cloud_password.h" +#include "data/components/promo_suggestions.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" +#include "main/main_session.h" #include "settings/cloud_password/settings_cloud_password_common.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" #include "settings/cloud_password/settings_cloud_password_hint.h" #include "settings/cloud_password/settings_cloud_password_manage.h" #include "settings/cloud_password/settings_cloud_password_step.h" +#include "settings/cloud_password/settings_cloud_password_validate_icon.h" +#include "ui/boxes/boost_box.h" // Ui::StartFireworks. #include "ui/boxes/confirm_box.h" +#include "ui/rect.h" #include "ui/text/format_values.h" +#include "ui/ui_utility.h" +#include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/password_input.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" -#include "ui/vertical_list.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" @@ -53,6 +59,10 @@ RecreateResetPassword: – Continue to RecreateResetHint. – Clear password and Back to Settings. – Back to Settings. + +ValidatePassword: +- Submit to show good validate. +- Back to Main Settings. */ namespace Settings { @@ -72,9 +82,7 @@ Icon CreateInteractiveLottieIcon( const auto raw = object.data(); const auto width = descriptor.sizeOverride.width(); - raw->resize(QRect( - QPoint(), - descriptor.sizeOverride).marginsAdded(padding).size()); + raw->resize((Rect(descriptor.sizeOverride) + padding).size()); auto owned = Lottie::MakeIcon(std::move(descriptor)); const auto icon = owned.get(); @@ -118,7 +126,10 @@ public: using TypedAbstractStep::TypedAbstractStep; [[nodiscard]] rpl::producer title() override; + [[nodiscard]] QPointer createPinnedToTop( + not_null parent) override; void setupContent(); + void setupValidateGood(); protected: [[nodiscard]] rpl::producer> removeTypes() override; @@ -130,6 +141,8 @@ private: not_null info, Fn recoverCallback); + QWidget *_parent = nullptr; + rpl::variable> _removesFromStack; rpl::lifetime _requestLifetime; @@ -143,12 +156,58 @@ rpl::producer Input::title() { return tr::lng_settings_cloud_password_password_title(); } +QPointer Input::createPinnedToTop( + not_null parent) { + _parent = parent; + return nullptr; +} + +void Input::setupValidateGood() { + const auto content = Ui::CreateChild(this); + + if (_parent) { + Ui::StartFireworks(_parent); + } + + if (auto owned = CreateValidateGoodIcon(&controller()->session())) { + owned->setParent(content); + content->add( + object_ptr>( + content, + std::move(owned)), + QMargins(0, st::lineWidth * 75, 0, 0)); + } + + SetupHeader( + content, + QString(), + rpl::never<>(), + tr::lng_settings_suggestion_password_step_finish_title(), + tr::lng_settings_suggestion_password_step_finish_about()); + + const auto button = AddDoneButton(content, tr::lng_share_done()); + button->setClickedCallback([=] { + showBack(); + }); + + Ui::ToggleChildrenVisibility(this, true); + Ui::ResizeFitChild(this, content); + content->resizeToWidth(width()); + Ui::SendPendingMoveResizeEvents(content); +} + void Input::setupContent() { + if (QWidget::children().count() > 0) { + return; + } + const auto content = Ui::CreateChild(this); auto currentStepData = stepData(); const auto currentStepDataPassword = base::take(currentStepData.password); const auto currentStepProcessRecover = base::take( currentStepData.processRecover); + const auto currentStepValidate = base::take( + currentStepData.suggestionValidate); setStepData(currentStepData); const auto currentState = cloudPassword().stateCurrent(); @@ -167,11 +226,10 @@ void Input::setupContent() { const auto icon = CreateInteractiveLottieIcon( content, { - .name = u"cloud_password/password_input"_q, - .sizeOverride = { - st::settingsCloudPasswordIconSize, - st::settingsCloudPasswordIconSize - }, + .name = currentStepValidate + ? u"cloud_password/validate"_q + : u"cloud_password/password_input"_q, + .sizeOverride = Size(st::settingsCloudPasswordIconSize), }, st::settingLocalPasscodeIconPadding); @@ -179,12 +237,16 @@ void Input::setupContent() { content, QString(), rpl::never<>(), - isCheck + currentStepValidate + ? tr::lng_settings_suggestion_password_step_input_title() + : isCheck ? tr::lng_settings_cloud_password_check_subtitle() : hasPassword ? tr::lng_settings_cloud_password_manage_password_change() : tr::lng_settings_cloud_password_password_subtitle(), - isCheck + currentStepValidate + ? tr::lng_settings_suggestion_password_step_input_about() + : isCheck ? tr::lng_settings_cloud_password_manage_about1() : tr::lng_cloud_password_about()); @@ -340,7 +402,9 @@ void Input::setupContent() { Ui::AddSkip(content); } - if (!newInput->text().isEmpty()) { + if (currentStepValidate) { + icon.icon->animate(icon.update, 0, icon.icon->framesCount() - 1); + } else if (!newInput->text().isEmpty()) { icon.icon->jumpTo(icon.icon->framesCount() / 2, icon.update); } @@ -376,10 +440,18 @@ void Input::setupContent() { } } - auto data = stepData(); - data.currentPassword = pass; - setStepData(std::move(data)); - showOther(CloudPasswordManageId()); + if (currentStepValidate) { + controller()->session().promoSuggestions().dismiss( + Data::PromoSuggestions::SugValidatePassword()); + setupValidateGood(); + delete content; + } else { + auto data = stepData(); + data.currentPassword = pass; + setStepData(std::move(data)); + showOther(CloudPasswordManageId()); + } + }); }; @@ -412,17 +484,19 @@ void Input::setupContent() { } }); - base::qt_signal_producer( - newInput.get(), - &QLineEdit::textChanged // Covers Undo. - ) | rpl::map([=] { - return newInput->text().isEmpty(); - }) | rpl::distinct_until_changed( - ) | rpl::start_with_next([=](bool empty) { - const auto from = icon.icon->frameIndex(); - const auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1); - icon.icon->animate(icon.update, from, to); - }, content->lifetime()); + if (!currentStepValidate) { + base::qt_signal_producer( + newInput.get(), + &QLineEdit::textChanged // Covers Undo. + ) | rpl::map([=] { + return newInput->text().isEmpty(); + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool empty) { + const auto from = icon.icon->frameIndex(); + const auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1); + icon.icon->animate(icon.update, from, to); + }, content->lifetime()); + } const auto submit = [=] { if (!reenterInput || reenterInput->hasFocus()) { @@ -437,7 +511,7 @@ void Input::setupContent() { QObject::connect(reenterInput, &MaskedInputField::submitted, submit); } - setFocusCallback([=] { + setFocusCallback(crl::guard(content, [=] { if (isCheck || newInput->text().isEmpty()) { newInput->setFocus(); } else if (reenterInput->text().isEmpty()) { @@ -445,7 +519,7 @@ void Input::setupContent() { } else { newInput->setFocus(); } - }); + })); Ui::ResizeFitChild(this, content); } @@ -586,10 +660,33 @@ void Input::setupRecoverButton( }); } +class SuggestionInput : public Input { +public: + SuggestionInput( + QWidget *parent, + not_null controller) + : Input(parent, controller) + , _stepData(StepData{ .suggestionValidate = true }) { + setStepDataReference(_stepData); + } + + [[nodiscard]] static Type Id() { + return SectionFactory::Instance(); + } + +private: + std::any _stepData; + +}; + } // namespace CloudPassword Type CloudPasswordInputId() { return CloudPassword::Input::Id(); } +Type CloudPasswordSuggestionInputId() { + return CloudPassword::SuggestionInput::Id(); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h index 1103047df4..0c1ae4f89c 100644 --- a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_input.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { Type CloudPasswordInputId(); +Type CloudPasswordSuggestionInputId(); } // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp new file mode 100644 index 0000000000..7ea244e8ff --- /dev/null +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.cpp @@ -0,0 +1,74 @@ +/* +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 "settings/cloud_password/settings_cloud_password_validate_icon.h" + +#include "apiwrap.h" +#include "base/object_ptr.h" +#include "chat_helpers/stickers_emoji_pack.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" +#include "main/main_session.h" +#include "ui/rect.h" +#include "ui/rp_widget.h" +#include "styles/style_settings.h" + +namespace Settings { +namespace { + +[[nodiscard]] DocumentData *EmojiValidateGood( + not_null session) { + auto emoji = TextWithEntities{ + .text = (QString(QChar(0xD83D)) + QChar(0xDC4D)), + }; + if (const auto e = Ui::Emoji::Find(emoji.text)) { + const auto sticker = session->emojiStickersPack().stickerForEmoji(e); + return sticker.document; + } + return nullptr; +} + +} // namespace + +object_ptr CreateValidateGoodIcon( + not_null session) { + const auto document = EmojiValidateGood(session); + if (!document) { + return nullptr; + } + + auto owned = object_ptr((QWidget*)nullptr); + const auto widget = owned.data(); + + struct State { + std::unique_ptr emoji; + }; + const auto state = widget->lifetime().make_state(); + const auto size = st::settingsCloudPasswordIconSize; + state->emoji = std::make_unique( + session->data().customEmojiManager().create( + document, + [=] { widget->update(); }, + Data::CustomEmojiManager::SizeTag::Normal, + size), + 1, + true); + widget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(widget); + state->emoji->paint(p, Ui::Text::CustomEmojiPaintContext{ + .textColor = st::windowFg->c, + .now = crl::now(), + }); + }, widget->lifetime()); + const auto padding = st::settingLocalPasscodeIconPadding; + widget->resize((Rect(Size(size)) + padding).size()); + + return owned; +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h new file mode 100644 index 0000000000..54280d7360 --- /dev/null +++ b/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_validate_icon.h @@ -0,0 +1,27 @@ +/* +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 + +template +class object_ptr; + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Settings { + +[[nodiscard]] object_ptr CreateValidateGoodIcon( + not_null session); + +} // namespace Settings + diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 07ad5e1f99..496e5e2933 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_main.h" +#include "settings/cloud_password/settings_cloud_password_input.h" #include "api/api_credits.h" #include "core/application.h" #include "core/click_handler_types.h" @@ -467,7 +468,7 @@ void SetupValidatePhoneNumberSuggestion( yes->setClickedCallback([=] { controller->session().promoSuggestions().dismiss( kSugValidatePhone.utf8()); - mainWrap->toggle(false, anim::type::normal); + mainWrap->toggle(false, anim::type::normal); }); const auto no = Ui::CreateChild( wrap, @@ -522,6 +523,77 @@ void SetupValidatePhoneNumberSuggestion( Ui::AddSkip(content); } +void SetupValidatePasswordSuggestion( + not_null controller, + not_null container, + Fn showOther) { + if (!controller->session().promoSuggestions().current( + Data::PromoSuggestions::SugValidatePassword()) + || controller->session().promoSuggestions().current( + kSugValidatePhone.utf8())) { + return; + } + const auto mainWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto content = mainWrap->entity(); + Ui::AddSubsectionTitle( + content, + tr::lng_settings_suggestion_password_title(), + QMargins( + st::boxRowPadding.left() + - st::defaultSubsectionTitlePadding.left(), + 0, + 0, + 0)); + const auto label = content->add( + object_ptr( + content, + tr::lng_settings_suggestion_password_about(), + st::boxLabel), + st::boxRowPadding); + + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto wrap = content->add( + object_ptr( + content, + st::inviteLinkButton.height), + st::inviteLinkButtonsPadding); + const auto yes = Ui::CreateChild( + wrap, + tr::lng_settings_suggestion_password_yes(), + st::inviteLinkButton); + yes->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + yes->setClickedCallback([=] { + controller->session().promoSuggestions().dismiss( + Data::PromoSuggestions::SugValidatePassword()); + mainWrap->toggle(false, anim::type::normal); + }); + const auto no = Ui::CreateChild( + wrap, + tr::lng_settings_suggestion_password_no(), + st::inviteLinkButton); + no->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + no->setClickedCallback([=] { + showOther(Settings::CloudPasswordSuggestionInputId()); + }); + + wrap->widthValue() | rpl::start_with_next([=](int width) { + const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2; + yes->setFullWidth(buttonWidth); + no->setFullWidth(buttonWidth); + yes->moveToLeft(0, 0, width); + no->moveToRight(0, 0, width); + }, wrap->lifetime()); + Ui::AddSkip(content); + Ui::AddSkip(content); + Ui::AddDivider(content); + Ui::AddSkip(content); +} + void SetupSections( not_null controller, not_null container, @@ -533,6 +605,10 @@ void SetupSections( controller, container, showOther); + SetupValidatePasswordSuggestion( + controller, + container, + showOther); const auto addSection = [&]( rpl::producer label, From b4120b156ecefaed9388004a73086f0805938231 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 31 May 2025 15:54:53 +0300 Subject: [PATCH 047/310] Added nice overlay to recorded round videos. --- .../chat_helpers/chat_helpers.style | 2 + .../ui/controls/round_video_recorder.cpp | 231 ++++++++++++++++-- .../ui/controls/round_video_recorder_data.h | 69 ++++++ Telegram/cmake/td_ui.cmake | 1 + 4 files changed, 277 insertions(+), 26 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/round_video_recorder_data.h diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 9c0369d1c8..06ac557554 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1594,3 +1594,5 @@ frozenInfoBox: Box(defaultBox) { } shadowIgnoreTopSkip: true; } + +roundVideoFont: font(14px semibold); diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index 3c904cd6be..dfd17f42f4 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ffmpeg/ffmpeg_bytes_io_wrap.h" #include "ffmpeg/ffmpeg_utility.h" #include "media/audio/media_audio_capture.h" +#include "ui/controls/round_video_recorder_data.h" #include "ui/image/image_prepare.h" #include "ui/arc_angles.h" #include "ui/dynamic_image.h" @@ -38,6 +39,13 @@ constexpr auto kMinithumbsInRow = 16; constexpr auto kFadeDuration = crl::time(150); constexpr auto kSkipFrames = 8; constexpr auto kMinScale = 0.7; +constexpr auto &kPlainLogoFrames = RoundVideoData::kLogoFrames; +constexpr auto kLogoSize = RoundVideoData::kLogoSize; +constexpr auto kLogoXShift = -10; +constexpr auto kLogoYShift = 10; +constexpr auto kOverlayOpacity = 0.1; +constexpr auto kOverlayOpaque = 1. - kOverlayOpacity; +constexpr auto kOverlayUVOpaque = 128 * kOverlayOpaque; using namespace FFmpeg; @@ -49,6 +57,115 @@ using namespace FFmpeg; return inner * style::DevicePixelRatio(); } +[[nodiscard]] QImage CircularTextImage( + const QString &text, + int width, + int height, + int radius, + float64 startAngle = 0.0, + float64 endAngle = 360.0, + const QColor &textColor = Qt::black, + const QColor &bgColor = Qt::white, + const QFont &font = QFont(), + bool reverseDirection = false) { + auto image = QImage(width, height, QImage::Format_ARGB32); + image.fill(bgColor); + + auto painter = QPainter(&image); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(textColor); + painter.setFont(font); + + auto center = QPoint(width / 2, height / 2); + painter.translate(center); + + if (endAngle < startAngle) { + std::swap(startAngle, endAngle); + } + + const auto startRad = float64(startAngle - 90) * M_PI / 180.0; + const auto endRad = float64(endAngle - 90) * M_PI / 180.0; + const auto angleRange = float64(endRad) - float64(startRad); + + const auto &metrics = QFontMetrics(font); + + for (auto i = 0; i < text.length(); ++i) { + const auto ratio = (text.length() <= 1) + ? 0.5 + : reverseDirection + ? 1.0 - static_cast(i) / (text.length() - 1) + : static_cast(i) / (text.length() - 1); + + const auto angle = startRad + ratio * angleRange; + + const auto x = radius * std::cos(angle); + const auto y = radius * std::sin(angle); + + const auto degrees = (angle * 180.0 / M_PI) - 90; + painter.save(); + painter.translate(x, y); + painter.rotate(degrees); + const auto offset = (i == text.length() - 1) ? 2. : 0.; + painter.drawText( + -metrics.horizontalAdvance(text[i]) / 2 + offset, + metrics.ascent() / 2, + QString(text[i])); + painter.restore(); + } + + return image; +} + +using PrecomputedLogo = std::array, kLogoSize>; +[[nodiscard]] const std::vector &PrecomputedLogos() { + static std::vector precomputedLogos; + + if (!precomputedLogos.empty()) { + return precomputedLogos; + } + constexpr auto kAntialiasRadius = 0.4; + precomputedLogos.resize(kPlainLogoFrames.size()); + const auto antialiasFactor = 1.0 + / (2. * kAntialiasRadius * kAntialiasRadius); + + for (auto index = size_t(0); index < kPlainLogoFrames.size(); ++index) { + uint8_t logoFrame[kLogoSize][kLogoSize] = {{ 0 }}; + RoundVideoData::DecompressLogoRLEFrame( + kPlainLogoFrames[index], + logoFrame); + + for (auto y = 0; y < kLogoSize; ++y) { + for (auto x = 0; x < kLogoSize; ++x) { + auto blendedValue = 0.; + auto weightSum = 0.; + + const auto minY = std::max(0, y - 1); + const auto maxY = std::min(kLogoSize - 1, y + 1); + const auto minX = std::max(0, x - 1); + const auto maxX = std::min(kLogoSize - 1, x + 1); + + for (auto sampleY = minY; sampleY <= maxY; ++sampleY) { + const auto dy = sampleY - y; + for (auto sampleX = minX; sampleX <= maxX; ++sampleX) { + const auto dx = sampleX - x; + const auto distanceSq = dx * dx + dy * dy; + const auto weight + = std::exp(-distanceSq * antialiasFactor); + + blendedValue += logoFrame[sampleY][sampleX] * weight; + weightSum += weight; + } + } + + precomputedLogos[index][y][x] = (weightSum > 0) + ? (blendedValue / weightSum) + : 0; + } + } + } + return precomputedLogos; +} + } // namespace class RoundVideoRecorder::Private final { @@ -77,6 +194,7 @@ private: void initEncoding(); void initCircleMask(); + void initCircularTextImage(); void initMinithumbsCanvas(); void maybeSaveMinithumb( not_null frame, @@ -102,7 +220,7 @@ private: void updateResultDuration(int64 pts, AVRational timeBase); void mirrorYUV420P(not_null frame); - void cutCircleFromYUV420P(not_null frame); + void drawLogoOnYUV420P(not_null frame); [[nodiscard]] RoundVideoResult appendToPrevious(RoundVideoResult video); [[nodiscard]] static FormatPointer OpenInputContext( @@ -158,6 +276,9 @@ private: ReadBytesWrap _forConcat1, _forConcat2; + uint8_t _logoFrameCounter = 0; + QImage _circularTextImage; + std::vector _circleMask; // Always nice to use vector! :D base::ConcurrentTimer _timeoutTimer; @@ -178,6 +299,7 @@ RoundVideoRecorder::Private::Private( , _timeoutTimer(_weak, [=] { timeout(); }) { initEncoding(); initCircleMask(); + initCircularTextImage(); initMinithumbsCanvas(); _timeoutTimer.callOnce(kInitTimeout); @@ -673,7 +795,7 @@ void RoundVideoRecorder::Private::encodeVideoFrame( _videoFrame->linesize); mirrorYUV420P(_videoFrame.get()); - cutCircleFromYUV420P(_videoFrame.get()); + drawLogoOnYUV420P(_videoFrame.get()); _videoFrame->pts = mcstimestamp - _videoFirstTimestamp; maybeSaveMinithumb(_videoFrame.get(), frame, crop); @@ -747,6 +869,23 @@ void RoundVideoRecorder::Private::initCircleMask() { } } +void RoundVideoRecorder::Private::initCircularTextImage() { + constexpr auto kCircularTextRadius = kSide / 2 + 17; + constexpr auto kCircularTextStartAngle = 125; + constexpr auto kCircularTextEndAngle = 145; + _circularTextImage = CircularTextImage( + u"Telegram"_q.toUpper(), + kSide, + kSide, + kCircularTextRadius, + kCircularTextStartAngle, + kCircularTextEndAngle, + Qt::white, + Qt::transparent, + st::roundVideoFont, + true); +} + void RoundVideoRecorder::Private::initMinithumbsCanvas() { const auto width = kMinithumbsInRow * _minithumbSize; const auto seconds = (kMaxDuration + 999) / 1000; @@ -772,45 +911,85 @@ void RoundVideoRecorder::Private::mirrorYUV420P(not_null frame) { } } -void RoundVideoRecorder::Private::cutCircleFromYUV420P( +void RoundVideoRecorder::Private::drawLogoOnYUV420P( not_null frame) { const auto width = frame->width; const auto height = frame->height; + const auto centerX = width / 2; + const auto centerY = height / 2; + const auto radius = std::min(centerX, centerY); + const auto radiusSquared = radius * radius; + + const auto logoBottom = height - kLogoSize + kLogoYShift; + const auto logoStartX = kLogoXShift; + const auto logoEndX = logoStartX + kLogoSize; + const auto logoStartY = logoBottom; + const auto logoEndY = logoBottom + kLogoSize; + + const auto ¤tLogo = PrecomputedLogos()[_logoFrameCounter]; + _logoFrameCounter = (_logoFrameCounter + 1) % kPlainLogoFrames.size(); - auto yMaskIndex = 0; auto yData = frame->data[0]; const auto ySkip = frame->linesize[0] - width; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { + + const auto uvWidth = width / 2; + const auto uvHeight = height / 2; + auto uData = frame->data[1]; + auto vData = frame->data[2]; + const auto uvSkip = frame->linesize[1] - uvWidth; + auto yMaskIndex = 0; + + for (auto y = 0; y < height; ++y) { + const auto dy = y - centerY; + const auto dySquared = dy * dy; + + for (auto x = 0; x < width; ++x) { + const auto dx = x - centerX; + const auto distanceSquared = dx * dx + dySquared; + if (_circleMask[yMaskIndex]) { - *yData = 255; + *yData = static_cast(*yData * kOverlayOpacity + + 16 * kOverlayOpaque); } + + if ((x >= logoStartX && x < logoEndX) + && (y >= logoStartY && y < logoEndY)) { + const auto logoX = x - kLogoXShift; + const auto logoY = y - logoBottom; + + const auto blendedValue = currentLogo[logoX][logoY]; + if (blendedValue > 0) { + const auto logoFactor = blendedValue / 255.0f; + *yData = static_cast(*yData * (1 - logoFactor) + + 255 * logoFactor); + } + } + + const auto textAlpha = qAlpha(_circularTextImage.pixel(x, y)) + / 255.; + *yData = std::min( + *yData + static_cast(textAlpha * 100), + 255); + ++yData; ++yMaskIndex; } yData += ySkip; - } - const auto whalf = width / 2; - const auto hhalf = height / 2; - - auto uvMaskIndex = 0; - auto uData = frame->data[1]; - auto vData = frame->data[2]; - const auto uSkip = frame->linesize[1] - whalf; - for (auto y = 0; y != hhalf; ++y) { - for (auto x = 0; x != whalf; ++x) { - if (_circleMask[uvMaskIndex]) { - *uData = 128; - *vData = 128; + if (y % 2 == 0) { + for (auto x = 0; x < uvWidth; ++x) { + if (_circleMask[(y * width) + (x * 2)]) { + *uData = static_cast(*uData * kOverlayOpacity + + kOverlayUVOpaque); + *vData = static_cast(*vData * kOverlayOpacity + + kOverlayUVOpaque); + } + ++uData; + ++vData; } - ++uData; - ++vData; - uvMaskIndex += 2; + uData += uvSkip; + vData += uvSkip; } - uData += uSkip; - vData += uSkip; - uvMaskIndex += width; } } diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder_data.h b/Telegram/SourceFiles/ui/controls/round_video_recorder_data.h new file mode 100644 index 0000000000..8a5496f7e6 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder_data.h @@ -0,0 +1,69 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace RoundVideoData { + +constexpr auto kLogoSize = 80; + +struct LogoRLENode final { + uint16_t count; + uint8_t value; +}; +using LogoRLEFrame = std::vector; + +void DecompressLogoRLEFrame( + const std::vector &rleFrame, + uint8_t outFrame[kLogoSize][kLogoSize]) { + auto pos = size_t(0); + for (const auto &node : rleFrame) { + for (auto i = uint16_t(0); i < node.count; ++i) { + if (pos >= kLogoSize * kLogoSize) { + break; + } + const auto y = int(pos / kLogoSize); + const auto x = int(pos % kLogoSize); + outFrame[y][x] = node.value; + pos++; + } + } +} + +const auto kLogoFrames = std::array{ { + + LogoRLEFrame{ { 997, 0 }, { 1, 9 }, { 1, 4 }, { 77, 0 }, { 1, 1 }, { 1, 27 }, { 78, 0 }, { 1, 32 }, { 1, 11 }, { 77, 0 }, { 1, 14 }, { 1, 44 }, { 77, 0 }, { 1, 2 }, { 1, 54 }, { 1, 17 }, { 77, 0 }, { 1, 33 }, { 1, 47 }, { 77, 0 }, { 1, 9 }, { 1, 61 }, { 1, 13 }, { 77, 0 }, { 1, 45 }, { 1, 40 }, { 77, 0 }, { 1, 19 }, { 1, 60 }, { 1, 6 }, { 76, 0 }, { 1, 2 }, { 1, 54 }, { 1, 31 }, { 77, 0 }, { 1, 27 }, { 1, 54 }, { 1, 2 }, { 76, 0 }, { 1, 1 }, { 1, 55 }, { 1, 15 }, { 9, 0 }, { 1, 53 }, { 1, 196 }, { 1, 65 }, { 65, 0 }, { 1, 24 }, { 1, 35 }, { 9, 0 }, { 1, 32 }, { 1, 238 }, { 1, 255 }, { 1, 115 }, { 65, 0 }, { 1, 41 }, { 1, 2 }, { 8, 0 }, { 1, 4 }, { 1, 199 }, { 2, 255 }, { 1, 121 }, { 20, 0 }, { 1, 2 }, { 1, 4 }, { 42, 0 }, { 1, 16 }, { 1, 9 }, { 9, 0 }, { 1, 135 }, { 3, 255 }, { 1, 98 }, { 19, 0 }, { 2, 19 }, { 43, 0 }, { 1, 7 }, { 9, 0 }, { 1, 68 }, { 1, 253 }, { 3, 255 }, { 1, 65 }, { 17, 0 }, { 1, 8 }, { 1, 43 }, { 1, 20 }, { 53, 0 }, { 1, 22 }, { 1, 231 }, { 3, 255 }, { 1, 217 }, { 1, 3 }, { 16, 0 }, { 1, 30 }, { 1, 56 }, { 1, 15 }, { 53, 0 }, { 1, 1 }, { 1, 183 }, { 3, 255 }, { 1, 250 }, { 1, 67 }, { 15, 0 }, { 1, 8 }, { 1, 48 }, { 1, 52 }, { 1, 10 }, { 54, 0 }, { 1, 115 }, { 4, 255 }, { 1, 91 }, { 15, 0 }, { 1, 23 }, { 1, 59 }, { 1, 39 }, { 1, 2 }, { 54, 0 }, { 1, 52 }, { 1, 250 }, { 3, 255 }, { 1, 114 }, { 14, 0 }, { 1, 4 }, { 1, 41 }, { 1, 60 }, { 1, 22 }, { 55, 0 }, { 1, 13 }, { 1, 219 }, { 3, 255 }, { 1, 139 }, { 14, 0 }, { 1, 10 }, { 1, 54 }, { 1, 51 }, { 1, 9 }, { 56, 0 }, { 1, 164 }, { 3, 255 }, { 1, 162 }, { 1, 1 }, { 1, 0 }, { 1, 102 }, { 1, 222 }, { 1, 236 }, { 1, 85 }, { 8, 0 }, { 1, 15 }, { 1, 54 }, { 1, 27 }, { 57, 0 }, { 1, 95 }, { 3, 255 }, { 1, 183 }, { 1, 5 }, { 1, 2 }, { 1, 152 }, { 3, 255 }, { 1, 164 }, { 7, 0 }, { 1, 22 }, { 1, 36 }, { 1, 4 }, { 57, 0 }, { 1, 38 }, { 1, 243 }, { 2, 255 }, { 1, 201 }, { 1, 11 }, { 1, 7 }, { 1, 172 }, { 4, 255 }, { 1, 188 }, { 6, 0 }, { 1, 16 }, { 1, 12 }, { 58, 0 }, { 1, 6 }, { 1, 206 }, { 2, 255 }, { 1, 217 }, { 1, 20 }, { 1, 14 }, { 1, 190 }, { 5, 255 }, { 1, 213 }, { 5, 0 }, { 1, 1 }, { 60, 0 }, { 1, 144 }, { 2, 255 }, { 1, 230 }, { 1, 32 }, { 1, 23 }, { 1, 207 }, { 6, 255 }, { 1, 238 }, { 65, 0 }, { 1, 76 }, { 2, 255 }, { 1, 240 }, { 1, 46 }, { 1, 35 }, { 1, 220 }, { 8, 255 }, { 1, 8 }, { 63, 0 }, { 1, 26 }, { 1, 235 }, { 1, 255 }, { 1, 248 }, { 1, 64 }, { 1, 48 }, { 1, 232 }, { 9, 255 }, { 1, 31 }, { 62, 0 }, { 1, 2 }, { 1, 191 }, { 1, 255 }, { 1, 253 }, { 1, 83 }, { 1, 64 }, { 1, 241 }, { 10, 255 }, { 1, 56 }, { 62, 0 }, { 1, 124 }, { 2, 255 }, { 1, 106 }, { 1, 83 }, { 1, 248 }, { 11, 255 }, { 1, 81 }, { 61, 0 }, { 1, 59 }, { 1, 252 }, { 1, 255 }, { 1, 130 }, { 1, 103 }, { 1, 253 }, { 12, 255 }, { 1, 105 }, { 60, 0 }, { 1, 17 }, { 1, 225 }, { 1, 255 }, { 1, 226 }, { 1, 143 }, { 14, 255 }, { 1, 103 }, { 60, 0 }, { 1, 173 }, { 17, 255 }, { 1, 223 }, { 1, 12 }, { 59, 0 }, { 1, 105 }, { 17, 255 }, { 1, 206 }, { 1, 30 }, { 59, 0 }, { 1, 44 }, { 1, 247 }, { 15, 255 }, { 1, 204 }, { 1, 95 }, { 1, 4 }, { 59, 0 }, { 1, 9 }, { 1, 213 }, { 12, 255 }, { 1, 252 }, { 1, 191 }, { 1, 110 }, { 1, 30 }, { 62, 0 }, { 1, 154 }, { 10, 255 }, { 1, 247 }, { 1, 178 }, { 1, 97 }, { 1, 20 }, { 64, 0 }, { 1, 85 }, { 8, 255 }, { 1, 240 }, { 1, 165 }, { 1, 84 }, { 1, 12 }, { 66, 0 }, { 1, 32 }, { 1, 240 }, { 5, 255 }, { 1, 232 }, { 1, 153 }, { 1, 71 }, { 1, 6 }, { 68, 0 }, { 1, 1 }, { 1, 196 }, { 3, 255 }, { 1, 221 }, { 1, 140 }, { 1, 59 }, { 1, 2 }, { 71, 0 }, { 1, 49 }, { 1, 253 }, { 1, 207 }, { 1, 127 }, { 1, 46 }, { 76, 0 }, { 1, 6 }, { 508, 0 }, { 1, 6 }, { 1, 1 }, { 76, 0 }, { 1, 7 }, { 1, 29 }, { 1, 9 }, { 75, 0 }, { 1, 6 }, { 1, 36 }, { 1, 41 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 31 }, { 1, 59 }, { 1, 29 }, { 75, 0 }, { 1, 17 }, { 1, 53 }, { 1, 51 }, { 1, 13 }, { 74, 0 }, { 1, 6 }, { 1, 42 }, { 1, 60 }, { 1, 29 }, { 75, 0 }, { 1, 25 }, { 1, 60 }, { 1, 47 }, { 1, 9 }, { 74, 0 }, { 1, 3 }, { 1, 41 }, { 1, 52 }, { 1, 21 }, { 75, 0 }, { 1, 12 }, { 1, 43 }, { 1, 21 }, { 76, 0 }, { 1, 15 }, { 1, 18 }, { 77, 0 }, { 1, 1 }, { 843, 0 } }, + LogoRLEFrame{ { 600, 0 }, { 1, 1 }, { 79, 0 }, { 1, 7 }, { 78, 0 }, { 1, 14 }, { 1, 1 }, { 77, 0 }, { 1, 7 }, { 1, 16 }, { 77, 0 }, { 1, 1 }, { 1, 26 }, { 1, 3 }, { 77, 0 }, { 1, 18 }, { 1, 19 }, { 77, 0 }, { 1, 6 }, { 1, 29 }, { 1, 3 }, { 77, 0 }, { 1, 24 }, { 1, 16 }, { 77, 0 }, { 1, 12 }, { 1, 28 }, { 1, 1 }, { 76, 0 }, { 1, 2 }, { 1, 28 }, { 1, 12 }, { 77, 0 }, { 1, 17 }, { 1, 25 }, { 77, 0 }, { 1, 3 }, { 1, 29 }, { 1, 7 }, { 77, 0 }, { 1, 16 }, { 1, 17 }, { 77, 0 }, { 1, 1 }, { 1, 24 }, { 1, 1 }, { 77, 0 }, { 1, 13 }, { 1, 5 }, { 77, 0 }, { 1, 1 }, { 1, 8 }, { 78, 0 }, { 1, 1 }, { 90, 0 }, { 1, 14 }, { 1, 167 }, { 1, 127 }, { 76, 0 }, { 1, 2 }, { 1, 189 }, { 1, 255 }, { 1, 196 }, { 76, 0 }, { 1, 124 }, { 2, 255 }, { 1, 204 }, { 75, 0 }, { 1, 58 }, { 1, 252 }, { 2, 255 }, { 1, 185 }, { 74, 0 }, { 1, 16 }, { 1, 224 }, { 3, 255 }, { 1, 155 }, { 74, 0 }, { 1, 171 }, { 4, 255 }, { 1, 64 }, { 73, 0 }, { 1, 101 }, { 4, 255 }, { 1, 167 }, { 73, 0 }, { 1, 42 }, { 1, 245 }, { 3, 255 }, { 1, 192 }, { 1, 7 }, { 72, 0 }, { 1, 8 }, { 1, 210 }, { 3, 255 }, { 1, 209 }, { 1, 15 }, { 57, 0 }, { 1, 5 }, { 15, 0 }, { 1, 148 }, { 3, 255 }, { 1, 223 }, { 1, 25 }, { 57, 0 }, { 1, 14 }, { 1, 1 }, { 14, 0 }, { 1, 80 }, { 3, 255 }, { 1, 235 }, { 1, 38 }, { 1, 0 }, { 1, 26 }, { 1, 159 }, { 1, 214 }, { 1, 142 }, { 52, 0 }, { 1, 4 }, { 1, 20 }, { 14, 0 }, { 1, 28 }, { 1, 237 }, { 2, 255 }, { 1, 245 }, { 1, 54 }, { 1, 0 }, { 1, 48 }, { 1, 231 }, { 3, 255 }, { 1, 7 }, { 51, 0 }, { 1, 29 }, { 1, 5 }, { 13, 0 }, { 1, 3 }, { 1, 193 }, { 2, 255 }, { 1, 251 }, { 1, 74 }, { 1, 0 }, { 1, 64 }, { 1, 241 }, { 4, 255 }, { 1, 32 }, { 50, 0 }, { 1, 14 }, { 1, 30 }, { 14, 0 }, { 1, 126 }, { 3, 255 }, { 1, 95 }, { 1, 0 }, { 1, 83 }, { 1, 248 }, { 5, 255 }, { 1, 59 }, { 49, 0 }, { 1, 1 }, { 1, 37 }, { 1, 12 }, { 13, 0 }, { 1, 60 }, { 1, 252 }, { 2, 255 }, { 1, 119 }, { 1, 0 }, { 1, 105 }, { 1, 253 }, { 6, 255 }, { 1, 85 }, { 49, 0 }, { 1, 21 }, { 1, 32 }, { 13, 0 }, { 1, 17 }, { 1, 225 }, { 2, 255 }, { 1, 144 }, { 1, 0 }, { 1, 128 }, { 8, 255 }, { 1, 111 }, { 48, 0 }, { 1, 3 }, { 1, 40 }, { 1, 11 }, { 13, 0 }, { 1, 173 }, { 2, 255 }, { 1, 167 }, { 1, 4 }, { 1, 152 }, { 9, 255 }, { 1, 138 }, { 48, 0 }, { 1, 25 }, { 1, 31 }, { 13, 0 }, { 1, 104 }, { 2, 255 }, { 1, 188 }, { 1, 14 }, { 1, 174 }, { 10, 255 }, { 1, 164 }, { 47, 0 }, { 1, 6 }, { 1, 41 }, { 1, 9 }, { 12, 0 }, { 1, 43 }, { 1, 246 }, { 1, 255 }, { 1, 206 }, { 1, 29 }, { 1, 192 }, { 11, 255 }, { 1, 191 }, { 47, 0 }, { 1, 28 }, { 1, 29 }, { 12, 0 }, { 1, 9 }, { 1, 211 }, { 1, 255 }, { 1, 221 }, { 1, 49 }, { 1, 209 }, { 12, 255 }, { 1, 218 }, { 46, 0 }, { 1, 5 }, { 1, 40 }, { 1, 5 }, { 12, 0 }, { 1, 151 }, { 2, 255 }, { 1, 125 }, { 1, 223 }, { 13, 255 }, { 1, 223 }, { 46, 0 }, { 1, 22 }, { 1, 20 }, { 12, 0 }, { 1, 82 }, { 18, 255 }, { 1, 121 }, { 45, 0 }, { 1, 1 }, { 1, 32 }, { 12, 0 }, { 1, 30 }, { 1, 238 }, { 17, 255 }, { 1, 139 }, { 1, 2 }, { 45, 0 }, { 1, 15 }, { 1, 8 }, { 11, 0 }, { 1, 3 }, { 1, 195 }, { 15, 255 }, { 1, 253 }, { 1, 186 }, { 1, 63 }, { 47, 0 }, { 1, 11 }, { 12, 0 }, { 1, 128 }, { 13, 255 }, { 1, 247 }, { 1, 179 }, { 1, 99 }, { 1, 22 }, { 48, 0 }, { 1, 1 }, { 12, 0 }, { 1, 62 }, { 1, 252 }, { 10, 255 }, { 1, 238 }, { 1, 163 }, { 1, 83 }, { 1, 12 }, { 63, 0 }, { 1, 18 }, { 1, 227 }, { 8, 255 }, { 1, 226 }, { 1, 146 }, { 1, 66 }, { 1, 4 }, { 66, 0 }, { 1, 175 }, { 6, 255 }, { 1, 209 }, { 1, 130 }, { 1, 50 }, { 69, 0 }, { 1, 100 }, { 3, 255 }, { 1, 252 }, { 1, 193 }, { 1, 113 }, { 1, 34 }, { 72, 0 }, { 1, 216 }, { 1, 246 }, { 1, 176 }, { 1, 97 }, { 1, 21 }, { 75, 0 }, { 1, 17 }, { 1, 4 }, { 349, 0 }, { 1, 18 }, { 1, 13 }, { 76, 0 }, { 1, 19 }, { 1, 42 }, { 1, 11 }, { 75, 0 }, { 1, 15 }, { 1, 49 }, { 1, 43 }, { 1, 3 }, { 74, 0 }, { 1, 5 }, { 1, 40 }, { 1, 59 }, { 1, 27 }, { 75, 0 }, { 1, 26 }, { 1, 59 }, { 1, 45 }, { 1, 7 }, { 74, 0 }, { 1, 12 }, { 1, 50 }, { 1, 57 }, { 1, 22 }, { 75, 0 }, { 1, 27 }, { 1, 60 }, { 1, 38 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 42 }, { 1, 38 }, { 1, 8 }, { 75, 0 }, { 1, 12 }, { 1, 29 }, { 1, 7 }, { 76, 0 }, { 1, 2 }, { 1, 5 }, { 999, 0 } }, + LogoRLEFrame{ { 2042, 0 }, { 1, 2 }, { 1, 144 }, { 1, 220 }, { 1, 25 }, { 1, 4 }, { 75, 0 }, { 1, 132 }, { 2, 255 }, { 1, 64 }, { 75, 0 }, { 1, 62 }, { 1, 253 }, { 2, 255 }, { 1, 55 }, { 74, 0 }, { 1, 16 }, { 1, 225 }, { 3, 255 }, { 1, 43 }, { 74, 0 }, { 1, 169 }, { 4, 255 }, { 1, 19 }, { 55, 0 }, { 2, 5 }, { 16, 0 }, { 1, 96 }, { 4, 255 }, { 1, 201 }, { 56, 0 }, { 1, 26 }, { 16, 0 }, { 1, 35 }, { 1, 243 }, { 3, 255 }, { 1, 252 }, { 1, 66 }, { 55, 0 }, { 1, 20 }, { 1, 21 }, { 15, 0 }, { 1, 4 }, { 1, 201 }, { 4, 255 }, { 1, 105 }, { 55, 0 }, { 1, 3 }, { 1, 50 }, { 1, 1 }, { 15, 0 }, { 1, 132 }, { 4, 255 }, { 1, 134 }, { 56, 0 }, { 1, 37 }, { 1, 32 }, { 15, 0 }, { 1, 62 }, { 1, 253 }, { 3, 255 }, { 1, 160 }, { 56, 0 }, { 1, 12 }, { 1, 60 }, { 1, 6 }, { 14, 0 }, { 1, 16 }, { 1, 225 }, { 3, 255 }, { 1, 183 }, { 1, 4 }, { 1, 0 }, { 1, 28 }, { 1, 132 }, { 1, 143 }, { 1, 31 }, { 51, 0 }, { 1, 46 }, { 1, 34 }, { 15, 0 }, { 1, 169 }, { 3, 255 }, { 1, 202 }, { 1, 11 }, { 1, 0 }, { 1, 73 }, { 1, 243 }, { 2, 255 }, { 1, 165 }, { 50, 0 }, { 1, 17 }, { 1, 59 }, { 1, 5 }, { 14, 0 }, { 1, 95 }, { 3, 255 }, { 1, 219 }, { 1, 21 }, { 1, 0 }, { 1, 94 }, { 1, 251 }, { 3, 255 }, { 1, 197 }, { 50, 0 }, { 1, 51 }, { 1, 32 }, { 14, 0 }, { 1, 35 }, { 1, 242 }, { 2, 255 }, { 1, 232 }, { 1, 34 }, { 1, 0 }, { 1, 118 }, { 5, 255 }, { 1, 229 }, { 49, 0 }, { 1, 24 }, { 1, 58 }, { 1, 3 }, { 13, 0 }, { 1, 4 }, { 1, 200 }, { 2, 255 }, { 1, 243 }, { 1, 51 }, { 1, 1 }, { 1, 143 }, { 7, 255 }, { 1, 7 }, { 48, 0 }, { 1, 55 }, { 1, 29 }, { 14, 0 }, { 1, 132 }, { 2, 255 }, { 1, 250 }, { 1, 70 }, { 1, 6 }, { 1, 167 }, { 8, 255 }, { 1, 38 }, { 47, 0 }, { 1, 20 }, { 1, 52 }, { 1, 1 }, { 13, 0 }, { 1, 61 }, { 1, 253 }, { 2, 255 }, { 1, 93 }, { 1, 13 }, { 1, 187 }, { 9, 255 }, { 1, 70 }, { 47, 0 }, { 1, 47 }, { 1, 15 }, { 13, 0 }, { 1, 16 }, { 1, 225 }, { 2, 255 }, { 1, 118 }, { 1, 23 }, { 1, 205 }, { 10, 255 }, { 1, 102 }, { 46, 0 }, { 1, 11 }, { 1, 37 }, { 14, 0 }, { 1, 168 }, { 2, 255 }, { 1, 145 }, { 1, 36 }, { 1, 221 }, { 11, 255 }, { 1, 134 }, { 46, 0 }, { 1, 29 }, { 1, 2 }, { 13, 0 }, { 1, 95 }, { 2, 255 }, { 1, 170 }, { 1, 53 }, { 1, 233 }, { 12, 255 }, { 1, 166 }, { 45, 0 }, { 1, 3 }, { 1, 10 }, { 13, 0 }, { 1, 35 }, { 1, 242 }, { 1, 255 }, { 1, 226 }, { 1, 89 }, { 1, 243 }, { 13, 255 }, { 1, 187 }, { 59, 0 }, { 1, 4 }, { 1, 200 }, { 18, 255 }, { 1, 136 }, { 59, 0 }, { 1, 131 }, { 18, 255 }, { 1, 190 }, { 1, 9 }, { 58, 0 }, { 1, 61 }, { 1, 253 }, { 16, 255 }, { 1, 237 }, { 1, 125 }, { 1, 4 }, { 58, 0 }, { 1, 16 }, { 1, 225 }, { 14, 255 }, { 1, 235 }, { 1, 161 }, { 1, 84 }, { 1, 11 }, { 60, 0 }, { 1, 168 }, { 12, 255 }, { 1, 213 }, { 1, 136 }, { 1, 59 }, { 1, 2 }, { 62, 0 }, { 1, 94 }, { 9, 255 }, { 1, 250 }, { 1, 188 }, { 1, 111 }, { 1, 34 }, { 65, 0 }, { 1, 34 }, { 1, 242 }, { 6, 255 }, { 1, 236 }, { 1, 162 }, { 1, 85 }, { 1, 14 }, { 67, 0 }, { 1, 1 }, { 1, 198 }, { 4, 255 }, { 1, 214 }, { 1, 137 }, { 1, 60 }, { 1, 3 }, { 70, 0 }, { 1, 67 }, { 1, 255 }, { 1, 251 }, { 1, 189 }, { 1, 112 }, { 1, 35 }, { 74, 0 }, { 1, 9 }, { 1, 57 }, { 1, 14 }, { 111, 0 }, { 1, 6 }, { 1, 11 }, { 76, 0 }, { 1, 5 }, { 1, 32 }, { 1, 20 }, { 75, 0 }, { 1, 4 }, { 1, 33 }, { 1, 51 }, { 1, 11 }, { 75, 0 }, { 1, 24 }, { 1, 57 }, { 1, 42 }, { 1, 3 }, { 74, 0 }, { 1, 11 }, { 1, 48 }, { 1, 56 }, { 1, 20 }, { 74, 0 }, { 1, 3 }, { 1, 35 }, { 1, 61 }, { 1, 38 }, { 1, 3 }, { 74, 0 }, { 1, 14 }, { 1, 55 }, { 1, 53 }, { 1, 15 }, { 75, 0 }, { 1, 28 }, { 1, 52 }, { 1, 24 }, { 75, 0 }, { 1, 4 }, { 1, 34 }, { 1, 23 }, { 76, 0 }, { 1, 4 }, { 1, 15 }, { 1156, 0 } }, + LogoRLEFrame{ { 1807, 0 }, { 2, 3 }, { 77, 0 }, { 1, 5 }, { 1, 34 }, { 74, 0 }, { 1, 15 }, { 1, 88 }, { 1, 9 }, { 1, 7 }, { 1, 52 }, { 1, 10 }, { 73, 0 }, { 1, 14 }, { 1, 210 }, { 1, 255 }, { 1, 94 }, { 1, 51 }, { 1, 33 }, { 57, 0 }, { 1, 2 }, { 16, 0 }, { 1, 160 }, { 2, 255 }, { 1, 151 }, { 1, 47 }, { 57, 0 }, { 1, 13 }, { 1, 1 }, { 15, 0 }, { 1, 80 }, { 3, 255 }, { 1, 178 }, { 1, 5 }, { 56, 0 }, { 1, 2 }, { 1, 27 }, { 15, 0 }, { 1, 21 }, { 1, 233 }, { 3, 255 }, { 1, 157 }, { 57, 0 }, { 1, 35 }, { 1, 9 }, { 15, 0 }, { 1, 175 }, { 4, 255 }, { 1, 143 }, { 56, 0 }, { 1, 13 }, { 1, 45 }, { 15, 0 }, { 1, 94 }, { 5, 255 }, { 1, 73 }, { 55, 0 }, { 1, 1 }, { 1, 52 }, { 1, 20 }, { 14, 0 }, { 1, 29 }, { 1, 240 }, { 4, 255 }, { 1, 199 }, { 1, 1 }, { 55, 0 }, { 1, 26 }, { 1, 52 }, { 14, 0 }, { 1, 1 }, { 1, 189 }, { 4, 255 }, { 1, 227 }, { 1, 24 }, { 55, 0 }, { 1, 3 }, { 1, 57 }, { 1, 20 }, { 14, 0 }, { 1, 109 }, { 4, 255 }, { 1, 241 }, { 1, 46 }, { 56, 0 }, { 1, 32 }, { 1, 50 }, { 14, 0 }, { 1, 39 }, { 1, 246 }, { 3, 255 }, { 1, 250 }, { 1, 68 }, { 56, 0 }, { 1, 6 }, { 1, 60 }, { 1, 18 }, { 13, 0 }, { 1, 3 }, { 1, 201 }, { 4, 255 }, { 1, 93 }, { 2, 0 }, { 1, 24 }, { 1, 97 }, { 1, 81 }, { 1, 1 }, { 51, 0 }, { 1, 38 }, { 1, 48 }, { 14, 0 }, { 1, 124 }, { 4, 255 }, { 1, 122 }, { 2, 0 }, { 1, 83 }, { 1, 240 }, { 2, 255 }, { 1, 113 }, { 50, 0 }, { 1, 7 }, { 1, 61 }, { 1, 14 }, { 13, 0 }, { 1, 50 }, { 1, 250 }, { 3, 255 }, { 1, 153 }, { 2, 0 }, { 1, 109 }, { 1, 253 }, { 3, 255 }, { 1, 164 }, { 50, 0 }, { 1, 33 }, { 1, 37 }, { 13, 0 }, { 1, 7 }, { 1, 212 }, { 3, 255 }, { 1, 180 }, { 1, 3 }, { 1, 1 }, { 1, 136 }, { 5, 255 }, { 1, 206 }, { 49, 0 }, { 1, 2 }, { 1, 52 }, { 1, 4 }, { 13, 0 }, { 1, 139 }, { 3, 255 }, { 1, 203 }, { 1, 11 }, { 1, 5 }, { 1, 163 }, { 6, 255 }, { 1, 247 }, { 1, 2 }, { 48, 0 }, { 1, 24 }, { 1, 20 }, { 13, 0 }, { 1, 61 }, { 1, 253 }, { 2, 255 }, { 1, 221 }, { 1, 22 }, { 1, 12 }, { 1, 186 }, { 8, 255 }, { 1, 35 }, { 48, 0 }, { 1, 27 }, { 13, 0 }, { 1, 12 }, { 1, 222 }, { 2, 255 }, { 1, 236 }, { 1, 38 }, { 1, 23 }, { 1, 205 }, { 9, 255 }, { 1, 78 }, { 47, 0 }, { 1, 6 }, { 1, 2 }, { 13, 0 }, { 1, 154 }, { 2, 255 }, { 1, 247 }, { 1, 58 }, { 1, 37 }, { 1, 222 }, { 10, 255 }, { 1, 120 }, { 61, 0 }, { 1, 74 }, { 2, 255 }, { 1, 253 }, { 1, 82 }, { 1, 55 }, { 1, 235 }, { 11, 255 }, { 1, 162 }, { 60, 0 }, { 1, 19 }, { 1, 230 }, { 2, 255 }, { 1, 110 }, { 1, 75 }, { 1, 245 }, { 12, 255 }, { 1, 205 }, { 60, 0 }, { 1, 169 }, { 2, 255 }, { 1, 180 }, { 1, 103 }, { 1, 251 }, { 13, 255 }, { 1, 242 }, { 59, 0 }, { 1, 89 }, { 19, 255 }, { 1, 232 }, { 58, 0 }, { 1, 26 }, { 1, 238 }, { 19, 255 }, { 1, 87 }, { 58, 0 }, { 1, 183 }, { 18, 255 }, { 1, 233 }, { 1, 78 }, { 58, 0 }, { 1, 104 }, { 16, 255 }, { 1, 243 }, { 1, 176 }, { 1, 98 }, { 1, 8 }, { 58, 0 }, { 1, 35 }, { 1, 244 }, { 13, 255 }, { 1, 209 }, { 1, 136 }, { 1, 64 }, { 1, 6 }, { 60, 0 }, { 1, 2 }, { 1, 196 }, { 10, 255 }, { 1, 238 }, { 1, 169 }, { 1, 97 }, { 1, 26 }, { 64, 0 }, { 1, 119 }, { 7, 255 }, { 1, 253 }, { 1, 201 }, { 1, 129 }, { 1, 57 }, { 1, 3 }, { 66, 0 }, { 1, 39 }, { 1, 249 }, { 4, 255 }, { 1, 232 }, { 1, 161 }, { 1, 89 }, { 1, 20 }, { 70, 0 }, { 1, 139 }, { 1, 255 }, { 1, 251 }, { 1, 193 }, { 1, 121 }, { 1, 49 }, { 1, 1 }, { 31, 0 }, { 1, 7 }, { 41, 0 }, { 1, 23 }, { 1, 62 }, { 1, 14 }, { 33, 0 }, { 1, 18 }, { 1, 24 }, { 1, 1 }, { 75, 0 }, { 1, 16 }, { 1, 46 }, { 1, 22 }, { 75, 0 }, { 1, 10 }, { 1, 46 }, { 1, 53 }, { 1, 11 }, { 74, 0 }, { 1, 2 }, { 1, 33 }, { 1, 60 }, { 1, 35 }, { 1, 2 }, { 74, 0 }, { 1, 19 }, { 1, 55 }, { 1, 51 }, { 1, 13 }, { 74, 0 }, { 1, 5 }, { 1, 43 }, { 1, 61 }, { 1, 30 }, { 1, 1 }, { 74, 0 }, { 1, 15 }, { 1, 55 }, { 1, 40 }, { 1, 9 }, { 75, 0 }, { 1, 29 }, { 1, 38 }, { 1, 9 }, { 75, 0 }, { 1, 4 }, { 1, 22 }, { 1, 8 }, { 77, 0 }, { 1, 2 }, { 1234, 0 } }, + LogoRLEFrame{ { 1649, 0 }, { 1, 23 }, { 1, 2 }, { 77, 0 }, { 1, 33 }, { 1, 24 }, { 77, 0 }, { 1, 33 }, { 1, 48 }, { 56, 0 }, { 2, 2 }, { 15, 0 }, { 1, 17 }, { 3, 0 }, { 1, 24 }, { 1, 59 }, { 1, 9 }, { 56, 0 }, { 1, 18 }, { 15, 0 }, { 1, 152 }, { 1, 255 }, { 1, 98 }, { 1, 0 }, { 1, 15 }, { 1, 61 }, { 1, 19 }, { 56, 0 }, { 1, 12 }, { 1, 20 }, { 14, 0 }, { 1, 98 }, { 2, 255 }, { 1, 169 }, { 1, 5 }, { 1, 58 }, { 1, 32 }, { 57, 0 }, { 1, 45 }, { 1, 1 }, { 13, 0 }, { 1, 24 }, { 1, 239 }, { 2, 255 }, { 1, 216 }, { 1, 43 }, { 1, 39 }, { 57, 0 }, { 1, 29 }, { 1, 33 }, { 14, 0 }, { 1, 173 }, { 3, 255 }, { 1, 250 }, { 1, 36 }, { 57, 0 }, { 1, 8 }, { 1, 59 }, { 1, 8 }, { 13, 0 }, { 1, 82 }, { 5, 255 }, { 1, 2 }, { 57, 0 }, { 1, 41 }, { 1, 39 }, { 13, 0 }, { 1, 16 }, { 1, 230 }, { 5, 255 }, { 1, 7 }, { 56, 0 }, { 1, 12 }, { 1, 61 }, { 1, 7 }, { 13, 0 }, { 1, 156 }, { 5, 255 }, { 1, 198 }, { 57, 0 }, { 1, 47 }, { 1, 36 }, { 13, 0 }, { 1, 66 }, { 6, 255 }, { 1, 85 }, { 56, 0 }, { 1, 18 }, { 1, 60 }, { 1, 6 }, { 12, 0 }, { 1, 10 }, { 1, 221 }, { 5, 255 }, { 1, 152 }, { 57, 0 }, { 1, 52 }, { 1, 34 }, { 13, 0 }, { 1, 140 }, { 5, 255 }, { 1, 192 }, { 1, 6 }, { 56, 0 }, { 1, 19 }, { 1, 57 }, { 1, 4 }, { 12, 0 }, { 1, 52 }, { 1, 252 }, { 4, 255 }, { 1, 216 }, { 1, 17 }, { 57, 0 }, { 1, 46 }, { 1, 22 }, { 12, 0 }, { 1, 5 }, { 1, 209 }, { 4, 255 }, { 1, 235 }, { 1, 35 }, { 3, 0 }, { 1, 23 }, { 1, 7 }, { 52, 0 }, { 1, 10 }, { 1, 46 }, { 13, 0 }, { 1, 123 }, { 4, 255 }, { 1, 247 }, { 1, 58 }, { 2, 0 }, { 1, 48 }, { 1, 206 }, { 1, 255 }, { 1, 246 }, { 1, 77 }, { 51, 0 }, { 1, 33 }, { 1, 7 }, { 12, 0 }, { 1, 40 }, { 1, 248 }, { 4, 255 }, { 1, 87 }, { 2, 0 }, { 1, 73 }, { 1, 244 }, { 3, 255 }, { 1, 173 }, { 50, 0 }, { 1, 4 }, { 1, 18 }, { 12, 0 }, { 1, 1 }, { 1, 196 }, { 4, 255 }, { 1, 121 }, { 2, 0 }, { 1, 100 }, { 1, 251 }, { 4, 255 }, { 1, 233 }, { 50, 0 }, { 1, 5 }, { 13, 0 }, { 1, 107 }, { 4, 255 }, { 1, 157 }, { 2, 0 }, { 1, 131 }, { 7, 255 }, { 1, 36 }, { 62, 0 }, { 1, 29 }, { 1, 242 }, { 3, 255 }, { 1, 188 }, { 2, 5 }, { 1, 160 }, { 8, 255 }, { 1, 95 }, { 62, 0 }, { 1, 181 }, { 3, 255 }, { 1, 213 }, { 1, 15 }, { 1, 14 }, { 1, 187 }, { 9, 255 }, { 1, 154 }, { 61, 0 }, { 1, 90 }, { 3, 255 }, { 1, 232 }, { 1, 32 }, { 1, 27 }, { 1, 209 }, { 10, 255 }, { 1, 214 }, { 60, 0 }, { 1, 20 }, { 1, 235 }, { 2, 255 }, { 1, 246 }, { 1, 54 }, { 1, 44 }, { 1, 227 }, { 12, 255 }, { 1, 18 }, { 59, 0 }, { 1, 165 }, { 3, 255 }, { 1, 82 }, { 1, 65 }, { 1, 240 }, { 13, 255 }, { 1, 76 }, { 58, 0 }, { 1, 74 }, { 3, 255 }, { 1, 166 }, { 1, 94 }, { 1, 249 }, { 14, 255 }, { 1, 134 }, { 57, 0 }, { 1, 13 }, { 1, 226 }, { 20, 255 }, { 1, 165 }, { 57, 0 }, { 1, 148 }, { 21, 255 }, { 1, 89 }, { 56, 0 }, { 1, 60 }, { 21, 255 }, { 1, 137 }, { 56, 0 }, { 1, 7 }, { 1, 215 }, { 18, 255 }, { 1, 252 }, { 1, 185 }, { 1, 68 }, { 57, 0 }, { 1, 132 }, { 15, 255 }, { 1, 253 }, { 1, 205 }, { 1, 140 }, { 1, 75 }, { 1, 14 }, { 58, 0 }, { 1, 46 }, { 1, 250 }, { 12, 255 }, { 1, 209 }, { 1, 144 }, { 1, 79 }, { 1, 17 }, { 61, 0 }, { 1, 3 }, { 1, 203 }, { 9, 255 }, { 1, 212 }, { 1, 148 }, { 1, 83 }, { 1, 20 }, { 26, 0 }, { 1, 1 }, { 38, 0 }, { 1, 105 }, { 6, 255 }, { 1, 216 }, { 1, 151 }, { 1, 86 }, { 1, 22 }, { 28, 0 }, { 1, 5 }, { 1, 21 }, { 1, 4 }, { 38, 0 }, { 1, 183 }, { 2, 255 }, { 1, 220 }, { 1, 155 }, { 1, 90 }, { 1, 26 }, { 30, 0 }, { 1, 3 }, { 1, 32 }, { 1, 33 }, { 1, 1 }, { 39, 0 }, { 1, 33 }, { 1, 74 }, { 1, 29 }, { 32, 0 }, { 1, 1 }, { 1, 29 }, { 1, 56 }, { 1, 22 }, { 75, 0 }, { 1, 17 }, { 1, 53 }, { 1, 50 }, { 1, 10 }, { 74, 0 }, { 1, 6 }, { 1, 42 }, { 1, 60 }, { 1, 28 }, { 75, 0 }, { 1, 28 }, { 1, 59 }, { 1, 45 }, { 1, 8 }, { 74, 0 }, { 1, 6 }, { 1, 46 }, { 1, 55 }, { 1, 22 }, { 75, 0 }, { 1, 16 }, { 1, 49 }, { 1, 24 }, { 1, 1 }, { 75, 0 }, { 2, 23 }, { 77, 0 }, { 1, 8 }, { 1391, 0 } }, + LogoRLEFrame{ { 1411, 0 }, { 1, 8 }, { 1, 2 }, { 77, 0 }, { 1, 11 }, { 1, 33 }, { 77, 0 }, { 1, 13 }, { 1, 55 }, { 1, 5 }, { 55, 0 }, { 1, 6 }, { 20, 0 }, { 1, 7 }, { 1, 57 }, { 1, 24 }, { 56, 0 }, { 1, 21 }, { 19, 0 }, { 1, 3 }, { 1, 51 }, { 1, 39 }, { 56, 0 }, { 1, 27 }, { 1, 9 }, { 19, 0 }, { 1, 44 }, { 1, 50 }, { 1, 1 }, { 55, 0 }, { 1, 7 }, { 1, 43 }, { 15, 0 }, { 1, 8 }, { 3, 0 }, { 1, 26 }, { 1, 57 }, { 1, 7 }, { 56, 0 }, { 1, 45 }, { 1, 20 }, { 13, 0 }, { 1, 15 }, { 1, 200 }, { 1, 249 }, { 1, 54 }, { 1, 0 }, { 1, 7 }, { 1, 52 }, { 1, 9 }, { 56, 0 }, { 1, 21 }, { 1, 55 }, { 14, 0 }, { 1, 158 }, { 2, 255 }, { 1, 150 }, { 1, 0 }, { 1, 36 }, { 1, 7 }, { 56, 0 }, { 1, 1 }, { 1, 54 }, { 1, 24 }, { 13, 0 }, { 1, 55 }, { 3, 255 }, { 1, 216 }, { 1, 13 }, { 1, 5 }, { 57, 0 }, { 1, 27 }, { 1, 53 }, { 13, 0 }, { 1, 2 }, { 1, 203 }, { 4, 255 }, { 1, 25 }, { 57, 0 }, { 1, 3 }, { 1, 58 }, { 1, 22 }, { 13, 0 }, { 1, 100 }, { 5, 255 }, { 1, 60 }, { 57, 0 }, { 1, 33 }, { 1, 51 }, { 13, 0 }, { 1, 17 }, { 1, 235 }, { 5, 255 }, { 1, 88 }, { 56, 0 }, { 1, 6 }, { 1, 60 }, { 1, 19 }, { 13, 0 }, { 1, 147 }, { 6, 255 }, { 1, 109 }, { 56, 0 }, { 1, 32 }, { 1, 45 }, { 13, 0 }, { 1, 46 }, { 1, 252 }, { 6, 255 }, { 1, 54 }, { 55, 0 }, { 1, 1 }, { 1, 55 }, { 1, 8 }, { 13, 0 }, { 1, 193 }, { 6, 255 }, { 1, 211 }, { 1, 1 }, { 55, 0 }, { 1, 23 }, { 1, 29 }, { 13, 0 }, { 1, 90 }, { 6, 255 }, { 1, 251 }, { 1, 50 }, { 56, 0 }, { 1, 36 }, { 13, 0 }, { 1, 12 }, { 1, 229 }, { 6, 255 }, { 1, 106 }, { 56, 0 }, { 1, 11 }, { 1, 7 }, { 13, 0 }, { 1, 137 }, { 6, 255 }, { 1, 148 }, { 57, 0 }, { 1, 2 }, { 13, 0 }, { 1, 38 }, { 1, 249 }, { 5, 255 }, { 1, 187 }, { 1, 4 }, { 71, 0 }, { 1, 183 }, { 5, 255 }, { 1, 217 }, { 1, 16 }, { 2, 0 }, { 1, 4 }, { 1, 109 }, { 1, 172 }, { 1, 145 }, { 1, 26 }, { 64, 0 }, { 1, 79 }, { 5, 255 }, { 1, 238 }, { 1, 37 }, { 2, 0 }, { 1, 22 }, { 1, 199 }, { 3, 255 }, { 1, 194 }, { 63, 0 }, { 1, 8 }, { 1, 222 }, { 4, 255 }, { 1, 251 }, { 1, 67 }, { 2, 0 }, { 1, 41 }, { 1, 222 }, { 4, 255 }, { 1, 253 }, { 1, 22 }, { 62, 0 }, { 1, 126 }, { 5, 255 }, { 1, 106 }, { 2, 0 }, { 1, 67 }, { 1, 240 }, { 6, 255 }, { 1, 100 }, { 61, 0 }, { 1, 31 }, { 1, 246 }, { 4, 255 }, { 1, 148 }, { 2, 0 }, { 1, 99 }, { 1, 250 }, { 7, 255 }, { 1, 181 }, { 61, 0 }, { 1, 173 }, { 4, 255 }, { 1, 187 }, { 1, 4 }, { 1, 1 }, { 1, 136 }, { 9, 255 }, { 1, 249 }, { 1, 13 }, { 59, 0 }, { 1, 69 }, { 4, 255 }, { 1, 217 }, { 1, 16 }, { 1, 9 }, { 1, 171 }, { 11, 255 }, { 1, 87 }, { 58, 0 }, { 1, 5 }, { 1, 215 }, { 3, 255 }, { 1, 238 }, { 1, 37 }, { 1, 22 }, { 1, 200 }, { 12, 255 }, { 1, 168 }, { 58, 0 }, { 1, 115 }, { 3, 255 }, { 1, 251 }, { 1, 67 }, { 1, 42 }, { 1, 223 }, { 13, 255 }, { 1, 242 }, { 1, 7 }, { 56, 0 }, { 1, 24 }, { 1, 242 }, { 3, 255 }, { 1, 173 }, { 1, 72 }, { 1, 240 }, { 15, 255 }, { 1, 74 }, { 56, 0 }, { 1, 162 }, { 22, 255 }, { 1, 144 }, { 55, 0 }, { 1, 59 }, { 23, 255 }, { 1, 164 }, { 54, 0 }, { 1, 2 }, { 1, 206 }, { 22, 255 }, { 1, 251 }, { 1, 54 }, { 54, 0 }, { 1, 105 }, { 22, 255 }, { 1, 236 }, { 1, 69 }, { 54, 0 }, { 1, 19 }, { 1, 237 }, { 19, 255 }, { 1, 224 }, { 1, 168 }, { 1, 98 }, { 1, 9 }, { 16, 0 }, { 1, 12 }, { 1, 4 }, { 37, 0 }, { 1, 151 }, { 15, 255 }, { 1, 243 }, { 1, 190 }, { 1, 134 }, { 1, 79 }, { 1, 24 }, { 18, 0 }, { 1, 15 }, { 1, 34 }, { 1, 7 }, { 37, 0 }, { 1, 49 }, { 1, 253 }, { 10, 255 }, { 1, 253 }, { 1, 211 }, { 1, 155 }, { 1, 100 }, { 1, 45 }, { 1, 2 }, { 20, 0 }, { 1, 13 }, { 1, 46 }, { 1, 37 }, { 1, 1 }, { 38, 0 }, { 1, 179 }, { 7, 255 }, { 1, 232 }, { 1, 177 }, { 1, 121 }, { 1, 66 }, { 1, 13 }, { 4, 0 }, { 1, 2 }, { 1, 8 }, { 17, 0 }, { 1, 5 }, { 1, 39 }, { 1, 59 }, { 1, 23 }, { 40, 0 }, { 1, 223 }, { 2, 255 }, { 1, 248 }, { 1, 198 }, { 1, 142 }, { 1, 87 }, { 1, 32 }, { 8, 0 }, { 1, 9 }, { 1, 20 }, { 17, 0 }, { 1, 26 }, { 1, 59 }, { 1, 43 }, { 1, 6 }, { 41, 0 }, { 1, 39 }, { 1, 88 }, { 1, 53 }, { 1, 6 }, { 11, 0 }, { 1, 21 }, { 1, 25 }, { 16, 0 }, { 1, 13 }, { 1, 50 }, { 1, 56 }, { 1, 20 }, { 56, 0 }, { 1, 5 }, { 1, 32 }, { 1, 26 }, { 16, 0 }, { 1, 32 }, { 1, 62 }, { 1, 38 }, { 1, 3 }, { 56, 0 }, { 1, 10 }, { 1, 38 }, { 1, 24 }, { 15, 0 }, { 1, 6 }, { 1, 47 }, { 1, 41 }, { 1, 11 }, { 57, 0 }, { 1, 17 }, { 1, 40 }, { 1, 18 }, { 15, 0 }, { 1, 17 }, { 1, 35 }, { 1, 10 }, { 58, 0 }, { 1, 24 }, { 1, 40 }, { 1, 12 }, { 15, 0 }, { 1, 11 }, { 1, 9 }, { 58, 0 }, { 1, 1 }, { 1, 31 }, { 1, 37 }, { 1, 8 }, { 75, 0 }, { 1, 2 }, { 1, 32 }, { 1, 31 }, { 1, 4 }, { 75, 0 }, { 1, 2 }, { 1, 31 }, { 1, 20 }, { 76, 0 }, { 1, 2 }, { 1, 25 }, { 1, 9 }, { 76, 0 }, { 1, 1 }, { 1, 11 }, { 1171, 0 } }, + LogoRLEFrame{ { 1174, 0 }, { 1, 1 }, { 78, 0 }, { 1, 29 }, { 77, 0 }, { 1, 1 }, { 1, 43 }, { 1, 17 }, { 55, 0 }, { 1, 11 }, { 21, 0 }, { 1, 43 }, { 1, 42 }, { 55, 0 }, { 1, 7 }, { 1, 18 }, { 20, 0 }, { 1, 33 }, { 1, 54 }, { 1, 4 }, { 55, 0 }, { 1, 38 }, { 1, 1 }, { 19, 0 }, { 1, 24 }, { 1, 60 }, { 1, 12 }, { 55, 0 }, { 1, 21 }, { 1, 33 }, { 19, 0 }, { 1, 11 }, { 1, 61 }, { 1, 23 }, { 55, 0 }, { 1, 4 }, { 1, 57 }, { 1, 8 }, { 19, 0 }, { 1, 50 }, { 1, 29 }, { 56, 0 }, { 1, 36 }, { 1, 42 }, { 19, 0 }, { 1, 29 }, { 1, 25 }, { 56, 0 }, { 1, 9 }, { 1, 61 }, { 1, 10 }, { 13, 0 }, { 1, 44 }, { 1, 19 }, { 3, 0 }, { 1, 9 }, { 1, 20 }, { 57, 0 }, { 1, 42 }, { 1, 40 }, { 13, 0 }, { 1, 125 }, { 1, 255 }, { 1, 232 }, { 1, 19 }, { 2, 0 }, { 1, 5 }, { 57, 0 }, { 1, 14 }, { 1, 61 }, { 1, 9 }, { 12, 0 }, { 1, 38 }, { 1, 251 }, { 2, 255 }, { 1, 107 }, { 60, 0 }, { 1, 48 }, { 1, 38 }, { 13, 0 }, { 1, 170 }, { 3, 255 }, { 1, 191 }, { 59, 0 }, { 1, 17 }, { 1, 60 }, { 1, 7 }, { 12, 0 }, { 1, 51 }, { 4, 255 }, { 1, 252 }, { 1, 22 }, { 58, 0 }, { 1, 45 }, { 1, 30 }, { 13, 0 }, { 1, 186 }, { 5, 255 }, { 1, 85 }, { 57, 0 }, { 1, 9 }, { 1, 52 }, { 1, 1 }, { 12, 0 }, { 1, 66 }, { 6, 255 }, { 1, 132 }, { 57, 0 }, { 1, 35 }, { 1, 14 }, { 13, 0 }, { 1, 202 }, { 6, 255 }, { 1, 180 }, { 56, 0 }, { 1, 3 }, { 1, 29 }, { 13, 0 }, { 1, 83 }, { 7, 255 }, { 1, 210 }, { 56, 0 }, { 1, 14 }, { 13, 0 }, { 1, 3 }, { 1, 216 }, { 7, 255 }, { 1, 167 }, { 70, 0 }, { 1, 99 }, { 8, 255 }, { 1, 86 }, { 69, 0 }, { 1, 8 }, { 1, 227 }, { 7, 255 }, { 1, 192 }, { 70, 0 }, { 1, 116 }, { 7, 255 }, { 1, 235 }, { 1, 31 }, { 69, 0 }, { 1, 15 }, { 1, 237 }, { 6, 255 }, { 1, 251 }, { 1, 66 }, { 70, 0 }, { 1, 132 }, { 7, 255 }, { 1, 112 }, { 70, 0 }, { 1, 24 }, { 1, 244 }, { 6, 255 }, { 1, 163 }, { 4, 0 }, { 1, 7 }, { 1, 45 }, { 1, 27 }, { 64, 0 }, { 1, 149 }, { 6, 255 }, { 1, 205 }, { 1, 9 }, { 3, 0 }, { 1, 81 }, { 1, 231 }, { 2, 255 }, { 1, 173 }, { 1, 2 }, { 61, 0 }, { 1, 35 }, { 1, 250 }, { 5, 255 }, { 1, 234 }, { 1, 30 }, { 2, 0 }, { 1, 1 }, { 1, 129 }, { 5, 255 }, { 1, 76 }, { 61, 0 }, { 1, 165 }, { 5, 255 }, { 1, 251 }, { 1, 64 }, { 2, 0 }, { 1, 11 }, { 1, 173 }, { 6, 255 }, { 1, 184 }, { 60, 0 }, { 1, 47 }, { 6, 255 }, { 1, 110 }, { 2, 0 }, { 1, 31 }, { 1, 208 }, { 8, 255 }, { 1, 37 }, { 59, 0 }, { 1, 181 }, { 5, 255 }, { 1, 161 }, { 2, 0 }, { 1, 60 }, { 1, 234 }, { 9, 255 }, { 1, 143 }, { 58, 0 }, { 1, 62 }, { 5, 255 }, { 1, 204 }, { 1, 8 }, { 1, 0 }, { 1, 99 }, { 1, 249 }, { 10, 255 }, { 1, 240 }, { 1, 11 }, { 57, 0 }, { 1, 198 }, { 4, 255 }, { 1, 233 }, { 1, 29 }, { 1, 3 }, { 1, 145 }, { 13, 255 }, { 1, 102 }, { 56, 0 }, { 1, 78 }, { 4, 255 }, { 1, 251 }, { 1, 63 }, { 1, 17 }, { 1, 186 }, { 14, 255 }, { 1, 210 }, { 55, 0 }, { 1, 2 }, { 1, 212 }, { 4, 255 }, { 1, 209 }, { 1, 49 }, { 1, 218 }, { 16, 255 }, { 1, 61 }, { 54, 0 }, { 1, 95 }, { 24, 255 }, { 1, 169 }, { 53, 0 }, { 1, 7 }, { 1, 224 }, { 24, 255 }, { 1, 250 }, { 1, 12 }, { 14, 0 }, { 1, 3 }, { 1, 2 }, { 36, 0 }, { 1, 111 }, { 25, 255 }, { 1, 237 }, { 1, 8 }, { 12, 0 }, { 1, 3 }, { 1, 26 }, { 1, 13 }, { 36, 0 }, { 1, 13 }, { 1, 234 }, { 25, 255 }, { 1, 109 }, { 11, 0 }, { 1, 2 }, { 1, 29 }, { 1, 45 }, { 1, 7 }, { 37, 0 }, { 1, 128 }, { 24, 255 }, { 1, 222 }, { 1, 92 }, { 11, 0 }, { 1, 24 }, { 1, 57 }, { 1, 37 }, { 1, 1 }, { 37, 0 }, { 1, 21 }, { 1, 243 }, { 19, 255 }, { 1, 240 }, { 1, 195 }, { 1, 150 }, { 1, 105 }, { 1, 57 }, { 1, 1 }, { 10, 0 }, { 1, 11 }, { 1, 48 }, { 1, 55 }, { 1, 18 }, { 39, 0 }, { 1, 144 }, { 14, 255 }, { 1, 250 }, { 1, 211 }, { 1, 183 }, { 1, 139 }, { 1, 74 }, { 1, 29 }, { 14, 0 }, { 1, 3 }, { 1, 35 }, { 1, 61 }, { 1, 36 }, { 1, 3 }, { 39, 0 }, { 1, 2 }, { 1, 239 }, { 9, 255 }, { 1, 225 }, { 1, 180 }, { 1, 134 }, { 1, 89 }, { 1, 44 }, { 1, 23 }, { 1, 59 }, { 1, 31 }, { 16, 0 }, { 1, 18 }, { 1, 56 }, { 1, 52 }, { 1, 14 }, { 41, 0 }, { 1, 3 }, { 1, 231 }, { 3, 255 }, { 1, 240 }, { 1, 195 }, { 1, 149 }, { 1, 104 }, { 1, 59 }, { 1, 15 }, { 4, 0 }, { 1, 31 }, { 1, 61 }, { 1, 22 }, { 15, 0 }, { 1, 1 }, { 1, 34 }, { 1, 56 }, { 1, 27 }, { 1, 1 }, { 43, 0 }, { 1, 30 }, { 1, 92 }, { 1, 74 }, { 1, 29 }, { 8, 0 }, { 1, 2 }, { 1, 42 }, { 1, 58 }, { 1, 15 }, { 15, 0 }, { 1, 7 }, { 1, 42 }, { 1, 26 }, { 1, 1 }, { 56, 0 }, { 1, 2 }, { 1, 47 }, { 1, 52 }, { 1, 9 }, { 15, 0 }, { 1, 12 }, { 1, 21 }, { 1, 1 }, { 57, 0 }, { 1, 2 }, { 1, 47 }, { 1, 38 }, { 1, 2 }, { 16, 0 }, { 1, 1 }, { 58, 0 }, { 1, 3 }, { 1, 44 }, { 1, 21 }, { 76, 0 }, { 1, 3 }, { 1, 27 }, { 1, 5 }, { 77, 0 }, { 1, 4 }, { 1407, 0 } }, + LogoRLEFrame{ { 1015, 0 }, { 1, 6 }, { 1, 1 }, { 54, 0 }, { 1, 1 }, { 22, 0 }, { 1, 9 }, { 1, 14 }, { 55, 0 }, { 1, 14 }, { 21, 0 }, { 1, 10 }, { 1, 26 }, { 1, 1 }, { 54, 0 }, { 1, 19 }, { 1, 9 }, { 20, 0 }, { 1, 7 }, { 1, 29 }, { 1, 8 }, { 54, 0 }, { 1, 3 }, { 1, 39 }, { 20, 0 }, { 1, 3 }, { 1, 28 }, { 1, 14 }, { 55, 0 }, { 1, 38 }, { 1, 20 }, { 19, 0 }, { 1, 1 }, { 1, 25 }, { 1, 21 }, { 55, 0 }, { 1, 15 }, { 1, 56 }, { 20, 0 }, { 1, 17 }, { 1, 24 }, { 1, 1 }, { 55, 0 }, { 1, 50 }, { 1, 28 }, { 19, 0 }, { 1, 6 }, { 1, 23 }, { 1, 1 }, { 55, 0 }, { 1, 22 }, { 1, 56 }, { 1, 2 }, { 19, 0 }, { 1, 17 }, { 1, 1 }, { 55, 0 }, { 1, 1 }, { 1, 55 }, { 1, 26 }, { 19, 0 }, { 1, 6 }, { 57, 0 }, { 1, 29 }, { 1, 55 }, { 1, 1 }, { 76, 0 }, { 1, 4 }, { 1, 58 }, { 1, 23 }, { 11, 0 }, { 1, 53 }, { 1, 218 }, { 1, 196 }, { 1, 20 }, { 62, 0 }, { 1, 31 }, { 1, 51 }, { 11, 0 }, { 1, 2 }, { 1, 217 }, { 2, 255 }, { 1, 155 }, { 61, 0 }, { 1, 1 }, { 1, 56 }, { 1, 14 }, { 11, 0 }, { 1, 87 }, { 3, 255 }, { 1, 243 }, { 1, 12 }, { 60, 0 }, { 1, 22 }, { 1, 37 }, { 12, 0 }, { 1, 207 }, { 4, 255 }, { 1, 99 }, { 60, 0 }, { 1, 42 }, { 1, 4 }, { 11, 0 }, { 1, 73 }, { 5, 255 }, { 1, 198 }, { 59, 0 }, { 1, 13 }, { 1, 15 }, { 12, 0 }, { 1, 194 }, { 6, 255 }, { 1, 20 }, { 58, 0 }, { 1, 10 }, { 12, 0 }, { 1, 59 }, { 7, 255 }, { 1, 82 }, { 71, 0 }, { 1, 180 }, { 7, 255 }, { 1, 146 }, { 70, 0 }, { 1, 47 }, { 8, 255 }, { 1, 206 }, { 70, 0 }, { 1, 167 }, { 8, 255 }, { 1, 198 }, { 69, 0 }, { 1, 35 }, { 1, 252 }, { 8, 255 }, { 1, 162 }, { 69, 0 }, { 1, 153 }, { 9, 255 }, { 1, 53 }, { 68, 0 }, { 1, 25 }, { 1, 248 }, { 8, 255 }, { 1, 167 }, { 69, 0 }, { 1, 139 }, { 8, 255 }, { 1, 218 }, { 1, 14 }, { 68, 0 }, { 1, 17 }, { 1, 243 }, { 7, 255 }, { 1, 246 }, { 1, 46 }, { 69, 0 }, { 1, 126 }, { 8, 255 }, { 1, 95 }, { 69, 0 }, { 1, 11 }, { 1, 236 }, { 7, 255 }, { 1, 154 }, { 5, 0 }, { 1, 20 }, { 1, 50 }, { 1, 15 }, { 62, 0 }, { 1, 112 }, { 7, 255 }, { 1, 204 }, { 1, 7 }, { 3, 0 }, { 1, 8 }, { 1, 144 }, { 1, 252 }, { 1, 255 }, { 1, 253 }, { 1, 148 }, { 60, 0 }, { 1, 6 }, { 1, 227 }, { 6, 255 }, { 1, 238 }, { 1, 33 }, { 3, 0 }, { 1, 31 }, { 1, 204 }, { 5, 255 }, { 1, 83 }, { 59, 0 }, { 1, 98 }, { 7, 255 }, { 1, 76 }, { 3, 0 }, { 1, 69 }, { 1, 236 }, { 6, 255 }, { 1, 215 }, { 1, 3 }, { 57, 0 }, { 1, 2 }, { 1, 217 }, { 6, 255 }, { 1, 133 }, { 2, 0 }, { 1, 1 }, { 1, 121 }, { 1, 253 }, { 8, 255 }, { 1, 97 }, { 57, 0 }, { 1, 85 }, { 6, 255 }, { 1, 189 }, { 1, 3 }, { 1, 0 }, { 1, 14 }, { 1, 175 }, { 10, 255 }, { 1, 225 }, { 1, 7 }, { 56, 0 }, { 1, 205 }, { 5, 255 }, { 1, 229 }, { 1, 22 }, { 1, 0 }, { 1, 42 }, { 1, 217 }, { 12, 255 }, { 1, 111 }, { 55, 0 }, { 1, 71 }, { 5, 255 }, { 1, 251 }, { 1, 59 }, { 1, 0 }, { 1, 86 }, { 1, 243 }, { 13, 255 }, { 1, 234 }, { 1, 12 }, { 54, 0 }, { 1, 192 }, { 5, 255 }, { 1, 150 }, { 1, 4 }, { 1, 141 }, { 16, 255 }, { 1, 125 }, { 53, 0 }, { 1, 58 }, { 6, 255 }, { 1, 228 }, { 1, 202 }, { 17, 255 }, { 1, 241 }, { 1, 19 }, { 52, 0 }, { 1, 179 }, { 26, 255 }, { 1, 140 }, { 51, 0 }, { 1, 45 }, { 27, 255 }, { 1, 245 }, { 1, 13 }, { 50, 0 }, { 1, 165 }, { 28, 255 }, { 1, 25 }, { 49, 0 }, { 1, 34 }, { 1, 252 }, { 27, 255 }, { 1, 179 }, { 50, 0 }, { 1, 151 }, { 27, 255 }, { 1, 170 }, { 1, 11 }, { 49, 0 }, { 1, 24 }, { 1, 248 }, { 21, 255 }, { 1, 248 }, { 1, 214 }, { 1, 178 }, { 1, 142 }, { 1, 105 }, { 1, 31 }, { 51, 0 }, { 1, 111 }, { 15, 255 }, { 1, 248 }, { 1, 219 }, { 1, 176 }, { 1, 139 }, { 1, 103 }, { 1, 67 }, { 1, 31 }, { 1, 2 }, { 56, 0 }, { 1, 127 }, { 8, 255 }, { 1, 245 }, { 1, 209 }, { 1, 173 }, { 1, 137 }, { 1, 101 }, { 1, 66 }, { 1, 69 }, { 1, 29 }, { 63, 0 }, { 1, 24 }, { 1, 184 }, { 1, 227 }, { 1, 207 }, { 1, 171 }, { 1, 134 }, { 1, 98 }, { 1, 62 }, { 1, 26 }, { 4, 0 }, { 1, 3 }, { 1, 36 }, { 1, 12 }, { 76, 0 }, { 1, 1 }, { 1, 15 }, { 1722, 0 } }, + LogoRLEFrame{ { 833, 0 }, { 1, 1 }, { 78, 0 }, { 1, 1 }, { 1, 7 }, { 78, 0 }, { 1, 15 }, { 78, 0 }, { 1, 7 }, { 1, 16 }, { 78, 0 }, { 1, 25 }, { 1, 3 }, { 77, 0 }, { 1, 15 }, { 1, 22 }, { 77, 0 }, { 1, 3 }, { 1, 29 }, { 1, 6 }, { 77, 0 }, { 1, 18 }, { 1, 21 }, { 77, 0 }, { 1, 5 }, { 1, 30 }, { 1, 5 }, { 77, 0 }, { 1, 21 }, { 1, 20 }, { 77, 0 }, { 1, 7 }, { 1, 30 }, { 1, 4 }, { 77, 0 }, { 1, 22 }, { 1, 18 }, { 77, 0 }, { 1, 4 }, { 1, 27 }, { 1, 1 }, { 9, 0 }, { 1, 104 }, { 1, 242 }, { 1, 191 }, { 1, 15 }, { 64, 0 }, { 1, 17 }, { 1, 10 }, { 9, 0 }, { 1, 23 }, { 1, 249 }, { 2, 255 }, { 1, 156 }, { 63, 0 }, { 1, 1 }, { 1, 19 }, { 10, 0 }, { 1, 126 }, { 3, 255 }, { 1, 248 }, { 1, 22 }, { 62, 0 }, { 1, 9 }, { 1, 2 }, { 9, 0 }, { 1, 4 }, { 1, 229 }, { 4, 255 }, { 1, 125 }, { 62, 0 }, { 1, 3 }, { 10, 0 }, { 1, 86 }, { 5, 255 }, { 1, 230 }, { 1, 5 }, { 72, 0 }, { 1, 194 }, { 6, 255 }, { 1, 82 }, { 71, 0 }, { 1, 47 }, { 7, 255 }, { 1, 158 }, { 71, 0 }, { 1, 155 }, { 7, 255 }, { 1, 233 }, { 1, 1 }, { 69, 0 }, { 1, 17 }, { 1, 246 }, { 8, 255 }, { 1, 53 }, { 69, 0 }, { 1, 115 }, { 9, 255 }, { 1, 117 }, { 68, 0 }, { 1, 2 }, { 1, 221 }, { 9, 255 }, { 1, 114 }, { 68, 0 }, { 1, 76 }, { 10, 255 }, { 1, 90 }, { 68, 0 }, { 1, 184 }, { 9, 255 }, { 1, 236 }, { 1, 9 }, { 67, 0 }, { 1, 38 }, { 10, 255 }, { 1, 119 }, { 68, 0 }, { 1, 145 }, { 9, 255 }, { 1, 190 }, { 1, 2 }, { 51, 0 }, { 1, 7 }, { 1, 4 }, { 14, 0 }, { 1, 12 }, { 1, 241 }, { 8, 255 }, { 1, 234 }, { 1, 25 }, { 52, 0 }, { 1, 27 }, { 15, 0 }, { 1, 105 }, { 9, 255 }, { 1, 72 }, { 52, 0 }, { 1, 12 }, { 1, 29 }, { 15, 0 }, { 1, 213 }, { 8, 255 }, { 1, 138 }, { 53, 0 }, { 1, 31 }, { 1, 18 }, { 14, 0 }, { 1, 66 }, { 8, 255 }, { 1, 200 }, { 1, 5 }, { 4, 0 }, { 1, 27 }, { 1, 158 }, { 1, 209 }, { 1, 192 }, { 1, 113 }, { 1, 5 }, { 42, 0 }, { 1, 6 }, { 1, 41 }, { 1, 3 }, { 14, 0 }, { 1, 174 }, { 7, 255 }, { 1, 240 }, { 1, 32 }, { 4, 0 }, { 1, 80 }, { 1, 239 }, { 4, 255 }, { 1, 163 }, { 42, 0 }, { 1, 23 }, { 1, 29 }, { 14, 0 }, { 1, 30 }, { 1, 252 }, { 7, 255 }, { 1, 84 }, { 3, 0 }, { 1, 5 }, { 1, 142 }, { 7, 255 }, { 1, 68 }, { 40, 0 }, { 1, 1 }, { 1, 39 }, { 1, 14 }, { 14, 0 }, { 1, 134 }, { 7, 255 }, { 1, 150 }, { 3, 0 }, { 1, 31 }, { 1, 200 }, { 8, 255 }, { 1, 219 }, { 1, 8 }, { 2, 0 }, { 1, 4 }, { 36, 0 }, { 1, 10 }, { 1, 37 }, { 1, 1 }, { 13, 0 }, { 1, 8 }, { 1, 235 }, { 6, 255 }, { 1, 209 }, { 1, 8 }, { 2, 0 }, { 1, 77 }, { 1, 238 }, { 10, 255 }, { 1, 131 }, { 1, 8 }, { 1, 25 }, { 37, 0 }, { 1, 22 }, { 1, 15 }, { 14, 0 }, { 1, 95 }, { 6, 255 }, { 1, 244 }, { 1, 40 }, { 1, 0 }, { 1, 4 }, { 1, 139 }, { 12, 255 }, { 1, 250 }, { 1, 72 }, { 38, 0 }, { 1, 26 }, { 15, 0 }, { 1, 204 }, { 6, 255 }, { 1, 96 }, { 1, 0 }, { 1, 29 }, { 1, 197 }, { 14, 255 }, { 1, 195 }, { 1, 1 }, { 36, 0 }, { 1, 3 }, { 1, 11 }, { 14, 0 }, { 1, 56 }, { 6, 255 }, { 1, 228 }, { 1, 0 }, { 1, 74 }, { 1, 236 }, { 16, 255 }, { 1, 99 }, { 36, 0 }, { 1, 4 }, { 15, 0 }, { 1, 164 }, { 7, 255 }, { 1, 196 }, { 18, 255 }, { 1, 238 }, { 1, 22 }, { 50, 0 }, { 1, 23 }, { 1, 249 }, { 27, 255 }, { 1, 163 }, { 50, 0 }, { 1, 124 }, { 29, 255 }, { 1, 61 }, { 48, 0 }, { 1, 4 }, { 1, 228 }, { 29, 255 }, { 1, 154 }, { 48, 0 }, { 1, 85 }, { 30, 255 }, { 1, 112 }, { 48, 0 }, { 1, 193 }, { 29, 255 }, { 1, 204 }, { 1, 17 }, { 47, 0 }, { 1, 45 }, { 27, 255 }, { 1, 247 }, { 1, 205 }, { 1, 119 }, { 1, 11 }, { 48, 0 }, { 1, 107 }, { 19, 255 }, { 1, 231 }, { 1, 201 }, { 1, 171 }, { 1, 141 }, { 1, 111 }, { 1, 81 }, { 1, 51 }, { 1, 21 }, { 52, 0 }, { 1, 84 }, { 10, 255 }, { 1, 245 }, { 1, 215 }, { 1, 185 }, { 1, 155 }, { 1, 125 }, { 1, 95 }, { 1, 65 }, { 1, 35 }, { 1, 7 }, { 60, 0 }, { 1, 3 }, { 1, 140 }, { 1, 224 }, { 1, 228 }, { 1, 198 }, { 1, 168 }, { 1, 138 }, { 1, 108 }, { 1, 78 }, { 1, 48 }, { 1, 19 }, { 1805, 0 } }, + LogoRLEFrame{ { 1717, 0 }, { 1, 74 }, { 1, 216 }, { 1, 178 }, { 1, 22 }, { 75, 0 }, { 1, 6 }, { 1, 234 }, { 2, 255 }, { 1, 190 }, { 75, 0 }, { 1, 84 }, { 4, 255 }, { 1, 62 }, { 74, 0 }, { 1, 182 }, { 4, 255 }, { 1, 182 }, { 73, 0 }, { 1, 28 }, { 1, 253 }, { 5, 255 }, { 1, 48 }, { 72, 0 }, { 1, 123 }, { 6, 255 }, { 1, 165 }, { 71, 0 }, { 1, 1 }, { 1, 220 }, { 6, 255 }, { 1, 247 }, { 1, 12 }, { 70, 0 }, { 1, 63 }, { 8, 255 }, { 1, 88 }, { 70, 0 }, { 1, 161 }, { 8, 255 }, { 1, 174 }, { 69, 0 }, { 1, 14 }, { 1, 245 }, { 8, 255 }, { 1, 247 }, { 1, 12 }, { 68, 0 }, { 1, 101 }, { 10, 255 }, { 1, 58 }, { 68, 0 }, { 1, 200 }, { 10, 255 }, { 1, 58 }, { 52, 0 }, { 1, 17 }, { 14, 0 }, { 1, 42 }, { 11, 255 }, { 1, 33 }, { 51, 0 }, { 1, 8 }, { 1, 31 }, { 14, 0 }, { 1, 140 }, { 10, 255 }, { 1, 191 }, { 52, 0 }, { 1, 42 }, { 1, 17 }, { 13, 0 }, { 1, 5 }, { 1, 233 }, { 10, 255 }, { 1, 68 }, { 51, 0 }, { 1, 9 }, { 1, 61 }, { 1, 3 }, { 13, 0 }, { 1, 80 }, { 10, 255 }, { 1, 142 }, { 52, 0 }, { 1, 34 }, { 1, 42 }, { 14, 0 }, { 1, 178 }, { 9, 255 }, { 1, 209 }, { 1, 7 }, { 51, 0 }, { 1, 1 }, { 1, 58 }, { 1, 19 }, { 13, 0 }, { 1, 25 }, { 1, 252 }, { 8, 255 }, { 1, 247 }, { 1, 43 }, { 52, 0 }, { 1, 22 }, { 1, 58 }, { 1, 1 }, { 13, 0 }, { 1, 118 }, { 9, 255 }, { 1, 108 }, { 53, 0 }, { 1, 41 }, { 1, 33 }, { 14, 0 }, { 1, 216 }, { 8, 255 }, { 1, 182 }, { 5, 0 }, { 1, 2 }, { 1, 102 }, { 1, 171 }, { 1, 174 }, { 1, 118 }, { 1, 13 }, { 7, 0 }, { 1, 4 }, { 1, 15 }, { 34, 0 }, { 1, 53 }, { 1, 3 }, { 13, 0 }, { 1, 59 }, { 8, 255 }, { 1, 233 }, { 1, 23 }, { 4, 0 }, { 1, 29 }, { 1, 194 }, { 4, 255 }, { 1, 226 }, { 1, 20 }, { 5, 0 }, { 1, 15 }, { 1, 32 }, { 34, 0 }, { 1, 13 }, { 1, 28 }, { 14, 0 }, { 1, 157 }, { 8, 255 }, { 1, 75 }, { 4, 0 }, { 1, 80 }, { 1, 237 }, { 6, 255 }, { 1, 175 }, { 3, 0 }, { 1, 1 }, { 1, 34 }, { 1, 39 }, { 35, 0 }, { 1, 22 }, { 1, 1 }, { 13, 0 }, { 1, 12 }, { 1, 243 }, { 7, 255 }, { 1, 148 }, { 3, 0 }, { 1, 8 }, { 1, 148 }, { 9, 255 }, { 1, 99 }, { 1, 0 }, { 1, 8 }, { 1, 50 }, { 1, 40 }, { 36, 0 }, { 1, 7 }, { 14, 0 }, { 1, 97 }, { 7, 255 }, { 1, 214 }, { 1, 9 }, { 2, 0 }, { 1, 40 }, { 1, 209 }, { 10, 255 }, { 1, 244 }, { 1, 48 }, { 1, 58 }, { 1, 35 }, { 52, 0 }, { 1, 195 }, { 6, 255 }, { 1, 249 }, { 1, 48 }, { 2, 0 }, { 1, 98 }, { 1, 245 }, { 12, 255 }, { 1, 212 }, { 1, 29 }, { 52, 0 }, { 1, 38 }, { 7, 255 }, { 1, 117 }, { 1, 0 }, { 1, 14 }, { 1, 167 }, { 15, 255 }, { 1, 128 }, { 52, 0 }, { 1, 135 }, { 7, 255 }, { 1, 58 }, { 1, 54 }, { 1, 221 }, { 16, 255 }, { 1, 252 }, { 1, 56 }, { 50, 0 }, { 1, 3 }, { 1, 230 }, { 7, 255 }, { 1, 242 }, { 1, 252 }, { 18, 255 }, { 1, 220 }, { 1, 12 }, { 49, 0 }, { 1, 76 }, { 29, 255 }, { 1, 156 }, { 49, 0 }, { 1, 174 }, { 30, 255 }, { 1, 75 }, { 47, 0 }, { 1, 22 }, { 1, 250 }, { 30, 255 }, { 1, 189 }, { 47, 0 }, { 1, 114 }, { 31, 255 }, { 1, 180 }, { 47, 0 }, { 1, 212 }, { 30, 255 }, { 1, 250 }, { 1, 73 }, { 46, 0 }, { 1, 48 }, { 29, 255 }, { 1, 252 }, { 1, 189 }, { 1, 67 }, { 47, 0 }, { 1, 82 }, { 20, 255 }, { 1, 251 }, { 1, 226 }, { 1, 200 }, { 1, 174 }, { 1, 147 }, { 1, 121 }, { 1, 95 }, { 1, 68 }, { 1, 42 }, { 1, 14 }, { 49, 0 }, { 1, 24 }, { 1, 248 }, { 10, 255 }, { 1, 233 }, { 1, 207 }, { 1, 180 }, { 1, 154 }, { 1, 128 }, { 1, 101 }, { 1, 75 }, { 1, 49 }, { 1, 23 }, { 1, 2 }, { 59, 0 }, { 1, 62 }, { 1, 187 }, { 1, 208 }, { 1, 187 }, { 1, 161 }, { 1, 135 }, { 1, 108 }, { 1, 82 }, { 1, 56 }, { 1, 29 }, { 1, 5 }, { 1804, 0 } }, + LogoRLEFrame{ { 1717, 0 }, { 1, 16 }, { 1, 160 }, { 1, 162 }, { 1, 39 }, { 76, 0 }, { 1, 154 }, { 2, 255 }, { 1, 226 }, { 1, 19 }, { 74, 0 }, { 1, 9 }, { 1, 242 }, { 3, 255 }, { 1, 134 }, { 74, 0 }, { 1, 86 }, { 4, 255 }, { 1, 242 }, { 1, 18 }, { 73, 0 }, { 1, 177 }, { 5, 255 }, { 1, 132 }, { 72, 0 }, { 1, 18 }, { 1, 249 }, { 5, 255 }, { 1, 241 }, { 1, 17 }, { 71, 0 }, { 1, 102 }, { 7, 255 }, { 1, 115 }, { 71, 0 }, { 1, 193 }, { 7, 255 }, { 1, 206 }, { 54, 0 }, { 1, 2 }, { 1, 12 }, { 14, 0 }, { 1, 29 }, { 9, 255 }, { 1, 43 }, { 53, 0 }, { 1, 31 }, { 1, 6 }, { 14, 0 }, { 1, 118 }, { 9, 255 }, { 1, 134 }, { 52, 0 }, { 1, 6 }, { 1, 52 }, { 15, 0 }, { 1, 209 }, { 9, 255 }, { 1, 222 }, { 52, 0 }, { 1, 34 }, { 1, 39 }, { 14, 0 }, { 1, 44 }, { 10, 255 }, { 1, 250 }, { 51, 0 }, { 1, 1 }, { 1, 57 }, { 1, 17 }, { 14, 0 }, { 1, 135 }, { 11, 255 }, { 1, 3 }, { 50, 0 }, { 1, 22 }, { 1, 56 }, { 14, 0 }, { 1, 1 }, { 1, 224 }, { 10, 255 }, { 1, 205 }, { 51, 0 }, { 1, 47 }, { 1, 34 }, { 14, 0 }, { 1, 60 }, { 11, 255 }, { 1, 113 }, { 50, 0 }, { 1, 4 }, { 1, 61 }, { 1, 9 }, { 14, 0 }, { 1, 151 }, { 10, 255 }, { 1, 215 }, { 1, 8 }, { 19, 0 }, { 1, 5 }, { 30, 0 }, { 1, 21 }, { 1, 37 }, { 14, 0 }, { 1, 5 }, { 1, 236 }, { 9, 255 }, { 1, 251 }, { 1, 52 }, { 18, 0 }, { 1, 9 }, { 1, 27 }, { 31, 0 }, { 1, 35 }, { 1, 5 }, { 14, 0 }, { 1, 76 }, { 10, 255 }, { 1, 127 }, { 18, 0 }, { 1, 24 }, { 1, 37 }, { 32, 0 }, { 1, 24 }, { 15, 0 }, { 1, 167 }, { 9, 255 }, { 1, 203 }, { 1, 4 }, { 16, 0 }, { 1, 4 }, { 1, 43 }, { 1, 40 }, { 32, 0 }, { 1, 4 }, { 1, 2 }, { 14, 0 }, { 1, 12 }, { 1, 245 }, { 8, 255 }, { 1, 246 }, { 1, 40 }, { 16, 0 }, { 1, 11 }, { 1, 55 }, { 1, 39 }, { 49, 0 }, { 1, 93 }, { 9, 255 }, { 1, 110 }, { 5, 0 }, { 1, 11 }, { 1, 140 }, { 1, 216 }, { 1, 227 }, { 1, 181 }, { 1, 69 }, { 5, 0 }, { 1, 20 }, { 1, 59 }, { 1, 31 }, { 50, 0 }, { 1, 183 }, { 8, 255 }, { 1, 189 }, { 1, 1 }, { 4, 0 }, { 1, 52 }, { 1, 218 }, { 5, 255 }, { 1, 96 }, { 3, 0 }, { 1, 31 }, { 1, 60 }, { 1, 22 }, { 50, 0 }, { 1, 22 }, { 1, 251 }, { 7, 255 }, { 1, 240 }, { 1, 30 }, { 3, 0 }, { 1, 1 }, { 1, 117 }, { 1, 250 }, { 6, 255 }, { 1, 246 }, { 1, 45 }, { 1, 2 }, { 1, 42 }, { 1, 58 }, { 1, 14 }, { 51, 0 }, { 1, 109 }, { 8, 255 }, { 1, 94 }, { 3, 0 }, { 1, 25 }, { 1, 187 }, { 9, 255 }, { 1, 217 }, { 1, 57 }, { 1, 51 }, { 1, 8 }, { 52, 0 }, { 1, 199 }, { 7, 255 }, { 1, 174 }, { 3, 0 }, { 1, 76 }, { 1, 235 }, { 11, 255 }, { 1, 180 }, { 53, 0 }, { 1, 35 }, { 7, 255 }, { 1, 233 }, { 1, 21 }, { 1, 0 }, { 1, 8 }, { 1, 147 }, { 14, 255 }, { 1, 107 }, { 52, 0 }, { 1, 125 }, { 7, 255 }, { 1, 99 }, { 1, 0 }, { 1, 43 }, { 1, 210 }, { 15, 255 }, { 1, 249 }, { 1, 53 }, { 51, 0 }, { 1, 215 }, { 7, 255 }, { 1, 159 }, { 1, 112 }, { 1, 246 }, { 17, 255 }, { 1, 224 }, { 1, 17 }, { 49, 0 }, { 1, 50 }, { 29, 255 }, { 1, 179 }, { 1, 1 }, { 48, 0 }, { 1, 141 }, { 30, 255 }, { 1, 119 }, { 47, 0 }, { 1, 2 }, { 1, 229 }, { 30, 255 }, { 1, 252 }, { 1, 45 }, { 46, 0 }, { 1, 67 }, { 32, 255 }, { 1, 139 }, { 46, 0 }, { 1, 157 }, { 32, 255 }, { 1, 108 }, { 45, 0 }, { 1, 7 }, { 1, 240 }, { 31, 255 }, { 1, 212 }, { 1, 21 }, { 45, 0 }, { 1, 58 }, { 29, 255 }, { 1, 244 }, { 1, 208 }, { 1, 127 }, { 1, 17 }, { 46, 0 }, { 1, 57 }, { 19, 255 }, { 1, 238 }, { 1, 213 }, { 1, 188 }, { 1, 163 }, { 1, 138 }, { 1, 113 }, { 1, 88 }, { 1, 63 }, { 1, 38 }, { 1, 13 }, { 51, 0 }, { 1, 191 }, { 7, 255 }, { 1, 253 }, { 1, 232 }, { 1, 207 }, { 1, 182 }, { 1, 157 }, { 1, 132 }, { 1, 107 }, { 1, 82 }, { 1, 57 }, { 1, 32 }, { 1, 8 }, { 61, 0 }, { 1, 6 }, { 1, 103 }, { 1, 139 }, { 1, 126 }, { 1, 101 }, { 1, 76 }, { 1, 51 }, { 1, 26 }, { 1, 3 }, { 1725, 0 } }, + LogoRLEFrame{ { 1718, 0 }, { 1, 88 }, { 1, 155 }, { 1, 71 }, { 76, 0 }, { 1, 47 }, { 1, 252 }, { 2, 255 }, { 1, 85 }, { 75, 0 }, { 1, 140 }, { 3, 255 }, { 1, 230 }, { 1, 9 }, { 74, 0 }, { 1, 225 }, { 4, 255 }, { 1, 115 }, { 57, 0 }, { 1, 14 }, { 15, 0 }, { 1, 55 }, { 5, 255 }, { 1, 234 }, { 1, 12 }, { 55, 0 }, { 1, 1 }, { 1, 35 }, { 15, 0 }, { 1, 141 }, { 6, 255 }, { 1, 121 }, { 55, 0 }, { 1, 28 }, { 1, 29 }, { 14, 0 }, { 1, 1 }, { 1, 226 }, { 6, 255 }, { 1, 236 }, { 1, 7 }, { 53, 0 }, { 1, 1 }, { 1, 57 }, { 1, 14 }, { 14, 0 }, { 1, 56 }, { 8, 255 }, { 1, 84 }, { 53, 0 }, { 1, 21 }, { 1, 54 }, { 15, 0 }, { 1, 142 }, { 8, 255 }, { 1, 181 }, { 53, 0 }, { 1, 47 }, { 1, 32 }, { 14, 0 }, { 1, 1 }, { 1, 227 }, { 8, 255 }, { 1, 252 }, { 1, 25 }, { 51, 0 }, { 1, 9 }, { 1, 62 }, { 1, 9 }, { 14, 0 }, { 1, 57 }, { 10, 255 }, { 1, 118 }, { 51, 0 }, { 1, 29 }, { 1, 46 }, { 15, 0 }, { 1, 143 }, { 10, 255 }, { 1, 170 }, { 51, 0 }, { 1, 46 }, { 1, 13 }, { 14, 0 }, { 1, 1 }, { 1, 227 }, { 10, 255 }, { 1, 185 }, { 21, 0 }, { 1, 4 }, { 1, 17 }, { 27, 0 }, { 1, 2 }, { 1, 40 }, { 15, 0 }, { 1, 58 }, { 11, 255 }, { 1, 167 }, { 20, 0 }, { 1, 16 }, { 1, 33 }, { 28, 0 }, { 1, 17 }, { 1, 8 }, { 15, 0 }, { 1, 144 }, { 11, 255 }, { 1, 84 }, { 18, 0 }, { 1, 1 }, { 1, 34 }, { 1, 40 }, { 29, 0 }, { 1, 9 }, { 15, 0 }, { 1, 1 }, { 1, 228 }, { 10, 255 }, { 1, 219 }, { 1, 8 }, { 17, 0 }, { 1, 8 }, { 1, 51 }, { 1, 40 }, { 46, 0 }, { 1, 59 }, { 10, 255 }, { 1, 253 }, { 1, 60 }, { 17, 0 }, { 1, 16 }, { 1, 58 }, { 1, 35 }, { 47, 0 }, { 1, 145 }, { 10, 255 }, { 1, 141 }, { 17, 0 }, { 1, 27 }, { 1, 61 }, { 1, 26 }, { 47, 0 }, { 1, 1 }, { 1, 229 }, { 9, 255 }, { 1, 217 }, { 1, 9 }, { 15, 0 }, { 1, 1 }, { 1, 38 }, { 1, 59 }, { 1, 18 }, { 48, 0 }, { 1, 60 }, { 9, 255 }, { 1, 253 }, { 1, 57 }, { 15, 0 }, { 1, 2 }, { 1, 47 }, { 1, 55 }, { 1, 11 }, { 49, 0 }, { 1, 146 }, { 9, 255 }, { 1, 137 }, { 6, 0 }, { 1, 42 }, { 1, 108 }, { 1, 118 }, { 1, 73 }, { 1, 3 }, { 4, 0 }, { 1, 2 }, { 1, 47 }, { 1, 44 }, { 1, 4 }, { 49, 0 }, { 1, 2 }, { 1, 229 }, { 8, 255 }, { 1, 214 }, { 1, 7 }, { 4, 0 }, { 1, 4 }, { 1, 129 }, { 1, 251 }, { 3, 255 }, { 1, 215 }, { 1, 35 }, { 2, 0 }, { 1, 3 }, { 1, 46 }, { 1, 27 }, { 51, 0 }, { 1, 61 }, { 8, 255 }, { 1, 252 }, { 1, 53 }, { 4, 0 }, { 1, 33 }, { 1, 198 }, { 6, 255 }, { 1, 211 }, { 1, 12 }, { 1, 3 }, { 1, 34 }, { 1, 10 }, { 52, 0 }, { 1, 147 }, { 8, 255 }, { 1, 133 }, { 4, 0 }, { 1, 90 }, { 1, 241 }, { 8, 255 }, { 1, 172 }, { 1, 10 }, { 53, 0 }, { 1, 2 }, { 1, 230 }, { 7, 255 }, { 1, 211 }, { 1, 6 }, { 2, 0 }, { 1, 13 }, { 1, 162 }, { 11, 255 }, { 1, 121 }, { 53, 0 }, { 1, 62 }, { 7, 255 }, { 1, 251 }, { 1, 50 }, { 2, 0 }, { 1, 54 }, { 1, 220 }, { 12, 255 }, { 1, 253 }, { 1, 73 }, { 52, 0 }, { 1, 148 }, { 7, 255 }, { 1, 133 }, { 1, 0 }, { 1, 2 }, { 1, 121 }, { 1, 251 }, { 14, 255 }, { 1, 239 }, { 1, 36 }, { 50, 0 }, { 1, 2 }, { 1, 231 }, { 7, 255 }, { 1, 100 }, { 1, 28 }, { 1, 191 }, { 17, 255 }, { 1, 212 }, { 1, 12 }, { 49, 0 }, { 1, 63 }, { 8, 255 }, { 1, 249 }, { 1, 245 }, { 19, 255 }, { 1, 172 }, { 1, 1 }, { 48, 0 }, { 1, 149 }, { 30, 255 }, { 1, 122 }, { 47, 0 }, { 1, 2 }, { 1, 232 }, { 30, 255 }, { 1, 253 }, { 1, 68 }, { 46, 0 }, { 1, 64 }, { 32, 255 }, { 1, 205 }, { 46, 0 }, { 1, 150 }, { 32, 255 }, { 1, 229 }, { 45, 0 }, { 1, 3 }, { 1, 232 }, { 32, 255 }, { 1, 150 }, { 45, 0 }, { 1, 58 }, { 31, 255 }, { 1, 248 }, { 1, 154 }, { 1, 5 }, { 45, 0 }, { 1, 77 }, { 23, 255 }, { 1, 249 }, { 1, 224 }, { 1, 199 }, { 1, 174 }, { 1, 149 }, { 1, 124 }, { 1, 99 }, { 1, 72 }, { 1, 13 }, { 47, 0 }, { 1, 18 }, { 1, 241 }, { 12, 255 }, { 1, 244 }, { 1, 219 }, { 1, 194 }, { 1, 169 }, { 1, 144 }, { 1, 118 }, { 1, 93 }, { 1, 68 }, { 1, 43 }, { 1, 18 }, { 57, 0 }, { 1, 63 }, { 1, 213 }, { 1, 250 }, { 1, 239 }, { 1, 214 }, { 1, 188 }, { 1, 163 }, { 1, 138 }, { 1, 113 }, { 1, 88 }, { 1, 63 }, { 1, 38 }, { 1, 13 }, { 490, 0 }, { 1, 1 }, { 1, 20 }, { 1, 4 }, { 75, 0 }, { 1, 1 }, { 1, 24 }, { 1, 45 }, { 1, 8 }, { 75, 0 }, { 1, 15 }, { 1, 53 }, { 1, 46 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 39 }, { 1, 60 }, { 1, 26 }, { 75, 0 }, { 1, 17 }, { 1, 57 }, { 1, 47 }, { 1, 8 }, { 75, 0 }, { 1, 28 }, { 1, 50 }, { 1, 21 }, { 75, 0 }, { 1, 1 }, { 1, 24 }, { 1, 17 }, { 77, 0 }, { 1, 2 }, { 678, 0 } }, + LogoRLEFrame{ { 1637, 0 }, { 1, 4 }, { 1, 151 }, { 1, 183 }, { 1, 74 }, { 61, 0 }, { 1, 12 }, { 14, 0 }, { 1, 111 }, { 2, 255 }, { 1, 251 }, { 1, 68 }, { 59, 0 }, { 1, 17 }, { 1, 18 }, { 14, 0 }, { 1, 199 }, { 3, 255 }, { 1, 212 }, { 1, 2 }, { 58, 0 }, { 1, 51 }, { 1, 4 }, { 13, 0 }, { 1, 27 }, { 5, 255 }, { 1, 92 }, { 57, 0 }, { 1, 21 }, { 1, 51 }, { 14, 0 }, { 1, 109 }, { 5, 255 }, { 1, 221 }, { 1, 5 }, { 56, 0 }, { 1, 47 }, { 1, 30 }, { 14, 0 }, { 1, 191 }, { 6, 255 }, { 1, 104 }, { 55, 0 }, { 1, 9 }, { 1, 62 }, { 1, 7 }, { 13, 0 }, { 1, 21 }, { 1, 252 }, { 6, 255 }, { 1, 224 }, { 1, 2 }, { 54, 0 }, { 1, 34 }, { 1, 46 }, { 14, 0 }, { 1, 100 }, { 8, 255 }, { 1, 70 }, { 54, 0 }, { 1, 55 }, { 1, 21 }, { 14, 0 }, { 1, 183 }, { 8, 255 }, { 1, 169 }, { 53, 0 }, { 1, 9 }, { 1, 50 }, { 14, 0 }, { 1, 16 }, { 1, 250 }, { 8, 255 }, { 1, 249 }, { 1, 19 }, { 52, 0 }, { 1, 26 }, { 1, 17 }, { 14, 0 }, { 1, 92 }, { 10, 255 }, { 1, 111 }, { 52, 0 }, { 1, 27 }, { 15, 0 }, { 1, 175 }, { 10, 255 }, { 1, 162 }, { 52, 0 }, { 1, 8 }, { 14, 0 }, { 1, 11 }, { 1, 246 }, { 10, 255 }, { 1, 180 }, { 67, 0 }, { 1, 84 }, { 11, 255 }, { 1, 161 }, { 67, 0 }, { 1, 167 }, { 11, 255 }, { 1, 79 }, { 66, 0 }, { 1, 7 }, { 1, 242 }, { 10, 255 }, { 1, 215 }, { 1, 7 }, { 66, 0 }, { 1, 76 }, { 10, 255 }, { 1, 253 }, { 1, 57 }, { 67, 0 }, { 1, 158 }, { 10, 255 }, { 1, 141 }, { 67, 0 }, { 1, 4 }, { 1, 237 }, { 9, 255 }, { 1, 219 }, { 1, 9 }, { 67, 0 }, { 1, 68 }, { 10, 255 }, { 1, 61 }, { 68, 0 }, { 1, 150 }, { 9, 255 }, { 1, 147 }, { 6, 0 }, { 1, 36 }, { 1, 110 }, { 1, 128 }, { 1, 88 }, { 1, 10 }, { 57, 0 }, { 1, 2 }, { 1, 231 }, { 8, 255 }, { 1, 222 }, { 1, 11 }, { 4, 0 }, { 1, 1 }, { 1, 112 }, { 1, 247 }, { 3, 255 }, { 1, 233 }, { 1, 64 }, { 56, 0 }, { 1, 59 }, { 9, 255 }, { 1, 66 }, { 4, 0 }, { 1, 22 }, { 1, 183 }, { 6, 255 }, { 1, 237 }, { 1, 34 }, { 55, 0 }, { 1, 142 }, { 8, 255 }, { 1, 152 }, { 4, 0 }, { 1, 72 }, { 1, 232 }, { 8, 255 }, { 1, 212 }, { 1, 13 }, { 54, 0 }, { 1, 224 }, { 7, 255 }, { 1, 226 }, { 1, 14 }, { 2, 0 }, { 1, 6 }, { 1, 142 }, { 11, 255 }, { 1, 177 }, { 1, 1 }, { 52, 0 }, { 1, 51 }, { 8, 255 }, { 1, 72 }, { 2, 0 }, { 1, 40 }, { 1, 207 }, { 13, 255 }, { 1, 132 }, { 52, 0 }, { 1, 134 }, { 7, 255 }, { 1, 162 }, { 2, 0 }, { 1, 101 }, { 1, 245 }, { 15, 255 }, { 1, 88 }, { 51, 0 }, { 1, 217 }, { 7, 255 }, { 1, 137 }, { 1, 17 }, { 1, 172 }, { 17, 255 }, { 1, 246 }, { 1, 51 }, { 49, 0 }, { 1, 43 }, { 9, 255 }, { 1, 242 }, { 19, 255 }, { 1, 227 }, { 1, 24 }, { 48, 0 }, { 1, 126 }, { 30, 255 }, { 1, 198 }, { 1, 7 }, { 47, 0 }, { 1, 208 }, { 31, 255 }, { 1, 151 }, { 46, 0 }, { 1, 35 }, { 33, 255 }, { 1, 34 }, { 45, 0 }, { 1, 118 }, { 33, 255 }, { 1, 50 }, { 45, 0 }, { 1, 200 }, { 32, 255 }, { 1, 219 }, { 1, 2 }, { 44, 0 }, { 1, 22 }, { 31, 255 }, { 1, 252 }, { 1, 184 }, { 1, 25 }, { 45, 0 }, { 1, 39 }, { 23, 255 }, { 1, 247 }, { 1, 221 }, { 1, 195 }, { 1, 169 }, { 1, 143 }, { 1, 117 }, { 1, 91 }, { 1, 66 }, { 1, 20 }, { 47, 0 }, { 1, 4 }, { 1, 219 }, { 12, 255 }, { 1, 250 }, { 1, 225 }, { 1, 199 }, { 1, 173 }, { 1, 147 }, { 1, 121 }, { 1, 95 }, { 1, 69 }, { 1, 43 }, { 1, 17 }, { 57, 0 }, { 1, 45 }, { 1, 206 }, { 1, 253 }, { 1, 252 }, { 1, 228 }, { 1, 202 }, { 1, 176 }, { 1, 150 }, { 1, 125 }, { 1, 99 }, { 1, 73 }, { 1, 47 }, { 1, 21 }, { 1, 1 }, { 68, 0 }, { 1, 5 }, { 1, 2 }, { 343, 0 }, { 1, 6 }, { 77, 0 }, { 1, 13 }, { 1, 35 }, { 1, 7 }, { 75, 0 }, { 1, 10 }, { 1, 44 }, { 1, 46 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 31 }, { 1, 60 }, { 1, 32 }, { 75, 0 }, { 1, 15 }, { 1, 53 }, { 1, 52 }, { 1, 12 }, { 75, 0 }, { 1, 28 }, { 1, 58 }, { 1, 31 }, { 1, 1 }, { 74, 0 }, { 1, 1 }, { 1, 34 }, { 1, 30 }, { 1, 3 }, { 76, 0 }, { 1, 13 }, { 1, 2 }, { 834, 0 } }, + LogoRLEFrame{ { 1383, 0 }, { 1, 8 }, { 1, 2 }, { 78, 0 }, { 1, 35 }, { 78, 0 }, { 1, 15 }, { 1, 41 }, { 13, 0 }, { 1, 16 }, { 1, 183 }, { 1, 198 }, { 1, 75 }, { 61, 0 }, { 1, 46 }, { 1, 26 }, { 13, 0 }, { 1, 142 }, { 2, 255 }, { 1, 250 }, { 1, 59 }, { 59, 0 }, { 1, 9 }, { 1, 61 }, { 1, 5 }, { 13, 0 }, { 1, 226 }, { 3, 255 }, { 1, 200 }, { 59, 0 }, { 1, 34 }, { 1, 44 }, { 13, 0 }, { 1, 53 }, { 5, 255 }, { 1, 80 }, { 57, 0 }, { 1, 1 }, { 1, 58 }, { 1, 21 }, { 13, 0 }, { 1, 135 }, { 5, 255 }, { 1, 213 }, { 1, 3 }, { 56, 0 }, { 1, 17 }, { 1, 57 }, { 1, 1 }, { 13, 0 }, { 1, 216 }, { 6, 255 }, { 1, 95 }, { 56, 0 }, { 1, 35 }, { 1, 27 }, { 13, 0 }, { 1, 42 }, { 7, 255 }, { 1, 216 }, { 56, 0 }, { 1, 43 }, { 1, 1 }, { 13, 0 }, { 1, 124 }, { 8, 255 }, { 1, 60 }, { 54, 0 }, { 1, 6 }, { 1, 21 }, { 14, 0 }, { 1, 205 }, { 8, 255 }, { 1, 161 }, { 54, 0 }, { 1, 10 }, { 14, 0 }, { 1, 32 }, { 9, 255 }, { 1, 246 }, { 1, 16 }, { 68, 0 }, { 1, 113 }, { 10, 255 }, { 1, 105 }, { 68, 0 }, { 1, 194 }, { 10, 255 }, { 1, 147 }, { 67, 0 }, { 1, 23 }, { 1, 253 }, { 10, 255 }, { 1, 165 }, { 67, 0 }, { 1, 102 }, { 11, 255 }, { 1, 135 }, { 67, 0 }, { 1, 183 }, { 11, 255 }, { 1, 54 }, { 66, 0 }, { 1, 15 }, { 1, 250 }, { 10, 255 }, { 1, 188 }, { 67, 0 }, { 1, 91 }, { 10, 255 }, { 1, 244 }, { 1, 33 }, { 67, 0 }, { 1, 173 }, { 10, 255 }, { 1, 109 }, { 67, 0 }, { 1, 9 }, { 1, 245 }, { 9, 255 }, { 1, 196 }, { 1, 2 }, { 67, 0 }, { 1, 80 }, { 9, 255 }, { 1, 247 }, { 1, 39 }, { 68, 0 }, { 1, 162 }, { 9, 255 }, { 1, 119 }, { 6, 0 }, { 1, 68 }, { 1, 147 }, { 1, 163 }, { 1, 123 }, { 1, 28 }, { 57, 0 }, { 1, 5 }, { 1, 238 }, { 8, 255 }, { 1, 204 }, { 1, 3 }, { 4, 0 }, { 1, 9 }, { 1, 152 }, { 4, 255 }, { 1, 248 }, { 1, 83 }, { 56, 0 }, { 1, 69 }, { 8, 255 }, { 1, 250 }, { 1, 46 }, { 4, 0 }, { 1, 45 }, { 1, 213 }, { 6, 255 }, { 1, 245 }, { 1, 49 }, { 55, 0 }, { 1, 151 }, { 8, 255 }, { 1, 129 }, { 4, 0 }, { 1, 105 }, { 1, 247 }, { 8, 255 }, { 1, 226 }, { 1, 23 }, { 53, 0 }, { 1, 1 }, { 1, 230 }, { 7, 255 }, { 1, 211 }, { 1, 6 }, { 2, 0 }, { 1, 18 }, { 1, 175 }, { 11, 255 }, { 1, 198 }, { 1, 7 }, { 52, 0 }, { 1, 58 }, { 7, 255 }, { 1, 252 }, { 1, 53 }, { 2, 0 }, { 1, 62 }, { 1, 226 }, { 13, 255 }, { 1, 160 }, { 52, 0 }, { 1, 140 }, { 7, 255 }, { 1, 146 }, { 1, 0 }, { 1, 3 }, { 1, 128 }, { 1, 252 }, { 15, 255 }, { 1, 116 }, { 51, 0 }, { 1, 221 }, { 7, 255 }, { 1, 144 }, { 1, 29 }, { 1, 194 }, { 17, 255 }, { 1, 253 }, { 1, 75 }, { 49, 0 }, { 1, 47 }, { 9, 255 }, { 1, 252 }, { 19, 255 }, { 1, 241 }, { 1, 42 }, { 48, 0 }, { 1, 129 }, { 30, 255 }, { 1, 220 }, { 1, 18 }, { 47, 0 }, { 1, 211 }, { 31, 255 }, { 1, 180 }, { 46, 0 }, { 1, 37 }, { 33, 255 }, { 1, 44 }, { 45, 0 }, { 1, 118 }, { 33, 255 }, { 1, 36 }, { 45, 0 }, { 1, 200 }, { 32, 255 }, { 1, 188 }, { 45, 0 }, { 1, 20 }, { 31, 255 }, { 1, 227 }, { 1, 138 }, { 1, 8 }, { 45, 0 }, { 1, 34 }, { 22, 255 }, { 1, 239 }, { 1, 212 }, { 1, 185 }, { 1, 158 }, { 1, 131 }, { 1, 104 }, { 1, 77 }, { 1, 50 }, { 1, 23 }, { 1, 1 }, { 47, 0 }, { 1, 2 }, { 1, 211 }, { 11, 255 }, { 1, 250 }, { 1, 225 }, { 1, 198 }, { 1, 171 }, { 1, 144 }, { 1, 117 }, { 1, 91 }, { 1, 64 }, { 1, 37 }, { 1, 10 }, { 58, 0 }, { 1, 36 }, { 1, 190 }, { 1, 243 }, { 1, 239 }, { 1, 212 }, { 1, 185 }, { 1, 158 }, { 1, 131 }, { 1, 104 }, { 1, 77 }, { 1, 50 }, { 1, 23 }, { 1, 1 }, { 415, 0 }, { 1, 5 }, { 1, 23 }, { 1, 2 }, { 75, 0 }, { 1, 4 }, { 1, 33 }, { 1, 42 }, { 1, 3 }, { 75, 0 }, { 1, 25 }, { 1, 58 }, { 1, 35 }, { 75, 0 }, { 1, 10 }, { 1, 48 }, { 1, 55 }, { 1, 16 }, { 75, 0 }, { 1, 27 }, { 1, 61 }, { 1, 38 }, { 1, 3 }, { 74, 0 }, { 1, 1 }, { 1, 38 }, { 1, 42 }, { 1, 12 }, { 75, 0 }, { 1, 4 }, { 1, 25 }, { 1, 9 }, { 77, 0 }, { 1, 1 }, { 913, 0 } }, + LogoRLEFrame{ { 1478, 0 }, { 1, 38 }, { 1, 100 }, { 1, 33 }, { 76, 0 }, { 1, 17 }, { 1, 233 }, { 1, 255 }, { 1, 246 }, { 1, 62 }, { 75, 0 }, { 1, 102 }, { 3, 255 }, { 1, 223 }, { 1, 6 }, { 74, 0 }, { 1, 184 }, { 4, 255 }, { 1, 107 }, { 73, 0 }, { 1, 16 }, { 1, 250 }, { 4, 255 }, { 1, 231 }, { 1, 10 }, { 72, 0 }, { 1, 94 }, { 6, 255 }, { 1, 121 }, { 72, 0 }, { 1, 176 }, { 6, 255 }, { 1, 239 }, { 1, 11 }, { 70, 0 }, { 1, 12 }, { 1, 247 }, { 7, 255 }, { 1, 96 }, { 70, 0 }, { 1, 86 }, { 8, 255 }, { 1, 196 }, { 70, 0 }, { 1, 169 }, { 9, 255 }, { 1, 40 }, { 68, 0 }, { 1, 8 }, { 1, 243 }, { 9, 255 }, { 1, 138 }, { 68, 0 }, { 1, 78 }, { 10, 255 }, { 1, 206 }, { 68, 0 }, { 1, 161 }, { 10, 255 }, { 1, 223 }, { 67, 0 }, { 1, 5 }, { 1, 238 }, { 10, 255 }, { 1, 218 }, { 67, 0 }, { 1, 70 }, { 11, 255 }, { 1, 142 }, { 67, 0 }, { 1, 153 }, { 10, 255 }, { 1, 251 }, { 1, 46 }, { 66, 0 }, { 1, 2 }, { 1, 233 }, { 10, 255 }, { 1, 134 }, { 67, 0 }, { 1, 62 }, { 10, 255 }, { 1, 215 }, { 1, 7 }, { 67, 0 }, { 1, 145 }, { 9, 255 }, { 1, 253 }, { 1, 57 }, { 67, 0 }, { 1, 1 }, { 1, 226 }, { 9, 255 }, { 1, 142 }, { 68, 0 }, { 1, 54 }, { 9, 255 }, { 1, 221 }, { 1, 10 }, { 5, 0 }, { 1, 10 }, { 1, 76 }, { 1, 103 }, { 1, 70 }, { 1, 7 }, { 58, 0 }, { 1, 137 }, { 9, 255 }, { 1, 65 }, { 5, 0 }, { 1, 55 }, { 1, 218 }, { 3, 255 }, { 1, 228 }, { 1, 70 }, { 57, 0 }, { 1, 219 }, { 8, 255 }, { 1, 151 }, { 4, 0 }, { 1, 1 }, { 1, 118 }, { 1, 251 }, { 5, 255 }, { 1, 241 }, { 1, 41 }, { 55, 0 }, { 1, 46 }, { 8, 255 }, { 1, 226 }, { 1, 13 }, { 3, 0 }, { 1, 22 }, { 1, 184 }, { 8, 255 }, { 1, 219 }, { 1, 17 }, { 54, 0 }, { 1, 129 }, { 8, 255 }, { 1, 72 }, { 3, 0 }, { 1, 68 }, { 1, 231 }, { 10, 255 }, { 1, 186 }, { 1, 3 }, { 46, 0 }, { 1, 5 }, { 6, 0 }, { 1, 211 }, { 7, 255 }, { 1, 160 }, { 2, 0 }, { 1, 4 }, { 1, 133 }, { 1, 253 }, { 12, 255 }, { 1, 142 }, { 45, 0 }, { 1, 1 }, { 1, 7 }, { 5, 0 }, { 1, 38 }, { 7, 255 }, { 1, 231 }, { 1, 17 }, { 1, 0 }, { 1, 30 }, { 1, 197 }, { 15, 255 }, { 1, 96 }, { 44, 0 }, { 1, 12 }, { 1, 1 }, { 5, 0 }, { 1, 121 }, { 7, 255 }, { 1, 165 }, { 1, 0 }, { 1, 82 }, { 1, 238 }, { 16, 255 }, { 1, 248 }, { 1, 57 }, { 42, 0 }, { 1, 4 }, { 1, 14 }, { 6, 0 }, { 1, 204 }, { 7, 255 }, { 1, 250 }, { 1, 187 }, { 19, 255 }, { 1, 231 }, { 1, 28 }, { 41, 0 }, { 1, 16 }, { 1, 7 }, { 5, 0 }, { 1, 31 }, { 30, 255 }, { 1, 203 }, { 1, 9 }, { 39, 0 }, { 1, 6 }, { 1, 18 }, { 6, 0 }, { 1, 113 }, { 31, 255 }, { 1, 159 }, { 39, 0 }, { 1, 17 }, { 1, 9 }, { 6, 0 }, { 1, 196 }, { 32, 255 }, { 1, 44 }, { 37, 0 }, { 1, 7 }, { 1, 19 }, { 6, 0 }, { 1, 24 }, { 1, 253 }, { 32, 255 }, { 1, 66 }, { 37, 0 }, { 1, 17 }, { 1, 10 }, { 6, 0 }, { 1, 105 }, { 32, 255 }, { 1, 232 }, { 1, 9 }, { 36, 0 }, { 1, 7 }, { 1, 19 }, { 1, 1 }, { 6, 0 }, { 1, 187 }, { 31, 255 }, { 1, 203 }, { 1, 37 }, { 37, 0 }, { 1, 16 }, { 1, 10 }, { 7, 0 }, { 1, 226 }, { 23, 255 }, { 1, 251 }, { 1, 224 }, { 1, 196 }, { 1, 168 }, { 1, 140 }, { 1, 112 }, { 1, 84 }, { 1, 36 }, { 38, 0 }, { 1, 4 }, { 1, 18 }, { 8, 0 }, { 1, 194 }, { 14, 255 }, { 1, 248 }, { 1, 220 }, { 1, 192 }, { 1, 164 }, { 1, 136 }, { 1, 108 }, { 1, 80 }, { 1, 52 }, { 1, 24 }, { 1, 2 }, { 45, 0 }, { 1, 12 }, { 1, 5 }, { 8, 0 }, { 1, 50 }, { 1, 230 }, { 4, 255 }, { 1, 245 }, { 1, 216 }, { 1, 188 }, { 1, 160 }, { 1, 132 }, { 1, 104 }, { 1, 76 }, { 1, 48 }, { 1, 20 }, { 55, 0 }, { 1, 12 }, { 10, 0 }, { 1, 15 }, { 1, 64 }, { 1, 72 }, { 1, 44 }, { 1, 16 }, { 63, 0 }, { 1, 6 }, { 1, 2 }, { 78, 0 }, { 1, 3 }, { 122, 0 }, { 1, 8 }, { 77, 0 }, { 1, 22 }, { 1, 35 }, { 1, 3 }, { 75, 0 }, { 1, 17 }, { 1, 52 }, { 1, 36 }, { 75, 0 }, { 1, 5 }, { 1, 41 }, { 1, 58 }, { 1, 22 }, { 75, 0 }, { 1, 24 }, { 1, 58 }, { 1, 43 }, { 1, 6 }, { 74, 0 }, { 1, 1 }, { 1, 38 }, { 1, 53 }, { 1, 22 }, { 75, 0 }, { 1, 5 }, { 1, 36 }, { 1, 21 }, { 77, 0 }, { 1, 11 }, { 1070, 0 } }, + LogoRLEFrame{ { 1479, 0 }, { 1, 41 }, { 1, 68 }, { 1, 5 }, { 76, 0 }, { 1, 52 }, { 1, 251 }, { 1, 255 }, { 1, 199 }, { 1, 11 }, { 75, 0 }, { 1, 164 }, { 3, 255 }, { 1, 137 }, { 74, 0 }, { 1, 8 }, { 1, 242 }, { 3, 255 }, { 1, 245 }, { 1, 24 }, { 73, 0 }, { 1, 80 }, { 5, 255 }, { 1, 145 }, { 73, 0 }, { 1, 166 }, { 5, 255 }, { 1, 248 }, { 1, 29 }, { 71, 0 }, { 1, 8 }, { 1, 243 }, { 6, 255 }, { 1, 147 }, { 71, 0 }, { 1, 81 }, { 7, 255 }, { 1, 239 }, { 1, 8 }, { 70, 0 }, { 1, 167 }, { 8, 255 }, { 1, 87 }, { 69, 0 }, { 1, 9 }, { 1, 244 }, { 8, 255 }, { 1, 184 }, { 69, 0 }, { 1, 83 }, { 9, 255 }, { 1, 253 }, { 1, 28 }, { 68, 0 }, { 1, 169 }, { 10, 255 }, { 1, 82 }, { 67, 0 }, { 1, 10 }, { 1, 244 }, { 10, 255 }, { 1, 95 }, { 67, 0 }, { 1, 84 }, { 11, 255 }, { 1, 77 }, { 67, 0 }, { 1, 170 }, { 10, 255 }, { 1, 240 }, { 1, 7 }, { 66, 0 }, { 1, 11 }, { 1, 245 }, { 10, 255 }, { 1, 133 }, { 67, 0 }, { 1, 86 }, { 10, 255 }, { 1, 212 }, { 1, 7 }, { 67, 0 }, { 1, 172 }, { 9, 255 }, { 1, 252 }, { 1, 53 }, { 67, 0 }, { 1, 12 }, { 1, 246 }, { 9, 255 }, { 1, 135 }, { 62, 0 }, { 1, 2 }, { 5, 0 }, { 1, 88 }, { 9, 255 }, { 1, 214 }, { 1, 7 }, { 61, 0 }, { 1, 5 }, { 1, 9 }, { 5, 0 }, { 1, 173 }, { 8, 255 }, { 1, 252 }, { 1, 55 }, { 5, 0 }, { 1, 11 }, { 1, 117 }, { 1, 162 }, { 1, 144 }, { 1, 77 }, { 52, 0 }, { 1, 29 }, { 5, 0 }, { 1, 12 }, { 1, 247 }, { 8, 255 }, { 1, 137 }, { 5, 0 }, { 1, 54 }, { 1, 221 }, { 4, 255 }, { 1, 182 }, { 1, 3 }, { 49, 0 }, { 1, 13 }, { 1, 29 }, { 5, 0 }, { 1, 89 }, { 8, 255 }, { 1, 215 }, { 1, 8 }, { 4, 0 }, { 1, 113 }, { 1, 250 }, { 6, 255 }, { 1, 137 }, { 49, 0 }, { 1, 49 }, { 1, 7 }, { 5, 0 }, { 1, 175 }, { 7, 255 }, { 1, 253 }, { 1, 56 }, { 3, 0 }, { 1, 18 }, { 1, 178 }, { 9, 255 }, { 1, 84 }, { 47, 0 }, { 1, 24 }, { 1, 46 }, { 5, 0 }, { 1, 13 }, { 1, 247 }, { 7, 255 }, { 1, 139 }, { 3, 0 }, { 1, 58 }, { 1, 225 }, { 10, 255 }, { 1, 243 }, { 1, 42 }, { 45, 0 }, { 1, 1 }, { 1, 56 }, { 1, 19 }, { 5, 0 }, { 1, 91 }, { 7, 255 }, { 1, 216 }, { 1, 8 }, { 1, 0 }, { 1, 1 }, { 1, 118 }, { 1, 251 }, { 12, 255 }, { 1, 217 }, { 1, 14 }, { 44, 0 }, { 1, 26 }, { 1, 52 }, { 6, 0 }, { 1, 177 }, { 6, 255 }, { 1, 253 }, { 1, 57 }, { 1, 0 }, { 1, 20 }, { 1, 182 }, { 15, 255 }, { 1, 176 }, { 1, 1 }, { 42, 0 }, { 1, 1 }, { 1, 56 }, { 1, 23 }, { 5, 0 }, { 1, 14 }, { 1, 248 }, { 6, 255 }, { 1, 243 }, { 1, 8 }, { 1, 62 }, { 1, 228 }, { 17, 255 }, { 1, 123 }, { 42, 0 }, { 1, 27 }, { 1, 55 }, { 6, 0 }, { 1, 92 }, { 8, 255 }, { 1, 222 }, { 1, 252 }, { 18, 255 }, { 1, 253 }, { 1, 72 }, { 40, 0 }, { 1, 2 }, { 1, 56 }, { 1, 26 }, { 6, 0 }, { 1, 178 }, { 29, 255 }, { 1, 238 }, { 1, 34 }, { 39, 0 }, { 1, 25 }, { 1, 55 }, { 1, 1 }, { 5, 0 }, { 1, 15 }, { 1, 249 }, { 30, 255 }, { 1, 203 }, { 1, 2 }, { 38, 0 }, { 1, 49 }, { 1, 19 }, { 6, 0 }, { 1, 94 }, { 32, 255 }, { 1, 64 }, { 37, 0 }, { 1, 11 }, { 1, 43 }, { 7, 0 }, { 1, 180 }, { 32, 255 }, { 1, 49 }, { 37, 0 }, { 1, 33 }, { 1, 7 }, { 6, 0 }, { 1, 16 }, { 1, 249 }, { 31, 255 }, { 1, 193 }, { 1, 1 }, { 36, 0 }, { 1, 1 }, { 1, 24 }, { 7, 0 }, { 1, 94 }, { 30, 255 }, { 1, 226 }, { 1, 137 }, { 1, 9 }, { 37, 0 }, { 1, 10 }, { 8, 0 }, { 1, 129 }, { 21, 255 }, { 1, 251 }, { 1, 225 }, { 1, 196 }, { 1, 167 }, { 1, 138 }, { 1, 109 }, { 1, 80 }, { 1, 51 }, { 1, 22 }, { 49, 0 }, { 1, 87 }, { 12, 255 }, { 1, 253 }, { 1, 230 }, { 1, 201 }, { 1, 172 }, { 1, 143 }, { 1, 114 }, { 1, 85 }, { 1, 56 }, { 1, 27 }, { 1, 2 }, { 57, 0 }, { 1, 1 }, { 1, 151 }, { 3, 255 }, { 1, 235 }, { 1, 206 }, { 1, 177 }, { 1, 148 }, { 1, 119 }, { 1, 90 }, { 1, 61 }, { 1, 32 }, { 1, 5 }, { 68, 0 }, { 1, 14 }, { 1, 32 }, { 1, 9 }, { 188, 0 }, { 1, 11 }, { 1, 23 }, { 1, 1 }, { 75, 0 }, { 1, 10 }, { 1, 42 }, { 1, 35 }, { 75, 0 }, { 1, 2 }, { 1, 34 }, { 1, 59 }, { 1, 25 }, { 75, 0 }, { 1, 18 }, { 1, 55 }, { 1, 48 }, { 1, 9 }, { 74, 0 }, { 1, 1 }, { 1, 37 }, { 1, 60 }, { 1, 28 }, { 75, 0 }, { 1, 5 }, { 1, 44 }, { 1, 33 }, { 1, 5 }, { 75, 0 }, { 1, 7 }, { 1, 22 }, { 1, 3 }, { 1226, 0 } }, + LogoRLEFrame{ { 1479, 0 }, { 1, 67 }, { 1, 135 }, { 1, 53 }, { 76, 0 }, { 1, 37 }, { 1, 247 }, { 1, 255 }, { 1, 248 }, { 1, 59 }, { 75, 0 }, { 1, 135 }, { 3, 255 }, { 1, 204 }, { 74, 0 }, { 1, 1 }, { 1, 224 }, { 4, 255 }, { 1, 75 }, { 73, 0 }, { 1, 61 }, { 5, 255 }, { 1, 203 }, { 73, 0 }, { 1, 152 }, { 6, 255 }, { 1, 74 }, { 71, 0 }, { 1, 6 }, { 1, 237 }, { 6, 255 }, { 1, 184 }, { 71, 0 }, { 1, 79 }, { 7, 255 }, { 1, 252 }, { 1, 23 }, { 70, 0 }, { 1, 170 }, { 8, 255 }, { 1, 110 }, { 69, 0 }, { 1, 14 }, { 1, 247 }, { 8, 255 }, { 1, 202 }, { 69, 0 }, { 1, 96 }, { 10, 255 }, { 1, 27 }, { 68, 0 }, { 1, 187 }, { 10, 255 }, { 1, 41 }, { 67, 0 }, { 1, 25 }, { 1, 253 }, { 10, 255 }, { 1, 40 }, { 67, 0 }, { 1, 114 }, { 10, 255 }, { 1, 221 }, { 62, 0 }, { 1, 2 }, { 5, 0 }, { 1, 205 }, { 10, 255 }, { 1, 121 }, { 62, 0 }, { 1, 15 }, { 4, 0 }, { 1, 40 }, { 10, 255 }, { 1, 210 }, { 1, 6 }, { 61, 0 }, { 1, 13 }, { 1, 15 }, { 4, 0 }, { 1, 131 }, { 9, 255 }, { 1, 250 }, { 1, 48 }, { 62, 0 }, { 1, 42 }, { 5, 0 }, { 1, 221 }, { 9, 255 }, { 1, 125 }, { 62, 0 }, { 1, 25 }, { 1, 32 }, { 4, 0 }, { 1, 57 }, { 9, 255 }, { 1, 203 }, { 1, 4 }, { 61, 0 }, { 1, 4 }, { 1, 57 }, { 1, 9 }, { 4, 0 }, { 1, 148 }, { 8, 255 }, { 1, 248 }, { 1, 42 }, { 5, 0 }, { 1, 18 }, { 1, 85 }, { 1, 104 }, { 1, 63 }, { 1, 1 }, { 52, 0 }, { 1, 32 }, { 1, 45 }, { 4, 0 }, { 1, 4 }, { 1, 235 }, { 8, 255 }, { 1, 116 }, { 5, 0 }, { 1, 76 }, { 1, 232 }, { 3, 255 }, { 1, 208 }, { 1, 27 }, { 50, 0 }, { 1, 4 }, { 1, 59 }, { 1, 15 }, { 4, 0 }, { 1, 75 }, { 8, 255 }, { 1, 196 }, { 1, 2 }, { 3, 0 }, { 1, 4 }, { 1, 137 }, { 6, 255 }, { 1, 194 }, { 1, 3 }, { 49, 0 }, { 1, 32 }, { 1, 48 }, { 5, 0 }, { 1, 166 }, { 7, 255 }, { 1, 245 }, { 1, 36 }, { 3, 0 }, { 1, 26 }, { 1, 194 }, { 8, 255 }, { 1, 131 }, { 48, 0 }, { 1, 4 }, { 1, 59 }, { 1, 18 }, { 4, 0 }, { 1, 12 }, { 1, 245 }, { 7, 255 }, { 1, 107 }, { 3, 0 }, { 1, 69 }, { 1, 234 }, { 9, 255 }, { 1, 253 }, { 1, 68 }, { 47, 0 }, { 1, 33 }, { 1, 51 }, { 5, 0 }, { 1, 92 }, { 7, 255 }, { 1, 188 }, { 1, 1 }, { 1, 0 }, { 1, 2 }, { 1, 128 }, { 1, 253 }, { 11, 255 }, { 1, 232 }, { 1, 24 }, { 45, 0 }, { 1, 2 }, { 1, 59 }, { 1, 20 }, { 5, 0 }, { 1, 183 }, { 6, 255 }, { 1, 241 }, { 1, 30 }, { 1, 0 }, { 1, 22 }, { 1, 187 }, { 14, 255 }, { 1, 188 }, { 1, 2 }, { 44, 0 }, { 1, 24 }, { 1, 44 }, { 5, 0 }, { 1, 22 }, { 1, 252 }, { 6, 255 }, { 1, 130 }, { 1, 0 }, { 1, 61 }, { 1, 229 }, { 16, 255 }, { 1, 124 }, { 44, 0 }, { 1, 46 }, { 1, 8 }, { 5, 0 }, { 1, 110 }, { 7, 255 }, { 1, 207 }, { 1, 134 }, { 1, 252 }, { 17, 255 }, { 1, 252 }, { 1, 63 }, { 42, 0 }, { 1, 10 }, { 1, 30 }, { 6, 0 }, { 1, 201 }, { 28, 255 }, { 1, 229 }, { 1, 21 }, { 41, 0 }, { 1, 24 }, { 1, 1 }, { 5, 0 }, { 1, 36 }, { 30, 255 }, { 1, 180 }, { 40, 0 }, { 1, 1 }, { 1, 10 }, { 6, 0 }, { 1, 127 }, { 31, 255 }, { 1, 56 }, { 47, 0 }, { 1, 218 }, { 31, 255 }, { 1, 69 }, { 46, 0 }, { 1, 54 }, { 31, 255 }, { 1, 226 }, { 1, 7 }, { 46, 0 }, { 1, 145 }, { 30, 255 }, { 1, 190 }, { 1, 29 }, { 47, 0 }, { 1, 216 }, { 23, 255 }, { 1, 232 }, { 1, 202 }, { 1, 171 }, { 1, 140 }, { 1, 109 }, { 1, 79 }, { 1, 28 }, { 49, 0 }, { 1, 220 }, { 14, 255 }, { 1, 251 }, { 1, 223 }, { 1, 192 }, { 1, 161 }, { 1, 130 }, { 1, 99 }, { 1, 69 }, { 1, 38 }, { 1, 8 }, { 56, 0 }, { 1, 104 }, { 6, 255 }, { 1, 244 }, { 1, 213 }, { 1, 182 }, { 1, 151 }, { 1, 120 }, { 1, 90 }, { 1, 59 }, { 1, 28 }, { 1, 2 }, { 65, 0 }, { 1, 61 }, { 1, 116 }, { 1, 111 }, { 1, 80 }, { 1, 49 }, { 1, 18 }, { 109, 0 }, { 1, 3 }, { 1, 9 }, { 76, 0 }, { 1, 3 }, { 2, 31 }, { 76, 0 }, { 1, 26 }, { 1, 57 }, { 1, 26 }, { 75, 0 }, { 1, 12 }, { 1, 49 }, { 1, 53 }, { 1, 14 }, { 74, 0 }, { 1, 1 }, { 1, 34 }, { 1, 61 }, { 1, 34 }, { 1, 1 }, { 74, 0 }, { 1, 5 }, { 1, 47 }, { 1, 46 }, { 1, 14 }, { 75, 0 }, { 1, 11 }, { 1, 35 }, { 1, 13 }, { 76, 0 }, { 1, 1 }, { 1, 7 }, { 1305, 0 } }, + LogoRLEFrame{ { 1400, 0 }, { 1, 1 }, { 77, 0 }, { 1, 1 }, { 1, 167 }, { 1, 246 }, { 1, 150 }, { 1, 1 }, { 75, 0 }, { 1, 82 }, { 3, 255 }, { 1, 102 }, { 75, 0 }, { 1, 182 }, { 3, 255 }, { 1, 222 }, { 1, 3 }, { 73, 0 }, { 1, 27 }, { 1, 252 }, { 4, 255 }, { 1, 89 }, { 73, 0 }, { 1, 123 }, { 5, 255 }, { 1, 209 }, { 72, 0 }, { 1, 1 }, { 1, 221 }, { 6, 255 }, { 1, 65 }, { 71, 0 }, { 1, 65 }, { 7, 255 }, { 1, 151 }, { 71, 0 }, { 1, 164 }, { 7, 255 }, { 1, 233 }, { 1, 3 }, { 69, 0 }, { 1, 16 }, { 1, 247 }, { 8, 255 }, { 1, 65 }, { 65, 0 }, { 1, 2 }, { 3, 0 }, { 1, 106 }, { 9, 255 }, { 1, 142 }, { 64, 0 }, { 1, 11 }, { 1, 3 }, { 3, 0 }, { 1, 205 }, { 9, 255 }, { 1, 152 }, { 64, 0 }, { 1, 28 }, { 3, 0 }, { 1, 48 }, { 10, 255 }, { 1, 143 }, { 63, 0 }, { 1, 25 }, { 1, 18 }, { 3, 0 }, { 1, 147 }, { 10, 255 }, { 1, 61 }, { 62, 0 }, { 1, 4 }, { 1, 52 }, { 3, 0 }, { 1, 7 }, { 1, 238 }, { 9, 255 }, { 1, 206 }, { 63, 0 }, { 1, 36 }, { 1, 35 }, { 3, 0 }, { 1, 89 }, { 9, 255 }, { 1, 249 }, { 1, 46 }, { 62, 0 }, { 1, 8 }, { 1, 61 }, { 1, 9 }, { 3, 0 }, { 1, 188 }, { 9, 255 }, { 1, 116 }, { 63, 0 }, { 1, 38 }, { 1, 41 }, { 3, 0 }, { 1, 32 }, { 9, 255 }, { 1, 192 }, { 1, 2 }, { 62, 0 }, { 1, 8 }, { 1, 61 }, { 1, 11 }, { 3, 0 }, { 1, 129 }, { 8, 255 }, { 1, 240 }, { 1, 30 }, { 63, 0 }, { 1, 39 }, { 1, 44 }, { 3, 0 }, { 1, 2 }, { 1, 226 }, { 8, 255 }, { 1, 92 }, { 5, 0 }, { 1, 48 }, { 1, 144 }, { 1, 166 }, { 1, 125 }, { 1, 23 }, { 53, 0 }, { 1, 8 }, { 1, 61 }, { 1, 15 }, { 3, 0 }, { 1, 71 }, { 8, 255 }, { 1, 169 }, { 4, 0 }, { 1, 1 }, { 1, 119 }, { 1, 251 }, { 3, 255 }, { 1, 241 }, { 1, 38 }, { 52, 0 }, { 1, 36 }, { 1, 46 }, { 4, 0 }, { 1, 170 }, { 7, 255 }, { 1, 228 }, { 1, 17 }, { 3, 0 }, { 1, 15 }, { 1, 176 }, { 6, 255 }, { 1, 200 }, { 1, 3 }, { 50, 0 }, { 1, 2 }, { 1, 58 }, { 1, 11 }, { 3, 0 }, { 1, 20 }, { 1, 249 }, { 7, 255 }, { 1, 68 }, { 3, 0 }, { 1, 45 }, { 1, 219 }, { 8, 255 }, { 1, 121 }, { 50, 0 }, { 1, 23 }, { 1, 37 }, { 4, 0 }, { 1, 112 }, { 7, 255 }, { 1, 144 }, { 3, 0 }, { 1, 92 }, { 1, 245 }, { 9, 255 }, { 1, 249 }, { 1, 47 }, { 49, 0 }, { 1, 43 }, { 1, 5 }, { 4, 0 }, { 1, 211 }, { 6, 255 }, { 1, 213 }, { 1, 8 }, { 1, 0 }, { 1, 6 }, { 1, 149 }, { 12, 255 }, { 1, 209 }, { 1, 6 }, { 47, 0 }, { 1, 9 }, { 1, 21 }, { 4, 0 }, { 1, 54 }, { 6, 255 }, { 1, 250 }, { 1, 49 }, { 1, 0 }, { 1, 29 }, { 1, 200 }, { 14, 255 }, { 1, 133 }, { 47, 0 }, { 1, 14 }, { 5, 0 }, { 1, 153 }, { 6, 255 }, { 1, 144 }, { 1, 0 }, { 1, 68 }, { 1, 235 }, { 15, 255 }, { 1, 252 }, { 1, 56 }, { 51, 0 }, { 1, 10 }, { 1, 241 }, { 6, 255 }, { 1, 204 }, { 1, 136 }, { 1, 253 }, { 17, 255 }, { 1, 217 }, { 1, 9 }, { 50, 0 }, { 1, 95 }, { 28, 255 }, { 1, 145 }, { 50, 0 }, { 1, 194 }, { 29, 255 }, { 1, 57 }, { 48, 0 }, { 1, 37 }, { 30, 255 }, { 1, 156 }, { 48, 0 }, { 1, 136 }, { 30, 255 }, { 1, 124 }, { 47, 0 }, { 1, 3 }, { 1, 230 }, { 29, 255 }, { 1, 221 }, { 1, 27 }, { 47, 0 }, { 1, 77 }, { 28, 255 }, { 1, 231 }, { 1, 143 }, { 1, 24 }, { 48, 0 }, { 1, 161 }, { 20, 255 }, { 1, 251 }, { 1, 220 }, { 1, 186 }, { 1, 153 }, { 1, 119 }, { 1, 85 }, { 1, 51 }, { 1, 18 }, { 51, 0 }, { 1, 180 }, { 13, 255 }, { 1, 234 }, { 1, 200 }, { 1, 167 }, { 1, 133 }, { 1, 99 }, { 1, 65 }, { 1, 32 }, { 1, 3 }, { 58, 0 }, { 1, 79 }, { 1, 252 }, { 4, 255 }, { 1, 247 }, { 1, 214 }, { 1, 181 }, { 1, 147 }, { 1, 113 }, { 1, 79 }, { 1, 46 }, { 1, 13 }, { 67, 0 }, { 1, 48 }, { 1, 102 }, { 1, 93 }, { 1, 59 }, { 1, 26 }, { 1, 1 }, { 112, 0 }, { 1, 19 }, { 1, 22 }, { 76, 0 }, { 1, 18 }, { 1, 49 }, { 1, 25 }, { 75, 0 }, { 1, 7 }, { 1, 43 }, { 1, 56 }, { 1, 16 }, { 75, 0 }, { 1, 27 }, { 1, 59 }, { 1, 40 }, { 1, 4 }, { 74, 0 }, { 1, 4 }, { 1, 47 }, { 1, 56 }, { 1, 19 }, { 56, 0 }, { 1, 1 }, { 1, 7 }, { 1, 1 }, { 16, 0 }, { 1, 11 }, { 1, 46 }, { 1, 25 }, { 1, 1 }, { 56, 0 }, { 1, 8 }, { 1, 12 }, { 17, 0 }, { 1, 9 }, { 1, 19 }, { 57, 0 }, { 1, 5 }, { 1, 17 }, { 1, 11 }, { 76, 0 }, { 1, 11 }, { 1, 19 }, { 1, 7 }, { 75, 0 }, { 1, 4 }, { 1, 17 }, { 1, 16 }, { 1, 3 }, { 75, 0 }, { 1, 10 }, { 1, 20 }, { 1, 11 }, { 75, 0 }, { 1, 2 }, { 1, 16 }, { 1, 18 }, { 1, 5 }, { 75, 0 }, { 1, 4 }, { 1, 18 }, { 1, 10 }, { 1, 1 }, { 75, 0 }, { 1, 7 }, { 1, 12 }, { 1, 2 }, { 76, 0 }, { 1, 5 }, { 1, 4 }, { 852, 0 } }, + LogoRLEFrame{ { 1479, 0 }, { 1, 2 }, { 1, 131 }, { 1, 159 }, { 1, 33 }, { 76, 0 }, { 1, 115 }, { 2, 255 }, { 1, 209 }, { 1, 1 }, { 74, 0 }, { 1, 4 }, { 1, 227 }, { 3, 255 }, { 1, 75 }, { 74, 0 }, { 1, 84 }, { 4, 255 }, { 1, 186 }, { 69, 0 }, { 1, 2 }, { 4, 0 }, { 1, 193 }, { 5, 255 }, { 1, 43 }, { 68, 0 }, { 1, 14 }, { 3, 0 }, { 1, 46 }, { 6, 255 }, { 1, 143 }, { 67, 0 }, { 1, 23 }, { 1, 5 }, { 3, 0 }, { 1, 155 }, { 6, 255 }, { 1, 219 }, { 66, 0 }, { 1, 3 }, { 1, 39 }, { 3, 0 }, { 1, 18 }, { 1, 246 }, { 7, 255 }, { 1, 38 }, { 65, 0 }, { 1, 36 }, { 1, 21 }, { 3, 0 }, { 1, 118 }, { 8, 255 }, { 1, 113 }, { 64, 0 }, { 1, 11 }, { 1, 58 }, { 1, 2 }, { 2, 0 }, { 1, 2 }, { 1, 224 }, { 8, 255 }, { 1, 161 }, { 64, 0 }, { 1, 44 }, { 1, 34 }, { 3, 0 }, { 1, 80 }, { 9, 255 }, { 1, 146 }, { 63, 0 }, { 1, 12 }, { 1, 61 }, { 1, 6 }, { 3, 0 }, { 1, 189 }, { 9, 255 }, { 1, 100 }, { 63, 0 }, { 1, 44 }, { 1, 37 }, { 3, 0 }, { 1, 42 }, { 9, 255 }, { 1, 234 }, { 1, 8 }, { 62, 0 }, { 1, 13 }, { 1, 61 }, { 1, 8 }, { 3, 0 }, { 1, 151 }, { 9, 255 }, { 1, 100 }, { 63, 0 }, { 1, 45 }, { 1, 40 }, { 3, 0 }, { 1, 15 }, { 1, 244 }, { 8, 255 }, { 1, 170 }, { 63, 0 }, { 1, 11 }, { 1, 62 }, { 1, 10 }, { 3, 0 }, { 1, 113 }, { 8, 255 }, { 1, 225 }, { 1, 16 }, { 63, 0 }, { 2, 36 }, { 3, 0 }, { 1, 1 }, { 1, 220 }, { 7, 255 }, { 1, 252 }, { 1, 59 }, { 63, 0 }, { 1, 1 }, { 1, 54 }, { 1, 4 }, { 3, 0 }, { 1, 75 }, { 8, 255 }, { 1, 127 }, { 5, 0 }, { 1, 54 }, { 1, 111 }, { 1, 96 }, { 1, 26 }, { 55, 0 }, { 1, 22 }, { 1, 26 }, { 4, 0 }, { 1, 184 }, { 7, 255 }, { 1, 194 }, { 1, 3 }, { 3, 0 }, { 1, 7 }, { 1, 153 }, { 3, 255 }, { 1, 244 }, { 1, 73 }, { 54, 0 }, { 1, 31 }, { 4, 0 }, { 1, 38 }, { 7, 255 }, { 1, 238 }, { 1, 29 }, { 3, 0 }, { 1, 27 }, { 1, 201 }, { 5, 255 }, { 1, 224 }, { 1, 10 }, { 52, 0 }, { 1, 5 }, { 1, 8 }, { 4, 0 }, { 1, 146 }, { 7, 255 }, { 1, 83 }, { 3, 0 }, { 1, 59 }, { 1, 231 }, { 7, 255 }, { 1, 131 }, { 57, 0 }, { 1, 13 }, { 1, 242 }, { 6, 255 }, { 1, 154 }, { 3, 0 }, { 1, 103 }, { 1, 250 }, { 8, 255 }, { 1, 248 }, { 1, 37 }, { 56, 0 }, { 1, 108 }, { 6, 255 }, { 1, 214 }, { 1, 10 }, { 1, 0 }, { 1, 6 }, { 1, 153 }, { 11, 255 }, { 1, 184 }, { 55, 0 }, { 1, 1 }, { 1, 216 }, { 5, 255 }, { 1, 248 }, { 1, 47 }, { 1, 0 }, { 1, 24 }, { 1, 197 }, { 13, 255 }, { 1, 80 }, { 54, 0 }, { 1, 70 }, { 6, 255 }, { 1, 110 }, { 1, 0 }, { 1, 55 }, { 1, 229 }, { 14, 255 }, { 1, 224 }, { 1, 10 }, { 53, 0 }, { 1, 179 }, { 5, 255 }, { 1, 250 }, { 1, 6 }, { 1, 99 }, { 1, 248 }, { 16, 255 }, { 1, 131 }, { 52, 0 }, { 1, 34 }, { 1, 253 }, { 6, 255 }, { 1, 228 }, { 18, 255 }, { 1, 248 }, { 1, 37 }, { 51, 0 }, { 1, 141 }, { 27, 255 }, { 1, 181 }, { 50, 0 }, { 1, 11 }, { 1, 239 }, { 28, 255 }, { 1, 33 }, { 49, 0 }, { 1, 103 }, { 28, 255 }, { 1, 249 }, { 1, 18 }, { 49, 0 }, { 1, 212 }, { 28, 255 }, { 1, 142 }, { 49, 0 }, { 1, 66 }, { 27, 255 }, { 1, 221 }, { 1, 110 }, { 50, 0 }, { 1, 175 }, { 21, 255 }, { 1, 231 }, { 1, 192 }, { 1, 154 }, { 1, 116 }, { 1, 77 }, { 1, 39 }, { 1, 1 }, { 50, 0 }, { 1, 2 }, { 1, 249 }, { 14, 255 }, { 1, 243 }, { 1, 205 }, { 1, 167 }, { 1, 128 }, { 1, 90 }, { 1, 51 }, { 1, 14 }, { 57, 0 }, { 1, 7 }, { 1, 241 }, { 7, 255 }, { 1, 251 }, { 1, 218 }, { 1, 179 }, { 1, 141 }, { 1, 102 }, { 1, 64 }, { 1, 26 }, { 65, 0 }, { 1, 83 }, { 1, 215 }, { 1, 225 }, { 1, 192 }, { 1, 153 }, { 1, 115 }, { 1, 77 }, { 1, 38 }, { 1, 5 }, { 33, 0 }, { 1, 4 }, { 1, 3 }, { 76, 0 }, { 1, 4 }, { 1, 18 }, { 1, 11 }, { 75, 0 }, { 1, 1 }, { 1, 17 }, { 1, 28 }, { 1, 8 }, { 56, 0 }, { 1, 11 }, { 1, 1 }, { 17, 0 }, { 1, 9 }, { 1, 27 }, { 1, 22 }, { 1, 3 }, { 55, 0 }, { 1, 10 }, { 1, 31 }, { 1, 5 }, { 16, 0 }, { 1, 2 }, { 1, 21 }, { 1, 29 }, { 1, 12 }, { 55, 0 }, { 1, 3 }, { 1, 35 }, { 1, 41 }, { 1, 2 }, { 16, 0 }, { 1, 5 }, { 1, 25 }, { 1, 19 }, { 1, 3 }, { 55, 0 }, { 1, 20 }, { 1, 57 }, { 1, 34 }, { 17, 0 }, { 1, 8 }, { 1, 15 }, { 1, 3 }, { 55, 0 }, { 1, 4 }, { 1, 41 }, { 1, 58 }, { 1, 20 }, { 17, 0 }, { 1, 1 }, { 1, 2 }, { 56, 0 }, { 1, 18 }, { 1, 56 }, { 1, 47 }, { 1, 7 }, { 74, 0 }, { 1, 2 }, { 1, 38 }, { 1, 61 }, { 1, 30 }, { 75, 0 }, { 1, 6 }, { 2, 50 }, { 1, 13 }, { 75, 0 }, { 1, 13 }, { 1, 49 }, { 1, 23 }, { 76, 0 }, { 1, 18 }, { 1, 29 }, { 1, 2 }, { 76, 0 }, { 1, 8 }, { 1, 6 }, { 1088, 0 } }, + LogoRLEFrame{ { 1396, 0 }, { 1, 1 }, { 78, 0 }, { 1, 14 }, { 78, 0 }, { 1, 3 }, { 1, 25 }, { 5, 0 }, { 1, 82 }, { 1, 174 }, { 1, 71 }, { 70, 0 }, { 1, 35 }, { 1, 7 }, { 4, 0 }, { 1, 43 }, { 1, 250 }, { 1, 255 }, { 1, 243 }, { 1, 18 }, { 68, 0 }, { 1, 11 }, { 1, 46 }, { 5, 0 }, { 1, 164 }, { 3, 255 }, { 1, 109 }, { 68, 0 }, { 1, 47 }, { 1, 24 }, { 4, 0 }, { 1, 33 }, { 1, 252 }, { 3, 255 }, { 1, 209 }, { 67, 0 }, { 1, 18 }, { 1, 57 }, { 1, 2 }, { 4, 0 }, { 1, 151 }, { 5, 255 }, { 1, 51 }, { 66, 0 }, { 1, 49 }, { 1, 30 }, { 4, 0 }, { 1, 24 }, { 1, 248 }, { 5, 255 }, { 1, 124 }, { 65, 0 }, { 1, 18 }, { 1, 59 }, { 1, 3 }, { 4, 0 }, { 1, 138 }, { 6, 255 }, { 1, 187 }, { 65, 0 }, { 1, 49 }, { 1, 33 }, { 4, 0 }, { 1, 16 }, { 1, 242 }, { 6, 255 }, { 1, 245 }, { 1, 5 }, { 63, 0 }, { 1, 18 }, { 1, 60 }, { 1, 5 }, { 4, 0 }, { 1, 124 }, { 8, 255 }, { 1, 40 }, { 63, 0 }, { 1, 48 }, { 1, 34 }, { 4, 0 }, { 1, 10 }, { 1, 235 }, { 8, 255 }, { 1, 15 }, { 62, 0 }, { 1, 9 }, { 1, 56 }, { 1, 3 }, { 4, 0 }, { 1, 111 }, { 8, 255 }, { 1, 214 }, { 63, 0 }, { 1, 34 }, { 1, 20 }, { 4, 0 }, { 1, 5 }, { 1, 226 }, { 8, 255 }, { 1, 85 }, { 62, 0 }, { 1, 1 }, { 1, 39 }, { 5, 0 }, { 1, 98 }, { 8, 255 }, { 1, 182 }, { 1, 1 }, { 62, 0 }, { 1, 17 }, { 1, 8 }, { 4, 0 }, { 1, 2 }, { 1, 217 }, { 7, 255 }, { 1, 227 }, { 1, 20 }, { 63, 0 }, { 1, 11 }, { 5, 0 }, { 1, 84 }, { 7, 255 }, { 1, 251 }, { 1, 60 }, { 70, 0 }, { 1, 205 }, { 7, 255 }, { 1, 119 }, { 70, 0 }, { 1, 71 }, { 7, 255 }, { 1, 182 }, { 1, 1 }, { 3, 0 }, { 1, 22 }, { 1, 153 }, { 1, 209 }, { 1, 187 }, { 1, 82 }, { 62, 0 }, { 1, 192 }, { 6, 255 }, { 1, 227 }, { 1, 20 }, { 3, 0 }, { 1, 56 }, { 1, 232 }, { 4, 255 }, { 1, 58 }, { 60, 0 }, { 1, 58 }, { 6, 255 }, { 1, 251 }, { 1, 60 }, { 3, 0 }, { 1, 91 }, { 1, 248 }, { 5, 255 }, { 1, 187 }, { 60, 0 }, { 1, 179 }, { 6, 255 }, { 1, 119 }, { 2, 0 }, { 1, 1 }, { 1, 133 }, { 8, 255 }, { 1, 62 }, { 58, 0 }, { 1, 46 }, { 6, 255 }, { 1, 182 }, { 1, 1 }, { 1, 0 }, { 1, 11 }, { 1, 174 }, { 9, 255 }, { 1, 192 }, { 58, 0 }, { 1, 166 }, { 5, 255 }, { 1, 227 }, { 1, 20 }, { 1, 0 }, { 1, 28 }, { 1, 207 }, { 11, 255 }, { 1, 65 }, { 56, 0 }, { 1, 34 }, { 1, 252 }, { 4, 255 }, { 1, 251 }, { 1, 59 }, { 1, 0 }, { 1, 55 }, { 1, 231 }, { 12, 255 }, { 1, 195 }, { 56, 0 }, { 1, 152 }, { 5, 255 }, { 1, 119 }, { 1, 0 }, { 1, 89 }, { 1, 247 }, { 14, 255 }, { 1, 68 }, { 54, 0 }, { 1, 25 }, { 1, 248 }, { 4, 255 }, { 1, 227 }, { 1, 2 }, { 1, 131 }, { 16, 255 }, { 1, 198 }, { 54, 0 }, { 1, 139 }, { 6, 255 }, { 1, 216 }, { 18, 255 }, { 1, 71 }, { 52, 0 }, { 1, 17 }, { 1, 243 }, { 25, 255 }, { 1, 196 }, { 52, 0 }, { 1, 126 }, { 26, 255 }, { 1, 239 }, { 51, 0 }, { 1, 11 }, { 1, 236 }, { 26, 255 }, { 1, 153 }, { 51, 0 }, { 1, 112 }, { 26, 255 }, { 1, 183 }, { 1, 11 }, { 50, 0 }, { 1, 6 }, { 1, 228 }, { 22, 255 }, { 1, 246 }, { 1, 203 }, { 1, 155 }, { 1, 65 }, { 1, 1 }, { 51, 0 }, { 1, 99 }, { 17, 255 }, { 1, 253 }, { 1, 218 }, { 1, 173 }, { 1, 128 }, { 1, 83 }, { 1, 37 }, { 1, 2 }, { 55, 0 }, { 1, 202 }, { 12, 255 }, { 1, 233 }, { 1, 188 }, { 1, 142 }, { 1, 97 }, { 1, 52 }, { 1, 10 }, { 61, 0 }, { 1, 232 }, { 6, 255 }, { 1, 246 }, { 1, 202 }, { 1, 157 }, { 1, 112 }, { 1, 67 }, { 1, 22 }, { 10, 0 }, { 1, 1 }, { 56, 0 }, { 1, 94 }, { 1, 219 }, { 1, 214 }, { 1, 172 }, { 1, 127 }, { 1, 82 }, { 1, 37 }, { 1, 2 }, { 13, 0 }, { 1, 1 }, { 1, 20 }, { 1, 6 }, { 76, 0 }, { 1, 18 }, { 1, 39 }, { 1, 5 }, { 75, 0 }, { 1, 9 }, { 1, 44 }, { 1, 42 }, { 1, 2 }, { 75, 0 }, { 1, 27 }, { 1, 60 }, { 1, 32 }, { 75, 0 }, { 1, 7 }, { 1, 46 }, { 1, 56 }, { 1, 16 }, { 75, 0 }, { 1, 24 }, { 1, 59 }, { 1, 43 }, { 1, 4 }, { 74, 0 }, { 1, 2 }, { 1, 42 }, { 1, 60 }, { 1, 25 }, { 75, 0 }, { 1, 6 }, { 1, 50 }, { 1, 41 }, { 1, 8 }, { 75, 0 }, { 1, 13 }, { 1, 43 }, { 1, 14 }, { 76, 0 }, { 1, 14 }, { 1, 19 }, { 77, 0 }, { 2, 1 }, { 1324, 0 } }, + LogoRLEFrame{ { 998, 0 }, { 1, 2 }, { 78, 0 }, { 1, 3 }, { 1, 10 }, { 78, 0 }, { 1, 28 }, { 78, 0 }, { 1, 11 }, { 1, 32 }, { 78, 0 }, { 1, 47 }, { 1, 10 }, { 77, 0 }, { 1, 21 }, { 1, 49 }, { 78, 0 }, { 1, 54 }, { 1, 22 }, { 77, 0 }, { 1, 23 }, { 1, 54 }, { 78, 0 }, { 1, 54 }, { 1, 26 }, { 6, 0 }, { 1, 21 }, { 1, 145 }, { 1, 100 }, { 68, 0 }, { 1, 24 }, { 1, 57 }, { 1, 1 }, { 6, 0 }, { 1, 192 }, { 2, 255 }, { 1, 46 }, { 66, 0 }, { 1, 1 }, { 1, 54 }, { 1, 29 }, { 6, 0 }, { 1, 79 }, { 3, 255 }, { 1, 133 }, { 66, 0 }, { 1, 22 }, { 1, 57 }, { 1, 3 }, { 5, 0 }, { 1, 2 }, { 1, 211 }, { 3, 255 }, { 1, 217 }, { 66, 0 }, { 1, 47 }, { 1, 22 }, { 6, 0 }, { 1, 93 }, { 5, 255 }, { 1, 38 }, { 64, 0 }, { 1, 8 }, { 1, 46 }, { 6, 0 }, { 1, 5 }, { 1, 222 }, { 5, 255 }, { 1, 87 }, { 64, 0 }, { 1, 31 }, { 1, 9 }, { 6, 0 }, { 1, 107 }, { 6, 255 }, { 1, 133 }, { 64, 0 }, { 1, 25 }, { 6, 0 }, { 1, 10 }, { 1, 231 }, { 6, 255 }, { 1, 174 }, { 63, 0 }, { 1, 9 }, { 1, 1 }, { 6, 0 }, { 1, 120 }, { 7, 255 }, { 1, 142 }, { 70, 0 }, { 1, 16 }, { 1, 238 }, { 7, 255 }, { 1, 74 }, { 70, 0 }, { 1, 134 }, { 7, 255 }, { 1, 185 }, { 70, 0 }, { 1, 24 }, { 1, 245 }, { 6, 255 }, { 1, 237 }, { 1, 32 }, { 70, 0 }, { 1, 148 }, { 6, 255 }, { 1, 253 }, { 1, 73 }, { 70, 0 }, { 1, 32 }, { 1, 249 }, { 6, 255 }, { 1, 127 }, { 71, 0 }, { 1, 161 }, { 6, 255 }, { 1, 182 }, { 1, 1 }, { 3, 0 }, { 1, 15 }, { 1, 54 }, { 1, 23 }, { 64, 0 }, { 1, 43 }, { 1, 253 }, { 5, 255 }, { 1, 222 }, { 1, 17 }, { 3, 0 }, { 1, 108 }, { 1, 245 }, { 1, 255 }, { 1, 253 }, { 1, 122 }, { 63, 0 }, { 1, 175 }, { 5, 255 }, { 1, 247 }, { 1, 49 }, { 2, 0 }, { 1, 3 }, { 1, 151 }, { 4, 255 }, { 1, 246 }, { 1, 17 }, { 61, 0 }, { 1, 55 }, { 6, 255 }, { 1, 96 }, { 2, 0 }, { 1, 12 }, { 1, 182 }, { 6, 255 }, { 1, 112 }, { 61, 0 }, { 1, 189 }, { 5, 255 }, { 1, 152 }, { 2, 0 }, { 1, 27 }, { 1, 207 }, { 7, 255 }, { 1, 216 }, { 60, 0 }, { 1, 67 }, { 5, 255 }, { 1, 202 }, { 1, 7 }, { 1, 0 }, { 1, 46 }, { 1, 227 }, { 9, 255 }, { 1, 65 }, { 59, 0 }, { 1, 201 }, { 4, 255 }, { 1, 235 }, { 1, 30 }, { 1, 0 }, { 1, 71 }, { 1, 242 }, { 10, 255 }, { 1, 169 }, { 58, 0 }, { 1, 81 }, { 4, 255 }, { 1, 253 }, { 1, 69 }, { 1, 0 }, { 1, 101 }, { 1, 251 }, { 11, 255 }, { 1, 250 }, { 1, 23 }, { 56, 0 }, { 1, 2 }, { 1, 213 }, { 4, 255 }, { 1, 122 }, { 1, 1 }, { 1, 135 }, { 14, 255 }, { 1, 122 }, { 56, 0 }, { 1, 95 }, { 4, 255 }, { 1, 199 }, { 1, 9 }, { 1, 168 }, { 15, 255 }, { 1, 224 }, { 1, 2 }, { 54, 0 }, { 1, 6 }, { 1, 223 }, { 4, 255 }, { 1, 239 }, { 1, 212 }, { 17, 255 }, { 1, 75 }, { 54, 0 }, { 1, 108 }, { 24, 255 }, { 1, 158 }, { 53, 0 }, { 1, 11 }, { 1, 232 }, { 24, 255 }, { 1, 113 }, { 53, 0 }, { 1, 122 }, { 24, 255 }, { 1, 200 }, { 1, 11 }, { 52, 0 }, { 1, 17 }, { 1, 239 }, { 22, 255 }, { 1, 237 }, { 1, 137 }, { 1, 9 }, { 53, 0 }, { 1, 136 }, { 19, 255 }, { 1, 216 }, { 1, 163 }, { 1, 110 }, { 1, 56 }, { 1, 7 }, { 1, 0 }, { 1, 8 }, { 1, 2 }, { 51, 0 }, { 1, 25 }, { 1, 245 }, { 14, 255 }, { 1, 227 }, { 1, 174 }, { 1, 120 }, { 1, 67 }, { 1, 15 }, { 4, 0 }, { 1, 4 }, { 1, 29 }, { 1, 9 }, { 52, 0 }, { 1, 144 }, { 10, 255 }, { 1, 237 }, { 1, 184 }, { 1, 131 }, { 1, 78 }, { 1, 25 }, { 8, 0 }, { 1, 27 }, { 1, 45 }, { 1, 5 }, { 53, 0 }, { 1, 215 }, { 5, 255 }, { 1, 245 }, { 1, 195 }, { 1, 142 }, { 1, 88 }, { 1, 35 }, { 11, 0 }, { 1, 13 }, { 1, 51 }, { 1, 43 }, { 1, 2 }, { 54, 0 }, { 1, 117 }, { 1, 227 }, { 1, 205 }, { 1, 152 }, { 1, 99 }, { 1, 46 }, { 1, 3 }, { 13, 0 }, { 1, 1 }, { 1, 32 }, { 1, 60 }, { 1, 29 }, { 75, 0 }, { 1, 11 }, { 1, 50 }, { 1, 53 }, { 1, 13 }, { 75, 0 }, { 1, 29 }, { 1, 61 }, { 1, 38 }, { 1, 2 }, { 74, 0 }, { 1, 2 }, { 1, 42 }, { 1, 57 }, { 1, 20 }, { 75, 0 }, { 1, 7 }, { 1, 50 }, { 1, 36 }, { 1, 3 }, { 75, 0 }, { 1, 14 }, { 1, 39 }, { 1, 12 }, { 76, 0 }, { 1, 7 }, { 1, 13 }, { 1639, 0 } }, + LogoRLEFrame{ { 680, 0 }, { 1, 7 }, { 78, 0 }, { 1, 5 }, { 1, 8 }, { 78, 0 }, { 1, 20 }, { 78, 0 }, { 1, 10 }, { 1, 17 }, { 77, 0 }, { 1, 1 }, { 1, 27 }, { 1, 6 }, { 77, 0 }, { 1, 14 }, { 1, 23 }, { 77, 0 }, { 1, 1 }, { 1, 28 }, { 1, 9 }, { 77, 0 }, { 1, 14 }, { 1, 25 }, { 77, 0 }, { 1, 1 }, { 1, 28 }, { 1, 10 }, { 77, 0 }, { 1, 15 }, { 1, 26 }, { 78, 0 }, { 1, 28 }, { 1, 11 }, { 77, 0 }, { 1, 10 }, { 1, 23 }, { 78, 0 }, { 1, 21 }, { 1, 5 }, { 7, 0 }, { 1, 85 }, { 1, 118 }, { 1, 1 }, { 67, 0 }, { 1, 3 }, { 1, 16 }, { 7, 0 }, { 1, 93 }, { 2, 255 }, { 1, 79 }, { 67, 0 }, { 1, 11 }, { 1, 1 }, { 6, 0 }, { 1, 16 }, { 1, 235 }, { 2, 255 }, { 1, 149 }, { 67, 0 }, { 1, 5 }, { 7, 0 }, { 1, 144 }, { 3, 255 }, { 1, 216 }, { 74, 0 }, { 1, 41 }, { 1, 251 }, { 4, 255 }, { 1, 10 }, { 73, 0 }, { 1, 185 }, { 5, 255 }, { 1, 38 }, { 72, 0 }, { 1, 78 }, { 6, 255 }, { 1, 67 }, { 71, 0 }, { 1, 6 }, { 1, 219 }, { 6, 255 }, { 1, 37 }, { 71, 0 }, { 1, 119 }, { 6, 255 }, { 1, 212 }, { 71, 0 }, { 1, 24 }, { 1, 242 }, { 5, 255 }, { 1, 253 }, { 1, 60 }, { 71, 0 }, { 1, 160 }, { 6, 255 }, { 1, 129 }, { 71, 0 }, { 1, 54 }, { 6, 255 }, { 1, 177 }, { 1, 1 }, { 70, 0 }, { 1, 1 }, { 1, 200 }, { 5, 255 }, { 1, 214 }, { 1, 13 }, { 71, 0 }, { 1, 93 }, { 5, 255 }, { 1, 239 }, { 1, 37 }, { 3, 0 }, { 1, 77 }, { 1, 130 }, { 1, 91 }, { 1, 2 }, { 64, 0 }, { 1, 12 }, { 1, 229 }, { 4, 255 }, { 1, 253 }, { 1, 72 }, { 2, 0 }, { 1, 11 }, { 1, 177 }, { 3, 255 }, { 1, 131 }, { 64, 0 }, { 1, 134 }, { 5, 255 }, { 1, 118 }, { 2, 0 }, { 1, 22 }, { 1, 203 }, { 4, 255 }, { 1, 217 }, { 63, 0 }, { 1, 34 }, { 1, 248 }, { 4, 255 }, { 1, 167 }, { 2, 0 }, { 1, 36 }, { 1, 220 }, { 6, 255 }, { 1, 42 }, { 62, 0 }, { 1, 175 }, { 4, 255 }, { 1, 206 }, { 1, 9 }, { 1, 0 }, { 1, 53 }, { 1, 234 }, { 7, 255 }, { 1, 123 }, { 51, 0 }, { 1, 4 }, { 9, 0 }, { 1, 68 }, { 4, 255 }, { 1, 234 }, { 1, 30 }, { 1, 0 }, { 1, 74 }, { 1, 244 }, { 8, 255 }, { 1, 204 }, { 50, 0 }, { 1, 1 }, { 1, 9 }, { 8, 0 }, { 1, 4 }, { 1, 212 }, { 3, 255 }, { 1, 250 }, { 1, 62 }, { 1, 0 }, { 1, 98 }, { 1, 251 }, { 10, 255 }, { 1, 30 }, { 49, 0 }, { 1, 13 }, { 1, 2 }, { 8, 0 }, { 1, 109 }, { 4, 255 }, { 1, 106 }, { 1, 0 }, { 1, 125 }, { 12, 255 }, { 1, 110 }, { 48, 0 }, { 1, 7 }, { 1, 13 }, { 8, 0 }, { 1, 19 }, { 1, 238 }, { 3, 255 }, { 1, 155 }, { 1, 3 }, { 1, 153 }, { 13, 255 }, { 1, 191 }, { 47, 0 }, { 1, 2 }, { 1, 19 }, { 1, 4 }, { 8, 0 }, { 1, 150 }, { 3, 255 }, { 1, 204 }, { 1, 16 }, { 1, 177 }, { 14, 255 }, { 1, 252 }, { 1, 20 }, { 46, 0 }, { 2, 13 }, { 8, 0 }, { 1, 46 }, { 1, 252 }, { 3, 255 }, { 1, 213 }, { 1, 206 }, { 16, 255 }, { 1, 89 }, { 45, 0 }, { 1, 4 }, { 1, 20 }, { 1, 2 }, { 8, 0 }, { 1, 190 }, { 22, 255 }, { 1, 116 }, { 45, 0 }, { 1, 17 }, { 1, 11 }, { 8, 0 }, { 1, 83 }, { 22, 255 }, { 1, 243 }, { 1, 24 }, { 44, 0 }, { 1, 8 }, { 1, 19 }, { 1, 1 }, { 7, 0 }, { 1, 8 }, { 1, 223 }, { 21, 255 }, { 1, 231 }, { 1, 53 }, { 5, 0 }, { 1, 16 }, { 1, 8 }, { 37, 0 }, { 1, 1 }, { 1, 19 }, { 1, 8 }, { 8, 0 }, { 1, 124 }, { 19, 255 }, { 1, 247 }, { 1, 191 }, { 1, 108 }, { 1, 11 }, { 4, 0 }, { 1, 10 }, { 1, 38 }, { 1, 10 }, { 38, 0 }, { 1, 10 }, { 1, 16 }, { 8, 0 }, { 1, 28 }, { 1, 245 }, { 15, 255 }, { 1, 241 }, { 1, 182 }, { 1, 120 }, { 1, 59 }, { 1, 6 }, { 5, 0 }, { 1, 3 }, { 1, 35 }, { 1, 49 }, { 1, 6 }, { 38, 0 }, { 1, 1 }, { 1, 18 }, { 1, 3 }, { 8, 0 }, { 1, 165 }, { 12, 255 }, { 1, 234 }, { 1, 173 }, { 1, 111 }, { 1, 50 }, { 1, 3 }, { 8, 0 }, { 1, 17 }, { 1, 56 }, { 1, 42 }, { 1, 2 }, { 39, 0 }, { 2, 9 }, { 8, 0 }, { 1, 58 }, { 9, 255 }, { 1, 226 }, { 1, 164 }, { 1, 102 }, { 1, 40 }, { 11, 0 }, { 1, 2 }, { 1, 37 }, { 1, 60 }, { 1, 25 }, { 41, 0 }, { 1, 12 }, { 9, 0 }, { 1, 168 }, { 5, 255 }, { 1, 216 }, { 1, 155 }, { 1, 93 }, { 1, 31 }, { 14, 0 }, { 1, 15 }, { 1, 54 }, { 1, 50 }, { 1, 9 }, { 41, 0 }, { 1, 5 }, { 1, 1 }, { 9, 0 }, { 1, 133 }, { 1, 243 }, { 1, 207 }, { 1, 146 }, { 1, 84 }, { 1, 23 }, { 17, 0 }, { 1, 32 }, { 1, 62 }, { 1, 34 }, { 1, 1 }, { 42, 0 }, { 1, 1 }, { 31, 0 }, { 1, 2 }, { 1, 43 }, { 1, 49 }, { 1, 14 }, { 75, 0 }, { 1, 7 }, { 1, 43 }, { 1, 22 }, { 76, 0 }, { 1, 10 }, { 1, 24 }, { 1, 1 }, { 77, 0 }, { 1, 3 }, { 1875, 0 } }, + LogoRLEFrame{ { 1643, 0 }, { 1, 62 }, { 1, 116 }, { 1, 1 }, { 76, 0 }, { 1, 66 }, { 1, 251 }, { 1, 255 }, { 1, 64 }, { 75, 0 }, { 1, 10 }, { 1, 223 }, { 2, 255 }, { 1, 116 }, { 75, 0 }, { 1, 138 }, { 3, 255 }, { 1, 165 }, { 74, 0 }, { 1, 47 }, { 1, 251 }, { 3, 255 }, { 1, 184 }, { 73, 0 }, { 1, 2 }, { 1, 200 }, { 4, 255 }, { 1, 195 }, { 73, 0 }, { 1, 107 }, { 5, 255 }, { 1, 180 }, { 72, 0 }, { 1, 26 }, { 1, 241 }, { 5, 255 }, { 1, 93 }, { 72, 0 }, { 1, 173 }, { 5, 255 }, { 1, 194 }, { 1, 3 }, { 71, 0 }, { 1, 77 }, { 5, 255 }, { 1, 231 }, { 1, 26 }, { 71, 0 }, { 1, 12 }, { 1, 226 }, { 4, 255 }, { 1, 247 }, { 1, 54 }, { 72, 0 }, { 1, 143 }, { 5, 255 }, { 1, 89 }, { 72, 0 }, { 1, 50 }, { 1, 252 }, { 4, 255 }, { 1, 129 }, { 2, 0 }, { 1, 3 }, { 1, 102 }, { 1, 161 }, { 1, 117 }, { 1, 5 }, { 56, 0 }, { 1, 5 }, { 8, 0 }, { 1, 3 }, { 1, 204 }, { 4, 255 }, { 1, 170 }, { 1, 1 }, { 1, 0 }, { 1, 17 }, { 1, 194 }, { 3, 255 }, { 1, 100 }, { 55, 0 }, { 1, 4 }, { 1, 16 }, { 8, 0 }, { 1, 112 }, { 4, 255 }, { 1, 204 }, { 1, 9 }, { 1, 0 }, { 1, 27 }, { 1, 212 }, { 4, 255 }, { 1, 161 }, { 55, 0 }, { 1, 34 }, { 8, 0 }, { 1, 29 }, { 1, 243 }, { 3, 255 }, { 1, 229 }, { 1, 26 }, { 1, 0 }, { 1, 40 }, { 1, 225 }, { 5, 255 }, { 1, 223 }, { 54, 0 }, { 1, 22 }, { 1, 28 }, { 8, 0 }, { 1, 178 }, { 3, 255 }, { 1, 246 }, { 1, 52 }, { 1, 0 }, { 1, 56 }, { 1, 236 }, { 7, 255 }, { 1, 29 }, { 52, 0 }, { 1, 7 }, { 1, 55 }, { 1, 3 }, { 7, 0 }, { 1, 82 }, { 4, 255 }, { 1, 85 }, { 1, 0 }, { 1, 74 }, { 1, 245 }, { 8, 255 }, { 1, 90 }, { 52, 0 }, { 1, 45 }, { 1, 34 }, { 7, 0 }, { 1, 14 }, { 1, 229 }, { 3, 255 }, { 1, 125 }, { 1, 0 }, { 1, 94 }, { 1, 251 }, { 9, 255 }, { 1, 152 }, { 51, 0 }, { 1, 20 }, { 1, 58 }, { 1, 4 }, { 7, 0 }, { 1, 148 }, { 3, 255 }, { 1, 167 }, { 1, 0 }, { 1, 117 }, { 11, 255 }, { 1, 213 }, { 50, 0 }, { 1, 2 }, { 1, 55 }, { 1, 26 }, { 7, 0 }, { 1, 55 }, { 1, 253 }, { 2, 255 }, { 1, 201 }, { 1, 10 }, { 1, 141 }, { 13, 255 }, { 1, 20 }, { 49, 0 }, { 1, 31 }, { 1, 53 }, { 7, 0 }, { 1, 4 }, { 1, 208 }, { 2, 255 }, { 1, 227 }, { 1, 30 }, { 1, 164 }, { 14, 255 }, { 1, 80 }, { 48, 0 }, { 1, 8 }, { 1, 61 }, { 1, 18 }, { 7, 0 }, { 1, 117 }, { 3, 255 }, { 1, 177 }, { 1, 187 }, { 15, 255 }, { 1, 129 }, { 11, 0 }, { 1, 4 }, { 1, 3 }, { 35, 0 }, { 1, 42 }, { 1, 46 }, { 7, 0 }, { 1, 32 }, { 1, 245 }, { 20, 255 }, { 1, 116 }, { 9, 0 }, { 1, 1 }, { 1, 25 }, { 1, 14 }, { 35, 0 }, { 1, 10 }, { 1, 59 }, { 1, 8 }, { 7, 0 }, { 1, 183 }, { 20, 255 }, { 1, 212 }, { 1, 9 }, { 8, 0 }, { 1, 18 }, { 1, 46 }, { 1, 11 }, { 36, 0 }, { 1, 40 }, { 1, 26 }, { 7, 0 }, { 1, 87 }, { 20, 255 }, { 1, 171 }, { 1, 15 }, { 7, 0 }, { 1, 6 }, { 1, 44 }, { 1, 50 }, { 1, 6 }, { 36, 0 }, { 1, 7 }, { 1, 45 }, { 7, 0 }, { 1, 16 }, { 1, 231 }, { 16, 255 }, { 1, 251 }, { 1, 195 }, { 1, 126 }, { 1, 40 }, { 8, 0 }, { 1, 23 }, { 1, 58 }, { 1, 38 }, { 1, 2 }, { 37, 0 }, { 1, 30 }, { 1, 5 }, { 7, 0 }, { 1, 153 }, { 14, 255 }, { 1, 217 }, { 1, 147 }, { 1, 78 }, { 1, 14 }, { 9, 0 }, { 1, 5 }, { 1, 43 }, { 1, 58 }, { 1, 20 }, { 38, 0 }, { 1, 4 }, { 1, 12 }, { 7, 0 }, { 1, 59 }, { 11, 255 }, { 1, 236 }, { 1, 168 }, { 1, 99 }, { 1, 30 }, { 12, 0 }, { 1, 19 }, { 1, 57 }, { 1, 47 }, { 1, 7 }, { 39, 0 }, { 1, 1 }, { 7, 0 }, { 1, 5 }, { 1, 212 }, { 7, 255 }, { 1, 249 }, { 1, 190 }, { 1, 120 }, { 1, 51 }, { 1, 2 }, { 14, 0 }, { 1, 33 }, { 1, 60 }, { 1, 29 }, { 49, 0 }, { 1, 106 }, { 5, 255 }, { 1, 211 }, { 1, 141 }, { 1, 72 }, { 1, 10 }, { 16, 0 }, { 1, 2 }, { 1, 43 }, { 1, 39 }, { 1, 6 }, { 50, 0 }, { 1, 156 }, { 1, 255 }, { 1, 231 }, { 1, 163 }, { 1, 93 }, { 1, 25 }, { 19, 0 }, { 1, 7 }, { 1, 36 }, { 1, 12 }, { 52, 0 }, { 1, 4 }, { 1, 24 }, { 22, 0 }, { 1, 5 }, { 1, 14 }, { 2190, 0 } }, + LogoRLEFrame{ { 1724, 0 }, { 1, 99 }, { 1, 81 }, { 77, 0 }, { 1, 129 }, { 1, 255 }, { 1, 228 }, { 76, 0 }, { 1, 57 }, { 1, 253 }, { 2, 255 }, { 1, 9 }, { 74, 0 }, { 1, 8 }, { 1, 216 }, { 3, 255 }, { 1, 35 }, { 74, 0 }, { 1, 140 }, { 4, 255 }, { 1, 31 }, { 73, 0 }, { 1, 58 }, { 1, 253 }, { 4, 255 }, { 1, 23 }, { 72, 0 }, { 1, 9 }, { 1, 217 }, { 4, 255 }, { 1, 201 }, { 73, 0 }, { 1, 141 }, { 5, 255 }, { 1, 72 }, { 61, 0 }, { 2, 5 }, { 9, 0 }, { 1, 58 }, { 1, 253 }, { 4, 255 }, { 1, 123 }, { 62, 0 }, { 1, 25 }, { 9, 0 }, { 1, 9 }, { 1, 217 }, { 4, 255 }, { 1, 162 }, { 62, 0 }, { 1, 24 }, { 1, 16 }, { 9, 0 }, { 1, 142 }, { 4, 255 }, { 1, 192 }, { 1, 6 }, { 61, 0 }, { 1, 8 }, { 1, 47 }, { 9, 0 }, { 1, 59 }, { 1, 253 }, { 3, 255 }, { 1, 216 }, { 1, 17 }, { 2, 0 }, { 1, 78 }, { 1, 131 }, { 1, 73 }, { 57, 0 }, { 1, 47 }, { 1, 23 }, { 8, 0 }, { 1, 9 }, { 1, 218 }, { 3, 255 }, { 1, 235 }, { 1, 35 }, { 1, 0 }, { 1, 10 }, { 1, 178 }, { 3, 255 }, { 1, 39 }, { 55, 0 }, { 1, 25 }, { 1, 53 }, { 1, 1 }, { 8, 0 }, { 1, 143 }, { 3, 255 }, { 1, 248 }, { 1, 58 }, { 1, 0 }, { 1, 18 }, { 1, 199 }, { 4, 255 }, { 1, 87 }, { 54, 0 }, { 1, 4 }, { 1, 58 }, { 1, 19 }, { 8, 0 }, { 1, 60 }, { 1, 253 }, { 3, 255 }, { 1, 88 }, { 1, 0 }, { 1, 28 }, { 1, 213 }, { 5, 255 }, { 1, 133 }, { 54, 0 }, { 1, 37 }, { 1, 47 }, { 8, 0 }, { 1, 10 }, { 1, 219 }, { 3, 255 }, { 1, 121 }, { 1, 0 }, { 1, 40 }, { 1, 225 }, { 6, 255 }, { 1, 180 }, { 1, 0 }, { 1, 18 }, { 1, 5 }, { 50, 0 }, { 1, 12 }, { 1, 61 }, { 1, 12 }, { 8, 0 }, { 1, 144 }, { 3, 255 }, { 1, 157 }, { 1, 0 }, { 1, 54 }, { 1, 235 }, { 7, 255 }, { 1, 227 }, { 1, 38 }, { 1, 6 }, { 51, 0 }, { 1, 49 }, { 1, 39 }, { 8, 0 }, { 1, 61 }, { 3, 255 }, { 1, 188 }, { 1, 5 }, { 1, 69 }, { 1, 243 }, { 9, 255 }, { 1, 20 }, { 51, 0 }, { 1, 21 }, { 1, 59 }, { 1, 6 }, { 7, 0 }, { 1, 10 }, { 1, 220 }, { 2, 255 }, { 1, 214 }, { 1, 16 }, { 1, 87 }, { 1, 249 }, { 10, 255 }, { 1, 63 }, { 16, 0 }, { 1, 11 }, { 1, 10 }, { 33, 0 }, { 1, 50 }, { 1, 23 }, { 8, 0 }, { 1, 145 }, { 2, 255 }, { 1, 233 }, { 1, 32 }, { 1, 107 }, { 1, 253 }, { 11, 255 }, { 1, 110 }, { 14, 0 }, { 1, 4 }, { 1, 34 }, { 1, 17 }, { 33, 0 }, { 1, 17 }, { 1, 44 }, { 8, 0 }, { 1, 62 }, { 2, 255 }, { 1, 246 }, { 1, 55 }, { 1, 128 }, { 13, 255 }, { 1, 157 }, { 13, 0 }, { 1, 27 }, { 1, 51 }, { 1, 11 }, { 34, 0 }, { 1, 42 }, { 1, 6 }, { 7, 0 }, { 1, 11 }, { 1, 221 }, { 2, 255 }, { 1, 124 }, { 1, 150 }, { 14, 255 }, { 1, 193 }, { 11, 0 }, { 1, 10 }, { 1, 49 }, { 1, 50 }, { 1, 6 }, { 34, 0 }, { 1, 13 }, { 1, 15 }, { 8, 0 }, { 1, 147 }, { 19, 255 }, { 1, 171 }, { 10, 0 }, { 1, 28 }, { 1, 60 }, { 1, 34 }, { 1, 1 }, { 35, 0 }, { 1, 11 }, { 8, 0 }, { 1, 63 }, { 19, 255 }, { 1, 232 }, { 1, 33 }, { 8, 0 }, { 1, 8 }, { 1, 47 }, { 1, 56 }, { 1, 16 }, { 45, 0 }, { 1, 11 }, { 1, 221 }, { 18, 255 }, { 1, 189 }, { 1, 29 }, { 8, 0 }, { 1, 22 }, { 1, 59 }, { 1, 43 }, { 1, 4 }, { 46, 0 }, { 1, 148 }, { 16, 255 }, { 1, 214 }, { 1, 139 }, { 1, 53 }, { 9, 0 }, { 1, 33 }, { 1, 55 }, { 1, 22 }, { 47, 0 }, { 1, 64 }, { 13, 255 }, { 1, 247 }, { 1, 182 }, { 1, 107 }, { 1, 33 }, { 10, 0 }, { 1, 2 }, { 1, 40 }, { 1, 30 }, { 1, 2 }, { 47, 0 }, { 1, 11 }, { 1, 222 }, { 10, 255 }, { 1, 226 }, { 1, 151 }, { 1, 76 }, { 1, 10 }, { 12, 0 }, { 1, 7 }, { 1, 26 }, { 1, 5 }, { 49, 0 }, { 1, 149 }, { 7, 255 }, { 1, 252 }, { 1, 195 }, { 1, 120 }, { 1, 45 }, { 16, 0 }, { 1, 5 }, { 50, 0 }, { 1, 61 }, { 5, 255 }, { 1, 236 }, { 1, 163 }, { 1, 89 }, { 1, 18 }, { 70, 0 }, { 1, 173 }, { 2, 255 }, { 1, 207 }, { 1, 132 }, { 1, 57 }, { 1, 2 }, { 73, 0 }, { 1, 55 }, { 1, 86 }, { 1, 27 }, { 2213, 0 } }, + LogoRLEFrame{ { 1804, 0 }, { 1, 34 }, { 1, 136 }, { 1, 20 }, { 76, 0 }, { 1, 26 }, { 1, 229 }, { 1, 255 }, { 1, 94 }, { 63, 0 }, { 1, 1 }, { 11, 0 }, { 1, 1 }, { 1, 185 }, { 2, 255 }, { 1, 116 }, { 63, 0 }, { 1, 14 }, { 11, 0 }, { 1, 108 }, { 3, 255 }, { 1, 124 }, { 62, 0 }, { 1, 23 }, { 1, 6 }, { 10, 0 }, { 1, 40 }, { 1, 246 }, { 3, 255 }, { 1, 104 }, { 61, 0 }, { 1, 8 }, { 1, 36 }, { 10, 0 }, { 1, 5 }, { 1, 204 }, { 4, 255 }, { 1, 67 }, { 61, 0 }, { 1, 48 }, { 1, 11 }, { 10, 0 }, { 1, 132 }, { 4, 255 }, { 1, 209 }, { 61, 0 }, { 1, 29 }, { 1, 46 }, { 10, 0 }, { 1, 58 }, { 1, 252 }, { 3, 255 }, { 1, 241 }, { 1, 44 }, { 60, 0 }, { 1, 7 }, { 1, 60 }, { 1, 13 }, { 9, 0 }, { 1, 12 }, { 1, 221 }, { 3, 255 }, { 1, 251 }, { 1, 71 }, { 61, 0 }, { 1, 42 }, { 1, 41 }, { 10, 0 }, { 1, 157 }, { 4, 255 }, { 1, 98 }, { 61, 0 }, { 1, 17 }, { 1, 60 }, { 1, 7 }, { 9, 0 }, { 1, 79 }, { 4, 255 }, { 1, 128 }, { 2, 0 }, { 1, 6 }, { 1, 38 }, { 1, 6 }, { 56, 0 }, { 1, 1 }, { 1, 53 }, { 1, 32 }, { 9, 0 }, { 1, 23 }, { 1, 234 }, { 3, 255 }, { 1, 158 }, { 2, 0 }, { 1, 85 }, { 1, 235 }, { 1, 255 }, { 1, 221 }, { 1, 4 }, { 5, 0 }, { 1, 9 }, { 1, 3 }, { 48, 0 }, { 1, 28 }, { 1, 57 }, { 1, 3 }, { 9, 0 }, { 1, 180 }, { 3, 255 }, { 1, 185 }, { 1, 4 }, { 1, 0 }, { 1, 114 }, { 4, 255 }, { 1, 35 }, { 3, 0 }, { 1, 4 }, { 1, 30 }, { 1, 10 }, { 48, 0 }, { 1, 2 }, { 1, 58 }, { 1, 22 }, { 9, 0 }, { 1, 104 }, { 3, 255 }, { 1, 207 }, { 1, 13 }, { 1, 0 }, { 1, 136 }, { 5, 255 }, { 1, 71 }, { 2, 0 }, { 1, 23 }, { 1, 47 }, { 1, 7 }, { 17, 0 }, { 1, 1 }, { 1, 2 }, { 30, 0 }, { 1, 27 }, { 1, 43 }, { 9, 0 }, { 1, 37 }, { 1, 245 }, { 2, 255 }, { 1, 225 }, { 1, 25 }, { 1, 3 }, { 1, 157 }, { 6, 255 }, { 1, 107 }, { 1, 8 }, { 1, 47 }, { 1, 48 }, { 1, 4 }, { 17, 0 }, { 2, 9 }, { 30, 0 }, { 1, 1 }, { 1, 51 }, { 1, 5 }, { 8, 0 }, { 1, 4 }, { 1, 201 }, { 2, 255 }, { 1, 239 }, { 1, 42 }, { 1, 8 }, { 1, 176 }, { 7, 255 }, { 1, 152 }, { 1, 59 }, { 1, 36 }, { 1, 1 }, { 16, 0 }, { 1, 5 }, { 1, 21 }, { 1, 9 }, { 31, 0 }, { 1, 24 }, { 1, 18 }, { 9, 0 }, { 1, 128 }, { 2, 255 }, { 1, 249 }, { 1, 64 }, { 1, 15 }, { 1, 193 }, { 8, 255 }, { 1, 195 }, { 1, 20 }, { 16, 0 }, { 1, 1 }, { 1, 17 }, { 1, 27 }, { 1, 5 }, { 32, 0 }, { 1, 23 }, { 9, 0 }, { 1, 55 }, { 1, 251 }, { 2, 255 }, { 1, 89 }, { 1, 24 }, { 1, 208 }, { 9, 255 }, { 1, 215 }, { 16, 0 }, { 1, 7 }, { 1, 26 }, { 1, 23 }, { 1, 3 }, { 32, 0 }, { 1, 4 }, { 9, 0 }, { 1, 11 }, { 1, 218 }, { 2, 255 }, { 1, 118 }, { 1, 35 }, { 1, 221 }, { 10, 255 }, { 1, 248 }, { 1, 2 }, { 14, 0 }, { 1, 16 }, { 1, 30 }, { 1, 14 }, { 44, 0 }, { 1, 152 }, { 2, 255 }, { 1, 149 }, { 1, 48 }, { 1, 232 }, { 12, 255 }, { 1, 29 }, { 12, 0 }, { 1, 6 }, { 1, 25 }, { 1, 26 }, { 1, 6 }, { 44, 0 }, { 1, 76 }, { 2, 255 }, { 1, 178 }, { 1, 66 }, { 1, 241 }, { 13, 255 }, { 1, 62 }, { 11, 0 }, { 1, 11 }, { 1, 30 }, { 1, 19 }, { 1, 1 }, { 44, 0 }, { 1, 21 }, { 1, 232 }, { 2, 255 }, { 1, 185 }, { 1, 248 }, { 14, 255 }, { 1, 59 }, { 10, 0 }, { 1, 16 }, { 1, 23 }, { 1, 6 }, { 46, 0 }, { 1, 176 }, { 18, 255 }, { 1, 184 }, { 9, 0 }, { 1, 1 }, { 1, 17 }, { 1, 10 }, { 47, 0 }, { 1, 99 }, { 18, 255 }, { 1, 168 }, { 1, 10 }, { 8, 0 }, { 1, 1 }, { 1, 8 }, { 48, 0 }, { 1, 34 }, { 1, 243 }, { 15, 255 }, { 1, 242 }, { 1, 169 }, { 1, 59 }, { 59, 0 }, { 1, 3 }, { 1, 198 }, { 13, 255 }, { 1, 227 }, { 1, 149 }, { 1, 71 }, { 1, 6 }, { 61, 0 }, { 1, 123 }, { 11, 255 }, { 1, 206 }, { 1, 128 }, { 1, 50 }, { 64, 0 }, { 1, 51 }, { 1, 250 }, { 7, 255 }, { 1, 250 }, { 1, 185 }, { 1, 107 }, { 1, 30 }, { 66, 0 }, { 1, 9 }, { 1, 215 }, { 5, 255 }, { 1, 238 }, { 1, 164 }, { 1, 86 }, { 1, 14 }, { 69, 0 }, { 1, 131 }, { 3, 255 }, { 1, 222 }, { 1, 143 }, { 1, 65 }, { 1, 4 }, { 72, 0 }, { 1, 155 }, { 1, 191 }, { 1, 122 }, { 1, 44 }, { 2212, 0 } }, + LogoRLEFrame{ { 1633, 0 }, { 1, 4 }, { 78, 0 }, { 1, 18 }, { 78, 0 }, { 1, 9 }, { 1, 25 }, { 78, 0 }, { 1, 45 }, { 1, 3 }, { 10, 0 }, { 1, 9 }, { 1, 171 }, { 1, 197 }, { 64, 0 }, { 1, 31 }, { 1, 34 }, { 11, 0 }, { 1, 167 }, { 1, 255 }, { 1, 248 }, { 63, 0 }, { 1, 11 }, { 1, 60 }, { 1, 7 }, { 10, 0 }, { 1, 96 }, { 3, 255 }, { 1, 3 }, { 62, 0 }, { 1, 47 }, { 1, 34 }, { 10, 0 }, { 1, 37 }, { 1, 243 }, { 2, 255 }, { 1, 240 }, { 62, 0 }, { 1, 22 }, { 1, 57 }, { 1, 3 }, { 9, 0 }, { 1, 5 }, { 1, 203 }, { 3, 255 }, { 1, 211 }, { 61, 0 }, { 1, 3 }, { 1, 56 }, { 1, 26 }, { 10, 0 }, { 1, 136 }, { 4, 255 }, { 1, 115 }, { 61, 0 }, { 1, 34 }, { 1, 52 }, { 10, 0 }, { 1, 66 }, { 1, 253 }, { 3, 255 }, { 1, 208 }, { 1, 9 }, { 60, 0 }, { 1, 9 }, { 1, 61 }, { 1, 18 }, { 9, 0 }, { 1, 19 }, { 1, 229 }, { 3, 255 }, { 1, 225 }, { 1, 26 }, { 13, 0 }, { 1, 2 }, { 47, 0 }, { 1, 38 }, { 1, 41 }, { 10, 0 }, { 1, 176 }, { 3, 255 }, { 1, 238 }, { 1, 41 }, { 13, 0 }, { 1, 19 }, { 1, 12 }, { 46, 0 }, { 1, 6 }, { 1, 55 }, { 1, 4 }, { 9, 0 }, { 1, 104 }, { 3, 255 }, { 1, 247 }, { 1, 59 }, { 12, 0 }, { 1, 10 }, { 1, 41 }, { 1, 13 }, { 47, 0 }, { 1, 34 }, { 1, 19 }, { 9, 0 }, { 1, 42 }, { 1, 246 }, { 2, 255 }, { 1, 253 }, { 1, 80 }, { 1, 0 }, { 1, 10 }, { 1, 136 }, { 1, 221 }, { 1, 194 }, { 1, 24 }, { 5, 0 }, { 1, 2 }, { 1, 33 }, { 1, 52 }, { 1, 8 }, { 47, 0 }, { 1, 4 }, { 1, 32 }, { 9, 0 }, { 1, 7 }, { 1, 209 }, { 3, 255 }, { 1, 104 }, { 1, 0 }, { 1, 19 }, { 1, 200 }, { 3, 255 }, { 1, 85 }, { 4, 0 }, { 1, 12 }, { 1, 52 }, { 1, 47 }, { 1, 5 }, { 48, 0 }, { 1, 17 }, { 1, 1 }, { 9, 0 }, { 1, 144 }, { 3, 255 }, { 1, 131 }, { 1, 0 }, { 1, 29 }, { 1, 214 }, { 4, 255 }, { 1, 113 }, { 3, 0 }, { 1, 29 }, { 1, 60 }, { 1, 32 }, { 50, 0 }, { 1, 2 }, { 9, 0 }, { 1, 74 }, { 3, 255 }, { 1, 157 }, { 1, 0 }, { 1, 41 }, { 1, 226 }, { 5, 255 }, { 1, 142 }, { 1, 0 }, { 1, 7 }, { 1, 47 }, { 1, 57 }, { 1, 16 }, { 60, 0 }, { 1, 23 }, { 1, 233 }, { 2, 255 }, { 1, 181 }, { 1, 4 }, { 1, 55 }, { 1, 236 }, { 6, 255 }, { 1, 171 }, { 1, 16 }, { 1, 58 }, { 1, 46 }, { 1, 5 }, { 60, 0 }, { 1, 1 }, { 1, 184 }, { 2, 255 }, { 1, 201 }, { 1, 10 }, { 1, 71 }, { 1, 244 }, { 7, 255 }, { 1, 204 }, { 1, 56 }, { 1, 24 }, { 62, 0 }, { 1, 113 }, { 2, 255 }, { 1, 218 }, { 1, 20 }, { 1, 90 }, { 1, 250 }, { 8, 255 }, { 1, 231 }, { 1, 3 }, { 62, 0 }, { 1, 48 }, { 1, 249 }, { 1, 255 }, { 1, 232 }, { 1, 33 }, { 1, 110 }, { 10, 255 }, { 1, 253 }, { 1, 4 }, { 61, 0 }, { 1, 10 }, { 1, 215 }, { 1, 255 }, { 1, 243 }, { 1, 50 }, { 1, 132 }, { 12, 255 }, { 1, 29 }, { 61, 0 }, { 1, 153 }, { 1, 255 }, { 1, 250 }, { 1, 72 }, { 1, 154 }, { 13, 255 }, { 1, 58 }, { 60, 0 }, { 1, 82 }, { 2, 255 }, { 2, 175 }, { 14, 255 }, { 1, 64 }, { 59, 0 }, { 1, 28 }, { 1, 237 }, { 17, 255 }, { 1, 209 }, { 1, 5 }, { 58, 0 }, { 1, 2 }, { 1, 191 }, { 17, 255 }, { 1, 204 }, { 1, 28 }, { 59, 0 }, { 1, 121 }, { 16, 255 }, { 1, 219 }, { 1, 106 }, { 1, 7 }, { 59, 0 }, { 1, 54 }, { 1, 251 }, { 13, 255 }, { 1, 205 }, { 1, 125 }, { 1, 45 }, { 61, 0 }, { 1, 13 }, { 1, 220 }, { 10, 255 }, { 1, 252 }, { 1, 190 }, { 1, 110 }, { 1, 30 }, { 64, 0 }, { 1, 162 }, { 8, 255 }, { 1, 246 }, { 1, 175 }, { 1, 95 }, { 1, 19 }, { 66, 0 }, { 1, 90 }, { 6, 255 }, { 1, 237 }, { 1, 160 }, { 1, 80 }, { 1, 10 }, { 68, 0 }, { 1, 27 }, { 1, 241 }, { 3, 255 }, { 1, 225 }, { 1, 145 }, { 1, 65 }, { 1, 4 }, { 71, 0 }, { 1, 126 }, { 1, 255 }, { 1, 211 }, { 1, 130 }, { 1, 50 }, { 75, 0 }, { 1, 7 }, { 1, 19 }, { 2135, 0 } }, + LogoRLEFrame{ { 1315, 0 }, { 1, 8 }, { 78, 0 }, { 1, 10 }, { 1, 13 }, { 77, 0 }, { 1, 1 }, { 1, 38 }, { 78, 0 }, { 1, 32 }, { 1, 22 }, { 77, 0 }, { 1, 13 }, { 1, 55 }, { 1, 1 }, { 77, 0 }, { 1, 52 }, { 1, 27 }, { 77, 0 }, { 1, 28 }, { 1, 53 }, { 1, 1 }, { 76, 0 }, { 1, 5 }, { 1, 59 }, { 1, 19 }, { 9, 0 }, { 1, 4 }, { 1, 141 }, { 1, 147 }, { 65, 0 }, { 1, 39 }, { 1, 47 }, { 10, 0 }, { 1, 153 }, { 1, 255 }, { 1, 224 }, { 64, 0 }, { 1, 14 }, { 1, 62 }, { 1, 12 }, { 9, 0 }, { 1, 86 }, { 2, 255 }, { 1, 232 }, { 64, 0 }, { 1, 48 }, { 1, 38 }, { 9, 0 }, { 1, 32 }, { 1, 240 }, { 2, 255 }, { 1, 212 }, { 63, 0 }, { 1, 15 }, { 1, 56 }, { 1, 3 }, { 8, 0 }, { 1, 4 }, { 1, 199 }, { 3, 255 }, { 1, 179 }, { 63, 0 }, { 1, 45 }, { 1, 17 }, { 9, 0 }, { 1, 135 }, { 4, 255 }, { 1, 83 }, { 16, 0 }, { 2, 9 }, { 44, 0 }, { 1, 12 }, { 1, 37 }, { 9, 0 }, { 1, 67 }, { 1, 253 }, { 3, 255 }, { 1, 182 }, { 15, 0 }, { 1, 2 }, { 1, 31 }, { 1, 18 }, { 45, 0 }, { 1, 29 }, { 1, 1 }, { 8, 0 }, { 1, 21 }, { 1, 230 }, { 3, 255 }, { 1, 201 }, { 1, 11 }, { 14, 0 }, { 1, 20 }, { 1, 51 }, { 1, 14 }, { 45, 0 }, { 1, 5 }, { 1, 7 }, { 8, 0 }, { 1, 1 }, { 1, 182 }, { 3, 255 }, { 1, 217 }, { 1, 20 }, { 13, 0 }, { 1, 4 }, { 1, 43 }, { 1, 55 }, { 1, 10 }, { 56, 0 }, { 1, 114 }, { 3, 255 }, { 1, 230 }, { 1, 32 }, { 13, 0 }, { 1, 17 }, { 1, 56 }, { 1, 43 }, { 1, 4 }, { 56, 0 }, { 1, 51 }, { 1, 249 }, { 2, 255 }, { 1, 241 }, { 1, 47 }, { 1, 0 }, { 1, 27 }, { 1, 167 }, { 1, 230 }, { 1, 169 }, { 1, 2 }, { 6, 0 }, { 1, 1 }, { 1, 35 }, { 1, 61 }, { 1, 27 }, { 57, 0 }, { 1, 12 }, { 1, 218 }, { 2, 255 }, { 1, 248 }, { 1, 64 }, { 1, 0 }, { 1, 46 }, { 1, 230 }, { 3, 255 }, { 1, 23 }, { 5, 0 }, { 1, 10 }, { 1, 52 }, { 1, 54 }, { 1, 12 }, { 58, 0 }, { 1, 162 }, { 2, 255 }, { 1, 253 }, { 1, 85 }, { 1, 0 }, { 1, 62 }, { 1, 240 }, { 4, 255 }, { 1, 48 }, { 4, 0 }, { 1, 16 }, { 1, 58 }, { 1, 38 }, { 1, 3 }, { 58, 0 }, { 1, 93 }, { 3, 255 }, { 1, 108 }, { 1, 0 }, { 1, 79 }, { 1, 247 }, { 5, 255 }, { 1, 73 }, { 3, 0 }, { 1, 24 }, { 1, 47 }, { 1, 12 }, { 59, 0 }, { 1, 36 }, { 1, 243 }, { 2, 255 }, { 1, 133 }, { 1, 0 }, { 1, 100 }, { 1, 252 }, { 6, 255 }, { 1, 98 }, { 2, 0 }, { 1, 26 }, { 1, 22 }, { 60, 0 }, { 1, 6 }, { 1, 204 }, { 2, 255 }, { 1, 158 }, { 1, 0 }, { 1, 123 }, { 8, 255 }, { 1, 123 }, { 1, 0 }, { 1, 11 }, { 1, 2 }, { 61, 0 }, { 1, 141 }, { 2, 255 }, { 1, 180 }, { 1, 6 }, { 1, 146 }, { 9, 255 }, { 1, 148 }, { 63, 0 }, { 1, 73 }, { 2, 255 }, { 1, 199 }, { 1, 16 }, { 1, 167 }, { 10, 255 }, { 1, 174 }, { 62, 0 }, { 1, 24 }, { 1, 234 }, { 1, 255 }, { 1, 215 }, { 1, 31 }, { 1, 186 }, { 11, 255 }, { 1, 199 }, { 61, 0 }, { 1, 1 }, { 1, 187 }, { 1, 255 }, { 1, 229 }, { 1, 52 }, { 1, 203 }, { 12, 255 }, { 1, 223 }, { 61, 0 }, { 1, 120 }, { 2, 255 }, { 1, 138 }, { 1, 217 }, { 13, 255 }, { 1, 225 }, { 60, 0 }, { 1, 56 }, { 1, 251 }, { 17, 255 }, { 1, 111 }, { 59, 0 }, { 1, 15 }, { 1, 222 }, { 16, 255 }, { 1, 253 }, { 1, 122 }, { 60, 0 }, { 1, 168 }, { 15, 255 }, { 1, 247 }, { 1, 168 }, { 1, 47 }, { 60, 0 }, { 1, 99 }, { 13, 255 }, { 1, 240 }, { 1, 164 }, { 1, 83 }, { 1, 11 }, { 61, 0 }, { 1, 40 }, { 1, 245 }, { 10, 255 }, { 1, 230 }, { 1, 151 }, { 1, 70 }, { 1, 5 }, { 63, 0 }, { 1, 8 }, { 1, 209 }, { 8, 255 }, { 1, 219 }, { 1, 138 }, { 1, 57 }, { 1, 1 }, { 66, 0 }, { 1, 147 }, { 6, 255 }, { 1, 206 }, { 1, 125 }, { 1, 44 }, { 69, 0 }, { 1, 73 }, { 3, 255 }, { 1, 252 }, { 1, 193 }, { 1, 112 }, { 1, 32 }, { 72, 0 }, { 1, 190 }, { 1, 247 }, { 1, 180 }, { 1, 99 }, { 1, 21 }, { 75, 0 }, { 1, 13 }, { 1, 5 }, { 664, 0 }, { 1, 7 }, { 1, 1 }, { 76, 0 }, { 1, 11 }, { 1, 16 }, { 1, 1 }, { 75, 0 }, { 1, 10 }, { 1, 25 }, { 1, 14 }, { 75, 0 }, { 1, 5 }, { 1, 23 }, { 1, 27 }, { 1, 7 }, { 74, 0 }, { 1, 1 }, { 1, 16 }, { 1, 30 }, { 1, 18 }, { 1, 1 }, { 74, 0 }, { 1, 9 }, { 1, 27 }, { 1, 25 }, { 1, 7 }, { 74, 0 }, { 1, 1 }, { 1, 19 }, { 1, 30 }, { 1, 15 }, { 75, 0 }, { 1, 5 }, { 1, 25 }, { 1, 17 }, { 1, 3 }, { 75, 0 }, { 1, 11 }, { 1, 16 }, { 1, 3 }, { 76, 0 }, { 1, 6 }, { 1, 2 }, { 764, 0 } }, + +} }; + +} // namespace RoundVideoData diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 2c0d2a7e3f..c6de18b209 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -379,6 +379,7 @@ PRIVATE ui/controls/invite_link_label.h ui/controls/peer_list_dummy.cpp ui/controls/peer_list_dummy.h + ui/controls/round_video_recorder_data.h ui/controls/round_video_recorder.cpp ui/controls/round_video_recorder.h ui/controls/send_as_button.cpp From 8b2a728a0d9846e3a923f82f137d1903ee15f8ea Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 3 Jun 2025 18:28:11 +0300 Subject: [PATCH 048/310] Fixed display of loading animation from search in Saved Messages. --- Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 9eba2eaddc..4c2775aeeb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -3382,6 +3382,9 @@ void InnerWidget::applySearchState(SearchState state) { ) | rpl::start_with_next([=] { refresh(); moveSearchIn(); + if (_loadingAnimation) { + _loadingAnimation->move(0, searchedOffset()); + } }, _searchTags->lifetime()); } else { _searchTags = nullptr; From 79f0b22276665a5e851bdbf86dfb8f005feddded Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 3 Jun 2025 18:30:38 +0300 Subject: [PATCH 049/310] Compressed lottie for media forbidden. --- .../Resources/animations/media_forbidden.tgs | Bin 86124 -> 8962 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Telegram/Resources/animations/media_forbidden.tgs b/Telegram/Resources/animations/media_forbidden.tgs index b1846cd5db0d2ba1b75c6d00764ec6fd71dc04ac..34e9808eea86d0f64f976d6160731521509b97a2 100644 GIT binary patch literal 8962 zcmV+dBmLYTiwFqq6hCMH18rqwX<=VxZ*pR3WMpM-E_7#e0PS5_j~q9W{wqd5XL5NT zew+bzW&y)qj5SX}Ly)burL|-UYRzE`^xrqWU|y9+Rb?fsRjn==?(V$EV2}}vU|j#X z`Sk1k&9|z$`RC2IH%>UAZ>pPLKHhxmWp(rB!_Bw!>plHak^cDC&9_eJ>gILx`}cSB z(F^I+i@*H!$7g@~@!5+X{{D=S9w=e&= z`KpUjOj&fAi}{YV^&g4A}3ve|~U}-g4V7b3geV-1s-W zCcF>T4ZW$T@%P=`d{5H7PZ@Fd?Pt%Q%c_>b>3CZ`4?@!ClKzrad#`S9-g1M_=N?sa zjok8-J%4VD2sGZBhMkhZXGVDEEBe|S#n*v8)m5E@mv-<%63#lb^YhKOpFaNM?(^;E zYGqahJE8b zX0&l}ZXd^JX$)FT7!Ov48LiLu-pu-(JgVjzX~#gIhHj+HN-`54-maVbJKz$)c`N_l zm!DpLqiA=1P224+FW=tZ@lbfv{dB{h>~7^YNtL_|^ox2RxSUQ_sib`xU#i=7zPkpL z#BBd-bdPSmrH%-9Z~u>f{q&9>GK4>V`uP60y9F43d3!Dxvph2d?|1J~mlgegoBJI+kqgk>|2BE;{^jHrgZ=E|%ir&RdH?a<&F9eeJ6U5QdPc~Jx zwnBw;NfINAOvKuW$_X|bI=Xe4F<26SGLz~klm}xfFMNdTMUsuSkcd)H4MjG1-zMYr zut`&qx?ocWnvB44k5#NVo$y4icQO&K&{Wdim=l=mJDe0xoE^0(B<{Obv9} zTY}E-&Z{N4&WY&M7&_v~P7%M>t0wj&A?J(3cTg~42nqKA^DC_#KdhG@DrB(oDOx#4?CB)uE|7bsJX||9AfV`RmyjyAi#+ z>7ktLE6;gjx#hL^yvOgq+f$I{2@hzn!?>L#zOs?fEIPTRk!TA*l6(P3l#B&HQCgOO z#pn{S*b>N9ZV9`wd-sS|d=c!%5(`(QrIov8L|QIkcexeSLD{vU{OS?Sw4!&gi#xW* z6kfV3%sDc1+{`=DripBs$3|dU_OT6^j(==~rsE(R!D(5@R&Z9KgiAU?|rcqTonZKDjl9Z$N zRq&($X-MEVpCEFRfSE7ebQdF?`>0+LDfzyg+$a<&6x5Sep>$7LI=a=4Nq2)%w)mhl z4Wt_s#$=D}Xt*55c>g>j<?Jv;am;&34mh|a0 z8^M&6+Y_6b7yPDuQkQC}I|_l#&6{oe+}@^E?JcgyxlWS-9?yllV}LX^SBvqO>wTu0TK59a^Gd#>_dIr@k za+2soq6-rR2~+s&HFvEKS{PN6bjs4yZ@Gu{utYFD{V#A<05aWp`+0M?o_uW5S> zlnDZusEvqTFY$6UkL}OHqnQQOr5gNfDRk$MsU!Dl@Ux}Z28xktQ}yRR=j$#A*L0|4 zmCoKDm_s9~QeUR;4~u=&#m_zAO4DUBDSr&306sR|foV<4n%%N;ymM!_TR)a620ktI*1=@^sTc|e=?CywFmAxPo&q>Y=V8=vL&jfxgqF? zpXz3<8`|b+eP)z7f(}a&yc3I03rV{!^`~t?pQe4(2*HE)qZ)n+4TO%!oknCCFEn@$ z#L(O}o)3HJQSy?pw72S`$&rvO9U2Uora=F+hHT>~W?_0Kx(kQt5!`^3EZ9G?*=8+4 zMiSfd#L}>duMij+M(bdbsc5Z-yjV2@P0T9Oqh)%8`6uORrFMYv5KH=NVYn8}YIFXn zq-N-fSc^HHvu>D5V3w!SFtfm#)gaH{!&zsNCek|0oMv}C5`B0k!K#1+L&)Ww-0zvl z6D+mYseDXRdBCW>ax!UmpqT)^Lf%&*?e09AA*%V=bfKAT#ChtlC z24Y$eL5E|QEPQJe_1Ey&WE{jd+U)!3q$V&|iVt>Xr;}cZP%3CXpkQa8IimqpQuz%M z{c^z^get$Y%8z^{=hoh2(&3c(QBTPGC+d6WakhPjuXySlpoBeq9k4!hE^Fi0-E6Ld2b-Ys!9_8JZFkUJ{{SOxq^ecB{`drX+t9#dD{C(43^Uw zopFSr?vx$x3L?2iMGIn_nA=x@WZcDlq z7+L2S_?fZKHlvAu&&`t&GC<8>KQt0Hp@C+y#|~|(z)y%J7*G{W9B-o3oM+cR%>)ZQ zNU_SH`h5{3pI^#4)pUY@Z*1)YR`+3lntmdn5K|bBClwO2&e!IiMk^V z1b$!`3Vn{i3dOLMa{P@-1y6_#iXfF(IHdO}(r7)@5ZPRYpuU*nFII#h5T%PrT3BR= zG8Cb}>wsX3tFP}iUthOteEF{zKfToh>r2`n1y)B=%dt%mg*pGQZoPXB%y(S%%J=&p;Ag7DuP2#RY42U#I!VrB9zsOL=+iu8IxB34Kx$0IQx1O(V^hq2t*3E8`1J=6PRkG); zo82V`+;y|d(^`rk~Pb%-U-aNZr)Q!a?Zq(NcBME0>lJ-1sV`4a~=E{Ej3v znW{GM%qw@?zPn7T$x{C67A}%jIbv&s$7u9ruUuVZ7f7RBU>Gx`52zr4Jv`E7?*P@_ zwO-9JY}(XBPHZr7T>=W|L$$^Vcx>VP8jw5`d<6Qz>7 zRC)Ku-R|LlQU^#AeoBe`lpM!jJ|J<9B7)*59aGKEdYH$1f2@Hb#kmA%h4pm{7KKL4O&Ljm#JdVOh>`Y`i{DUr) z-n8ictu3EaE$rHlco=4$L!*%u8VLbkliE>R|1*yb%x)0o~RC7pk4~y zIKJX>`!qk~XTw!7|3znYKq38sJEp4=W>xQ9p8`2gy zLV>Zw0%Pg2@eSG-5?u%1Rgs-jWaZI%teWVtN7Tkxe+=|_oDU;LN8AjC<>Fb6+89+w zsx_kFZA9o#@ovo4ZssHV#@)1$eZdIlGtOLElSOGw`i+&HjIwU*%;|%s-AKKo%|oQ! z=$2S~nC{HM?jnEQ{oK}FyX5v@lfSM8`HW_IQat$X=Uh+G&-XN3SYIrk$o^w8WL@U^ z1g1G{Mvxwoc;oCq1>i-d6uv|5!5 z*8^t}ieYfVh`4)BT&OwE0Pv5L1~%tbLw!8UDYsNk#}rZY1Ht1*ovM9hG;p#5&T&3B zTLX|{i8!oQE{vNRDH8A4iXa=KChh!yj&Gse>5hhD@my})!tpS~HKSNOkBbvRZ8ZIo zHPEguDkV<#M%B&k{zE4nO1^Xuo%SnWG zTqS8ZL;~6gn=N0O{cemFTD56L=Bt?<1L1r{GoEnH~`0|2_pdIARSJ=3Eyxv29T2kA*TgPKr&wy0&un}*9Bp4;;3?Y5RNtA zi<{($Q^)1uaO`byc@Q4pH5V|;2Wa^W5Y9OaoO$NXtZSUdzyFEIX6BIh@RH@V_a64p z)FT%?y&&h|dgP}v@`Bm*aH;3KVD`LH?Qgy%kzFU49Uqu_#|frBpYt$0w-RSV461kC zl1-w={IGpi_kLZ$4e+r1PPMoF77xlGIXmLdlh2s|b>uXOF1BYM)Pz=o4|{SkcqIN# zrG1B=?4p?1K%Oij%!ExmVe*(*KA!Bd7+XA^EFz}c=u@VT>HZNVdpc)%7|`;iVT((C z+CHrF)BLc<#^Nj0{^keRK$2lNQd>p_vP>g=a!y;U1r1CkLPD;34Ob}CAxXzsIlhi{ zfAt-{`VMbwq8=RRCJ z)J%YXt`pjv$$fZbCqpmK$+SKR|KW$~&m8XWEFQ$m4$Fldh~uO4AKvvm8Tqx;J64EQDGV*vlb) zLoqlyKCSmn7AIj->IG@)0@*c4NeS!}7KX#D(~fmx|||042v&__A&)^De{=RVT4G>73(*xxLFrSV)_EJMq1e#M%vN zH?q$cZAezbLKgKnQGF>NV%kk)thgRX2Vvw&BdF78C+=a9c5Yn(@sMEhRh=)l0yyv2 zcR>Ko`w(XU@p2jiD3K9p44@_HLsu4gfSLlId`&X-j#Nf?v z3WqbvY+X}N^mRaFhuQVrTw^$hh6c5%n{n8Hh? zDyp`eV`GKFmUnEcnb>(PZUd%e9~@F(^t6EVd)UR_X2+N$~ zsvtZy1z{sN?i9)O0ThId`bsK;<$VgmN$g~!zH&+WL7eIb1LZ)Em4jAZo8xp#7+B(R z$ZZq5fVj|ar+9BqfYw|L&`tzU*8#Gh8c$k)fi3Hp2%r`UC<<^Cv|gy_+5n)DbgR7Z z!BvTr7hk4m;;d#QNy*WPT@>k(;EM3@^CEcHcsQnra_dGozM%h;4YS*Nz zML=nCWR$RQC#SCyZrC=lfJ6N@e67t)jr4v@^P#V!sMMvLoTG7Rp}omBLhs{GYbu~T zFL73-4~z?Y9h~Ojt<5g~mIjD~5}`@HT^kFBdH8#eE3&U2R+{zvc=W?k^0;M48z>?& zBJFd79N!kH*pZ6QyVb*Tc~n}q!%{L2D+;}RSn9Z8$vmt8Iik$4i04?0%e3P{o_Jeo zR0~NcANgdNuX9-TxM6Kxg9aGElCwx<0FHr}{sfmYqqBcnBz>1&vm*n*NyBmrSv(*4 zZ29AcRerV%F?N`p=ymDVOxH1^I&wGu(v!t!7spM&m)&7+ugcn0Svyf#({ie;?V70d zq2d4}@GF`aB21SqOmhjOp=d6+GxRjohMA@2z}_Jh1`!z&O+I*bJS7U)$o1x# z-bw&*@FY#hY7xiK=Igq-aUO-@;eb3{Iq<~Gbmh3(Lkna?O{63=^Uoa!sntM?IW@JN zH0K%OM9SN7xFF_p#4_!m#OC|>vLu|-oJ?;L2cQhW6agcxK_HPf0wf2gcFf*jg)LJ1 zgJAJ=>HC5vd0viwut`2$CcYpX&P%r!g~NHSeMTHE3!P`>!l#Omh1{;LUQgGsh&i`~ ztJl-K!@@NzV%}BZ8WwRf=eT-3JvFbVYgoiJEaHo}J>f*@OF0O@{w@hSXY_kgS__5r z#gWvNb$(Cq#0e4M9;93Zzu+YRCkHqfUD#op7RhA~r0jrX=teNe-4>(n<4BMXIwC2W z=0el?tM;Rz24j;5mb4oagiEP$X$mOo9YxZ38}TGo@yB{^;m(uK=Z+kci9$((m~^T9 z-a*klrC}4$G$_t9=q&4p$a}{rFV7?z8iNzVn; zC#iH0eS$(%te_2$;Bh&xBeY`2RuwZ=p{K*rI1x_l+!r_tV#&#xKR7&YRG#a-gE5>i zDr?#kigOHV?x5_Vr)q z9^o>swl4?7CegZd#4m<<`H;gETfnr|b|Q9YP~wo7Ha*Lib#wjLS41TGq>&p#Tj*u4 z2YsgXfb15W&0^qdXoU?hn9?&haoAI|Ke7w8l$@uLFYD&|k*^1Dgf+B#Ci8WMZHy3z1EWUQNQ(F4}H z*&f~V*3CBQ0e9VOmmcuft+wd_gWU?&ki~Acbq|>A)+`?#m)&Zq9`f0(*6Jar-I_N< z7oox(X1f)nA-hezi_etbma)u(%^rE+W2Of_X3c>QvT|6}9HZLvB@mBDv(cZX#+-FS zjWaMng%#CDeG0pRD(#pm@AC{TlI8ArjQKo4oyw;hv?6_>dQ7u~P-n{Qm-+&w z*1TeOR%-ia)jT^Rxq>idE>Xx-myJ$V_@r^f<89ArP}5Im(@S$Glt1A^MR@##$=H;m zCK5-}M$|{-AcftbRUHC`1zCF^-3;IXQB+E~D6~ zv*R~tzm9^CwTOX-pzP34w#t-EzuvvYn;UJjq&-Ofr&Q7hgoLMUj$Oe{Mg@L;djIN= zQMC4<0zafVN>c^J$pkLTffOn85+_K|a**+XxQ%nLy(+G$Jlt8Oa0vDDA_kM$R7@s& zjP7oiJKEX?e^3-Csv?Km4d8PjP6ZkZU{KfDN`T#5*7!K!32QH8jWGm8z z0+-!HPBdDww4>}z1a^AV^MxSJ&N}N}2ykvI=CmQIj!;Z|!YIn#0%=Y}M07iiYx*3B}uVs@w(Qm5IZ<7BCnYC1i;al`UYf?U??AP~JTW z$ER_3M_5x&;T#(cIVTxqm$^rwSl-Rn*tp&bw1zdD!6KX>WX#N^Imk-cL#pjlUCJ(r_yY{+%O4;+;c#B#j4+ydPS~e7eq4o17u`Q3QZ`kw4$gB>l2Q) zJ)bPmQVpoi5)pf}H)HNC|oE=xGwxP(ybu>0Kob(*1%P|g#j9`sP1e%*1 zth2%Ja4dpAbLg%psH3A}9TDT|ARLIqEBWz1U}o+>sL%o~?MCnnR0r){gDlfZOh zU=mHmaj{Tz00ns}m@8Z^>3q_{fL}z~qvCCF0N>}bc8&Od1o6Dri2rNE|7b;ABmS=u|6fzY|L#7=$LRaE zIU}droLaNZUE`c?uW?QfMxI}T?0J|b)tDxE0WOl{HOA>0<8+O2`r=eOP7!yi6 zIb>7hzd!F-fP8PC>g~C=Q(i#ZK*Tyj1WriF7`y+^XK|YWhfnw4A{i&|MSfz~)C&zR*=T>q?behBc z6&;_AhtOGccy7)}e#YT>KRY~jyqF`D(lXFB$n+XydJQst#PZxNUU>WRkGqeV6YL0g z4HdNbdLf;9@t42;`0P(VKBKNa-u?1F^wQ1G@85lRA;<2X6X#A};s05SK>w`gPPBfU cB4}SXzXyZJ?!h#Q+7HI@|Gd7Ua%@lm0ARqB{r~^~ literal 86124 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 Date: Mon, 19 May 2025 16:35:51 +0400 Subject: [PATCH 050/310] Fix dropping filter in gifts. --- .../info/peer_gifts/info_peer_gifts_widget.cpp | 10 ++++------ .../info/peer_gifts/info_peer_gifts_widget.h | 8 ++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index b3454d1e17..47981cdf73 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -368,7 +368,9 @@ void InnerWidget::loadMore() { } else { _allLoaded = true; } - _totalCount = data.vcount().v; + if (!filter.skipsSomething()) { + _totalCount = data.vcount().v; + } const auto owner = &_peer->owner(); owner->processUsers(data.vusers()); @@ -583,11 +585,7 @@ void InnerWidget::refreshAbout() { const auto filter = _filter.current(); const auto filteredEmpty = _allLoaded && _entries.empty() - && (filter.skipLimited - || filter.skipUnlimited - || filter.skipSaved - || filter.skipUnsaved - || filter.skipUnique); + && filter.skipsSomething(); if (filteredEmpty) { auto text = tr::lng_peer_gifts_empty_search( diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h index 2624658016..2ef1fb67cd 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h @@ -34,6 +34,14 @@ struct Filter { bool skipSaved : 1 = false; bool skipUnsaved : 1 = false; + [[nodiscard]] bool skipsSomething() const { + return skipLimited + || skipUnlimited + || skipSaved + || skipUnsaved + || skipUnique; + } + friend inline bool operator==(Filter, Filter) = default; }; From 23eedb468faeafeed43255b520496688eb80f22c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 6 May 2025 09:25:22 +0400 Subject: [PATCH 051/310] Update API scheme to layer 204. --- .../SourceFiles/boxes/peers/edit_peer_info_box.cpp | 1 + Telegram/SourceFiles/data/data_saved_messages.cpp | 3 +++ Telegram/SourceFiles/mtproto/scheme/api.tl | 14 +++++++------- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 9d52003021..18c9341375 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -227,6 +227,7 @@ void SaveStarsPerMessage( const auto key = Api::RequestKey("stars_per_message", channel->id); const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice( + MTP_flags(0), // #TODO Support broadcast_messages_allowed flag in UI channel->inputChannel, MTP_long(starsPerMessage) )).done([=](const MTPUpdates &result) { diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 9a4f4441f9..e587c83549 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -81,6 +81,7 @@ void SavedMessages::sendLoadMore() { _loadMoreRequestId = _owner->session().api().request( MTPmessages_GetSavedDialogs( MTP_flags(MTPmessages_GetSavedDialogs::Flag::f_exclude_pinned), + MTPInputPeer(), // parent_peer MTP_int(_offsetDate), MTP_int(_offsetId), _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), @@ -125,6 +126,8 @@ void SavedMessages::sendLoadMore(not_null sublist) { const auto limit = offsetId ? kPerPage : kFirstPerPage; const auto requestId = _owner->session().api().request( MTPmessages_GetSavedHistory( + MTP_flags(0), + MTPInputPeer(), // parent_peer sublist->peer()->input, MTP_int(offsetId), MTP_int(offsetDate), diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 9c2351b24e..accfcd29e4 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -99,11 +99,11 @@ userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long 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#6592a1a7 id:long title:string = Chat; -channel#7482147e 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 fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date: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 usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long = Chat; +channel#7482147e 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 fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date: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 usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#52d6806b 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 flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long 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:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long 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?long 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 ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; +channelFull#7fc3facc 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 flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long 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:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long 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?long 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 ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int linked_monoforum_id:flags2.21?long = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -186,7 +186,7 @@ messageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long trans messageActionStarGift#4717e8a4 flags:# name_hidden:flags.0?true saved:flags.2?true converted:flags.3?true upgraded:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true gift:StarGift message:flags.1?TextWithEntities convert_stars:flags.4?long upgrade_msg_id:flags.5?int upgrade_stars:flags.8?long from_id:flags.11?Peer peer:flags.12?Peer saved_id:flags.12?long = MessageAction; messageActionStarGiftUnique#2e3ae60e flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long resale_stars:flags.8?long can_transfer_at:flags.9?int can_resell_at:flags.10?int = MessageAction; messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction; -messageActionPaidMessagesPrice#bcd71419 stars:long = MessageAction; +messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction; messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; @@ -2354,8 +2354,8 @@ messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; messages.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = WebViewResult; messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; -messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; -messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.getSavedDialogs#1e91fc99 flags:# exclude_pinned:flags.0?true parent_peer:flags.1?InputPeer offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; +messages.getSavedHistory#998ab009 flags:# parent_peer:flags.0?InputPeer peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; @@ -2498,7 +2498,7 @@ channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; -channels.updatePaidMessagesPrice#fc84653f channel:InputChannel send_paid_messages_stars:long = Updates; +channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; @@ -2707,4 +2707,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 203 +// LAYER 204 From d3f9a84a0a55b5159a66c2a930bdde69eff41c15 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 6 May 2025 17:48:18 +0400 Subject: [PATCH 052/310] Allow enabling direct messages in channels. --- Telegram/Resources/langs/lang.strings | 11 ++ Telegram/SourceFiles/apiwrap.cpp | 4 + .../SourceFiles/boxes/edit_privacy_box.cpp | 60 +++++++- Telegram/SourceFiles/boxes/edit_privacy_box.h | 6 + .../boxes/peers/edit_peer_info_box.cpp | 128 ++++++++++++++++-- .../SourceFiles/boxes/premium_limits_box.cpp | 1 + .../chat_helpers/chat_helpers.style | 4 + Telegram/SourceFiles/data/data_changes.h | 9 +- Telegram/SourceFiles/data/data_channel.cpp | 53 +++++++- Telegram/SourceFiles/data/data_channel.h | 40 ++++-- .../data/data_chat_participant_status.cpp | 3 +- Telegram/SourceFiles/data/data_peer.cpp | 14 ++ Telegram/SourceFiles/data/data_peer.h | 4 + .../SourceFiles/data/data_peer_values.cpp | 4 +- .../SourceFiles/data/data_saved_messages.cpp | 43 ++++-- .../SourceFiles/data/data_saved_messages.h | 14 +- .../SourceFiles/data/data_saved_sublist.cpp | 17 ++- .../SourceFiles/data/data_saved_sublist.h | 6 +- Telegram/SourceFiles/data/data_session.cpp | 27 ++-- .../dialogs/dialogs_inner_widget.cpp | 35 +++-- .../dialogs/dialogs_inner_widget.h | 6 +- .../export/data/export_data_types.cpp | 1 + .../export/data/export_data_types.h | 1 + .../export/output/export_output_html.cpp | 10 +- .../export/output/export_output_json.cpp | 1 + Telegram/SourceFiles/history/history.cpp | 37 +++++ Telegram/SourceFiles/history/history.h | 12 +- Telegram/SourceFiles/history/history_item.cpp | 27 +++- .../SourceFiles/history/history_widget.cpp | 97 ++++++++++--- Telegram/SourceFiles/history/history_widget.h | 6 +- .../view/history_view_sublist_section.cpp | 2 +- .../info/media/info_media_buttons.cpp | 38 +++--- .../info/media/info_media_widget.cpp | 42 +++--- .../info/media/info_media_widget.h | 10 +- .../info/saved/info_saved_sublists_widget.cpp | 2 +- Telegram/SourceFiles/window/main_window.cpp | 43 ++---- .../SourceFiles/window/window_separate_id.cpp | 41 +++--- .../SourceFiles/window/window_separate_id.h | 38 +++--- .../window/window_session_controller.cpp | 30 +--- 39 files changed, 685 insertions(+), 242 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e0e7281441..2d1ccfd8cc 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1886,6 +1886,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_linked_channel_posted" = "All new posts from this channel are forwarded to the group."; "lng_manage_discussion_group_warning" = "\"Chat history for new members\" will be switched to **Visible**."; +"lng_manage_monoforum" = "Direct Messages"; +"lng_manage_monoforum_off" = "Off"; +"lng_manage_monoforum_free" = "Free"; +"lng_manage_monoforum_allow" = "Allow Direct Messages"; +"lng_manage_monoforum_about" = "Allow users to write direct private messages to your channel, with the option to charge a fee for every message."; +"lng_manage_monoforum_price_about" = "Charge users for the ability to write a direct message to your channel. Your channel will receive {percent} of the selected fee ({amount}) for each incoming message."; + "lng_manage_history_visibility_title" = "Chat history for new members"; "lng_manage_history_visibility_shown" = "Visible"; "lng_manage_history_visibility_shown_about" = "New members will see messages that were sent before they joined."; @@ -2243,6 +2250,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_message_price_free" = "Messages are now free in this group."; "lng_action_message_price_paid#one" = "Messages now cost {count} Star each in this group."; "lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group."; +"lng_action_direct_messages_enabled" = "Channel enabled Direct Messages."; +"lng_action_direct_messages_paid#one" = "Channel allows Direct Messages for {count} Star each."; +"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each"; +"lng_action_direct_messages_disabled" = "Channel disabled Direct Messages."; "lng_you_paid_stars#one" = "You paid {count} Star."; "lng_you_paid_stars#other" = "You paid {count} Stars."; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 749c3722f9..0c3b2a521c 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -42,6 +42,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_search_controller.h" #include "data/data_session.h" @@ -381,6 +382,9 @@ void ApiWrap::savePinnedOrder(not_null forum) { } void ApiWrap::savePinnedOrder(not_null saved) { + if (saved->parentChat()) { + return; + } const auto &order = _session->data().pinnedChatsOrder(saved); const auto input = [](Dialogs::Key key) { if (const auto sublist = key.sublist()) { diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index c3c92325a0..1f83b004cb 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -1168,12 +1168,13 @@ rpl::producer SetupChargeSlider( struct State { rpl::variable stars; }; - const auto group = !peer->isUser(); + const auto broadcast = peer->isBroadcast(); + const auto group = !broadcast && !peer->isUser(); const auto state = container->lifetime().make_state(); const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars; state->stars = chargeStars; - Ui::AddSubsectionTitle(container, group + Ui::AddSubsectionTitle(container, (group || broadcast) ? tr::lng_rights_charge_price() : tr::lng_messages_privacy_price()); @@ -1225,7 +1226,9 @@ rpl::producer SetupChargeSlider( const auto percent = peer->session().appConfig().paidMessageCommission(); Ui::AddDividerText( container, - (group + (broadcast + ? tr::lng_manage_monoforum_price_about + : group ? tr::lng_rights_charge_price_about : tr::lng_messages_privacy_price_about)( lt_percent, @@ -1235,3 +1238,54 @@ rpl::producer SetupChargeSlider( return state->stars.value(); } + +void EditDirectMessagesPriceBox( + not_null box, + not_null channel, + std::optional savedValue, + Fn)> callback) { + box->setTitle(tr::lng_manage_monoforum()); + + const auto toggle = box->addRow(object_ptr( + box, + tr::lng_manage_monoforum_allow(), + st::settingsButtonNoIcon + ), {})->toggleOn(rpl::single(savedValue.has_value())); + Ui::AddSkip(box->verticalLayout()); + + Ui::AddDividerText( + box->verticalLayout(), + tr::lng_manage_monoforum_about()); + + const auto wrap = box->addRow( + object_ptr>( + box, + object_ptr(box)), + {}); + wrap->toggle(savedValue.has_value(), anim::type::instant); + wrap->toggleOn(toggle->toggledChanges()); + + const auto result = box->lifetime().make_state( + savedValue.value_or(0)); + + const auto inner = wrap->entity(); + Ui::AddSkip(inner); + SetupChargeSlider( + inner, + channel, + savedValue.value_or(0) + ) | rpl::start_with_next([=](int stars) { + *result = stars; + }, box->lifetime()); + + box->addButton(tr::lng_settings_save(), [=] { + const auto weak = Ui::MakeWeak(box); + callback(toggle->toggled() ? *result : std::optional()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index 256ebe5b51..d194572dc3 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -174,3 +174,9 @@ void EditMessagesPrivacyBox( not_null container, not_null peer, int savedValue); + +void EditDirectMessagesPriceBox( + not_null box, + not_null channel, + std::optional savedValue, + Fn)> callback); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 18c9341375..4bc44e3bd6 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/replace_boost_box.h" #include "boxes/peers/verify_peers_box.h" #include "boxes/peer_list_controllers.h" +#include "boxes/edit_privacy_box.h" // EditDirectMessagesPriceBox #include "boxes/stickers_box.h" #include "boxes/username_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -220,28 +221,41 @@ void SaveSlowmodeSeconds( } void SaveStarsPerMessage( + std::shared_ptr show, not_null channel, int starsPerMessage, - Fn done) { + Fn done) { const auto api = &channel->session().api(); const auto key = Api::RequestKey("stars_per_message", channel->id); + const auto broadcast = channel->isBroadcast(); + + using Flag = MTPchannels_UpdatePaidMessagesPrice::Flag; + const auto broadcastAllowed = broadcast && (starsPerMessage >= 0); const auto requestId = api->request(MTPchannels_UpdatePaidMessagesPrice( - MTP_flags(0), // #TODO Support broadcast_messages_allowed flag in UI + MTP_flags(broadcastAllowed + ? Flag::f_broadcast_messages_allowed + : Flag(0)), channel->inputChannel, MTP_long(starsPerMessage) )).done([=](const MTPUpdates &result) { api->clearModifyRequest(key); api->applyUpdates(result); - channel->setStarsPerMessage(starsPerMessage); - done(); + if (!broadcast) { + channel->setStarsPerMessage(starsPerMessage); + } + done(true); }).fail([=](const MTP::Error &error) { api->clearModifyRequest(key); if (error.type() != u"CHAT_NOT_MODIFIED"_q) { - return; + show->showToast(error.type()); + done(false); + } else { + if (!broadcast) { + channel->setStarsPerMessage(starsPerMessage); + } + done(true); } - channel->setStarsPerMessage(starsPerMessage); - done(); }).send(); api->registerModifyRequest(key, requestId); @@ -281,6 +295,7 @@ void SaveBoostsUnrestrict( void ShowEditPermissions( not_null navigation, not_null peer) { + const auto show = navigation->uiShow(); auto createBox = [=](not_null box) { const auto saving = box->lifetime().make_state(0); const auto save = [=]( @@ -299,7 +314,10 @@ void ShowEditPermissions( channel, result.boostsUnrestrict, close); - SaveStarsPerMessage(channel, result.starsPerMessage, close); + const auto price = result.starsPerMessage; + SaveStarsPerMessage(show, channel, price, [=](bool ok) { + close(); + }); } }; auto done = [=](EditPeerPermissionsBoxResult result) { @@ -366,6 +384,7 @@ private: std::optional joinToWrite; std::optional requestToJoin; std::optional discussionLink; + std::optional starsPerDirectMessage; }; [[nodiscard]] object_ptr createPhotoAndTitleEdit(); @@ -382,8 +401,10 @@ private: void showEditPeerTypeBox( std::optional> error = {}); void showEditDiscussionLinkBox(); + void showEditDirectMessagesBox(); void fillPrivacyTypeButton(); void fillDiscussionLinkButton(); + void fillDirectMessagesButton(); //void fillInviteLinkButton(); void fillForumButton(); void fillColorIndexButton(); @@ -412,6 +433,7 @@ private: [[nodiscard]] bool validateUsernamesOrder(Saving &to) const; [[nodiscard]] bool validateUsername(Saving &to) const; [[nodiscard]] bool validateDiscussionLink(Saving &to) const; + [[nodiscard]] bool validateDirectMessagesPrice(Saving &to) const; [[nodiscard]] bool validateTitle(Saving &to) const; [[nodiscard]] bool validateDescription(Saving &to) const; [[nodiscard]] bool validateHistoryVisibility(Saving &to) const; @@ -426,6 +448,7 @@ private: void saveUsernamesOrder(); void saveUsername(); void saveDiscussionLink(); + void saveDirectMessagesPrice(); void saveTitle(); void saveDescription(); void saveHistoryVisibility(); @@ -454,6 +477,7 @@ private: std::optional _discussionLinkSavedValue; ChannelData *_discussionLinkOriginalValue = nullptr; bool _channelHasLocationOriginalValue = false; + std::optional> _starsPerDirectMessageSavedValue; std::optional _historyVisibilitySavedValue; std::optional _typeDataSavedValue; std::optional _forumSavedValue; @@ -918,6 +942,20 @@ void Controller::showEditDiscussionLinkBox() { }).send(); } +void Controller::showEditDirectMessagesBox() { + Expects(_peer->isBroadcast()); + Expects(_starsPerDirectMessageSavedValue.has_value()); + + const auto stars = _starsPerDirectMessageSavedValue->current(); + _navigation->parentController()->show(Box( + EditDirectMessagesPriceBox, + _peer->asChannel(), + (stars >= 0) ? stars : std::optional(), + [=](std::optional value) { + *_starsPerDirectMessageSavedValue = value.value_or(-1); + })); +} + void Controller::fillPrivacyTypeButton() { Expects(_controls.buttonsLayout != nullptr); @@ -983,9 +1021,11 @@ void Controller::fillPrivacyTypeButton() { void Controller::fillDiscussionLinkButton() { Expects(_controls.buttonsLayout != nullptr); - _discussionLinkSavedValue = _discussionLinkOriginalValue = _peer->isChannel() - ? _peer->asChannel()->discussionLink() - : nullptr; + _discussionLinkSavedValue + = _discussionLinkOriginalValue + = (_peer->isChannel() + ? _peer->asChannel()->discussionLink() + : nullptr); const auto isGroup = (_peer->isChat() || _peer->isMegagroup()); auto text = !isGroup @@ -1019,6 +1059,33 @@ void Controller::fillDiscussionLinkButton() { { isGroup ? &st::menuIconChannel : &st::menuIconGroups }); _discussionLinkUpdates.fire_copy(*_discussionLinkSavedValue); } + +void Controller::fillDirectMessagesButton() { + Expects(_controls.buttonsLayout != nullptr); + + if (!_peer->isBroadcast() || !_peer->asChannel()->canEditInformation()) { + return; + } + + const auto monoforumLink = _peer->asChannel()->monoforumLink(); + _starsPerDirectMessageSavedValue = rpl::variable( + monoforumLink ? monoforumLink->starsPerMessage() : -1); + + auto label = _starsPerDirectMessageSavedValue->value( + ) | rpl::map([](int starsPerMessage) { + return (starsPerMessage < 0) + ? tr::lng_manage_monoforum_off() + : !starsPerMessage + ? tr::lng_manage_monoforum_free() + : rpl::single(Lang::FormatCountDecimal(starsPerMessage)); + }) | rpl::flatten_latest(); + AddButtonWithText( + _controls.buttonsLayout, + tr::lng_manage_monoforum(), + std::move(label), + [=] { showEditDirectMessagesBox(); }, + { &st::menuIconChatBubble }); +} // //void Controller::fillInviteLinkButton() { // Expects(_controls.buttonsLayout != nullptr); @@ -1359,6 +1426,8 @@ void Controller::fillManageSection() { const auto canViewOrEditDiscussionLink = isChannel && (channel->discussionLink() || (channel->isBroadcast() && channel->canEditInformation())); + const auto canEditDirectMessages = isChannel + && (channel->isBroadcast() && channel->canEditInformation()); ::AddSkip(_controls.buttonsLayout, 0); @@ -1370,6 +1439,9 @@ void Controller::fillManageSection() { if (canViewOrEditDiscussionLink) { fillDiscussionLinkButton(); } + if (canEditDirectMessages) { + fillDirectMessagesButton(); + } if (canEditPreHistoryHidden) { fillHistoryVisibilityButton(); } @@ -1973,6 +2045,7 @@ std::optional Controller::validate() const { if (validateUsernamesOrder(result) && validateUsername(result) && validateDiscussionLink(result) + && validateDirectMessagesPrice(result) && validateTitle(result) && validateDescription(result) && validateHistoryVisibility(result) @@ -2022,6 +2095,14 @@ bool Controller::validateDiscussionLink(Saving &to) const { return true; } +bool Controller::validateDirectMessagesPrice(Saving &to) const { + if (!_starsPerDirectMessageSavedValue) { + return true; + } + to.starsPerDirectMessage = _starsPerDirectMessageSavedValue->current(); + return true; +} + bool Controller::validateTitle(Saving &to) const { if (!_controls.title) { return true; @@ -2120,6 +2201,7 @@ void Controller::save() { pushSaveStage([=] { saveUsernamesOrder(); }); pushSaveStage([=] { saveUsername(); }); pushSaveStage([=] { saveDiscussionLink(); }); + pushSaveStage([=] { saveDirectMessagesPrice(); }); pushSaveStage([=] { saveTitle(); }); pushSaveStage([=] { saveDescription(); }); pushSaveStage([=] { saveHistoryVisibility(); }); @@ -2277,6 +2359,30 @@ void Controller::saveDiscussionLink() { }).send(); } +void Controller::saveDirectMessagesPrice() { + const auto channel = _peer->asChannel(); + if (!channel) { + return continueSave(); + } + const auto monoforumLink = channel->monoforumLink(); + const auto current = monoforumLink ? monoforumLink->starsPerMessage() : -1; + const auto desired = _savingData.starsPerDirectMessage + ? *_savingData.starsPerDirectMessage + : current; + if (desired == current) { + return continueSave(); + } + const auto show = _navigation->uiShow(); + const auto done = [=](bool ok) { + if (ok) { + continueSave(); + } else { + cancelSave(); + } + }; + SaveStarsPerMessage(show, channel, desired, crl::guard(this, done)); +} + void Controller::saveTitle() { if (!_savingData.title || *_savingData.title == _peer->name()) { return continueSave(); diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 325e92c443..da4120f0f7 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -907,6 +907,7 @@ void PinsLimitBox( limits.dialogsPinnedPremium(), PinsCount(session->data().chatsList())); } + void SublistsPinsLimitBox( not_null box, not_null session) { diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 06ac557554..b0a98edbfb 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -871,6 +871,10 @@ historyGiftToChannel: IconButton(defaultIconButton) { rippleAreaSize: 40px; ripple: universalRippleAnimation; } +historyDirectMessage: IconButton(historyGiftToChannel) { + icon: icon{{ "menu/chat_bubble", windowActiveTextFg }}; + iconOver: icon{{ "menu/chat_bubble", windowActiveTextFg }}; +} historyUnblock: FlatButton(historyComposeButton) { color: attentionButtonFg; overColor: attentionButtonFgOver; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 2eb84caedf..22cfec1e66 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -112,12 +112,13 @@ struct PeerUpdate { StickersSet = (1ULL << 46), EmojiSet = (1ULL << 47), DiscussionLink = (1ULL << 48), - ChannelLocation = (1ULL << 49), - Slowmode = (1ULL << 50), - GroupCall = (1ULL << 51), + MonoforumLink = (1ULL << 49), + ChannelLocation = (1ULL << 50), + Slowmode = (1ULL << 51), + GroupCall = (1ULL << 52), // For iteration - LastUsedBit = (1ULL << 51), + LastUsedBit = (1ULL << 52), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 8761065915..817e59fb97 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_group_call.h" #include "data/data_message_reactions.h" +#include "data/data_saved_messages.h" #include "data/data_wall_paper.h" #include "data/notify/data_notify_settings.h" #include "main/main_session.h" @@ -89,6 +90,29 @@ std::unique_ptr MegagroupInfo::takeForumData() { return nullptr; } +void MegagroupInfo::ensureMonoforum(not_null that) { + if (!_monoforum) { + const auto history = that->owner().history(that); + _monoforum = std::make_unique( + &that->owner(), + that); + history->monoforumChanged(nullptr); + } +} + +Data::SavedMessages *MegagroupInfo::monoforum() const { + return _monoforum.get(); +} + +std::unique_ptr MegagroupInfo::takeMonoforumData() { + if (auto result = base::take(_monoforum)) { + const auto history = result->owner().history(result->parentChat()); + history->monoforumChanged(result.get()); + return result; + } + return nullptr; +} + ChannelData::ChannelData(not_null owner, PeerId id) : PeerData(owner, id) , inputChannel( @@ -161,6 +185,12 @@ void ChannelData::setAccessHash(uint64 accessHash) { } void ChannelData::setFlags(ChannelDataFlags which) { + if (which & (Flag::Forum | Flag::Monoforum)) { + which |= Flag::Megagroup; + } + if (which & Flag::Monoforum) { + which &= ~Flag::Forum; + } const auto diff = flags() ^ which; if ((which & Flag::Megagroup) && !mgInfo) { mgInfo = std::make_unique(); @@ -276,8 +306,9 @@ const ChannelLocation *ChannelData::getLocation() const { } void ChannelData::setDiscussionLink(ChannelData *linked) { - if (_discussionLink != linked) { + if (_discussionLink != linked || !_discussionLinkKnown) { _discussionLink = linked; + _discussionLinkKnown = true; if (const auto history = owner().historyLoaded(this)) { history->forceFullResize(); } @@ -286,11 +317,22 @@ void ChannelData::setDiscussionLink(ChannelData *linked) { } ChannelData *ChannelData::discussionLink() const { - return _discussionLink.value_or(nullptr); + return _discussionLink; } bool ChannelData::discussionLinkKnown() const { - return _discussionLink.has_value(); + return _discussionLinkKnown; +} + +void ChannelData::setMonoforumLink(ChannelData *link) { + if (_monoforumLink != link) { + _monoforumLink = link; + session().changes().peerUpdated(this, UpdateFlag::MonoforumLink); + } +} + +ChannelData *ChannelData::monoforumLink() const { + return _monoforumLink; } void ChannelData::setMembersCount(int newMembersCount) { @@ -1240,6 +1282,11 @@ void ApplyChannelUpdate( } else { channel->setDiscussionLink(nullptr); } + if (const auto chat = update.vlinked_monoforum_id()) { + channel->setMonoforumLink(channel->owner().channelLoaded(chat->v)); + } else { + channel->setMonoforumLink(nullptr); + } if (const auto history = channel->owner().historyLoaded(channel)) { if (const auto available = update.vavailable_min_id()) { history->clearUpTill(available->v); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index b4f99b5243..6dc802dcfc 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -16,6 +16,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class ChannelData; +namespace Data { +class Forum; +class SavedMessages; +} // namespace Data + struct ChannelLocation { QString address; Data::LocationPoint point; @@ -74,6 +79,7 @@ enum class ChannelDataFlag : uint64 { StargiftsAvailable = (1ULL << 36), PaidMessagesAvailable = (1ULL << 37), AutoTranslation = (1ULL << 38), + Monoforum = (1ULL << 39), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -118,6 +124,10 @@ public: [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] std::unique_ptr takeForumData(); + void ensureMonoforum(not_null that); + [[nodiscard]] Data::SavedMessages *monoforum() const; + [[nodiscard]] std::unique_ptr takeMonoforumData(); + std::deque> lastParticipants; base::flat_map, Admin> lastAdmins; base::flat_map, Restricted> lastRestricted; @@ -154,6 +164,7 @@ private: ChannelLocation _location; Data::ChatBotCommands _botCommands; std::unique_ptr _forum; + std::unique_ptr _monoforum; int _starsPerMessage = 0; friend class ChannelData; @@ -301,6 +312,9 @@ public: [[nodiscard]] bool isForum() const { return flags() & Flag::Forum; } + [[nodiscard]] bool isMonoforum() const { + return flags() & Flag::Monoforum; + } [[nodiscard]] bool hasUsername() const { return flags() & Flag::Username; } @@ -413,6 +427,9 @@ public: [[nodiscard]] ChannelData *discussionLink() const; [[nodiscard]] bool discussionLinkKnown() const; + void setMonoforumLink(ChannelData *link); + [[nodiscard]] ChannelData *monoforumLink() const; + void ptsInit(int32 pts) { _ptsWaiter.init(pts); } @@ -510,6 +527,9 @@ public: [[nodiscard]] Data::Forum *forum() const { return mgInfo ? mgInfo->forum() : nullptr; } + [[nodiscard]] Data::SavedMessages *monoforum() const { + return mgInfo ? mgInfo->monoforum() : nullptr; + } void processTopics(const MTPVector &topics); @@ -546,18 +566,11 @@ private: std::vector &&reasons) override; Flags _flags = ChannelDataFlags(Flag::Forbidden); - int _peerGiftsCount = 0; PtsWaiter _ptsWaiter; Data::UsernamesInfo _username; - int _membersCount = -1; - int _adminsCount = 1; - int _restrictedCount = 0; - int _kickedCount = 0; - int _pendingRequestsCount = 0; - int _levelHint = 0; std::vector _recentRequesters; MsgId _availableMinId = 0; @@ -570,7 +583,18 @@ private: std::vector _unavailableReasons; std::unique_ptr _invitePeek; QString _inviteLink; - std::optional _discussionLink; + + ChannelData *_discussionLink = nullptr; + ChannelData *_monoforumLink = nullptr; + bool _discussionLinkKnown = false; + + int _peerGiftsCount = 0; + int _membersCount = -1; + int _adminsCount = 1; + int _restrictedCount = 0; + int _kickedCount = 0; + int _pendingRequestsCount = 0; + int _levelHint = 0; Data::AllowedReactions _allowedReactions; diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 81b3330972..2a85f44d13 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -159,7 +159,8 @@ bool CanSendAnyOf( using Flag = ChannelDataFlag; const auto allowed = channel->amIn() || ((channel->flags() & Flag::HasLink) - && !(channel->flags() & Flag::JoinToWrite)); + && !(channel->flags() & Flag::JoinToWrite)) + || channel->isMonoforum(); if (!allowed || (forbidInForums && channel->isForum())) { return false; } else if (channel->canPostMessages()) { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index c2c614577c..6a480a075a 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1333,6 +1333,13 @@ bool PeerData::isForum() const { return false; } +bool PeerData::isMonoforum() const { + if (const auto channel = asChannel()) { + return channel->isMonoforum(); + } + return false; +} + bool PeerData::isGigagroup() const { if (const auto channel = asChannel()) { return channel->isGigagroup(); @@ -1416,6 +1423,13 @@ Data::ForumTopic *PeerData::forumTopicFor(MsgId rootId) const { return nullptr; } +Data::SavedMessages *PeerData::monoforum() const { + if (const auto channel = asChannel()) { + return channel->monoforum(); + } + return nullptr; +} + bool PeerData::allowsForwarding() const { if (isUser()) { return true; diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index b0563c42d5..a4a1ebe531 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -37,6 +37,7 @@ class Forum; class ForumTopic; class Session; class GroupCall; +class SavedMessages; struct ReactionId; class WallPaper; @@ -232,6 +233,7 @@ public: [[nodiscard]] bool isMegagroup() const; [[nodiscard]] bool isBroadcast() const; [[nodiscard]] bool isForum() const; + [[nodiscard]] bool isMonoforum() const; [[nodiscard]] bool isGigagroup() const; [[nodiscard]] bool isRepliesChat() const; [[nodiscard]] bool isVerifyCodes() const; @@ -257,6 +259,8 @@ public: [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] Data::ForumTopic *forumTopicFor(MsgId rootId) const; + [[nodiscard]] Data::SavedMessages *monoforum() const; + [[nodiscard]] Data::PeerNotifySettings ¬ify() { return _notify; } diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 287dd2e42b..0c435d5347 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -269,6 +269,7 @@ inline auto DefaultRestrictionValue( | Flag::Left | Flag::Forum | Flag::JoinToWrite + | Flag::Monoforum | Flag::HasLink | Flag::Forbidden | Flag::Creator @@ -292,7 +293,8 @@ inline auto DefaultRestrictionValue( && (flags & Flag::Forum); const auto allowed = !(flags & notAmInFlags) || ((flags & Flag::HasLink) - && !(flags & Flag::JoinToWrite)); + && !(flags & Flag::JoinToWrite)) + || (flags & Flag::Monoforum); const auto restricted = sendRestriction | (defaultSendRestriction && !unrestrictedByBoosts); return allowed diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index e587c83549..6a401afd17 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_messages.h" #include "apiwrap.h" +#include "data/data_channel.h" #include "data/data_peer.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -25,12 +26,15 @@ constexpr auto kListFirstPerPage = 20; } // namespace -SavedMessages::SavedMessages(not_null owner) +SavedMessages::SavedMessages( + not_null owner, + ChannelData *parentChat) : _owner(owner) +, _parentChat(parentChat) , _chatsList( - &owner->session(), + &_owner->session(), FilterId(), - owner->maxPinnedChatsLimitValue(this)) + _owner->maxPinnedChatsLimitValue(this)) , _loadMore([=] { sendLoadMoreRequests(); }) { } @@ -40,6 +44,10 @@ bool SavedMessages::supported() const { return !_unsupported; } +ChannelData *SavedMessages::parentChat() const { + return _parentChat; +} + Session &SavedMessages::owner() const { return *_owner; } @@ -59,7 +67,11 @@ not_null SavedMessages::sublist(not_null peer) { } return _sublists.emplace( peer, - std::make_unique(peer)).first->second.get(); + std::make_unique(this, peer)).first->second.get(); +} + +rpl::producer<> SavedMessages::chatsListChanges() const { + return _chatsListChanges.events(); } void SavedMessages::loadMore() { @@ -78,10 +90,12 @@ void SavedMessages::sendLoadMore() { } else if (!_pinnedLoaded) { loadPinned(); } + using Flag = MTPmessages_GetSavedDialogs::Flag; _loadMoreRequestId = _owner->session().api().request( MTPmessages_GetSavedDialogs( - MTP_flags(MTPmessages_GetSavedDialogs::Flag::f_exclude_pinned), - MTPInputPeer(), // parent_peer + MTP_flags(Flag::f_exclude_pinned + | (_parentChat ? Flag::f_parent_peer : Flag(0))), + _parentChat ? _parentChat->input : MTPInputPeer(), MTP_int(_offsetDate), MTP_int(_offsetId), _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), @@ -89,6 +103,7 @@ void SavedMessages::sendLoadMore() { MTP_long(0)) // hash ).done([=](const MTPmessages_SavedDialogs &result) { apply(result, false); + _chatsListChanges.fire({}); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { _unsupported = true; @@ -99,13 +114,14 @@ void SavedMessages::sendLoadMore() { } void SavedMessages::loadPinned() { - if (_pinnedRequestId) { + if (_pinnedRequestId || parentChat()) { return; } _pinnedRequestId = _owner->session().api().request( MTPmessages_GetPinnedSavedDialogs() ).done([=](const MTPmessages_SavedDialogs &result) { apply(result, true); + _chatsListChanges.fire({}); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { _unsupported = true; @@ -124,10 +140,11 @@ void SavedMessages::sendLoadMore(not_null sublist) { const auto offsetId = list.empty() ? MsgId(0) : list.back()->id; const auto offsetDate = list.empty() ? MsgId(0) : list.back()->date(); const auto limit = offsetId ? kPerPage : kFirstPerPage; + using Flag = MTPmessages_GetSavedHistory::Flag; const auto requestId = _owner->session().api().request( MTPmessages_GetSavedHistory( - MTP_flags(0), - MTPInputPeer(), // parent_peer + MTP_flags(_parentChat ? Flag::f_parent_peer : Flag(0)), + _parentChat ? _parentChat->input : MTPInputPeer(), sublist->peer()->input, MTP_int(offsetId), MTP_int(offsetDate), @@ -261,6 +278,8 @@ void SavedMessages::sendLoadMoreRequests() { } void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { + Expects(!parentChat()); + const auto list = update.vorder(); if (!list) { loadPinned(); @@ -286,6 +305,8 @@ void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { } void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { + Expects(!parentChat()); + update.vpeer().match([&](const MTPDdialogPeer &data) { const auto peer = _owner->peer(peerFromMTP(data.vpeer())); const auto i = _sublists.find(peer); @@ -300,4 +321,8 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { }); } +rpl::lifetime &SavedMessages::lifetime() { + return _lifetime; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 3e09f4db0a..3ef9aae9c0 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -20,10 +20,13 @@ class SavedSublist; class SavedMessages final { public: - explicit SavedMessages(not_null owner); + explicit SavedMessages( + not_null owner, + ChannelData *parentChat = nullptr); ~SavedMessages(); [[nodiscard]] bool supported() const; + [[nodiscard]] ChannelData *parentChat() const; [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; @@ -31,12 +34,16 @@ public: [[nodiscard]] not_null chatsList(); [[nodiscard]] not_null sublist(not_null peer); + [[nodiscard]] rpl::producer<> chatsListChanges() const; + void loadMore(); void loadMore(not_null sublist); void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); + [[nodiscard]] rpl::lifetime &lifetime(); + private: void loadPinned(); void apply(const MTPmessages_SavedDialogs &result, bool pinned); @@ -46,6 +53,7 @@ private: void sendLoadMoreRequests(); const not_null _owner; + ChannelData *_parentChat = nullptr; Dialogs::MainList _chatsList; base::flat_map< @@ -64,9 +72,13 @@ private: base::flat_set> _loadMoreSublistsScheduled; bool _loadMoreScheduled = false; + rpl::event_stream<> _chatsListChanges; + bool _pinnedLoaded = false; bool _unsupported = false; + rpl::lifetime _lifetime; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 2d129a5f67..134ada5295 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -17,13 +17,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { -SavedSublist::SavedSublist(not_null peer) +SavedSublist::SavedSublist( + not_null parent, + not_null peer) : Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist) +, _parent(parent) , _history(peer->owner().history(peer)) { } SavedSublist::~SavedSublist() = default; +not_null SavedSublist::parent() const { + return _parent; +} + +ChannelData *SavedSublist::parentChat() const { + return _parent->parentChat(); +} + not_null SavedSublist::history() const { return _history; } @@ -101,9 +112,7 @@ void SavedSublist::removeOne(not_null item) { updateChatListExistence(); } else { updateChatListEntry(); - crl::on_main(this, [=] { - owner().savedMessages().loadMore(this); - }); + crl::on_main(this, [=] { _parent->loadMore(this); }); } } else { setChatListTimeId(_items.front()->date()); diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 15c3428fff..8e59854e45 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -16,12 +16,15 @@ class History; namespace Data { class Session; +class SavedMessages; class SavedSublist final : public Dialogs::Entry { public: - explicit SavedSublist(not_null peer); + SavedSublist(not_null parent,not_null peer); ~SavedSublist(); + [[nodiscard]] not_null parent() const; + [[nodiscard]] ChannelData *parentChat() const; [[nodiscard]] not_null history() const; [[nodiscard]] not_null peer() const; [[nodiscard]] bool isHiddenAuthor() const; @@ -72,6 +75,7 @@ private: void allowChatListMessageResolve(); void resolveChatListMessageGroup(); + const not_null _parent; const not_null _history; std::vector> _items; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 8a19ce6483..71368f588a 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -967,7 +967,8 @@ not_null Session::processChat(const MTPChat &data) { | ((!minimal && !data.is_stories_hidden_min()) ? Flag::StoriesHidden : Flag()) - | Flag::AutoTranslation; + | Flag::AutoTranslation + | Flag::Monoforum; const auto storiesState = minimal ? std::optional() : data.is_stories_unavailable() @@ -1007,7 +1008,8 @@ not_null Session::processChat(const MTPChat &data) { && data.is_stories_hidden()) ? Flag::StoriesHidden : Flag()) - | (data.is_autotranslation() ? Flag::AutoTranslation : Flag()); + | (data.is_autotranslation() ? Flag::AutoTranslation : Flag()) + | (data.is_monoforum() ? Flag::Monoforum : Flag()); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setBotVerifyDetailsIcon( data.vbot_verification_icon().value_or_empty()); @@ -2310,6 +2312,9 @@ void Session::applyDialog( bool Session::pinnedCanPin(not_null entry) const { if ([[maybe_unused]] const auto sublist = entry->asSublist()) { + if (sublist->parentChat()) { + return false; + } const auto saved = &savedMessages(); return pinnedChatsOrder(saved).size() < pinnedChatsLimit(saved); } else if (const auto topic = entry->asTopic()) { @@ -2351,6 +2356,9 @@ int Session::pinnedChatsLimit(not_null forum) const { } int Session::pinnedChatsLimit(not_null saved) const { + if (saved->parentChat()) { + return 0; + } const auto limits = Data::PremiumLimits(_session); return limits.savedSublistsPinnedCurrent(); } @@ -2391,6 +2399,9 @@ rpl::producer Session::maxPinnedChatsLimitValue( rpl::producer Session::maxPinnedChatsLimitValue( not_null saved) const { + if (saved->parentChat()) { + return rpl::single(0); + } // Premium limit from appconfig. // We always use premium limit in the MainList limit producer, // because it slices the list to that limit. We don't want to slice @@ -4563,12 +4574,12 @@ not_null Session::processFolder(const MTPDfolder &data) { not_null Session::chatsListFor( not_null entry) { - const auto topic = entry->asTopic(); - return topic - ? topic->forum()->topicsList() - : entry->asSublist() - ? _savedMessages->chatsList() - : chatsList(entry->folder()); + if (const auto topic = entry->asTopic()) { + return topic->forum()->topicsList(); + } else if (const auto sublist = entry->asSublist()) { + return sublist->parent()->chatsList(); + } + return chatsList(entry->folder()); } not_null Session::chatsList(Data::Folder *folder) { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 4c2775aeeb..4b21f5c6e9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -781,11 +781,14 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) { } } -void InnerWidget::showSavedSublists() { +void InnerWidget::showSavedSublists(ChannelData *parentChat) { + Expects(!parentChat || parentChat->monoforum()); Expects(!_geometryInited); Expects(!_savedSublists); - _savedSublists = true; + _savedSublists = parentChat + ? parentChat->monoforum() + : &session().data().savedMessages(); stopReorderPinned(); clearSelection(); @@ -2115,7 +2118,7 @@ bool InnerWidget::addQuickActionRipple( const std::vector &InnerWidget::pinnedChatsOrder() const { const auto owner = &session().data(); return _savedSublists - ? owner->pinnedChatsOrder(&owner->savedMessages()) + ? owner->pinnedChatsOrder(_savedSublists) : _openedForum ? owner->pinnedChatsOrder(_openedForum) : _filterId @@ -2179,6 +2182,9 @@ int InnerWidget::countPinnedIndex(Row *ofRow) { } void InnerWidget::savePinnedOrder() { + if (_savedSublists && _savedSublists->parentChat()) { + return; + } const auto &newOrder = pinnedChatsOrder(); if (newOrder.size() != _pinnedOnDragStart.size()) { return; // Something has changed in the set of pinned chats. @@ -2316,8 +2322,11 @@ bool InnerWidget::updateReorderPinned(QPoint localPosition) { const auto delta = [&] { if (localPosition.y() < _visibleTop) { return localPosition.y() - _visibleTop; - } else if ((_savedSublists || _openedFolder || _openedForum || _filterId) - && localPosition.y() > _visibleBottom) { + } else if ((localPosition.y() > _visibleBottom) + && (_savedSublists + || _openedFolder + || _openedForum + || _filterId)) { return localPosition.y() - _visibleBottom; } return 0; @@ -2685,8 +2694,8 @@ void InnerWidget::handleChatListEntryRefreshes() { return false; } else if (const auto topic = event.key.topic()) { return (topic->forum() == _openedForum); - } else if (event.key.sublist()) { - return _savedSublists; + } else if (const auto sublist = event.key.sublist()) { + return sublist->parent() == _savedSublists; } else { return !_openedForum; } @@ -2704,7 +2713,7 @@ void InnerWidget::handleChatListEntryRefreshes() { && (key.topic() ? (key.topic()->forum() == _openedForum) : key.sublist() - ? _savedSublists + ? (key.sublist()->parent() == _savedSublists) : (entry->folder() == _openedFolder))) { _dialogMoved.fire({ from, to }); } @@ -2909,7 +2918,8 @@ void InnerWidget::enterEventHook(QEnterEvent *e) { Row *InnerWidget::shownRowByKey(Key key) { const auto entry = key.entry(); if (_savedSublists) { - if (!entry->asSublist()) { + const auto sublist = entry->asSublist(); + if (!sublist || sublist->parent() != _savedSublists) { return nullptr; } } else if (_openedForum) { @@ -2978,7 +2988,7 @@ void InnerWidget::updateSelectedRow(Key key) { void InnerWidget::refreshShownList() { const auto list = _savedSublists - ? session().data().savedMessages().chatsList()->indexed() + ? _savedSublists->chatsList()->indexed() : _openedForum ? _openedForum->topicsList()->indexed() : _filterId @@ -3440,8 +3450,7 @@ void InnerWidget::applySearchState(SearchState state) { }; if (_searchState.filterChatsList() && !words.isEmpty()) { if (_savedSublists) { - const auto owner = &session().data(); - append(owner->savedMessages().chatsList()->indexed()); + append(_savedSublists->chatsList()->indexed()); } else if (_openedForum) { append(_openedForum->topicsList()->indexed()); } else { @@ -4012,7 +4021,7 @@ void InnerWidget::refreshEmpty() { const auto state = !_shownList->empty() ? EmptyState::None : _savedSublists - ? (data->savedMessages().chatsList()->loaded() + ? (_savedSublists->chatsList()->loaded() ? EmptyState::EmptySavedSublists : EmptyState::Loading) : _openedForum diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 9842faf67a..bc6b54e832 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -58,6 +58,7 @@ class ChatFilter; class Thread; class Folder; class Forum; +class SavedMessages; struct ReactionId; } // namespace Data @@ -140,7 +141,7 @@ public: void changeOpenedFolder(Data::Folder *folder); void changeOpenedForum(Data::Forum *forum); - void showSavedSublists(); + void showSavedSublists(ChannelData *parentChat); void selectSkip(int32 direction); void selectSkipPage(int32 pixels, int32 direction); @@ -668,7 +669,8 @@ private: float64 _narrowRatio = 0.; bool _geometryInited = false; - bool _savedSublists = false; + Data::SavedMessages *_savedSublists = nullptr; + bool _searchLoading = false; bool _searchWaiting = false; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index e3bf2f2100..d9b66c85f8 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1712,6 +1712,7 @@ ServiceAction ParseServiceAction( }, [&](const MTPDmessageActionPaidMessagesPrice &data) { result.content = ActionPaidMessagesPrice{ .stars = int(data.vstars().v), + .broadcastAllowed = data.is_broadcast_messages_allowed(), }; }, [&](const MTPDmessageActionConferenceCall &data) { auto content = ActionPhoneCall(); diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index ca9a95916d..5a3e2c3ec2 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -673,6 +673,7 @@ struct ActionPaidMessagesRefunded { struct ActionPaidMessagesPrice { int stars = 0; + bool broadcastAllowed = false; }; struct ServiceAction { diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 3d960a38e2..ed73f81b0f 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1383,7 +1383,15 @@ auto HtmlWriter::Wrap::pushMessage( + " messages to you"); return result; }, [&](const ActionPaidMessagesPrice &data) { - auto result = "Price per messages changed to " + if (isChannel) { + auto result = !data.broadcastAllowed + ? "Direct messages were disabled." + : ("Price per direct message changed to " + + QString::number(data.stars).toUtf8() + + " Telegram Stars."); + return result; + } + auto result = "Price per message changed to " + QString::number(data.stars).toUtf8() + " Telegram Stars."; return result; diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 772bc6f3f2..7af9bc0b97 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -679,6 +679,7 @@ QByteArray SerializeMessage( pushActor(); pushAction("paid_messages_price_change"); push("price_stars", data.stars); + push("is_broadcast_messages_allowed", data.broadcastAllowed); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index cd8578a860..98a7b830d8 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" @@ -3131,6 +3132,42 @@ bool History::isForum() const { return (_flags & Flag::IsForum); } +void History::monoforumChanged(Data::SavedMessages *old) { + if (inChatList()) { + notifyUnreadStateChange(old + ? AdjustedForumUnreadState(old->chatsList()->unreadState()) + : computeUnreadState()); + } + + if (const auto monoforum = peer->monoforum()) { + _flags |= Flag::IsMonoforum; + + monoforum->chatsList()->unreadStateChanges( + ) | rpl::filter([=] { + return (_flags & Flag::IsMonoforum) && inChatList(); + }) | rpl::map( + AdjustedForumUnreadState + ) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { + notifyUnreadStateChange(old); + }, monoforum->lifetime()); + + monoforum->chatsListChanges( + ) | rpl::start_with_next([=] { + updateChatListEntry(); + }, monoforum->lifetime()); + } else { + _flags &= ~Flag::IsMonoforum; + } + if (cloudDraft(MsgId(0))) { + updateChatListSortPosition(); + } + _flags |= Flag::PendingAllItemsResize; +} + +bool History::isMonoforum() const { + return (_flags & Flag::IsMonoforum); +} + not_null History::migrateToOrMe() const { if (const auto to = peer->migrateTo()) { return owner().history(to); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 57b1203d02..8963ab0a08 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -27,12 +27,14 @@ struct LanguageId; namespace Data { struct Draft; +class Forum; class Session; class Folder; class ChatFilter; struct SponsoredFrom; class SponsoredMessages; class HistoryMessages; +class SavedMessages; } // namespace Data namespace Dialogs { @@ -71,6 +73,9 @@ public: void forumChanged(Data::Forum *old); [[nodiscard]] bool isForum() const; + void monoforumChanged(Data::SavedMessages *old); + [[nodiscard]] bool isMonoforum() const; + [[nodiscard]] not_null migrateToOrMe() const; [[nodiscard]] History *migrateFrom() const; [[nodiscard]] MsgRange rangeForDifferenceRequest() const; @@ -430,9 +435,10 @@ private: PendingAllItemsResize = (1 << 1), IsTopPromoted = (1 << 2), IsForum = (1 << 3), - FakeUnreadWhileOpened = (1 << 4), - HasPinnedMessages = (1 << 5), - ResolveChatListMessage = (1 << 6), + IsMonoforum = (1 << 4), + FakeUnreadWhileOpened = (1 << 5), + HasPinnedMessages = (1 << 6), + ResolveChatListMessage = (1 << 7), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0b7571674c..275edd80f8 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3563,6 +3563,12 @@ Data::SavedSublist *HistoryItem::savedSublist() const { that->AddComponents(HistoryMessageSaved::Bit()); that->Get()->sublist = sublist; return sublist; + } else if (const auto monoforum = _history->peer->monoforum()) { + const auto sublist = monoforum->sublist(_history->peer); + const auto that = const_cast(this); + that->AddComponents(HistoryMessageSaved::Bit()); + that->Get()->sublist = sublist; + return sublist; } return nullptr; } @@ -3785,7 +3791,9 @@ void HistoryItem::createComponents(CreateConfig &&config) { } } const auto peer = _history->owner().peer(config.savedSublistPeer); - saved->sublist = _history->owner().savedMessages().sublist(peer); + saved->sublist = _history->peer->isSelf() + ? _history->owner().savedMessages().sublist(peer) + : _history->peer->monoforum()->sublist(peer); } if (const auto reply = Get()) { @@ -5744,8 +5752,23 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto preparePaidMessagesPrice = [&](const MTPDmessageActionPaidMessagesPrice &action) { const auto stars = action.vstars().v; + const auto broadcastAllowed = action.is_broadcast_messages_allowed(); auto result = PreparedServiceText(); - result.text = stars + result.text = _history->peer->isBroadcast() + ? (stars > 0 + ? tr::lng_action_direct_messages_paid( + tr::now, + lt_count, + stars, + Ui::Text::WithEntities) + : broadcastAllowed + ? tr::lng_action_direct_messages_enabled( + tr::now, + Ui::Text::WithEntities) + : tr::lng_action_direct_messages_disabled( + tr::now, + Ui::Text::WithEntities)) + : stars ? tr::lng_action_message_price_paid( tr::now, lt_count, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index f32fd69ffe..9e829886f7 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -383,6 +383,7 @@ HistoryWidget::HistoryWidget( _joinChannel->addClickHandler([=] { joinChannel(); }); _muteUnmute->addClickHandler([=] { toggleMuteUnmute(); }); setupGiftToChannelButton(); + setupDirectMessageButton(); _reportMessages->addClickHandler([=] { reportSelectedMessages(); }); _field->submits( ) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) { @@ -1050,15 +1051,23 @@ void HistoryWidget::refreshJoinChannelText() { } void HistoryWidget::refreshGiftToChannelShown() { - if (!_giftToChannelIn || !_giftToChannelOut) { + if (!_giftToChannel || !_peer) { return; } const auto channel = _peer->asChannel(); - const auto shown = channel + _giftToChannel->setVisible(channel && channel->isBroadcast() - && channel->stargiftsAvailable(); - _giftToChannelIn->setVisible(shown); - _giftToChannelOut->setVisible(shown); + && channel->stargiftsAvailable()); +} + +void HistoryWidget::refreshDirectMessageShown() { + if (!_directMessage || !_peer) { + return; + } + const auto channel = _peer->asChannel(); + _directMessage->setVisible(channel + && channel->isBroadcast() + && channel->monoforumLink()); } void HistoryWidget::refreshTopBarActiveChat() { @@ -2074,22 +2083,63 @@ void HistoryWidget::setupShortcuts() { } void HistoryWidget::setupGiftToChannelButton() { - const auto setupButton = [=](not_null parent) { - auto *button = Ui::CreateChild( - parent.get(), - st::historyGiftToChannel); - parent->widthValue() | rpl::start_with_next([=](int width) { - button->moveToRight(0, 0); - }, button->lifetime()); - button->setClickedCallback([=] { - if (_peer) { - Ui::ShowStarGiftBox(controller(), _peer); + _giftToChannel = Ui::CreateChild( + _muteUnmute.data(), + st::historyGiftToChannel); + widthValue() | rpl::start_with_next([=](int width) { + _giftToChannel->moveToRight(0, 0, width); + }, _giftToChannel->lifetime()); + _giftToChannel->setClickedCallback([=] { + Ui::ShowStarGiftBox(controller(), _peer); + }); + rpl::combine( + _muteUnmute->shownValue(), + _joinChannel->shownValue() + ) | rpl::start_with_next([=](bool muteUnmute, bool joinChannel) { + const auto newParent = (muteUnmute && !joinChannel) + ? _muteUnmute.data() + : (joinChannel && !muteUnmute) + ? _joinChannel.data() + : nullptr; + if (newParent) { + _giftToChannel->setParent(newParent); + _giftToChannel->moveToRight(0, 0); + refreshGiftToChannelShown(); + } + }, _giftToChannel->lifetime()); +} + +void HistoryWidget::setupDirectMessageButton() { + _directMessage = Ui::CreateChild( + _muteUnmute.data(), + st::historyDirectMessage); + widthValue() | rpl::start_with_next([=](int width) { + _directMessage->moveToRight(0, 0, width); + }, _directMessage->lifetime()); + _directMessage->setClickedCallback([=] { + if (const auto channel = _peer ? _peer->asChannel() : nullptr) { + if (const auto monoforum = channel->monoforumLink()) { + controller()->showPeerHistory( + monoforum, + Window::SectionShow::Way::Forward); } - }); - return button; - }; - _giftToChannelIn = setupButton(_muteUnmute); - _giftToChannelOut = setupButton(_joinChannel); + } + }); + rpl::combine( + _muteUnmute->shownValue(), + _joinChannel->shownValue() + ) | rpl::start_with_next([=](bool muteUnmute, bool joinChannel) { + const auto newParent = (muteUnmute && !joinChannel) + ? _muteUnmute.data() + : (joinChannel && !muteUnmute) + ? _joinChannel.data() + : nullptr; + if (newParent) { + _directMessage->setParent(newParent); + _directMessage->moveToLeft(0, 0); + refreshDirectMessageShown(); + } + }, _directMessage->lifetime()); } void HistoryWidget::pushReplyReturn(not_null item) { @@ -2456,6 +2506,7 @@ void HistoryWidget::showHistory( }, _contactStatus->bar().lifetime()); refreshGiftToChannelShown(); + refreshDirectMessageShown(); if (const auto user = _peer->asUser()) { _paysStatus = std::make_unique( controller(), @@ -5220,7 +5271,10 @@ bool HistoryWidget::isBlocked() const { } bool HistoryWidget::isJoinChannel() const { - return _peer && _peer->isChannel() && !_peer->asChannel()->amIn(); + if (const auto channel = _peer ? _peer->asChannel() : nullptr) { + return !channel->amIn() && !channel->isMonoforum(); + } + return false; } bool HistoryWidget::isChoosingTheme() const { @@ -8639,6 +8693,7 @@ void HistoryWidget::fullInfoUpdated() { sendBotStartCommand(); } refreshGiftToChannelShown(); + refreshDirectMessageShown(); } if (updateCmdStartShown()) { refresh = true; diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 33571de179..c11ecdc7c0 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -406,6 +406,7 @@ private: void refreshJoinChannelText(); void refreshGiftToChannelShown(); + void refreshDirectMessageShown(); void requestMessageData(MsgId msgId); void messageDataReceived(not_null peer, MsgId msgId); @@ -535,6 +536,7 @@ private: void setupShortcuts(); void setupGiftToChannelButton(); + void setupDirectMessageButton(); void handlePeerMigration(); @@ -797,8 +799,8 @@ private: object_ptr _botStart; object_ptr _joinChannel; object_ptr _muteUnmute; - QPointer _giftToChannelIn; - QPointer _giftToChannelOut; + QPointer _giftToChannel; + QPointer _directMessage; object_ptr _reportMessages; struct { object_ptr button = { nullptr }; diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp index 793133bbd0..c3f74ac708 100644 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp @@ -606,7 +606,7 @@ rpl::producer SublistWidget::listSource( ? (*result.fullCount - after - useBefore) : std::optional(); if (!result.fullCount || useBefore < limitBefore) { - _sublist->owner().savedMessages().loadMore(_sublist); + _sublist->parent()->loadMore(_sublist); } consumer.put_next(std::move(result)); }; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index ae10a739e6..c15b1c8f1a 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_stories_ids.h" #include "data/data_user.h" #include "history/view/history_view_sublist_section.h" +#include "history/history.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "info/profile/info_profile_values.h" @@ -32,39 +33,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Media { namespace { -[[nodiscard]] Window::SeparateSharedMediaType ToSeparateType( - Storage::SharedMediaType type) { +[[nodiscard]] bool SeparateSupported(Storage::SharedMediaType type) { using Type = Storage::SharedMediaType; - using SeparatedType = Window::SeparateSharedMediaType; return (type == Type::Photo) - ? SeparatedType::Photos - : (type == Type::Video) - ? SeparatedType::Videos - : (type == Type::File) - ? SeparatedType::Files - : (type == Type::MusicFile) - ? SeparatedType::Audio - : (type == Type::Link) - ? SeparatedType::Links - : (type == Type::RoundVoiceFile) - ? SeparatedType::Voices - : (type == Type::GIF) - ? SeparatedType::GIF - : SeparatedType::None; + || (type == Type::Video) + || (type == Type::File) + || (type == Type::MusicFile) + || (type == Type::Link) + || (type == Type::RoundVoiceFile) + || (type == Type::GIF); } [[nodiscard]] Window::SeparateId SeparateId( not_null peer, MsgId topicRootId, Storage::SharedMediaType type) { - if (peer->isSelf()) { + if (peer->isSelf() || !SeparateSupported(type)) { return { nullptr }; } - const auto separateType = ToSeparateType(type); - if (separateType == Window::SeparateSharedMediaType::None) { + const auto topic = topicRootId + ? peer->forumTopicFor(topicRootId) + : nullptr; + if (topicRootId && !topic) { return { nullptr }; } - return { Window::SeparateSharedMedia{ separateType, peer, topicRootId } }; + const auto thread = topic + ? (Data::Thread*)topic + : peer->owner().history(peer); + return { thread, type }; } void AddContextMenuToButton( diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index 5d2af53daa..e01d44f533 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -40,6 +40,28 @@ Type TabIndexToType(int index) { Unexpected("Index in Info::Media::TabIndexToType()"); } +tr::phrase<> SharedMediaTitle(Type type) { + switch (type) { + case Type::Photo: + return tr::lng_media_type_photos; + case Type::GIF: + return tr::lng_media_type_gifs; + case Type::Video: + return tr::lng_media_type_videos; + case Type::MusicFile: + return tr::lng_media_type_songs; + case Type::File: + return tr::lng_media_type_files; + case Type::RoundVoiceFile: + return tr::lng_media_type_audios; + case Type::Link: + return tr::lng_media_type_links; + case Type::RoundFile: + return tr::lng_media_type_rounds; + } + Unexpected("Bad media type in Info::TitleValue()"); +} + Memento::Memento(not_null controller) : Memento( (controller->peer() @@ -119,25 +141,7 @@ rpl::producer Widget::title() { if (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) { return tr::lng_profile_shared_media(); } - switch (controller()->section().mediaType()) { - case Section::MediaType::Photo: - return tr::lng_media_type_photos(); - case Section::MediaType::GIF: - return tr::lng_media_type_gifs(); - case Section::MediaType::Video: - return tr::lng_media_type_videos(); - case Section::MediaType::MusicFile: - return tr::lng_media_type_songs(); - case Section::MediaType::File: - return tr::lng_media_type_files(); - case Section::MediaType::RoundVoiceFile: - return tr::lng_media_type_audios(); - case Section::MediaType::Link: - return tr::lng_media_type_links(); - case Section::MediaType::RoundFile: - return tr::lng_media_type_rounds(); - } - Unexpected("Bad media type in Info::TitleValue()"); + return SharedMediaTitle(controller()->section().mediaType())(); } void Widget::setIsStackBottom(bool isStackBottom) { diff --git a/Telegram/SourceFiles/info/media/info_media_widget.h b/Telegram/SourceFiles/info/media/info_media_widget.h index 693d01aeed..b7c53879b3 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_widget.h @@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_shared_media.h" #include "data/data_search_controller.h" +namespace tr { +template +struct phrase; +} // namespace tr + namespace Data { class ForumTopic; } // namespace Data @@ -19,8 +24,9 @@ namespace Info::Media { using Type = Storage::SharedMediaType; -std::optional TypeToTabIndex(Type type); -Type TabIndexToType(int index); +[[nodiscard]] std::optional TypeToTabIndex(Type type); +[[nodiscard]] Type TabIndexToType(int index); +[[nodiscard]] tr::phrase<> SharedMediaTitle(Type type); class InnerWidget; diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index 9a070889aa..dbd6557888 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -57,7 +57,7 @@ SublistsWidget::SublistsWidget( this, controller->parentController(), rpl::single(Dialogs::InnerWidget::ChildListShown()))); - _list->showSavedSublists(); + _list->showSavedSublists(nullptr); _list->setNarrowRatio(0.); _list->chosenRow() | rpl::start_with_next([=](Dialogs::ChosenRow row) { diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index 81e21fb3c4..8b220f9792 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/platform/ui_platform_window.h" #include "platform/platform_window_title.h" #include "history/history.h" +#include "info/media/info_media_widget.h" // SharedMediaTitle. #include "window/window_separate_id.h" #include "window/window_session_controller.h" #include "window/window_lock_widgets.h" @@ -87,42 +88,24 @@ base::options::toggle OptionDisableTouchbar({ .restartRequired = true, }); -[[nodiscard]] QString TitleFromSeparateId( +[[nodiscard]] QString TitleFromSeparateSharedMedia( const Core::WindowTitleContent &settings, const SeparateId &id) { - if (id.sharedMedia == SeparateSharedMediaType::None - || !id.sharedMediaPeer()) { + if (id.type != SeparateType::SharedMedia) { return QString(); } - const auto result = (id.sharedMedia == SeparateSharedMediaType::Photos) - ? tr::lng_media_type_photos(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Videos) - ? tr::lng_media_type_videos(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Files) - ? tr::lng_media_type_files(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Audio) - ? tr::lng_media_type_songs(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Links) - ? tr::lng_media_type_links(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::GIF) - ? tr::lng_media_type_gifs(tr::now) - : (id.sharedMedia == SeparateSharedMediaType::Voices) - ? tr::lng_media_type_audios(tr::now) - : QString(); - + const auto type = id.sharedMediaType; + const auto result = Info::Media::SharedMediaTitle(type)(tr::now); if (settings.hideChatName) { return result; } - const auto peer = id.sharedMediaPeer(); - const auto topicRootId = id.sharedMediaTopicRootId(); - const auto topic = topicRootId - ? peer->forumTopicFor(topicRootId) - : nullptr; + const auto thread = id.thread; + const auto topic = thread->asTopic(); const auto name = topic ? topic->title() - : peer->isSelf() + : thread->peer()->isSelf() ? tr::lng_saved_messages(tr::now) - : peer->name(); + : thread->peer()->name(); const auto wrapped = st::wrap_rtl(name); return name + u" @ "_q + result; } @@ -902,11 +885,11 @@ void MainWindow::updateTitle() { && Core::App().domain().accountsAuthedCount() > 1) ? st::wrap_rtl(session->authedName()) : QString(); - const auto separateIdTitle = session - ? TitleFromSeparateId(settings, session->windowId()) + const auto separateSharedMediaTitle = session + ? TitleFromSeparateSharedMedia(settings, session->windowId()) : QString(); - if (!separateIdTitle.isEmpty()) { - setTitle(separateIdTitle); + if (!separateSharedMediaTitle.isEmpty()) { + setTitle(separateSharedMediaTitle); return; } const auto key = (session && !settings.hideChatName) diff --git a/Telegram/SourceFiles/window/window_separate_id.cpp b/Telegram/SourceFiles/window/window_separate_id.cpp index 2b88968962..c6b0f0da62 100644 --- a/Telegram/SourceFiles/window/window_separate_id.cpp +++ b/Telegram/SourceFiles/window/window_separate_id.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "window/window_separate_id.h" +#include "data/data_channel.h" #include "data/data_folder.h" #include "data/data_peer.h" #include "data/data_saved_messages.h" @@ -30,10 +31,14 @@ SeparateId::SeparateId(SeparateType type, not_null session) , account(&session->account()) { } -SeparateId::SeparateId(SeparateType type, not_null thread) +SeparateId::SeparateId( + SeparateType type, + not_null thread, + ChannelData *parentChat) : type(type) , account(&thread->session().account()) -, thread(thread) { +, thread(thread) +, parentChat((type == SeparateType::SavedSublist) ? parentChat : nullptr) { } SeparateId::SeparateId(not_null thread) @@ -44,12 +49,13 @@ SeparateId::SeparateId(not_null peer) : SeparateId(SeparateType::Chat, peer->owner().history(peer)) { } -SeparateId::SeparateId(SeparateSharedMedia data) +SeparateId::SeparateId( + not_null thread, + Storage::SharedMediaType sharedMediaType) : type(SeparateType::SharedMedia) -, sharedMedia(data.type) -, account(&data.peer->session().account()) -, sharedMediaDataPeer(data.peer) -, sharedMediaDataTopicRootId(data.topicRootId) { +, sharedMediaType(sharedMediaType) +, account(&thread->session().account()) +, thread(thread) { } bool SeparateId::primary() const { @@ -71,9 +77,12 @@ Data::Folder *SeparateId::folder() const { } Data::SavedSublist *SeparateId::sublist() const { - return (type == SeparateType::SavedSublist) - ? thread->owner().savedMessages().sublist(thread->peer()).get() - : nullptr; + const auto monoforum = parentChat ? parentChat->monoforum() : nullptr; + return (type != SeparateType::SavedSublist) + ? nullptr + : monoforum + ? monoforum->sublist(thread->peer()).get() + : thread->owner().savedMessages().sublist(thread->peer()).get(); } bool SeparateId::hasChatsList() const { @@ -82,16 +91,4 @@ bool SeparateId::hasChatsList() const { || (type == SeparateType::Forum); } -PeerData *SeparateId::sharedMediaPeer() const { - return (type == SeparateType::SharedMedia) - ? sharedMediaDataPeer - : nullptr; -} - -MsgId SeparateId::sharedMediaTopicRootId() const { - return (type == SeparateType::SharedMedia) - ? sharedMediaDataTopicRootId - : MsgId(); -} - } // namespace Window diff --git a/Telegram/SourceFiles/window/window_separate_id.h b/Telegram/SourceFiles/window/window_separate_id.h index 81f417d4fb..e36e8c2a98 100644 --- a/Telegram/SourceFiles/window/window_separate_id.h +++ b/Telegram/SourceFiles/window/window_separate_id.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +class ChannelData; class PeerData; namespace Data { @@ -21,6 +22,10 @@ class Account; class Session; } // namespace Main +namespace Storage { +enum class SharedMediaType : signed char; +} // namespace Storage + namespace Window { enum class SeparateType { @@ -32,39 +37,30 @@ enum class SeparateType { SharedMedia, }; -enum class SeparateSharedMediaType { - None, - Photos, - Videos, - Files, - Audio, - Links, - Voices, - GIF, -}; - struct SeparateSharedMedia { - SeparateSharedMediaType type = SeparateSharedMediaType::None; - not_null peer; - MsgId topicRootId = MsgId(); + not_null thread; + Storage::SharedMediaType type = {}; }; struct SeparateId { SeparateId(std::nullptr_t); SeparateId(not_null account); SeparateId(SeparateType type, not_null session); - SeparateId(SeparateType type, not_null thread); + SeparateId( + SeparateType type, + not_null thread, + ChannelData *parentChat = nullptr); SeparateId(not_null thread); SeparateId(not_null peer); - SeparateId(SeparateSharedMedia data); + SeparateId( + not_null thread, + Storage::SharedMediaType sharedMediaType); SeparateType type = SeparateType::Primary; - SeparateSharedMediaType sharedMedia = SeparateSharedMediaType::None; + Storage::SharedMediaType sharedMediaType = {}; Main::Account *account = nullptr; Data::Thread *thread = nullptr; // For types except Main and Archive. - PeerData *sharedMediaDataPeer = nullptr; - MsgId sharedMediaDataTopicRootId = MsgId(); - + ChannelData *parentChat = nullptr; [[nodiscard]] bool valid() const { return account != nullptr; } @@ -77,8 +73,6 @@ struct SeparateId { [[nodiscard]] Data::Forum *forum() const; [[nodiscard]] Data::Folder *folder() const; [[nodiscard]] Data::SavedSublist *sublist() const; - [[nodiscard]] PeerData *sharedMediaPeer() const; - [[nodiscard]] MsgId sharedMediaTopicRootId() const; [[nodiscard]] bool hasChatsList() const; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 4f474261b7..6edfaf3da0 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1321,35 +1321,13 @@ void SessionNavigation::showByInitialId( showThread(id.thread, msgId, instant); break; case SeparateType::SharedMedia: { - Assert(id.sharedMedia != SeparateSharedMediaType::None); clearSectionStack(instant); - const auto type = (id.sharedMedia == SeparateSharedMediaType::Photos) - ? Storage::SharedMediaType::Photo - : (id.sharedMedia == SeparateSharedMediaType::Videos) - ? Storage::SharedMediaType::Video - : (id.sharedMedia == SeparateSharedMediaType::Files) - ? Storage::SharedMediaType::File - : (id.sharedMedia == SeparateSharedMediaType::Audio) - ? Storage::SharedMediaType::MusicFile - : (id.sharedMedia == SeparateSharedMediaType::Links) - ? Storage::SharedMediaType::Link - : (id.sharedMedia == SeparateSharedMediaType::Voices) - ? Storage::SharedMediaType::RoundVoiceFile - : (id.sharedMedia == SeparateSharedMediaType::GIF) - ? Storage::SharedMediaType::GIF - : Storage::SharedMediaType::Photo; - const auto topicRootId = id.sharedMediaTopicRootId(); - const auto peer = id.sharedMediaPeer(); - const auto topic = topicRootId - ? peer->forumTopicFor(topicRootId) - : nullptr; - if (topicRootId && !topic) { - break; - } + const auto type = id.sharedMediaType; + const auto topic = id.thread->asTopic(); showSection( - topicRootId + (topic ? std::make_shared(topic, type) - : std::make_shared(peer, type), + : std::make_shared(id.thread->peer(), type)), instant); parent->widget()->setMaximumWidth(st::maxWidthSharedMediaWindow); break; From 51878ab38e4724b715cf92563875fe591fcb613e Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 6 May 2025 21:22:36 +0400 Subject: [PATCH 053/310] Allow opening monoforums. --- Telegram/SourceFiles/data/data_channel.cpp | 10 +- .../SourceFiles/data/data_saved_messages.cpp | 24 ++++ .../SourceFiles/data/data_saved_messages.h | 8 ++ .../dialogs/dialogs_inner_widget.cpp | 41 ++++++ .../dialogs/dialogs_inner_widget.h | 1 + .../SourceFiles/dialogs/dialogs_main_list.cpp | 2 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 130 ++++++++++++++++-- Telegram/SourceFiles/dialogs/dialogs_widget.h | 16 ++- .../view/history_view_top_bar_widget.cpp | 4 + .../info/profile/info_profile_actions.cpp | 17 +++ Telegram/SourceFiles/mainwidget.cpp | 12 ++ Telegram/SourceFiles/mainwidget.h | 4 + .../window/window_session_controller.cpp | 59 +++++++- .../window/window_session_controller.h | 9 ++ 14 files changed, 317 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 817e59fb97..0159fcca80 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -199,12 +199,18 @@ void ChannelData::setFlags(ChannelDataFlags which) { // Let Data::Forum live till the end of _flags.set. // That way the data can be used in changes handler. // Example: render frame for forum auto-closing animation. - const auto taken = ((diff & Flag::Forum) && !(which & Flag::Forum)) + const auto takenForum = ((diff & Flag::Forum) && !(which & Flag::Forum)) ? mgInfo->takeForumData() : nullptr; + const auto takenMonoforum = ((diff & Flag::Monoforum) + && !(which & Flag::Monoforum)) + ? mgInfo->takeMonoforumData() + : nullptr; const auto wasIn = amIn(); if ((diff & Flag::Forum) && (which & Flag::Forum)) { mgInfo->ensureForum(this); + } else if ((diff & Flag::Monoforum) && (which & Flag::Monoforum)) { + mgInfo->ensureMonoforum(this); } _flags.set(which); if (diff & (Flag::Left | Flag::Forbidden)) { @@ -252,7 +258,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { } } } - if (const auto raw = taken.get()) { + if (const auto raw = takenForum.get()) { owner().forumIcons().clearUserpicsReset(raw); } } diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 6a401afd17..74a597b287 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -74,6 +74,10 @@ rpl::producer<> SavedMessages::chatsListChanges() const { return _chatsListChanges.events(); } +rpl::producer<> SavedMessages::chatsListLoadedEvents() const { + return _chatsListLoadedEvents.events(); +} + void SavedMessages::loadMore() { _loadMoreScheduled = true; _loadMore.call(); @@ -104,6 +108,9 @@ void SavedMessages::sendLoadMore() { ).done([=](const MTPmessages_SavedDialogs &result) { apply(result, false); _chatsListChanges.fire({}); + if (_chatsList.loaded()) { + _chatsListLoadedEvents.fire({}); + } }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { _unsupported = true; @@ -321,6 +328,23 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { }); } +rpl::producer<> SavedMessages::destroyed() const { + if (!_parentChat) { + return rpl::never<>(); + } + return _parentChat->flagsValue( + ) | rpl::filter([=](const ChannelData::Flags::Change &update) { + using Flag = ChannelData::Flag; + return (update.diff & Flag::Monoforum) + && !(update.value & Flag::Monoforum); + }) | rpl::take(1) | rpl::to_empty; +} + +auto SavedMessages::sublistDestroyed() const +-> rpl::producer> { + return _sublistDestroyed.events(); +} + rpl::lifetime &SavedMessages::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 3ef9aae9c0..6d6bbe3236 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -35,6 +35,11 @@ public: [[nodiscard]] not_null sublist(not_null peer); [[nodiscard]] rpl::producer<> chatsListChanges() const; + [[nodiscard]] rpl::producer<> chatsListLoadedEvents() const; + + [[nodiscard]] rpl::producer<> destroyed() const; + [[nodiscard]] auto sublistDestroyed() const + -> rpl::producer>; void loadMore(); void loadMore(not_null sublist); @@ -55,6 +60,8 @@ private: const not_null _owner; ChannelData *_parentChat = nullptr; + rpl::event_stream> _sublistDestroyed; + Dialogs::MainList _chatsList; base::flat_map< not_null, @@ -73,6 +80,7 @@ private: bool _loadMoreScheduled = false; rpl::event_stream<> _chatsListChanges; + rpl::event_stream<> _chatsListLoadedEvents; bool _pinnedLoaded = false; bool _unsupported = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 4b21f5c6e9..79996bd7b6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -781,6 +781,47 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) { } } +void InnerWidget::changeOpenedMonoforum(Data::SavedMessages *monoforum) { + if (_savedSublists == monoforum) { + return; + } + stopReorderPinned(); + clearSelection(); + + if (monoforum) { + saveChatsFilterScrollState(_filterId); + } + _filterId = monoforum + ? 0 + : _controller->activeChatsFilterCurrent(); + if (_openedForum) { + // If we close it inside forum destruction we should not schedule. + session().data().forumIcons().scheduleUserpicsReset(_openedForum); + } + _savedSublists = monoforum; + _st = &st::defaultDialogRow; + refreshShownList(); + + _openedForumLifetime.destroy(); + if (monoforum) { + rpl::merge( + monoforum->chatsListChanges(), + monoforum->chatsListLoadedEvents() + ) | rpl::start_with_next([=] { + refresh(); + }, _openedForumLifetime); + } + + refreshWithCollapsedRows(true); + if (_loadMoreCallback) { + _loadMoreCallback(); + } + + if (!monoforum) { + restoreChatsFilterScrollState(_filterId); + } +} + void InnerWidget::showSavedSublists(ChannelData *parentChat) { Expects(!parentChat || parentChat->monoforum()); Expects(!_geometryInited); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index bc6b54e832..f840a99b43 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -141,6 +141,7 @@ public: void changeOpenedFolder(Data::Folder *folder); void changeOpenedForum(Data::Forum *forum); + void changeOpenedMonoforum(Data::SavedMessages *monoforum); void showSavedSublists(ChannelData *parentChat); void selectSkip(int32 direction); void selectSkipPage(int32 pixels, int32 direction); diff --git a/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp b/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp index 9c9ad67bae..5e525b5a2c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_main_list.cpp @@ -28,7 +28,7 @@ MainList::MainList( std::move( pinnedLimit ) | rpl::start_with_next([=](int limit) { - _pinned.setLimit(limit); + _pinned.setLimit(std::max(limit, 1)); }, _lifetime); session->changes().realtimeNameUpdates( diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 4223f86538..cc817e3011 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_contact_status.h" #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_group_call_bar.h" +#include "history/view/history_view_sublist_section.h" #include "boxes/peers/edit_peer_requests_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -78,6 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_download_manager.h" #include "data/data_chat_filters.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" @@ -418,6 +420,8 @@ Widget::Widget( ) | rpl::filter([=](const Data::HistoryUpdate &update) { if (_openedForum) { return (update.history == _openedForum->history()); + } else if (_openedMonoforum) { + return (update.history->peer == _openedMonoforum->parentChat()); } else if (_openedFolder) { return (update.history->folder() == _openedFolder) && !update.history->isPinnedDialog(FilterId()); @@ -596,6 +600,7 @@ Widget::Widget( _search->setFocusFast(); if (_childList) { controller->closeForum(); + controller->closeMonoforum(); } }); @@ -618,6 +623,8 @@ Widget::Widget( searchMore(); } else if (_openedForum && state == WidgetState::Default) { _openedForum->requestTopics(); + } else if (_openedMonoforum && state == WidgetState::Default) { + _openedMonoforum->loadMore(); } else { const auto folder = _inner->shownFolder(); if (!folder || !folder->chatsList()->loaded()) { @@ -666,7 +673,16 @@ Widget::Widget( ) | rpl::filter(!rpl::mappers::_1) | rpl::start_with_next([=] { if (_openedForum) { changeOpenedForum(nullptr, anim::type::normal); - } else if (_childList) { + } else if (_childList && !_childList->openedMonoforum()) { + closeChildList(anim::type::normal); + } + }, lifetime()); + + controller->shownMonoforum().changes( + ) | rpl::filter(!rpl::mappers::_1) | rpl::start_with_next([=] { + if (_openedMonoforum) { + changeOpenedMonoforum(nullptr, anim::type::normal); + } else if (_childList && !_childList->openedForum()) { closeChildList(anim::type::normal); } }, lifetime()); @@ -795,7 +811,7 @@ void Widget::setupSwipeBack() { } }); } - if (controller()->shownForum().current()) { + if (controller()->shownForum().current()) { // #TODO monoforum if (!isRightToLeft) { return Ui::Controls::SwipeHandlerFinishData(); } @@ -924,6 +940,26 @@ void Widget::chosenRow(const ChosenRow &row) { } } return; + } else if (history + && history->isMonoforum() + && !row.message.fullId + && !controller()->adaptive().isOneColumn()) { + const auto monoforum = history->peer->monoforum(); + if (controller()->shownMonoforum().current() == monoforum) { + controller()->closeMonoforum(); + //} else if (row.newWindow) { // #TODO monoforum + // controller()->showInNewWindow( + // Window::SeparateId(Window::SeparateType::Forum, history)); + } else { + controller()->showMonoforum( + monoforum, + Window::SectionShow().withChildColumn()); + controller()->showThread( + history, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } + return; } else if (history) { const auto peer = history->peer; const auto showAtMsgId = controller()->uniqueChatsInSearchResults() @@ -958,6 +994,13 @@ void Widget::chosenRow(const ChosenRow &row) { } controller()->openFolder(folder); hideChildList(); + } else if (const auto sublist = row.key.sublist()) { + using namespace Window; + auto params = SectionShow(SectionShow::Way::Forward); + params.dropSameFromStack = true; + controller()->showSection( + std::make_shared(sublist), + params); } if (row.filteredRow && !session().supportMode()) { if (_subsectionTopBar) { @@ -1032,7 +1075,8 @@ void Widget::setupTopBarSuggestions(not_null dialogs) { ) | rpl::filter(_1 == nullptr) | rpl::map([=] { auto on = rpl::combine( controller()->activeChatsFilter(), - _openedFolderOrForumChanges.events_starting_with(false), + _openedFolderOrForumOrMonoforumChanges.events_starting_with( + false), widthValue() | rpl::map( _1 >= st::columnMinimalWidthLeft ) | rpl::distinct_until_changed(), @@ -1041,11 +1085,11 @@ void Widget::setupTopBarSuggestions(not_null dialogs) { _jumpToDate->toggledValue() ) | rpl::map([=]( FilterId id, - bool folderOrForum, + bool folderOrForumOrMonoforum, bool wide, bool search, bool searchInPeer) { - return !folderOrForum + return !folderOrForumOrMonoforum && wide && !search && !searchInPeer @@ -1102,7 +1146,8 @@ void Widget::updateFrozenAccountBar() { void Widget::updateTopBarSuggestions() { if (_topBarSuggestion) { - _openedFolderOrForumChanges.fire(_openedForum || _openedFolder); + _openedFolderOrForumOrMonoforumChanges.fire( + _openedFolder || _openedForum || _openedMonoforum); } } @@ -1540,7 +1585,7 @@ void Widget::updateControlsVisibility(bool fast) { if (_chatFilters) { _chatFilters->setVisible(!_openedForum); } - if (_openedFolder || _openedForum) { + if (_openedFolder || _openedForum || _openedMonoforum) { _subsectionTopBar->show(); if (_forumTopShadow) { _forumTopShadow->show(); @@ -1919,6 +1964,29 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { }, (forum != nullptr), animated); } +void Widget::changeOpenedMonoforum( + Data::SavedMessages *monoforum, + anim::type animated) { + if (_openedMonoforum == monoforum) { + return; + } + changeOpenedSubsection([&] { + cancelSearch({ .forceFullCancel = true }); + closeChildList(anim::type::instant); + _openedMonoforum = monoforum; + _searchState.tab = monoforum + ? ChatSearchTab::ThisPeer + : ChatSearchTab::MyMessages; + _searchWithPostsPreview = computeSearchWithPostsPreview(); + _api.request(base::take(_topicSearchRequest)).cancel(); + _inner->changeOpenedMonoforum(monoforum); + storiesToggleExplicitExpand(false); + updateFrozenAccountBar(); + updateTopBarSuggestions(); + updateStoriesVisibility(); + }, (monoforum != nullptr), animated); +} + void Widget::hideChildList() { if (_childList) { controller()->closeForum(); @@ -1926,7 +1994,7 @@ void Widget::hideChildList() { } void Widget::refreshTopBars() { - if (_openedFolder || _openedForum) { + if (_openedFolder || _openedForum || _openedMonoforum) { if (!_subsectionTopBar) { _subsectionTopBar.create(this, controller()); if (_stories) { @@ -1956,10 +2024,12 @@ void Widget::refreshTopBars() { } const auto history = _openedForum ? _openedForum->history().get() + : _openedMonoforum + ? session().data().history(_openedMonoforum->parentChat()).get() : nullptr; _subsectionTopBar->setActiveChat( HistoryView::TopBarWidget::ActiveChat{ - .key = (_openedForum + .key = ((_openedForum || _openedMonoforum) ? Dialogs::Key(history) : Dialogs::Key(_openedFolder)), .section = Dialogs::EntryState::Section::ChatsList, @@ -2121,6 +2191,14 @@ bool Widget::searchHasFocus() const { return _searchHasFocus; } +Data::Forum *Widget::openedForum() const { + return _openedForum; +} + +Data::SavedMessages *Widget::openedMonoforum() const { + return _openedMonoforum; +} + void Widget::jumpToTop(bool belowPinned) { if (session().supportMode()) { return; @@ -3283,12 +3361,33 @@ void Widget::showForum( return; } cancelSearch({ .forceFullCancel = true }); - openChildList(forum, params); + openChildList(forum, nullptr, params); +} + +void Widget::showMonoforum( + not_null monoforum, + const Window::SectionShow ¶ms) { + if (_openedMonoforum == monoforum) { + return; + } + const auto nochat = !controller()->mainSectionShown(); + if (!params.childColumn + || (Core::App().settings().dialogsWidthRatio(nochat) == 0.) + || (_layout != Layout::Main) + || OptionForumHideChatsList.value()) { + changeOpenedMonoforum(monoforum, params.animated); + return; + } + cancelSearch({ .forceFullCancel = true }); + openChildList(nullptr, monoforum, params); } void Widget::openChildList( - not_null forum, + Data::Forum *forum, + Data::SavedMessages *monoforum, const Window::SectionShow ¶ms) { + Expects(forum || monoforum); + auto slide = Window::SectionSlideParams(); const auto animated = !_childList && (params.animated == anim::type::normal); @@ -3309,8 +3408,13 @@ void Widget::openChildList( this, controller(), Layout::Child); - _childList->showForum(forum, copy); - _childListPeerId = forum->channel()->id; + if (forum) { + _childList->showForum(forum, copy); + _childListPeerId = forum->channel()->id; + } else { + _childList->showMonoforum(monoforum, copy); + _childListPeerId = monoforum->parentChat()->id; + } } _childListShadow = std::make_unique(this); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index a38ce878f8..b2584462f2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -23,6 +23,7 @@ class Error; namespace Data { class Forum; +class SavedMessages; enum class StorySourcesList : uchar; struct ReactionId; } // namespace Data @@ -108,9 +109,15 @@ public: void showForum( not_null forum, const Window::SectionShow ¶ms); + void showMonoforum( + not_null monoforum, + const Window::SectionShow ¶ms); void setInnerFocus(bool unfocusSearch = false); [[nodiscard]] bool searchHasFocus() const; + [[nodiscard]] Data::Forum *openedForum() const; + [[nodiscard]] Data::SavedMessages *openedMonoforum() const; + void jumpToTop(bool belowPinned = false); void raiseWithTooltip(); @@ -247,6 +254,9 @@ private: anim::type animated); void changeOpenedFolder(Data::Folder *folder, anim::type animated); void changeOpenedForum(Data::Forum *forum, anim::type animated); + void changeOpenedMonoforum( + Data::SavedMessages *monoforum, + anim::type animated); void hideChildList(); void destroyChildListCanvas(); [[nodiscard]] QPixmap grabForFolderSlideAnimation(); @@ -256,7 +266,8 @@ private: Window::SlideDirection direction); void openChildList( - not_null forum, + Data::Forum *forum, + Data::SavedMessages *monoforum, const Window::SectionShow ¶ms); void closeChildList(anim::type animated); @@ -332,7 +343,7 @@ private: Ui::SlideWrap *_topBarSuggestion = nullptr; rpl::event_stream _topBarSuggestionHeightChanged; rpl::event_stream _searchStateForTopBarSuggestion; - rpl::event_stream _openedFolderOrForumChanges; + rpl::event_stream _openedFolderOrForumOrMonoforumChanges; object_ptr _scroll; QPointer _inner; @@ -358,6 +369,7 @@ private: Data::Folder *_openedFolder = nullptr; Data::Forum *_openedForum = nullptr; + Data::SavedMessages *_openedMonoforum = nullptr; SearchState _searchState; History *_searchInMigrated = nullptr; rpl::lifetime _searchTagsLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index c4548ee4bf..c5c3a6187a 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -762,6 +762,10 @@ void TopBarWidget::backClicked() { && _activeChat.key.history() && _activeChat.key.history()->isForum()) { _controller->closeForum(); + } else if (_activeChat.section == Section::ChatsList + && _activeChat.key.history() + && _activeChat.key.history()->isMonoforum()) { + _controller->closeMonoforum(); } else { _controller->showBackFromStack(); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index e38d772d83..bc7de9ef5c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -2183,6 +2183,23 @@ Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( std::move(viewChannel), tracker); + auto viewDirectVisible = channel->flagsValue() | rpl::map([=] { + return channel->monoforumLink() != nullptr; + }) | rpl::distinct_until_changed(); + auto viewDirect = [=] { + if (const auto linked = channel->monoforumLink()) { + if (const auto monoforum = linked->monoforum()) { + window->showMonoforum(monoforum); + } + } + }; + AddMainButton( // #TODO monoforum + _wrap, + rpl::single(u"View Direct Messages"_q), + std::move(viewDirectVisible), + std::move(viewDirect), + tracker); + return tracker; } diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 2a458c24e9..d3cbed09a0 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1565,6 +1565,18 @@ void MainWidget::showForum( } } +void MainWidget::showMonoforum( + not_null monoforum, + const SectionShow ¶ms) { + Expects(_dialogs != nullptr); + + _dialogs->showMonoforum(monoforum, params); + + if (params.activation != anim::activation::background) { + _controller->window().hideSettingsAndLayer(); + } +} + PeerData *MainWidget::peer() const { return _history->peer(); } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 48ec64735b..54bb67513c 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -32,6 +32,7 @@ class Thread; class WallPaper; struct ForwardDraft; class Forum; +class SavedMessages; struct ReportInput; } // namespace Data @@ -198,6 +199,9 @@ public: not_null item, const SectionShow ¶ms); void showForum(not_null forum, const SectionShow ¶ms); + void showMonoforum( + not_null monoforum, + const SectionShow ¶ms); bool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 6edfaf3da0..2d9af18ff0 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -603,10 +603,15 @@ void SessionNavigation::showPeerByLinkResolved( showPeerInfo(peer, params); } else if (resolveType == ResolveType::HashtagSearch) { searchMessages(info.text, peer->owner().history(peer)); - } else if (peer->isForum() && resolveType != ResolveType::Boost) { + } else if ((peer->isForum() || peer->isMonoforum()) + && resolveType != ResolveType::Boost) { const auto itemId = info.messageId; if (!itemId) { - parentController()->showForum(peer->forum(), params); + if (peer->isForum()) { + parentController()->showForum(peer->forum(), params); + } else { + parentController()->showMonoforum(peer->monoforum(), params); + } } else if (const auto item = peer->owner().message(peer, itemId)) { showMessageByLinkResolved(item, info); } else { @@ -1924,6 +1929,56 @@ const rpl::variable &SessionController::shownForum() const { return _shownForum; } +void SessionController::showMonoforum( + not_null monoforum, + const SectionShow ¶ms) { + // if (showForumInDifferentWindow(forum, params)) { + // return; + // } + _shownMonoforumLifetime.destroy(); + if (_shownMonoforum.current() != monoforum) { + resetFakeUnreadWhileOpened(); + } + if (monoforum + && _activeChatEntry.current().key.peer() + && adaptive().isOneColumn()) { + clearSectionStack(params); + } + _shownMonoforum = monoforum.get(); + if (_shownMonoforum.current() != monoforum) { + return; + } + monoforum->destroyed( + ) | rpl::start_with_next([=] { + closeMonoforum(); + }, _shownMonoforumLifetime); + content()->showMonoforum(monoforum, params); +} + +void SessionController::closeMonoforum() { + if (const auto monoforum = _shownMonoforum.current()) { + const auto id = windowId(); + if (id.type == SeparateType::SavedSublist) { + const auto initial = id.parentChat; + if (!initial + || !initial->monoforum() + || initial == monoforum->parentChat()) { + Core::App().closeWindow(_window); + } else { + showMonoforum(initial->monoforum()); + } + return; + } + } + _shownMonoforumLifetime.destroy(); + _shownMonoforum = nullptr; +} + +auto SessionController::shownMonoforum() const +-> const rpl::variable & { + return _shownMonoforum; +} + void SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) { if (windowId().type == SeparateType::SharedMedia) { return; diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 040739589d..7eea85827a 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -26,6 +26,7 @@ enum class WindowLayout; namespace Data { struct StoriesContext; +class SavedMessages; enum class StorySourcesList : uchar; } // namespace Data @@ -405,6 +406,12 @@ public: void closeForum(); const rpl::variable &shownForum() const; + void showMonoforum( + not_null monoforum, + const SectionShow ¶ms = SectionShow::Way::ClearStack); + void closeMonoforum(); + const rpl::variable &shownMonoforum() const; + void setActiveChatEntry(Dialogs::RowDescriptor row); void setActiveChatEntry(Dialogs::Key key); Dialogs::RowDescriptor activeChatEntryCurrent() const; @@ -746,6 +753,8 @@ private: rpl::variable _openedFolder; rpl::variable _shownForum; rpl::lifetime _shownForumLifetime; + rpl::variable _shownMonoforum; + rpl::lifetime _shownMonoforumLifetime; rpl::event_stream<> _filtersMenuChanged; From f8913bf9b9282536c855c8fcf69a8620891c57c6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 9 May 2025 13:46:03 +0400 Subject: [PATCH 054/310] Show square userpics for monoforums. --- Telegram/SourceFiles/data/data_peer.cpp | 2 +- Telegram/SourceFiles/ui/controls/userpic_button.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 6a480a075a..851fd9a0c4 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -438,7 +438,7 @@ void PeerData::paintUserpic( cloud, cloud ? nullptr : ensureEmptyUserpic().get(), size * ratio, - !forceCircle && isForum()); + !forceCircle && (isForum() || isMonoforum())); p.drawImage(QRect(x, y, size, size), view.cached); } diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 39faf9ca80..9c4bb61a35 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -888,7 +888,9 @@ void UserpicButton::processNewPeerPhoto() { } bool UserpicButton::useForumShape() const { - return _forceForumShape || (_peer && _peer->isForum()); + return _forceForumShape + || (_peer && _peer->isForum()) + || (_peer && _peer->isMonoforum()); } void UserpicButton::grabOldUserpic() { From abcf7e3a47f2d200678c1e11c78c2b1867e1e07d Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 9 May 2025 13:46:14 +0400 Subject: [PATCH 055/310] Update API scheme & fix monoforum send. --- Telegram/SourceFiles/data/data_channel.cpp | 6 ++++ Telegram/SourceFiles/data/data_channel.h | 1 + Telegram/SourceFiles/data/data_histories.cpp | 33 +++++++++++++++---- Telegram/SourceFiles/data/data_msg_id.h | 3 +- .../SourceFiles/data/data_saved_messages.cpp | 11 +++++-- Telegram/SourceFiles/data/data_session.cpp | 7 ++-- .../SourceFiles/dialogs/dialogs_widget.cpp | 9 +++++ .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 6 ++-- Telegram/SourceFiles/history/history.cpp | 6 ++++ Telegram/SourceFiles/history/history_item.cpp | 23 +++++++++++-- .../history/history_item_components.cpp | 7 ++++ .../history/history_item_components.h | 1 + .../view/history_view_top_bar_widget.cpp | 8 +++-- Telegram/SourceFiles/mtproto/scheme/api.tl | 3 +- 14 files changed, 102 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 0159fcca80..fc0842c896 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -341,6 +341,12 @@ ChannelData *ChannelData::monoforumLink() const { return _monoforumLink; } +bool ChannelData::requiresMonoforumPeer() const { + return isMonoforum() + && _monoforumLink + && (_monoforumLink->amCreator() || _monoforumLink->hasAdminRights()); +} + void ChannelData::setMembersCount(int newMembersCount) { if (_membersCount != newMembersCount) { if (isMegagroup() diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 6dc802dcfc..9b3150de8f 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -429,6 +429,7 @@ public: void setMonoforumLink(ChannelData *link); [[nodiscard]] ChannelData *monoforumLink() const; + [[nodiscard]] bool requiresMonoforumPeer() const; void ptsInit(int32 pts) { _ptsWaiter.init(pts); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index e9b0b02575..e7c5a14da4 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -60,6 +60,15 @@ MTPInputReplyTo ReplyToForMTP( && (to->history() != history || to->id != replyingToTopicId)) ? to->topicRootId() : replyingToTopicId; + const auto possibleMonoforumPeer = (to && to->savedSublistPeer()) + ? to->savedSublistPeer() + : replyTo.monoforumPeerId + ? history->owner().peer(replyTo.monoforumPeerId).get() + : history->session().user().get(); + const auto replyToMonoforumPeer = (history->peer->isChannel() + && history->peer->asChannel()->requiresMonoforumPeer()) + ? possibleMonoforumPeer + : nullptr; const auto external = replyTo.messageId && (replyTo.messageId.peer != history->peer->id || replyingToTopicId != replyToTopicId); @@ -74,6 +83,7 @@ MTPInputReplyTo ReplyToForMTP( | (replyTo.quote.text.isEmpty() ? Flag() : (Flag::f_quote_text | Flag::f_quote_offset)) + | (replyToMonoforumPeer ? Flag::f_monoforum_peer_id : Flag()) | (quoteEntities.v.isEmpty() ? Flag() : Flag::f_quote_entities)), @@ -84,7 +94,17 @@ MTPInputReplyTo ReplyToForMTP( : MTPInputPeer()), MTP_string(replyTo.quote.text), quoteEntities, - MTP_int(replyTo.quoteOffset)); + MTP_int(replyTo.quoteOffset), + (replyToMonoforumPeer + ? replyToMonoforumPeer->input + : MTPInputPeer())); + } else if (history->peer->isChannel() + && history->peer->asChannel()->requiresMonoforumPeer() + && replyTo.monoforumPeerId) { + const auto replyToMonoforumPeer = replyTo.monoforumPeerId + ? history->owner().peer(replyTo.monoforumPeerId) + : history->session().user(); + return MTP_inputReplyToMonoForum(replyToMonoforumPeer->input); } return MTPInputReplyTo(); } @@ -1054,13 +1074,12 @@ int Histories::sendPreparedMessage( _creatingTopicRequests.emplace(id); return id; } - const auto realReplyTo = FullReplyTo{ - .messageId = convertTopicReplyToId(history, replyTo.messageId), - .quote = replyTo.quote, - .storyId = replyTo.storyId, - .topicRootId = convertTopicReplyToId(history, replyTo.topicRootId), - .quoteOffset = replyTo.quoteOffset, + auto realReplyTo = replyTo; + const auto topicReplyToId = [&](const auto &id) { + return convertTopicReplyToId(history, id); }; + realReplyTo.messageId = topicReplyToId(replyTo.messageId); + realReplyTo.topicRootId = topicReplyToId(replyTo.topicRootId); return v::match(message(history, realReplyTo), [&](const auto &request) { const auto type = RequestType::Send; return sendRequest(history, type, [=](Fn finish) { diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 48c57091b1..6a94211cbf 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -177,10 +177,11 @@ struct FullReplyTo { TextWithEntities quote; FullStoryId storyId; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; int quoteOffset = 0; [[nodiscard]] bool valid() const { - return messageId || (storyId && storyId.peer); + return messageId || (storyId && storyId.peer) || monoforumPeerId; } explicit operator bool() const { return valid(); diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 74a597b287..72f5aca91f 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -163,8 +163,15 @@ void SavedMessages::sendLoadMore(not_null sublist) { ).done([=](const MTPmessages_Messages &result) { auto count = 0; auto list = (const QVector*)nullptr; - result.match([](const MTPDmessages_channelMessages &) { - LOG(("API Error: messages.channelMessages in sublist.")); + result.match([&](const MTPDmessages_channelMessages &data) { + if (const auto channel = _parentChat) { + channel->ptsReceived(data.vpts().v); + channel->processTopics(data.vtopics()); + list = &data.vmessages().v; + count = data.vcount().v; + } else { + LOG(("API Error: messages.channelMessages in sublist.")); + } }, [](const MTPDmessages_messagesNotModified &) { LOG(("API Error: messages.messagesNotModified in sublist.")); }, [&](const auto &data) { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 71368f588a..de96c6c58a 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -372,18 +372,19 @@ void Session::clear() { // Optimization: clear notifications before destroying items. Core::App().notifications().clearFromSession(_session); - // We must clear all forums before clearing customEmojiManager. + // We must clear all [mono]forums before clearing customEmojiManager. // Because in Data::ForumTopic an Ui::Text::CustomEmoji is cached. auto forums = base::flat_set>(); for (const auto &[peerId, peer] : _peers) { if (const auto channel = peer->asChannel()) { - if (channel->isForum()) { + if (channel->isForum() || channel->isMonoforum()) { forums.emplace(channel); } } } for (const auto &channel : forums) { - channel->setFlags(channel->flags() & ~ChannelDataFlag::Forum); + channel->setFlags(channel->flags() + & ~(ChannelDataFlag::Forum | ChannelDataFlag::Monoforum)); } _sendActionManager->clear(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index cc817e3011..31dd678231 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -2497,6 +2497,15 @@ void Widget::escape() { } else if (initial != forum) { controller()->showForum(initial); } + } else if (const auto monoforum + = controller()->shownMonoforum().current()) { + const auto id = controller()->windowId(); // #TODO monoforum + const auto initial = (Data::SavedMessages*)nullptr; + if (!initial) { + controller()->closeMonoforum(); + } else if (initial != monoforum) { + controller()->showMonoforum(initial); + } } else if (controller()->openedFolder().current()) { if (!controller()->windowId().folder()) { controller()->closeFolder(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 9d312d44c9..dfdb7891d3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -458,7 +458,9 @@ void PaintRow( const auto promoted = (history && history->useTopPromotion()) && !context.search; - const auto verifyInfo = (from && !from->isSelf()) + const auto verifyInfo = (from + && (!from->isSelf() + || (!(flags & Flag::SavedMessages) && !(flags & Flag::MyNotes)))) ? from->botVerifyDetails() : nullptr; if (promoted) { @@ -996,7 +998,7 @@ void RowPainter::Paint( : nullptr; const auto allowUserOnline = true;// !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) - | ((sublist && from->isSelf()) + | ((sublist && !sublist->parentChat() && from->isSelf()) ? Flag::MyNotes : (peer && peer->isSelf()) ? Flag::SavedMessages diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 98a7b830d8..641adcae73 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2241,6 +2241,12 @@ Dialogs::BadgesState History::chatListBadgesState() const { forum->topicsList()->unreadState(), Dialogs::CountInBadge::Chats, Dialogs::IncludeInBadge::UnmutedOrAll)); + } else if (const auto monoforum = peer->monoforum()) { + return adjustBadgesStateByFolder( + Dialogs::BadgesForUnread( + monoforum->chatsList()->unreadState(), + Dialogs::CountInBadge::Chats, + Dialogs::IncludeInBadge::UnmutedOrAll)); } return computeBadgesState(); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 275edd80f8..2f9bf79236 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3435,8 +3435,12 @@ FullStoryId HistoryItem::replyToStory() const { } FullReplyTo HistoryItem::replyTo() const { + const auto monoforumPeer = _history->peer->isMonoforum() + ? savedSublistPeer() + : nullptr; auto result = FullReplyTo{ .topicRootId = topicRootId(), + .monoforumPeerId = monoforumPeer ? monoforumPeer->id : PeerId(), }; if (const auto reply = Get()) { const auto &fields = reply->fields(); @@ -3564,7 +3568,7 @@ Data::SavedSublist *HistoryItem::savedSublist() const { that->Get()->sublist = sublist; return sublist; } else if (const auto monoforum = _history->peer->monoforum()) { - const auto sublist = monoforum->sublist(_history->peer); + const auto sublist = monoforum->sublist(_from); const auto that = const_cast(this); that->AddComponents(HistoryMessageSaved::Bit()); that->Get()->sublist = sublist; @@ -3766,7 +3770,11 @@ void HistoryItem::createComponents(CreateConfig &&config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } - if (_history->peer->isSelf()) { + const auto requiresMonoforumPeer = _history->peer->isChannel() + && _history->peer->asChannel()->requiresMonoforumPeer(); + if (_history->peer->isSelf() + || config.savedSublistPeer + || requiresMonoforumPeer) { mask |= HistoryMessageSaved::Bit(); } if (!config.restrictions.empty()) { @@ -3780,7 +3788,11 @@ void HistoryItem::createComponents(CreateConfig &&config) { if (const auto saved = Get()) { if (!config.savedSublistPeer) { - if (config.savedFromPeer) { + if (config.reply.monoforumPeerId) { + config.savedSublistPeer = config.reply.monoforumPeerId; + } else if (!_history->peer->isSelf()) { + config.savedSublistPeer = _from->id; + } else if (config.savedFromPeer) { config.savedSublistPeer = config.savedFromPeer; } else if (config.originalSenderId) { config.savedSublistPeer = config.originalSenderId; @@ -4023,6 +4035,11 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { ? replyTo.messageId.peer : PeerId(); const auto to = LookupReplyTo(_history, replyTo.messageId); + config.reply.monoforumPeerId = (to && to->savedSublistPeer()) + ? to->savedSublistPeer()->id + : replyTo.monoforumPeerId + ? replyTo.monoforumPeerId + : PeerId(); const auto replyToTop = replyTo.topicRootId ? replyTo.topicRootId : LookupReplyToTop(_history, to); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 9b3f23fb17..28eea2027d 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -417,6 +417,13 @@ FullReplyTo ReplyToFromMTP( }; } return FullReplyTo(); + }, [&](const MTPDinputReplyToMonoForum &data) { + const auto parsed = Data::PeerFromInputMTP( + &history->owner(), + data.vmonoforum_peer_id()); + return FullReplyTo{ + .monoforumPeerId = parsed ? parsed->id : PeerId(), + }; }); } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 331ee24dfe..57dbeba73f 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -256,6 +256,7 @@ struct ReplyFields { QString externalSenderName; QString externalPostAuthor; PeerId externalPeerId = 0; + PeerId monoforumPeerId = 0; MsgId messageId = 0; MsgId topMessageId = 0; StoryId storyId = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index c5c3a6187a..568d747b95 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -79,8 +79,10 @@ constexpr auto kEmojiInteractionSeenDuration = 3 * crl::time(1000); QString TopBarNameText( not_null peer, - Dialogs::EntryState::Section section) { - if (section == Dialogs::EntryState::Section::SavedSublist) { + const Dialogs::EntryState &state) { + if (state.section == Dialogs::EntryState::Section::SavedSublist + && state.key.history() + && state.key.history()->peer->isSelf()) { if (peer->isSelf()) { return tr::lng_my_notes(tr::now); } else if (peer->isSavedHiddenAuthor()) { @@ -567,7 +569,7 @@ void TopBarWidget::paintTopBar(Painter &p) { _titleNameVersion = namePeer->nameVersion(); _title.setText( st::msgNameStyle, - TopBarNameText(namePeer, _activeChat.section), + TopBarNameText(namePeer, _activeChat), Ui::NameTextOptions()); } if (const auto info = namePeer->botVerifyDetails()) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index accfcd29e4..ba20014278 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -1638,8 +1638,9 @@ stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; -inputReplyToMessage#22c0f6d5 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int = InputReplyTo; +inputReplyToMessage#b07038b0 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; +inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; From 40053e3388857cea9a1e3f020008d6285a4af78a Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 9 May 2025 15:40:30 +0400 Subject: [PATCH 056/310] Rename RepliesWidget/Memento to ChatWidget/Memento. --- Telegram/CMakeLists.txt | 4 +- .../boxes/peers/edit_forum_topic_box.cpp | 12 +- .../SourceFiles/data/data_forum_topic.cpp | 2 +- .../SourceFiles/history/history_widget.cpp | 2 +- ...tion.cpp => history_view_chat_section.cpp} | 723 +++++++++--------- ..._section.h => history_view_chat_section.h} | 73 +- .../window/notifications_manager.cpp | 11 +- .../window/window_session_controller.cpp | 25 +- 8 files changed, 453 insertions(+), 399 deletions(-) rename Telegram/SourceFiles/history/view/{history_view_replies_section.cpp => history_view_chat_section.cpp} (82%) rename Telegram/SourceFiles/history/view/{history_view_replies_section.h => history_view_chat_section.h} (92%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 92ce8f31a3..1d52a00a53 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -836,6 +836,8 @@ PRIVATE history/view/history_view_bottom_info.h history/view/history_view_chat_preview.cpp history/view/history_view_chat_preview.h + history/view/history_view_chat_section.cpp + history/view/history_view_chat_section.h history/view/history_view_contact_status.cpp history/view/history_view_contact_status.h history/view/history_view_context_menu.cpp @@ -870,8 +872,6 @@ PRIVATE history/view/history_view_pinned_tracker.h history/view/history_view_quick_action.cpp history/view/history_view_quick_action.h - history/view/history_view_replies_section.cpp - history/view/history_view_replies_section.h history/view/history_view_reply.cpp history/view/history_view_reply.h history/view/history_view_requests_bar.cpp diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index fe675532a1..1e6ab6206b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -27,7 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/premium_preview_box.h" #include "main/main_session.h" #include "history/history.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_sticker_toast.h" #include "lang/lang_keys.h" #include "info/profile/info_profile_emoji_status_panel.h" @@ -518,13 +518,15 @@ void EditForumTopicBox( title->showError(); return; } + using namespace HistoryView; controller->showSection( - std::make_shared( - forum, - channel->forum()->reserveCreatingId( + std::make_shared(ChatViewId{ + .history = forum, + .repliesRootId = channel->forum()->reserveCreatingId( title->getLastText().trimmed(), state->defaultIcon.current().colorId, - state->iconId.current())), + state->iconId.current()), + }), Window::SectionShow::Way::ClearStack); }; diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 7e7998897c..eb93c36375 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -26,7 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_unread_things.h" #include "history/view/history_view_item_preview.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "main/main_session.h" #include "base/unixtime.h" #include "ui/painter.h" diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 9e829886f7..85682e38eb 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5311,7 +5311,7 @@ void HistoryWidget::updateSendButtonType() { using Type = Ui::SendButton::Type; const auto type = computeSendButtonType(); - // This logic is duplicated in RepliesWidget. + // This logic is duplicated in ChatWidget. const auto disabledBySlowmode = _peer && _peer->slowmodeApplied() && (_history->latestSendingMessage() != nullptr); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp similarity index 82% rename from Telegram/SourceFiles/history/view/history_view_replies_section.cpp rename to Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 768f42b178..250d952f0c 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -5,7 +5,7 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "history/view/controls/history_view_compose_controls.h" #include "history/view/controls/history_view_compose_search.h" @@ -111,32 +111,34 @@ rpl::producer RootViewContent( } // namespace -RepliesMemento::RepliesMemento( - not_null history, - MsgId rootId, +ChatMemento::ChatMemento( + ChatViewId id, MsgId highlightId, const TextWithEntities &highlightPart, int highlightPartOffsetHint) -: _history(history) -, _rootId(rootId) +: _id(id) , _highlightPart(highlightPart) , _highlightPartOffsetHint(highlightPartOffsetHint) , _highlightId(highlightId) { if (highlightId) { _list.setAroundPosition({ - .fullId = FullMsgId(_history->peer->id, highlightId), + .fullId = FullMsgId(_id.history->peer->id, highlightId), .date = TimeId(0), }); } } -RepliesMemento::RepliesMemento( +ChatMemento::ChatMemento( + Comments, not_null commentsItem, MsgId commentId) -: RepliesMemento(commentsItem->history(), commentsItem->id, commentId) { +: ChatMemento({ + .history = commentsItem->history(), + .repliesRootId = commentsItem->id, +}, commentId) { } -void RepliesMemento::setFromTopic(not_null topic) { +void ChatMemento::setFromTopic(not_null topic) { _replies = topic->replies(); if (!_list.aroundPosition()) { _list = *topic->listMemento(); @@ -144,31 +146,35 @@ void RepliesMemento::setFromTopic(not_null topic) { } -Data::ForumTopic *RepliesMemento::topicForRemoveRequests() const { - return _history->peer->forumTopicFor(_rootId); +Data::ForumTopic *ChatMemento::topicForRemoveRequests() const { + return _id.repliesRootId + ? _id.history->peer->forumTopicFor(_id.repliesRootId) + : nullptr; } -void RepliesMemento::setReadInformation( +void ChatMemento::setReadInformation( MsgId inboxReadTillId, int unreadCount, MsgId outboxReadTillId) { - if (!_replies) { - if (const auto forum = _history->asForum()) { - if (const auto topic = forum->topicFor(_rootId)) { + if (!_id.repliesRootId) { + return; + } else if (!_replies) { + if (const auto forum = _id.history->asForum()) { + if (const auto topic = forum->topicFor(_id.repliesRootId)) { _replies = topic->replies(); } } if (!_replies) { _replies = std::make_shared( - _history, - _rootId); + _id.history, + _id.repliesRootId); } } _replies->setInboxReadTill(inboxReadTillId, unreadCount); _replies->setOutboxReadTill(outboxReadTillId); } -object_ptr RepliesMemento::createWidget( +object_ptr ChatMemento::createWidget( QWidget *parent, not_null controller, Window::Column column, @@ -184,40 +190,42 @@ object_ptr RepliesMemento::createWidget( Data::MinMessagePosition }); } - auto result = object_ptr( - parent, - controller, - _history, - _rootId); + auto result = object_ptr(parent, controller, _id); result->setInternalState(geometry, this); return result; } -void RepliesMemento::setupTopicViewer() { - _history->owner().itemIdChanged( - ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { - if (_rootId == change.oldId) { - _rootId = change.newId.msg; - _replies = nullptr; - } - }, _lifetime); +void ChatMemento::setupTopicViewer() { + if (_id.repliesRootId) { + _id.history->owner().itemIdChanged( + ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { + if (_id.repliesRootId == change.oldId) { + _id.repliesRootId = change.newId.msg; + _replies = nullptr; + } + }, _lifetime); + } } -RepliesWidget::RepliesWidget( +ChatWidget::ChatWidget( QWidget *parent, not_null controller, - not_null history, - MsgId rootId) -: Window::SectionWidget(parent, controller, history->peer) + ChatViewId id) +: Window::SectionWidget(parent, controller, id.history->peer) , WindowListDelegate(controller) -, _history(history) -, _rootId(rootId) -, _root(lookupRoot()) +, _history(id.history) +, _peer(_history->peer) +, _id(id) +, _repliesRootId(_id.repliesRootId) +, _repliesRoot(lookupRepliesRoot()) , _topic(lookupTopic()) , _areComments(computeAreComments()) -, _sendAction(history->owner().sendActionManager().repliesPainter( - history, - rootId)) +, _sublist(_id.sublist) +, _sendAction(_repliesRootId + ? _history->owner().sendActionManager().repliesPainter( + _history, + _repliesRootId) + : nullptr) , _topBar(this, controller) , _topBarShadow(this) , _composeControls(std::make_unique( @@ -239,7 +247,7 @@ RepliesWidget::RepliesWidget( }) | rpl::type_erased() : rpl::single(false), })) -, _translateBar(std::make_unique(this, controller, history)) +, _translateBar(std::make_unique(this, controller, _history)) , _scroll(std::make_unique( this, controller->chatStyle()->value(lifetime(), st::historyScroll), @@ -255,7 +263,7 @@ RepliesWidget::RepliesWidget( Window::ChatThemeValueFromPeer( controller, - history->peer + _peer ) | rpl::start_with_next([=](std::shared_ptr &&theme) { _theme = std::move(theme); controller->setChatStyleTheme(_theme); @@ -266,7 +274,7 @@ RepliesWidget::RepliesWidget( setupShortcuts(); setupTranslateBar(); - _history->peer->updateFull(); + _peer->updateFull(); refreshTopBarActiveChat(); @@ -274,8 +282,8 @@ RepliesWidget::RepliesWidget( _topBar->resizeToWidth(width()); _topBar->show(); - if (_rootView) { - _rootView->move(0, _topBar->height()); + if (_repliesRootView) { + _repliesRootView->move(0, _topBar->height()); } _topBar->deleteSelectionRequest( @@ -331,7 +339,7 @@ RepliesWidget::RepliesWidget( ) | rpl::start_with_next([=](ListWidget::ReplyToMessageRequest request) { const auto canSendReply = _topic ? Data::CanSendAnything(_topic) - : Data::CanSendAnything(_history->peer); + : Data::CanSendAnything(_peer); const auto &to = request.to; const auto still = _history->owner().message(to.messageId); const auto allowInAnotherChat = still && still->allowsForward(); @@ -356,16 +364,18 @@ RepliesWidget::RepliesWidget( _composeControls->sendActionUpdates( ) | rpl::start_with_next([=](ComposeControls::SendActionUpdate &&data) { - if (!data.cancel) { + if (!_repliesRootId) { + return; + } else if (!data.cancel) { session().sendProgressManager().update( _history, - _rootId, + _repliesRootId, data.type, data.progress); } else { session().sendProgressManager().cancel( _history, - _rootId, + _repliesRootId, data.type); } }, lifetime()); @@ -373,8 +383,8 @@ RepliesWidget::RepliesWidget( _history->session().changes().messageUpdates( Data::MessageUpdate::Flag::Destroyed ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { - if (update.item == _root) { - _root = nullptr; + if (update.item == _repliesRoot) { + _repliesRoot = nullptr; updatePinnedVisibility(); if (!_topic) { controller->showBackFromStack(); @@ -415,7 +425,7 @@ RepliesWidget::RepliesWidget( } } -RepliesWidget::~RepliesWidget() { +ChatWidget::~ChatWidget() { base::take(_sendAction); session().api().saveCurrentDraftToCloud(); controller()->sendingAnimation().clear(); @@ -428,18 +438,20 @@ RepliesWidget::~RepliesWidget() { _inner->saveState(_topic->listMemento()); } } - _history->owner().sendActionManager().repliesPainterRemoved( - _history, - _rootId); + if (_repliesRootId) { + _history->owner().sendActionManager().repliesPainterRemoved( + _history, + _repliesRootId); + } } -void RepliesWidget::orderWidgets() { +void ChatWidget::orderWidgets() { _translateBar->raise(); if (_topicReopenBar) { _topicReopenBar->bar().raise(); } - if (_rootView) { - _rootView->raise(); + if (_repliesRootView) { + _repliesRootView->raise(); } if (_pinnedBar) { _pinnedBar->raise(); @@ -451,81 +463,84 @@ void RepliesWidget::orderWidgets() { _composeControls->raisePanels(); } -void RepliesWidget::setupRoot() { - if (!_root) { +void ChatWidget::setupRoot() { + if (_repliesRootId && !_repliesRoot) { const auto done = crl::guard(this, [=] { - _root = lookupRoot(); - if (_root) { + _repliesRoot = lookupRepliesRoot(); + if (_repliesRoot) { _areComments = computeAreComments(); _inner->update(); } updatePinnedVisibility(); }); _history->session().api().requestMessageData( - _history->peer, - _rootId, + _peer, + _repliesRootId, done); } } -void RepliesWidget::setupRootView() { - if (_topic) { +void ChatWidget::setupRootView() { + if (_topic || !_repliesRootId) { return; } - _rootView = std::make_unique(this, [=] { + _repliesRootView = std::make_unique(this, [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); - _rootView->setContent(rpl::combine( + _repliesRootView->setContent(rpl::combine( RootViewContent( _history, - _rootId, - [bar = _rootView.get()] { bar->customEmojiRepaint(); }), - _rootVisible.value() + _repliesRootId, + [bar = _repliesRootView.get()] { bar->customEmojiRepaint(); }), + _repliesRootVisible.value() ) | rpl::map([=](Ui::MessageBarContent &&content, bool show) { const auto shown = !content.title.isEmpty() && !content.text.empty(); _shownPinnedItem = shown - ? _history->owner().message(_history->peer->id, _rootId) + ? _history->owner().message(_peer->id, _repliesRootId) : nullptr; return show ? std::move(content) : Ui::MessageBarContent(); })); controller()->adaptive().oneColumnValue( ) | rpl::start_with_next([=](bool one) { - _rootView->setShadowGeometryPostprocess([=](QRect geometry) { + _repliesRootView->setShadowGeometryPostprocess([=](QRect geometry) { if (!one) { geometry.setLeft(geometry.left() + st::lineWidth); } return geometry; }); - }, _rootView->lifetime()); + }, _repliesRootView->lifetime()); - _rootView->barClicks( + _repliesRootView->barClicks( ) | rpl::start_with_next([=] { showAtStart(); }, lifetime()); - _rootViewHeight = 0; - _rootView->heightValue( + _repliesRootViewHeight = 0; + _repliesRootView->heightValue( ) | rpl::start_with_next([=](int height) { - if (const auto delta = height - _rootViewHeight) { - _rootViewHeight = height; + if (const auto delta = height - _repliesRootViewHeight) { + _repliesRootViewHeight = height; setGeometryWithTopMoved(geometry(), delta); } - }, _rootView->lifetime()); + }, _repliesRootView->lifetime()); } -void RepliesWidget::setupTopicViewer() { +void ChatWidget::setupTopicViewer() { + if (!_repliesRootId) { + return; + } const auto owner = &_history->owner(); owner->itemIdChanged( ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { - if (_rootId == change.oldId) { - _rootId = change.newId.msg; - _composeControls->updateTopicRootId(_rootId); + if (_repliesRootId == change.oldId) { + _repliesRootId = _id.repliesRootId = change.newId.msg; + _composeControls->updateTopicRootId(_repliesRootId); _sendAction = owner->sendActionManager().repliesPainter( _history, - _rootId); - _root = lookupRoot(); + _repliesRootId); + _repliesRoot = lookupRepliesRoot(); if (_topic && _topic->rootId() == change.oldId) { setTopic(_topic->forum()->topicFor(change.newId.msg)); } else { @@ -544,7 +559,7 @@ void RepliesWidget::setupTopicViewer() { } } -void RepliesWidget::subscribeToTopic() { +void ChatWidget::subscribeToTopic() { Expects(_topic != nullptr); _topicReopenBar = std::make_unique(this, _topic); @@ -594,7 +609,7 @@ void RepliesWidget::subscribeToTopic() { _cornerButtons.updateUnreadThingsVisibility(); } -void RepliesWidget::subscribeToPinnedMessages() { +void ChatWidget::subscribeToPinnedMessages() { using EntryUpdateFlag = Data::EntryUpdate::Flag; session().changes().entryUpdates( EntryUpdateFlag::HasPinnedMessages @@ -609,7 +624,7 @@ void RepliesWidget::subscribeToPinnedMessages() { setupPinnedTracker(); } -void RepliesWidget::setTopic(Data::ForumTopic *topic) { +void ChatWidget::setTopic(Data::ForumTopic *topic) { if (_topic == topic) { return; } @@ -618,10 +633,10 @@ void RepliesWidget::setTopic(Data::ForumTopic *topic) { refreshReplies(); refreshTopBarActiveChat(); if (_topic) { - if (_rootView) { + if (_repliesRootView) { _shownPinnedItem = nullptr; - _rootView = nullptr; - _rootViewHeight = 0; + _repliesRootView = nullptr; + _repliesRootViewHeight = 0; } subscribeToTopic(); } @@ -632,18 +647,22 @@ void RepliesWidget::setTopic(Data::ForumTopic *topic) { } } -HistoryItem *RepliesWidget::lookupRoot() const { - return _history->owner().message(_history->peer, _rootId); +HistoryItem *ChatWidget::lookupRepliesRoot() const { + return _repliesRootId + ? _history->owner().message(_peer, _repliesRootId) + : nullptr; } -Data::ForumTopic *RepliesWidget::lookupTopic() { - if (const auto forum = _history->asForum()) { - if (const auto result = forum->topicFor(_rootId)) { +Data::ForumTopic *ChatWidget::lookupTopic() { + if (!_repliesRoot) { + return nullptr; + } else if (const auto forum = _history->asForum()) { + if (const auto result = forum->topicFor(_repliesRootId)) { return result; } else { - forum->requestTopic(_rootId, crl::guard(this, [=] { + forum->requestTopic(_repliesRootId, crl::guard(this, [=] { if (const auto forum = _history->asForum()) { - setTopic(forum->topicFor(_rootId)); + setTopic(forum->topicFor(_repliesRootId)); } })); } @@ -651,21 +670,21 @@ Data::ForumTopic *RepliesWidget::lookupTopic() { return nullptr; } -bool RepliesWidget::computeAreComments() const { - return _root && _root->isDiscussionPost(); +bool ChatWidget::computeAreComments() const { + return _repliesRoot && _repliesRoot->isDiscussionPost(); } -void RepliesWidget::setupComposeControls() { +void ChatWidget::setupComposeControls() { auto topicWriteRestrictions = rpl::single( ) | rpl::then(session().changes().topicUpdates( Data::TopicUpdate::Flag::Closed ) | rpl::filter([=](const Data::TopicUpdate &update) { return (update.topic->history() == _history) - && (update.topic->rootId() == _rootId); + && (update.topic->rootId() == _repliesRootId); }) | rpl::to_empty) | rpl::map([=] { const auto topic = _topic ? _topic - : _history->peer->forumTopicFor(_rootId); + : _peer->forumTopicFor(_repliesRootId); return (!topic || topic->canToggleClosed() || !topic->closed()) ? Data::SendError() : tr::lng_forum_topic_closed(tr::now); @@ -673,10 +692,12 @@ void RepliesWidget::setupComposeControls() { auto writeRestriction = rpl::combine( session().frozenValue(), session().changes().peerFlagsValue( - _history->peer, + _peer, Data::PeerUpdate::Flag::Rights), - Data::CanSendAnythingValue(_history->peer), - std::move(topicWriteRestrictions) + Data::CanSendAnythingValue(_peer), + (_repliesRootId + ? std::move(topicWriteRestrictions) + : (rpl::single(Data::SendError()) | rpl::type_erased())) ) | rpl::map([=]( const Main::FreezeInfo &info, auto, @@ -691,9 +712,9 @@ void RepliesWidget::setupComposeControls() { & ~ChatRestriction::SendPolls; const auto canSendAnything = _topic ? Data::CanSendAnyOf(_topic, allWithoutPolls) - : Data::CanSendAnyOf(_history->peer, allWithoutPolls); + : Data::CanSendAnyOf(_peer, allWithoutPolls); const auto restriction = Data::RestrictionError( - _history->peer, + _peer, ChatRestriction::SendOther); auto text = !canSendAnything ? (restriction @@ -716,8 +737,8 @@ void RepliesWidget::setupComposeControls() { .topicRootId = _topic ? _topic->rootId() : MsgId(0), .showSlowmodeError = [=] { return showSlowmodeError(); }, .sendActionFactory = [=] { return prepareSendAction({}); }, - .slowmodeSecondsLeft = SlowmodeSecondsLeft(_history->peer), - .sendDisabledBySlowmode = SendDisabledBySlowmode(_history->peer), + .slowmodeSecondsLeft = SlowmodeSecondsLeft(_peer), + .sendDisabledBySlowmode = SendDisabledBySlowmode(_peer), .writeRestriction = std::move(writeRestriction), }); @@ -872,7 +893,7 @@ void RepliesWidget::setupComposeControls() { _composeControls->finishAnimating(); - if (const auto channel = _history->peer->asChannel()) { + if (const auto channel = _peer->asChannel()) { channel->updateFull(); if (!channel->isBroadcast()) { rpl::combine( @@ -887,11 +908,11 @@ void RepliesWidget::setupComposeControls() { } } -void RepliesWidget::setupSwipeReplyAndBack() { +void ChatWidget::setupSwipeReplyAndBack() { const auto can = [=](not_null still) { const auto canSendReply = _topic ? Data::CanSendAnything(_topic) - : Data::CanSendAnything(_history->peer); + : Data::CanSendAnything(_peer); const auto allowInAnotherChat = still && still->allowsForward(); if (allowInAnotherChat && (_joinGroup || !canSendReply)) { return true; @@ -926,8 +947,8 @@ void RepliesWidget::setupSwipeReplyAndBack() { || (_gestureHorizontal.reachRatio != data.reachRatio); if (changed) { _gestureHorizontal = data; - const auto item = _history->peer->owner().message( - _history->peer->id, + const auto item = _peer->owner().message( + _peer->id, MsgId{ data.msgBareId }); if (item) { _history->owner().requestItemRepaint(item); @@ -985,11 +1006,11 @@ void RepliesWidget::setupSwipeReplyAndBack() { }); } -void RepliesWidget::chooseAttach( +void ChatWidget::chooseAttach( std::optional overrideSendImagesAsPhotos) { _choosingAttach = false; - if (const auto error = Data::AnyFileRestrictionError(_history->peer)) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + if (const auto error = Data::AnyFileRestrictionError(_peer)) { + Data::ShowSendErrorToast(controller(), _peer, error); return; } else if (showSlowmodeError()) { return; @@ -1028,7 +1049,7 @@ void RepliesWidget::chooseAttach( }), nullptr); } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( not_null data, std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel) { @@ -1062,7 +1083,7 @@ bool RepliesWidget::confirmSendingFiles( return false; } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel) { if (_composeControls->confirmMediaEdit(list)) { @@ -1075,7 +1096,7 @@ bool RepliesWidget::confirmSendingFiles( controller(), std::move(list), _composeControls->getTextWithAppliedMarkdown(), - _history->peer, + _peer, Api::SendType::Normal, sendMenuDetails()); @@ -1101,7 +1122,7 @@ bool RepliesWidget::confirmSendingFiles( return true; } -void RepliesWidget::sendingFilesConfirmed( +void ChatWidget::sendingFilesConfirmed( Ui::PreparedList &&list, Ui::SendFilesWay way, TextWithTags &&caption, @@ -1115,7 +1136,7 @@ void RepliesWidget::sendingFilesConfirmed( auto groups = DivideByGroups( std::move(list), way, - _history->peer->slowmodeApplied()); + _peer->slowmodeApplied()); auto bundle = PrepareFilesBundle( std::move(groups), way, @@ -1124,19 +1145,19 @@ void RepliesWidget::sendingFilesConfirmed( sendingFilesConfirmed(std::move(bundle), options); } -bool RepliesWidget::checkSendPayment( +bool ChatWidget::checkSendPayment( int messagesCount, int starsApproved, Fn withPaymentApproved) { return _sendPayment.check( controller(), - _history->peer, + _peer, messagesCount, starsApproved, std::move(withPaymentApproved)); } -void RepliesWidget::sendingFilesConfirmed( +void ChatWidget::sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options) { const auto withPaymentApproved = [=](int approved) { @@ -1180,7 +1201,7 @@ void RepliesWidget::sendingFilesConfirmed( finishSending(); } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( QImage &&image, QByteArray &&content, std::optional overrideSendImagesAsPhotos, @@ -1197,14 +1218,14 @@ bool RepliesWidget::confirmSendingFiles( return confirmSendingFiles(std::move(list), insertTextOnCancel); } -bool RepliesWidget::showSlowmodeError() { +bool ChatWidget::showSlowmodeError() { const auto text = [&] { - if (const auto left = _history->peer->slowmodeSecondsLeft()) { + if (const auto left = _peer->slowmodeSecondsLeft()) { return tr::lng_slowmode_enabled( tr::now, lt_left, Ui::FormatDurationWordsSlowmode(left)); - } else if (_history->peer->slowmodeApplied()) { + } else if (_peer->slowmodeApplied()) { if (const auto item = _history->latestSendingMessage()) { showAtPosition(item->position()); return tr::lng_slowmode_no_many(tr::now); @@ -1219,13 +1240,15 @@ bool RepliesWidget::showSlowmodeError() { return true; } -void RepliesWidget::pushReplyReturn(not_null item) { - if (item->history() == _history && item->inThread(_rootId)) { - _cornerButtons.pushReplyReturn(item); +void ChatWidget::pushReplyReturn(not_null item) { + if (_repliesRootId) { + if (item->history() == _history && item->inThread(_repliesRootId)) { + _cornerButtons.pushReplyReturn(item); + } } } -void RepliesWidget::checkReplyReturns() { +void ChatWidget::checkReplyReturns() { const auto currentTop = _scroll->scrollTop(); while (const auto replyReturn = _cornerButtons.replyReturn()) { const auto position = replyReturn->position(); @@ -1241,26 +1264,26 @@ void RepliesWidget::checkReplyReturns() { } } -void RepliesWidget::uploadFile( +void ChatWidget::uploadFile( const QByteArray &fileContent, SendMediaType type) { session().api().sendFile(fileContent, type, prepareSendAction({})); } -bool RepliesWidget::showSendingFilesError( +bool ChatWidget::showSendingFilesError( const Ui::PreparedList &list) const { return showSendingFilesError(list, std::nullopt); } -bool RepliesWidget::showSendingFilesError( +bool ChatWidget::showSendingFilesError( const Ui::PreparedList &list, std::optional compress) const { const auto error = [&]() -> Data::SendError { - const auto peer = _history->peer; + const auto peer = _peer; const auto error = Data::FileRestrictionError(peer, list, compress); if (error) { return error; - } else if (const auto left = _history->peer->slowmodeSecondsLeft()) { + } else if (const auto left = _peer->slowmodeSecondsLeft()) { return tr::lng_slowmode_enabled( tr::now, lt_left, @@ -1288,11 +1311,11 @@ bool RepliesWidget::showSendingFilesError( return true; } - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return true; } -Api::SendAction RepliesWidget::prepareSendAction( +Api::SendAction ChatWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); @@ -1300,14 +1323,14 @@ Api::SendAction RepliesWidget::prepareSendAction( return result; } -void RepliesWidget::send() { +void ChatWidget::send() { if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) { return; } send({}); } -void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) { +void ChatWidget::sendVoice(const ComposeControls::VoiceToSend &data) { const auto withPaymentApproved = [=](int approved) { auto copy = data; copy.options.starsApproved = approved; @@ -1334,7 +1357,7 @@ void RepliesWidget::sendVoice(const ComposeControls::VoiceToSend &data) { finishSending(); } -void RepliesWidget::send(Api::SendOptions options) { +void ChatWidget::send(Api::SendOptions options) { if (!options.scheduled && showSlowmodeError()) { return; } @@ -1354,9 +1377,9 @@ void RepliesWidget::send(Api::SendOptions options) { .ignoreSlowmodeCountdown = (options.scheduled != 0), }; request.messagesCount = ComputeSendingMessagesCount(_history, request); - const auto error = GetErrorForSending(_history->peer, request); + const auto error = GetErrorForSending(_peer, request); if (error) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return; } if (!options.scheduled) { @@ -1376,11 +1399,13 @@ void RepliesWidget::send(Api::SendOptions options) { session().api().sendMessage(std::move(message)); _composeControls->clear(); - session().sendProgressManager().update( - _history, - _rootId, - Api::SendProgressType::Typing, - -1); + if (_repliesRootId) { + session().sendProgressManager().update( + _history, + _repliesRootId, + Api::SendProgressType::Typing, + -1); + } //_saveDraftText = true; //_saveDraftStart = crl::now(); @@ -1389,7 +1414,7 @@ void RepliesWidget::send(Api::SendOptions options) { finishSending(); } -void RepliesWidget::edit( +void ChatWidget::edit( not_null item, Api::SendOptions options, mtpRequestId *const saveEditMsgRequestId, @@ -1468,7 +1493,7 @@ void RepliesWidget::edit( doSetInnerFocus(); } -void RepliesWidget::refreshJoinGroupButton() { +void ChatWidget::refreshJoinGroupButton() { const auto set = [&](std::unique_ptr button) { if (!button && !_joinGroup) { return; @@ -1488,7 +1513,7 @@ void RepliesWidget::refreshJoinGroupButton() { listScrollTo(_scroll->scrollTopMax()); } }; - const auto channel = _history->peer->asChannel(); + const auto channel = _peer->asChannel(); const auto canSend = !channel->isForum() ? Data::CanSendAnything(channel) : (_topic && Data::CanSendAnything(_topic)); @@ -1512,15 +1537,15 @@ void RepliesWidget::refreshJoinGroupButton() { } } -bool RepliesWidget::sendExistingDocument( +bool ChatWidget::sendExistingDocument( not_null document, Api::MessageToSend messageToSend, std::optional localId) { const auto error = Data::RestrictionError( - _history->peer, + _peer, ChatRestriction::SendStickers); if (error) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return false; } else if (showSlowmodeError() || ShowSendPremiumError(controller(), document)) { @@ -1549,18 +1574,18 @@ bool RepliesWidget::sendExistingDocument( return true; } -void RepliesWidget::sendExistingPhoto(not_null photo) { +void ChatWidget::sendExistingPhoto(not_null photo) { sendExistingPhoto(photo, {}); } -bool RepliesWidget::sendExistingPhoto( +bool ChatWidget::sendExistingPhoto( not_null photo, Api::SendOptions options) { const auto error = Data::RestrictionError( - _history->peer, + _peer, ChatRestriction::SendPhotos); if (error) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return false; } else if (showSlowmodeError()) { return false; @@ -1588,11 +1613,11 @@ bool RepliesWidget::sendExistingPhoto( return true; } -void RepliesWidget::sendInlineResult( +void ChatWidget::sendInlineResult( std::shared_ptr result, not_null bot) { if (const auto error = result->getErrorOnSend(_history)) { - Data::ShowSendErrorToast(controller(), _history->peer, error); + Data::ShowSendErrorToast(controller(), _peer, error); return; } sendInlineResult(std::move(result), bot, {}, std::nullopt); @@ -1604,7 +1629,7 @@ void RepliesWidget::sendInlineResult( // Ui::LayerOption::KeepOther); } -void RepliesWidget::sendInlineResult( +void ChatWidget::sendInlineResult( std::shared_ptr result, not_null bot, Api::SendOptions options, @@ -1649,26 +1674,28 @@ void RepliesWidget::sendInlineResult( finishSending(); } -SendMenu::Details RepliesWidget::sendMenuDetails() const { +SendMenu::Details ChatWidget::sendMenuDetails() const { using Type = SendMenu::Type; - const auto type = (_topic && !_history->peer->starsPerMessageChecked()) + const auto type = (_topic && !_peer->starsPerMessageChecked()) ? Type::Scheduled : Type::SilentOnly; return SendMenu::Details{ .type = type }; } -FullReplyTo RepliesWidget::replyTo() const { +FullReplyTo ChatWidget::replyTo() const { if (auto custom = _composeControls->replyingToMessage()) { - custom.topicRootId = _rootId; + custom.topicRootId = _repliesRootId; return custom; } return FullReplyTo{ - .messageId = FullMsgId(_history->peer->id, _rootId), - .topicRootId = _rootId, + .messageId = (_repliesRootId + ? FullMsgId(_peer->id, _repliesRootId) + : FullMsgId()), + .topicRootId = _repliesRootId, }; } -void RepliesWidget::refreshTopBarActiveChat() { +void ChatWidget::refreshTopBarActiveChat() { using namespace Dialogs; const auto state = EntryState{ .key = (_topic ? Key{ _topic } : Key{ _history }), @@ -1680,13 +1707,13 @@ void RepliesWidget::refreshTopBarActiveChat() { controller()->setDialogsEntryState(state); } -void RepliesWidget::refreshUnreadCountBadge(std::optional count) { +void ChatWidget::refreshUnreadCountBadge(std::optional count) { if (count.has_value()) { _cornerButtons.updateJumpDownVisibility(count); } } -void RepliesWidget::updatePinnedViewer() { +void ChatWidget::updatePinnedViewer() { if (_scroll->isHidden() || !_topic || !_pinnedTracker) { return; } @@ -1704,7 +1731,7 @@ void RepliesWidget::updatePinnedViewer() { _pinnedClickedId = FullMsgId(); } if (_pinnedClickedId && !_minPinnedId) { - _minPinnedId = Data::ResolveMinPinnedId(_history->peer, _rootId); + _minPinnedId = Data::ResolveMinPinnedId(_peer, _repliesRootId); } if (_pinnedClickedId && _minPinnedId && _minPinnedId >= _pinnedClickedId) { // After click on the last pinned message we should the top one. @@ -1714,7 +1741,7 @@ void RepliesWidget::updatePinnedViewer() { } } -void RepliesWidget::checkLastPinnedClickedIdReset( +void ChatWidget::checkLastPinnedClickedIdReset( int wasScrollTop, int nowScrollTop) { if (_scroll->isHidden() || !_topic) { @@ -1728,7 +1755,7 @@ void RepliesWidget::checkLastPinnedClickedIdReset( } } -void RepliesWidget::setupTranslateBar() { +void ChatWidget::setupTranslateBar() { controller()->adaptive().oneColumnValue( ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { raw->setShadowGeometryPostprocess([=](QRect geometry) { @@ -1751,7 +1778,7 @@ void RepliesWidget::setupTranslateBar() { _translateBar->finishAnimating(); } -void RepliesWidget::setupPinnedTracker() { +void ChatWidget::setupPinnedTracker() { Expects(_topic != nullptr); _pinnedTracker = std::make_unique(_topic); @@ -1761,7 +1788,7 @@ void RepliesWidget::setupPinnedTracker() { &_topic->session(), Storage::SharedMediaKey( _topic->channel()->id, - _rootId, + _repliesRootId, Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, @@ -1772,13 +1799,13 @@ void RepliesWidget::setupPinnedTracker() { _topic->setHasPinnedMessages(*result.fullCount() != 0); if (result.skippedAfter() == 0) { auto &settings = _history->session().settings(); - const auto peerId = _history->peer->id; + const auto peerId = _peer->id; const auto hiddenId = settings.hiddenPinnedMessageId( peerId, - _rootId); + _repliesRootId); const auto last = result.size() ? result[result.size() - 1] : 0; if (hiddenId && hiddenId != last) { - settings.setHiddenPinnedMessageId(peerId, _rootId, 0); + settings.setHiddenPinnedMessageId(peerId, _repliesRootId, 0); _history->session().saveSettingsDelayed(); } } @@ -1786,17 +1813,18 @@ void RepliesWidget::setupPinnedTracker() { }, _topicLifetime); } -void RepliesWidget::checkPinnedBarState() { +void ChatWidget::checkPinnedBarState() { Expects(_pinnedTracker != nullptr); Expects(_inner != nullptr); - const auto peer = _history->peer; - const auto hiddenId = peer->canPinMessages() + const auto hiddenId = _peer->canPinMessages() ? MsgId(0) - : peer->session().settings().hiddenPinnedMessageId( - peer->id, - _rootId); - const auto currentPinnedId = Data::ResolveTopPinnedId(peer, _rootId); + : _peer->session().settings().hiddenPinnedMessageId( + _peer->id, + _repliesRootId); + const auto currentPinnedId = Data::ResolveTopPinnedId( + _peer, + _repliesRootId); const auto universalPinnedId = !currentPinnedId ? MsgId(0) : currentPinnedId.msg; @@ -1825,8 +1853,8 @@ void RepliesWidget::checkPinnedBarState() { Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); auto pinnedRefreshed = Info::Profile::SharedMediaCountValue( - _history->peer, - _rootId, + _peer, + _repliesRootId, nullptr, Storage::SharedMediaType::Pinned ) | rpl::distinct_until_changed( @@ -1855,7 +1883,7 @@ void RepliesWidget::checkPinnedBarState() { [bar = _pinnedBar.get()] { bar->customEmojiRepaint(); }), std::move(pinnedRefreshed), std::move(customButtonItem), - _rootVisible.value() + _repliesRootVisible.value() ) | rpl::map([=](Ui::MessageBarContent &&content, auto, auto, bool show) { const auto shown = !content.title.isEmpty() && !content.text.empty(); _shownPinnedItem = shown @@ -1910,7 +1938,7 @@ void RepliesWidget::checkPinnedBarState() { } } -void RepliesWidget::clearHidingPinnedBar() { +void ChatWidget::clearHidingPinnedBar() { if (!_hidingPinnedBar) { return; } @@ -1921,7 +1949,7 @@ void RepliesWidget::clearHidingPinnedBar() { _hidingPinnedBar = nullptr; } -void RepliesWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { +void ChatWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { if (!_pinnedBar) { return; // It can be in process of hiding. } @@ -1974,14 +2002,14 @@ void RepliesWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { _pinnedBar->setRightButton(std::move(button)); } -void RepliesWidget::hidePinnedMessage() { +void ChatWidget::hidePinnedMessage() { Expects(_pinnedBar != nullptr); const auto id = _pinnedTracker->currentMessageId(); if (!id.message) { return; } - if (_history->peer->canPinMessages()) { + if (_peer->canPinMessages()) { Window::ToggleMessagePinned(controller(), id.message, false); } else { const auto callback = [=] { @@ -1991,30 +2019,30 @@ void RepliesWidget::hidePinnedMessage() { }; Window::HidePinnedBar( controller(), - _history->peer, - _rootId, + _peer, + _repliesRootId, crl::guard(this, callback)); } } -void RepliesWidget::cornerButtonsShowAtPosition( +void ChatWidget::cornerButtonsShowAtPosition( Data::MessagePosition position) { showAtPosition(position); } -Data::Thread *RepliesWidget::cornerButtonsThread() { +Data::Thread *ChatWidget::cornerButtonsThread() { return _topic ? static_cast(_topic) : _history; } -FullMsgId RepliesWidget::cornerButtonsCurrentId() { +FullMsgId ChatWidget::cornerButtonsCurrentId() { return _lastShownAt; } -bool RepliesWidget::cornerButtonsIgnoreVisibility() { +bool ChatWidget::cornerButtonsIgnoreVisibility() { return animatingShow(); } -std::optional RepliesWidget::cornerButtonsDownShown() { +std::optional ChatWidget::cornerButtonsDownShown() { if (_composeControls->isLockPresent() || _composeControls->isTTLButtonShown()) { return false; @@ -2028,25 +2056,25 @@ std::optional RepliesWidget::cornerButtonsDownShown() { return std::nullopt; } -bool RepliesWidget::cornerButtonsUnreadMayBeShown() { +bool ChatWidget::cornerButtonsUnreadMayBeShown() { return _loaded && !_composeControls->isLockPresent() && !_composeControls->isTTLButtonShown(); } -bool RepliesWidget::cornerButtonsHas(CornerButtonType type) { +bool ChatWidget::cornerButtonsHas(CornerButtonType type) { return _topic || (type == CornerButtonType::Down); } -void RepliesWidget::showAtStart() { +void ChatWidget::showAtStart() { showAtPosition(Data::MinMessagePosition); } -void RepliesWidget::showAtEnd() { +void ChatWidget::showAtEnd() { showAtPosition(Data::MaxMessagePosition); } -void RepliesWidget::finishSending() { +void ChatWidget::finishSending() { _composeControls->hidePanelsAnimated(); //if (_previewData && _previewData->pendingTill) previewCancel(); doSetInnerFocus(); @@ -2054,46 +2082,46 @@ void RepliesWidget::finishSending() { refreshTopBarActiveChat(); } -void RepliesWidget::showAtPosition( +void ChatWidget::showAtPosition( Data::MessagePosition position, FullMsgId originItemId) { showAtPosition(position, originItemId, {}); } -void RepliesWidget::showAtPosition( +void ChatWidget::showAtPosition( Data::MessagePosition position, FullMsgId originItemId, const Window::SectionShow ¶ms) { _lastShownAt = position.fullId; controller()->setActiveChatEntry(activeChat()); - const auto ignore = (position.fullId.msg == _rootId); + const auto ignore = (position.fullId.msg == _repliesRootId); _inner->showAtPosition( position, params, _cornerButtons.doneJumpFrom(position.fullId, originItemId, ignore)); } -void RepliesWidget::updateAdaptiveLayout() { +void ChatWidget::updateAdaptiveLayout() { _topBarShadow->moveToLeft( controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, _topBar->height()); } -not_null RepliesWidget::history() const { +not_null ChatWidget::history() const { return _history; } -Dialogs::RowDescriptor RepliesWidget::activeChat() const { +Dialogs::RowDescriptor ChatWidget::activeChat() const { const auto messageId = _lastShownAt ? _lastShownAt - : FullMsgId(_history->peer->id, ShowAtUnreadMsgId); + : FullMsgId(_peer->id, ShowAtUnreadMsgId); if (_topic) { return { _topic, messageId }; } return { _history, messageId }; } -bool RepliesWidget::preventsClose(Fn &&continueCallback) const { +bool ChatWidget::preventsClose(Fn &&continueCallback) const { if (_composeControls->preventsClose(base::duplicate(continueCallback))) { return true; } else if (!_newTopicDiscarded @@ -2120,7 +2148,7 @@ bool RepliesWidget::preventsClose(Fn &&continueCallback) const { return false; } -QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { +QPixmap ChatWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { _topBar->updateControlsVisibility(); if (params.withTopBarShadow) _topBarShadow->hide(); if (_joinGroup) { @@ -2132,8 +2160,8 @@ QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams &pa if (params.withTopBarShadow) { _topBarShadow->show(); } - if (_rootView) { - _rootView->hide(); + if (_repliesRootView) { + _repliesRootView->hide(); } if (_pinnedBar) { _pinnedBar->hide(); @@ -2142,11 +2170,11 @@ QPixmap RepliesWidget::grabForShowAnimation(const Window::SectionSlideParams &pa return result; } -void RepliesWidget::checkActivation() { +void ChatWidget::checkActivation() { _inner->checkActivation(); } -void RepliesWidget::doSetInnerFocus() { +void ChatWidget::doSetInnerFocus() { if (_composeSearch && _inner->getSelectedText().rich.text.isEmpty() && _inner->getSelectedItems().empty()) { @@ -2158,12 +2186,11 @@ void RepliesWidget::doSetInnerFocus() { } } -bool RepliesWidget::showInternal( +bool ChatWidget::showInternal( not_null memento, const Window::SectionShow ¶ms) { - if (auto logMemento = dynamic_cast(memento.get())) { - if (logMemento->getHistory() == history() - && logMemento->getRootId() == _rootId) { + if (auto logMemento = dynamic_cast(memento.get())) { + if (logMemento->id() == _id) { restoreState(logMemento); if (!logMemento->highlightId()) { showAtPosition(Data::UnreadMessagePosition); @@ -2178,15 +2205,15 @@ bool RepliesWidget::showInternal( return false; } -void RepliesWidget::setInternalState( +void ChatWidget::setInternalState( const QRect &geometry, - not_null memento) { + not_null memento) { setGeometry(geometry); Ui::SendPendingMoveResizeEvents(this); restoreState(memento); } -bool RepliesWidget::pushTabbedSelectorToThirdSection( +bool ChatWidget::pushTabbedSelectorToThirdSection( not_null thread, const Window::SectionShow ¶ms) { return _composeControls->pushTabbedSelectorToThirdSection( @@ -2194,26 +2221,31 @@ bool RepliesWidget::pushTabbedSelectorToThirdSection( params); } -bool RepliesWidget::returnTabbedSelector() { +bool ChatWidget::returnTabbedSelector() { return _composeControls->returnTabbedSelector(); } -std::shared_ptr RepliesWidget::createMemento() { - auto result = std::make_shared(history(), _rootId); +std::shared_ptr ChatWidget::createMemento() { + auto result = std::make_shared(_id); saveState(result.get()); return result; } -bool RepliesWidget::showMessage( +bool ChatWidget::showMessage( PeerId peerId, const Window::SectionShow ¶ms, MsgId messageId) { - if (peerId != _history->peer->id) { + if (peerId != _peer->id) { return false; } - const auto id = FullMsgId(_history->peer->id, messageId); + const auto id = FullMsgId(_peer->id, messageId); const auto message = _history->owner().message(id); - if (!message || (!message->inThread(_rootId) && id.msg != _rootId)) { + if (!message) { + return false; + } + if (_repliesRootId + && !message->inThread(_repliesRootId) + && id.msg != _repliesRootId) { return false; } const auto originMessage = [&]() -> HistoryItem* { @@ -2222,7 +2254,8 @@ bool RepliesWidget::showMessage( if (const auto returnTo = session().data().message(origin->id)) { if (returnTo->history() != _history) { return nullptr; - } else if (returnTo->inThread(_rootId)) { + } else if (_repliesRootId + && returnTo->inThread(_repliesRootId)) { return returnTo; } } @@ -2239,24 +2272,24 @@ bool RepliesWidget::showMessage( return true; } -Window::SectionActionResult RepliesWidget::sendBotCommand( +Window::SectionActionResult ChatWidget::sendBotCommand( Bot::SendCommandRequest request) { - if (request.peer != _history->peer) { + if (request.peer != _peer) { return Window::SectionActionResult::Ignore; } listSendBotCommand(request.command, request.context); return Window::SectionActionResult::Handle; } -bool RepliesWidget::confirmSendingFiles(const QStringList &files) { +bool ChatWidget::confirmSendingFiles(const QStringList &files) { return confirmSendingFiles(files, QString()); } -bool RepliesWidget::confirmSendingFiles(not_null data) { +bool ChatWidget::confirmSendingFiles(not_null data) { return confirmSendingFiles(data, std::nullopt); } -bool RepliesWidget::confirmSendingFiles( +bool ChatWidget::confirmSendingFiles( const QStringList &files, const QString &insertTextOnCancel) { const auto premium = controller()->session().user()->isPremium(); @@ -2265,28 +2298,31 @@ bool RepliesWidget::confirmSendingFiles( insertTextOnCancel); } -void RepliesWidget::replyToMessage(FullReplyTo id) { +void ChatWidget::replyToMessage(FullReplyTo id) { _composeControls->replyToMessage(std::move(id)); refreshTopBarActiveChat(); } -void RepliesWidget::saveState(not_null memento) { +void ChatWidget::saveState(not_null memento) { memento->setReplies(_replies); memento->setReplyReturns(_cornerButtons.replyReturns()); _inner->saveState(memento->list()); } -void RepliesWidget::refreshReplies() { +void ChatWidget::refreshReplies() { + if (!_repliesRootId) { + return; + } auto old = base::take(_replies); setReplies(_topic ? _topic->replies() - : std::make_shared(_history, _rootId)); + : std::make_shared(_history, _repliesRootId)); if (old) { _inner->refreshViewer(); } } -void RepliesWidget::setReplies(std::shared_ptr replies) { +void ChatWidget::setReplies(std::shared_ptr replies) { _replies = std::move(replies); _repliesLifetime.destroy(); @@ -2329,7 +2365,7 @@ void RepliesWidget::setReplies(std::shared_ptr replies) { }, _repliesLifetime); } -void RepliesWidget::restoreState(not_null memento) { +void ChatWidget::restoreState(not_null memento) { if (auto replies = memento->getReplies()) { setReplies(std::move(replies)); } else if (!_replies) { @@ -2344,13 +2380,13 @@ void RepliesWidget::restoreState(not_null memento) { params.highlightPart = memento->highlightPart(); params.highlightPartOffsetHint = memento->highlightPartOffsetHint(); showAtPosition(Data::MessagePosition{ - .fullId = FullMsgId(_history->peer->id, highlight), + .fullId = FullMsgId(_peer->id, highlight), .date = TimeId(0), }, {}, params); } } -void RepliesWidget::resizeEvent(QResizeEvent *e) { +void ChatWidget::resizeEvent(QResizeEvent *e) { if (!width() || !height()) { return; } @@ -2359,14 +2395,14 @@ void RepliesWidget::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } -void RepliesWidget::recountChatWidth() { +void ChatWidget::recountChatWidth() { auto layout = (width() < st::adaptiveChatWideWidth) ? Window::Adaptive::ChatLayout::Normal : Window::Adaptive::ChatLayout::Wide; controller()->adaptive().setChatLayout(layout); } -void RepliesWidget::updateControlsGeometry() { +void ChatWidget::updateControlsGeometry() { const auto contentWidth = width(); const auto newScrollTop = _scroll->isHidden() @@ -2378,10 +2414,10 @@ void RepliesWidget::updateControlsGeometry() { : 0; _topBar->resizeToWidth(contentWidth); _topBarShadow->resize(contentWidth, st::lineWidth); - if (_rootView) { - _rootView->resizeToWidth(contentWidth); + if (_repliesRootView) { + _repliesRootView->resizeToWidth(contentWidth); } - auto top = _topBar->height() + _rootViewHeight; + auto top = _topBar->height() + _repliesRootViewHeight; if (_pinnedBar) { _pinnedBar->move(0, top); _pinnedBar->resizeToWidth(contentWidth); @@ -2427,7 +2463,7 @@ void RepliesWidget::updateControlsGeometry() { _cornerButtons.updatePositions(); } -void RepliesWidget::paintEvent(QPaintEvent *e) { +void ChatWidget::paintEvent(QPaintEvent *e) { if (animatingShow()) { SectionWidget::paintEvent(e); return; @@ -2441,20 +2477,20 @@ void RepliesWidget::paintEvent(QPaintEvent *e) { SectionWidget::PaintBackground(controller(), _theme.get(), this, bg); } -bool RepliesWidget::emptyShown() const { +bool ChatWidget::emptyShown() const { return _topic && (_inner->isEmpty() - || (_topic->lastKnownServerMessageId() == _rootId)); + || (_topic->lastKnownServerMessageId() == _repliesRootId)); } -void RepliesWidget::onScroll() { +void ChatWidget::onScroll() { if (_skipScrollEvent) { return; } updateInnerVisibleArea(); } -void RepliesWidget::updateInnerVisibleArea() { +void ChatWidget::updateInnerVisibleArea() { if (!_inner->animatedScrolling()) { checkReplyReturns(); } @@ -2472,18 +2508,18 @@ void RepliesWidget::updateInnerVisibleArea() { } } -void RepliesWidget::updatePinnedVisibility() { - if (!_loaded) { +void ChatWidget::updatePinnedVisibility() { + if (!_loaded || !_repliesRootId) { return; - } else if (!_topic && (!_root || _root->isEmpty())) { - setPinnedVisibility(!_root); + } else if (!_topic && (!_repliesRoot || _repliesRoot->isEmpty())) { + setPinnedVisibility(!_repliesRoot); return; } const auto rootItem = [&] { - if (const auto group = _history->owner().groups().find(_root)) { + if (const auto group = _history->owner().groups().find(_repliesRoot)) { return group->items.front().get(); } - return _root; + return _repliesRoot; }; const auto view = _inner->viewByPosition(_topic ? Data::MinMessagePosition @@ -2493,14 +2529,14 @@ void RepliesWidget::updatePinnedVisibility() { setPinnedVisibility(visible || (_topic && !view->data()->isPinned())); } -void RepliesWidget::setPinnedVisibility(bool shown) { - if (animatingShow()) { +void ChatWidget::setPinnedVisibility(bool shown) { + if (animatingShow() || !_repliesRootId) { return; } else if (!_topic) { - if (!_rootViewInitScheduled) { + if (!_repliesRootViewInitScheduled) { const auto height = shown ? st::historyReplyHeight : 0; - if (const auto delta = height - _rootViewHeight) { - _rootViewHeight = height; + if (const auto delta = height - _repliesRootViewHeight) { + _repliesRootViewHeight = height; if (_scroll->scrollTop() == _scroll->scrollTopMax()) { setGeometryWithTopMoved(geometry(), delta); } else { @@ -2508,22 +2544,22 @@ void RepliesWidget::setPinnedVisibility(bool shown) { } } } - _rootVisible = shown; - if (!_rootViewInited) { - _rootView->finishAnimating(); - if (!_rootViewInitScheduled) { - _rootViewInitScheduled = true; + _repliesRootVisible = shown; + if (!_repliesRootViewInited) { + _repliesRootView->finishAnimating(); + if (!_repliesRootViewInitScheduled) { + _repliesRootViewInitScheduled = true; InvokeQueued(this, [=] { - _rootViewInited = true; + _repliesRootViewInited = true; }); } } } else { - _rootVisible = shown; + _repliesRootVisible = shown; } } -void RepliesWidget::showAnimatedHook( +void ChatWidget::showAnimatedHook( const Window::SectionSlideParams ¶ms) { _topBar->setAnimatingMode(true); if (params.withTopBarShadow) { @@ -2532,7 +2568,7 @@ void RepliesWidget::showAnimatedHook( _composeControls->showStarted(); } -void RepliesWidget::showFinishedHook() { +void ChatWidget::showFinishedHook() { _topBar->setAnimatingMode(false); if (_joinGroup) { if (Ui::InFocusChain(this)) { @@ -2543,8 +2579,8 @@ void RepliesWidget::showFinishedHook() { _composeControls->showFinished(); } _inner->showFinished(); - if (_rootView) { - _rootView->show(); + if (_repliesRootView) { + _repliesRootView->show(); } if (_pinnedBar) { _pinnedBar->show(); @@ -2561,19 +2597,19 @@ void RepliesWidget::showFinishedHook() { updatePinnedVisibility(); } -bool RepliesWidget::floatPlayerHandleWheelEvent(QEvent *e) { +bool ChatWidget::floatPlayerHandleWheelEvent(QEvent *e) { return _scroll->viewportEvent(e); } -QRect RepliesWidget::floatPlayerAvailableRect() { +QRect ChatWidget::floatPlayerAvailableRect() { return mapToGlobal(_scroll->geometry()); } -Context RepliesWidget::listContext() { +Context ChatWidget::listContext() { return Context::Replies; } -bool RepliesWidget::listScrollTo(int top, bool syntetic) { +bool ChatWidget::listScrollTo(int top, bool syntetic) { top = std::clamp(top, 0, _scroll->scrollTopMax()); const auto scrolled = (_scroll->scrollTop() != top); _synteticScrollEvent = syntetic; @@ -2586,7 +2622,7 @@ bool RepliesWidget::listScrollTo(int top, bool syntetic) { return scrolled; } -void RepliesWidget::listCancelRequest() { +void ChatWidget::listCancelRequest() { if (_composeSearch) { if (_inner && (!_inner->getSelectedItems().empty() @@ -2607,15 +2643,15 @@ void RepliesWidget::listCancelRequest() { controller()->showBackFromStack(); } -void RepliesWidget::listDeleteRequest() { +void ChatWidget::listDeleteRequest() { confirmDeleteSelected(); } -void RepliesWidget::listTryProcessKeyInput(not_null e) { +void ChatWidget::listTryProcessKeyInput(not_null e) { _composeControls->tryProcessKeyInput(e); } -rpl::producer RepliesWidget::listSource( +rpl::producer ChatWidget::listSource( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { @@ -2633,22 +2669,22 @@ rpl::producer RepliesWidget::listSource( }); } -bool RepliesWidget::listAllowsMultiSelect() { +bool ChatWidget::listAllowsMultiSelect() { return true; } -bool RepliesWidget::listIsItemGoodForSelection( +bool ChatWidget::listIsItemGoodForSelection( not_null item) { return item->isRegular() && !item->isService(); } -bool RepliesWidget::listIsLessInOrder( +bool ChatWidget::listIsLessInOrder( not_null first, not_null second) { return first->position() < second->position(); } -void RepliesWidget::listSelectionChanged(SelectedItems &&items) { +void ChatWidget::listSelectionChanged(SelectedItems &&items) { HistoryView::TopBarWidget::SelectedState state; state.count = items.size(); for (const auto &item : items) { @@ -2668,16 +2704,16 @@ void RepliesWidget::listSelectionChanged(SelectedItems &&items) { } } -void RepliesWidget::listMarkReadTill(not_null item) { +void ChatWidget::listMarkReadTill(not_null item) { _replies->readTill(item); } -void RepliesWidget::listMarkContentsRead( +void ChatWidget::listMarkContentsRead( const base::flat_set> &items) { session().api().markContentsRead(items); } -MessagesBarData RepliesWidget::listMessagesBar( +MessagesBarData ChatWidget::listMessagesBar( const std::vector> &elements) { if (elements.empty()) { return {}; @@ -2704,10 +2740,10 @@ MessagesBarData RepliesWidget::listMessagesBar( return {}; } -void RepliesWidget::listContentRefreshed() { +void ChatWidget::listContentRefreshed() { } -void RepliesWidget::listUpdateDateLink( +void ChatWidget::listUpdateDateLink( ClickHandlerPtr &link, not_null view) { if (!_topic) { @@ -2722,17 +2758,17 @@ void RepliesWidget::listUpdateDateLink( } } -bool RepliesWidget::listElementHideReply(not_null view) { +bool ChatWidget::listElementHideReply(not_null view) { if (const auto reply = view->data()->Get()) { const auto replyToPeerId = reply->externalPeerId() ? reply->externalPeerId() - : _history->peer->id; + : _peer->id; if (reply->fields().manualQuote) { return false; - } else if (replyToPeerId == _history->peer->id) { - return (reply->messageId() == _rootId); - } else if (_root) { - const auto forwarded = _root->Get(); + } else if (replyToPeerId == _peer->id) { + return (_repliesRootId && reply->messageId() == _repliesRootId); + } else if (const auto root = _repliesRoot) { + const auto forwarded = root->Get(); if (forwarded && forwarded->savedFromPeer && forwarded->savedFromPeer->id == replyToPeerId @@ -2744,22 +2780,22 @@ bool RepliesWidget::listElementHideReply(not_null view) { return false; } -bool RepliesWidget::listElementShownUnread(not_null view) { +bool ChatWidget::listElementShownUnread(not_null view) { return _replies->isServerSideUnread(view->data()); } -bool RepliesWidget::listIsGoodForAroundPosition( +bool ChatWidget::listIsGoodForAroundPosition( not_null view) { return view->data()->isRegular(); } -void RepliesWidget::listSendBotCommand( +void ChatWidget::listSendBotCommand( const QString &command, const FullMsgId &context) { sendBotCommandWithOptions(command, context, {}); } -void RepliesWidget::sendBotCommandWithOptions( +void ChatWidget::sendBotCommandWithOptions( const QString &command, const FullMsgId &context, Api::SendOptions options) { @@ -2777,7 +2813,7 @@ void RepliesWidget::sendBotCommandWithOptions( } const auto text = Bot::WrapCommandInChat( - _history->peer, + _peer, command, context); auto message = Api::MessageToSend(prepareSendAction(options)); @@ -2786,40 +2822,40 @@ void RepliesWidget::sendBotCommandWithOptions( finishSending(); } -void RepliesWidget::listSearch( +void ChatWidget::listSearch( const QString &query, const FullMsgId &context) { controller()->searchMessages(query, _history); } -void RepliesWidget::listHandleViaClick(not_null bot) { +void ChatWidget::listHandleViaClick(not_null bot) { _composeControls->setText({ '@' + bot->username() + ' ' }); } -not_null RepliesWidget::listChatTheme() { +not_null ChatWidget::listChatTheme() { return _theme.get(); } -CopyRestrictionType RepliesWidget::listCopyRestrictionType( +CopyRestrictionType ChatWidget::listCopyRestrictionType( HistoryItem *item) { - return CopyRestrictionTypeFor(_history->peer, item); + return CopyRestrictionTypeFor(_peer, item); } -CopyRestrictionType RepliesWidget::listCopyMediaRestrictionType( +CopyRestrictionType ChatWidget::listCopyMediaRestrictionType( not_null item) { - return CopyMediaRestrictionTypeFor(_history->peer, item); + return CopyMediaRestrictionTypeFor(_peer, item); } -CopyRestrictionType RepliesWidget::listSelectRestrictionType() { - return SelectRestrictionTypeFor(_history->peer); +CopyRestrictionType ChatWidget::listSelectRestrictionType() { + return SelectRestrictionTypeFor(_peer); } -auto RepliesWidget::listAllowedReactionsValue() +auto ChatWidget::listAllowedReactionsValue() -> rpl::producer { - return Data::PeerAllowedReactionsValue(_history->peer); + return Data::PeerAllowedReactionsValue(_peer); } -void RepliesWidget::listShowPremiumToast(not_null document) { +void ChatWidget::listShowPremiumToast(not_null document) { if (!_stickerToast) { _stickerToast = std::make_unique( controller(), @@ -2829,23 +2865,23 @@ void RepliesWidget::listShowPremiumToast(not_null document) { _stickerToast->showFor(document); } -void RepliesWidget::listOpenPhoto( +void ChatWidget::listOpenPhoto( not_null photo, FullMsgId context) { - controller()->openPhoto(photo, { context, _rootId }); + controller()->openPhoto(photo, { context, _repliesRootId }); } -void RepliesWidget::listOpenDocument( +void ChatWidget::listOpenDocument( not_null document, FullMsgId context, bool showInMediaView) { controller()->openDocument( document, showInMediaView, - { context, _rootId }); + { context, _repliesRootId }); } -void RepliesWidget::listPaintEmpty( +void ChatWidget::listPaintEmpty( Painter &p, const Ui::ChatPaintContext &context) { if (!emptyShown()) { @@ -2856,29 +2892,29 @@ void RepliesWidget::listPaintEmpty( _emptyPainter->paint(p, context.st, width(), _scroll->height()); } -QString RepliesWidget::listElementAuthorRank(not_null view) { +QString ChatWidget::listElementAuthorRank(not_null view) { return (_topic && view->data()->from()->id == _topic->creatorId()) ? tr::lng_topic_author_badge(tr::now) : QString(); } -bool RepliesWidget::listElementHideTopicButton( +bool ChatWidget::listElementHideTopicButton( not_null view) { return true; } -History *RepliesWidget::listTranslateHistory() { +History *ChatWidget::listTranslateHistory() { return _history; } -void RepliesWidget::listAddTranslatedItems( +void ChatWidget::listAddTranslatedItems( not_null tracker) { if (_shownPinnedItem) { tracker->add(_shownPinnedItem); } } -Ui::ChatPaintContext RepliesWidget::listPreparePaintContext( +Ui::ChatPaintContext ChatWidget::listPreparePaintContext( Ui::ChatPaintContextArgs &&args) { auto context = WindowListDelegate::listPreparePaintContext( std::move(args)); @@ -2886,7 +2922,7 @@ Ui::ChatPaintContext RepliesWidget::listPreparePaintContext( return context; } -base::unique_qptr RepliesWidget::listFillSenderUserpicMenu( +base::unique_qptr ChatWidget::listFillSenderUserpicMenu( PeerId userpicPeerId) { const auto searchInEntry = _topic ? Dialogs::Key(_topic) @@ -2903,7 +2939,7 @@ base::unique_qptr RepliesWidget::listFillSenderUserpicMenu( return menu->empty() ? nullptr : std::move(menu); } -void RepliesWidget::setupEmptyPainter() { +void ChatWidget::setupEmptyPainter() { Expects(_topic != nullptr); _emptyPainter = std::make_unique(_topic, [=] { @@ -2918,27 +2954,26 @@ void RepliesWidget::setupEmptyPainter() { }); } -void RepliesWidget::confirmDeleteSelected() { +void ChatWidget::confirmDeleteSelected() { ConfirmDeleteSelectedItems(_inner); } -void RepliesWidget::confirmForwardSelected() { +void ChatWidget::confirmForwardSelected() { ConfirmForwardSelectedItems(_inner); } -void RepliesWidget::clearSelected() { +void ChatWidget::clearSelected() { _inner->cancelSelection(); } -void RepliesWidget::setupDragArea() { +void ChatWidget::setupDragArea() { const auto filter = [=](const auto &d) { if (!_history || _composeControls->isRecording()) { return false; } - const auto peer = _history->peer; return _topic ? Data::CanSendAnyOf(_topic, Data::FilesSendRestrictions()) - : Data::CanSendAnyOf(peer, Data::FilesSendRestrictions()); + : Data::CanSendAnyOf(_peer, Data::FilesSendRestrictions()); }; const auto areas = DragArea::SetupDragAreaToContainer( this, @@ -2956,7 +2991,7 @@ void RepliesWidget::setupDragArea() { areas.photo->setDroppedCallback(droppedCallback(true)); } -void RepliesWidget::setupShortcuts() { +void ChatWidget::setupShortcuts() { Shortcuts::Requests( ) | rpl::filter([=] { return Ui::AppInFocus() @@ -2974,7 +3009,7 @@ void RepliesWidget::setupShortcuts() { }, lifetime()); } -void RepliesWidget::searchInTopic() { +void ChatWidget::searchInTopic() { if (_topic) { controller()->searchInChat(_topic); } else { @@ -2992,7 +3027,7 @@ void RepliesWidget::searchInTopic() { controller(), _history, from); - _composeSearch->setTopMsgId(_rootId); + _composeSearch->setTopMsgId(_repliesRootId); update(); doSetInnerFocus(); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h similarity index 92% rename from Telegram/SourceFiles/history/view/history_view_replies_section.h rename to Telegram/SourceFiles/history/view/history_view_chat_section.h index 3bbe7f5c59..f66391d801 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -64,7 +64,7 @@ struct VoiceToSend; class Element; class TopBarWidget; -class RepliesMemento; +class ChatMemento; class ComposeControls; class ComposeSearch; class SendActionPainter; @@ -74,17 +74,24 @@ class EmptyPainter; class PinnedTracker; class TranslateBar; -class RepliesWidget final +struct ChatViewId { + not_null history; + MsgId repliesRootId; + Data::SavedSublist *sublist = nullptr; + + friend inline bool operator==(ChatViewId, ChatViewId) = default; +}; + +class ChatWidget final : public Window::SectionWidget , private WindowListDelegate , private CornerButtonsDelegate { public: - RepliesWidget( + ChatWidget( QWidget *parent, not_null controller, - not_null history, - MsgId rootId); - ~RepliesWidget(); + ChatViewId id); + ~ChatWidget(); [[nodiscard]] not_null history() const; Dialogs::RowDescriptor activeChat() const override; @@ -114,7 +121,7 @@ public: void setInternalState( const QRect &geometry, - not_null memento); + not_null memento); // Tabbed selector management. bool pushTabbedSelectorToThirdSection( @@ -218,8 +225,8 @@ private: void updateInnerVisibleArea(); void updateControlsGeometry(); void updateAdaptiveLayout(); - void saveState(not_null memento); - void restoreState(not_null memento); + void saveState(not_null memento); + void restoreState(not_null memento); void setReplies(std::shared_ptr replies); void refreshReplies(); void showAtStart(); @@ -267,7 +274,7 @@ private: void chooseAttach(std::optional overrideSendImagesAsPhotos); [[nodiscard]] SendMenu::Details sendMenuDetails() const; [[nodiscard]] FullReplyTo replyTo() const; - [[nodiscard]] HistoryItem *lookupRoot() const; + [[nodiscard]] HistoryItem *lookupRepliesRoot() const; [[nodiscard]] Data::ForumTopic *lookupTopic(); [[nodiscard]] bool computeAreComments() const; void orderWidgets(); @@ -347,16 +354,21 @@ private: [[nodiscard]] bool showSlowmodeError(); const not_null _history; - MsgId _rootId = 0; - std::shared_ptr _theme; - HistoryItem *_root = nullptr; + const not_null _peer; + ChatViewId _id; + + MsgId _repliesRootId = 0; + HistoryItem *_repliesRoot = nullptr; Data::ForumTopic *_topic = nullptr; mutable bool _newTopicDiscarded = false; - std::shared_ptr _replies; rpl::lifetime _repliesLifetime; rpl::variable _areComments = false; + + Data::SavedSublist *_sublist = nullptr; + std::shared_ptr _sendAction; + std::shared_ptr _theme; QPointer _inner; object_ptr _topBar; object_ptr _topBarShadow; @@ -380,11 +392,11 @@ private: std::optional _minPinnedId; HistoryItem *_shownPinnedItem = nullptr; - std::unique_ptr _rootView; - int _rootViewHeight = 0; - bool _rootViewInited = false; - bool _rootViewInitScheduled = false; - rpl::variable _rootVisible = false; + std::unique_ptr _repliesRootView; + int _repliesRootViewHeight = 0; + bool _repliesRootViewInited = false; + bool _repliesRootViewInitScheduled = false; + rpl::variable _repliesRootVisible = false; std::unique_ptr _scroll; std::unique_ptr _stickerToast; @@ -408,15 +420,18 @@ private: }; -class RepliesMemento final : public Window::SectionMemento { +class ChatMemento final : public Window::SectionMemento { public: - RepliesMemento( - not_null history, - MsgId rootId, + explicit ChatMemento( + ChatViewId id, MsgId highlightId = 0, const TextWithEntities &highlightPart = {}, int highlightPartOffsetHint = 0); - explicit RepliesMemento( + + struct Comments { + }; + explicit ChatMemento( + Comments, not_null commentsItem, MsgId commentId = 0); @@ -431,11 +446,8 @@ public: Window::Column column, const QRect &geometry) override; - [[nodiscard]] not_null getHistory() const { - return _history; - } - [[nodiscard]] MsgId getRootId() const { - return _rootId; + [[nodiscard]] ChatViewId id() const { + return _id; } void setReplies(std::shared_ptr replies) { @@ -472,8 +484,7 @@ public: private: void setupTopicViewer(); - const not_null _history; - MsgId _rootId = 0; + ChatViewId _id; const TextWithEntities _highlightPart; const int _highlightPartOffsetHint = 0; const MsgId _highlightId = 0; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 3f312386bb..f34c3a1084 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_config.h" #include "history/history.h" #include "history/history_item_components.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "lang/lang_keys.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_custom_emoji.h" @@ -1221,11 +1221,12 @@ Window::SessionController *Manager::openNotificationMessage( if (window) { window->widget()->showFromTray(); if (topic) { + using namespace HistoryView; window->showSection( - std::make_shared( - history, - topic->rootId(), - itemId), + std::make_shared(ChatViewId{ + .history = history, + .repliesRootId = topic->rootId(), + }, itemId), SectionShow::Way::Forward); } else { window->showPeerHistory( diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 2d9af18ff0..d8950d56c5 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -25,7 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/view/reactions/history_view_reactions.h" //#include "history/view/reactions/history_view_reactions_button.h" -#include "history/view/history_view_replies_section.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_scheduled_section.h" #include "history/view/history_view_sublist_section.h" #include "media/player/media_player_instance.h" @@ -1140,9 +1140,12 @@ void SessionNavigation::showRepliesForMessage( if (const auto topic = history->peer->forumTopicFor(rootId)) { auto replies = topic->replies(); if (replies->unreadCountKnown()) { - auto memento = std::make_shared( - history, - rootId, + using namespace HistoryView; + auto memento = std::make_shared( + ChatViewId{ + .history = history, + .repliesRootId = rootId, + }, commentId, params.highlightPart, params.highlightPartOffsetHint); @@ -1156,7 +1159,7 @@ void SessionNavigation::showRepliesForMessage( && _showingRepliesRootId == rootId) { return; } else if (!history->peer->asChannel()) { - // HistoryView::RepliesWidget right now handles only channels. + // HistoryView::ChatWidget replies right now handles only channels. return; } _api.request(base::take(_showingRepliesRequestId)).cancel(); @@ -1211,14 +1214,16 @@ void SessionNavigation::showRepliesForMessage( } } if (deleted || item) { + using namespace HistoryView; auto memento = item - ? std::make_shared( + ? std::make_shared( + ChatMemento::Comments(), item, commentId) - : std::make_shared( - history, - rootId, - commentId); + : std::make_shared(ChatViewId{ + .history = history, + .repliesRootId = rootId, + }, commentId); memento->setReadInformation( data.vread_inbox_max_id().value_or_empty(), data.vunread_count().v, From 21f840335707263e64a09a37255b29bf018abbf2 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 9 May 2025 16:45:44 +0400 Subject: [PATCH 057/310] Merge SublistSection into ChatSection. --- Telegram/CMakeLists.txt | 2 - .../SourceFiles/data/data_saved_sublist.cpp | 13 +- .../SourceFiles/data/data_saved_sublist.h | 2 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 12 +- .../view/history_view_chat_section.cpp | 334 ++++++-- .../history/view/history_view_chat_section.h | 28 +- .../view/history_view_sublist_section.cpp | 795 ------------------ .../view/history_view_sublist_section.h | 237 ------ .../info/media/info_media_buttons.cpp | 11 +- .../info/saved/info_saved_sublists_widget.cpp | 9 +- Telegram/SourceFiles/mainwidget.cpp | 9 +- .../window/window_session_controller.cpp | 8 +- 12 files changed, 355 insertions(+), 1105 deletions(-) delete mode 100644 Telegram/SourceFiles/history/view/history_view_sublist_section.cpp delete mode 100644 Telegram/SourceFiles/history/view/history_view_sublist_section.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1d52a00a53..85a7703fd6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -888,8 +888,6 @@ PRIVATE history/view/history_view_sponsored_click_handler.h history/view/history_view_sticker_toast.cpp history/view/history_view_sticker_toast.h - history/view/history_view_sublist_section.cpp - history/view/history_view_sublist_section.h history/view/history_view_text_helper.cpp history/view/history_view_text_helper.h history/view/history_view_transcribe_button.cpp diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 134ada5295..0ecfcf6739 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_peer.h" +#include "data/data_user.h" #include "data/data_saved_messages.h" #include "data/data_session.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" #include "history/history_item.h" +#include "main/main_session.h" namespace Data { @@ -31,12 +33,15 @@ not_null SavedSublist::parent() const { return _parent; } -ChannelData *SavedSublist::parentChat() const { - return _parent->parentChat(); +not_null SavedSublist::parentHistory() const { + const auto chat = parentChat(); + return _history->owner().history(chat + ? (PeerData*)chat + : _history->session().user().get()); } -not_null SavedSublist::history() const { - return _history; +ChannelData *SavedSublist::parentChat() const { + return _parent->parentChat(); } not_null SavedSublist::peer() const { diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 8e59854e45..669aa97311 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -24,8 +24,8 @@ public: ~SavedSublist(); [[nodiscard]] not_null parent() const; + [[nodiscard]] not_null parentHistory() const; [[nodiscard]] ChannelData *parentChat() const; - [[nodiscard]] not_null history() const; [[nodiscard]] not_null peer() const; [[nodiscard]] bool isHiddenAuthor() const; [[nodiscard]] bool isFullLoaded() const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 31dd678231..b593d960b5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -21,11 +21,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_key.h" #include "history/history.h" #include "history/history_item.h" -#include "history/view/history_view_top_bar_widget.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_contact_status.h" -#include "history/view/history_view_requests_bar.h" #include "history/view/history_view_group_call_bar.h" -#include "history/view/history_view_sublist_section.h" +#include "history/view/history_view_requests_bar.h" +#include "history/view/history_view_top_bar_widget.h" #include "boxes/peers/edit_peer_requests_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -998,8 +998,12 @@ void Widget::chosenRow(const ChosenRow &row) { using namespace Window; auto params = SectionShow(SectionShow::Way::Forward); params.dropSameFromStack = true; + using namespace HistoryView; controller()->showSection( - std::make_shared(sublist), + std::make_shared(ChatViewId{ + .history = sublist->parentHistory(), + .sublist = sublist, + }), params); } if (row.filteredRow && !session().supportMode()) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 250d952f0c..b70bc08ec7 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -57,6 +57,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "data/components/scheduled_messages.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_chat.h" @@ -120,7 +122,7 @@ ChatMemento::ChatMemento( , _highlightPart(highlightPart) , _highlightPartOffsetHint(highlightPartOffsetHint) , _highlightId(highlightId) { - if (highlightId) { + if (highlightId || _id.sublist) { _list.setAroundPosition({ .fullId = FullMsgId(_id.history->peer->id, highlightId), .date = TimeId(0), @@ -271,6 +273,8 @@ ChatWidget::ChatWidget( setupRoot(); setupRootView(); + setupOpenChatButton(); + setupAboutHiddenAuthor(); setupShortcuts(); setupTranslateBar(); @@ -300,9 +304,7 @@ ChatWidget::ChatWidget( }, _topBar->lifetime()); _topBar->searchRequest( ) | rpl::start_with_next([=] { - if (!preventsClose(crl::guard(this, [=]{ searchInTopic(); }))) { - searchInTopic(); - } + searchRequested(); }, _topBar->lifetime()); controller->adaptive().value( @@ -427,8 +429,10 @@ ChatWidget::ChatWidget( ChatWidget::~ChatWidget() { base::take(_sendAction); - session().api().saveCurrentDraftToCloud(); - controller()->sendingAnimation().clear(); + if (_repliesRootId) { + session().api().saveCurrentDraftToCloud(); + controller()->sendingAnimation().clear(); + } if (_topic) { if (_topic->creating()) { _emptyPainter = nullptr; @@ -1494,6 +1498,9 @@ void ChatWidget::edit( } void ChatWidget::refreshJoinGroupButton() { + if (!_repliesRootId) { + return; + } const auto set = [&](std::unique_ptr button) { if (!button && !_joinGroup) { return; @@ -1518,8 +1525,10 @@ void ChatWidget::refreshJoinGroupButton() { ? Data::CanSendAnything(channel) : (_topic && Data::CanSendAnything(_topic)); if (channel->amIn() || canSend) { + _canSendTexts = true; set(nullptr); } else { + _canSendTexts = false; if (!_joinGroup) { set(std::make_unique( this, @@ -1697,9 +1706,16 @@ FullReplyTo ChatWidget::replyTo() const { void ChatWidget::refreshTopBarActiveChat() { using namespace Dialogs; + const auto state = EntryState{ - .key = (_topic ? Key{ _topic } : Key{ _history }), - .section = EntryState::Section::Replies, + .key = (_sublist + ? Key{ _sublist } + : _topic + ? Key{ _topic } + : Key{ _history }), + .section = _sublist + ? EntryState::Section::SavedSublist + : EntryState::Section::Replies, .currentReplyTo = replyTo(), }; _topBar->setActiveChat(state, _sendAction.get()); @@ -1755,6 +1771,53 @@ void ChatWidget::checkLastPinnedClickedIdReset( } } +void ChatWidget::setupOpenChatButton() { + if (!_sublist || _sublist->peer()->isSavedHiddenAuthor()) { + return; + } else if (_sublist->parentChat()) { + _canSendTexts = true; + return; + } + _openChatButton = std::make_unique( + this, + (_sublist->peer()->isBroadcast() + ? tr::lng_saved_open_channel(tr::now) + : _sublist->peer()->isUser() + ? tr::lng_saved_open_chat(tr::now) + : tr::lng_saved_open_group(tr::now)), + st::historyComposeButton); + + _openChatButton->setClickedCallback([=] { + controller()->showPeerHistory( + _sublist->peer(), + Window::SectionShow::Way::Forward); + }); +} + +void ChatWidget::setupAboutHiddenAuthor() { + if (!_sublist || !_sublist->peer()->isSavedHiddenAuthor()) { + return; + } else if (_sublist->parentChat()) { + _canSendTexts = true; + return; + } + _aboutHiddenAuthor = std::make_unique(this); + _aboutHiddenAuthor->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_aboutHiddenAuthor.get()); + auto rect = _aboutHiddenAuthor->rect(); + + p.fillRect(rect, st::historyReplyBg); + + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawText( + rect.marginsRemoved( + QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), + tr::lng_saved_about_hidden(tr::now), + style::al_center); + }, _aboutHiddenAuthor->lifetime()); +} + void ChatWidget::setupTranslateBar() { controller()->adaptive().oneColumnValue( ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { @@ -2031,7 +2094,11 @@ void ChatWidget::cornerButtonsShowAtPosition( } Data::Thread *ChatWidget::cornerButtonsThread() { - return _topic ? static_cast(_topic) : _history; + return _sublist + ? nullptr + : _topic + ? static_cast(_topic) + : _history; } FullMsgId ChatWidget::cornerButtonsCurrentId() { @@ -2094,7 +2161,8 @@ void ChatWidget::showAtPosition( const Window::SectionShow ¶ms) { _lastShownAt = position.fullId; controller()->setActiveChatEntry(activeChat()); - const auto ignore = (position.fullId.msg == _repliesRootId); + const auto ignore = _repliesRootId + && (position.fullId.msg == _repliesRootId); _inner->showAtPosition( position, params, @@ -2107,15 +2175,13 @@ void ChatWidget::updateAdaptiveLayout() { _topBar->height()); } -not_null ChatWidget::history() const { - return _history; -} - Dialogs::RowDescriptor ChatWidget::activeChat() const { const auto messageId = _lastShownAt ? _lastShownAt : FullMsgId(_peer->id, ShowAtUnreadMsgId); - if (_topic) { + if (_sublist) { + return { _sublist, messageId }; + } else if (_topic) { return { _topic, messageId }; } return { _history, messageId }; @@ -2205,6 +2271,10 @@ bool ChatWidget::showInternal( return false; } +bool ChatWidget::sameTypeAs(not_null memento) { + return dynamic_cast(memento.get()) != nullptr; +} + void ChatWidget::setInternalState( const QRect &geometry, not_null memento) { @@ -2242,11 +2312,14 @@ bool ChatWidget::showMessage( const auto message = _history->owner().message(id); if (!message) { return false; - } - if (_repliesRootId + } else if (_repliesRootId && !message->inThread(_repliesRootId) && id.msg != _repliesRootId) { return false; + } else if (_sublist && message->savedSublist() != _sublist) { + return false; + } else { + Unexpected("ChatWidget::showMessage context."); } const auto originMessage = [&]() -> HistoryItem* { using OriginMessage = Window::SectionShow::OriginMessage; @@ -2257,6 +2330,9 @@ bool ChatWidget::showMessage( } else if (_repliesRootId && returnTo->inThread(_repliesRootId)) { return returnTo; + } else if (_sublist + && returnTo->savedSublist() == _sublist) { + return returnTo; } } } @@ -2274,7 +2350,9 @@ bool ChatWidget::showMessage( Window::SectionActionResult ChatWidget::sendBotCommand( Bot::SendCommandRequest request) { - if (request.peer != _peer) { + if (!_repliesRootId) { + return Window::SectionActionResult::Fallback; + } else if (request.peer != _peer) { return Window::SectionActionResult::Ignore; } listSendBotCommand(request.command, request.context); @@ -2368,7 +2446,7 @@ void ChatWidget::setReplies(std::shared_ptr replies) { void ChatWidget::restoreState(not_null memento) { if (auto replies = memento->getReplies()) { setReplies(std::move(replies)); - } else if (!_replies) { + } else if (!_replies && _repliesRootId) { refreshReplies(); } _cornerButtons.setReplyReturns(memento->replyReturns()); @@ -2431,11 +2509,24 @@ void ChatWidget::updateControlsGeometry() { _translateBar->resizeToWidth(contentWidth); top += _translateBarHeight; - const auto bottom = height(); - const auto controlsHeight = _joinGroup - ? _joinGroup->height() - : _composeControls->heightCurrent(); - const auto scrollHeight = bottom - top - controlsHeight; + auto bottom = height(); + if (_openChatButton) { + _openChatButton->resizeToWidth(width()); + bottom -= _openChatButton->height(); + _openChatButton->move(0, bottom); + } else if (_aboutHiddenAuthor) { + _aboutHiddenAuthor->resize(width(), st::historyUnblock.height); + bottom -= _aboutHiddenAuthor->height(); + _aboutHiddenAuthor->move(0, bottom); + } else if (_joinGroup) { + _joinGroup->resizeToWidth(width()); + bottom -= _joinGroup->height(); + _joinGroup->move(0, bottom); + } else { + bottom -= _composeControls->heightCurrent(); + } + + const auto scrollHeight = bottom - top; const auto scrollSize = QSize(contentWidth, scrollHeight); if (_scroll->size() != scrollSize) { _skipScrollEvent = true; @@ -2450,14 +2541,7 @@ void ChatWidget::updateControlsGeometry() { } updateInnerVisibleArea(); } - if (_joinGroup) { - _joinGroup->setGeometry( - 0, - bottom - _joinGroup->height(), - contentWidth, - _joinGroup->height()); - } - _composeControls->move(0, bottom - controlsHeight); + _composeControls->move(0, bottom); _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); _cornerButtons.updatePositions(); @@ -2570,7 +2654,7 @@ void ChatWidget::showAnimatedHook( void ChatWidget::showFinishedHook() { _topBar->setAnimatingMode(false); - if (_joinGroup) { + if (_joinGroup || _openChatButton || _aboutHiddenAuthor) { if (Ui::InFocusChain(this)) { _inner->setFocus(); } @@ -2606,7 +2690,7 @@ QRect ChatWidget::floatPlayerAvailableRect() { } Context ChatWidget::listContext() { - return Context::Replies; + return _sublist ? Context::SavedSublist : Context::Replies; } bool ChatWidget::listScrollTo(int top, bool syntetic) { @@ -2651,24 +2735,97 @@ void ChatWidget::listTryProcessKeyInput(not_null e) { _composeControls->tryProcessKeyInput(e); } +void ChatWidget::markLoaded() { + if (!_loaded) { + _loaded = true; + crl::on_main(this, [=] { + updatePinnedVisibility(); + }); + } +} + rpl::producer ChatWidget::listSource( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { + if (_replies) { + return repliesSource(aroundId, limitBefore, limitAfter); + } else if (_sublist) { + return sublistSource(aroundId, limitBefore, limitAfter); + } + Unexpected("ChatWidget::listSource in unknown mode"); +} + +rpl::producer ChatWidget::repliesSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { return _replies->source( aroundId, limitBefore, limitAfter ) | rpl::before_next([=] { // after_next makes a copy of value. - if (!_loaded) { - _loaded = true; - crl::on_main(this, [=] { - updatePinnedVisibility(); - }); - } + markLoaded(); }); } +rpl::producer ChatWidget::sublistSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto messageId = aroundId.fullId.msg + ? aroundId.fullId.msg + : (ServerMaxMsgId - 1); + return [=](auto consumer) { + const auto pushSlice = [=] { + auto result = Data::MessagesSlice(); + result.fullCount = _sublist->fullCount(); + _topBar->setCustomTitle(result.fullCount + ? tr::lng_forum_messages( + tr::now, + lt_count_decimal, + *result.fullCount) + : tr::lng_contacts_loading(tr::now)); + const auto &messages = _sublist->messages(); + const auto i = ranges::lower_bound( + messages, + messageId, + ranges::greater(), + [](not_null item) { return item->id; }); + const auto before = int(end(messages) - i); + const auto useBefore = std::min(before, limitBefore); + const auto after = int(i - begin(messages)); + const auto useAfter = std::min(after, limitAfter); + const auto from = i - useAfter; + const auto till = i + useBefore; + auto nearestDistance = std::numeric_limits::max(); + result.ids.reserve(useAfter + useBefore); + for (auto j = till; j != from;) { + const auto item = *--j; + result.ids.push_back(item->fullId()); + const auto distance = std::abs((messageId - item->id).bare); + if (nearestDistance > distance) { + nearestDistance = distance; + result.nearestToAround = result.ids.back(); + } + } + result.skippedAfter = after - useAfter; + result.skippedBefore = result.fullCount + ? (*result.fullCount - after - useBefore) + : std::optional(); + if (!result.fullCount || useBefore < limitBefore) { + _sublist->parent()->loadMore(_sublist); + } + markLoaded(); + consumer.put_next(std::move(result)); + }; + auto lifetime = rpl::lifetime(); + _sublist->changes() | rpl::start_with_next(pushSlice, lifetime); + pushSlice(); + return lifetime; + }; +} + bool ChatWidget::listAllowsMultiSelect() { return true; } @@ -2681,7 +2838,9 @@ bool ChatWidget::listIsItemGoodForSelection( bool ChatWidget::listIsLessInOrder( not_null first, not_null second) { - return first->position() < second->position(); + return _sublist + ? (first->id < second->id) + : first->position() < second->position(); } void ChatWidget::listSelectionChanged(SelectedItems &&items) { @@ -2705,17 +2864,21 @@ void ChatWidget::listSelectionChanged(SelectedItems &&items) { } void ChatWidget::listMarkReadTill(not_null item) { - _replies->readTill(item); + if (_replies) { + _replies->readTill(item); + } } void ChatWidget::listMarkContentsRead( const base::flat_set> &items) { - session().api().markContentsRead(items); + if (!_sublist) { + session().api().markContentsRead(items); + } } MessagesBarData ChatWidget::listMessagesBar( const std::vector> &elements) { - if (elements.empty()) { + if (_sublist || elements.empty()) { return {}; } const auto till = _replies->computeInboxReadTillFull(); @@ -2759,7 +2922,9 @@ void ChatWidget::listUpdateDateLink( } bool ChatWidget::listElementHideReply(not_null view) { - if (const auto reply = view->data()->Get()) { + if (_sublist) { + return false; + } else if (const auto reply = view->data()->Get()) { const auto replyToPeerId = reply->externalPeerId() ? reply->externalPeerId() : _peer->id; @@ -2781,7 +2946,9 @@ bool ChatWidget::listElementHideReply(not_null view) { } bool ChatWidget::listElementShownUnread(not_null view) { - return _replies->isServerSideUnread(view->data()); + return _replies + ? _replies->isServerSideUnread(view->data()) + : view->data()->unread(view->data()->history()); } bool ChatWidget::listIsGoodForAroundPosition( @@ -2792,7 +2959,9 @@ bool ChatWidget::listIsGoodForAroundPosition( void ChatWidget::listSendBotCommand( const QString &command, const FullMsgId &context) { - sendBotCommandWithOptions(command, context, {}); + if (!_sublist) { + sendBotCommandWithOptions(command, context, {}); + } } void ChatWidget::sendBotCommandWithOptions( @@ -2825,11 +2994,18 @@ void ChatWidget::sendBotCommandWithOptions( void ChatWidget::listSearch( const QString &query, const FullMsgId &context) { - controller()->searchMessages(query, _history); + const auto inChat = !_sublist + ? Dialogs::Key(_history) + : Data::SearchTagFromQuery(query) + ? Dialogs::Key(_sublist) + : Dialogs::Key(); + controller()->searchMessages(query, inChat); } void ChatWidget::listHandleViaClick(not_null bot) { - _composeControls->setText({ '@' + bot->username() + ' ' }); + if (_canSendTexts) { + _composeControls->setText({ '@' + bot->username() + ' ' }); + } } not_null ChatWidget::listChatTheme() { @@ -3001,14 +3177,20 @@ void ChatWidget::setupShortcuts() { }) | rpl::start_with_next([=](not_null request) { using Command = Shortcuts::Command; request->check(Command::Search, 1) && request->handle([=] { - if (!preventsClose(crl::guard(this, [=]{ searchInTopic(); }))) { - searchInTopic(); - } + searchRequested(); return true; }); }, lifetime()); } +void ChatWidget::searchRequested() { + if (_sublist) { + controller()->searchInChat(_sublist); + } else if (!preventsClose(crl::guard(this, [=] { searchInTopic(); }))) { + searchInTopic(); + } +} + void ChatWidget::searchInTopic() { if (_topic) { controller()->searchInChat(_topic); @@ -3048,4 +3230,52 @@ void ChatWidget::searchInTopic() { } } +bool ChatWidget::searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom) { + const auto sublist = chat.sublist(); + if (!sublist || sublist != _sublist) { + return false; + } else if (_composeSearch) { + _composeSearch->setQuery(query); + _composeSearch->setInnerFocus(); + return true; + } + _composeSearch = std::make_unique( + this, + controller(), + _history, + sublist->peer(), + query); + + updateControlsGeometry(); + setInnerFocus(); + + _composeSearch->activations( + ) | rpl::start_with_next([=](ComposeSearch::Activation activation) { + const auto item = activation.item; + auto params = ::Window::SectionShow( + ::Window::SectionShow::Way::ClearStack); + params.highlightPart = { activation.query }; + params.highlightPartOffsetHint = kSearchQueryOffsetHint; + controller()->showPeerHistory( + item->history()->peer->id, + params, + item->fullId().msg); + }, _composeSearch->lifetime()); + + _composeSearch->destroyRequests( + ) | rpl::take( + 1 + ) | rpl::start_with_next([=] { + _composeSearch = nullptr; + + updateControlsGeometry(); + setInnerFocus(); + }, _composeSearch->lifetime()); + + return true; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index f66391d801..c38fe6dbe8 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -93,7 +93,9 @@ public: ChatViewId id); ~ChatWidget(); - [[nodiscard]] not_null history() const; + [[nodiscard]] ChatViewId id() const { + return _id; + } Dialogs::RowDescriptor activeChat() const override; bool preventsClose(Fn &&continueCallback) const override; @@ -107,6 +109,7 @@ public: bool showInternal( not_null memento, const Window::SectionShow ¶ms) override; + bool sameTypeAs(not_null memento) override; std::shared_ptr createMemento() override; bool showMessage( PeerId peerId, @@ -116,6 +119,11 @@ public: Window::SectionActionResult sendBotCommand( Bot::SendCommandRequest request) override; + bool searchInChatEmbedded( + QString query, + Dialogs::Key chat, + PeerData *searchFrom = nullptr) override; + bool confirmSendingFiles(const QStringList &files) override; bool confirmSendingFiles(not_null data) override; @@ -221,6 +229,16 @@ private: int starsApproved, Fn withPaymentApproved); + void markLoaded(); + [[nodiscard]] rpl::producer repliesSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter); + [[nodiscard]] rpl::producer sublistSource( + Data::MessagePosition aroundId, + int limitBefore, + int limitAfter); + void onScroll(); void updateInnerVisibleArea(); void updateControlsGeometry(); @@ -249,10 +267,15 @@ private: void subscribeToTopic(); void subscribeToPinnedMessages(); void setTopic(Data::ForumTopic *topic); + + void setupOpenChatButton(); + void setupAboutHiddenAuthor(); + void setupDragArea(); void setupShortcuts(); void setupTranslateBar(); + void searchRequested(); void searchInTopic(); void updatePinnedVisibility(); @@ -377,7 +400,10 @@ private: std::unique_ptr _joinGroup; std::unique_ptr _payForMessage; std::unique_ptr _topicReopenBar; + std::unique_ptr _openChatButton; + std::unique_ptr _aboutHiddenAuthor; std::unique_ptr _emptyPainter; + bool _canSendTexts = false; bool _skipScrollEvent = false; bool _synteticScrollEvent = false; diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp b/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp deleted file mode 100644 index c3f74ac708..0000000000 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.cpp +++ /dev/null @@ -1,795 +0,0 @@ -/* -This file is part of Telegram Desktop, -the official desktop application for the Telegram messaging service. - -For license and copyright information please follow this link: -https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL -*/ -#include "history/view/history_view_sublist_section.h" - -#include "main/main_session.h" -#include "core/application.h" -#include "core/shortcuts.h" -#include "data/data_message_reaction_id.h" -#include "data/data_saved_messages.h" -#include "data/data_saved_sublist.h" -#include "data/data_session.h" -#include "data/data_peer_values.h" -#include "data/data_user.h" -#include "history/view/controls/history_view_compose_search.h" -#include "history/view/history_view_top_bar_widget.h" -#include "history/view/history_view_translate_bar.h" -#include "history/view/history_view_list_widget.h" -#include "history/history.h" -#include "history/history_item.h" -#include "history/history_view_swipe_back_session.h" -#include "lang/lang_keys.h" -#include "ui/chat/chat_style.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/scroll_area.h" -#include "ui/widgets/shadow.h" -#include "ui/ui_utility.h" -#include "window/window_session_controller.h" -#include "styles/style_chat.h" -#include "styles/style_chat_helpers.h" -#include "styles/style_window.h" - -namespace HistoryView { -namespace { - -} // namespace - -SublistMemento::SublistMemento(not_null sublist) -: _sublist(sublist) { - const auto selfId = sublist->session().userPeerId(); - _list.setAroundPosition({ - .fullId = FullMsgId(selfId, ShowAtUnreadMsgId), - .date = TimeId(0), - }); -} - -object_ptr SublistMemento::createWidget( - QWidget *parent, - not_null controller, - Window::Column column, - const QRect &geometry) { - if (column == Window::Column::Third) { - return nullptr; - } - auto result = object_ptr( - parent, - controller, - _sublist); - result->setInternalState(geometry, this); - return result; -} - -SublistWidget::SublistWidget( - QWidget *parent, - not_null controller, - not_null sublist) -: Window::SectionWidget(parent, controller, sublist->peer()) -, WindowListDelegate(controller) -, _sublist(sublist) -, _history(sublist->owner().history(sublist->session().user())) -, _topBar(this, controller) -, _topBarShadow(this) -, _translateBar(std::make_unique(this, controller, _history)) -, _scroll(std::make_unique( - this, - controller->chatStyle()->value(lifetime(), st::historyScroll), - false)) -, _cornerButtons( - _scroll.get(), - controller->chatStyle(), - static_cast(this)) { - controller->chatStyle()->paletteChanged( - ) | rpl::start_with_next([=] { - _scroll->updateBars(); - }, _scroll->lifetime()); - - setupOpenChatButton(); - setupAboutHiddenAuthor(); - - Window::ChatThemeValueFromPeer( - controller, - sublist->peer() - ) | rpl::start_with_next([=](std::shared_ptr &&theme) { - _theme = std::move(theme); - controller->setChatStyleTheme(_theme); - }, lifetime()); - - _topBar->setActiveChat( - TopBarWidget::ActiveChat{ - .key = sublist, - .section = Dialogs::EntryState::Section::SavedSublist, - }, - nullptr); - - _topBar->move(0, 0); - _topBar->resizeToWidth(width()); - _topBar->show(); - - _topBar->deleteSelectionRequest( - ) | rpl::start_with_next([=] { - confirmDeleteSelected(); - }, _topBar->lifetime()); - _topBar->forwardSelectionRequest( - ) | rpl::start_with_next([=] { - confirmForwardSelected(); - }, _topBar->lifetime()); - _topBar->clearSelectionRequest( - ) | rpl::start_with_next([=] { - clearSelected(); - }, _topBar->lifetime()); - _topBar->searchRequest( - ) | rpl::start_with_next([=] { - searchInSublist(); - }, _topBar->lifetime()); - - _translateBar->raise(); - _topBarShadow->raise(); - controller->adaptive().value( - ) | rpl::start_with_next([=] { - updateAdaptiveLayout(); - }, lifetime()); - - _inner = _scroll->setOwnedWidget(object_ptr( - this, - &controller->session(), - static_cast(this))); - _scroll->move(0, _topBar->height()); - _scroll->show(); - _scroll->scrolls( - ) | rpl::start_with_next([=] { - onScroll(); - }, lifetime()); - - setupShortcuts(); - setupTranslateBar(); - Window::SetupSwipeBackSection(this, _scroll.get(), _inner); -} - -SublistWidget::~SublistWidget() = default; - -void SublistWidget::setupOpenChatButton() { - if (_sublist->peer()->isSavedHiddenAuthor()) { - return; - } - _openChatButton = std::make_unique( - this, - (_sublist->peer()->isBroadcast() - ? tr::lng_saved_open_channel(tr::now) - : _sublist->peer()->isUser() - ? tr::lng_saved_open_chat(tr::now) - : tr::lng_saved_open_group(tr::now)), - st::historyComposeButton); - - _openChatButton->setClickedCallback([=] { - controller()->showPeerHistory( - _sublist->peer(), - Window::SectionShow::Way::Forward); - }); -} - -void SublistWidget::setupAboutHiddenAuthor() { - if (!_sublist->peer()->isSavedHiddenAuthor()) { - return; - } - _aboutHiddenAuthor = std::make_unique(this); - _aboutHiddenAuthor->paintRequest() | rpl::start_with_next([=] { - auto p = QPainter(_aboutHiddenAuthor.get()); - auto rect = _aboutHiddenAuthor->rect(); - - p.fillRect(rect, st::historyReplyBg); - - p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); - p.drawText( - rect.marginsRemoved( - QMargins(st::historySendPadding, 0, st::historySendPadding, 0)), - tr::lng_saved_about_hidden(tr::now), - style::al_center); - }, _aboutHiddenAuthor->lifetime()); -} - -void SublistWidget::setupTranslateBar() { - controller()->adaptive().oneColumnValue( - ) | rpl::start_with_next([=, raw = _translateBar.get()](bool one) { - raw->setShadowGeometryPostprocess([=](QRect geometry) { - if (!one) { - geometry.setLeft(geometry.left() + st::lineWidth); - } - return geometry; - }); - }, _translateBar->lifetime()); - - _translateBarHeight = 0; - _translateBar->heightValue( - ) | rpl::start_with_next([=](int height) { - if (const auto delta = height - _translateBarHeight) { - _translateBarHeight = height; - setGeometryWithTopMoved(geometry(), delta); - } - }, _translateBar->lifetime()); - - _translateBar->finishAnimating(); -} - -void SublistWidget::cornerButtonsShowAtPosition( - Data::MessagePosition position) { - showAtPosition(position); -} - -Data::Thread *SublistWidget::cornerButtonsThread() { - return nullptr; -} - -FullMsgId SublistWidget::cornerButtonsCurrentId() { - return _lastShownAt; -} - -bool SublistWidget::cornerButtonsIgnoreVisibility() { - return animatingShow(); -} - -std::optional SublistWidget::cornerButtonsDownShown() { - const auto top = _scroll->scrollTop() + st::historyToDownShownAfter; - if (top < _scroll->scrollTopMax() || _cornerButtons.replyReturn()) { - return true; - } else if (_inner->loadedAtBottomKnown()) { - return !_inner->loadedAtBottom(); - } - return std::nullopt; -} - -bool SublistWidget::cornerButtonsUnreadMayBeShown() { - return _inner->loadedAtBottomKnown(); -} - -bool SublistWidget::cornerButtonsHas(CornerButtonType type) { - return (type == CornerButtonType::Down); -} - -void SublistWidget::showAtPosition( - Data::MessagePosition position, - FullMsgId originId) { - showAtPosition(position, originId, {}); -} - -void SublistWidget::showAtPosition( - Data::MessagePosition position, - FullMsgId originItemId, - const Window::SectionShow ¶ms) { - _lastShownAt = position.fullId; - controller()->setActiveChatEntry(activeChat()); - _inner->showAtPosition( - position, - params, - _cornerButtons.doneJumpFrom(position.fullId, originItemId)); -} -void SublistWidget::updateAdaptiveLayout() { - _topBarShadow->moveToLeft( - controller()->adaptive().isOneColumn() ? 0 : st::lineWidth, - _topBar->height()); -} - -not_null SublistWidget::sublist() const { - return _sublist; -} - -Dialogs::RowDescriptor SublistWidget::activeChat() const { - const auto messageId = _lastShownAt - ? _lastShownAt - : FullMsgId(_history->peer->id, ShowAtUnreadMsgId); - return { _sublist, messageId }; -} - -QPixmap SublistWidget::grabForShowAnimation( - const Window::SectionSlideParams ¶ms) { - _topBar->updateControlsVisibility(); - if (params.withTopBarShadow) _topBarShadow->hide(); - auto result = Ui::GrabWidget(this); - if (params.withTopBarShadow) _topBarShadow->show(); - _translateBar->hide(); - return result; -} - -void SublistWidget::checkActivation() { - _inner->checkActivation(); -} - -void SublistWidget::doSetInnerFocus() { - if (_composeSearch) { - _composeSearch->setInnerFocus(); - } else { - _inner->setFocus(); - } -} - -bool SublistWidget::showInternal( - not_null memento, - const Window::SectionShow ¶ms) { - if (auto logMemento = dynamic_cast(memento.get())) { - if (logMemento->getSublist() == sublist()) { - restoreState(logMemento); - return true; - } - } - return false; -} - -bool SublistWidget::sameTypeAs(not_null memento) { - return dynamic_cast(memento.get()) != nullptr; -} - -void SublistWidget::setInternalState( - const QRect &geometry, - not_null memento) { - setGeometry(geometry); - Ui::SendPendingMoveResizeEvents(this); - restoreState(memento); -} - -bool SublistWidget::searchInChatEmbedded( - QString query, - Dialogs::Key chat, - PeerData *searchFrom) { - const auto sublist = chat.sublist(); - if (!sublist || sublist != _sublist) { - return false; - } else if (_composeSearch) { - _composeSearch->setQuery(query); - _composeSearch->setInnerFocus(); - return true; - } - _composeSearch = std::make_unique( - this, - controller(), - _history, - sublist->peer(), - query); - - updateControlsGeometry(); - setInnerFocus(); - - _composeSearch->activations( - ) | rpl::start_with_next([=](ComposeSearch::Activation activation) { - const auto item = activation.item; - auto params = ::Window::SectionShow( - ::Window::SectionShow::Way::ClearStack); - params.highlightPart = { activation.query }; - params.highlightPartOffsetHint = kSearchQueryOffsetHint; - controller()->showPeerHistory( - item->history()->peer->id, - params, - item->fullId().msg); - }, _composeSearch->lifetime()); - - _composeSearch->destroyRequests( - ) | rpl::take( - 1 - ) | rpl::start_with_next([=] { - _composeSearch = nullptr; - - updateControlsGeometry(); - setInnerFocus(); - }, _composeSearch->lifetime()); - - return true; -} - -std::shared_ptr SublistWidget::createMemento() { - auto result = std::make_shared(sublist()); - saveState(result.get()); - return result; -} - -bool SublistWidget::showMessage( - PeerId peerId, - const Window::SectionShow ¶ms, - MsgId messageId) { - const auto id = FullMsgId(_history->peer->id, messageId); - const auto message = _history->owner().message(id); - if (!message || message->savedSublist() != _sublist) { - return false; - } - const auto originMessage = [&]() -> HistoryItem* { - using OriginMessage = Window::SectionShow::OriginMessage; - if (const auto origin = std::get_if(¶ms.origin)) { - if (const auto returnTo = session().data().message(origin->id)) { - if (returnTo->savedSublist() == _sublist) { - return returnTo; - } - } - } - return nullptr; - }(); - const auto currentReplyReturn = _cornerButtons.replyReturn(); - const auto originItemId = !originMessage - ? FullMsgId() - : (currentReplyReturn != originMessage) - ? originMessage->fullId() - : FullMsgId(); - showAtPosition(message->position(), originItemId, params); - return true; -} - -void SublistWidget::saveState(not_null memento) { - _inner->saveState(memento->list()); -} - -void SublistWidget::restoreState(not_null memento) { - _inner->restoreState(memento->list()); -} - -void SublistWidget::resizeEvent(QResizeEvent *e) { - if (!width() || !height()) { - return; - } - recountChatWidth(); - updateControlsGeometry(); -} - -void SublistWidget::recountChatWidth() { - auto layout = (width() < st::adaptiveChatWideWidth) - ? Window::Adaptive::ChatLayout::Normal - : Window::Adaptive::ChatLayout::Wide; - controller()->adaptive().setChatLayout(layout); -} - -void SublistWidget::updateControlsGeometry() { - const auto contentWidth = width(); - - const auto newScrollTop = _scroll->isHidden() - ? std::nullopt - : base::make_optional(_scroll->scrollTop() + topDelta()); - _topBar->resizeToWidth(contentWidth); - _topBarShadow->resize(contentWidth, st::lineWidth); - - auto bottom = height(); - if (_openChatButton) { - _openChatButton->resizeToWidth(width()); - bottom -= _openChatButton->height(); - _openChatButton->move(0, bottom); - } - if (_aboutHiddenAuthor) { - _aboutHiddenAuthor->resize(width(), st::historyUnblock.height); - bottom -= _aboutHiddenAuthor->height(); - _aboutHiddenAuthor->move(0, bottom); - } - const auto controlsHeight = 0; - auto top = _topBar->height(); - _translateBar->move(0, top); - _translateBar->resizeToWidth(contentWidth); - top += _translateBarHeight; - const auto scrollHeight = bottom - top - controlsHeight; - const auto scrollSize = QSize(contentWidth, scrollHeight); - if (_scroll->size() != scrollSize) { - _skipScrollEvent = true; - _scroll->resize(scrollSize); - _inner->resizeToWidth(scrollSize.width(), _scroll->height()); - _skipScrollEvent = false; - } - _scroll->move(0, top); - if (!_scroll->isHidden()) { - if (newScrollTop) { - _scroll->scrollToY(*newScrollTop); - } - updateInnerVisibleArea(); - } - - _cornerButtons.updatePositions(); -} - -void SublistWidget::paintEvent(QPaintEvent *e) { - if (animatingShow()) { - SectionWidget::paintEvent(e); - return; - } else if (controller()->contentOverlapped(this, e)) { - return; - } - - const auto aboveHeight = _topBar->height(); - const auto bg = e->rect().intersected( - QRect(0, aboveHeight, width(), height() - aboveHeight)); - SectionWidget::PaintBackground(controller(), _theme.get(), this, bg); -} - -void SublistWidget::onScroll() { - if (_skipScrollEvent) { - return; - } - updateInnerVisibleArea(); -} - -void SublistWidget::updateInnerVisibleArea() { - const auto scrollTop = _scroll->scrollTop(); - _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); - _cornerButtons.updateJumpDownVisibility(); - _cornerButtons.updateUnreadThingsVisibility(); -} - -void SublistWidget::showAnimatedHook( - const Window::SectionSlideParams ¶ms) { - _topBar->setAnimatingMode(true); - if (params.withTopBarShadow) { - _topBarShadow->show(); - } -} - -void SublistWidget::showFinishedHook() { - _topBar->setAnimatingMode(false); - _inner->showFinished(); - _translateBar->show(); -} - -bool SublistWidget::floatPlayerHandleWheelEvent(QEvent *e) { - return _scroll->viewportEvent(e); -} - -QRect SublistWidget::floatPlayerAvailableRect() { - return mapToGlobal(_scroll->geometry()); -} - -Context SublistWidget::listContext() { - return Context::SavedSublist; -} - -bool SublistWidget::listScrollTo(int top, bool syntetic) { - top = std::clamp(top, 0, _scroll->scrollTopMax()); - if (_scroll->scrollTop() == top) { - updateInnerVisibleArea(); - return false; - } - _scroll->scrollToY(top); - return true; -} - -void SublistWidget::listCancelRequest() { - if (_inner && !_inner->getSelectedIds().empty()) { - clearSelected(); - return; - } - controller()->showBackFromStack(); -} - -void SublistWidget::listDeleteRequest() { - confirmDeleteSelected(); -} - -void SublistWidget::listTryProcessKeyInput(not_null e) { -} - -rpl::producer SublistWidget::listSource( - Data::MessagePosition aroundId, - int limitBefore, - int limitAfter) { - const auto messageId = aroundId.fullId.msg - ? aroundId.fullId.msg - : (ServerMaxMsgId - 1); - return [=](auto consumer) { - const auto pushSlice = [=] { - auto result = Data::MessagesSlice(); - result.fullCount = _sublist->fullCount(); - _topBar->setCustomTitle(result.fullCount - ? tr::lng_forum_messages( - tr::now, - lt_count_decimal, - *result.fullCount) - : tr::lng_contacts_loading(tr::now)); - const auto &messages = _sublist->messages(); - const auto i = ranges::lower_bound( - messages, - messageId, - ranges::greater(), - [](not_null item) { return item->id; }); - const auto before = int(end(messages) - i); - const auto useBefore = std::min(before, limitBefore); - const auto after = int(i - begin(messages)); - const auto useAfter = std::min(after, limitAfter); - const auto from = i - useAfter; - const auto till = i + useBefore; - auto nearestDistance = std::numeric_limits::max(); - result.ids.reserve(useAfter + useBefore); - for (auto j = till; j != from;) { - const auto item = *--j; - result.ids.push_back(item->fullId()); - const auto distance = std::abs((messageId - item->id).bare); - if (nearestDistance > distance) { - nearestDistance = distance; - result.nearestToAround = result.ids.back(); - } - } - result.skippedAfter = after - useAfter; - result.skippedBefore = result.fullCount - ? (*result.fullCount - after - useBefore) - : std::optional(); - if (!result.fullCount || useBefore < limitBefore) { - _sublist->parent()->loadMore(_sublist); - } - consumer.put_next(std::move(result)); - }; - auto lifetime = rpl::lifetime(); - _sublist->changes() | rpl::start_with_next(pushSlice, lifetime); - pushSlice(); - return lifetime; - }; -} - -bool SublistWidget::listAllowsMultiSelect() { - return true; -} - -bool SublistWidget::listIsItemGoodForSelection( - not_null item) { - return item->isRegular() && !item->isService(); -} - -bool SublistWidget::listIsLessInOrder( - not_null first, - not_null second) { - return first->id < second->id; -} - -void SublistWidget::listSelectionChanged(SelectedItems &&items) { - HistoryView::TopBarWidget::SelectedState state; - state.count = items.size(); - for (const auto &item : items) { - if (item.canDelete) { - ++state.canDeleteCount; - } - if (item.canForward) { - ++state.canForwardCount; - } - } - _topBar->showSelected(state); - if ((state.count > 0) && _composeSearch) { - _composeSearch->hideAnimated(); - } -} - -void SublistWidget::listMarkReadTill(not_null item) { -} - -void SublistWidget::listMarkContentsRead( - const base::flat_set> &items) { -} - -MessagesBarData SublistWidget::listMessagesBar( - const std::vector> &elements) { - return {}; -} - -void SublistWidget::listContentRefreshed() { -} - -void SublistWidget::listUpdateDateLink( - ClickHandlerPtr &link, - not_null view) { -} - -bool SublistWidget::listElementHideReply(not_null view) { - return false; -} - -bool SublistWidget::listElementShownUnread(not_null view) { - return view->data()->unread(view->data()->history()); -} - -bool SublistWidget::listIsGoodForAroundPosition( - not_null view) { - return view->data()->isRegular(); -} - -void SublistWidget::listSendBotCommand( - const QString &command, - const FullMsgId &context) { -} - -void SublistWidget::listSearch( - const QString &query, - const FullMsgId &context) { - const auto inChat = Data::SearchTagFromQuery(query) - ? Dialogs::Key(_sublist) - : Dialogs::Key(); - controller()->searchMessages(query, inChat); -} - -void SublistWidget::listHandleViaClick(not_null bot) { -} - -not_null SublistWidget::listChatTheme() { - return _theme.get(); -} - -CopyRestrictionType SublistWidget::listCopyRestrictionType( - HistoryItem *item) { - return CopyRestrictionTypeFor(_history->peer, item); -} - -CopyRestrictionType SublistWidget::listCopyMediaRestrictionType( - not_null item) { - return CopyMediaRestrictionTypeFor(_history->peer, item); -} - -CopyRestrictionType SublistWidget::listSelectRestrictionType() { - return SelectRestrictionTypeFor(_history->peer); -} - -auto SublistWidget::listAllowedReactionsValue() --> rpl::producer { - return Data::PeerAllowedReactionsValue(_history->peer); -} - -void SublistWidget::listShowPremiumToast(not_null document) { -} - -void SublistWidget::listOpenPhoto( - not_null photo, - FullMsgId context) { - controller()->openPhoto(photo, { context }); -} - -void SublistWidget::listOpenDocument( - not_null document, - FullMsgId context, - bool showInMediaView) { - controller()->openDocument(document, showInMediaView, { context }); -} - -void SublistWidget::listPaintEmpty( - Painter &p, - const Ui::ChatPaintContext &context) { -} - -QString SublistWidget::listElementAuthorRank(not_null view) { - return {}; -} - -bool SublistWidget::listElementHideTopicButton( - not_null view) { - return true; -} - -History *SublistWidget::listTranslateHistory() { - return _history; -} - -void SublistWidget::listAddTranslatedItems( - not_null tracker) { -} - -void SublistWidget::confirmDeleteSelected() { - ConfirmDeleteSelectedItems(_inner); -} - -void SublistWidget::confirmForwardSelected() { - ConfirmForwardSelectedItems(_inner); -} - -void SublistWidget::clearSelected() { - _inner->cancelSelection(); -} - -void SublistWidget::setupShortcuts() { - Shortcuts::Requests( - ) | rpl::filter([=] { - return Ui::AppInFocus() - && Ui::InFocusChain(this) - && !controller()->isLayerShown() - && (Core::App().activeWindow() == &controller()->window()); - }) | rpl::start_with_next([=](not_null request) { - using Command = Shortcuts::Command; - request->check(Command::Search, 1) && request->handle([=] { - searchInSublist(); - return true; - }); - }, lifetime()); -} - -void SublistWidget::searchInSublist() { - controller()->searchInChat(_sublist); -} - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_sublist_section.h b/Telegram/SourceFiles/history/view/history_view_sublist_section.h deleted file mode 100644 index 33b655720e..0000000000 --- a/Telegram/SourceFiles/history/view/history_view_sublist_section.h +++ /dev/null @@ -1,237 +0,0 @@ -/* -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 "window/section_widget.h" -#include "window/section_memento.h" -#include "history/view/history_view_list_widget.h" -#include "history/view/history_view_corner_buttons.h" -#include "data/data_messages.h" -#include "base/weak_ptr.h" -#include "base/timer.h" - -class History; - -namespace Ui { -class ScrollArea; -class PlainShadow; -class FlatButton; -} // namespace Ui - -namespace Profile { -class BackButton; -} // namespace Profile - -namespace HistoryView { - -class Element; -class TopBarWidget; -class SublistMemento; -class TranslateBar; -class ComposeSearch; - -class SublistWidget final - : public Window::SectionWidget - , private WindowListDelegate - , private CornerButtonsDelegate { -public: - SublistWidget( - QWidget *parent, - not_null controller, - not_null sublist); - ~SublistWidget(); - - [[nodiscard]] not_null sublist() const; - Dialogs::RowDescriptor activeChat() const override; - - bool hasTopBarShadow() const override { - return true; - } - - QPixmap grabForShowAnimation( - const Window::SectionSlideParams ¶ms) override; - - bool showInternal( - not_null memento, - const Window::SectionShow ¶ms) override; - bool sameTypeAs(not_null memento) override; - - std::shared_ptr createMemento() override; - bool showMessage( - PeerId peerId, - const Window::SectionShow ¶ms, - MsgId messageId) override; - - void setInternalState( - const QRect &geometry, - not_null memento); - - Window::SectionActionResult sendBotCommand( - Bot::SendCommandRequest request) override { - return Window::SectionActionResult::Fallback; - } - - bool searchInChatEmbedded( - QString query, - Dialogs::Key chat, - PeerData *searchFrom = nullptr) override; - - // Float player interface. - bool floatPlayerHandleWheelEvent(QEvent *e) override; - QRect floatPlayerAvailableRect() override; - - // ListDelegate interface. - Context listContext() override; - bool listScrollTo(int top, bool syntetic = true) override; - void listCancelRequest() override; - void listDeleteRequest() override; - void listTryProcessKeyInput(not_null e) override; - rpl::producer listSource( - Data::MessagePosition aroundId, - int limitBefore, - int limitAfter) override; - bool listAllowsMultiSelect() override; - bool listIsItemGoodForSelection(not_null item) override; - bool listIsLessInOrder( - not_null first, - not_null second) override; - void listSelectionChanged(SelectedItems &&items) override; - void listMarkReadTill(not_null item) override; - void listMarkContentsRead( - const base::flat_set> &items) override; - MessagesBarData listMessagesBar( - const std::vector> &elements) override; - void listContentRefreshed() override; - void listUpdateDateLink( - ClickHandlerPtr &link, - not_null view) override; - bool listElementHideReply(not_null view) override; - bool listElementShownUnread(not_null view) override; - bool listIsGoodForAroundPosition(not_null view) override; - void listSendBotCommand( - const QString &command, - const FullMsgId &context) override; - void listSearch( - const QString &query, - const FullMsgId &context) override; - void listHandleViaClick(not_null bot) override; - not_null listChatTheme() override; - CopyRestrictionType listCopyRestrictionType(HistoryItem *item) override; - CopyRestrictionType listCopyMediaRestrictionType( - not_null item) override; - CopyRestrictionType listSelectRestrictionType() override; - auto listAllowedReactionsValue() - -> rpl::producer override; - void listShowPremiumToast(not_null document) override; - void listOpenPhoto( - not_null photo, - FullMsgId context) override; - void listOpenDocument( - not_null document, - FullMsgId context, - bool showInMediaView) override; - void listPaintEmpty( - Painter &p, - const Ui::ChatPaintContext &context) override; - QString listElementAuthorRank(not_null view) override; - bool listElementHideTopicButton(not_null view) override; - History *listTranslateHistory() override; - void listAddTranslatedItems( - not_null tracker) override; - - // CornerButtonsDelegate delegate. - void cornerButtonsShowAtPosition( - Data::MessagePosition position) override; - Data::Thread *cornerButtonsThread() override; - FullMsgId cornerButtonsCurrentId() override; - bool cornerButtonsIgnoreVisibility() override; - std::optional cornerButtonsDownShown() override; - bool cornerButtonsUnreadMayBeShown() override; - bool cornerButtonsHas(CornerButtonType type) override; - -private: - void resizeEvent(QResizeEvent *e) override; - void paintEvent(QPaintEvent *e) override; - - void showAnimatedHook( - const Window::SectionSlideParams ¶ms) override; - void showFinishedHook() override; - void doSetInnerFocus() override; - void checkActivation() override; - - void onScroll(); - void updateInnerVisibleArea(); - void updateControlsGeometry(); - void updateAdaptiveLayout(); - void saveState(not_null memento); - void restoreState(not_null memento); - void showAtPosition( - Data::MessagePosition position, - FullMsgId originId = {}); - void showAtPosition( - Data::MessagePosition position, - FullMsgId originItemId, - const Window::SectionShow ¶ms); - - void setupOpenChatButton(); - void setupAboutHiddenAuthor(); - void setupTranslateBar(); - void setupShortcuts(); - - void confirmDeleteSelected(); - void confirmForwardSelected(); - void clearSelected(); - void recountChatWidth(); - void searchInSublist(); - - const not_null _sublist; - const not_null _history; - std::shared_ptr _theme; - QPointer _inner; - object_ptr _topBar; - object_ptr _topBarShadow; - - std::unique_ptr _translateBar; - int _translateBarHeight = 0; - - bool _skipScrollEvent = false; - std::unique_ptr _scroll; - std::unique_ptr _openChatButton; - std::unique_ptr _aboutHiddenAuthor; - std::unique_ptr _composeSearch; - - FullMsgId _lastShownAt; - CornerButtons _cornerButtons; - -}; - -class SublistMemento : public Window::SectionMemento { -public: - explicit SublistMemento(not_null sublist); - - object_ptr createWidget( - QWidget *parent, - not_null controller, - Window::Column column, - const QRect &geometry) override; - - [[nodiscard]] not_null getSublist() const { - return _sublist; - } - - [[nodiscard]] not_null list() { - return &_list; - } - -private: - const not_null _sublist; - ListMemento _list; - -}; - -} // namespace HistoryView diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index c15b1c8f1a..9f53a17e2b 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -12,10 +12,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "data/data_channel.h" #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories_ids.h" #include "data/data_user.h" -#include "history/view/history_view_sublist_section.h" +#include "history/view/history_view_chat_section.h" #include "history/history.h" #include "info/info_controller.h" #include "info/info_memento.h" @@ -270,9 +271,13 @@ not_null AddSavedSublistButton( }, tracker)->entity(); result->addClickHandler([=] { + using namespace HistoryView; + const auto sublist = peer->owner().savedMessages().sublist(peer); navigation->showSection( - std::make_shared( - peer->owner().savedMessages().sublist(peer))); + std::make_shared(ChatViewId{ + .history = sublist->parentHistory(), + .sublist = sublist, + })); }); return result; } diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index dbd6557888..3afead749b 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -8,10 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/saved/info_saved_sublists_widget.h" // #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "dialogs/dialogs_inner_widget.h" -#include "history/view/history_view_sublist_section.h" +#include "history/view/history_view_chat_section.h" #include "info/media/info_media_buttons.h" #include "info/profile/info_profile_icon.h" #include "info/info_controller.h" @@ -63,10 +64,14 @@ SublistsWidget::SublistsWidget( _list->chosenRow() | rpl::start_with_next([=](Dialogs::ChosenRow row) { if (const auto sublist = row.key.sublist()) { using namespace Window; + using namespace HistoryView; auto params = SectionShow(SectionShow::Way::Forward); params.dropSameFromStack = true; controller->showSection( - std::make_shared(sublist), + std::make_shared(ChatViewId{ + .history = sublist->parentHistory(), + .sublist = sublist, + }), params); } }, _list->lifetime()); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index d3cbed09a0..3c33394661 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_game.h" #include "data/data_peer_values.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_folder.h" @@ -54,8 +55,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_widget.h" #include "history/history_item_helpers.h" // GetErrorForSending. #include "history/view/media/history_view_media.h" +#include "history/view/history_view_chat_section.h" #include "history/view/history_view_service_message.h" -#include "history/view/history_view_sublist_section.h" #include "lang/lang_keys.h" #include "lang/lang_cloud_manager.h" #include "inline_bots/inline_bot_layout_item.h" @@ -776,8 +777,12 @@ void MainWidget::searchMessages( } } else { if (const auto sublist = inChat.sublist()) { + using namespace HistoryView; controller()->showSection( - std::make_shared(sublist)); + std::make_shared(ChatViewId{ + .history = sublist->parentHistory(), + .sublist = sublist, + })); } else if (!tags.empty()) { inChat = controller()->session().data().history( controller()->session().user()); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index d8950d56c5..2cb230d6c8 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -27,13 +27,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL //#include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_chat_section.h" #include "history/view/history_view_scheduled_section.h" -#include "history/view/history_view_sublist_section.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document_resolver.h" #include "data/data_download_manager.h" #include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_file_origin.h" #include "data/data_folder.h" @@ -1343,8 +1343,12 @@ void SessionNavigation::showByInitialId( break; } case SeparateType::SavedSublist: + using namespace HistoryView; showSection( - std::make_shared(id.sublist()), + std::make_shared(ChatViewId{ + .history = id.sublist()->parentHistory(), + .sublist = id.sublist(), + }), instant); break; } From c6d43a802ca1552e92195aa6aecb7a49589839b6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 9 May 2025 17:41:19 +0400 Subject: [PATCH 058/310] Fix sending messages in monoforums. --- .../history_view_compose_controls.cpp | 1 + .../view/history_view_chat_section.cpp | 22 ++++++++++++++----- .../view/history_view_top_bar_widget.cpp | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 50a49a7371..5e4f936f1f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1807,6 +1807,7 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const { switch (_currentDialogsEntryState.section) { case Section::History: case Section::Replies: + case Section::SavedSublist: return (type == DraftType::Edit) ? Key::LocalEdit(_topicRootId) : Key::Local(_topicRootId); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index b70bc08ec7..50d61f25d7 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -658,7 +658,7 @@ HistoryItem *ChatWidget::lookupRepliesRoot() const { } Data::ForumTopic *ChatWidget::lookupTopic() { - if (!_repliesRoot) { + if (!_repliesRootId) { return nullptr; } else if (const auto forum = _history->asForum()) { if (const auto result = forum->topicFor(_repliesRootId)) { @@ -1692,15 +1692,29 @@ SendMenu::Details ChatWidget::sendMenuDetails() const { } FullReplyTo ChatWidget::replyTo() const { + const auto monoforumPeerId = (_sublist && _sublist->parentChat()) + ? _sublist->peer()->id + : PeerId(); if (auto custom = _composeControls->replyingToMessage()) { - custom.topicRootId = _repliesRootId; - return custom; + const auto item = custom.messageId + ? session().data().message(custom.messageId) + : nullptr; + const auto sublistPeer = item ? item->savedSublistPeer() : nullptr; + if (!item + || !monoforumPeerId + || (sublistPeer && sublistPeer->id == monoforumPeerId)) { + // Never answer to a message in a wrong monoforum peer id. + custom.topicRootId = _repliesRootId; + custom.monoforumPeerId = monoforumPeerId; + return custom; + } } return FullReplyTo{ .messageId = (_repliesRootId ? FullMsgId(_peer->id, _repliesRootId) : FullMsgId()), .topicRootId = _repliesRootId, + .monoforumPeerId = monoforumPeerId, }; } @@ -2318,8 +2332,6 @@ bool ChatWidget::showMessage( return false; } else if (_sublist && message->savedSublist() != _sublist) { return false; - } else { - Unexpected("ChatWidget::showMessage context."); } const auto originMessage = [&]() -> HistoryItem* { using OriginMessage = Window::SectionShow::OriginMessage; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 568d747b95..af060ea552 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -81,8 +81,8 @@ QString TopBarNameText( not_null peer, const Dialogs::EntryState &state) { if (state.section == Dialogs::EntryState::Section::SavedSublist - && state.key.history() - && state.key.history()->peer->isSelf()) { + && state.key.sublist() + && state.key.sublist()->parentHistory()->peer->isSelf()) { if (peer->isSelf()) { return tr::lng_my_notes(tr::now); } else if (peer->isSavedHiddenAuthor()) { From 43b44991251183d807c08b6f108ffcd06b1a5a4a Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 12 May 2025 12:41:00 +0400 Subject: [PATCH 059/310] Add monoforum sender bar divider. --- Telegram/Resources/langs/lang.strings | 2 + .../boxes/peers/edit_peer_info_box.cpp | 4 +- Telegram/SourceFiles/data/data_channel.cpp | 6 - Telegram/SourceFiles/data/data_channel.h | 1 - .../data/data_chat_participant_status.h | 5 +- Telegram/SourceFiles/data/data_histories.cpp | 6 +- Telegram/SourceFiles/data/data_peer.cpp | 43 ++++++- Telegram/SourceFiles/data/data_peer.h | 33 ++++- Telegram/SourceFiles/data/data_session.cpp | 4 + .../SourceFiles/dialogs/dialogs_widget.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 5 +- Telegram/SourceFiles/history/history_item.cpp | 3 +- .../history/history_item_components.h | 82 +++++++------ .../SourceFiles/history/history_widget.cpp | 17 ++- .../view/history_view_chat_section.cpp | 10 +- .../history/view/history_view_element.cpp | 114 +++++++++++++++++- .../history/view/history_view_element.h | 38 ++++-- .../history/view/history_view_message.cpp | 30 +++++ .../history/view/history_view_message.h | 8 +- .../view/history_view_service_message.cpp | 27 +++++ .../view/history_view_top_bar_widget.cpp | 8 +- .../info/profile/info_profile_cover.cpp | 16 ++- .../profile/info_profile_inner_widget.cpp | 4 +- .../SourceFiles/overview/overview_layout.h | 2 +- Telegram/SourceFiles/ui/chat/chat.style | 1 + .../ui/controls/userpic_button.cpp | 4 +- .../SourceFiles/ui/controls/userpic_button.h | 3 +- 27 files changed, 388 insertions(+), 90 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2d1ccfd8cc..0ad0b60ee9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6091,6 +6091,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_forum_messages#other" = "{count} messages"; "lng_forum_show_topics_list" = "Show Topics List"; +"lng_monoforum_choose_to_reply" = "Choose a message to reply."; + "lng_request_peer_requirements" = "Requirements"; "lng_request_peer_rights" = "You must have these admin rights: {rights}."; "lng_request_peer_rights_and" = "{rights} and {last}"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 4bc44e3bd6..4028efa07a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -2936,7 +2936,9 @@ bool EditPeerInfoBox::Available(not_null peer) { // canViewAdmins() is removed, because in supergroups it is // always true and in channels it is equal to canViewBanned(). - + if (channel->isMonoforum()) { + return false; + } return false //|| channel->canViewMembers() //|| channel->canViewAdmins() diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index fc0842c896..0159fcca80 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -341,12 +341,6 @@ ChannelData *ChannelData::monoforumLink() const { return _monoforumLink; } -bool ChannelData::requiresMonoforumPeer() const { - return isMonoforum() - && _monoforumLink - && (_monoforumLink->amCreator() || _monoforumLink->hasAdminRights()); -} - void ChannelData::setMembersCount(int newMembersCount) { if (_membersCount != newMembersCount) { if (isMegagroup() diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 9b3150de8f..6dc802dcfc 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -429,7 +429,6 @@ public: void setMonoforumLink(ChannelData *link); [[nodiscard]] ChannelData *monoforumLink() const; - [[nodiscard]] bool requiresMonoforumPeer() const; void ptsInit(int32 pts) { _ptsWaiter.init(pts); diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index b3db584a4e..17a6ddfe7d 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -190,18 +190,21 @@ struct SendError { struct Args { QString text; int boostsToLift = 0; + bool monoforumAdmin = false; bool premiumToLift = false; bool frozen = false; }; SendError(Args &&args) : text(std::move(args.text)) , boostsToLift(args.boostsToLift) + , monoforumAdmin(args.monoforumAdmin) , premiumToLift(args.premiumToLift) , frozen(args.frozen) { } QString text; int boostsToLift = 0; + bool monoforumAdmin = false; bool premiumToLift = false; bool frozen = false; @@ -210,7 +213,7 @@ struct SendError { } explicit operator bool() const { - return !text.isEmpty(); + return monoforumAdmin || !text.isEmpty(); } [[nodiscard]] bool has_value() const { return !text.isEmpty(); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index e7c5a14da4..bd7093579c 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -65,8 +65,7 @@ MTPInputReplyTo ReplyToForMTP( : replyTo.monoforumPeerId ? history->owner().peer(replyTo.monoforumPeerId).get() : history->session().user().get(); - const auto replyToMonoforumPeer = (history->peer->isChannel() - && history->peer->asChannel()->requiresMonoforumPeer()) + const auto replyToMonoforumPeer = history->peer->amMonoforumAdmin() ? possibleMonoforumPeer : nullptr; const auto external = replyTo.messageId @@ -98,8 +97,7 @@ MTPInputReplyTo ReplyToForMTP( (replyToMonoforumPeer ? replyToMonoforumPeer->input : MTPInputPeer())); - } else if (history->peer->isChannel() - && history->peer->asChannel()->requiresMonoforumPeer() + } else if (history->peer->amMonoforumAdmin() && replyTo.monoforumPeerId) { const auto replyToMonoforumPeer = replyTo.monoforumPeerId ? history->owner().peer(replyTo.monoforumPeerId) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 851fd9a0c4..b6191a4dfe 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -427,10 +427,12 @@ QImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const { void PeerData::paintUserpic( Painter &p, Ui::PeerUserpicView &view, - int x, - int y, - int size, - bool forceCircle) const { + const PaintUserpicContext &context) const { + if (const auto broadcast = monoforumBroadcast()) { + broadcast->paintUserpic(p, view, context); + return; + } + const auto size = context.size; const auto cloud = userpicCloudImage(view); const auto ratio = style::DevicePixelRatio(); Ui::ValidateUserpicCache( @@ -438,8 +440,8 @@ void PeerData::paintUserpic( cloud, cloud ? nullptr : ensureEmptyUserpic().get(), size * ratio, - !forceCircle && (isForum() || isMonoforum())); - p.drawImage(QRect(x, y, size, size), view.cached); + context.forumLayout); + p.drawImage(QRect(context.position, QSize(size, size)), view.cached); } void PeerData::loadUserpic() { @@ -1118,6 +1120,16 @@ const ChannelData *PeerData::asChannelOrMigrated() const { return migrateTo(); } +ChannelData *PeerData::asMonoforum() { + const auto channel = asMegagroup(); + return (channel && channel->isMonoforum()) ? channel : nullptr; +} + +const ChannelData *PeerData::asMonoforum() const { + const auto channel = asMegagroup(); + return (channel && channel->isMonoforum()) ? channel : nullptr; +} + ChatData *PeerData::migrateFrom() const { if (const auto megagroup = asMegagroup()) { return megagroup->amIn() @@ -1150,6 +1162,16 @@ not_null PeerData::migrateToOrMe() const { return this; } +ChannelData *PeerData::monoforumBroadcast() const { + const auto monoforum = asMonoforum(); + return monoforum ? monoforum->monoforumLink() : nullptr; +} + +ChannelData *PeerData::broadcastMonoforum() const { + const auto broadcast = asBroadcast(); + return broadcast ? broadcast->monoforumLink() : nullptr; +} + const QString &PeerData::topBarNameText() const { if (const auto to = migrateTo()) { return to->topBarNameText(); @@ -1572,12 +1594,21 @@ bool PeerData::canManageGroupCall() const { return chat->amCreator() || (chat->adminRights() & ChatAdminRight::ManageCall); } else if (const auto group = asChannel()) { + if (group->isMonoforum()) { + return false; + } return group->amCreator() || (group->adminRights() & ChatAdminRight::ManageCall); } return false; } +bool PeerData::amMonoforumAdmin() const { + const auto broadcast = monoforumBroadcast(); + return broadcast + && (broadcast->amCreator() || broadcast->hasAdminRights()); +} + int PeerData::starsPerMessage() const { if (const auto user = asUser()) { return user->starsPerMessage(); diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index a4a1ebe531..e185fc58ea 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -277,6 +277,7 @@ public: [[nodiscard]] rpl::producer slowmodeAppliedValue() const; [[nodiscard]] int slowmodeSecondsLeft() const; [[nodiscard]] bool canManageGroupCall() const; + [[nodiscard]] bool amMonoforumAdmin() const; [[nodiscard]] int starsPerMessage() const; [[nodiscard]] int starsPerMessageChecked() const; @@ -297,12 +298,20 @@ public: [[nodiscard]] const ChatData *asChatNotMigrated() const; [[nodiscard]] ChannelData *asChannelOrMigrated(); [[nodiscard]] const ChannelData *asChannelOrMigrated() const; + [[nodiscard]] ChannelData *asMonoforum(); + [[nodiscard]] const ChannelData *asMonoforum() const; [[nodiscard]] ChatData *migrateFrom() const; [[nodiscard]] ChannelData *migrateTo() const; [[nodiscard]] not_null migrateToOrMe(); [[nodiscard]] not_null migrateToOrMe() const; + // isMonoforum() ? monoforumLink() : nullptr + [[nodiscard]] ChannelData *monoforumBroadcast() const; + + // isMonoforum() ? nullptr : monoforumLink() + [[nodiscard]] ChannelData *broadcastMonoforum() const; + void updateFull(); void updateFullForced(); void fullUpdated(); @@ -332,13 +341,29 @@ public: const ImageLocation &location, bool hasVideo); void setUserpicPhoto(const MTPPhoto &data); + + struct PaintUserpicContext { + QPoint position; + int size = 0; + bool forumLayout = false; + }; void paintUserpic( Painter &p, Ui::PeerUserpicView &view, - int x, - int y, - int size, - bool forceCircle = false) const; + const PaintUserpicContext &context) const; + void paintUserpic( + Painter &p, + Ui::PeerUserpicView &view, + int x, + int y, + int size, + bool forceCircle = false) const { + paintUserpic(p, view, { + .position = { x, y }, + .size = size, + .forumLayout = !forceCircle && (isForum() || isMonoforum()), + }); + } void paintUserpicLeft( Painter &p, Ui::PeerUserpicView &view, diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index de96c6c58a..d995d42083 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4657,6 +4657,10 @@ void Session::refreshChatListEntry(Dialogs::Key key) { if (const auto forum = history->peer->forum()) { forum->preloadTopics(); } + if (history->peer->isMonoforum() + && !history->peer->monoforumBroadcast()) { + history->peer->updateFull(); + } } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index b593d960b5..6dea61f38a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -941,7 +941,7 @@ void Widget::chosenRow(const ChosenRow &row) { } return; } else if (history - && history->isMonoforum() + && history->peer->amMonoforumAdmin() && !row.message.fullId && !controller()->adaptive().isOneColumn()) { const auto monoforum = history->peer->monoforum(); diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 641adcae73..9e433de7c2 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2789,7 +2789,10 @@ bool History::shouldBeInChatList() const { } else if (isPinnedDialog(FilterId())) { return true; } else if (const auto channel = peer->asChannel()) { - if (!channel->amIn()) { + if (channel->isMonoforum()) { + return !lastMessageKnown() + || (lastMessage() != nullptr); + } else if (!channel->amIn()) { return isTopPromoted(); } } else if (const auto chat = peer->asChat()) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 2f9bf79236..e084f47e6e 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3770,8 +3770,7 @@ void HistoryItem::createComponents(CreateConfig &&config) { } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } - const auto requiresMonoforumPeer = _history->peer->isChannel() - && _history->peer->asChannel()->requiresMonoforumPeer(); + const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin(); if (_history->peer->isSelf() || config.savedSublistPeer || requiresMonoforumPeer) { diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 57dbeba73f..33fa873334 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -57,7 +57,7 @@ struct BotKeyboardButton; extern const char kOptionFastButtonsMode[]; [[nodiscard]] bool FastButtonsMode(); -struct HistoryMessageVia : public RuntimeComponent { +struct HistoryMessageVia : RuntimeComponent { void create(not_null owner, UserId userId); void resize(int32 availw) const; @@ -68,7 +68,8 @@ struct HistoryMessageVia : public RuntimeComponent { +struct HistoryMessageViews +: RuntimeComponent { static constexpr auto kMaxRecentRepliers = 3; struct Part { @@ -87,13 +88,15 @@ struct HistoryMessageViews : public RuntimeComponent { +struct HistoryMessageSigned +: RuntimeComponent { QString author; UserData *viaBusinessBot = nullptr; bool isAnonymousRank = false; }; -struct HistoryMessageEdited : public RuntimeComponent { +struct HistoryMessageEdited +: RuntimeComponent { TimeId date = 0; }; @@ -134,7 +137,8 @@ private: }; -struct HistoryMessageForwarded : public RuntimeComponent { +struct HistoryMessageForwarded +: RuntimeComponent { void create( const HistoryMessageVia *via, not_null item) const; @@ -162,12 +166,14 @@ struct HistoryMessageForwarded : public RuntimeComponent { +struct HistoryMessageSavedMediaData +: RuntimeComponent { TextWithEntities text; std::unique_ptr media; }; -struct HistoryMessageSaved : public RuntimeComponent { +struct HistoryMessageSaved +: RuntimeComponent { Data::SavedSublist *sublist = nullptr; }; @@ -274,7 +280,7 @@ struct ReplyFields { const MTPInputReplyTo &reply); struct HistoryMessageReply - : public RuntimeComponent { +: RuntimeComponent { HistoryMessageReply(); HistoryMessageReply(const HistoryMessageReply &other) = delete; HistoryMessageReply(HistoryMessageReply &&other) = delete; @@ -358,7 +364,7 @@ private: }; struct HistoryMessageTranslation - : public RuntimeComponent { +: RuntimeComponent { TextWithEntities text; LanguageId to; bool requested = false; @@ -367,7 +373,7 @@ struct HistoryMessageTranslation }; struct HistoryMessageReplyMarkup - : public RuntimeComponent { +: RuntimeComponent { using Button = HistoryMessageMarkupButton; void createForwarded(const HistoryMessageReplyMarkup &original); @@ -565,7 +571,7 @@ private: // Special type of Component for the channel actions log. struct HistoryMessageLogEntryOriginal -: public RuntimeComponent { +: RuntimeComponent { HistoryMessageLogEntryOriginal(); HistoryMessageLogEntryOriginal(HistoryMessageLogEntryOriginal &&other); HistoryMessageLogEntryOriginal &operator=(HistoryMessageLogEntryOriginal &&other); @@ -597,19 +603,19 @@ struct MessageFactcheck { const tl::conditional &factcheck); struct HistoryMessageFactcheck -: public RuntimeComponent { +: RuntimeComponent { MessageFactcheck data; WebPageData *page = nullptr; bool requested = false; }; struct HistoryMessageRestrictions -: public RuntimeComponent { +: RuntimeComponent { std::vector reasons; }; struct HistoryServiceData -: public RuntimeComponent { +: RuntimeComponent { std::vector textLinks; }; @@ -625,13 +631,13 @@ struct HistoryServiceDependentData { }; struct HistoryServicePinned -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { }; struct HistoryServiceTopicInfo -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { QString title; DocumentId iconId = 0; bool closed = false; @@ -652,14 +658,14 @@ struct HistoryServiceTopicInfo }; struct HistoryServiceGameScore -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { int score = 0; }; struct HistoryServicePayment -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { QString slug; TextWithEntities amount; ClickHandlerPtr invoiceLink; @@ -669,22 +675,22 @@ struct HistoryServicePayment }; struct HistoryServiceSameBackground -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { }; struct HistoryServiceGiveawayResults -: public RuntimeComponent -, public HistoryServiceDependentData { +: RuntimeComponent +, HistoryServiceDependentData { }; struct HistoryServiceCustomLink -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; }; struct HistoryServicePaymentRefund -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; PeerData *peer = nullptr; QString transactionId; @@ -707,7 +713,7 @@ struct TimeToLiveSingleView { }; struct HistoryServiceSelfDestruct -: public RuntimeComponent { +: RuntimeComponent { using Type = HistorySelfDestructType; Type type = Type::Photo; @@ -716,24 +722,25 @@ struct HistoryServiceSelfDestruct }; struct HistoryServiceOngoingCall -: public RuntimeComponent { +: RuntimeComponent { CallId id = 0; ClickHandlerPtr link; rpl::lifetime lifetime; }; struct HistoryServiceChatThemeChange -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; }; struct HistoryServiceTTLChange -: public RuntimeComponent { +: RuntimeComponent { ClickHandlerPtr link; }; class FileClickHandler; -struct HistoryDocumentThumbed : public RuntimeComponent { +struct HistoryDocumentThumbed +: RuntimeComponent { std::shared_ptr linksavel; std::shared_ptr linkopenwithl; std::shared_ptr linkcancell; @@ -745,13 +752,15 @@ struct HistoryDocumentThumbed : public RuntimeComponent { +struct HistoryDocumentCaptioned +: RuntimeComponent { HistoryDocumentCaptioned(); Ui::Text::String caption; }; -struct HistoryDocumentNamed : public RuntimeComponent { +struct HistoryDocumentNamed +: RuntimeComponent { Ui::Text::String name; }; @@ -763,7 +772,8 @@ struct HistoryDocumentVoicePlayback { Ui::Animations::Basic progressAnimation; }; -class HistoryDocumentVoice : public RuntimeComponent { +class HistoryDocumentVoice +: public RuntimeComponent { // We don't use float64 because components should align to pointer even on 32bit systems. static constexpr float64 kFloatToIntMultiplier = 65536.; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 85682e38eb..0ce8c1becd 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6633,6 +6633,12 @@ int HistoryWidget::countAutomaticScrollTop() { } Data::SendError HistoryWidget::computeSendRestriction() const { + if (!_canSendMessages && _peer->amMonoforumAdmin()) { + return Data::SendError({ + .text = tr::lng_monoforum_choose_to_reply(tr::now), + .monoforumAdmin = true, + }); + } const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; return (_peer && !Data::CanSendAnyOf(_peer, allWithoutPolls)) @@ -8753,10 +8759,17 @@ bool HistoryWidget::updateCanSendMessage() { const auto topic = resolveReplyToTopic(); const auto allWithoutPolls = Data::AllSendRestrictions() & ~ChatRestriction::SendPolls; - const auto newCanSendMessages = topic + const auto onlyReplies = _peer->amMonoforumAdmin(); + const auto restrictedOnlyReplies = onlyReplies + && (!_replyTo.messageId || _replyTo.messageId.peer != _peer->id); + const auto newCanSendMessages = restrictedOnlyReplies + ? false + : topic ? Data::CanSendAnyOf(topic, allWithoutPolls) : Data::CanSendAnyOf(_peer, allWithoutPolls); - const auto newCanSendTexts = topic + const auto newCanSendTexts = restrictedOnlyReplies + ? false + : topic ? Data::CanSend(topic, ChatRestriction::SendOther) : Data::CanSend(_peer, ChatRestriction::SendOther); if (_canSendMessages == newCanSendMessages diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 50d61f25d7..6883918710 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2702,7 +2702,11 @@ QRect ChatWidget::floatPlayerAvailableRect() { } Context ChatWidget::listContext() { - return _sublist ? Context::SavedSublist : Context::Replies; + return !_sublist + ? Context::Replies + : _sublist->parentChat() + ? Context::Monoforum + : Context::SavedSublist; } bool ChatWidget::listScrollTo(int top, bool syntetic) { @@ -2883,9 +2887,7 @@ void ChatWidget::listMarkReadTill(not_null item) { void ChatWidget::listMarkContentsRead( const base::flat_set> &items) { - if (!_sublist) { - session().api().markContentsRead(items); - } + session().api().markContentsRead(items); } MessagesBarData ChatWidget::listMessagesBar( diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 63faa7f783..21f0946ef0 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -41,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/rect.h" #include "data/components/sponsored_messages.h" +#include "data/data_channel.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" @@ -475,6 +477,83 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } +void MonoforumSenderBar::init( + not_null parentChat, + not_null peer) { + author = peer; + text.setText(st::semiboldTextStyle, peer->name()); + const auto skip = st::monoforumBarUserpicSkip; + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + width = skip + userpic + skip * 2 + text.maxWidth() + st::msgServicePadding.right(); +} + +int MonoforumSenderBar::height() const { + return st::msgServiceMargin.top() + + st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + + st::msgServiceMargin.bottom(); +} + +void MonoforumSenderBar::paint( + Painter &p, + not_null st, + int y, + int w, + bool chatWide) const { + Expects(author != nullptr); + + int left = st::msgServiceMargin.left(); + const auto maxwidth = chatWide + ? std::min(w, WideChatWidth()) + : w; + w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); + + const auto use = std::min(w, width); + + left += (w - use) / 2; + int h = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); + ServiceMessagePainter::PaintBubble( + p, + st->msgServiceBg(), + st->serviceBgCornersNormal(), + QRect(left, y + st::msgServiceMargin.top(), use, h)); + + const auto skip = st::monoforumBarUserpicSkip; + { + auto pen = st->msgServiceBg()->p; + pen.setWidthF(skip); + pen.setCapStyle(Qt::RoundCap); + pen.setDashPattern({ 2., 2. }); + p.setPen(pen); + const auto top = y + st::msgServiceMargin.top() + (h / 2); + p.drawLine(0, top, left, top); + p.drawLine(left + use, top, 2 * w, top); + } + + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + const auto available = use - (skip + userpic + skip * 2 + st::msgServicePadding.right()); + + author->paintUserpic(p, view, left + skip, y + st::msgServiceMargin.top() + skip, userpic); + + p.setFont(st::msgServiceFont); + p.setPen(st->msgServiceFg()); + text.draw(p, { + .position = { + left + skip + userpic + skip * 2, + y + st::msgServiceMargin.top() + st::msgServicePadding.top(), + }, + .availableWidth = available, + .elisionLines = 1, + }); +} + void ServicePreMessage::init(PreparedServiceText string) { text = Ui::Text::String( st::serviceTextStyle, @@ -1220,6 +1299,7 @@ void Element::validateTextSkipBlock(bool has, int width, int height) { } void Element::previousInBlocksChanged() { + recountMonoforumSenderBarInBlocks(); recountDisplayDateInBlocks(); recountAttachToPreviousInBlocks(); } @@ -1255,7 +1335,8 @@ bool Element::computeIsAttachToPrevious(not_null previous) { const auto item = data(); if (!Has() && !Has() - && !Has()) { + && !Has() + && !Has()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) @@ -1385,6 +1466,37 @@ void Element::recountAttachToPreviousInBlocks() { setAttachToPrevious(attachToPrevious, previous); } +void Element::recountMonoforumSenderBarInBlocks() { + const auto item = data(); + const auto sublist = item->savedSublist(); + const auto parentChat = sublist ? sublist->parentChat() : nullptr; + const auto barPeer = [&]() -> PeerData* { + if (!parentChat + || isHidden() + || item->isEmpty() + || item->isSponsored()) { + return nullptr; + } + const auto peer = sublist->peer(); + if (const auto previous = previousDisplayedInBlocks()) { + const auto prev = previous->data(); + if (const auto prevSublist = prev->savedSublist()) { + Assert(prevSublist->parentChat() == parentChat); + if (prevSublist->peer() == peer) { + return nullptr; + } + } + } + return peer; + }(); + if (barPeer && !Has()) { + AddComponents(MonoforumSenderBar::Bit()); + Get()->init(parentChat, barPeer); + } else if (!barPeer && Has()) { + RemoveComponents(MonoforumSenderBar::Bit()); + } +} + void Element::recountDisplayDateInBlocks() { setDisplayDate([&] { const auto item = data(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index af62b3f3b6..e7c5ac94f9 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/runtime_composer.h" #include "base/flags.h" #include "base/weak_ptr.h" +#include "ui/userpic_view.h" class History; class HistoryBlock; @@ -58,6 +59,7 @@ enum class Context : char { Pinned, AdminLog, ContactPreview, + Monoforum, SavedSublist, TTLViewer, ShortcutMessages, @@ -220,7 +222,7 @@ QString DateTooltipText(not_null view); // Any HistoryView::Element can have this Component for // displaying the unread messages bar above the message. -struct UnreadBar : public RuntimeComponent { +struct UnreadBar : RuntimeComponent { void init(const QString &string); static int height(); @@ -241,7 +243,7 @@ struct UnreadBar : public RuntimeComponent { // Any HistoryView::Element can have this Component for // displaying the day mark above the message. -struct DateBadge : public RuntimeComponent { +struct DateBadge : RuntimeComponent { void init(const QString &date); int height() const; @@ -257,10 +259,27 @@ struct DateBadge : public RuntimeComponent { }; +struct MonoforumSenderBar : RuntimeComponent { + void init(not_null parentChat, not_null peer); + + int height() const; + void paint( + Painter &p, + not_null st, + int y, + int w, + bool chatWide) const; + + PeerData *author = nullptr; + Ui::Text::String text; + ClickHandlerPtr link; + mutable Ui::PeerUserpicView view; + int width = 0; +}; + // Any HistoryView::Element can have this Component for // displaying some text in layout of a service message above the message. -struct ServicePreMessage - : public RuntimeComponent { +struct ServicePreMessage : RuntimeComponent { void init(PreparedServiceText string); int resizeToWidth(int newWidth, bool chatWide); @@ -281,7 +300,7 @@ struct ServicePreMessage }; -struct FakeBotAboutTop : public RuntimeComponent { +struct FakeBotAboutTop : RuntimeComponent { void init(); Ui::Text::String text; @@ -289,7 +308,7 @@ struct FakeBotAboutTop : public RuntimeComponent { int height = 0; }; -struct PurchasedTag : public RuntimeComponent { +struct PurchasedTag : RuntimeComponent { Ui::Text::String text; }; @@ -629,14 +648,17 @@ protected: std::unique_ptr _reactions; private: + void recountMonoforumSenderBarInBlocks(); + // This should be called only from previousInBlocksChanged() // to add required bits to the Composer mask // after that always use Has(). void recountDisplayDateInBlocks(); // This should be called only from previousInBlocksChanged() or when - // DateBadge or UnreadBar bit is changed in the Composer mask - // then the result should be cached in a client side flag + // DateBadge or UnreadBar or MonoforumSenderBar bit + // is changed in the Composer mask then the result + // should be cached in a client side flag // HistoryView::Element::Flag::AttachedToPrevious. void recountAttachToPreviousInBlocks(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 9d7c809974..0482f59ec9 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1088,6 +1088,9 @@ int Message::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } + if (const auto monoforumBar = Get()) { + result += monoforumBar->height(); + } if (const auto service = Get()) { result += service->height; } @@ -1146,6 +1149,27 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } + if (const auto monoforumBar = Get()) { + auto barh = monoforumBar->height(); + auto skip = 0; + if (const auto date = Get()) { + skip += date->height(); + } + if (const auto bar = Get()) { + skip += bar->height(); + } + if (context.clip.intersects(QRect(0, skip, width(), barh))) { + p.translate(0, skip); + monoforumBar->paint( + p, + context.st, + 0, + width(), + delegate()->elementIsChatWide()); + p.translate(0, -skip); + } + } + if (const auto service = Get()) { service->paint(p, context, g, delegate()->elementIsChatWide()); } @@ -2458,6 +2482,8 @@ bool Message::hasFromPhoto() const { switch (context()) { case Context::AdminLog: return true; + case Context::Monoforum: + return delegate()->elementIsChatWide(); case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -3685,6 +3711,8 @@ bool Message::hasFromName() const { switch (context()) { case Context::AdminLog: return true; + case Context::Monoforum: + return false; case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -3953,6 +3981,8 @@ bool Message::displayFastShare() const { bool Message::displayGoToOriginal() const { if (isPinnedContext()) { return !hasOutLayout(); + } else if (context() == Context::Monoforum) { + return false; } const auto item = data(); if (const auto forwarded = item->Get()) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index a68c5fa971..efade0c7fa 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -35,8 +35,7 @@ class InlineList; } // namespace Reactions // Special type of Component for the channel actions log. -struct LogEntryOriginal - : public RuntimeComponent { +struct LogEntryOriginal : RuntimeComponent { LogEntryOriginal(); LogEntryOriginal(LogEntryOriginal &&other); LogEntryOriginal &operator=(LogEntryOriginal &&other); @@ -45,13 +44,12 @@ struct LogEntryOriginal std::unique_ptr page; }; -struct Factcheck -: public RuntimeComponent { +struct Factcheck : RuntimeComponent { std::unique_ptr page; bool expanded = false; }; -struct PsaTooltipState : public RuntimeComponent { +struct PsaTooltipState : RuntimeComponent { QString type; mutable ClickHandlerPtr link; mutable Ui::Animations::Simple buttonVisibleAnimation; diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 02a4aed234..9acf977d62 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -452,6 +452,9 @@ QSize Service::performCountCurrentSize(int newWidth) { if (const auto bar = Get()) { newHeight += bar->height(); } + if (const auto monoforumBar = Get()) { + newHeight += monoforumBar->height(); + } data()->resolveDependent(); @@ -525,6 +528,9 @@ int Service::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } + if (const auto monoforumBar = Get()) { + result += monoforumBar->height(); + } return result; } @@ -557,6 +563,27 @@ void Service::draw(Painter &p, const PaintContext &context) const { } } + if (const auto monoforumBar = Get()) { + auto barh = monoforumBar->height(); + auto skip = 0; + if (const auto date = Get()) { + skip += date->height(); + } + if (const auto bar = Get()) { + skip += bar->height(); + } + if (context.clip.intersects(QRect(0, skip, width(), barh))) { + p.translate(0, skip); + monoforumBar->paint( + p, + context.st, + 0, + width(), + delegate()->elementIsChatWide()); + p.translate(0, -skip); + } + } + if (isHidden()) { return; } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index af060ea552..978b95796b 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -492,6 +492,9 @@ void TopBarWidget::paintTopBar(Painter &p) { ? history->peer.get() : sublist ? sublist->peer().get() : nullptr; + const auto broadcastForMonoforum = history + ? history->peer->monoforumBroadcast() + : nullptr; if (topic && _activeChat.section == Section::Replies) { p.setPen(st::dialogsNameFg); topic->chatListNameText().drawElided( @@ -515,9 +518,12 @@ void TopBarWidget::paintTopBar(Painter &p) { } } else if (folder || (peer && (peer->sharedMediaInfo() || peer->isVerifyCodes())) + || broadcastForMonoforum || (_activeChat.section == Section::Scheduled) || (_activeChat.section == Section::Pinned)) { - auto text = (_activeChat.section == Section::Scheduled) + auto text = broadcastForMonoforum + ? broadcastForMonoforum->name() + u" Messages"_q AssertIsDebug() + : (_activeChat.section == Section::Scheduled) ? ((peer && peer->isSelf()) ? tr::lng_reminder_messages(tr::now) : tr::lng_scheduled_messages(tr::now)) diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index aaf8a82689..e8a1226cfe 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -628,10 +628,13 @@ Cover::Cover( : object_ptr( this, controller, - _peer, + (_peer->monoforumBroadcast() + ? _peer->monoforumBroadcast() + : _peer), Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, - _st.photo)) + _st.photo, + _peer->monoforumBroadcast() != nullptr)) , _changePersonal((role == Role::Info || topic || !_peer->isUser() @@ -647,6 +650,9 @@ Cover::Cover( , _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) , _refreshStatusTimer([this] { refreshStatusText(); }) { _peer->updateFull(); + if (const auto broadcast = _peer->monoforumBroadcast()) { + broadcast->updateFull(); + } _name->setSelectable(true); _name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now)); @@ -979,6 +985,12 @@ void Cover::refreshStatusText() { chat->count, int(chat->participants.size())); return { .text = ChatStatusText(fullCount, onlineCount, true) }; + } else if (auto broadcast = _peer->monoforumBroadcast()) { + auto result = ChatStatusText( + qMax(broadcast->membersCount(), 1), + 0, + false); + return TextWithEntities{ .text = result }; } else if (auto channel = _peer->asChannel()) { const auto onlineCount = _onlineCount.current(); const auto fullCount = qMax(channel->membersCount(), 1); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 42594dd010..aa8b1862fe 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -99,7 +99,9 @@ object_ptr InnerWidget::setupContent( result->add(std::move(actions)); } if (_peer->isChat() || _peer->isMegagroup()) { - setupMembers(result.data()); + if (!_peer->isMonoforum()) { + setupMembers(result.data()); + } } return result; } diff --git a/Telegram/SourceFiles/overview/overview_layout.h b/Telegram/SourceFiles/overview/overview_layout.h index 489e2666e5..ff0273d1d6 100644 --- a/Telegram/SourceFiles/overview/overview_layout.h +++ b/Telegram/SourceFiles/overview/overview_layout.h @@ -181,7 +181,7 @@ private: }; -struct Info : public RuntimeComponent { +struct Info : RuntimeComponent { int top = 0; }; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0ae4d9ba54..ef30b941de 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -49,6 +49,7 @@ msgReplyBarSize: size(2px, 36px); msgReplyBarSkip: 10px; msgServicePadding: margins(12px, 3px, 12px, 4px); msgServiceMargin: margins(10px, 10px, 10px, 2px); +monoforumBarUserpicSkip: 2px; msgDateSpace: 12px; msgDateDelta: point(2px, 5px); diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 9c4bb61a35..967ad72fa6 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -180,12 +180,14 @@ UserpicButton::UserpicButton( not_null peer, Role role, Source source, - const style::UserpicButton &st) + const style::UserpicButton &st, + bool forceForumShape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(controller) , _window(&controller->window()) , _peer(peer) +, _forceForumShape(forceForumShape) , _role(role) , _source(source) { if (_source == Source::Custom) { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index 07abeec1a2..6bd96f330e 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -69,7 +69,8 @@ public: not_null peer, Role role, Source source, - const style::UserpicButton &st); + const style::UserpicButton &st, + bool forceForumShape = false); UserpicButton( QWidget *parent, not_null peer, // Role::Custom, Source::PeerPhoto From e17bf18350dd536c1f6a22a34efac346d0880ce7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 13 May 2025 14:45:12 +0400 Subject: [PATCH 060/310] Update API scheme on layer 204. --- Telegram/SourceFiles/apiwrap.cpp | 1 + Telegram/SourceFiles/boxes/share_box.cpp | 1 + Telegram/SourceFiles/mtproto/scheme/api.tl | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 0c3b2a521c..645bd2766c 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3332,6 +3332,7 @@ void ApiWrap::forwardMessages( MTP_vector(randomIds), peer->input, MTP_int(topMsgId), + MTPInputReplyTo(), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index c262435b7f..a8ed979a42 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1708,6 +1708,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_vector(generateRandom()), peer->input, MTP_int(topMsgId), + MTPInputReplyTo(), MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as Data::ShortcutIdToMTP(session, options.shortcutId), diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index ba20014278..4a76f7a93f 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2178,7 +2178,7 @@ messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; messages.sendMessage#fbf2340a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; messages.sendMedia#a550cd78 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; -messages.forwardMessages#bb9fa475 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates; +messages.forwardMessages#38f0188c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#fc78af9b peer:InputPeer id:Vector option:bytes message:string = ReportResult; From 76db55ff19b83611e85ce2d564a129afa02aaee7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 13 May 2025 18:18:24 +0400 Subject: [PATCH 061/310] Support forwarding to monoforums. --- Telegram/SourceFiles/apiwrap.cpp | 18 ++- .../boxes/peer_list_controllers.cpp | 146 ++++++++++++++++++ .../SourceFiles/boxes/peer_list_controllers.h | 29 ++++ .../boxes/peers/edit_peer_invite_link.cpp | 4 + Telegram/SourceFiles/boxes/share_box.cpp | 86 ++++++++++- Telegram/SourceFiles/data/data_channel.cpp | 5 +- Telegram/SourceFiles/data/data_forum.cpp | 1 - .../SourceFiles/data/data_forum_topic.cpp | 2 +- Telegram/SourceFiles/data/data_forum_topic.h | 2 +- .../data/data_message_reactions.cpp | 2 +- .../SourceFiles/data/data_saved_messages.cpp | 25 ++- .../SourceFiles/data/data_saved_messages.h | 2 + .../SourceFiles/data/data_saved_sublist.cpp | 51 +++++- .../SourceFiles/data/data_saved_sublist.h | 25 +-- Telegram/SourceFiles/data/data_session.cpp | 2 + Telegram/SourceFiles/data/data_thread.cpp | 8 + Telegram/SourceFiles/data/data_thread.h | 3 +- .../SourceFiles/dialogs/dialogs_entry.cpp | 8 +- Telegram/SourceFiles/dialogs/dialogs_entry.h | 8 +- .../dialogs/dialogs_inner_widget.cpp | 4 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 14 +- .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 4 +- Telegram/SourceFiles/history/history.h | 2 +- Telegram/SourceFiles/history/history_item.cpp | 3 +- .../history/history_item_helpers.cpp | 2 + .../view/history_view_chat_section.cpp | 14 +- .../history/view/history_view_element.cpp | 6 +- .../history/view/history_view_message.cpp | 2 +- .../view/history_view_top_bar_widget.cpp | 9 +- .../info/media/info_media_buttons.cpp | 2 +- .../info/saved/info_saved_sublists_widget.cpp | 2 +- Telegram/SourceFiles/mainwidget.cpp | 25 ++- .../SourceFiles/window/window_peer_menu.cpp | 46 +++++- .../SourceFiles/window/window_peer_menu.h | 7 + .../window/window_session_controller.cpp | 20 ++- .../window/window_session_controller.h | 5 + 36 files changed, 516 insertions(+), 78 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 645bd2766c..90d013cb60 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -388,7 +388,7 @@ void ApiWrap::savePinnedOrder(not_null saved) { const auto &order = _session->data().pinnedChatsOrder(saved); const auto input = [](Dialogs::Key key) { if (const auto sublist = key.sublist()) { - return MTP_inputDialogPeer(sublist->peer()->input); + return MTP_inputDialogPeer(sublist->sublistPeer()->input); } Unexpected("Key type in pinnedDialogsOrder()."); }; @@ -3303,6 +3303,13 @@ void ApiWrap::forwardMessages( if (topMsgId) { sendFlags |= SendFlag::f_top_msg_id; } + const auto monoforumPeerId = action.replyTo.monoforumPeerId; + const auto monoforumPeer = monoforumPeerId + ? session().data().peer(monoforumPeerId).get() + : nullptr; + if (monoforumPeer) { + sendFlags |= SendFlag::f_reply_to; + } auto forwardFrom = draft.items.front()->history()->peer; auto ids = QVector(); @@ -3332,7 +3339,9 @@ void ApiWrap::forwardMessages( MTP_vector(randomIds), peer->input, MTP_int(topMsgId), - MTPInputReplyTo(), + (monoforumPeer + ? MTP_inputReplyToMonoForum(monoforumPeer->input) + : MTPInputReplyTo()), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), @@ -3379,7 +3388,10 @@ void ApiWrap::forwardMessages( .id = newId.msg, .flags = flags, .from = NewMessageFromId(action), - .replyTo = { .topicRootId = topMsgId }, + .replyTo = { + .topicRootId = topMsgId, + .monoforumPeerId = monoforumPeerId, + }, .date = NewMessageDate(action.options), .shortcutId = action.options.shortcutId, .starsPaid = action.options.starsApproved, diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 3b22147532..c33da2ab35 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -23,6 +23,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "main/main_session.h" #include "data/data_peer_values.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_channel.h" @@ -867,6 +869,45 @@ void ChooseRecipientBoxController::rowClicked(not_null row) { *weak = owned.data(); delegate()->peerListUiShow()->showBox(std::move(owned)); return; + } else if (const auto monoforum = peer->monoforum()) { + const auto weak = std::make_shared>(); + auto callback = [=](not_null sublist) { + const auto exists = guard.get(); + if (!exists) { + if (*weak) { + (*weak)->closeBox(); + } + return; + } + auto onstack = std::move(_callback); + onstack(sublist); + if (guard) { + _callback = std::move(onstack); + } else if (*weak) { + (*weak)->closeBox(); + } + }; + const auto filter = [=](not_null sublist) { + return guard && (!_filter || _filter(sublist)); + }; + auto owned = Box( + std::make_unique( + monoforum, + std::move(callback), + filter), + [=](not_null box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + monoforum->destroyed( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }); + *weak = owned.data(); + delegate()->peerListUiShow()->showBox(std::move(owned)); + return; } const auto history = peer->owner().history(peer); auto callback = std::move(_callback); @@ -1137,6 +1178,111 @@ auto ChooseTopicBoxController::createRow(not_null topic) return skip ? nullptr : std::make_unique(topic); }; +ChooseSublistBoxController::ChooseSublistBoxController( + not_null monoforum, + FnMut)> callback, + Fn)> filter) +: _monoforum(monoforum) +, _callback(std::move(callback)) +, _filter(std::move(filter)) { + setStyleOverrides(&st::chooseTopicList); + + _monoforum->chatsListChanges( + ) | rpl::start_with_next([=] { + refreshRows(); + }, lifetime()); + + _monoforum->sublistDestroyed( + ) | rpl::start_with_next([=](not_null sublist) { + const auto id = sublist->sublistPeer()->id.value; + if (const auto row = delegate()->peerListFindRow(id)) { + delegate()->peerListRemoveRow(row); + delegate()->peerListRefreshRows(); + } + }, lifetime()); +} + +Main::Session &ChooseSublistBoxController::session() const { + return _monoforum->session(); +} + +void ChooseSublistBoxController::rowClicked(not_null row) { + const auto weak = base::make_weak(this); + auto onstack = base::take(_callback); + onstack(_monoforum->sublist(row->peer())); + if (weak) { + _callback = std::move(onstack); + } +} + +void ChooseSublistBoxController::prepare() { + delegate()->peerListSetTitle(tr::lng_forward_choose()); + setSearchNoResultsText(tr::lng_topics_not_found(tr::now)); + delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled); + refreshRows(true); + + session().changes().entryUpdates( + Data::EntryUpdate::Flag::Repaint + ) | rpl::start_with_next([=](const Data::EntryUpdate &update) { + if (const auto sublist = update.entry->asSublist()) { + if (sublist->parent() == _monoforum) { + const auto id = sublist->sublistPeer()->id.value; + if (const auto row = delegate()->peerListFindRow(id)) { + delegate()->peerListUpdateRow(row); + } + } + } + }, lifetime()); +} + +void ChooseSublistBoxController::refreshRows(bool initial) { + auto added = false; + for (const auto &row : _monoforum->chatsList()->indexed()->all()) { + if (const auto sublist = row->sublist()) { + const auto id = sublist->sublistPeer()->id.value; + auto already = delegate()->peerListFindRow(id); + if (initial || !already) { + if (auto created = createRow(sublist)) { + delegate()->peerListAppendRow(std::move(created)); + added = true; + } + } else if (already->isSearchResult()) { + delegate()->peerListAppendFoundRow(already); + added = true; + } + } + } + if (added) { + delegate()->peerListRefreshRows(); + } +} + +void ChooseSublistBoxController::loadMoreRows() { + _monoforum->loadMore(); +} + +std::unique_ptr ChooseSublistBoxController::createSearchRow( + PeerListRowId id) { + const auto peer = session().data().peer(PeerId(id)); + if (const auto sublist = _monoforum->sublistLoaded(peer)) { + auto result = std::make_unique(sublist->sublistPeer()); + result->setCustomStatus(QString()); + return result; + } + return nullptr; +} + +auto ChooseSublistBoxController::createRow( + not_null sublist) +-> std::unique_ptr { + if (const auto skip = _filter && !_filter(sublist)) { + return nullptr; + } + auto result = std::make_unique(sublist->sublistPeer()); + result->setCustomStatus(QString()); + return result; +}; + void PaintRestrictionBadge( Painter &p, not_null st, diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index de9c67dbfe..24887d3df2 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -27,6 +27,8 @@ namespace Data { class Thread; class Forum; class ForumTopic; +class SavedSublist; +class SavedMessages; } // namespace Data namespace Ui { @@ -393,3 +395,30 @@ private: Fn)> _filter; }; + +class ChooseSublistBoxController final + : public PeerListController + , public base::has_weak_ptr { +public: + ChooseSublistBoxController( + not_null monoforum, + FnMut)> callback, + Fn)> filter = nullptr); + + Main::Session &session() const override; + void rowClicked(not_null row) override; + + void prepare() override; + void loadMoreRows() override; + std::unique_ptr createSearchRow(PeerListRowId id) override; + +private: + void refreshRows(bool initial = false); + [[nodiscard]] std::unique_ptr createRow( + not_null sublist); + + const not_null _monoforum; + FnMut)> _callback; + Fn)> _filter; + +}; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index 70ad41acb0..aba3714395 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_histories.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" @@ -1163,8 +1164,11 @@ void SingleRowController::prepare() { return; } const auto topic = strong->asTopic(); + const auto sublist = strong->asSublist(); auto row = topic ? ChooseTopicBoxController::MakeRow(topic) + : sublist + ? std::make_unique(sublist->sublistPeer()) : std::make_unique(strong->peer()); const auto raw = row.get(); if (_status) { diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index a8ed979a42..385ea7ea4f 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -45,6 +45,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_histories.h" #include "data/data_user.h" #include "data/data_peer_values.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_forum.h" @@ -114,7 +116,9 @@ private: not_null history; not_null peer; Data::ForumTopic *topic = nullptr; + Data::SavedSublist *sublist = nullptr; rpl::lifetime topicLifetime; + rpl::lifetime sublistLifetime; Ui::RoundImageCheckbox checkbox; Ui::Text::String name; Ui::Animations::Simple nameActive; @@ -143,6 +147,7 @@ private: void preloadUserpic(not_null entry); void changeCheckState(Chat *chat); void chooseForumTopic(not_null forum); + void chooseMonoforumSublist(not_null monoforum); enum class ChangeStateWay { Default, SkipCallback, @@ -638,15 +643,18 @@ void ShareBox::addPeerToMultiSelect(not_null thread) { auto addItemWay = Ui::MultiSelect::AddItemWay::Default; const auto peer = thread->peer(); const auto topic = thread->asTopic(); + const auto sublist = thread->asSublist(); _select->addItem( peer->id.value, (topic ? topic->title() + : sublist + ? sublist->sublistPeer()->shortName() : peer->isSelf() ? tr::lng_saved_short(tr::now) : peer->shortName()), st::activeButtonBg, - (topic + ((topic || sublist) ? ForceRoundUserpicCallback(peer) : PaintUserpicCallback(peer, true)), addItemWay); @@ -970,6 +978,8 @@ void ShareBox::Inner::updateChatName(not_null chat) { const auto peer = chat->peer; const auto text = chat->topic ? chat->topic->title() + : chat->sublist + ? chat->sublist->sublistPeer()->name() : peer->isSelf() ? tr::lng_saved_messages(tr::now) : peer->isRepliesChat() @@ -1209,7 +1219,7 @@ ShareBox::Inner::Chat::Chat( st.checkbox, updateCallback, PaintUserpicCallback(peer, true), - [=](int size) { return peer->isForum() + [=](int size) { return (peer->isForum() || peer->isMonoforum()) ? int(size * Ui::ForumUserpicRadiusMultiplier()) : std::optional(); }) , name(st.checkbox.imageRadius * 2) { @@ -1350,10 +1360,13 @@ void ShareBox::Inner::changeCheckState(Chat *chat) { const auto checked = chat->checkbox.checked(); const auto forum = chat->peer->forum(); - if (checked || !forum) { + const auto monoforum = chat->peer->monoforum(); + if (checked || (!forum && !monoforum)) { changePeerCheckState(chat, !checked); - } else { - chooseForumTopic(chat->peer->forum()); + } else if (forum) { + chooseForumTopic(forum); + } else if (monoforum) { + chooseMonoforumSublist(monoforum); } } @@ -1404,6 +1417,54 @@ void ShareBox::Inner::chooseForumTopic(not_null forum) { _show->showBox(std::move(box)); } +void ShareBox::Inner::chooseMonoforumSublist( + not_null monoforum) { + const auto guard = Ui::MakeWeak(this); + const auto weak = std::make_shared>(); + auto chosen = [=](not_null sublist) { + if (const auto strong = *weak) { + strong->closeBox(); + } + if (!guard) { + return; + } + const auto row = _chatsIndexed->getRow(sublist->owningHistory()); + if (!row) { + return; + } + const auto chat = getChat(row); + Assert(!chat->sublist); + chat->sublist = sublist; + chat->sublist->destroyed( + ) | rpl::start_with_next([=] { + changePeerCheckState(chat, false); + }, chat->sublistLifetime); + updateChatName(chat); + changePeerCheckState(chat, true); + }; + auto initBox = [=](not_null box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + monoforum->destroyed( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }; + auto filter = [=](not_null sublist) { + return guard && _descriptor.filterCallback(sublist); + }; + auto box = Box( + std::make_unique( + monoforum, + std::move(chosen), + std::move(filter)), + std::move(initBox)); + *weak = box.data(); + _show->showBox(std::move(box)); +} + void ShareBox::Inner::peerUnselected(not_null peer) { if (const auto i = _dataMap.find(peer); i != end(_dataMap)) { changePeerCheckState( @@ -1434,6 +1495,11 @@ void ShareBox::Inner::changePeerCheckState( chat->topic = nullptr; updateChatName(chat); } + if (chat->sublist) { + chat->sublistLifetime.destroy(); + chat->sublist = nullptr; + updateChatName(chat); + } } if (useCallback != ChangeStateWay::SkipCallback && _peerSelectedChangedCallback) { @@ -1565,6 +1631,8 @@ not_null ShareBox::Inner::chatThread( not_null chat) const { return chat->topic ? (Data::Thread*)chat->topic + : chat->sublist + ? (Data::Thread*)chat->sublist : chat->peer->owner().history(chat->peer).get(); } @@ -1675,6 +1743,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( api.sendMessage(std::move(message)); } const auto topicRootId = thread->topicRootId(); + const auto sublistPeer = thread->maybeSublistPeer(); const auto kGeneralId = Data::ForumTopic::kGeneralId; const auto topMsgId = (topicRootId == kGeneralId) ? MsgId(0) @@ -1699,7 +1768,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) - | (starsPaid ? Flag::f_allow_paid_stars : Flag()); + | (starsPaid ? Flag::f_allow_paid_stars : Flag()) + | (sublistPeer ? Flag::f_reply_to : Flag()); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( MTP_flags(sendFlags), @@ -1708,7 +1778,9 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_vector(generateRandom()), peer->input, MTP_int(topMsgId), - MTPInputReplyTo(), + (sublistPeer + ? MTP_inputReplyToMonoForum(sublistPeer->input) + : MTPInputReplyTo()), MTP_int(options.scheduled), MTP_inputPeerEmpty(), // send_as Data::ShortcutIdToMTP(session, options.shortcutId), diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 0159fcca80..179b77d38a 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -228,6 +228,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { } } if (diff & (Flag::Forum + | Flag::Monoforum | Flag::CallNotEmpty | Flag::SimilarExpanded | Flag::Signatures @@ -236,12 +237,14 @@ void ChannelData::setFlags(ChannelDataFlags which) { if (diff & Flag::CallNotEmpty) { history->updateChatListEntry(); } - if (diff & Flag::Forum) { + if (diff & (Flag::Forum | Flag::Monoforum)) { Core::App().notifications().clearFromHistory(history); history->updateChatListEntryHeight(); if (history->inChatList()) { if (const auto forum = this->forum()) { forum->preloadTopics(); + } else if (const auto monoforum = this->monoforum()) { + monoforum->loadMore(); } } } diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 361135d885..4172ad1806 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -48,7 +48,6 @@ Forum::Forum(not_null history) , _topicsList(&session(), {}, owner().maxPinnedChatsLimitValue(this)) { Expects(_history->peer->isChannel()); - if (_history->inChatList()) { preloadTopics(); } diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index eb93c36375..6e03125eb9 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -867,7 +867,7 @@ void ForumTopic::setMuted(bool muted) { session().changes().topicUpdated(this, UpdateFlag::Notifications); } -not_null ForumTopic::sendActionPainter() { +HistoryView::SendActionPainter *ForumTopic::sendActionPainter() { return _sendActionPainter.get(); } diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index 06423e4750..aafa12a788 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -181,7 +181,7 @@ public: void setMuted(bool muted) override; [[nodiscard]] auto sendActionPainter() - ->not_null override; + -> HistoryView::SendActionPainter* override; private: enum class Flag : uchar { diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index e5dd4cdfbd..fec98a7920 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -1028,7 +1028,7 @@ void Reactions::requestMyTags(SavedSublist *sublist) { using Flag = MTPmessages_GetSavedReactionTags::Flag; my.requestId = api.request(MTPmessages_GetSavedReactionTags( MTP_flags(sublist ? Flag::f_peer : Flag()), - (sublist ? sublist->peer()->input : MTP_inputPeerEmpty()), + (sublist ? sublist->sublistPeer()->input : MTP_inputPeerEmpty()), MTP_long(my.hash) )).done([=](const MTPmessages_SavedReactionTags &result) { auto &my = _myTags[sublist]; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 72f5aca91f..a2c3f75daf 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_unread_things.h" #include "main/main_session.h" namespace Data { @@ -23,6 +24,7 @@ constexpr auto kPerPage = 50; constexpr auto kFirstPerPage = 10; constexpr auto kListPerPage = 100; constexpr auto kListFirstPerPage = 20; +constexpr auto kLoadedSublistsMinCount = 20; } // namespace @@ -36,6 +38,10 @@ SavedMessages::SavedMessages( FilterId(), _owner->maxPinnedChatsLimitValue(this)) , _loadMore([=] { sendLoadMoreRequests(); }) { + if (_parentChat + && _parentChat->owner().history(_parentChat)->inChatList()) { + preloadSublists(); + } } SavedMessages::~SavedMessages() = default; @@ -61,15 +67,19 @@ not_null SavedMessages::chatsList() { } not_null SavedMessages::sublist(not_null peer) { - const auto i = _sublists.find(peer); - if (i != end(_sublists)) { - return i->second.get(); + if (const auto loaded = sublistLoaded(peer)) { + return loaded; } return _sublists.emplace( peer, std::make_unique(this, peer)).first->second.get(); } +SavedSublist *SavedMessages::sublistLoaded(not_null peer) { + const auto i = _sublists.find(peer); + return (i != end(_sublists)) ? i->second.get() : nullptr; +} + rpl::producer<> SavedMessages::chatsListChanges() const { return _chatsListChanges.events(); } @@ -78,6 +88,13 @@ rpl::producer<> SavedMessages::chatsListLoadedEvents() const { return _chatsListLoadedEvents.events(); } +void SavedMessages::preloadSublists() { + if (parentChat() + && chatsList()->indexed()->size() < kLoadedSublistsMinCount) { + loadMore(); + } +} + void SavedMessages::loadMore() { _loadMoreScheduled = true; _loadMore.call(); @@ -152,7 +169,7 @@ void SavedMessages::sendLoadMore(not_null sublist) { MTPmessages_GetSavedHistory( MTP_flags(_parentChat ? Flag::f_parent_peer : Flag(0)), _parentChat ? _parentChat->input : MTPInputPeer(), - sublist->peer()->input, + sublist->sublistPeer()->input, MTP_int(offsetId), MTP_int(offsetDate), MTP_int(0), // add_offset diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 6d6bbe3236..983fb7d08e 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -33,6 +33,7 @@ public: [[nodiscard]] not_null chatsList(); [[nodiscard]] not_null sublist(not_null peer); + [[nodiscard]] SavedSublist *sublistLoaded(not_null peer); [[nodiscard]] rpl::producer<> chatsListChanges() const; [[nodiscard]] rpl::producer<> chatsListLoadedEvents() const; @@ -41,6 +42,7 @@ public: [[nodiscard]] auto sublistDestroyed() const -> rpl::producer>; + void preloadSublists(); void loadMore(); void loadMore(not_null sublist); diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 0ecfcf6739..c33361008c 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_item_preview.h" #include "history/history.h" #include "history/history_item.h" +#include "history/history_unread_things.h" #include "main/main_session.h" namespace Data { @@ -22,7 +23,7 @@ namespace Data { SavedSublist::SavedSublist( not_null parent, not_null peer) -: Entry(&peer->owner(), Dialogs::Entry::Type::SavedSublist) +: Thread(&peer->owner(), Dialogs::Entry::Type::SavedSublist) , _parent(parent) , _history(peer->owner().history(peer)) { } @@ -33,7 +34,7 @@ not_null SavedSublist::parent() const { return _parent; } -not_null SavedSublist::parentHistory() const { +not_null SavedSublist::owningHistory() { const auto chat = parentChat(); return _history->owner().history(chat ? (PeerData*)chat @@ -44,18 +45,27 @@ ChannelData *SavedSublist::parentChat() const { return _parent->parentChat(); } -not_null SavedSublist::peer() const { +not_null SavedSublist::sublistPeer() const { return _history->peer; } bool SavedSublist::isHiddenAuthor() const { - return peer()->isSavedHiddenAuthor(); + return sublistPeer()->isSavedHiddenAuthor(); } bool SavedSublist::isFullLoaded() const { return (_flags & Flag::FullLoaded) != 0; } +rpl::producer<> SavedSublist::destroyed() const { + using namespace rpl::mappers; + return rpl::merge( + _parent->destroyed(), + _parent->sublistDestroyed() | rpl::filter( + _1 == this + ) | rpl::to_empty); +} + auto SavedSublist::messages() const -> const std::vector> & { return _items; @@ -231,8 +241,39 @@ void SavedSublist::paintUserpic( _history->paintUserpic(p, view, context); } +HistoryView::SendActionPainter *SavedSublist::sendActionPainter() { + return nullptr; +} + +void SavedSublist::hasUnreadMentionChanged(bool has) { + auto was = chatListUnreadState(); + if (has) { + was.mentions = 0; + } else { + was.mentions = 1; + } + notifyUnreadStateChange(was); +} + +void SavedSublist::hasUnreadReactionChanged(bool has) { + auto was = chatListUnreadState(); + if (has) { + was.reactions = was.reactionsMuted = 0; + } else { + was.reactions = 1; + was.reactionsMuted = muted() ? was.reactions : 0; + } + notifyUnreadStateChange(was); +} + +bool SavedSublist::isServerSideUnread( + not_null item) const { + return false; +} + + void SavedSublist::chatListPreloadData() { - peer()->loadUserpic(); + sublistPeer()->loadUserpic(); allowChatListMessageResolve(); } diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 669aa97311..4217a8fb67 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -7,8 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "data/data_thread.h" #include "dialogs/ui/dialogs_message_view.h" -#include "dialogs/dialogs_entry.h" class PeerData; class History; @@ -18,17 +18,18 @@ namespace Data { class Session; class SavedMessages; -class SavedSublist final : public Dialogs::Entry { +class SavedSublist final : public Data::Thread { public: - SavedSublist(not_null parent,not_null peer); + SavedSublist(not_null parent, not_null peer); ~SavedSublist(); [[nodiscard]] not_null parent() const; - [[nodiscard]] not_null parentHistory() const; + [[nodiscard]] not_null owningHistory() override; [[nodiscard]] ChannelData *parentChat() const; - [[nodiscard]] not_null peer() const; + [[nodiscard]] not_null sublistPeer() const; [[nodiscard]] bool isHiddenAuthor() const; [[nodiscard]] bool isFullLoaded() const; + [[nodiscard]] rpl::producer<> destroyed() const; [[nodiscard]] auto messages() const -> const std::vector> &; @@ -41,10 +42,6 @@ public: [[nodiscard]] std::optional fullCount() const; [[nodiscard]] rpl::producer fullCountValue() const; - [[nodiscard]] Dialogs::Ui::MessageView &lastItemDialogsView() { - return _lastItemDialogsView; - } - int fixedOnTopIndex() const override; bool shouldBeInChatList() const override; Dialogs::UnreadState chatListUnreadState() const override; @@ -57,12 +54,21 @@ public: const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; + void hasUnreadMentionChanged(bool has) override; + void hasUnreadReactionChanged(bool has) override; + + [[nodiscard]] bool isServerSideUnread( + not_null item) const override; + void chatListPreloadData() override; void paintUserpic( Painter &p, Ui::PeerUserpicView &view, const Dialogs::Ui::PaintContext &context) const override; + [[nodiscard]] auto sendActionPainter() + -> HistoryView::SendActionPainter* override; + private: enum class Flag : uchar { ResolveChatListMessage = (1 << 0), @@ -81,7 +87,6 @@ private: std::vector> _items; std::optional _fullCount; rpl::event_stream<> _changed; - Dialogs::Ui::MessageView _lastItemDialogsView; Flags _flags; }; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index d995d42083..dd71002706 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4656,6 +4656,8 @@ void Session::refreshChatListEntry(Dialogs::Key key) { } if (const auto forum = history->peer->forum()) { forum->preloadTopics(); + } else if (const auto monoforum = history->peer->monoforum()) { + monoforum->preloadSublists(); } if (history->peer->isMonoforum() && !history->peer->monoforumBroadcast()) { diff --git a/Telegram/SourceFiles/data/data_thread.cpp b/Telegram/SourceFiles/data/data_thread.cpp index 1934c34507..67a346f7e2 100644 --- a/Telegram/SourceFiles/data/data_thread.cpp +++ b/Telegram/SourceFiles/data/data_thread.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum_topic.h" #include "data/data_changes.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_unread_things.h" @@ -31,6 +32,13 @@ MsgId Thread::topicRootId() const { return MsgId(); } +PeerData *Thread::maybeSublistPeer() const { + if (const auto sublist = asSublist()) { + return sublist->sublistPeer(); + } + return nullptr; +} + not_null Thread::peer() const { return owningHistory()->peer; } diff --git a/Telegram/SourceFiles/data/data_thread.h b/Telegram/SourceFiles/data/data_thread.h index 9bbc6635fe..10eb0d27ee 100644 --- a/Telegram/SourceFiles/data/data_thread.h +++ b/Telegram/SourceFiles/data/data_thread.h @@ -67,6 +67,7 @@ public: return const_cast(this)->owningHistory(); } [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] PeerData *maybeSublistPeer() const; [[nodiscard]] not_null peer() const; [[nodiscard]] PeerNotifySettings ¬ify(); [[nodiscard]] const PeerNotifySettings ¬ify() const; @@ -112,7 +113,7 @@ public: } [[nodiscard]] virtual auto sendActionPainter() - -> not_null = 0; + -> HistoryView::SendActionPainter* = 0; [[nodiscard]] bool hasPinnedMessages() const; void setHasPinnedMessages(bool has); diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index 501883a416..899fdde8db 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -84,9 +84,9 @@ Entry::Entry(not_null owner, Type type) , _flags((type == Type::History) ? (Flag::IsThread | Flag::IsHistory) : (type == Type::ForumTopic) - ? Flag::IsThread + ? (Flag::IsThread | Flag::IsForumTopic) : (type == Type::SavedSublist) - ? Flag::IsSavedSublist + ? (Flag::IsThread | Flag::IsSavedSublist) : Flag(0)) { } @@ -113,7 +113,7 @@ Data::Forum *Entry::asForum() { } Data::Folder *Entry::asFolder() { - return (_flags & (Flag::IsThread | Flag::IsSavedSublist)) + return (_flags & Flag::IsThread) ? nullptr : static_cast(this); } @@ -125,7 +125,7 @@ Data::Thread *Entry::asThread() { } Data::ForumTopic *Entry::asTopic() { - return ((_flags & Flag::IsThread) && !(_flags & Flag::IsHistory)) + return (_flags & Flag::IsForumTopic) ? static_cast(this) : nullptr; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h index e52b45048d..8838bd05fb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.h +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h @@ -27,6 +27,7 @@ class Forum; class Folder; class ForumTopic; class SavedSublist; +class SavedMessages; class Thread; } // namespace Data @@ -168,9 +169,10 @@ private: enum class Flag : uchar { IsThread = (1 << 0), IsHistory = (1 << 1), - IsSavedSublist = (1 << 2), - UpdatePostponed = (1 << 3), - InUnreadChangeBlock = (1 << 4), + IsForumTopic = (1 << 2), + IsSavedSublist = (1 << 3), + UpdatePostponed = (1 << 4), + InUnreadChangeBlock = (1 << 5), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 79996bd7b6..2df6141026 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -3444,7 +3444,7 @@ void InnerWidget::applySearchState(SearchState state) { _searchFromShown = ignoreInChat ? nullptr : sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : state.fromPeer; if (state.inChat) { onHashtagFilterUpdate(QStringView()); @@ -4222,7 +4222,7 @@ void InnerWidget::updateSearchIn() { const auto peerIcon = peer ? Ui::MakeUserpicThumbnail(peer) : sublist - ? Ui::MakeUserpicThumbnail(sublist->peer()) + ? Ui::MakeUserpicThumbnail(sublist->sublistPeer()) : nullptr; const auto myIcon = Ui::MakeIconThumbnail(st::menuIconChats); const auto publicIcon = (_searchHashOrCashtag != HashOrCashtag::None) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 6dea61f38a..b311225bf2 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1001,7 +1001,7 @@ void Widget::chosenRow(const ChosenRow &row) { using namespace HistoryView; controller()->showSection( std::make_shared(ChatViewId{ - .history = sublist->parentHistory(), + .history = sublist->owningHistory(), .sublist = sublist, }), params); @@ -2037,7 +2037,7 @@ void Widget::refreshTopBars() { ? Dialogs::Key(history) : Dialogs::Key(_openedFolder)), .section = Dialogs::EntryState::Section::ChatsList, - }, history ? history->sendActionPainter().get() : nullptr); + }, history ? history->sendActionPainter() : nullptr); if (_forumSearchRequested) { showSearchInTopBar(anim::type::instant); } @@ -2680,7 +2680,7 @@ bool Widget::search(bool inCache, SearchRequestDelay delay) { : _searchState.inChat.sublist(); const auto fromPeer = sublist ? nullptr : _searchQueryFrom; const auto savedPeer = sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : nullptr; _historiesRequest = histories.sendRequest(history, type, [=]( Fn finish) { @@ -2856,7 +2856,7 @@ void Widget::searchMore() { : _searchState.inChat.sublist(); const auto fromPeer = sublist ? nullptr : _searchQueryFrom; const auto savedPeer = sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : nullptr; _historiesRequest = histories.sendRequest(history, type, [=]( Fn finish) { @@ -4284,8 +4284,12 @@ PeerData *Widget::searchInPeer() const { ? nullptr : _openedForum ? _openedForum->channel().get() + : _openedMonoforum + ? (_openedMonoforum->parentChat() + ? _openedMonoforum->parentChat() + : (PeerData*)session().user().get()) : _searchState.inChat.sublist() - ? session().user().get() + ? _searchState.inChat.sublist()->owningHistory()->peer.get() : _searchState.inChat.peer(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index dfdb7891d3..628c0ae4d5 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -62,7 +62,7 @@ const auto kPsaBadgePrefix = "cloud_lng_badge_psa_"; [[nodiscard]] bool ShowSendActionInDialogs(Data::Thread *thread) { const auto history = thread ? thread->owningHistory().get() : nullptr; - if (!history) { + if (!history || thread->asSublist()) { return false; } else if (const auto user = history->peer->asUser()) { return !user->lastseen().isHidden(); @@ -994,7 +994,7 @@ void RowPainter::Paint( ? history->peer->migrateTo() : history->peer.get()) : sublist - ? sublist->peer().get() + ? sublist->sublistPeer().get() : nullptr; const auto allowUserOnline = true;// !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 8963ab0a08..f8e0791d74 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -273,7 +273,7 @@ public: void setHasPendingResizedItems(); [[nodiscard]] auto sendActionPainter() - -> not_null override { + -> HistoryView::SendActionPainter* override { return &_sendActionPainter; } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index e084f47e6e..f5e349a505 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -537,6 +537,7 @@ HistoryItem::HistoryItem( const auto topicRootId = fields.replyTo.topicRootId; config.reply.messageId = config.reply.topMessageId = topicRootId; config.reply.topicPost = (topicRootId != 0) ? 1 : 0; + config.reply.monoforumPeerId = fields.replyTo.monoforumPeerId; if (const auto originalReply = original->Get()) { if (originalReply->external()) { config.reply = originalReply->fields().clone(this); @@ -3579,7 +3580,7 @@ Data::SavedSublist *HistoryItem::savedSublist() const { PeerData *HistoryItem::savedSublistPeer() const { if (const auto sublist = savedSublist()) { - return sublist->peer(); + return sublist->sublistPeer(); } return nullptr; } diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 7c38991768..6bf1382f86 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -503,6 +503,8 @@ TimeId NewMessageDate(const Api::SendOptions &options) { PeerId NewMessageFromId(const Api::SendAction &action) { return action.options.sendAs ? action.options.sendAs->id + : action.history->peer->amMonoforumAdmin() + ? action.history->peer->monoforumBroadcast()->id : action.history->peer->amAnonymous() ? PeerId() : action.history->session().userPeerId(); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 6883918710..7bada5f52e 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1693,7 +1693,7 @@ SendMenu::Details ChatWidget::sendMenuDetails() const { FullReplyTo ChatWidget::replyTo() const { const auto monoforumPeerId = (_sublist && _sublist->parentChat()) - ? _sublist->peer()->id + ? _sublist->sublistPeer()->id : PeerId(); if (auto custom = _composeControls->replyingToMessage()) { const auto item = custom.messageId @@ -1786,7 +1786,7 @@ void ChatWidget::checkLastPinnedClickedIdReset( } void ChatWidget::setupOpenChatButton() { - if (!_sublist || _sublist->peer()->isSavedHiddenAuthor()) { + if (!_sublist || _sublist->sublistPeer()->isSavedHiddenAuthor()) { return; } else if (_sublist->parentChat()) { _canSendTexts = true; @@ -1794,22 +1794,22 @@ void ChatWidget::setupOpenChatButton() { } _openChatButton = std::make_unique( this, - (_sublist->peer()->isBroadcast() + (_sublist->sublistPeer()->isBroadcast() ? tr::lng_saved_open_channel(tr::now) - : _sublist->peer()->isUser() + : _sublist->sublistPeer()->isUser() ? tr::lng_saved_open_chat(tr::now) : tr::lng_saved_open_group(tr::now)), st::historyComposeButton); _openChatButton->setClickedCallback([=] { controller()->showPeerHistory( - _sublist->peer(), + _sublist->sublistPeer(), Window::SectionShow::Way::Forward); }); } void ChatWidget::setupAboutHiddenAuthor() { - if (!_sublist || !_sublist->peer()->isSavedHiddenAuthor()) { + if (!_sublist || !_sublist->sublistPeer()->isSavedHiddenAuthor()) { return; } else if (_sublist->parentChat()) { _canSendTexts = true; @@ -3260,7 +3260,7 @@ bool ChatWidget::searchInChatEmbedded( this, controller(), _history, - sublist->peer(), + sublist->sublistPeer(), query); updateControlsGeometry(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 21f0946ef0..eec58baf5c 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1477,17 +1477,17 @@ void Element::recountMonoforumSenderBarInBlocks() { || item->isSponsored()) { return nullptr; } - const auto peer = sublist->peer(); + const auto sublistPeer = sublist->sublistPeer(); if (const auto previous = previousDisplayedInBlocks()) { const auto prev = previous->data(); if (const auto prevSublist = prev->savedSublist()) { Assert(prevSublist->parentChat() == parentChat); - if (prevSublist->peer() == peer) { + if (prevSublist->sublistPeer() == sublistPeer) { return nullptr; } } } - return peer; + return sublistPeer; }(); if (barPeer && !Has()) { AddComponents(MonoforumSenderBar::Bit()); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 0482f59ec9..49a7afafce 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3712,7 +3712,7 @@ bool Message::hasFromName() const { case Context::AdminLog: return true; case Context::Monoforum: - return false; + return data()->out(); case Context::History: case Context::ChatPreview: case Context::TTLViewer: diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 978b95796b..d0e2aca0e6 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -82,7 +82,7 @@ QString TopBarNameText( const Dialogs::EntryState &state) { if (state.section == Dialogs::EntryState::Section::SavedSublist && state.key.sublist() - && state.key.sublist()->parentHistory()->peer->isSelf()) { + && state.key.sublist()->owningHistory()->peer->isSelf()) { if (peer->isSelf()) { return tr::lng_my_notes(tr::now); } else if (peer->isSavedHiddenAuthor()) { @@ -490,7 +490,8 @@ void TopBarWidget::paintTopBar(Painter &p) { const auto history = _activeChat.key.history(); const auto namePeer = history ? history->peer.get() - : sublist ? sublist->peer().get() + : sublist + ? sublist->sublistPeer().get() : nullptr; const auto broadcastForMonoforum = history ? history->peer->monoforumBroadcast() @@ -746,9 +747,9 @@ void TopBarWidget::infoClicked() { return; } else if (const auto topic = key.topic()) { _controller->showSection(std::make_shared(topic)); - } else if ([[maybe_unused]] const auto sublist = key.sublist()) { + } else if (const auto sublist = key.sublist()) { _controller->showSection(std::make_shared( - _controller->session().user(), + sublist->owningHistory()->peer, Info::Section(Storage::SharedMediaType::Photo))); } else if (key.peer()->savedSublistsInfo()) { _controller->showSection(std::make_shared( diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index 9f53a17e2b..9d91702861 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -275,7 +275,7 @@ not_null AddSavedSublistButton( const auto sublist = peer->owner().savedMessages().sublist(peer); navigation->showSection( std::make_shared(ChatViewId{ - .history = sublist->parentHistory(), + .history = sublist->owningHistory(), .sublist = sublist, })); }); diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index 3afead749b..0ab5a23af5 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -69,7 +69,7 @@ SublistsWidget::SublistsWidget( params.dropSameFromStack = true; controller->showSection( std::make_shared(ChatViewId{ - .history = sublist->parentHistory(), + .history = sublist->owningHistory(), .sublist = sublist, }), params); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 3c33394661..e0953d45ca 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -644,6 +644,17 @@ bool MainWidget::filesOrForwardDrop( clearHider(_hider); } return true; + } else if (const auto history = thread->asHistory() + ; history && history->peer->monoforum()) { + Window::ShowDropMediaBox( + _controller, + Core::ShareMimeMediaData(data), + history->peer->monoforum()); + if (_hider) { + _hider->startHide(); + clearHider(_hider); + } + return true; } if (data->hasFormat(u"application/x-td-forward"_q)) { auto draft = Data::ForwardDraft{ @@ -780,7 +791,7 @@ void MainWidget::searchMessages( using namespace HistoryView; controller()->showSection( std::make_shared(ChatViewId{ - .history = sublist->parentHistory(), + .history = sublist->owningHistory(), .sublist = sublist, })); } else if (!tags.empty()) { @@ -1548,6 +1559,12 @@ void MainWidget::showMessage( if (params.activation != anim::activation::background) { _controller->window().activate(); } + } else if (const auto sublist = item->savedSublist() + ; sublist && sublist->parentChat()) { + _controller->showSublist(sublist, item->id, params); + if (params.activation != anim::activation::background) { + _controller->window().activate(); + } } else { // showPeerHistory may be redirected to different window, // so we don't call activate() on current controller's window. @@ -2621,10 +2638,10 @@ auto MainWidget::thirdSectionForCurrentMainSection( return std::make_shared( peer, Info::Memento::DefaultSection(peer)); - } else if (key.sublist()) { + } else if (const auto sublist = key.sublist()) { return std::make_shared( - session().user(), - Info::Memento::DefaultSection(session().user())); + sublist->owningHistory()->peer, + Info::Memento::DefaultSection(sublist->owningHistory()->peer)); } Unexpected("Key in MainWidget::thirdSectionForCurrentMainSection()."); } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 841bffbd82..2f14936778 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -88,6 +88,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_user.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_histories.h" #include "data/data_chat_filters.h" @@ -435,7 +436,7 @@ void TogglePinnedThread( : MTPmessages_ToggleSavedDialogPin::Flag(0); owner->session().api().request(MTPmessages_ToggleSavedDialogPin( MTP_flags(flags), - MTP_inputDialogPeer(sublist->peer()->input) + MTP_inputDialogPeer(sublist->sublistPeer()->input) )).done([=] { owner->notifyPinnedDialogsOrderUpdated(); if (onToggled) { @@ -655,10 +656,9 @@ void Filler::addNewWindow() { _addAction(tr::lng_context_new_window(tr::now), [=] { Ui::PreventDelayedActivation(); if (const auto sublist = weak.get()) { - const auto peer = sublist->peer(); controller->showInNewWindow(SeparateId( SeparateType::SavedSublist, - peer->owner().history(peer))); + sublist->owner().history(sublist->sublistPeer()))); } }, &st::menuIconNewWindow); AddSeparatorAndShiftUp(_addAction); @@ -2850,6 +2850,46 @@ QPointer ShowDropMediaBox( return weak->data(); } +QPointer ShowDropMediaBox( + not_null navigation, + std::shared_ptr data, + not_null monoforum, + FnMut &&successCallback) { + const auto weak = std::make_shared>(); + auto chosen = [ + data = std::move(data), + callback = std::move(successCallback), + weak, + navigation + ](not_null sublist) mutable { + const auto content = navigation->parentController()->content(); + if (!content->filesOrForwardDrop(sublist, data.get())) { + return; + } else if (const auto strong = *weak) { + strong->closeBox(); + } + if (callback) { + callback(); + } + }; + auto initBox = [=](not_null box) { + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + monoforum->destroyed( + ) | rpl::start_with_next([=] { + box->closeBox(); + }, box->lifetime()); + }; + *weak = navigation->parentController()->show(Box( + std::make_unique( + monoforum, + std::move(chosen)), + std::move(initBox))); + return weak->data(); +} + QPointer ShowSendNowMessagesBox( not_null navigation, not_null history, diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index a125c3c55d..fbc677bdb6 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -32,6 +32,8 @@ class Folder; class Session; struct ForwardDraft; class ForumTopic; +class SavedMessages; +class SavedSublist; class Thread; } // namespace Data @@ -188,6 +190,11 @@ QPointer ShowDropMediaBox( std::shared_ptr data, not_null forum, FnMut &&successCallback = nullptr); +QPointer ShowDropMediaBox( + not_null navigation, + std::shared_ptr data, + not_null monoforum, + FnMut &&successCallback = nullptr); QPointer ShowSendNowMessagesBox( not_null navigation, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 2cb230d6c8..f22dd049fe 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1259,12 +1259,30 @@ void SessionNavigation::showTopic( params); } +void SessionNavigation::showSublist( + not_null sublist, + MsgId itemId, + const SectionShow ¶ms) { + using namespace HistoryView; + auto memento = std::make_shared( + ChatViewId{ + .history = sublist->owningHistory(), + .sublist = sublist, + }, + itemId, + params.highlightPart, + params.highlightPartOffsetHint); + showSection(std::move(memento), params); +} + void SessionNavigation::showThread( not_null thread, MsgId itemId, const SectionShow ¶ms) { if (const auto topic = thread->asTopic()) { showTopic(topic, itemId, params); + } else if (const auto sublist = thread->asSublist()) { + showSublist(sublist, itemId, params); } else { showPeerHistory(thread->asHistory(), params, itemId); } @@ -1346,7 +1364,7 @@ void SessionNavigation::showByInitialId( using namespace HistoryView; showSection( std::make_shared(ChatViewId{ - .history = id.sublist()->parentHistory(), + .history = id.sublist()->owningHistory(), .sublist = id.sublist(), }), instant); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 7eea85827a..391fbbafd3 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -74,6 +74,7 @@ enum class CloudThemeType; class Thread; class Forum; class ForumTopic; +class SavedSublist; class WallPaper; } // namespace Data @@ -201,6 +202,10 @@ public: not_null topic, MsgId itemId = 0, const SectionShow ¶ms = SectionShow()); + void showSublist( + not_null sublist, + MsgId itemId = 0, + const SectionShow ¶ms = SectionShow()); void showThread( not_null thread, MsgId itemId = 0, From b91a040a32a2c4305f78d9005749eb6ae40a9aad Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 16 May 2025 12:58:49 +0400 Subject: [PATCH 062/310] Update API scheme on layer 204. --- Telegram/SourceFiles/data/data_channel.cpp | 39 +++++++++------ Telegram/SourceFiles/data/data_channel.h | 1 + Telegram/SourceFiles/data/data_peer.cpp | 7 +-- .../SourceFiles/data/data_saved_messages.cpp | 48 ++++++++++++------- Telegram/SourceFiles/data/data_session.cpp | 21 ++++++-- Telegram/SourceFiles/history/history_item.cpp | 29 +++++++---- .../history/history_item_components.h | 6 ++- Telegram/SourceFiles/mtproto/scheme/api.tl | 8 +++- .../window/window_session_controller.cpp | 2 +- 9 files changed, 108 insertions(+), 53 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 179b77d38a..b207e62857 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -185,6 +185,9 @@ void ChannelData::setAccessHash(uint64 accessHash) { } void ChannelData::setFlags(ChannelDataFlags which) { + if (which & Flag::MonoforumAdmin) { + which |= Flag::Monoforum; + } if (which & (Flag::Forum | Flag::Monoforum)) { which |= Flag::Megagroup; } @@ -202,15 +205,15 @@ void ChannelData::setFlags(ChannelDataFlags which) { const auto takenForum = ((diff & Flag::Forum) && !(which & Flag::Forum)) ? mgInfo->takeForumData() : nullptr; - const auto takenMonoforum = ((diff & Flag::Monoforum) - && !(which & Flag::Monoforum)) + const auto takenMonoforum = ((diff & Flag::MonoforumAdmin) + && !(which & Flag::MonoforumAdmin)) ? mgInfo->takeMonoforumData() : nullptr; const auto wasIn = amIn(); - if ((diff & Flag::Forum) && (which & Flag::Forum)) { - mgInfo->ensureForum(this); - } else if ((diff & Flag::Monoforum) && (which & Flag::Monoforum)) { + if ((diff & Flag::MonoforumAdmin) && (which & Flag::MonoforumAdmin)) { mgInfo->ensureMonoforum(this); + } else if ((diff & Flag::Forum) && (which & Flag::Forum)) { + mgInfo->ensureForum(this); } _flags.set(which); if (diff & (Flag::Left | Flag::Forbidden)) { @@ -228,7 +231,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { } } if (diff & (Flag::Forum - | Flag::Monoforum + | Flag::MonoforumAdmin | Flag::CallNotEmpty | Flag::SimilarExpanded | Flag::Signatures @@ -237,7 +240,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { if (diff & Flag::CallNotEmpty) { history->updateChatListEntry(); } - if (diff & (Flag::Forum | Flag::Monoforum)) { + if (diff & (Flag::Forum | Flag::MonoforumAdmin)) { Core::App().notifications().clearFromHistory(history); history->updateChatListEntryHeight(); if (history->inChatList()) { @@ -334,9 +337,14 @@ bool ChannelData::discussionLinkKnown() const { } void ChannelData::setMonoforumLink(ChannelData *link) { - if (_monoforumLink != link) { - _monoforumLink = link; - session().changes().peerUpdated(this, UpdateFlag::MonoforumLink); + if (_monoforumLink || !link) { + return; + } + _monoforumLink = link; + link->setMonoforumLink(this); + session().changes().peerUpdated(this, UpdateFlag::MonoforumLink); + if (isMegagroup() && (link->amCreator() || link->hasAdminRights())) { + setFlags(flags() | Flag::MonoforumAdmin); } } @@ -826,6 +834,12 @@ void ChannelData::setAdminRights(ChatAdminRights rights) { session().changes().peerUpdated( this, UpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers); + if (isBroadcast() && _monoforumLink) { + const auto flags = _monoforumLink->flags(); + const auto admin = (amCreator() || hasAdminRights()); + _monoforumLink->setFlags((flags & ~Flag::MonoforumAdmin) + | (admin ? Flag::MonoforumAdmin : Flag())); + } } void ChannelData::setRestrictions(ChatRestrictionsInfo rights) { @@ -1291,11 +1305,6 @@ void ApplyChannelUpdate( } else { channel->setDiscussionLink(nullptr); } - if (const auto chat = update.vlinked_monoforum_id()) { - channel->setMonoforumLink(channel->owner().channelLoaded(chat->v)); - } else { - channel->setMonoforumLink(nullptr); - } if (const auto history = channel->owner().historyLoaded(channel)) { if (const auto available = update.vavailable_min_id()) { history->clearUpTill(available->v); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 6dc802dcfc..1a502f5290 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -80,6 +80,7 @@ enum class ChannelDataFlag : uint64 { PaidMessagesAvailable = (1ULL << 37), AutoTranslation = (1ULL << 38), Monoforum = (1ULL << 39), + MonoforumAdmin = (1ULL << 40), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index b6191a4dfe..2799c278c3 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1604,9 +1604,10 @@ bool PeerData::canManageGroupCall() const { } bool PeerData::amMonoforumAdmin() const { - const auto broadcast = monoforumBroadcast(); - return broadcast - && (broadcast->amCreator() || broadcast->hasAdminRights()); + if (const auto channel = asChannel()) { + return channel->flags() & ChannelDataFlag::MonoforumAdmin; + } + return false; } int PeerData::starsPerMessage() const { diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index a2c3f75daf..39b5821a08 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -264,21 +264,35 @@ void SavedMessages::apply( auto offsetPeer = (PeerData*)nullptr; const auto selfId = _owner->session().userPeerId(); for (const auto &dialog : *list) { - const auto &data = dialog.data(); - const auto peer = _owner->peer(peerFromMTP(data.vpeer())); - const auto topId = MsgId(data.vtop_message().v); - if (const auto item = _owner->message(selfId, topId)) { - offsetPeer = peer; - offsetDate = item->date(); - offsetId = topId; - lastValid = true; - const auto entry = sublist(peer); - const auto entryPinned = pinned || data.is_pinned(); - entry->applyMaybeLast(item); - _owner->setPinnedFromEntryList(entry, entryPinned); - } else { - lastValid = false; - } + dialog.match([&](const MTPDsavedDialog &data) { + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + const auto topId = MsgId(data.vtop_message().v); + if (const auto item = _owner->message(selfId, topId)) { + offsetPeer = peer; + offsetDate = item->date(); + offsetId = topId; + lastValid = true; + const auto entry = sublist(peer); + const auto entryPinned = pinned || data.is_pinned(); + entry->applyMaybeLast(item); + _owner->setPinnedFromEntryList(entry, entryPinned); + } else { + lastValid = false; + } + }, [&](const MTPDmonoForumDialog &data) { + const auto peer = _owner->peer(peerFromMTP(data.vpeer())); + const auto topId = MsgId(data.vtop_message().v); + if (const auto item = _owner->message(selfId, topId)) { + offsetPeer = peer; + offsetDate = item->date(); + offsetId = topId; + lastValid = true; + const auto entry = sublist(peer); + entry->applyMaybeLast(item); + } else { + lastValid = false; + } + }); } if (pinned) { } else if (!lastValid) { @@ -359,8 +373,8 @@ rpl::producer<> SavedMessages::destroyed() const { return _parentChat->flagsValue( ) | rpl::filter([=](const ChannelData::Flags::Change &update) { using Flag = ChannelData::Flag; - return (update.diff & Flag::Monoforum) - && !(update.value & Flag::Monoforum); + return (update.diff & Flag::MonoforumAdmin) + && !(update.value & Flag::MonoforumAdmin); }) | rpl::take(1) | rpl::to_empty; } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index dd71002706..89074c285e 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -377,14 +377,14 @@ void Session::clear() { auto forums = base::flat_set>(); for (const auto &[peerId, peer] : _peers) { if (const auto channel = peer->asChannel()) { - if (channel->isForum() || channel->isMonoforum()) { + if (channel->isForum() || channel->amMonoforumAdmin()) { forums.emplace(channel); } } } for (const auto &channel : forums) { channel->setFlags(channel->flags() - & ~(ChannelDataFlag::Forum | ChannelDataFlag::Monoforum)); + & ~(ChannelDataFlag::Forum | ChannelDataFlag::MonoforumAdmin)); } _sendActionManager->clear(); @@ -1026,6 +1026,16 @@ not_null Session::processChat(const MTPChat &data) { channel->setStarsPerMessage( data.vsend_paid_messages_stars().value_or_empty()); + if (const auto monoforum = data.vlinked_monoforum_id()) { + if (const auto linked = channelLoaded(monoforum->v)) { + channel->setMonoforumLink(linked); + } else { + channel->updateFull(); + } + } else { + channel->setMonoforumLink(nullptr); + } + if (wasInChannel != channel->amIn()) { flags |= UpdateFlag::ChannelAmIn; } @@ -4659,9 +4669,10 @@ void Session::refreshChatListEntry(Dialogs::Key key) { } else if (const auto monoforum = history->peer->monoforum()) { monoforum->preloadSublists(); } - if (history->peer->isMonoforum() - && !history->peer->monoforumBroadcast()) { - history->peer->updateFull(); + if (const auto broadcast = history->peer->monoforumBroadcast()) { + if (!broadcast->isFullLoaded()) { + broadcast->updateFull(); + } } } } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index f5e349a505..b7f9305f33 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -790,7 +790,14 @@ HistoryItem::~HistoryItem() { reply->clearData(this); } if (const auto saved = Get()) { - saved->sublist->removeOne(this); + if (saved->savedMessagesSublist) { + saved->savedMessagesSublist->removeOne(this); + } else if (const auto monoforum = _history->peer->monoforum()) { + const auto peer = _history->owner().peer(saved->sublistPeerId); + if (const auto sublist = monoforum->sublistLoaded(peer)) { + sublist->removeOne(this); + } + } } clearDependencyMessage(); applyTTL(0); @@ -3436,7 +3443,7 @@ FullStoryId HistoryItem::replyToStory() const { } FullReplyTo HistoryItem::replyTo() const { - const auto monoforumPeer = _history->peer->isMonoforum() + const auto monoforumPeer = _history->peer->amMonoforumAdmin() ? savedSublistPeer() : nullptr; auto result = FullReplyTo{ @@ -3560,19 +3567,26 @@ bool HistoryItem::isEmpty() const { Data::SavedSublist *HistoryItem::savedSublist() const { if (const auto saved = Get()) { - return saved->sublist; + if (saved->savedMessagesSublist) { + return saved->savedMessagesSublist; + } else if (const auto monoforum = _history->peer->monoforum()) { + const auto peer = _history->owner().peer(saved->sublistPeerId); + return monoforum->sublist(peer).get(); + } } else if (_history->peer->isSelf()) { const auto sublist = _history->owner().savedMessages().sublist( _history->peer); const auto that = const_cast(this); that->AddComponents(HistoryMessageSaved::Bit()); - that->Get()->sublist = sublist; + const auto saved = that->Get(); + saved->sublistPeerId = _history->peer->id; + saved->savedMessagesSublist = sublist; return sublist; } else if (const auto monoforum = _history->peer->monoforum()) { const auto sublist = monoforum->sublist(_from); const auto that = const_cast(this); that->AddComponents(HistoryMessageSaved::Bit()); - that->Get()->sublist = sublist; + that->Get()->sublistPeerId = _from->id; return sublist; } return nullptr; @@ -3802,10 +3816,7 @@ void HistoryItem::createComponents(CreateConfig &&config) { config.savedSublistPeer = _history->session().userPeerId(); } } - const auto peer = _history->owner().peer(config.savedSublistPeer); - saved->sublist = _history->peer->isSelf() - ? _history->owner().savedMessages().sublist(peer) - : _history->peer->monoforum()->sublist(peer); + saved->sublistPeerId = config.savedSublistPeer; } if (const auto reply = Get()) { diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 33fa873334..6ec434a967 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -174,7 +174,11 @@ struct HistoryMessageSavedMediaData struct HistoryMessageSaved : RuntimeComponent { - Data::SavedSublist *sublist = nullptr; + PeerId sublistPeerId = 0; + + // This can't change after the message is created, but is required + // frequently in reactions, so we cache the value here. + Data::SavedSublist *savedMessagesSublist = nullptr; }; class ReplyToMessagePointer final { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 4a76f7a93f..48cffcfef6 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -99,11 +99,11 @@ userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long 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#6592a1a7 id:long title:string = Chat; -channel#7482147e 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 fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date: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 usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long = Chat; +channel#fe685355 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 fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date: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 usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#7fc3facc 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 flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long 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:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long 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?long 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 ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int linked_monoforum_id:flags2.21?long = ChatFull; +channelFull#52d6806b 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 flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long 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:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long 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?long 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 ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -433,6 +433,8 @@ updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Updat updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update; updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector next_offset:int = Update; +updateReadMonoForumInbox#bcf34712 flags:# channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -1713,6 +1715,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; +monoForumDialog#31ac5089 peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; @@ -2392,6 +2395,7 @@ messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector = Bool; +messages.readSavedHistory#baab7bd6 parent_peer:InputPeer peer:InputPeer max_id:int = messages.Messages; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index f22dd049fe..9ea12353a1 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -603,7 +603,7 @@ void SessionNavigation::showPeerByLinkResolved( showPeerInfo(peer, params); } else if (resolveType == ResolveType::HashtagSearch) { searchMessages(info.text, peer->owner().history(peer)); - } else if ((peer->isForum() || peer->isMonoforum()) + } else if ((peer->isForum() || peer->amMonoforumAdmin()) && resolveType != ResolveType::Boost) { const auto itemId = info.messageId; if (!itemId) { From 5dc50b6d96d7137ca8c2b4de20e3d0f462aeb0db Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 16 May 2025 13:45:00 +0400 Subject: [PATCH 063/310] Respect price of messages to channels. --- .../SourceFiles/boxes/edit_privacy_box.cpp | 26 ++++++++++++------- Telegram/SourceFiles/boxes/edit_privacy_box.h | 4 ++- .../boxes/peers/edit_peer_permissions_box.cpp | 4 ++- Telegram/SourceFiles/data/data_channel.cpp | 12 +++++---- Telegram/SourceFiles/data/data_channel.h | 2 +- Telegram/SourceFiles/data/data_peer.cpp | 6 ++--- .../SourceFiles/history/history_widget.cpp | 2 +- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 1f83b004cb..2047539943 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -45,8 +45,8 @@ namespace { constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value; constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value; -constexpr auto kStarsMin = 1; -constexpr auto kDefaultChargeStars = 10; +constexpr auto kDefaultDirectMessagesPrice = 10; +constexpr auto kDefaultPrivateMessagesPrice = 10; using Exceptions = Api::UserPrivacy::Exceptions; @@ -464,6 +464,7 @@ auto PrivacyExceptionsBoxController::createRow(not_null history) int valuesCount, Fn valueByIndex, int value, + int minValue, int maxValue, Fn valueProgress, Fn valueFinished) { @@ -473,7 +474,7 @@ auto PrivacyExceptionsBoxController::createRow(not_null history) const auto labels = raw->add(object_ptr(raw)); const auto min = Ui::CreateChild( raw, - QString::number(kStarsMin), + QString::number(minValue), *labelStyle); const auto max = Ui::CreateChild( raw, @@ -1035,7 +1036,8 @@ void EditMessagesPrivacyBox( state->stars = SetupChargeSlider( chargeInner, session->user(), - savedValue); + (savedValue > 0) ? savedValue : std::optional(), + kDefaultPrivateMessagesPrice); Ui::AddSkip(chargeInner); Ui::AddSubsectionTitle( @@ -1164,14 +1166,16 @@ void EditMessagesPrivacyBox( rpl::producer SetupChargeSlider( not_null container, not_null peer, - int savedValue) { + std::optional savedValue, + int defaultValue, + bool allowZero) { struct State { rpl::variable stars; }; const auto broadcast = peer->isBroadcast(); const auto group = !broadcast && !peer->isUser(); const auto state = container->lifetime().make_state(); - const auto chargeStars = savedValue ? savedValue : kDefaultChargeStars; + const auto chargeStars = savedValue.value_or(defaultValue); state->stars = chargeStars; Ui::AddSubsectionTitle(container, (group || broadcast) @@ -1179,11 +1183,12 @@ rpl::producer SetupChargeSlider( : tr::lng_messages_privacy_price()); auto values = std::vector(); + const auto minStars = allowZero ? 0 : 1; const auto maxStars = peer->session().appConfig().paidMessageStarsMax(); - if (chargeStars < kStarsMin) { + if (chargeStars < minStars) { values.push_back(chargeStars); } - for (auto i = kStarsMin; i < std::min(100, maxStars); ++i) { + for (auto i = minStars; i < std::min(100, maxStars); ++i) { values.push_back(i); } for (auto i = 100; i < std::min(1000, maxStars); i += 10) { @@ -1210,6 +1215,7 @@ rpl::producer SetupChargeSlider( valuesCount, [=](int index) { return values[index]; }, chargeStars, + minStars, maxStars, setStars, setStars), @@ -1273,7 +1279,9 @@ void EditDirectMessagesPriceBox( SetupChargeSlider( inner, channel, - savedValue.value_or(0) + savedValue, + kDefaultDirectMessagesPrice, + true ) | rpl::start_with_next([=](int stars) { *result = stars; }, box->lifetime()); diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.h b/Telegram/SourceFiles/boxes/edit_privacy_box.h index d194572dc3..d46cbbfa8e 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.h +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.h @@ -173,7 +173,9 @@ void EditMessagesPrivacyBox( [[nodiscard]] rpl::producer SetupChargeSlider( not_null container, not_null peer, - int savedValue); + std::optional savedValue, + int defaultValue, + bool allowZero = false); void EditDirectMessagesPriceBox( not_null box, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 58cfd01fcf..d171501b95 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -53,6 +53,7 @@ namespace { constexpr auto kSlowmodeValues = 7; constexpr auto kBoostsUnrestrictValues = 5; constexpr auto kForceDisableTooltipDuration = 3 * crl::time(1000); +constexpr auto kDefaultChargeStars = 10; [[nodiscard]] auto Dependencies(PowerSaving::Flags) -> std::vector> { @@ -1196,7 +1197,8 @@ void ShowEditPeerPermissionsBox( state->starsPerMessage = SetupChargeSlider( chargeInner, peer, - starsPerMessage); + (starsPerMessage > 0) ? starsPerMessage : std::optional(), + kDefaultChargeStars); } static constexpr auto kSendRestrictions = Flag::EmbedLinks diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index b207e62857..213737dd45 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -934,15 +934,17 @@ void ChannelData::growSlowmodeLastMessage(TimeId when) { } int ChannelData::starsPerMessage() const { - if (const auto info = mgInfo.get()) { - return info->_starsPerMessage; + if (const auto broadcast = monoforumBroadcast()) { + if (!amMonoforumAdmin()) { + return broadcast->starsPerMessage(); + } } - return 0; + return _starsPerMessage; } void ChannelData::setStarsPerMessage(int stars) { - if (mgInfo && starsPerMessage() != stars) { - mgInfo->_starsPerMessage = stars; + if (_starsPerMessage != stars) { + _starsPerMessage = stars; session().changes().peerUpdated(this, UpdateFlag::StarsPerMessage); } checkTrustedPayForMessage(); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 1a502f5290..5b8c637120 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -166,7 +166,6 @@ private: Data::ChatBotCommands _botCommands; std::unique_ptr _forum; std::unique_ptr _monoforum; - int _starsPerMessage = 0; friend class ChannelData; @@ -596,6 +595,7 @@ private: int _kickedCount = 0; int _pendingRequestsCount = 0; int _levelHint = 0; + int _starsPerMessage = 0; Data::AllowedReactions _allowedReactions; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 2799c278c3..80d81c74bb 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1621,9 +1621,9 @@ int PeerData::starsPerMessage() const { int PeerData::starsPerMessageChecked() const { if (const auto channel = asChannel()) { - return (channel->adminRights() || channel->amCreator()) - ? 0 - : channel->starsPerMessage(); + if (channel->adminRights() || channel->amCreator()) { + return 0; + } } return starsPerMessage(); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0ce8c1becd..c66d5cbb86 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2114,7 +2114,7 @@ void HistoryWidget::setupDirectMessageButton() { _muteUnmute.data(), st::historyDirectMessage); widthValue() | rpl::start_with_next([=](int width) { - _directMessage->moveToRight(0, 0, width); + _directMessage->moveToLeft(0, 0, width); }, _directMessage->lifetime()); _directMessage->setClickedCallback([=] { if (const auto channel = _peer ? _peer->asChannel() : nullptr) { From 358e64f2ccb0e54336de5b84c3ae1e23bb0d1734 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 16 May 2025 16:29:40 +0400 Subject: [PATCH 064/310] Show monoforums as forums in chats list. --- Telegram/SourceFiles/data/data_channel.cpp | 2 +- Telegram/SourceFiles/data/data_forum.cpp | 2 +- Telegram/SourceFiles/data/data_histories.cpp | 22 ++-- .../SourceFiles/data/data_saved_messages.cpp | 75 ++++++++++++- .../SourceFiles/data/data_saved_messages.h | 12 +++ .../SourceFiles/data/data_saved_sublist.cpp | 11 ++ .../SourceFiles/dialogs/dialogs_entry.cpp | 7 ++ .../dialogs/dialogs_inner_widget.cpp | 9 +- Telegram/SourceFiles/dialogs/dialogs_row.cpp | 4 +- .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 25 +++-- .../dialogs/ui/dialogs_message_view.cpp | 27 +++-- .../dialogs/ui/dialogs_message_view.h | 5 +- .../dialogs/ui/dialogs_topics_view.cpp | 102 ++++++++++++++++-- .../dialogs/ui/dialogs_topics_view.h | 15 ++- Telegram/SourceFiles/history/history.cpp | 10 +- Telegram/SourceFiles/history/history.h | 4 +- Telegram/SourceFiles/history/history_item.cpp | 24 +++-- Telegram/SourceFiles/history/history_item.h | 2 +- .../view/history_view_chat_section.cpp | 4 +- .../history/view/history_view_element.cpp | 2 +- .../view/history_view_top_bar_widget.cpp | 5 +- .../info/profile/info_profile_actions.cpp | 9 +- 22 files changed, 303 insertions(+), 75 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 213737dd45..9250df02a9 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -247,7 +247,7 @@ void ChannelData::setFlags(ChannelDataFlags which) { if (const auto forum = this->forum()) { forum->preloadTopics(); } else if (const auto monoforum = this->monoforum()) { - monoforum->loadMore(); + monoforum->preloadSublists(); } } } diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 4172ad1806..80f51c42b2 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -202,7 +202,7 @@ void Forum::applyTopicDeleted(MsgId rootId) { } void Forum::reorderLastTopics() { - // We want first kShowChatNamesCount histories, by last message date. + // We want first kShowTopicNamesCount histories, by last message date. const auto pred = [](not_null a, not_null b) { const auto aItem = a->chatListMessage(); const auto bItem = b->chatListMessage(); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index bd7093579c..f13dccc97a 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -60,14 +60,14 @@ MTPInputReplyTo ReplyToForMTP( && (to->history() != history || to->id != replyingToTopicId)) ? to->topicRootId() : replyingToTopicId; - const auto possibleMonoforumPeer = (to && to->savedSublistPeer()) - ? to->savedSublistPeer() + const auto possibleMonoforumPeerId = (to && to->sublistPeerId()) + ? to->sublistPeerId() : replyTo.monoforumPeerId - ? history->owner().peer(replyTo.monoforumPeerId).get() - : history->session().user().get(); - const auto replyToMonoforumPeer = history->peer->amMonoforumAdmin() - ? possibleMonoforumPeer - : nullptr; + ? replyTo.monoforumPeerId + : history->session().user()->id; + const auto replyToMonoforumPeerId = history->peer->amMonoforumAdmin() + ? possibleMonoforumPeerId + : PeerId(); const auto external = replyTo.messageId && (replyTo.messageId.peer != history->peer->id || replyingToTopicId != replyToTopicId); @@ -82,7 +82,9 @@ MTPInputReplyTo ReplyToForMTP( | (replyTo.quote.text.isEmpty() ? Flag() : (Flag::f_quote_text | Flag::f_quote_offset)) - | (replyToMonoforumPeer ? Flag::f_monoforum_peer_id : Flag()) + | (replyToMonoforumPeerId + ? Flag::f_monoforum_peer_id + : Flag()) | (quoteEntities.v.isEmpty() ? Flag() : Flag::f_quote_entities)), @@ -94,8 +96,8 @@ MTPInputReplyTo ReplyToForMTP( MTP_string(replyTo.quote.text), quoteEntities, MTP_int(replyTo.quoteOffset), - (replyToMonoforumPeer - ? replyToMonoforumPeer->input + (replyToMonoforumPeerId + ? history->owner().peer(replyToMonoforumPeerId)->input : MTPInputPeer())); } else if (history->peer->amMonoforumAdmin() && replyTo.monoforumPeerId) { diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 39b5821a08..82fc03c3a5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -25,6 +25,7 @@ constexpr auto kFirstPerPage = 10; constexpr auto kListPerPage = 100; constexpr auto kListFirstPerPage = 20; constexpr auto kLoadedSublistsMinCount = 20; +constexpr auto kShowSublistNamesCount = 5; } // namespace @@ -33,13 +34,13 @@ SavedMessages::SavedMessages( ChannelData *parentChat) : _owner(owner) , _parentChat(parentChat) +, _parentHistory(parentChat ? owner->history(parentChat).get() : nullptr) , _chatsList( &_owner->session(), FilterId(), _owner->maxPinnedChatsLimitValue(this)) , _loadMore([=] { sendLoadMoreRequests(); }) { - if (_parentChat - && _parentChat->owner().history(_parentChat)->inChatList()) { + if (_parentHistory && _parentHistory->inChatList()) { preloadSublists(); } } @@ -128,6 +129,7 @@ void SavedMessages::sendLoadMore() { if (_chatsList.loaded()) { _chatsListLoadedEvents.fire({}); } + reorderLastSublists(); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { _unsupported = true; @@ -366,6 +368,75 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { }); } +void SavedMessages::reorderLastSublists() { + if (!_parentHistory) { + return; + } + + // We want first kShowChatNamesCount histories, by last message date. + const auto pred = []( + not_null a, + not_null b) { + const auto aItem = a->chatListMessage(); + const auto bItem = b->chatListMessage(); + const auto aDate = aItem ? aItem->date() : TimeId(0); + const auto bDate = bItem ? bItem->date() : TimeId(0); + return aDate > bDate; + }; + _lastSublists.clear(); + _lastSublists.reserve(kShowSublistNamesCount + 1); + auto &&sublists = ranges::views::all( + *_chatsList.indexed() + ) | ranges::views::transform([](not_null row) { + return row->sublist(); + }); + auto nonPinnedChecked = 0; + for (const auto sublist : sublists) { + const auto i = ranges::upper_bound( + _lastSublists, + not_null(sublist), + pred); + if (size(_lastSublists) < kShowSublistNamesCount + || i != end(_lastSublists)) { + _lastSublists.insert(i, sublist); + } + if (size(_lastSublists) > kShowSublistNamesCount) { + _lastSublists.pop_back(); + } + if (!sublist->isPinnedDialog(FilterId()) + && ++nonPinnedChecked >= kShowSublistNamesCount) { + break; + } + } + ++_lastSublistsVersion; + _parentHistory->updateChatListEntry(); +} + +void SavedMessages::listMessageChanged(HistoryItem *from, HistoryItem *to) { + if (from || to) { + reorderLastSublists(); + } +} + +int SavedMessages::recentSublistsListVersion() const { + return _lastSublistsVersion; +} + +void SavedMessages::recentSublistsInvalidate( + not_null sublist) { + Expects(_parentHistory != nullptr); + + if (ranges::contains(_lastSublists, sublist)) { + ++_lastSublistsVersion; + _parentHistory->updateChatListEntry(); + } +} + +auto SavedMessages::recentSublists() const +-> const std::vector> & { + return _lastSublists; +} + rpl::producer<> SavedMessages::destroyed() const { if (!_parentChat) { return rpl::never<>(); diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 983fb7d08e..dd03e2d2a8 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -49,18 +49,27 @@ public: void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); + void listMessageChanged(HistoryItem *from, HistoryItem *to); + [[nodiscard]] int recentSublistsListVersion() const; + void recentSublistsInvalidate(not_null sublist); + [[nodiscard]] auto recentSublists() const + -> const std::vector> &; + [[nodiscard]] rpl::lifetime &lifetime(); private: void loadPinned(); void apply(const MTPmessages_SavedDialogs &result, bool pinned); + void reorderLastSublists(); + void sendLoadMore(); void sendLoadMore(not_null sublist); void sendLoadMoreRequests(); const not_null _owner; ChannelData *_parentChat = nullptr; + History *_parentHistory = nullptr; rpl::event_stream> _sublistDestroyed; @@ -81,6 +90,9 @@ private: base::flat_set> _loadMoreSublistsScheduled; bool _loadMoreScheduled = false; + std::vector> _lastSublists; + int _lastSublistsVersion = 0; + rpl::event_stream<> _chatsListChanges; rpl::event_stream<> _chatsListLoadedEvents; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index c33361008c..367570cc0e 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_sublist.h" #include "data/data_histories.h" +#include "data/data_channel.h" #include "data/data_peer.h" #include "data/data_user.h" #include "data/data_saved_messages.h" @@ -80,6 +81,7 @@ void SavedSublist::applyMaybeLast(not_null item, bool added) { : (IsServerMsgId(b->id) ? false : (a->id < b->id)); }; + const auto was = _items.empty() ? nullptr : _items.front().get(); if (_items.empty()) { _items.push_back(item); } else if (_items.front() == item) { @@ -104,6 +106,8 @@ void SavedSublist::applyMaybeLast(not_null item, bool added) { if (_items.front() == item) { setChatListTimeId(item->date()); resolveChatListMessageGroup(); + + _parent->listMessageChanged(was, item.get()); } _changed.fire({}); } @@ -132,6 +136,8 @@ void SavedSublist::removeOne(not_null item) { } else { setChatListTimeId(_items.front()->date()); } + + _parent->listMessageChanged(item.get(), chatListMessage()); } if (removed || _fullCount) { _changed.fire({}); @@ -195,6 +201,11 @@ int SavedSublist::fixedOnTopIndex() const { } bool SavedSublist::shouldBeInChatList() const { + if (const auto monoforum = _parent->parentChat()) { + if (monoforum == sublistPeer()) { + return false; + } + } return isPinnedDialog(FilterId()) || !_items.empty(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index 899fdde8db..747cb519af 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -229,6 +229,13 @@ uint64 Entry::computeSortPosition(FilterId filterId) const { } void Entry::updateChatListExistence() { + if (const auto history = asHistory()) { + if (const auto channel = history->peer->asMonoforum()) { + if (!folderKnown()) { + history->clearFolder(); + } + } + } setChatListExistence(shouldBeInChatList()); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 2df6141026..deab114692 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -892,10 +892,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { const auto active = mayBeActive && isRowActive(row, activeEntry); const auto history = key.history(); const auto forum = history && history->isForum(); - if (forum && !_topicJumpCache) { + const auto monoforum = history && history->amMonoforumAdmin(); + if ((forum || monoforum) && !_topicJumpCache) { _topicJumpCache = std::make_unique(); } - const auto expanding = forum + const auto expanding = (forum || monoforum) && (history->peer->id == childListShown.peerId); context.rightButton = maybeCacheRightButton(row); if (history) { @@ -921,14 +922,14 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } } - context.st = (forum ? &st::forumDialogRow : _st.get()); + context.st = (forum || monoforum) ? &st::forumDialogRow : _st.get(); auto chatsFilterTags = std::vector(); if (context.narrow) { context.chatsFilterTags = nullptr; } else if (row->entry()->hasChatsFilterTags(context.filter)) { const auto a = active; - context.st = forum + context.st = (forum || monoforum) ? &st::taggedForumDialogRow : &st::taggedDialogRow; auto availableWidth = context.width diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index c39133e52a..bac34785a1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -320,7 +320,7 @@ Row::~Row() { void Row::recountHeight(float64 narrowRatio, FilterId filterId) { if (const auto history = _id.history()) { const auto hasTags = _id.entry()->hasChatsFilterTags(filterId); - _height = history->isForum() + _height = (history->isForum() || history->amMonoforumAdmin()) ? anim::interpolate( hasTags ? st::taggedForumDialogRow.height @@ -466,7 +466,7 @@ void Row::PaintCornerBadgeFrame( for (auto i = 0; i != storiesUnreadCount; ++i) { segments.push_back({ storiesUnreadBrush, storiesUnread }); } - if (peer && peer->forum()) { + if (peer && (peer->forum() || peer->monoforum())) { const auto radius = context.st->photoSize * Ui::ForumUserpicRadiusMultiplier(); Ui::PaintOutlineSegments(q, outline, radius, segments); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 628c0ae4d5..36b57fd5d8 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -67,7 +67,7 @@ const auto kPsaBadgePrefix = "cloud_lng_badge_psa_"; } else if (const auto user = history->peer->asUser()) { return !user->lastseen().isHidden(); } - return !history->isForum(); + return !history->isForum() && !history->amMonoforumAdmin(); } void PaintRowTopRight( @@ -1046,21 +1046,23 @@ void RowPainter::Paint( ? nullptr : thread ? &thread->lastItemDialogsView() - : sublist - ? &sublist->lastItemDialogsView() : nullptr; if (view) { - const auto forum = context.st->topicsHeight - ? row->history()->peer->forum() + const auto forum = (peer && context.st->topicsHeight) + ? peer->forum() : nullptr; - if (!view->prepared(item, forum)) { + const auto monoforum = (peer && context.st->topicsHeight) + ? peer->monoforum() + : nullptr; + if (!view->prepared(item, forum, monoforum)) { view->prepare( item, forum, + monoforum, [=] { entry->updateChatListEntry(); }, {}); } - if (forum) { + if (forum || monoforum) { rect.setHeight(context.st->topicsHeight + rect.height()); } view->paint(p, rect, context); @@ -1154,8 +1156,13 @@ void RowPainter::Paint( availableWidth, st::dialogsTextFont->height); auto &view = row->itemView(); - if (!view.prepared(item, nullptr)) { - view.prepare(item, nullptr, row->repaint(), previewOptions); + if (!view.prepared(item, nullptr, nullptr)) { + view.prepare( + item, + nullptr, + nullptr, + row->repaint(), + previewOptions); } view.paint(p, itemRect, context); }; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index b45708b844..87fd7232cb 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -138,26 +138,39 @@ bool MessageView::dependsOn(not_null item) const { bool MessageView::prepared( not_null item, - Data::Forum *forum) const { + Data::Forum *forum, + Data::SavedMessages *monoforum) const { return (_textCachedFor == item.get()) - && (!forum + && ((!forum && !monoforum) || (_topics && _topics->forum() == forum + && _topics->monoforum() == monoforum && _topics->prepared())); } void MessageView::prepare( not_null item, Data::Forum *forum, + Data::SavedMessages *monoforum, Fn customEmojiRepaint, ToPreviewOptions options) { - if (!forum) { + if (!forum && !monoforum) { _topics = nullptr; - } else if (!_topics || _topics->forum() != forum) { - _topics = std::make_unique(forum); - _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else if (!_topics + || _topics->forum() != forum + || _topics->monoforum() != monoforum) { + _topics = std::make_unique(forum, monoforum); + if (forum) { + _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else { + _topics->prepare(item->sublistPeerId(), customEmojiRepaint); + } } else if (!_topics->prepared()) { - _topics->prepare(item->topicRootId(), customEmojiRepaint); + if (forum) { + _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else { + _topics->prepare(item->sublistPeerId(), customEmojiRepaint); + } } if (_textCachedFor == item.get()) { return; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h index 4ee12aebd3..1cbe888a72 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h @@ -24,6 +24,7 @@ class SpoilerAnimation; namespace Data { class Forum; +class SavedMessages; } // namespace Data namespace HistoryView { @@ -56,10 +57,12 @@ public: [[nodiscard]] bool prepared( not_null item, - Data::Forum *forum) const; + Data::Forum *forum, + Data::SavedMessages *monoforum) const; void prepare( not_null item, Data::Forum *forum, + Data::SavedMessages *monoforum, Fn customEmojiRepaint, ToPreviewOptions options); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index 82fc64a7a9..ba42d90cc0 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -8,10 +8,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_topics_view.h" #include "dialogs/ui/dialogs_layout.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_peer.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" #include "core/ui_integration.h" #include "lang/lang_keys.h" +#include "main/main_session.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/text/text_options.h" @@ -26,29 +32,35 @@ constexpr auto kIconLoopCount = 1; } // namespace -TopicsView::TopicsView(not_null forum) -: _forum(forum) { +TopicsView::TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum) +: _forum(forum) +, _monoforum(monoforum) { } TopicsView::~TopicsView() = default; bool TopicsView::prepared() const { - return (_version == _forum->recentTopicsListVersion()); + const auto version = _forum + ? _forum->recentTopicsListVersion() + : _monoforum->recentSublistsListVersion(); + return (_version == version); } void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { + Expects(_forum != nullptr); + const auto &list = _forum->recentTopics(); _version = _forum->recentTopicsListVersion(); _titles.reserve(list.size()); auto index = 0; for (const auto &topic : list) { const auto from = begin(_titles) + index; - const auto rootId = topic->rootId(); + const auto key = topic->rootId().bare; const auto i = ranges::find( from, end(_titles), - rootId, - &Title::topicRootId); + key, + &Title::key); if (i != end(_titles)) { if (i != from) { ranges::rotate(from, i, i + 1); @@ -58,7 +70,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { } auto &title = _titles[index++]; const auto unread = topic->chatListBadgesState().unread; - if (title.topicRootId == rootId + if (title.key == key && title.unread == unread && title.version == topic->titleVersion()) { continue; @@ -69,7 +81,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { .customEmojiLoopLimit = kIconLoopCount, }); auto topicTitle = topic->titleWithIcon(); - title.topicRootId = rootId; + title.key = key; title.version = topic->titleVersion(); title.unread = unread; title.title.setMarkedText( @@ -87,7 +99,79 @@ void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { _titles.pop_back(); } const auto i = frontRootId - ? ranges::find(_titles, frontRootId, &Title::topicRootId) + ? ranges::find(_titles, frontRootId.bare, &Title::key) + : end(_titles); + _jumpToTopic = (i != end(_titles)); + if (_jumpToTopic) { + if (i != begin(_titles)) { + ranges::rotate(begin(_titles), i, i + 1); + } + if (!_titles.front().unread) { + _jumpToTopic = false; + } + } +} + +void TopicsView::prepare(PeerId frontPeerId, Fn customEmojiRepaint) { + Expects(_monoforum != nullptr); + + const auto &list = _monoforum->recentSublists(); + const auto manager = &_monoforum->session().data().customEmojiManager(); + _version = _monoforum->recentSublistsListVersion(); + _titles.reserve(list.size()); + auto index = 0; + for (const auto &sublist : list) { + const auto from = begin(_titles) + index; + const auto peer = sublist->sublistPeer(); + const auto key = peer->id.value; + const auto i = ranges::find( + from, + end(_titles), + key, + &Title::key); + if (i != end(_titles)) { + if (i != from) { + ranges::rotate(from, i, i + 1); + } + } else if (index >= _titles.size()) { + _titles.emplace_back(); + } + auto &title = _titles[index++]; + const auto unread = sublist->chatListBadgesState().unread; + if (title.key == key + && title.unread == unread + && title.version == peer->nameVersion()) { + continue; + } + const auto context = Core::TextContext({ + .session = &sublist->session(), + .repaint = customEmojiRepaint, + .customEmojiLoopLimit = kIconLoopCount, + }); + auto topicTitle = TextWithEntities().append( + Ui::Text::SingleCustomEmoji( + manager->peerUserpicEmojiData(peer), + u"@"_q) + ).append(peer->shortName()); + title.key = key; + title.version = peer->nameVersion(); + title.unread = unread; + title.title.setMarkedText( + st::dialogsTextStyle, + (unread + ? Ui::Text::Colorized( + Ui::Text::Wrapped( + std::move(topicTitle), + EntityType::Bold)) + : std::move(topicTitle)), + DialogTextOptions(), + context); + } + while (_titles.size() > index) { + _titles.pop_back(); + } + const auto i = frontPeerId + ? ranges::find(_titles, frontPeerId.value, &Title::key) : end(_titles); _jumpToTopic = (i != end(_titles)); if (_jumpToTopic) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h index b90c3d5417..7c41337fad 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h @@ -16,6 +16,8 @@ struct DialogRow; namespace Data { class Forum; class ForumTopic; +class SavedMessages; +class SavedSublist; } // namespace Data namespace Ui { @@ -59,15 +61,19 @@ void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context); class TopicsView final { public: - explicit TopicsView(not_null forum); + TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum); ~TopicsView(); - [[nodiscard]] not_null forum() const { + [[nodiscard]] Data::Forum *forum() const { return _forum; } + [[nodiscard]] Data::SavedMessages *monoforum() const { + return _monoforum; + } [[nodiscard]] bool prepared() const; void prepare(MsgId frontRootId, Fn customEmojiRepaint); + void prepare(PeerId frontPeerId, Fn customEmojiRepaint); [[nodiscard]] int jumpToTopicWidth() const; @@ -99,7 +105,7 @@ public: private: struct Title { Text::String title; - MsgId topicRootId = 0; + uint64 key = 0; int version = -1; bool unread = false; }; @@ -107,7 +113,8 @@ private: [[nodiscard]] QImage topicJumpRippleMask( not_null topicJumpCache) const; - const not_null _forum; + Data::Forum * const _forum = nullptr; + Data::SavedMessages * const _monoforum = nullptr; mutable std::vector _titles; mutable std::unique_ptr<RippleAnimation> _ripple; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 9e433de7c2..51bdcc9719 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -3149,11 +3149,11 @@ void History::monoforumChanged(Data::SavedMessages *old) { } if (const auto monoforum = peer->monoforum()) { - _flags |= Flag::IsMonoforum; + _flags |= Flag::IsMonoforumAdmin; monoforum->chatsList()->unreadStateChanges( ) | rpl::filter([=] { - return (_flags & Flag::IsMonoforum) && inChatList(); + return (_flags & Flag::IsMonoforumAdmin) && inChatList(); }) | rpl::map( AdjustedForumUnreadState ) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { @@ -3165,7 +3165,7 @@ void History::monoforumChanged(Data::SavedMessages *old) { updateChatListEntry(); }, monoforum->lifetime()); } else { - _flags &= ~Flag::IsMonoforum; + _flags &= ~Flag::IsMonoforumAdmin; } if (cloudDraft(MsgId(0))) { updateChatListSortPosition(); @@ -3173,8 +3173,8 @@ void History::monoforumChanged(Data::SavedMessages *old) { _flags |= Flag::PendingAllItemsResize; } -bool History::isMonoforum() const { - return (_flags & Flag::IsMonoforum); +bool History::amMonoforumAdmin() const { + return (_flags & Flag::IsMonoforumAdmin); } not_null<History*> History::migrateToOrMe() const { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index f8e0791d74..16fe57e351 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -74,7 +74,7 @@ public: [[nodiscard]] bool isForum() const; void monoforumChanged(Data::SavedMessages *old); - [[nodiscard]] bool isMonoforum() const; + [[nodiscard]] bool amMonoforumAdmin() const; [[nodiscard]] not_null<History*> migrateToOrMe() const; [[nodiscard]] History *migrateFrom() const; @@ -435,7 +435,7 @@ private: PendingAllItemsResize = (1 << 1), IsTopPromoted = (1 << 2), IsForum = (1 << 3), - IsMonoforum = (1 << 4), + IsMonoforumAdmin = (1 << 4), FakeUnreadWhileOpened = (1 << 5), HasPinnedMessages = (1 << 6), ResolveChatListMessage = (1 << 7), diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index b7f9305f33..42f9127032 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3443,12 +3443,12 @@ FullStoryId HistoryItem::replyToStory() const { } FullReplyTo HistoryItem::replyTo() const { - const auto monoforumPeer = _history->peer->amMonoforumAdmin() - ? savedSublistPeer() - : nullptr; + const auto monoforumPeerId = _history->peer->amMonoforumAdmin() + ? sublistPeerId() + : PeerId(); auto result = FullReplyTo{ .topicRootId = topicRootId(), - .monoforumPeerId = monoforumPeer ? monoforumPeer->id : PeerId(), + .monoforumPeerId = monoforumPeerId, }; if (const auto reply = Get<HistoryMessageReply>()) { const auto &fields = reply->fields(); @@ -3592,11 +3592,15 @@ Data::SavedSublist *HistoryItem::savedSublist() const { return nullptr; } -PeerData *HistoryItem::savedSublistPeer() const { - if (const auto sublist = savedSublist()) { - return sublist->sublistPeer(); +PeerId HistoryItem::sublistPeerId() const { + if (const auto saved = Get<HistoryMessageSaved>()) { + return saved->sublistPeerId; + } else if (_history->peer->isSelf()) { + return _history->peer->id; + } else if (_history->peer->monoforum()) { + return _from->id; } - return nullptr; + return PeerId(); } PeerData *HistoryItem::savedFromSender() const { @@ -4046,8 +4050,8 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { ? replyTo.messageId.peer : PeerId(); const auto to = LookupReplyTo(_history, replyTo.messageId); - config.reply.monoforumPeerId = (to && to->savedSublistPeer()) - ? to->savedSublistPeer()->id + config.reply.monoforumPeerId = (to && to->sublistPeerId()) + ? to->sublistPeerId() : replyTo.monoforumPeerId ? replyTo.monoforumPeerId : PeerId(); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index fcfd616382..6a64ccfe4c 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -491,7 +491,7 @@ public: [[nodiscard]] MsgId originalId() const; [[nodiscard]] Data::SavedSublist *savedSublist() const; - [[nodiscard]] PeerData *savedSublistPeer() const; + [[nodiscard]] PeerId sublistPeerId() const; [[nodiscard]] PeerData *savedFromSender() const; [[nodiscard]] const HiddenSenderInfo *savedFromHiddenSenderInfo() const; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 7bada5f52e..d789f7f07b 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1699,10 +1699,10 @@ FullReplyTo ChatWidget::replyTo() const { const auto item = custom.messageId ? session().data().message(custom.messageId) : nullptr; - const auto sublistPeer = item ? item->savedSublistPeer() : nullptr; + const auto sublistPeerId = item ? item->sublistPeerId() : PeerId(); if (!item || !monoforumPeerId - || (sublistPeer && sublistPeer->id == monoforumPeerId)) { + || (sublistPeerId == monoforumPeerId)) { // Never answer to a message in a wrong monoforum peer id. custom.topicRootId = _repliesRootId; custom.monoforumPeerId = monoforumPeerId; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index eec58baf5c..b89b170eb6 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1487,7 +1487,7 @@ void Element::recountMonoforumSenderBarInBlocks() { } } } - return sublistPeer; + return (sublistPeer == parentChat) ? nullptr : sublistPeer.get(); }(); if (barPeer && !Has<MonoforumSenderBar>()) { AddComponents(MonoforumSenderBar::Bit()); diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index d0e2aca0e6..b93e55033e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -773,7 +773,7 @@ void TopBarWidget::backClicked() { _controller->closeForum(); } else if (_activeChat.section == Section::ChatsList && _activeChat.key.history() - && _activeChat.key.history()->isMonoforum()) { + && _activeChat.key.history()->amMonoforumAdmin()) { _controller->closeMonoforum(); } else { _controller->showBackFromStack(); @@ -1236,7 +1236,8 @@ void TopBarWidget::updateMembersShowArea() { } else if (const auto chat = peer->asChat()) { return chat->amIn(); } else if (const auto megagroup = peer->asMegagroup()) { - return megagroup->canViewMembers() + return !megagroup->isMonoforum() + && megagroup->canViewMembers() && (megagroup->membersCount() < megagroup->session().serverConfig().chatSizeMax); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index bc7de9ef5c..dd760a3e07 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1777,9 +1777,14 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupPersonalChannel( style::al_left); return; } - if (!state->view.prepared(item, nullptr)) { + if (!state->view.prepared(item, nullptr, nullptr)) { const auto repaint = [=] { preview->update(); }; - state->view.prepare(item, nullptr, repaint, {}); + state->view.prepare( + item, + nullptr, + nullptr, + repaint, + {}); } state->view.paint(p, preview->rect(), { .st = &st::defaultDialogRow, From 2b24fe95c2bfe299f2550bad5501c9866623226d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 16 May 2025 17:11:22 +0400 Subject: [PATCH 065/310] Update API scheme on layer 204. --- Telegram/SourceFiles/data/data_histories.cpp | 1 + Telegram/SourceFiles/data/data_saved_messages.cpp | 8 +++++--- Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp | 4 ++-- Telegram/SourceFiles/mtproto/scheme/api.tl | 12 ++++++------ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index f13dccc97a..44dd7b86f7 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -495,6 +495,7 @@ void Histories::changeDialogUnreadMark( using Flag = MTPmessages_MarkDialogUnread::Flag; session().api().request(MTPmessages_MarkDialogUnread( MTP_flags(unread ? Flag::f_unread : Flag(0)), + MTPInputPeer(), // parent_peer MTP_inputDialogPeer(history->peer->input) )).send(); } diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 82fc03c3a5..3645afe9e5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -264,12 +264,14 @@ void SavedMessages::apply( auto offsetDate = TimeId(); auto offsetId = MsgId(); auto offsetPeer = (PeerData*)nullptr; - const auto selfId = _owner->session().userPeerId(); + const auto parentPeerId = _parentChat + ? _parentChat->id + : _owner->session().userPeerId(); for (const auto &dialog : *list) { dialog.match([&](const MTPDsavedDialog &data) { const auto peer = _owner->peer(peerFromMTP(data.vpeer())); const auto topId = MsgId(data.vtop_message().v); - if (const auto item = _owner->message(selfId, topId)) { + if (const auto item = _owner->message(parentPeerId, topId)) { offsetPeer = peer; offsetDate = item->date(); offsetId = topId; @@ -284,7 +286,7 @@ void SavedMessages::apply( }, [&](const MTPDmonoForumDialog &data) { const auto peer = _owner->peer(peerFromMTP(data.vpeer())); const auto topId = MsgId(data.vtop_message().v); - if (const auto item = _owner->message(selfId, topId)) { + if (const auto item = _owner->message(parentPeerId, topId)) { offsetPeer = peer; offsetDate = item->date(); offsetId = topId; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 36b57fd5d8..1cd8a1e353 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -923,12 +923,12 @@ const style::icon *ChatTypeIcon( st::dialogsChannelIcon, context.active, context.selected); - } else if (peer->isForum()) { + } else if (peer->isForum() || peer->amMonoforumAdmin()) { return &ThreeStateIcon( st::dialogsForumIcon, context.active, context.selected); - } else { + } else if (!peer->isMonoforum()) { return &ThreeStateIcon( st::dialogsChatIcon, context.active, diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 48cffcfef6..0cb4504194 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -332,7 +332,7 @@ updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update; -updateDraftMessage#1b49ec6d flags:# peer:Peer top_msg_id:flags.0?int draft:DraftMessage = Update; +updateDraftMessage#edfc111e flags:# peer:Peer top_msg_id:flags.0?int saved_peer_id:flags.1?Peer draft:DraftMessage = Update; updateReadFeaturedStickers#571d2742 = Update; updateRecentStickers#9a422c20 = Update; updateConfig#a229dd06 = Update; @@ -351,7 +351,7 @@ updateFavedStickers#e511996d = Update; updateChannelReadMessagesContents#ea29055d flags:# channel_id:long top_msg_id:flags.0?int messages:Vector<int> = Update; updateContactsReset#7084a7be = Update; updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; -updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; +updateDialogUnreadMark#b658f23e flags:# unread:flags.0?true peer:DialogPeer saved_peer_id:flags.1?Peer = Update; updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; updateFolderPeers#19360dc0 folder_peers:Vector<FolderPeer> pts:int pts_count:int = Update; @@ -1715,7 +1715,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; -monoForumDialog#31ac5089 peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int = SavedDialog; +monoForumDialog#7d25fd43 flags:# unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int draft:flags.1?DraftMessage = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; @@ -2263,8 +2263,8 @@ messages.sendMultiMedia#1bf89d74 flags:# silent:flags.5?true background:flags.6? messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector<MessageRange>; -messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; -messages.getDialogUnreadMarks#22e24e22 = Vector<DialogPeer>; +messages.markDialogUnread#8c5006f8 flags:# unread:flags.0?true parent_peer:flags.1?InputPeer peer:InputDialogPeer = Bool; +messages.getDialogUnreadMarks#21202222 flags:# parent_peer:flags.0?InputPeer = Vector<DialogPeer>; messages.clearAllDrafts#7e58ee9c = Bool; messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector<bytes> = Updates; @@ -2395,7 +2395,7 @@ messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool; -messages.readSavedHistory#baab7bd6 parent_peer:InputPeer peer:InputPeer max_id:int = messages.Messages; +messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; From 4bc5e81513669d7c9a4d627a920cf5a4ffb8fb32 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 19 May 2025 10:30:16 +0400 Subject: [PATCH 066/310] Update API scheme on layer 204. --- Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp | 3 ++- Telegram/SourceFiles/data/data_channel.h | 1 + Telegram/SourceFiles/data/data_session.cpp | 9 ++++++--- Telegram/SourceFiles/mtproto/scheme/api.tl | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 4028efa07a..7cfffe5c15 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -2607,7 +2607,8 @@ void Controller::saveForum() { } _api.request(MTPchannels_ToggleForum( channel->inputChannel, - MTP_bool(*_savingData.forum) + MTP_bool(*_savingData.forum), + MTP_bool(channel->flags() & ChannelDataFlag::ForumTabs) )).done([=](const MTPUpdates &result) { const auto weak = base::make_weak(this); channel->session().api().applyUpdates(result); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 5b8c637120..21cbaafd0d 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -81,6 +81,7 @@ enum class ChannelDataFlag : uint64 { AutoTranslation = (1ULL << 38), Monoforum = (1ULL << 39), MonoforumAdmin = (1ULL << 40), + ForumTabs = (1ULL << 41), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags<ChannelDataFlag>; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 89074c285e..82bda952cd 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -960,7 +960,9 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) { | Flag::CallActive | Flag::CallNotEmpty | Flag::Forbidden - | (!minimal ? (Flag::Left | Flag::Creator) : Flag()) + | (!minimal + ? (Flag::Left | Flag::Creator | Flag::ForumTabs) + : Flag()) | Flag::NoForwards | Flag::JoinToWrite | Flag::RequestToJoin @@ -995,8 +997,9 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) { ? Flag::CallNotEmpty : Flag()) | (!minimal - ? (data.is_left() ? Flag::Left : Flag()) - | (data.is_creator() ? Flag::Creator : Flag()) + ? ((data.is_left() ? Flag::Left : Flag()) + | (data.is_creator() ? Flag::Creator : Flag()) + | (data.is_forum_tabs() ? Flag::ForumTabs : Flag())) : Flag()) | (data.is_noforwards() ? Flag::NoForwards : Flag()) | (data.is_join_to_send() ? Flag::JoinToWrite : Flag()) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 0cb4504194..4e8faa448e 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -99,7 +99,7 @@ userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long 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#6592a1a7 id:long title:string = Chat; -channel#fe685355 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 fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector<Username> stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; +channel#fe685355 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 fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector<RestrictionReason> admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector<Username> stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; @@ -2484,7 +2484,7 @@ channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool; channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool; channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool; -channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates; +channels.toggleForum#3ff75734 channel:InputChannel enabled:Bool tabs:Bool = Updates; channels.createForumTopic#f40c0224 flags:# channel:InputChannel title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates; channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; channels.getForumTopicsByID#b0831eb9 channel:InputChannel topics:Vector<int> = messages.ForumTopics; From b2c01991a66887b18b26f4776e7bf292092c8d35 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 19 May 2025 14:59:57 +0400 Subject: [PATCH 067/310] Support unread state in sublists. --- Telegram/SourceFiles/api/api_updates.cpp | 26 + Telegram/SourceFiles/apiwrap.cpp | 10 + .../SourceFiles/data/data_forum_topic.cpp | 4 +- Telegram/SourceFiles/data/data_replies_list.h | 4 +- .../SourceFiles/data/data_saved_messages.cpp | 119 +- .../SourceFiles/data/data_saved_messages.h | 9 +- .../SourceFiles/data/data_saved_sublist.cpp | 1154 ++++++++++++++--- .../SourceFiles/data/data_saved_sublist.h | 126 +- Telegram/SourceFiles/data/data_session.cpp | 27 + Telegram/SourceFiles/data/data_session.h | 12 + Telegram/SourceFiles/history/history.cpp | 6 + Telegram/SourceFiles/history/history_item.cpp | 10 - .../view/history_view_chat_section.cpp | 111 +- .../history/view/history_view_chat_section.h | 1 + .../info/profile/info_profile_values.cpp | 4 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 3 +- 16 files changed, 1287 insertions(+), 339 deletions(-) diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index cade72e1b9..8efbfd32a1 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2442,6 +2442,32 @@ void Updates::feedUpdate(const MTPUpdate &update) { session().data().updateRepliesReadTill({ id, readTillId, true }); } break; + case mtpc_updateReadMonoForumInbox: { + const auto &d = update.c_updateReadMonoForumInbox(); + const auto parentChatId = ChannelId(d.vchannel_id()); + const auto sublistPeerId = peerFromMTP(d.vsaved_peer_id()); + const auto readTillId = d.vread_max_id().v; + session().data().updateSublistReadTill({ + parentChatId, + sublistPeerId, + readTillId, + false, + }); + } break; + + case mtpc_updateReadMonoForumOutbox: { + const auto &d = update.c_updateReadMonoForumOutbox(); + const auto parentChatId = ChannelId(d.vchannel_id()); + const auto sublistPeerId = peerFromMTP(d.vsaved_peer_id()); + const auto readTillId = d.vread_max_id().v; + session().data().updateSublistReadTill({ + parentChatId, + sublistPeerId, + readTillId, + true, + }); + } break; + case mtpc_updateChannelAvailableMessages: { auto &d = update.c_updateChannelAvailableMessages(); if (const auto channel = session().data().channelLoaded(d.vchannel_id())) { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 90d013cb60..352319e89e 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3197,11 +3197,21 @@ void ApiWrap::sendAction(const SendAction &action) { && !action.options.shortcutId && !action.replaceMediaOf) { const auto topicRootId = action.replyTo.topicRootId; + const auto monoforumPeerId = action.replyTo.monoforumPeerId; const auto topic = topicRootId ? action.history->peer->forumTopicFor(topicRootId) : nullptr; + const auto monoforum = monoforumPeerId + ? action.history->peer->monoforum() + : nullptr; + const auto sublist = monoforum + ? monoforum->sublistLoaded( + action.history->owner().peer(monoforumPeerId)) + : nullptr; if (topic) { topic->readTillEnd(); + } else if (sublist) { + sublist->readTillEnd(); } else { _session->data().histories().readInbox(action.history); } diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 6e03125eb9..e3bf4757cb 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -362,8 +362,8 @@ void ForumTopic::subscribeToUnreadChanges() { ) | rpl::filter([=] { return inChatList(); }) | rpl::start_with_next([=]( - std::optional<int> previous, - std::optional<int> now) { + std::optional<int> previous, + std::optional<int> now) { if (previous.value_or(0) != now.value_or(0)) { _forum->recentTopicsInvalidate(this); } diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h index 42f56c1aec..f5d32ddcfb 100644 --- a/Telegram/SourceFiles/data/data_replies_list.h +++ b/Telegram/SourceFiles/data/data_replies_list.h @@ -58,8 +58,6 @@ public: [[nodiscard]] bool isServerSideUnread( not_null<const HistoryItem*> item) const; - [[nodiscard]] std::optional<int> computeUnreadCountLocally( - MsgId afterId) const; void requestUnreadCount(); void readTill(not_null<HistoryItem*> item); @@ -79,6 +77,8 @@ private: void subscribeToUpdates(); void appendClientSideMessages(MessagesSlice &slice); + [[nodiscard]] std::optional<int> computeUnreadCountLocally( + MsgId afterId) const; [[nodiscard]] bool buildFromData(not_null<Viewer*> viewer); [[nodiscard]] bool applyItemDestroyed( diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 3645afe9e5..3bcc65ad4e 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_channel.h" -#include "data/data_peer.h" +#include "data/data_user.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "history/history.h" @@ -34,13 +34,15 @@ SavedMessages::SavedMessages( ChannelData *parentChat) : _owner(owner) , _parentChat(parentChat) -, _parentHistory(parentChat ? owner->history(parentChat).get() : nullptr) +, _owningHistory(parentChat ? owner->history(parentChat).get() : nullptr) , _chatsList( &_owner->session(), FilterId(), _owner->maxPinnedChatsLimitValue(this)) , _loadMore([=] { sendLoadMoreRequests(); }) { - if (_parentHistory && _parentHistory->inChatList()) { + // We don't assign _owningHistory for my Saved Messages here, + // because the data structures are not ready yet. + if (_owningHistory && _owningHistory->inChatList()) { preloadSublists(); } } @@ -51,10 +53,22 @@ bool SavedMessages::supported() const { return !_unsupported; } +void SavedMessages::markUnsupported() { + _unsupported = true; +} + ChannelData *SavedMessages::parentChat() const { return _parentChat; } +not_null<History*> SavedMessages::owningHistory() const { + if (!_owningHistory) { + const_cast<SavedMessages*>(this)->_owningHistory + = _owner->history(_owner->session().user()); + } + return _owningHistory; +} + Session &SavedMessages::owner() const { return *_owner; } @@ -101,11 +115,6 @@ void SavedMessages::loadMore() { _loadMore.call(); } -void SavedMessages::loadMore(not_null<SavedSublist*> sublist) { - _loadMoreSublistsScheduled.emplace(sublist); - _loadMore.call(); -} - void SavedMessages::sendLoadMore() { if (_loadMoreRequestId || _chatsList.loaded()) { return; @@ -132,7 +141,7 @@ void SavedMessages::sendLoadMore() { reorderLastSublists(); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; + markUnsupported(); } _chatsList.setLoaded(); _loadMoreRequestId = 0; @@ -150,7 +159,7 @@ void SavedMessages::loadPinned() { _chatsListChanges.fire({}); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; + markUnsupported(); } else { _pinnedLoaded = true; } @@ -158,82 +167,6 @@ void SavedMessages::loadPinned() { }).send(); } -void SavedMessages::sendLoadMore(not_null<SavedSublist*> sublist) { - if (_loadMoreRequests.contains(sublist) || sublist->isFullLoaded()) { - return; - } - const auto &list = sublist->messages(); - const auto offsetId = list.empty() ? MsgId(0) : list.back()->id; - const auto offsetDate = list.empty() ? MsgId(0) : list.back()->date(); - const auto limit = offsetId ? kPerPage : kFirstPerPage; - using Flag = MTPmessages_GetSavedHistory::Flag; - const auto requestId = _owner->session().api().request( - MTPmessages_GetSavedHistory( - MTP_flags(_parentChat ? Flag::f_parent_peer : Flag(0)), - _parentChat ? _parentChat->input : MTPInputPeer(), - sublist->sublistPeer()->input, - MTP_int(offsetId), - MTP_int(offsetDate), - MTP_int(0), // add_offset - MTP_int(limit), - MTP_int(0), // max_id - MTP_int(0), // min_id - MTP_long(0)) // hash - ).done([=](const MTPmessages_Messages &result) { - auto count = 0; - auto list = (const QVector<MTPMessage>*)nullptr; - result.match([&](const MTPDmessages_channelMessages &data) { - if (const auto channel = _parentChat) { - channel->ptsReceived(data.vpts().v); - channel->processTopics(data.vtopics()); - list = &data.vmessages().v; - count = data.vcount().v; - } else { - LOG(("API Error: messages.channelMessages in sublist.")); - } - }, [](const MTPDmessages_messagesNotModified &) { - LOG(("API Error: messages.messagesNotModified in sublist.")); - }, [&](const auto &data) { - owner().processUsers(data.vusers()); - owner().processChats(data.vchats()); - list = &data.vmessages().v; - if constexpr (MTPDmessages_messages::Is<decltype(data)>()) { - count = int(list->size()); - } else { - count = data.vcount().v; - } - }); - - _loadMoreRequests.remove(sublist); - if (!list) { - sublist->setFullLoaded(); - return; - } - auto items = std::vector<not_null<HistoryItem*>>(); - items.reserve(list->size()); - for (const auto &message : *list) { - const auto item = owner().addNewMessage( - message, - {}, - NewMessageType::Existing); - if (item) { - items.push_back(item); - } - } - sublist->append(std::move(items), count); - if (result.type() == mtpc_messages_messages) { - sublist->setFullLoaded(); - } - }).fail([=](const MTP::Error &error) { - if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { - _unsupported = true; - } - sublist->setFullLoaded(); - _loadMoreRequests.remove(sublist); - }).send(); - _loadMoreRequests[sublist] = requestId; -} - void SavedMessages::apply( const MTPmessages_SavedDialogs &result, bool pinned) { @@ -291,8 +224,7 @@ void SavedMessages::apply( offsetDate = item->date(); offsetId = topId; lastValid = true; - const auto entry = sublist(peer); - entry->applyMaybeLast(item); + sublist(peer)->applyMonoforumDialog(data, item); } else { lastValid = false; } @@ -321,9 +253,6 @@ void SavedMessages::sendLoadMoreRequests() { if (_loadMoreScheduled) { sendLoadMore(); } - for (const auto sublist : base::take(_loadMoreSublistsScheduled)) { - sendLoadMore(sublist); - } } void SavedMessages::apply(const MTPDupdatePinnedSavedDialogs &update) { @@ -371,7 +300,7 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { } void SavedMessages::reorderLastSublists() { - if (!_parentHistory) { + if (!_parentChat) { return; } @@ -411,7 +340,7 @@ void SavedMessages::reorderLastSublists() { } } ++_lastSublistsVersion; - _parentHistory->updateChatListEntry(); + owningHistory()->updateChatListEntry(); } void SavedMessages::listMessageChanged(HistoryItem *from, HistoryItem *to) { @@ -426,11 +355,11 @@ int SavedMessages::recentSublistsListVersion() const { void SavedMessages::recentSublistsInvalidate( not_null<SavedSublist*> sublist) { - Expects(_parentHistory != nullptr); + Expects(_parentChat != nullptr); if (ranges::contains(_lastSublists, sublist)) { ++_lastSublistsVersion; - _parentHistory->updateChatListEntry(); + owningHistory()->updateChatListEntry(); } } diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index dd03e2d2a8..206b905f21 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -26,7 +26,10 @@ public: ~SavedMessages(); [[nodiscard]] bool supported() const; + void markUnsupported(); + [[nodiscard]] ChannelData *parentChat() const; + [[nodiscard]] not_null<History*> owningHistory() const; [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; @@ -44,7 +47,6 @@ public: void preloadSublists(); void loadMore(); - void loadMore(not_null<SavedSublist*> sublist); void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); @@ -64,12 +66,11 @@ private: void reorderLastSublists(); void sendLoadMore(); - void sendLoadMore(not_null<SavedSublist*> sublist); void sendLoadMoreRequests(); const not_null<Session*> _owner; ChannelData *_parentChat = nullptr; - History *_parentHistory = nullptr; + History *_owningHistory = nullptr; rpl::event_stream<not_null<SavedSublist*>> _sublistDestroyed; @@ -78,7 +79,6 @@ private: not_null<PeerData*>, std::unique_ptr<SavedSublist>> _sublists; - base::flat_map<not_null<SavedSublist*>, mtpRequestId> _loadMoreRequests; mtpRequestId _loadMoreRequestId = 0; mtpRequestId _pinnedRequestId = 0; @@ -87,7 +87,6 @@ private: PeerData *_offsetPeer = nullptr; SingleQueuedInvokation _loadMore; - base::flat_set<not_null<SavedSublist*>> _loadMoreSublistsScheduled; bool _loadMoreScheduled = false; std::vector<not_null<SavedSublist*>> _lastSublists; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 367570cc0e..64a02b2d9d 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_saved_sublist.h" -#include "data/data_histories.h" +#include "apiwrap.h" +#include "data/data_changes.h" #include "data/data_channel.h" +#include "data/data_histories.h" +#include "data/data_messages.h" #include "data/data_peer.h" -#include "data/data_user.h" #include "data/data_saved_messages.h" #include "data/data_session.h" +#include "data/data_user.h" #include "history/view/history_view_item_preview.h" #include "history/history.h" #include "history/history_item.h" @@ -20,26 +23,143 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" namespace Data { +namespace { + +constexpr auto kMessagesPerPage = 50; +constexpr auto kReadRequestTimeout = 3 * crl::time(1000); + +} // namespace + +struct SavedSublist::Viewer { + MessagesSlice slice; + MsgId around = 0; + int limitBefore = 0; + int limitAfter = 0; + base::has_weak_ptr guard; + bool scheduled = false; +}; SavedSublist::SavedSublist( not_null<SavedMessages*> parent, - not_null<PeerData*> peer) -: Thread(&peer->owner(), Dialogs::Entry::Type::SavedSublist) + not_null<PeerData*> sublistPeer) +: Thread(&sublistPeer->owner(), Dialogs::Entry::Type::SavedSublist) , _parent(parent) -, _history(peer->owner().history(peer)) { +, _sublistHistory(sublistPeer->owner().history(sublistPeer)) +, _readRequestTimer([=] { sendReadTillRequest(); }) { + if (parent->parentChat()) { + _flags |= Flag::InMonoforum; + } + subscribeToUnreadChanges(); } -SavedSublist::~SavedSublist() = default; +SavedSublist::~SavedSublist() { + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + if (_readRequestTimer.isActive()) { + sendReadTillRequest(); + } + // session().api().unreadThings().cancelRequests(this); +} + +bool SavedSublist::inMonoforum() const { + return (_flags & Flag::InMonoforum) != 0; +} + +void SavedSublist::apply(const SublistReadTillUpdate &update) { + if (update.out) { + setOutboxReadTill(update.readTillId); + } else if (update.readTillId >= _inboxReadTillId) { + setInboxReadTill( + update.readTillId, + computeUnreadCountLocally(update.readTillId)); + } +} + +void SavedSublist::apply(const MessageUpdate &update) { + if (applyUpdate(update)) { + _instantChanges.fire({}); + } +} + +void SavedSublist::applyDifferenceTooLong() { + if (_skippedAfter.has_value()) { + _skippedAfter = std::nullopt; + _listChanges.fire({}); + } +} + +bool SavedSublist::removeOne(not_null<HistoryItem*> item) { + const auto id = item->id; + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + changeUnreadCountByMessage(id, -1); + if (i == end(_list) || *i != id) { + return false; + } + _list.erase(i); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + if (*known > 0) { + _fullCount = (*known - 1); + } + } + return true; +} + +rpl::producer<MessagesSlice> SavedSublist::source( + MessagePosition aroundId, + int limitBefore, + int limitAfter) { + const auto around = aroundId.fullId.msg; + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto viewer = lifetime.make_state<Viewer>(); + const auto push = [=] { + if (viewer->scheduled) { + viewer->scheduled = false; + if (buildFromData(viewer)) { + appendClientSideMessages(viewer->slice); + consumer.put_next_copy(viewer->slice); + } + } + }; + const auto pushInstant = [=] { + viewer->scheduled = true; + push(); + }; + const auto pushDelayed = [=] { + if (!viewer->scheduled) { + viewer->scheduled = true; + crl::on_main(&viewer->guard, push); + } + }; + viewer->around = around; + viewer->limitBefore = limitBefore; + viewer->limitAfter = limitAfter; + + const auto history = owningHistory(); + history->session().changes().historyUpdates( + history, + HistoryUpdate::Flag::ClientSideMessages + ) | rpl::start_with_next(pushDelayed, lifetime); + + _listChanges.events( + ) | rpl::start_with_next(pushDelayed, lifetime); + + _instantChanges.events( + ) | rpl::start_with_next(pushInstant, lifetime); + + pushInstant(); + return lifetime; + }; +} not_null<SavedMessages*> SavedSublist::parent() const { return _parent; } not_null<History*> SavedSublist::owningHistory() { - const auto chat = parentChat(); - return _history->owner().history(chat - ? (PeerData*)chat - : _history->session().user().get()); + return _parent->owningHistory(); } ChannelData *SavedSublist::parentChat() const { @@ -47,17 +167,13 @@ ChannelData *SavedSublist::parentChat() const { } not_null<PeerData*> SavedSublist::sublistPeer() const { - return _history->peer; + return _sublistHistory->peer; } bool SavedSublist::isHiddenAuthor() const { return sublistPeer()->isSavedHiddenAuthor(); } -bool SavedSublist::isFullLoaded() const { - return (_flags & Flag::FullLoaded) != 0; -} - rpl::producer<> SavedSublist::destroyed() const { using namespace rpl::mappers; return rpl::merge( @@ -67,133 +183,611 @@ rpl::producer<> SavedSublist::destroyed() const { ) | rpl::to_empty); } -auto SavedSublist::messages() const --> const std::vector<not_null<HistoryItem*>> & { - return _items; +void SavedSublist::applyMaybeLast(not_null<HistoryItem*> item, bool added) { + growLastKnownServerMessageId(item->id); + if (!_lastServerMessage || (*_lastServerMessage)->id < item->id) { + setLastServerMessage(item); + resolveChatListMessageGroup(); + } } -void SavedSublist::applyMaybeLast(not_null<HistoryItem*> item, bool added) { - const auto before = []( - not_null<HistoryItem*> a, - not_null<HistoryItem*> b) { - return IsServerMsgId(a->id) - ? (IsServerMsgId(b->id) ? (a->id < b->id) : true) - : (IsServerMsgId(b->id) ? false : (a->id < b->id)); - }; +void SavedSublist::applyItemAdded(not_null<HistoryItem*> item) { + if (item->isRegular()) { + setLastServerMessage(item); + } else { + setLastMessage(item); + } +} - const auto was = _items.empty() ? nullptr : _items.front().get(); - if (_items.empty()) { - _items.push_back(item); - } else if (_items.front() == item) { - return; - } else if (!isFullLoaded() - && _items.size() == 1 - && before(_items.front(), item)) { - _items[0] = item; - } else if (before(_items.back(), item)) { - for (auto i = begin(_items); i != end(_items); ++i) { - if (item == *i) { - return; - } else if (before(*i, item)) { - _items.insert(i, item); - break; - } +void SavedSublist::applyItemRemoved(MsgId id) { + if (const auto lastItem = lastMessage()) { + if (lastItem->id == id) { + _lastMessage = std::nullopt; } } - if (added && _fullCount) { - ++*_fullCount; + if (const auto lastServerItem = lastServerMessage()) { + if (lastServerItem->id == id) { + _lastServerMessage = std::nullopt; + } } - if (_items.front() == item) { - setChatListTimeId(item->date()); - resolveChatListMessageGroup(); - - _parent->listMessageChanged(was, item.get()); + if (const auto chatListItem = _chatListMessage.value_or(nullptr)) { + if (chatListItem->id == id) { + _chatListMessage = std::nullopt; + requestChatListMessage(); + } } - _changed.fire({}); } -void SavedSublist::removeOne(not_null<HistoryItem*> item) { - if (_items.empty()) { - return; +void SavedSublist::requestChatListMessage() { + if (!chatListMessageKnown()) { + //forum()->requestTopic(_rootId); // #TODO monoforum } - const auto last = (_items.front() == item); - const auto from = ranges::remove(_items, item); - const auto removed = end(_items) - from; - if (removed) { - _items.erase(from, end(_items)); +} + +void SavedSublist::readTillEnd() { + readTill(_lastKnownServerMessageId); +} + +bool SavedSublist::buildFromData(not_null<Viewer*> viewer) { + if (_list.empty() && _skippedBefore == 0 && _skippedAfter == 0) { + viewer->slice.ids.clear(); + viewer->slice.nearestToAround = FullMsgId(); + viewer->slice.fullCount + = viewer->slice.skippedBefore + = viewer->slice.skippedAfter + = 0; + ranges::reverse(viewer->slice.ids); + return true; } - if (_fullCount) { - --*_fullCount; + const auto around = (viewer->around != ShowAtUnreadMsgId) + ? viewer->around + : computeInboxReadTillFull(); + if (_list.empty() + || (!around && _skippedAfter != 0) + || (around > _list.front() && _skippedAfter != 0) + || (around > 0 && around < _list.back() && _skippedBefore != 0)) { + loadAround(around); + return false; } - if (last) { - if (_items.empty()) { - if (isFullLoaded()) { - updateChatListExistence(); + const auto i = around + ? ranges::lower_bound(_list, around, std::greater<>()) + : end(_list); + const auto availableBefore = int(end(_list) - i); + const auto availableAfter = int(i - begin(_list)); + const auto useBefore = std::min(availableBefore, viewer->limitBefore + 1); + const auto useAfter = std::min(availableAfter, viewer->limitAfter); + const auto slice = &viewer->slice; + if (_skippedBefore.has_value()) { + slice->skippedBefore + = (*_skippedBefore + (availableBefore - useBefore)); + } + if (_skippedAfter.has_value()) { + slice->skippedAfter + = (*_skippedAfter + (availableAfter - useAfter)); + } + + const auto peerId = owningHistory()->peer->id; + slice->ids.clear(); + auto nearestToAround = std::optional<MsgId>(); + slice->ids.reserve(useAfter + useBefore); + for (auto j = i - useAfter, e = i + useBefore; j != e; ++j) { + const auto id = *j; + if (!nearestToAround && id < around) { + nearestToAround = (j == i - useAfter) + ? id + : *(j - 1); + } + slice->ids.emplace_back(peerId, id); + } + slice->nearestToAround = FullMsgId( + peerId, + nearestToAround.value_or( + slice->ids.empty() ? 0 : slice->ids.back().msg)); + slice->fullCount = _fullCount.current(); + + ranges::reverse(viewer->slice.ids); + + if (_skippedBefore != 0 && useBefore < viewer->limitBefore + 1) { + loadBefore(); + } + if (_skippedAfter != 0 && useAfter < viewer->limitAfter) { + loadAfter(); + } + + return true; +} + +bool SavedSublist::applyUpdate(const MessageUpdate &update) { + using Flag = MessageUpdate::Flag; + + if (update.item->history() != owningHistory() + || !update.item->isRegular() + || update.item->sublistPeerId() != sublistPeer()->id) { + return false; + } else if (update.flags & Flag::Destroyed) { + return removeOne(update.item); + } + const auto id = update.item->id; + if (update.flags & Flag::NewAdded) { + changeUnreadCountByMessage(id, 1); + } + const auto i = ranges::lower_bound(_list, id, std::greater<>()); + if (_skippedAfter != 0 + || (i != end(_list) && *i == id)) { + return false; + } + _list.insert(i, id); + if (_skippedBefore && _skippedAfter) { + _fullCount = *_skippedBefore + _list.size() + *_skippedAfter; + } else if (const auto known = _fullCount.current()) { + _fullCount = *known + 1; + } + return true; +} + +bool SavedSublist::processMessagesIsEmpty( + const MTPmessages_Messages &result) { + const auto guard = gsl::finally([&] { _listChanges.fire({}); }); + + const auto list = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return QVector<MTPMessage>(); + }, [&](const auto &data) { + owner().processUsers(data.vusers()); + owner().processChats(data.vchats()); + return data.vmessages().v; + }); + + const auto fullCount = result.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(HistoryWidget::messagesReceived)")); + return 0; + }, [&](const MTPDmessages_messages &data) { + return int(data.vmessages().v.size()); + }, [&](const MTPDmessages_messagesSlice &data) { + return data.vcount().v; + }, [&](const MTPDmessages_channelMessages &data) { + if (const auto channel = owningHistory()->peer->asChannel()) { + channel->ptsReceived(data.vpts().v); + channel->processTopics(data.vtopics()); + } else { + LOG(("API Error: received messages.channelMessages when " + "no channel was passed! (HistoryWidget::messagesReceived)")); + } + return data.vcount().v; + }); + + if (list.isEmpty()) { + return true; + } + + const auto maxId = IdFromMessage(list.front()); + const auto wasSize = int(_list.size()); + const auto toFront = (wasSize > 0) && (maxId > _list.front()); + const auto localFlags = MessageFlags(); + const auto type = NewMessageType::Existing; + auto refreshed = std::vector<MsgId>(); + if (toFront) { + refreshed.reserve(_list.size() + list.size()); + } + auto skipped = 0; + for (const auto &message : list) { + if (const auto item = owner().addNewMessage(message, localFlags, type)) { + if (item->sublistPeerId() == sublistPeer()->id) { + if (toFront && item->id > _list.front()) { + refreshed.push_back(item->id); + } else if (_list.empty() || item->id < _list.back()) { + _list.push_back(item->id); + } } else { - updateChatListEntry(); - crl::on_main(this, [=] { _parent->loadMore(this); }); + ++skipped; } } else { - setChatListTimeId(_items.front()->date()); + ++skipped; } + } + if (toFront) { + refreshed.insert(refreshed.end(), _list.begin(), _list.end()); + _list = std::move(refreshed); + } - _parent->listMessageChanged(item.get(), chatListMessage()); + const auto nowSize = int(_list.size()); + auto &decrementFrom = toFront ? _skippedAfter : _skippedBefore; + if (decrementFrom.has_value()) { + *decrementFrom = std::max( + *decrementFrom - (nowSize - wasSize), + 0); } - if (removed || _fullCount) { - _changed.fire({}); + + const auto checkedCount = std::max(fullCount - skipped, nowSize); + if (_skippedBefore && _skippedAfter) { + auto &correct = toFront ? _skippedBefore : _skippedAfter; + *correct = std::max( + checkedCount - *decrementFrom - nowSize, + 0); + *decrementFrom = checkedCount - *correct - nowSize; + Assert(*decrementFrom >= 0); + } else if (_skippedBefore) { + *_skippedBefore = std::min(*_skippedBefore, checkedCount - nowSize); + _skippedAfter = checkedCount - *_skippedBefore - nowSize; + } else if (_skippedAfter) { + *_skippedAfter = std::min(*_skippedAfter, checkedCount - nowSize); + _skippedBefore = checkedCount - *_skippedAfter - nowSize; } + _fullCount = checkedCount; + + checkReadTillEnd(); + + Ensures(list.size() >= skipped); + return (list.size() == skipped); +} + +void SavedSublist::setInboxReadTill( + MsgId readTillId, + std::optional<int> unreadCount) { + const auto newReadTillId = std::max(readTillId.bare, int64(1)); + const auto ignore = (newReadTillId < _inboxReadTillId); + if (ignore) { + return; + } + const auto changed = (newReadTillId > _inboxReadTillId); + if (changed) { + _inboxReadTillId = newReadTillId; + } + if (_skippedAfter == 0 + && !_list.empty() + && _inboxReadTillId >= _list.front()) { + unreadCount = 0; + } + const auto wasUnreadCount = _unreadCount; + if (_unreadCount.current() != unreadCount + && (changed || unreadCount.has_value())) { + setUnreadCount(unreadCount); + } +} + +MsgId SavedSublist::inboxReadTillId() const { + return _inboxReadTillId; +} + +MsgId SavedSublist::computeInboxReadTillFull() const { + return _inboxReadTillId; +} + +void SavedSublist::setOutboxReadTill(MsgId readTillId) { + const auto newReadTillId = std::max(readTillId.bare, int64(1)); + if (newReadTillId > _outboxReadTillId) { + _outboxReadTillId = newReadTillId; + const auto history = owningHistory(); + history->session().changes().historyUpdated( + history, + HistoryUpdate::Flag::OutboxRead); + } +} + +MsgId SavedSublist::computeOutboxReadTillFull() const { + return _outboxReadTillId; +} + +void SavedSublist::setUnreadCount(std::optional<int> count) { + _unreadCount = count; + if (!count && !_readRequestTimer.isActive() && !_readRequestId) { + reloadUnreadCountIfNeeded(); + } +} + +bool SavedSublist::unreadCountKnown() const { + return !inMonoforum() || _unreadCount.current().has_value(); +} + +int SavedSublist::unreadCountCurrent() const { + return _unreadCount.current().value_or(0); +} + +rpl::producer<std::optional<int>> SavedSublist::unreadCountValue() const { + if (!inMonoforum()) { + return rpl::single(std::optional<int>(0)); + } + return _unreadCount.value(); +} + +int SavedSublist::displayedUnreadCount() const { + return (_inboxReadTillId > 1) ? unreadCountCurrent() : 0; +} + +void SavedSublist::changeUnreadCountByMessage(MsgId id, int delta) { + if (!inMonoforum() || !_inboxReadTillId) { + setUnreadCount(std::nullopt); + return; + } + const auto count = _unreadCount.current(); + if (count.has_value() && (id > _inboxReadTillId)) { + setUnreadCount(std::max(*count + delta, 0)); + } +} + +bool SavedSublist::isServerSideUnread( + not_null<const HistoryItem*> item) const { + if (!inMonoforum()) { + return false; + } + const auto till = item->out() + ? computeOutboxReadTillFull() + : computeInboxReadTillFull(); + return (item->id > till); +} + +void SavedSublist::checkReadTillEnd() { + if (_unreadCount.current() != 0 + && _skippedAfter == 0 + && !_list.empty() + && _inboxReadTillId >= _list.front()) { + setUnreadCount(0); + } +} + +std::optional<int> SavedSublist::computeUnreadCountLocally( + MsgId afterId) const { + Expects(afterId >= _inboxReadTillId); + + const auto currentUnreadCountAfter = _unreadCount.current(); + const auto startingMarkingAsRead = (currentUnreadCountAfter == 0) + && (_inboxReadTillId == 1) + && (afterId > 1); + const auto wasUnreadCountAfter = startingMarkingAsRead + ? _fullCount.current().value_or(0) + : currentUnreadCountAfter; + const auto readTillId = std::max(afterId, MsgId(1)); + const auto wasReadTillId = _inboxReadTillId; + const auto backLoaded = (_skippedBefore == 0); + const auto frontLoaded = (_skippedAfter == 0); + const auto fullLoaded = backLoaded && frontLoaded; + const auto allUnread = (readTillId == MsgId(1)) + || (fullLoaded && _list.empty()); + if (allUnread && fullLoaded) { + // Should not happen too often unless the list is empty. + return int(_list.size()); + } else if (frontLoaded && !_list.empty() && readTillId >= _list.front()) { + // Always "count by local data" if read till the end. + return 0; + } else if (wasReadTillId == readTillId) { + // Otherwise don't recount the same value over and over. + return wasUnreadCountAfter; + } else if (frontLoaded && !_list.empty() && readTillId >= _list.back()) { + // And count by local data if it is available and read-till changed. + return int(ranges::lower_bound(_list, readTillId, std::greater<>()) + - begin(_list)); + } else if (_list.empty()) { + return std::nullopt; + } else if (wasUnreadCountAfter.has_value() + && (frontLoaded || readTillId <= _list.front()) + && (backLoaded || wasReadTillId >= _list.back())) { + // Count how many were read since previous value. + const auto from = ranges::lower_bound( + _list, + readTillId, + std::greater<>()); + const auto till = ranges::lower_bound( + from, + end(_list), + wasReadTillId, + std::greater<>()); + return std::max(*wasUnreadCountAfter - int(till - from), 0); + } + return std::nullopt; +} + +void SavedSublist::requestUnreadCount() { + if (_reloadUnreadCountRequestId) { + return; + } + //const auto weak = base::make_weak(this); // #TODO monoforum + //const auto session = &_parent->session(); + //const auto apply = [weak](MsgId readTill, int unreadCount) { + // if (const auto strong = weak.get()) { + // strong->setInboxReadTill(readTill, unreadCount); + // } + //}; + //_reloadUnreadCountRequestId = session->api().request( + // ... + //).done([=](const ... &result) { + // if (weak) { + // _reloadUnreadCountRequestId = 0; + // } + // ... + //}).send(); +} + +void SavedSublist::readTill(not_null<HistoryItem*> item) { + readTill(item->id, item); +} + +void SavedSublist::readTill(MsgId tillId) { + const auto parentChat = _parent->parentChat(); + if (!parentChat) { + return; + } + readTill(tillId, owner().message(parentChat->id, tillId)); +} + +void SavedSublist::readTill( + MsgId tillId, + HistoryItem *tillIdItem) { + if (!IsServerMsgId(tillId)) { + return; + } + const auto was = computeInboxReadTillFull(); + const auto now = tillId; + if (now < was) { + return; + } + const auto unreadCount = computeUnreadCountLocally(now); + const auto fast = (tillIdItem && tillIdItem->out()) + || !unreadCount.has_value(); + if (was < now || (fast && now == was)) { + setInboxReadTill(now, unreadCount); + if (!_readRequestTimer.isActive()) { + _readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout); + } else if (fast && _readRequestTimer.remainingTime() > 0) { + _readRequestTimer.callOnce(0); + } + } + // Core::App().notifications().clearIncomingFromSublist(this); // #TODO monoforum +} + +void SavedSublist::sendReadTillRequest() { + const auto parentChat = _parent->parentChat(); + if (!parentChat) { + return; + } + if (_readRequestTimer.isActive()) { + _readRequestTimer.cancel(); + } + const auto api = &_parent->session().api(); + api->request(base::take(_readRequestId)).cancel(); + + _readRequestId = api->request(MTPmessages_ReadSavedHistory( + parentChat->input, + sublistPeer()->input, + MTP_int(computeInboxReadTillFull()) + )).done(crl::guard(this, [=] { + _readRequestId = 0; + reloadUnreadCountIfNeeded(); + })).send(); +} + +void SavedSublist::reloadUnreadCountIfNeeded() { + if (unreadCountKnown()) { + return; + } else if (inboxReadTillId() < computeInboxReadTillFull()) { + _readRequestTimer.callOnce(0); + } else { + requestUnreadCount(); + } +} + +void SavedSublist::subscribeToUnreadChanges() { + if (!inMonoforum()) { + return; + } + _unreadCount.value( + ) | rpl::map([=](std::optional<int> value) { + return value ? displayedUnreadCount() : value; + }) | rpl::distinct_until_changed( + ) | rpl::combine_previous( + ) | rpl::filter([=] { + return inChatList(); + }) | rpl::start_with_next([=]( + std::optional<int> previous, + std::optional<int> now) { + if (previous.value_or(0) != now.value_or(0)) { + _parent->recentSublistsInvalidate(this); + } + notifyUnreadStateChange(unreadStateFor( + previous.value_or(0), + previous.has_value())); + }, _lifetime); +} + +void SavedSublist::applyMonoforumDialog( + const MTPDmonoForumDialog &data, + not_null<HistoryItem*> topItem) { + //if (const auto draft = data.vdraft()) { // #TODO monoforum + // draft->match([&](const MTPDdraftMessage &data) { + // Data::ApplyPeerCloudDraft( + // &session(), + // channel()->id, + // _rootId, + // data); + // }, [](const MTPDdraftMessageEmpty&) {}); + //} + + setInboxReadTill( + data.vread_inbox_max_id().v, + data.vunread_count().v); + setOutboxReadTill(data.vread_outbox_max_id().v); + applyMaybeLast(topItem); } rpl::producer<> SavedSublist::changes() const { - return _changed.events(); + return _listChanges.events(); +} + +void SavedSublist::loadFullCount() { + if (!_fullCount.current() && !_loadingAround) { + loadAround(0); + } +} + +void SavedSublist::appendClientSideMessages(MessagesSlice &slice) { + const auto &messages = owningHistory()->clientSideMessages(); + if (messages.empty()) { + return; + } else if (slice.ids.empty()) { + if (slice.skippedBefore != 0 || slice.skippedAfter != 0) { + return; + } + slice.ids.reserve(messages.size()); + const auto sublistPeerId = sublistPeer()->id; + for (const auto &item : messages) { + if (item->sublistPeerId() != sublistPeerId) { + continue; + } + slice.ids.push_back(item->fullId()); + } + ranges::sort(slice.ids); + return; + } + const auto sublistPeerId = sublistPeer()->id; + auto dates = std::vector<TimeId>(); + dates.reserve(slice.ids.size()); + for (const auto &id : slice.ids) { + const auto message = owner().message(id); + Assert(message != nullptr); + + dates.push_back(message->date()); + } + for (const auto &item : messages) { + if (item->sublistPeerId() != sublistPeerId) { + continue; + } + const auto date = item->date(); + if (date < dates.front()) { + if (slice.skippedBefore != 0) { + if (slice.skippedBefore) { + ++*slice.skippedBefore; + } + continue; + } + dates.insert(dates.begin(), date); + slice.ids.insert(slice.ids.begin(), item->fullId()); + } else { + auto to = dates.size(); + for (; to != 0; --to) { + const auto checkId = slice.ids[to - 1].msg; + if (dates[to - 1] > date) { + continue; + } else if (dates[to - 1] < date + || IsServerMsgId(checkId) + || checkId < item->id) { + break; + } + } + dates.insert(dates.begin() + to, date); + slice.ids.insert(slice.ids.begin() + to, item->fullId()); + } + } } std::optional<int> SavedSublist::fullCount() const { - return isFullLoaded() ? int(_items.size()) : _fullCount; + return _fullCount.current(); } rpl::producer<int> SavedSublist::fullCountValue() const { - return _changed.events_starting_with({}) | rpl::map([=] { - return fullCount(); - }) | rpl::filter_optional(); -} - -void SavedSublist::append( - std::vector<not_null<HistoryItem*>> &&items, - int fullCount) { - _fullCount = fullCount; - if (items.empty()) { - setFullLoaded(); - } else if (_items.empty()) { - _items = std::move(items); - setChatListTimeId(_items.front()->date()); - _changed.fire({}); - } else if (_items.back()->id > items.front()->id) { - _items.insert(end(_items), begin(items), end(items)); - _changed.fire({}); - } else { - _items.insert(end(_items), begin(items), end(items)); - ranges::stable_sort( - _items, - ranges::greater(), - &HistoryItem::id); - ranges::unique(_items, ranges::greater(), &HistoryItem::id); - _changed.fire({}); - } -} - -void SavedSublist::setFullLoaded(bool loaded) { - if (loaded != isFullLoaded()) { - if (loaded) { - _flags |= Flag::FullLoaded; - if (_items.empty()) { - updateChatListExistence(); - } - } else { - _flags &= ~Flag::FullLoaded; - } - _changed.fire({}); - } + return _fullCount.value() | rpl::filter_optional(); } int SavedSublist::fixedOnTopIndex() const { @@ -206,50 +800,87 @@ bool SavedSublist::shouldBeInChatList() const { return false; } } - return isPinnedDialog(FilterId()) || !_items.empty(); + return isPinnedDialog(FilterId()) + || !lastMessageKnown() + || (lastMessage() != nullptr); +} + +HistoryItem *SavedSublist::lastMessage() const { + return _lastMessage.value_or(nullptr); +} + +bool SavedSublist::lastMessageKnown() const { + return _lastMessage.has_value(); +} + +HistoryItem *SavedSublist::lastServerMessage() const { + return _lastServerMessage.value_or(nullptr); +} + +bool SavedSublist::lastServerMessageKnown() const { + return _lastServerMessage.has_value(); +} + +MsgId SavedSublist::lastKnownServerMessageId() const { + return _lastKnownServerMessageId; } Dialogs::UnreadState SavedSublist::chatListUnreadState() const { - return {}; + if (!inMonoforum()) { + return {}; + } + return unreadStateFor(displayedUnreadCount(), unreadCountKnown()); } Dialogs::BadgesState SavedSublist::chatListBadgesState() const { - return {}; + if (!inMonoforum()) { + return {}; + } + auto result = Dialogs::BadgesForUnread( + chatListUnreadState(), + Dialogs::CountInBadge::Messages, + Dialogs::IncludeInBadge::All); + if (!result.unread && inboxReadTillId() < 2) { + result.unread = (_lastKnownServerMessageId + > _parent->owningHistory()->inboxReadTillId()); + result.unreadMuted = muted(); + } + return result; } HistoryItem *SavedSublist::chatListMessage() const { - return _items.empty() ? nullptr : _items.front().get(); + return _lastMessage.value_or(nullptr); } bool SavedSublist::chatListMessageKnown() const { - return true; + return _lastMessage.has_value(); } const QString &SavedSublist::chatListName() const { - return _history->chatListName(); + return _sublistHistory->chatListName(); } const base::flat_set<QString> &SavedSublist::chatListNameWords() const { - return _history->chatListNameWords(); + return _sublistHistory->chatListNameWords(); } const base::flat_set<QChar> &SavedSublist::chatListFirstLetters() const { - return _history->chatListFirstLetters(); + return _sublistHistory->chatListFirstLetters(); } const QString &SavedSublist::chatListNameSortKey() const { - return _history->chatListNameSortKey(); + return _sublistHistory->chatListNameSortKey(); } int SavedSublist::chatListNameVersion() const { - return _history->chatListNameVersion(); + return _sublistHistory->chatListNameVersion(); } void SavedSublist::paintUserpic( Painter &p, Ui::PeerUserpicView &view, const Dialogs::Ui::PaintContext &context) const { - _history->paintUserpic(p, view, context); + _sublistHistory->paintUserpic(p, view, context); } HistoryView::SendActionPainter *SavedSublist::sendActionPainter() { @@ -277,17 +908,6 @@ void SavedSublist::hasUnreadReactionChanged(bool has) { notifyUnreadStateChange(was); } -bool SavedSublist::isServerSideUnread( - not_null<const HistoryItem*> item) const { - return false; -} - - -void SavedSublist::chatListPreloadData() { - sublistPeer()->loadUserpic(); - allowChatListMessageResolve(); -} - void SavedSublist::allowChatListMessageResolve() { if (_flags & Flag::ResolveChatListMessage) { return; @@ -296,27 +916,257 @@ void SavedSublist::allowChatListMessageResolve() { resolveChatListMessageGroup(); } -bool SavedSublist::hasOrphanMediaGroupPart() const { - if (isFullLoaded() || _items.size() != 1) { - return false; - } - return (_items.front()->groupId() != MessageGroupId()); -} - void SavedSublist::resolveChatListMessageGroup() { - const auto item = chatListMessage(); - if (!(_flags & Flag::ResolveChatListMessage) - || !item - || !hasOrphanMediaGroupPart()) { + if (!(_flags & Flag::ResolveChatListMessage)) { return; } // If we set a single album part, request the full album. - const auto withImages = !item->toPreview({ - .hideSender = true, - .hideCaption = true }).images.empty(); - if (withImages) { - owner().histories().requestGroupAround(item); + const auto item = _lastServerMessage.value_or(nullptr); + if (item && item->groupId() != MessageGroupId()) { + if (owner().groups().isGroupOfOne(item) + && !item->toPreview({ + .hideSender = true, + .hideCaption = true }).images.empty() + && _requestedGroups.emplace(item->fullId()).second) { + owner().histories().requestGroupAround(item); + } } } +void SavedSublist::growLastKnownServerMessageId(MsgId id) { + _lastKnownServerMessageId = std::max(_lastKnownServerMessageId, id); +} + +void SavedSublist::setLastServerMessage(HistoryItem *item) { + if (item) { + growLastKnownServerMessageId(item->id); + } + _lastServerMessage = item; + if (_lastMessage + && *_lastMessage + && !(*_lastMessage)->isRegular() + && (!item + || (*_lastMessage)->date() > item->date() + || (*_lastMessage)->isSending())) { + return; + } + setLastMessage(item); +} + +void SavedSublist::setLastMessage(HistoryItem *item) { + if (_lastMessage && *_lastMessage == item) { + return; + } + _lastMessage = item; + if (!item || item->isRegular()) { + _lastServerMessage = item; + if (item) { + growLastKnownServerMessageId(item->id); + } + } + setChatListMessage(item); +} + +void SavedSublist::setChatListMessage(HistoryItem *item) { + if (_chatListMessage && *_chatListMessage == item) { + return; + } + const auto was = _chatListMessage.value_or(nullptr); + if (item) { + if (item->isSponsored()) { + return; + } + if (_chatListMessage + && *_chatListMessage + && !(*_chatListMessage)->isRegular() + && (*_chatListMessage)->date() > item->date()) { + return; + } + _chatListMessage = item; + setChatListTimeId(item->date()); + } else if (!_chatListMessage || *_chatListMessage) { + _chatListMessage = nullptr; + updateChatListEntry(); + } + _parent->listMessageChanged(was, item); +} + +void SavedSublist::chatListPreloadData() { + sublistPeer()->loadUserpic(); + allowChatListMessageResolve(); +} + +Dialogs::UnreadState SavedSublist::unreadStateFor( + int count, + bool known) const { + auto result = Dialogs::UnreadState(); + const auto muted = this->muted(); + result.messages = count; + result.chats = count ? 1 : 0; + result.chatsMuted = muted ? result.chats : 0; + result.known = known; + return result; +} + +Histories &SavedSublist::histories() { + return owner().histories(); +} + +void SavedSublist::loadAround(MsgId id) { + if (_loadingAround && *_loadingAround == id) { + return; + } + histories().cancelRequest(base::take(_beforeId)); + histories().cancelRequest(base::take(_afterId)); + + const auto send = [=](Fn<void()> finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(id), // offset_id + MTP_int(0), // offset_date + MTP_int(id ? (-kMessagesPerPage / 2) : 0), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // max_id + MTP_int(0), // min_id + MTP_long(0)) // hash + ).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + + if (!id) { + _skippedAfter = 0; + } else { + _skippedAfter = std::nullopt; + } + _skippedBefore = std::nullopt; + _list.clear(); + if (processMessagesIsEmpty(result)) { + _fullCount = _skippedBefore = _skippedAfter = 0; + } else if (id) { + Assert(!_list.empty()); + if (_list.front() <= id) { + _skippedAfter = 0; + } else if (_list.back() >= id) { + _skippedBefore = 0; + } + } + checkReadTillEnd(); + }).fail([=](const MTP::Error &error) { + if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { + _parent->markUnsupported(); + } + _beforeId = 0; + _loadingAround = std::nullopt; + finish(); + }).send(); + }; + _loadingAround = id; + _beforeId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + +void SavedSublist::loadBefore() { + Expects(!_list.empty()); + + if (_loadingAround) { + histories().cancelRequest(base::take(_beforeId)); + } else if (_beforeId) { + return; + } + + const auto last = _list.back(); + const auto send = [=](Fn<void()> finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(last), // offset_id + MTP_int(0), // offset_date + MTP_int(0), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // min_id + MTP_int(0), // max_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { + _beforeId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.back() != last) { + loadBefore(); + } else if (processMessagesIsEmpty(result)) { + _skippedBefore = 0; + if (_skippedAfter == 0) { + _fullCount = _list.size(); + } + } + }).fail([=] { + _beforeId = 0; + finish(); + }).send(); + }; + _beforeId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + +void SavedSublist::loadAfter() { + Expects(!_list.empty()); + + if (_afterId) { + return; + } + + const auto first = _list.front(); + const auto send = [=](Fn<void()> finish) { + using Flag = MTPmessages_GetSavedHistory::Flag; + const auto parentChat = _parent->parentChat(); + return session().api().request(MTPmessages_GetSavedHistory( + MTP_flags(parentChat ? Flag::f_parent_peer : Flag(0)), + parentChat ? parentChat->input : MTPInputPeer(), + sublistPeer()->input, + MTP_int(first + 1), // offset_id + MTP_int(0), // offset_date + MTP_int(-kMessagesPerPage), // add_offset + MTP_int(kMessagesPerPage), // limit + MTP_int(0), // min_id + MTP_int(0), // max_id + MTP_long(0) // hash + )).done([=](const MTPmessages_Messages &result) { + _afterId = 0; + finish(); + + if (_list.empty()) { + return; + } else if (_list.front() != first) { + loadAfter(); + } else if (processMessagesIsEmpty(result)) { + _skippedAfter = 0; + if (_skippedBefore == 0) { + _fullCount = _list.size(); + } + checkReadTillEnd(); + } + }).fail([=] { + _afterId = 0; + finish(); + }).send(); + }; + _afterId = histories().sendRequest( + owningHistory(), + Histories::RequestType::History, + send); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 4217a8fb67..468e4b647d 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/timer.h" #include "data/data_thread.h" #include "dialogs/ui/dialogs_message_view.h" @@ -16,31 +17,60 @@ class History; namespace Data { class Session; +class Histories; class SavedMessages; +struct MessagePosition; +struct MessageUpdate; +struct SublistReadTillUpdate; +struct MessagesSlice; class SavedSublist final : public Data::Thread { public: - SavedSublist(not_null<SavedMessages*> parent, not_null<PeerData*> peer); + SavedSublist( + not_null<SavedMessages*> parent, + not_null<PeerData*> sublistPeer); ~SavedSublist(); + [[nodiscard]] bool inMonoforum() const; + + void apply(const SublistReadTillUpdate &update); + void apply(const MessageUpdate &update); + void applyDifferenceTooLong(); + bool removeOne(not_null<HistoryItem*> item); + + [[nodiscard]] rpl::producer<MessagesSlice> source( + MessagePosition aroundId, + int limitBefore, + int limitAfter); + [[nodiscard]] not_null<SavedMessages*> parent() const; [[nodiscard]] not_null<History*> owningHistory() override; [[nodiscard]] ChannelData *parentChat() const; [[nodiscard]] not_null<PeerData*> sublistPeer() const; [[nodiscard]] bool isHiddenAuthor() const; - [[nodiscard]] bool isFullLoaded() const; [[nodiscard]] rpl::producer<> destroyed() const; - [[nodiscard]] auto messages() const - -> const std::vector<not_null<HistoryItem*>> &; + void growLastKnownServerMessageId(MsgId id); void applyMaybeLast(not_null<HistoryItem*> item, bool added = false); - void removeOne(not_null<HistoryItem*> item); - void append(std::vector<not_null<HistoryItem*>> &&items, int fullCount); - void setFullLoaded(bool loaded = true); + void applyItemAdded(not_null<HistoryItem*> item); + void applyItemRemoved(MsgId id); [[nodiscard]] rpl::producer<> changes() const; [[nodiscard]] std::optional<int> fullCount() const; [[nodiscard]] rpl::producer<int> fullCountValue() const; + [[nodiscard]] rpl::producer<std::optional<int>> maybeFullCount() const; + void loadFullCount(); + + [[nodiscard]] bool unreadCountKnown() const; + [[nodiscard]] int unreadCountCurrent() const; + [[nodiscard]] int displayedUnreadCount() const; + [[nodiscard]] rpl::producer<std::optional<int>> unreadCountValue() const; + + void applyMonoforumDialog( + const MTPDmonoForumDialog &dialog, + not_null<HistoryItem*> topItem); + void readTillEnd(); + void requestChatListMessage(); int fixedOnTopIndex() const override; bool shouldBeInChatList() const override; @@ -57,9 +87,27 @@ public: void hasUnreadMentionChanged(bool has) override; void hasUnreadReactionChanged(bool has) override; + [[nodiscard]] HistoryItem *lastMessage() const; + [[nodiscard]] HistoryItem *lastServerMessage() const; + [[nodiscard]] bool lastMessageKnown() const; + [[nodiscard]] bool lastServerMessageKnown() const; + [[nodiscard]] MsgId lastKnownServerMessageId() const; + + void setInboxReadTill(MsgId readTillId, std::optional<int> unreadCount); + [[nodiscard]] MsgId inboxReadTillId() const; + [[nodiscard]] MsgId computeInboxReadTillFull() const; + + void setOutboxReadTill(MsgId readTillId); + [[nodiscard]] MsgId computeOutboxReadTillFull() const; + [[nodiscard]] bool isServerSideUnread( not_null<const HistoryItem*> item) const override; + void requestUnreadCount(); + + void readTill(not_null<HistoryItem*> item); + void readTill(MsgId tillId); + void chatListPreloadData() override; void paintUserpic( Painter &p, @@ -70,25 +118,75 @@ public: -> HistoryView::SendActionPainter* override; private: + struct Viewer; + enum class Flag : uchar { ResolveChatListMessage = (1 << 0), - FullLoaded = (1 << 1), + InMonoforum = (1 << 1), }; friend inline constexpr bool is_flag_type(Flag) { return true; } using Flags = base::flags<Flag>; - bool hasOrphanMediaGroupPart() const; + [[nodiscard]] Histories &histories(); + + void subscribeToUnreadChanges(); + [[nodiscard]] Dialogs::UnreadState unreadStateFor( + int count, + bool known) const; + void setLastMessage(HistoryItem *item); + void setLastServerMessage(HistoryItem *item); + void setChatListMessage(HistoryItem *item); void allowChatListMessageResolve(); void resolveChatListMessageGroup(); - const not_null<SavedMessages*> _parent; - const not_null<History*> _history; + void changeUnreadCountByMessage(MsgId id, int delta); + void setUnreadCount(std::optional<int> count); + void readTill(MsgId tillId, HistoryItem *tillIdItem); + void checkReadTillEnd(); + void sendReadTillRequest(); + void reloadUnreadCountIfNeeded(); - std::vector<not_null<HistoryItem*>> _items; - std::optional<int> _fullCount; - rpl::event_stream<> _changed; + [[nodiscard]] bool buildFromData(not_null<Viewer*> viewer); + [[nodiscard]] bool applyUpdate(const MessageUpdate &update); + void appendClientSideMessages(MessagesSlice &slice); + [[nodiscard]] std::optional<int> computeUnreadCountLocally( + MsgId afterId) const; + bool processMessagesIsEmpty(const MTPmessages_Messages &result); + void loadAround(MsgId id); + void loadBefore(); + void loadAfter(); + + const not_null<SavedMessages*> _parent; + const not_null<History*> _sublistHistory; + + MsgId _lastKnownServerMessageId = 0; + + std::vector<MsgId> _list; + std::optional<int> _skippedBefore; + std::optional<int> _skippedAfter; + rpl::variable<std::optional<int>> _fullCount; + rpl::event_stream<> _listChanges; + rpl::event_stream<> _instantChanges; + std::optional<MsgId> _loadingAround; + rpl::variable<std::optional<int>> _unreadCount; + MsgId _inboxReadTillId = 0; + MsgId _outboxReadTillId = 0; Flags _flags; + std::optional<HistoryItem*> _lastMessage; + std::optional<HistoryItem*> _lastServerMessage; + std::optional<HistoryItem*> _chatListMessage; + base::flat_set<FullMsgId> _requestedGroups; + int _beforeId = 0; + int _afterId = 0; + + base::Timer _readRequestTimer; + mtpRequestId _readRequestId = 0; + + mtpRequestId _reloadUnreadCountRequestId = 0; + + rpl::lifetime _lifetime; + }; } // namespace Data diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 82bda952cd..ec41e8ee17 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -341,6 +341,19 @@ void Session::subscribeForTopicRepliesLists() { } }, _lifetime); + sublistReadTillUpdates( + ) | rpl::start_with_next([=](const SublistReadTillUpdate &update) { + if (const auto parentChat = channelLoaded(update.parentChatId)) { + if (const auto monoforum = parentChat->monoforum()) { + const auto sublistPeerId = update.sublistPeerId; + const auto peer = monoforum->owner().peer(sublistPeerId); + if (const auto sublist = monoforum->sublistLoaded(peer)) { + sublist->apply(update); + } + } + } + }, _lifetime); + session().changes().messageUpdates( MessageUpdate::Flag::NewAdded | MessageUpdate::Flag::NewMaybeAdded @@ -349,6 +362,11 @@ void Session::subscribeForTopicRepliesLists() { ) | rpl::start_with_next([=](const MessageUpdate &update) { if (const auto topic = update.item->topic()) { topic->replies()->apply(update); + } else if (update.flags == MessageUpdate::Flag::ReplyToTopAdded) { + // Not interested in this one for sublist. + return; + } else if (const auto sublist = update.item->savedSublist()) { + sublist->apply(update); } }, _lifetime); @@ -2914,6 +2932,15 @@ auto Session::repliesReadTillUpdates() const return _repliesReadTillUpdates.events(); } +void Session::updateSublistReadTill(SublistReadTillUpdate update) { + _sublistReadTillUpdates.fire(std::move(update)); +} + +auto Session::sublistReadTillUpdates() const +-> rpl::producer<SublistReadTillUpdate> { + return _sublistReadTillUpdates.events(); +} + int Session::computeUnreadBadge(const Dialogs::UnreadState &state) const { const auto all = Core::App().settings().includeMutedCounter(); return std::max(state.marks - (all ? 0 : state.marksMuted), 0) diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 2a32df2bc2..2ac7d93d75 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -80,6 +80,13 @@ struct RepliesReadTillUpdate { bool out = false; }; +struct SublistReadTillUpdate { + ChannelId parentChatId; + PeerId sublistPeerId; + MsgId readTillId; + bool out = false; +}; + struct GiftUpdate { enum class Action : uchar { Save, @@ -565,6 +572,10 @@ public: [[nodiscard]] auto repliesReadTillUpdates() const -> rpl::producer<RepliesReadTillUpdate>; + void updateSublistReadTill(SublistReadTillUpdate update); + [[nodiscard]] auto sublistReadTillUpdates() const + -> rpl::producer<SublistReadTillUpdate>; + void selfDestructIn(not_null<HistoryItem*> item, crl::time delay); [[nodiscard]] not_null<PhotoData*> photo(PhotoId id); @@ -1004,6 +1015,7 @@ private: rpl::event_stream<ChatListEntryRefresh> _chatListEntryRefreshes; rpl::event_stream<> _unreadBadgeChanges; rpl::event_stream<RepliesReadTillUpdate> _repliesReadTillUpdates; + rpl::event_stream<SublistReadTillUpdate> _sublistReadTillUpdates; rpl::event_stream<SentToScheduled> _sentToScheduled; rpl::event_stream<SentFromScheduled> _sentFromScheduled; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 51bdcc9719..a67e60302b 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -163,6 +163,9 @@ void History::itemRemoved(not_null<HistoryItem*> item) { if (const auto topic = item->topic()) { topic->applyItemRemoved(item->id); } + if (const auto sublist = item->savedSublist()) { + sublist->applyItemRemoved(item->id); + } if (const auto chat = peer->asChat()) { if (const auto to = chat->getMigrateToChannel()) { if (const auto history = owner().historyLoaded(to)) { @@ -1311,6 +1314,9 @@ void History::newItemAdded(not_null<HistoryItem*> item) { if (const auto topic = item->topic()) { topic->applyItemAdded(item); } + if (const auto sublist = item->savedSublist()) { + sublist->applyItemAdded(item); + } if (const auto media = item->media()) { if (const auto gift = media->gift()) { if (const auto unique = gift->unique.get()) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 42f9127032..04c3f1076b 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -789,16 +789,6 @@ HistoryItem::~HistoryItem() { if (const auto reply = Get<HistoryMessageReply>()) { reply->clearData(this); } - if (const auto saved = Get<HistoryMessageSaved>()) { - if (saved->savedMessagesSublist) { - saved->savedMessagesSublist->removeOne(this); - } else if (const auto monoforum = _history->peer->monoforum()) { - const auto peer = _history->owner().peer(saved->sublistPeerId); - if (const auto sublist = monoforum->sublistLoaded(peer)) { - sublist->removeOne(this); - } - } - } clearDependencyMessage(); applyTTL(0); } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index d789f7f07b..0cd5a0fe00 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -191,6 +191,13 @@ object_ptr<Window::SectionWidget> ChatMemento::createWidget( _list.setScrollTopState(ListMemento::ScrollTopState{ Data::MinMessagePosition }); + } else if (!_list.aroundPosition().fullId + && _id.sublist + && _id.sublist->computeInboxReadTillFull() == MsgId(1)) { + _list.setAroundPosition(Data::MinMessagePosition); + _list.setScrollTopState(ListMemento::ScrollTopState{ + Data::MinMessagePosition + }); } auto result = object_ptr<ChatWidget>(parent, controller, _id); result->setInternalState(geometry, this); @@ -394,7 +401,9 @@ ChatWidget::ChatWidget( } }, lifetime()); - if (!_topic) { + if (_sublist) { + subscribeToSublist(); + } else if (!_topic) { _history->session().changes().historyUpdates( _history, Data::HistoryUpdate::Flag::OutboxRead @@ -2455,6 +2464,19 @@ void ChatWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { }, _repliesLifetime); } +void ChatWidget::subscribeToSublist() { + Expects(_sublist != nullptr); + + _sublist->unreadCountValue( + ) | rpl::start_with_next([=](std::optional<int> count) { + refreshUnreadCountBadge(count); + }, lifetime()); + + refreshUnreadCountBadge(_sublist->unreadCountKnown() + ? _sublist->unreadCountCurrent() + : std::optional<int>()); +} + void ChatWidget::restoreState(not_null<ChatMemento*> memento) { if (auto replies = memento->getReplies()) { setReplies(std::move(replies)); @@ -2792,54 +2814,20 @@ rpl::producer<Data::MessagesSlice> ChatWidget::sublistSource( const auto messageId = aroundId.fullId.msg ? aroundId.fullId.msg : (ServerMaxMsgId - 1); - return [=](auto consumer) { - const auto pushSlice = [=] { - auto result = Data::MessagesSlice(); - result.fullCount = _sublist->fullCount(); - _topBar->setCustomTitle(result.fullCount - ? tr::lng_forum_messages( - tr::now, - lt_count_decimal, - *result.fullCount) - : tr::lng_contacts_loading(tr::now)); - const auto &messages = _sublist->messages(); - const auto i = ranges::lower_bound( - messages, - messageId, - ranges::greater(), - [](not_null<HistoryItem*> item) { return item->id; }); - const auto before = int(end(messages) - i); - const auto useBefore = std::min(before, limitBefore); - const auto after = int(i - begin(messages)); - const auto useAfter = std::min(after, limitAfter); - const auto from = i - useAfter; - const auto till = i + useBefore; - auto nearestDistance = std::numeric_limits<int64>::max(); - result.ids.reserve(useAfter + useBefore); - for (auto j = till; j != from;) { - const auto item = *--j; - result.ids.push_back(item->fullId()); - const auto distance = std::abs((messageId - item->id).bare); - if (nearestDistance > distance) { - nearestDistance = distance; - result.nearestToAround = result.ids.back(); - } - } - result.skippedAfter = after - useAfter; - result.skippedBefore = result.fullCount - ? (*result.fullCount - after - useBefore) - : std::optional<int>(); - if (!result.fullCount || useBefore < limitBefore) { - _sublist->parent()->loadMore(_sublist); - } - markLoaded(); - consumer.put_next(std::move(result)); - }; - auto lifetime = rpl::lifetime(); - _sublist->changes() | rpl::start_with_next(pushSlice, lifetime); - pushSlice(); - return lifetime; - }; + return _sublist->source( + aroundId, + limitBefore, + limitAfter + ) | rpl::before_next([=](const Data::MessagesSlice &result) { + // after_next makes a copy of value. + _topBar->setCustomTitle(result.fullCount + ? tr::lng_forum_messages( + tr::now, + lt_count_decimal, + *result.fullCount) + : tr::lng_contacts_loading(tr::now)); + markLoaded(); + }); } bool ChatWidget::listAllowsMultiSelect() { @@ -2882,6 +2870,8 @@ void ChatWidget::listSelectionChanged(SelectedItems &&items) { void ChatWidget::listMarkReadTill(not_null<HistoryItem*> item) { if (_replies) { _replies->readTill(item); + } else if (_sublist) { + _sublist->readTill(item); } } @@ -2892,16 +2882,22 @@ void ChatWidget::listMarkContentsRead( MessagesBarData ChatWidget::listMessagesBar( const std::vector<not_null<Element*>> &elements) { - if (_sublist || elements.empty()) { + if ((!_sublist && !_replies) || elements.empty()) { return {}; } - const auto till = _replies->computeInboxReadTillFull(); + const auto till = _replies + ? _replies->computeInboxReadTillFull() + : _sublist->computeInboxReadTillFull(); const auto hidden = (till < 2); for (auto i = 0, count = int(elements.size()); i != count; ++i) { const auto item = elements[i]->data(); if (item->isRegular() && item->id > till) { - if (item->out() || !item->replyToId()) { - _replies->readTill(item); + if (item->out() || (_replies && !item->replyToId())) { + if (_replies) { + _replies->readTill(item); + } else { + _sublist->readTill(item); + } } else { return { .bar = { @@ -2960,9 +2956,12 @@ bool ChatWidget::listElementHideReply(not_null<const Element*> view) { } bool ChatWidget::listElementShownUnread(not_null<const Element*> view) { + const auto item = view->data(); return _replies - ? _replies->isServerSideUnread(view->data()) - : view->data()->unread(view->data()->history()); + ? _replies->isServerSideUnread(item) + : _sublist + ? _sublist->isServerSideUnread(item) + : item->unread(item->history()); } bool ChatWidget::listIsGoodForAroundPosition( @@ -2973,7 +2972,7 @@ bool ChatWidget::listIsGoodForAroundPosition( void ChatWidget::listSendBotCommand( const QString &command, const FullMsgId &context) { - if (!_sublist) { + if (!_sublist || _sublist->parentChat()) { sendBotCommandWithOptions(command, context, {}); } } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index c38fe6dbe8..a62a30a2e9 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -265,6 +265,7 @@ private: void setupRootView(); void setupTopicViewer(); void subscribeToTopic(); + void subscribeToSublist(); void subscribeToPinnedMessages(); void setTopic(Data::ForumTopic *topic); diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 8caddbf0c7..725ad58ad7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -588,8 +588,8 @@ rpl::producer<int> SavedSublistCountValue( not_null<PeerData*> peer) { const auto saved = &peer->owner().savedMessages(); const auto sublist = saved->sublist(peer); - if (!sublist->fullCount()) { - saved->loadMore(sublist); + if (!sublist->fullCount().has_value()) { + sublist->loadFullCount(); return rpl::single(0) | rpl::then(sublist->fullCountValue()); } return sublist->fullCountValue(); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 4e8faa448e..70a3f6f488 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -433,7 +433,7 @@ updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Updat updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update; updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector<bytes> next_offset:int = Update; -updateReadMonoForumInbox#bcf34712 flags:# channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update; updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -2395,6 +2395,7 @@ messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool; +messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector<InputPeer> = messages.SavedDialogs; messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; updates.getState#edd4882a = updates.State; From f65556acb7e46e3826dbbd9aaa6a954de0a7b0e9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 20 May 2025 20:32:24 +0400 Subject: [PATCH 068/310] Support drafts in monoforum sublists. --- Telegram/SourceFiles/api/api_polls.cpp | 9 +- Telegram/SourceFiles/api/api_updates.cpp | 11 +- Telegram/SourceFiles/apiwrap.cpp | 69 ++++++--- Telegram/SourceFiles/data/data_drafts.cpp | 21 ++- Telegram/SourceFiles/data/data_drafts.h | 87 +++++++++--- Telegram/SourceFiles/data/data_forum.cpp | 4 +- .../SourceFiles/data/data_forum_topic.cpp | 3 +- Telegram/SourceFiles/data/data_msg_id.h | 6 +- .../SourceFiles/data/data_saved_messages.cpp | 11 +- .../SourceFiles/data/data_saved_sublist.cpp | 34 +++-- .../SourceFiles/data/data_saved_sublist.h | 2 + Telegram/SourceFiles/data/data_thread.cpp | 7 + Telegram/SourceFiles/data/data_thread.h | 1 + Telegram/SourceFiles/data/data_types.h | 2 - .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 6 +- Telegram/SourceFiles/history/history.cpp | 132 +++++++++++------- Telegram/SourceFiles/history/history.h | 91 ++++++++---- .../SourceFiles/history/history_widget.cpp | 53 ++++--- .../view/controls/compose_controls_common.h | 1 + .../history_view_compose_controls.cpp | 45 ++++-- .../controls/history_view_compose_controls.h | 1 + .../controls/history_view_draft_options.cpp | 6 +- .../controls/history_view_forward_panel.cpp | 27 +++- .../controls/history_view_forward_panel.h | 1 + .../view/history_view_chat_section.cpp | 7 +- Telegram/SourceFiles/mainwidget.cpp | 13 +- .../SourceFiles/storage/storage_account.cpp | 18 ++- .../SourceFiles/support/support_helper.cpp | 19 ++- .../SourceFiles/window/window_peer_menu.cpp | 6 +- .../window/window_session_controller.cpp | 3 +- 30 files changed, 488 insertions(+), 208 deletions(-) diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 398bd1acbc..d6ffaf551d 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -47,6 +47,7 @@ void Polls::create( const auto topicRootId = action.replyTo.messageId ? action.replyTo.topicRootId : 0; + const auto monoforumPeerId = action.replyTo.monoforumPeerId; auto sendFlags = MTPmessages_SendMedia::Flags(0); if (action.replyTo) { sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; @@ -54,9 +55,9 @@ void Polls::create( const auto clearCloudDraft = action.clearDraft; if (clearCloudDraft) { sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; - history->clearLocalDraft(topicRootId); - history->clearCloudDraft(topicRootId); - history->startSavingCloudDraft(topicRootId); + history->clearLocalDraft(topicRootId, monoforumPeerId); + history->clearCloudDraft(topicRootId, monoforumPeerId); + history->startSavingCloudDraft(topicRootId, monoforumPeerId); } const auto silentPost = ShouldSendSilent(peer, action.options); const auto starsPaid = std::min( @@ -106,6 +107,7 @@ void Polls::create( if (clearCloudDraft) { history->finishSavingCloudDraft( topicRootId, + monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } _session->changes().historyUpdated( @@ -118,6 +120,7 @@ void Polls::create( if (clearCloudDraft) { history->finishSavingCloudDraft( topicRootId, + monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } fail(); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 8efbfd32a1..c3928b6073 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2687,13 +2687,22 @@ void Updates::feedUpdate(const MTPUpdate &update) { const auto &data = update.c_updateDraftMessage(); const auto peerId = peerFromMTP(data.vpeer()); const auto topicRootId = data.vtop_msg_id().value_or_empty(); + const auto monoforumPeerId = data.vsaved_peer_id() + ? peerFromMTP(*data.vsaved_peer_id()) + : PeerId(); data.vdraft().match([&](const MTPDdraftMessage &data) { - Data::ApplyPeerCloudDraft(&session(), peerId, topicRootId, data); + Data::ApplyPeerCloudDraft( + &session(), + peerId, + topicRootId, + monoforumPeerId, + data); }, [&](const MTPDdraftMessageEmpty &data) { Data::ClearPeerCloudDraft( &session(), peerId, topicRootId, + monoforumPeerId, data.vdate().value_or_empty()); }); } break; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 352319e89e..5f4a5fda8b 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2064,8 +2064,13 @@ void ApiWrap::saveCurrentDraftToCloud() { _session->local().writeDrafts(history); const auto topicRootId = thread->topicRootId(); - const auto localDraft = history->localDraft(topicRootId); - const auto cloudDraft = history->cloudDraft(topicRootId); + const auto monoforumPeerId = thread->monoforumPeerId(); + const auto localDraft = history->localDraft( + topicRootId, + monoforumPeerId); + const auto cloudDraft = history->cloudDraft( + topicRootId, + monoforumPeerId); if (!Data::DraftsAreEqual(localDraft, cloudDraft) && !_session->supportMode()) { saveDraftToCloudDelayed(thread); @@ -2088,15 +2093,22 @@ void ApiWrap::saveDraftsToCloud() { const auto history = thread->owningHistory(); const auto topicRootId = thread->topicRootId(); - auto cloudDraft = history->cloudDraft(topicRootId); - auto localDraft = history->localDraft(topicRootId); + const auto monoforumPeerId = thread->monoforumPeerId(); + auto cloudDraft = history->cloudDraft(topicRootId, monoforumPeerId); + auto localDraft = history->localDraft(topicRootId, monoforumPeerId); if (cloudDraft && cloudDraft->saveRequestId) { request(base::take(cloudDraft->saveRequestId)).cancel(); } if (!_session->supportMode()) { - cloudDraft = history->createCloudDraft(topicRootId, localDraft); + cloudDraft = history->createCloudDraft( + topicRootId, + monoforumPeerId, + localDraft); } else if (!cloudDraft) { - cloudDraft = history->createCloudDraft(topicRootId, nullptr); + cloudDraft = history->createCloudDraft( + topicRootId, + monoforumPeerId, + nullptr); } auto flags = MTPmessages_SaveDraft::Flags(0); @@ -2106,7 +2118,9 @@ void ApiWrap::saveDraftsToCloud() { } else if (!cloudDraft->webpage.url.isEmpty()) { flags |= MTPmessages_SaveDraft::Flag::f_media; } - if (cloudDraft->reply.messageId || cloudDraft->reply.topicRootId) { + if (cloudDraft->reply.messageId + || cloudDraft->reply.topicRootId + || cloudDraft->reply.monoforumPeerId) { flags |= MTPmessages_SaveDraft::Flag::f_reply_to; } if (!textWithTags.tags.isEmpty()) { @@ -2117,7 +2131,7 @@ void ApiWrap::saveDraftsToCloud() { TextUtilities::ConvertTextTagsToEntities(textWithTags.tags), Api::ConvertOption::SkipLocal); - history->startSavingCloudDraft(topicRootId); + history->startSavingCloudDraft(topicRootId, monoforumPeerId); cloudDraft->saveRequestId = request(MTPmessages_SaveDraft( MTP_flags(flags), ReplyToForMTP(history, cloudDraft->reply), @@ -2132,11 +2146,15 @@ void ApiWrap::saveDraftsToCloud() { const auto requestId = response.requestId; history->finishSavingCloudDraft( topicRootId, + monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); - if (const auto cloudDraft = history->cloudDraft(topicRootId)) { + const auto cloudDraft = history->cloudDraft( + topicRootId, + monoforumPeerId); + if (cloudDraft) { if (cloudDraft->saveRequestId == requestId) { cloudDraft->saveRequestId = 0; - history->draftSavedToCloud(topicRootId); + history->draftSavedToCloud(topicRootId, monoforumPeerId); } } const auto i = _draftsSaveRequestIds.find(weak); @@ -2149,10 +2167,14 @@ void ApiWrap::saveDraftsToCloud() { const auto requestId = response.requestId; history->finishSavingCloudDraft( topicRootId, + monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); - if (const auto cloudDraft = history->cloudDraft(topicRootId)) { + const auto cloudDraft = history->cloudDraft( + topicRootId, + monoforumPeerId); + if (cloudDraft) { if (cloudDraft->saveRequestId == requestId) { - history->clearCloudDraft(topicRootId); + history->clearCloudDraft(topicRootId, monoforumPeerId); } } const auto i = _draftsSaveRequestIds.find(weak); @@ -3223,7 +3245,10 @@ void ApiWrap::sendAction(const SendAction &action) { void ApiWrap::finishForwarding(const SendAction &action) { const auto history = action.history; const auto topicRootId = action.replyTo.topicRootId; - auto toForward = history->resolveForwardDraft(topicRootId); + const auto monoforumPeerId = action.replyTo.monoforumPeerId; + auto toForward = history->resolveForwardDraft( + topicRootId, + monoforumPeerId); if (!toForward.items.empty()) { const auto error = GetErrorForSending( history->peer, @@ -3236,7 +3261,7 @@ void ApiWrap::finishForwarding(const SendAction &action) { } forwardMessages(std::move(toForward), action); - history->setForwardDraft(topicRootId, {}); + history->setForwardDraft(topicRootId, monoforumPeerId, {}); } _session->data().sendHistoryChangeNotifications(); @@ -3728,6 +3753,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { const auto clearCloudDraft = action.clearDraft; const auto draftTopicRootId = action.replyTo.topicRootId; + const auto draftMonoforumPeerId = action.replyTo.monoforumPeerId; const auto replyTo = action.replyTo.messageId ? peer->owner().message(action.replyTo.messageId) : nullptr; @@ -3837,8 +3863,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) { if (clearCloudDraft) { sendFlags |= MTPmessages_SendMessage::Flag::f_clear_draft; mediaFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; - history->clearCloudDraft(draftTopicRootId); - history->startSavingCloudDraft(draftTopicRootId); + history->clearCloudDraft(draftTopicRootId, draftMonoforumPeerId); + history->startSavingCloudDraft( + draftTopicRootId, + draftMonoforumPeerId); } const auto sendAs = action.options.sendAs; if (sendAs) { @@ -3884,6 +3912,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { if (clearCloudDraft) { history->finishSavingCloudDraft( draftTopicRootId, + draftMonoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } }; @@ -3898,6 +3927,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { if (clearCloudDraft) { history->finishSavingCloudDraft( draftTopicRootId, + draftMonoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } }; @@ -4016,6 +4046,7 @@ void ApiWrap::sendInlineResult( const auto topicRootId = action.replyTo.messageId ? action.replyTo.topicRootId : 0; + const auto monoforumPeerId = action.replyTo.monoforumPeerId; using SendFlag = MTPmessages_SendInlineBotResult::Flag; auto flags = NewMessageFlags(peer); @@ -4068,8 +4099,8 @@ void ApiWrap::sendInlineResult( .postAuthor = NewMessagePostAuthor(action), }); - history->clearCloudDraft(topicRootId); - history->startSavingCloudDraft(topicRootId); + history->clearCloudDraft(topicRootId, monoforumPeerId); + history->startSavingCloudDraft(topicRootId, monoforumPeerId); auto &histories = history->owner().histories(); histories.sendPreparedMessage( @@ -4090,6 +4121,7 @@ void ApiWrap::sendInlineResult( ), [=](const MTPUpdates &result, const MTP::Response &response) { history->finishSavingCloudDraft( topicRootId, + monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); if (done) { done(true); @@ -4098,6 +4130,7 @@ void ApiWrap::sendInlineResult( sendMessageFail(error, peer, randomId, newId); history->finishSavingCloudDraft( topicRootId, + monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); if (done) { done(false); diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index 1bd7135e86..ee8d348f9e 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -70,10 +70,11 @@ void ApplyPeerCloudDraft( not_null<Main::Session*> session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, const MTPDdraftMessage &draft) { const auto history = session->data().history(peerId); const auto date = draft.vdate().v; - if (history->skipCloudDraftUpdate(topicRootId, date)) { + if (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) { return; } const auto textWithTags = TextWithTags{ @@ -87,6 +88,7 @@ void ApplyPeerCloudDraft( ? ReplyToFromMTP(history, *draft.vreply_to()) : FullReplyTo(); replyTo.topicRootId = topicRootId; + replyTo.monoforumPeerId = monoforumPeerId; auto webpage = WebPageDraft{ .invert = draft.is_invert_media(), .removed = draft.is_no_webpage(), @@ -112,21 +114,22 @@ void ApplyPeerCloudDraft( cloudDraft->date = date; history->setCloudDraft(std::move(cloudDraft)); - history->applyCloudDraft(topicRootId); + history->applyCloudDraft(topicRootId, monoforumPeerId); } void ClearPeerCloudDraft( not_null<Main::Session*> session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, TimeId date) { const auto history = session->data().history(peerId); - if (history->skipCloudDraftUpdate(topicRootId, date)) { + if (history->skipCloudDraftUpdate(topicRootId, monoforumPeerId, date)) { return; } - history->clearCloudDraft(topicRootId); - history->applyCloudDraft(topicRootId); + history->clearCloudDraft(topicRootId, monoforumPeerId); + history->applyCloudDraft(topicRootId, monoforumPeerId); } void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) { @@ -146,12 +149,16 @@ void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) { }; const auto history = peer->owner().history(peer->id); const auto topicRootId = MsgId(); + const auto monoforumPeerId = PeerId(); history->setLocalDraft(std::make_unique<Data::Draft>( textWithTags, - FullReplyTo{ .topicRootId = topicRootId }, + FullReplyTo{ + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }, cursor, Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( history, Data::EntryUpdate::Flag::LocalDraftSet); diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 1330995ed0..ab19e0cb2e 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -23,11 +23,13 @@ void ApplyPeerCloudDraft( not_null<Main::Session*> session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, const MTPDdraftMessage &draft); void ClearPeerCloudDraft( not_null<Main::Session*> session, PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, TimeId date); struct WebPageDraft { @@ -72,22 +74,38 @@ public: [[nodiscard]] static constexpr DraftKey None() { return 0; } - [[nodiscard]] static constexpr DraftKey Local(MsgId topicRootId) { - return (topicRootId < 0 || topicRootId >= ServerMaxMsgId) + [[nodiscard]] static constexpr DraftKey Local( + MsgId topicRootId, + PeerId monoforumPeerId) { + return Invalid(topicRootId, monoforumPeerId) ? None() - : (topicRootId ? topicRootId.bare : kLocalDraftIndex); + : (topicRootId + ? topicRootId.bare + : monoforumPeerId + ? (monoforumPeerId.value + kMonoforumDraftBit) + : kLocalDraftIndex); } - [[nodiscard]] static constexpr DraftKey LocalEdit(MsgId topicRootId) { - return (topicRootId < 0 || topicRootId >= ServerMaxMsgId) + [[nodiscard]] static constexpr DraftKey LocalEdit( + MsgId topicRootId, + PeerId monoforumPeerId) { + return Invalid(topicRootId, monoforumPeerId) ? None() - : ((topicRootId ? topicRootId.bare : kLocalDraftIndex) - + kEditDraftShift); + : (kEditDraftShift + + (topicRootId + ? topicRootId.bare + : monoforumPeerId + ? (monoforumPeerId.value + kMonoforumDraftBit) + : kLocalDraftIndex)); } - [[nodiscard]] static constexpr DraftKey Cloud(MsgId topicRootId) { - return (topicRootId < 0 || topicRootId >= ServerMaxMsgId) + [[nodiscard]] static constexpr DraftKey Cloud( + MsgId topicRootId, + PeerId monoforumPeerId) { + return Invalid(topicRootId, monoforumPeerId) ? None() : topicRootId ? (kCloudDraftShift + topicRootId.bare) + : monoforumPeerId + ? (kCloudDraftShift + monoforumPeerId.value + kMonoforumDraftBit) : kCloudDraftIndex; } [[nodiscard]] static constexpr DraftKey Scheduled() { @@ -120,40 +138,62 @@ public: return !value ? None() : (value == kLocalDraftIndex + kEditDraftShiftOld) - ? LocalEdit(0) + ? LocalEdit(MsgId(), PeerId()) : (value == kScheduledDraftIndex + kEditDraftShiftOld) ? ScheduledEdit() : (value > 0 && value < 0x4000'0000) - ? Local(MsgId(value)) + ? Local(MsgId(value), PeerId()) : (value > kEditDraftShiftOld && value < kEditDraftShiftOld + 0x4000'000) - ? LocalEdit(int64(value - kEditDraftShiftOld)) + ? LocalEdit(MsgId(int64(value - kEditDraftShiftOld)), PeerId()) : None(); } [[nodiscard]] constexpr bool isLocal() const { return (_value == kLocalDraftIndex) - || (_value > 0 && _value < ServerMaxMsgId.bare); + || (_value > 0 + && (_value & kMonoforumDraftMask) < ServerMaxMsgId.bare); } [[nodiscard]] constexpr bool isCloud() const { return (_value == kCloudDraftIndex) - || (_value > kCloudDraftShift - && _value < kCloudDraftShift + ServerMaxMsgId.bare); + || ((_value & kMonoforumDraftMask) > kCloudDraftShift + && ((_value & kMonoforumDraftMask) + < kCloudDraftShift + ServerMaxMsgId.bare)); } [[nodiscard]] constexpr MsgId topicRootId() const { const auto max = ServerMaxMsgId.bare; - if (_value > kCloudDraftShift && _value < kCloudDraftShift + max) { + if (_value & kMonoforumDraftBit) { + return 0; + } else if ((_value > kCloudDraftShift) + && (_value < kCloudDraftShift + max)) { return (_value - kCloudDraftShift); - } else if (_value > kEditDraftShift && _value < kEditDraftShift + max) { + } else if ((_value > kEditDraftShift) + && (_value < kEditDraftShift + max)) { return (_value - kEditDraftShift); } else if (_value > 0 && _value < max) { return _value; } return 0; } - + [[nodiscard]] constexpr PeerId monoforumPeerId() const { + const auto max = ServerMaxMsgId.bare; + const auto value = _value & kMonoforumDraftMask; + if (!(_value & kMonoforumDraftBit)) { + return 0; + } else if ((value > kCloudDraftShift) + && (value < kCloudDraftShift + max)) { + return PeerId(UserId(value - kCloudDraftShift)); + } else if ((value > kEditDraftShift) + && (value < kEditDraftShift + max)) { + return PeerId(UserId(value - kEditDraftShift)); + } else if (value > 0 && value < max) { + return PeerId(UserId(value)); + } + return 0; + } friend inline constexpr auto operator<=>(DraftKey, DraftKey) = default; + friend inline constexpr bool operator==(DraftKey, DraftKey) = default; inline explicit operator bool() const { return _value != 0; @@ -163,9 +203,20 @@ private: constexpr DraftKey(int64 value) : _value(value) { } + [[nodiscard]] static constexpr bool Invalid( + MsgId topicRootId, + PeerId monoforumPeerId) { + return (topicRootId < 0) + || (topicRootId >= ServerMaxMsgId) + || !peerIsUser(monoforumPeerId) + || (monoforumPeerId.value >= ServerMaxMsgId); + } + static constexpr auto kLocalDraftIndex = -1; static constexpr auto kCloudDraftIndex = -2; static constexpr auto kScheduledDraftIndex = -3; + static constexpr auto kMonoforumDraftBit = (int64(1) << 60); + static constexpr auto kMonoforumDraftMask = (kMonoforumDraftBit - 1); static constexpr auto kEditDraftShift = ServerMaxMsgId.bare; static constexpr auto kCloudDraftShift = 2 * ServerMaxMsgId.bare; static constexpr auto kShortcutDraftShift = 3 * ServerMaxMsgId.bare; diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 80f51c42b2..d422b01947 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -73,7 +73,7 @@ Forum::~Forum() { const auto peerId = _history->peer->id; for (const auto &[rootId, topic] : _topics) { storage.unload(Storage::SharedMediaUnloadThread(peerId, rootId)); - _history->setForwardDraft(rootId, {}); + _history->setForwardDraft(rootId, PeerId(), {}); const auto raw = topic.get(); changes.topicRemoved(raw); @@ -197,7 +197,7 @@ void Forum::applyTopicDeleted(MsgId rootId) { session().storage().unload(Storage::SharedMediaUnloadThread( _history->peer->id, rootId)); - _history->setForwardDraft(rootId, {}); + _history->setForwardDraft(rootId, PeerId(), {}); } } diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index e3bf4757cb..12232d8538 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -406,6 +406,7 @@ void ForumTopic::applyTopic(const MTPDforumTopic &data) { &session(), channel()->id, _rootId, + PeerId(), data); }, [](const MTPDdraftMessageEmpty&) {}); } @@ -709,7 +710,7 @@ void ForumTopic::requestChatListMessage() { TimeId ForumTopic::adjustedChatListTimeId() const { const auto result = chatListTimeId(); - if (const auto draft = history()->cloudDraft(_rootId)) { + if (const auto draft = history()->cloudDraft(_rootId, PeerId())) { if (!Data::DraftIsNull(draft) && !session().supportMode()) { return std::max(result, draft->date); } diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 6a94211cbf..355aff7679 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -180,11 +180,11 @@ struct FullReplyTo { PeerId monoforumPeerId = 0; int quoteOffset = 0; - [[nodiscard]] bool valid() const { - return messageId || (storyId && storyId.peer) || monoforumPeerId; + [[nodiscard]] bool replying() const { + return messageId || (storyId && storyId.peer); } explicit operator bool() const { - return valid(); + return replying() || monoforumPeerId; } friend inline auto operator<=>(FullReplyTo, FullReplyTo) = default; friend inline bool operator==(FullReplyTo, FullReplyTo) = default; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 3bcc65ad4e..ef67795445 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_messages.h" #include "apiwrap.h" +#include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_user.h" #include "data/data_saved_sublist.h" @@ -47,7 +48,15 @@ SavedMessages::SavedMessages( } } -SavedMessages::~SavedMessages() = default; +SavedMessages::~SavedMessages() { + auto &changes = session().changes(); + for (const auto &[peer, sublist] : _sublists) { + _owningHistory->setForwardDraft(MsgId(), peer->id, {}); + + const auto raw = sublist.get(); + changes.entryRemoved(raw); + } +} bool SavedMessages::supported() const { return !_unsupported; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 64a02b2d9d..e4e420a75e 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "data/data_changes.h" #include "data/data_channel.h" +#include "data/data_drafts.h" #include "data/data_histories.h" #include "data/data_messages.h" #include "data/data_peer.h" @@ -695,15 +696,18 @@ void SavedSublist::subscribeToUnreadChanges() { void SavedSublist::applyMonoforumDialog( const MTPDmonoForumDialog &data, not_null<HistoryItem*> topItem) { - //if (const auto draft = data.vdraft()) { // #TODO monoforum - // draft->match([&](const MTPDdraftMessage &data) { - // Data::ApplyPeerCloudDraft( - // &session(), - // channel()->id, - // _rootId, - // data); - // }, [](const MTPDdraftMessageEmpty&) {}); - //} + if (const auto parent = parentChat()) { + if (const auto draft = data.vdraft()) { + draft->match([&](const MTPDdraftMessage &data) { + Data::ApplyPeerCloudDraft( + &session(), + parent->id, + MsgId(), + sublistPeer()->id, + data); + }, [](const MTPDdraftMessageEmpty&) {}); + } + } setInboxReadTill( data.vread_inbox_max_id().v, @@ -712,6 +716,18 @@ void SavedSublist::applyMonoforumDialog( applyMaybeLast(topItem); } +TimeId SavedSublist::adjustedChatListTimeId() const { + const auto result = chatListTimeId(); + const auto monoforumPeerId = sublistPeer()->id; + const auto history = _parent->owningHistory(); + if (const auto draft = history->cloudDraft(MsgId(), monoforumPeerId)) { + if (!Data::DraftIsNull(draft) && !session().supportMode()) { + return std::max(result, draft->date); + } + } + return result; +} + rpl::producer<> SavedSublist::changes() const { return _listChanges.events(); } diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 468e4b647d..0708e1a7a0 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -72,6 +72,8 @@ public: void readTillEnd(); void requestChatListMessage(); + TimeId adjustedChatListTimeId() const override; + int fixedOnTopIndex() const override; bool shouldBeInChatList() const override; Dialogs::UnreadState chatListUnreadState() const override; diff --git a/Telegram/SourceFiles/data/data_thread.cpp b/Telegram/SourceFiles/data/data_thread.cpp index 67a346f7e2..dcf9b85f51 100644 --- a/Telegram/SourceFiles/data/data_thread.cpp +++ b/Telegram/SourceFiles/data/data_thread.cpp @@ -32,6 +32,13 @@ MsgId Thread::topicRootId() const { return MsgId(); } +PeerId Thread::monoforumPeerId() const { + if (const auto sublist = asSublist()) { + return sublist->sublistPeer()->id; + } + return PeerId(); +} + PeerData *Thread::maybeSublistPeer() const { if (const auto sublist = asSublist()) { return sublist->sublistPeer(); diff --git a/Telegram/SourceFiles/data/data_thread.h b/Telegram/SourceFiles/data/data_thread.h index 10eb0d27ee..74462a1944 100644 --- a/Telegram/SourceFiles/data/data_thread.h +++ b/Telegram/SourceFiles/data/data_thread.h @@ -67,6 +67,7 @@ public: return const_cast<Thread*>(this)->owningHistory(); } [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] PeerId monoforumPeerId() const; [[nodiscard]] PeerData *maybeSublistPeer() const; [[nodiscard]] not_null<PeerData*> peer() const; [[nodiscard]] PeerNotifySettings ¬ify(); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 9f0562e1b7..c1ed9c42f7 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -382,8 +382,6 @@ struct ForwardDraft { const ForwardDraft&) = default; }; -using ForwardDrafts = base::flat_map<MsgId, ForwardDraft>; - struct ResolvedForwardDraft { HistoryItemsList items; ForwardOptions options = ForwardOptions::PreserveInfo; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 1cd8a1e353..f2d58e995e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -962,10 +962,12 @@ void RowPainter::Paint( if (!thread) { return nullptr; } - if ((!peer || !peer->isForum()) && (!item || !badgesState.unread)) { + if ((!peer || (!peer->isForum() && !peer->amMonoforumAdmin())) + && (!item || !badgesState.unread)) { // Draw item, if there are unread messages. const auto draft = thread->owningHistory()->cloudDraft( - thread->topicRootId()); + thread->topicRootId(), + thread->monoforumPeerId()); if (!Data::DraftIsNull(draft)) { return draft; } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index a67e60302b..3c40fbe973 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -206,33 +206,39 @@ void History::itemVanished(not_null<HistoryItem*> item) { void History::takeLocalDraft(not_null<History*> from) { const auto topicRootId = MsgId(0); - const auto i = from->_drafts.find(Data::DraftKey::Local(topicRootId)); + const auto monoforumPeerId = PeerId(0); + const auto i = from->_drafts.find( + Data::DraftKey::Local(topicRootId, monoforumPeerId)); if (i == end(from->_drafts)) { return; } auto &draft = i->second; if (!draft->textWithTags.text.isEmpty() - && !_drafts.contains(Data::DraftKey::Local(topicRootId))) { + && !_drafts.contains( + Data::DraftKey::Local(topicRootId, monoforumPeerId))) { // Edit and reply to drafts can't migrate. // Cloud drafts do not migrate automatically. draft->reply = FullReplyTo(); setLocalDraft(std::move(draft)); } - from->clearLocalDraft(topicRootId); + from->clearLocalDraft(topicRootId, monoforumPeerId); session().api().saveDraftToCloudDelayed(from); } -void History::createLocalDraftFromCloud(MsgId topicRootId) { - const auto draft = cloudDraft(topicRootId); +void History::createLocalDraftFromCloud( + MsgId topicRootId, + PeerId monoforumPeerId) { + const auto draft = cloudDraft(topicRootId, monoforumPeerId); if (!draft) { - clearLocalDraft(topicRootId); + clearLocalDraft(topicRootId, monoforumPeerId); return; } else if (Data::DraftIsNull(draft) || !draft->date) { return; } draft->reply.topicRootId = topicRootId; - auto existing = localDraft(topicRootId); + draft->reply.monoforumPeerId = monoforumPeerId; + auto existing = localDraft(topicRootId, monoforumPeerId); if (Data::DraftIsNull(existing) || !existing->date || draft->date >= existing->date) { @@ -242,7 +248,7 @@ void History::createLocalDraftFromCloud(MsgId topicRootId) { draft->reply, draft->cursor, draft->webpage)); - existing = localDraft(topicRootId); + existing = localDraft(topicRootId, monoforumPeerId); } else if (existing != draft) { existing->textWithTags = draft->textWithTags; existing->reply = draft->reply; @@ -268,7 +274,7 @@ void History::setDraft( return; } const auto cloudThread = key.isCloud() - ? threadFor(key.topicRootId()) + ? threadFor(key.topicRootId(), key.monoforumPeerId()) : nullptr; if (cloudThread) { cloudThread->cloudDraftTextCache().clear(); @@ -298,7 +304,7 @@ void History::clearDraft(Data::DraftKey key) { void History::clearDrafts() { for (auto &[key, draft] : base::take(_drafts)) { const auto cloudThread = key.isCloud() - ? threadFor(key.topicRootId()) + ? threadFor(key.topicRootId(), key.monoforumPeerId()) : nullptr; if (cloudThread) { cloudThread->cloudDraftTextCache().clear(); @@ -309,25 +315,30 @@ void History::clearDrafts() { Data::Draft *History::createCloudDraft( MsgId topicRootId, + PeerId monoforumPeerId, const Data::Draft *fromDraft) { if (Data::DraftIsNull(fromDraft)) { setCloudDraft(std::make_unique<Data::Draft>( TextWithTags(), - FullReplyTo{ .topicRootId = topicRootId }, + FullReplyTo{ + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }, MessageCursor(), Data::WebPageDraft())); - cloudDraft(topicRootId)->date = TimeId(0); + cloudDraft(topicRootId, monoforumPeerId)->date = TimeId(0); } else { - auto existing = cloudDraft(topicRootId); + auto existing = cloudDraft(topicRootId, monoforumPeerId); if (!existing) { auto reply = fromDraft->reply; reply.topicRootId = topicRootId; + reply.monoforumPeerId = monoforumPeerId; setCloudDraft(std::make_unique<Data::Draft>( fromDraft->textWithTags, reply, fromDraft->cursor, fromDraft->webpage)); - existing = cloudDraft(topicRootId); + existing = cloudDraft(topicRootId, monoforumPeerId); } else if (existing != fromDraft) { existing->textWithTags = fromDraft->textWithTags; existing->reply = fromDraft->reply; @@ -336,44 +347,56 @@ Data::Draft *History::createCloudDraft( } existing->date = base::unixtime::now(); existing->reply.topicRootId = topicRootId; + existing->reply.monoforumPeerId = monoforumPeerId; } - if (const auto thread = threadFor(topicRootId)) { + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->cloudDraftTextCache().clear(); thread->updateChatListSortPosition(); } - return cloudDraft(topicRootId); + return cloudDraft(topicRootId, monoforumPeerId); } -bool History::skipCloudDraftUpdate(MsgId topicRootId, TimeId date) const { - const auto i = _acceptCloudDraftsAfter.find(topicRootId); - return _savingCloudDraftRequests.contains(topicRootId) +bool History::skipCloudDraftUpdate( + MsgId topicRootId, + PeerId monoforumPeerId, + TimeId date) const { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); + const auto i = _acceptCloudDraftsAfter.find(key); + return _savingCloudDraftRequests.contains(key) || (i != _acceptCloudDraftsAfter.end() && date < i->second); } -void History::startSavingCloudDraft(MsgId topicRootId) { - ++_savingCloudDraftRequests[topicRootId]; +void History::startSavingCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); + ++_savingCloudDraftRequests[key]; } -void History::finishSavingCloudDraft(MsgId topicRootId, TimeId savedAt) { - const auto i = _savingCloudDraftRequests.find(topicRootId); +void History::finishSavingCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId, + TimeId savedAt) { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); + const auto i = _savingCloudDraftRequests.find(key); if (i != _savingCloudDraftRequests.end()) { if (--i->second <= 0) { _savingCloudDraftRequests.erase(i); } } - auto &after = _acceptCloudDraftsAfter[topicRootId]; + auto &after = _acceptCloudDraftsAfter[key]; after = std::max(after, savedAt + kSkipCloudDraftsFor); } -void History::applyCloudDraft(MsgId topicRootId) { +void History::applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId) { if (!topicRootId && session().supportMode()) { updateChatListEntry(); session().supportHelper().cloudDraftChanged(this); } else { - createLocalDraftFromCloud(topicRootId); - if (const auto thread = threadFor(topicRootId)) { + createLocalDraftFromCloud(topicRootId, monoforumPeerId); + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->updateChatListSortPosition(); if (!topicRootId) { session().changes().historyUpdated( @@ -388,17 +411,19 @@ void History::applyCloudDraft(MsgId topicRootId) { } } -void History::draftSavedToCloud(MsgId topicRootId) { - if (const auto thread = threadFor(topicRootId)) { +void History::draftSavedToCloud(MsgId topicRootId, PeerId monoforumPeerId) { + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->updateChatListEntry(); } session().local().writeDrafts(this); } const Data::ForwardDraft &History::forwardDraft( - MsgId topicRootId) const { + MsgId topicRootId, + PeerId monoforumPeerId) const { + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); static const auto kEmpty = Data::ForwardDraft(); - const auto i = _forwardDrafts.find(topicRootId); + const auto i = _forwardDrafts.find(key); return (i != end(_forwardDrafts)) ? i->second : kEmpty; } @@ -411,11 +436,12 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( } Data::ResolvedForwardDraft History::resolveForwardDraft( - MsgId topicRootId) { - const auto &draft = forwardDraft(topicRootId); + MsgId topicRootId, + PeerId monoforumPeerId) { + const auto &draft = forwardDraft(topicRootId, monoforumPeerId); auto result = resolveForwardDraft(draft); if (result.items.size() != draft.ids.size()) { - setForwardDraft(topicRootId, { + setForwardDraft(topicRootId, monoforumPeerId, { .ids = owner().itemsToIds(result.items), .options = result.options, }); @@ -425,24 +451,23 @@ Data::ResolvedForwardDraft History::resolveForwardDraft( void History::setForwardDraft( MsgId topicRootId, + PeerId monoforumPeerId, Data::ForwardDraft &&draft) { auto changed = false; + const auto key = Data::DraftKey::Local(topicRootId, monoforumPeerId); if (draft.ids.empty()) { - changed = _forwardDrafts.remove(topicRootId); + changed = _forwardDrafts.remove(key); } else { - auto &now = _forwardDrafts[topicRootId]; + auto &now = _forwardDrafts[key]; if (now != draft) { now = std::move(draft); changed = true; } } if (changed) { - const auto entry = topicRootId - ? peer->forumTopicFor(topicRootId) - : (Dialogs::Entry*)this; - if (entry) { + if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { session().changes().entryUpdated( - entry, + thread, Data::EntryUpdate::Flag::ForwardDraft); } } @@ -2081,7 +2106,7 @@ void History::applyPinnedUpdate(const MTPDupdateDialogPinned &data) { TimeId History::adjustedChatListTimeId() const { const auto result = chatListTimeId(); - if (const auto draft = cloudDraft(MsgId(0))) { + if (const auto draft = cloudDraft(MsgId(), PeerId())) { if (!peer->forum() && !Data::DraftIsNull(draft) && !session().supportMode()) { @@ -2871,7 +2896,8 @@ void History::applyDialog( Data::ApplyPeerCloudDraft( &session(), peer->id, - MsgId(0), // topicRootId + MsgId(), // topicRootId + PeerId(), // monoforumPeerId draft->c_draftMessage()); } if (const auto ttl = data.vttl_period()) { @@ -3101,14 +3127,22 @@ void History::forceFullResize() { _flags |= Flag::HasPendingResizedItems; } -Data::Thread *History::threadFor(MsgId topicRootId) { +Data::Thread *History::threadFor(MsgId topicRootId, PeerId monoforumPeerId) { return topicRootId ? peer->forumTopicFor(topicRootId) - : static_cast<Data::Thread*>(this); + : !monoforumPeerId + ? static_cast<Data::Thread*>(this) + : peer->monoforum() + ? peer->monoforum()->sublistLoaded(owner().peer(monoforumPeerId)) + : nullptr; } -const Data::Thread *History::threadFor(MsgId topicRootId) const { - return const_cast<History*>(this)->threadFor(topicRootId); +const Data::Thread *History::threadFor( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return const_cast<History*>(this)->threadFor( + topicRootId, + monoforumPeerId); } void History::forumChanged(Data::Forum *old) { @@ -3137,7 +3171,7 @@ void History::forumChanged(Data::Forum *old) { } else { _flags &= ~Flag::IsForum; } - if (cloudDraft(MsgId(0))) { + if (cloudDraft(MsgId(), PeerId())) { updateChatListSortPosition(); } _flags |= Flag::PendingAllItemsResize; @@ -3173,7 +3207,7 @@ void History::monoforumChanged(Data::SavedMessages *old) { } else { _flags &= ~Flag::IsMonoforumAdmin; } - if (cloudDraft(MsgId(0))) { + if (cloudDraft(MsgId(), PeerId())) { updateChatListSortPosition(); } _flags |= Flag::PendingAllItemsResize; diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 16fe57e351..4a009a4d20 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -62,8 +62,12 @@ public: [[nodiscard]] not_null<History*> owningHistory() override { return this; } - [[nodiscard]] Data::Thread *threadFor(MsgId topicRootId); - [[nodiscard]] const Data::Thread *threadFor(MsgId topicRootId) const; + [[nodiscard]] Data::Thread *threadFor( + MsgId topicRootId, + PeerId monoforumPeerId); + [[nodiscard]] const Data::Thread *threadFor( + MsgId topicRootId, + PeerId monoforumPeerId) const; [[nodiscard]] auto delegateMixin() const -> not_null<HistoryMainElementDelegateMixin*> { @@ -288,60 +292,89 @@ public: [[nodiscard]] const Data::HistoryDrafts &draftsMap() const; void setDraftsMap(Data::HistoryDrafts &&map); - Data::Draft *localDraft(MsgId topicRootId) const { - return draft(Data::DraftKey::Local(topicRootId)); + Data::Draft *localDraft( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return draft(Data::DraftKey::Local(topicRootId, monoforumPeerId)); } - Data::Draft *localEditDraft(MsgId topicRootId) const { - return draft(Data::DraftKey::LocalEdit(topicRootId)); + Data::Draft *localEditDraft( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return draft( + Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId)); } - Data::Draft *cloudDraft(MsgId topicRootId) const { - return draft(Data::DraftKey::Cloud(topicRootId)); + Data::Draft *cloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId) const { + return draft(Data::DraftKey::Cloud(topicRootId, monoforumPeerId)); } void setLocalDraft(std::unique_ptr<Data::Draft> &&draft) { setDraft( - Data::DraftKey::Local(draft->reply.topicRootId), + Data::DraftKey::Local( + draft->reply.topicRootId, + draft->reply.monoforumPeerId), std::move(draft)); } void setLocalEditDraft(std::unique_ptr<Data::Draft> &&draft) { setDraft( - Data::DraftKey::LocalEdit(draft->reply.topicRootId), + Data::DraftKey::LocalEdit( + draft->reply.topicRootId, + draft->reply.monoforumPeerId), std::move(draft)); } void setCloudDraft(std::unique_ptr<Data::Draft> &&draft) { setDraft( - Data::DraftKey::Cloud(draft->reply.topicRootId), + Data::DraftKey::Cloud( + draft->reply.topicRootId, + draft->reply.monoforumPeerId), std::move(draft)); } - void clearLocalDraft(MsgId topicRootId) { - clearDraft(Data::DraftKey::Local(topicRootId)); + void clearLocalDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + clearDraft(Data::DraftKey::Local(topicRootId, monoforumPeerId)); } - void clearCloudDraft(MsgId topicRootId) { - clearDraft(Data::DraftKey::Cloud(topicRootId)); + void clearCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + clearDraft(Data::DraftKey::Cloud(topicRootId, monoforumPeerId)); } - void clearLocalEditDraft(MsgId topicRootId) { - clearDraft(Data::DraftKey::LocalEdit(topicRootId)); + void clearLocalEditDraft( + MsgId topicRootId, + PeerId monoforumPeerId) { + clearDraft(Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId)); } void clearDrafts(); Data::Draft *createCloudDraft( MsgId topicRootId, + PeerId monoforumPeerId, const Data::Draft *fromDraft); [[nodiscard]] bool skipCloudDraftUpdate( MsgId topicRootId, + PeerId monoforumPeerId, TimeId date) const; - void startSavingCloudDraft(MsgId topicRootId); - void finishSavingCloudDraft(MsgId topicRootId, TimeId savedAt); + void startSavingCloudDraft(MsgId topicRootId, PeerId monoforumPeerId); + void finishSavingCloudDraft( + MsgId topicRootId, + PeerId monoforumPeerId, + TimeId savedAt); void takeLocalDraft(not_null<History*> from); - void applyCloudDraft(MsgId topicRootId); - void draftSavedToCloud(MsgId topicRootId); + void applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId); + void draftSavedToCloud(MsgId topicRootId, PeerId monoforumPeerId); void requestChatListMessage(); [[nodiscard]] const Data::ForwardDraft &forwardDraft( - MsgId topicRootId) const; + MsgId topicRootId, + PeerId monoforumPeerId) const; [[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft( const Data::ForwardDraft &draft) const; [[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft( - MsgId topicRootId); - void setForwardDraft(MsgId topicRootId, Data::ForwardDraft &&draft); + MsgId topicRootId, + PeerId monoforumPeerId); + void setForwardDraft( + MsgId topicRootId, + PeerId monoforumPeerId, + Data::ForwardDraft &&draft); History *migrateSibling() const; [[nodiscard]] bool useTopPromotion() const; @@ -548,7 +581,9 @@ private: void viewReplaced(not_null<const Element*> was, Element *now); - void createLocalDraftFromCloud(MsgId topicRootId); + void createLocalDraftFromCloud( + MsgId topicRootId, + PeerId monoforumPeerId); HistoryItem *insertJoinedMessage(); void insertMessageToBlocks(not_null<HistoryItem*> item); @@ -606,9 +641,9 @@ private: std::unique_ptr<HistoryTranslation> _translation; Data::HistoryDrafts _drafts; - base::flat_map<MsgId, TimeId> _acceptCloudDraftsAfter; - base::flat_map<MsgId, int> _savingCloudDraftRequests; - Data::ForwardDrafts _forwardDrafts; + base::flat_map<Data::DraftKey, TimeId> _acceptCloudDraftsAfter; + base::flat_map<Data::DraftKey, int> _savingCloudDraftRequests; + base::flat_map<Data::DraftKey, Data::ForwardDraft> _forwardDrafts; QString _topPromotedMessage; QString _topPromotedType; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index c66d5cbb86..4007594a05 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1118,7 +1118,7 @@ void HistoryWidget::initVoiceRecordBar() { }); const auto applyLocalDraft = [=] { - if (_history && _history->localDraft({})) { + if (_history && _history->localDraft(MsgId(), PeerId())) { applyDraft(); } }; @@ -1874,12 +1874,14 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { } const auto topicRootId = MsgId(); + const auto monoforumPeerId = PeerId(); if (_editMsgId) { _history->setLocalEditDraft(std::make_unique<Data::Draft>( _field, FullReplyTo{ .messageId = FullMsgId(_history->peer->id, _editMsgId), .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, }, _preview->draft(), _saveEditMsgRequestId)); @@ -1890,9 +1892,9 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { _replyTo, _preview->draft())); } else { - _history->clearLocalDraft(topicRootId); + _history->clearLocalDraft(topicRootId, monoforumPeerId); } - _history->clearLocalEditDraft(topicRootId); + _history->clearLocalEditDraft(topicRootId, monoforumPeerId); } } @@ -2187,11 +2189,13 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { } }); - const auto editDraft = _history ? _history->localEditDraft({}) : nullptr; + const auto editDraft = _history + ? _history->localEditDraft(MsgId(), PeerId()) + : nullptr; const auto draft = editDraft ? editDraft : _history - ? _history->localDraft({}) + ? _history->localDraft(MsgId(), PeerId()) : nullptr; auto fieldAvailable = canWriteMessage(); const auto editMsgId = editDraft ? editDraft->reply.messageId.msg : 0; @@ -2241,7 +2245,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { requestMessageData(_editMsgId); } } else { - const auto draft = _history->localDraft({}); + const auto draft = _history->localDraft(MsgId(), PeerId()); _processingReplyTo = draft ? draft->reply : FullReplyTo(); if (_processingReplyTo) { _processingReplyItem = session().data().message( @@ -2408,7 +2412,7 @@ void HistoryWidget::showHistory( info->inlineReturnTo = wasState; } sendBotStartCommand(); - _history->clearLocalDraft({}); + _history->clearLocalDraft(MsgId(), PeerId()); applyDraft(); _send->finishAnimating(); } @@ -2864,10 +2868,10 @@ void HistoryWidget::unregisterDraftSources() { } session().local().unregisterDraftSource( _history, - Data::DraftKey::Local({})); + Data::DraftKey::Local(MsgId(), PeerId())); session().local().unregisterDraftSource( _history, - Data::DraftKey::LocalEdit({})); + Data::DraftKey::LocalEdit(MsgId(), PeerId())); } void HistoryWidget::registerDraftSource() { @@ -2892,8 +2896,8 @@ void HistoryWidget::registerDraftSource() { session().local().registerDraftSource( _history, (editMsgId - ? Data::DraftKey::LocalEdit({}) - : Data::DraftKey::Local({})), + ? Data::DraftKey::LocalEdit(MsgId(), PeerId()) + : Data::DraftKey::Local(MsgId(), PeerId())), std::move(draftSource)); } @@ -3630,6 +3634,7 @@ void HistoryWidget::unreadCountUpdated() { }); } else { const auto hideCounter = _history->isForum() + || _history->amMonoforumAdmin() || !_history->trackUnreadMessages(); _cornerButtons.updateJumpDownVisibility(hideCounter ? 0 @@ -4376,16 +4381,16 @@ void HistoryWidget::saveEditMsg() { cancelEdit(); } })(); - if (const auto editDraft = history->localEditDraft({})) { + if (const auto editDraft = history->localEditDraft({}, {})) { if (editDraft->saveRequestId == requestId) { - history->clearLocalEditDraft({}); + history->clearLocalEditDraft(MsgId(), PeerId()); history->session().local().writeDrafts(history); } } }; const auto fail = [=](const QString &error, mtpRequestId requestId) { - if (const auto editDraft = history->localEditDraft({})) { + if (const auto editDraft = history->localEditDraft({}, {})) { if (editDraft->saveRequestId == requestId) { editDraft->saveRequestId = 0; } @@ -7276,7 +7281,7 @@ void HistoryWidget::editDraftOptions() { } else { cancelReply(); } - history->setForwardDraft({}, std::move(forward)); + history->setForwardDraft(MsgId(), PeerId(), std::move(forward)); _preview->apply(webpage); }; const auto replyToId = reply.messageId; @@ -7295,7 +7300,9 @@ void HistoryWidget::editDraftOptions() { .resolver = _preview->resolver(), .done = done, .highlight = highlight, - .clearOldDraft = [=] { ClearDraftReplyTo(history, 0, replyToId); }, + .clearOldDraft = [=] { + ClearDraftReplyTo(history, MsgId(), PeerId(), replyToId); + }, }); } @@ -8418,7 +8425,7 @@ void HistoryWidget::setReplyFieldsFromProcessing() { const auto id = base::take(_processingReplyTo); const auto item = base::take(_processingReplyItem); if (_editMsgId) { - if (const auto localDraft = _history->localDraft({})) { + if (const auto localDraft = _history->localDraft({}, {})) { localDraft->reply = id; } else { _history->setLocalDraft(std::make_unique<Data::Draft>( @@ -8470,7 +8477,7 @@ void HistoryWidget::editMessage( _replyTo, _preview->draft())); } else { - _history->clearLocalDraft({}); + _history->clearLocalDraft(MsgId(), PeerId()); } } @@ -8580,10 +8587,10 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { updateControlsGeometry(); update(); } else if (const auto localDraft - = (_history ? _history->localDraft({}) : nullptr)) { + = (_history ? _history->localDraft({}, {}) : nullptr)) { if (localDraft->reply) { if (localDraft->textWithTags.text.isEmpty()) { - _history->clearLocalDraft({}); + _history->clearLocalDraft(MsgId(), PeerId()); } else { localDraft->reply = {}; } @@ -8629,7 +8636,7 @@ void HistoryWidget::cancelEdit() { updateReplaceMediaButton(); _replyEditMsg = nullptr; setEditMsgId(0); - _history->clearLocalEditDraft({}); + _history->clearLocalEditDraft(MsgId(), PeerId()); applyDraft(); if (_saveEditMsgRequestId) { @@ -8671,7 +8678,7 @@ void HistoryWidget::cancelFieldAreaState() { } else if (_replyTo) { cancelReply(); } else if (readyToForward()) { - _history->setForwardDraft(MsgId(), {}); + _history->setForwardDraft(MsgId(), PeerId(), {}); } else if (_kbReplyTo) { toggleKeyboard(); } @@ -9039,7 +9046,7 @@ void HistoryWidget::updateReplyEditTexts(bool force) { void HistoryWidget::updateForwarding() { _forwardPanel->update(_history, _history - ? _history->resolveForwardDraft(MsgId()) + ? _history->resolveForwardDraft(MsgId(), PeerId()) : Data::ResolvedForwardDraft()); updateControlsVisibility(); updateControlsGeometry(); diff --git a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h index ea4bcb3178..3e76145f2e 100644 --- a/Telegram/SourceFiles/history/view/controls/compose_controls_common.h +++ b/Telegram/SourceFiles/history/view/controls/compose_controls_common.h @@ -66,6 +66,7 @@ struct WriteRestriction { struct SetHistoryArgs { required<History*> history; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; Fn<bool()> showSlowmodeError; Fn<Api::SendAction()> sendActionFactory; rpl::producer<int> slowmodeSecondsLeft; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 5e4f936f1f..e0b3256f4d 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_drafts.h" #include "data/data_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_chat.h" @@ -197,6 +198,7 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; Preview _preview; rpl::event_stream<> _editCancelled; @@ -254,6 +256,7 @@ FieldHeader::FieldHeader( void FieldHeader::setHistory(const SetHistoryArgs &args) { _history = *args.history; _topicRootId = args.topicRootId; + _monoforumPeerId = args.monoforumPeerId; } void FieldHeader::updateTopicRootId(MsgId topicRootId) { @@ -282,7 +285,7 @@ void FieldHeader::init() { st::historyLinkIcon.paint(p, position, width()); } else if (isEditingMessage()) { st::historyEditIcon.paint(p, position, width()); - } else if (const auto reply = replyingToMessage()) { + } else if (const auto reply = replyingToMessage(); reply.replying()) { if (!reply.quote.empty()) { st::historyQuoteIcon.paint(p, position, width()); } else { @@ -760,6 +763,7 @@ void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { } void FieldHeader::replyToMessage(FullReplyTo id) { + id.monoforumPeerId = 0; _replyTo = id; } @@ -956,6 +960,7 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { unregisterDraftSources(); _history = history; _topicRootId = args.topicRootId; + _monoforumPeerId = args.monoforumPeerId; _historyLifetime.destroy(); _header->setHistory(args); registerDraftSource(); @@ -999,6 +1004,7 @@ void ComposeControls::setCurrentDialogsEntryState( Dialogs::EntryState state) { unregisterDraftSources(); state.currentReplyTo.topicRootId = _topicRootId; + state.currentReplyTo.monoforumPeerId = _monoforumPeerId; _currentDialogsEntryState = state; updateForwarding(); registerDraftSource(); @@ -1405,6 +1411,7 @@ void ComposeControls::init() { ) | rpl::start_with_next([=] { const auto history = _history; const auto topicRootId = _topicRootId; + const auto monoforumPeerId = _monoforumPeerId; const auto reply = _header->replyingToMessage(); const auto webpage = _preview->draft(); @@ -1417,7 +1424,10 @@ void ComposeControls::init() { } else { cancelReplyMessage(); } - history->setForwardDraft(topicRootId, std::move(forward)); + history->setForwardDraft( + topicRootId, + monoforumPeerId, + std::move(forward)); _preview->apply(webpage); _field->setFocus(); }; @@ -1440,6 +1450,7 @@ void ComposeControls::init() { .clearOldDraft = [=] { ClearDraftReplyTo( history, topicRootId, + monoforumPeerId, replyToId); }, }); }, _wrap->lifetime()); @@ -1809,8 +1820,8 @@ Data::DraftKey ComposeControls::draftKey(DraftType type) const { case Section::Replies: case Section::SavedSublist: return (type == DraftType::Edit) - ? Key::LocalEdit(_topicRootId) - : Key::Local(_topicRootId); + ? Key::LocalEdit(_topicRootId, _monoforumPeerId) + : Key::Local(_topicRootId, _monoforumPeerId); case Section::Scheduled: return (type == DraftType::Edit) ? Key::ScheduledEdit() @@ -2038,7 +2049,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } void ComposeControls::cancelForward() { - _history->setForwardDraft(_topicRootId, {}); + _history->setForwardDraft(_topicRootId, _monoforumPeerId, {}); updateForwarding(); } @@ -2902,9 +2913,11 @@ void ComposeControls::toggleTabbedSelectorMode() { && !_regularWindow->adaptive().isOneColumn()) { Core::App().settings().setTabbedSelectorSectionEnabled(true); Core::App().saveSettingsDelayed(); - const auto topic = _history->peer->forumTopicFor(_topicRootId); + const auto thread = _history->threadFor( + _topicRootId, + _monoforumPeerId); pushTabbedSelectorToThirdSection( - (topic ? topic : (Data::Thread*)_history), + thread ? thread : _history, Window::SectionShow::Way::ClearStack); } else { _tabbedPanel->toggleAnimated(); @@ -2958,6 +2971,7 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) { FullReplyTo{ .messageId = item->fullId(), .topicRootId = key.topicRootId(), + .monoforumPeerId = key.monoforumPeerId(), }, cursor, Data::WebPageDraft::FromItem(item))); @@ -3038,6 +3052,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { Expects(draftKeyCurrent() != Data::DraftKey::None()); id.topicRootId = _topicRootId; + id.monoforumPeerId = _monoforumPeerId; if (!id) { cancelReplyMessage(); return; @@ -3045,6 +3060,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { if (isEditingMessage()) { const auto key = draftKey(DraftType::Normal); Assert(key.topicRootId() == id.topicRootId); + Assert(key.monoforumPeerId() == id.monoforumPeerId); if (const auto localDraft = _history->draft(key)) { localDraft->reply = id; } else { @@ -3088,12 +3104,11 @@ void ComposeControls::cancelReplyMessage() { } void ComposeControls::updateForwarding() { - const auto rootId = _topicRootId; - const auto thread = (_history && rootId) - ? _history->peer->forumTopicFor(rootId) + const auto thread = (_history && (_topicRootId || _monoforumPeerId)) + ? _history->threadFor(_topicRootId, _monoforumPeerId) : (Data::Thread*)_history; _header->updateForwarding(thread, thread - ? _history->resolveForwardDraft(rootId) + ? _history->resolveForwardDraft(_topicRootId, _monoforumPeerId) : Data::ResolvedForwardDraft()); updateSendButtonType(); } @@ -3108,7 +3123,7 @@ bool ComposeControls::handleCancelRequest() { } else if (isEditingMessage()) { maybeCancelEditMessage(); return true; - } else if (replyingToMessage()) { + } else if (replyingToMessage().replying()) { cancelReplyMessage(); return true; } else if (readyToForward()) { @@ -3186,6 +3201,11 @@ void ComposeControls::initForwardProcess() { && topic->rootId() == _topicRootId) { updateForwarding(); } + } else if (const auto sublist = update.entry->asSublist()) { + if (sublist->owningHistory() == _history + && sublist->sublistPeer()->id == _monoforumPeerId) { + updateForwarding(); + } } }, _wrap->lifetime()); @@ -3209,6 +3229,7 @@ bool ComposeControls::isEditingMessage() const { FullReplyTo ComposeControls::replyingToMessage() const { auto result = _header->replyingToMessage(); result.topicRootId = _topicRootId; + result.monoforumPeerId = _monoforumPeerId; return result; } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 2b97c4f985..cd391e0078 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -365,6 +365,7 @@ private: History *_history = nullptr; MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; BusinessShortcutId _shortcutId = 0; Fn<bool()> _showSlowmodeError; Fn<Api::SendAction()> _sendActionFactory; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 549d252331..1277052948 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -1375,18 +1375,20 @@ void ShowReplyToChatBox( auto chosen = [=](not_null<Data::Thread*> thread) mutable { const auto history = thread->owningHistory(); const auto topicRootId = thread->topicRootId(); - const auto draft = history->localDraft(topicRootId); + const auto monoforumPeerId = thread->monoforumPeerId(); + const auto draft = history->localDraft(topicRootId, monoforumPeerId); const auto textWithTags = draft ? draft->textWithTags : TextWithTags(); const auto cursor = draft ? draft->cursor : MessageCursor(); reply.topicRootId = topicRootId; + reply.monoforumPeerId = monoforumPeerId; history->setLocalDraft(std::make_unique<Data::Draft>( textWithTags, reply, cursor, Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( thread, Data::EntryUpdate::Flag::LocalDraftSet); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index 44e059ff34..ff314cdf9e 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/history_item_components.h" #include "history/view/history_view_item_preview.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_forum_topic.h" @@ -74,6 +75,11 @@ void ForwardPanel::update( ) | rpl::start_with_next([=] { update(nullptr, {}); }, _dataLifetime); + } else if (const auto sublist = _to->asSublist()) { + sublist->destroyed( + ) | rpl::start_with_next([=] { + update(nullptr, {}); + }, _dataLifetime); } updateTexts(); @@ -231,8 +237,10 @@ void ForwardPanel::applyOptions(Data::ForwardOptions options) { if (_data.items.empty()) { return; } else if (_data.options != options) { + const auto topicRootId = _to->topicRootId(); + const auto monoforumPeerId = _to->monoforumPeerId(); _data.options = options; - _to->owningHistory()->setForwardDraft(_to->topicRootId(), { + _to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, { .ids = _to->owner().itemsToIds(_data.items), .options = options, }); @@ -256,7 +264,9 @@ void ForwardPanel::editToNextOption() { ? Options::NoNamesAndCaptions : Options::PreserveInfo; - _to->owningHistory()->setForwardDraft(_to->topicRootId(), { + const auto topicRootId = _to->topicRootId(); + const auto monoforumPeerId = _to->monoforumPeerId(); + _to->owningHistory()->setForwardDraft(topicRootId, monoforumPeerId, { .ids = _to->owner().itemsToIds(_data.items), .options = next, }); @@ -332,20 +342,25 @@ void ForwardPanel::paint( void ClearDraftReplyTo( not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, FullMsgId equalTo) { - const auto local = history->localDraft(topicRootId); + const auto local = history->localDraft(topicRootId, monoforumPeerId); if (!local || (equalTo && local->reply.messageId != equalTo)) { return; } auto draft = *local; - draft.reply = { .topicRootId = topicRootId }; + draft.reply = { + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }; if (Data::DraftIsNull(&draft)) { - history->clearLocalDraft(topicRootId); + history->clearLocalDraft(topicRootId, monoforumPeerId); } else { history->setLocalDraft( std::make_unique<Data::Draft>(std::move(draft))); } - if (const auto thread = history->threadFor(topicRootId)) { + const auto thread = history->threadFor(topicRootId, monoforumPeerId); + if (thread) { history->session().api().saveDraftToCloudDelayed(thread); } } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index 54624d47d8..2e8dcc36d5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -76,6 +76,7 @@ private: void ClearDraftReplyTo( not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, FullMsgId equalTo); void EditWebPageOptions( diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 0cd5a0fe00..7545a3e79f 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -438,8 +438,10 @@ ChatWidget::ChatWidget( ChatWidget::~ChatWidget() { base::take(_sendAction); - if (_repliesRootId) { + if (_repliesRootId || _sublist) { session().api().saveCurrentDraftToCloud(); + } + if (_repliesRootId) { controller()->sendingAnimation().clear(); } if (_topic) { @@ -747,7 +749,8 @@ void ChatWidget::setupComposeControls() { _composeControls->setHistory({ .history = _history.get(), - .topicRootId = _topic ? _topic->rootId() : MsgId(0), + .topicRootId = _topic ? _topic->rootId() : MsgId(), + .monoforumPeerId = _sublist ? _sublist->sublistPeer()->id : PeerId(), .showSlowmodeError = [=] { return showSlowmodeError(); }, .sendActionFactory = [=] { return prepareSendAction({}); }, .slowmodeSecondsLeft = SlowmodeSecondsLeft(_peer), diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index e0953d45ca..e2de892e69 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -557,6 +557,7 @@ bool MainWidget::setForwardDraft( const auto history = thread->owningHistory(); const auto items = session().data().idsToItems(draft.ids); const auto topicRootId = thread->topicRootId(); + const auto monoforumPeerId = thread->monoforumPeerId(); const auto error = GetErrorForSending( history->peer, { @@ -569,7 +570,7 @@ bool MainWidget::setForwardDraft( return false; } - history->setForwardDraft(topicRootId, std::move(draft)); + history->setForwardDraft(topicRootId, monoforumPeerId, std::move(draft)); _controller->showThread( thread, ShowAtUnreadMsgId, @@ -596,12 +597,16 @@ bool MainWidget::shareUrl( }; const auto history = thread->owningHistory(); const auto topicRootId = thread->topicRootId(); + const auto monoforumPeerId = thread->monoforumPeerId(); history->setLocalDraft(std::make_unique<Data::Draft>( textWithTags, - FullReplyTo{ .topicRootId = topicRootId }, + FullReplyTo{ + .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, + }, cursor, Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( thread, Data::EntryUpdate::Flag::LocalDraftSet); @@ -2044,6 +2049,8 @@ bool MainWidget::showBackFromStack(const SectionShow ¶ms) { }); return (_dialogs != nullptr); } + session().api().saveCurrentDraftToCloud(); + auto item = std::move(_stack.back()); _stack.pop_back(); if (const auto currentHistoryPeer = _history->peer()) { diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 339840de23..5d88879bfa 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -1176,7 +1176,9 @@ void EnumerateDrafts( } else if (key.isLocal() && (!supportMode || key.topicRootId())) { const auto i = map.find( - Data::DraftKey::Cloud(key.topicRootId())); + Data::DraftKey::Cloud( + key.topicRootId(), + key.monoforumPeerId())); const auto cloud = (i != end(map)) ? i->second.get() : nullptr; if (Data::DraftsAreEqual(draft.get(), cloud)) { continue; @@ -1426,7 +1428,7 @@ void Account::readDraftCursors(PeerId peerId, Data::HistoryDrafts &map) { ? Data::DraftKey::FromSerialized(keyValue) : keysOld ? Data::DraftKey::FromSerializedOld(keyValueOld) - : Data::DraftKey::Local(0); + : Data::DraftKey::Local(MsgId(), PeerId()); qint32 position = 0, anchor = 0, scroll = Ui::kQFixedMax; draft.stream >> position >> anchor >> scroll; if (const auto i = map.find(key); i != end(map)) { @@ -1453,13 +1455,14 @@ void Account::readDraftCursorsLegacy( return; } - if (const auto i = map.find(Data::DraftKey::Local({})); i != end(map)) { + if (const auto i = map.find(Data::DraftKey::Local(MsgId(), PeerId())) + ; i != end(map)) { i->second->cursor = MessageCursor( localPosition, localAnchor, localScroll); } - if (const auto i = map.find(Data::DraftKey::LocalEdit({})) + if (const auto i = map.find(Data::DraftKey::LocalEdit(MsgId(), PeerId())) ; i != end(map)) { i->second->cursor = MessageCursor( editPosition, @@ -1472,7 +1475,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) { const auto guard = gsl::finally([&] { if (const auto migrated = history->migrateFrom()) { readDraftsWithCursors(migrated); - migrated->clearLocalEditDraft({}); + migrated->clearLocalEditDraft(MsgId(), PeerId()); history->takeLocalDraft(migrated); } }); @@ -1643,10 +1646,11 @@ void Account::readDraftsWithCursorsLegacy( editData.text.size()); const auto topicRootId = MsgId(); + const auto monoforumPeerId = PeerId(); auto map = base::flat_map<Data::DraftKey, std::unique_ptr<Data::Draft>>(); if (!msgData.text.isEmpty() || msgReplyTo) { map.emplace( - Data::DraftKey::Local(topicRootId), + Data::DraftKey::Local(topicRootId, monoforumPeerId), std::make_unique<Data::Draft>( msgData, FullReplyTo{ FullMsgId(peerId, MsgId(msgReplyTo)) }, @@ -1657,7 +1661,7 @@ void Account::readDraftsWithCursorsLegacy( } if (editMsgId) { map.emplace( - Data::DraftKey::LocalEdit(topicRootId), + Data::DraftKey::LocalEdit(topicRootId, monoforumPeerId), std::make_unique<Data::Draft>( editData, FullReplyTo{ FullMsgId(peerId, editMsgId) }, diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 892bda6311..64e9ff8152 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -54,6 +54,7 @@ constexpr auto kOccupyFor = TimeId(60); constexpr auto kReoccupyEach = 30 * crl::time(1000); constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4; constexpr auto kTopicRootId = MsgId(0); +constexpr auto kMonoforumPeerId = PeerId(0); class EditInfoBox : public Ui::BoxContent { public: @@ -183,7 +184,7 @@ uint32 ParseOccupationTag(History *history) { if (!TrackHistoryOccupation(history)) { return 0; } - const auto draft = history->cloudDraft(kTopicRootId); + const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId); if (!draft) { return 0; } @@ -209,7 +210,7 @@ QString ParseOccupationName(History *history) { if (!TrackHistoryOccupation(history)) { return QString(); } - const auto draft = history->cloudDraft(kTopicRootId); + const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId); if (!draft) { return QString(); } @@ -235,7 +236,7 @@ TimeId OccupiedBySomeoneTill(History *history) { if (!TrackHistoryOccupation(history)) { return 0; } - const auto draft = history->cloudDraft(kTopicRootId); + const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId); if (!draft) { return 0; } @@ -353,7 +354,7 @@ void Helper::updateOccupiedHistory( not_null<Window::SessionController*> controller, History *history) { if (isOccupiedByMe(_occupiedHistory)) { - _occupiedHistory->clearCloudDraft(kTopicRootId); + _occupiedHistory->clearCloudDraft(kTopicRootId, kMonoforumPeerId); _session->api().saveDraftToCloudDelayed(_occupiedHistory); } _occupiedHistory = history; @@ -377,7 +378,10 @@ void Helper::occupyInDraft() { && !isOccupiedBySomeone(_occupiedHistory) && !_supportName.isEmpty()) { const auto draft = OccupiedDraft(_supportNameNormalized); - _occupiedHistory->createCloudDraft(kTopicRootId, &draft); + _occupiedHistory->createCloudDraft( + kTopicRootId, + kMonoforumPeerId, + &draft); _session->api().saveDraftToCloudDelayed(_occupiedHistory); _reoccupyTimer.callEach(kReoccupyEach); } @@ -386,7 +390,10 @@ void Helper::occupyInDraft() { void Helper::reoccupy() { if (isOccupiedByMe(_occupiedHistory)) { const auto draft = OccupiedDraft(_supportNameNormalized); - _occupiedHistory->createCloudDraft(kTopicRootId, &draft); + _occupiedHistory->createCloudDraft( + kTopicRootId, + kMonoforumPeerId, + &draft); _session->api().saveDraftToCloudDelayed(_occupiedHistory); } } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 2f14936778..39f8100ffd 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1802,8 +1802,10 @@ void PeerMenuCreatePoll( peer->owner().history(peer), result.options); action.replyTo = replyTo; - const auto topicRootId = replyTo.topicRootId; - if (const auto local = action.history->localDraft(topicRootId)) { + const auto local = action.history->localDraft( + replyTo.topicRootId, + replyTo.monoforumPeerId); + if (local) { action.clearDraft = local->textWithTags.text.isEmpty(); } else { action.clearDraft = false; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 9ea12353a1..73e6e0110f 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2150,8 +2150,9 @@ bool SessionController::switchInlineQuery( params); } else { const auto topicRootId = to.currentReplyTo.topicRootId; + const auto monoforumPeerId = to.currentReplyTo.monoforumPeerId; history->setLocalDraft(std::move(draft)); - history->clearLocalEditDraft(topicRootId); + history->clearLocalEditDraft(topicRootId, monoforumPeerId); if (to.section == Section::Replies) { const auto commentId = MsgId(); showRepliesForMessage(history, topicRootId, commentId, params); From 075f754a718b18904339493e17ea1254245838f1 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 22 May 2025 13:03:37 +0400 Subject: [PATCH 069/310] Update API scheme on layer 204. --- Telegram/SourceFiles/mtproto/scheme/api.tl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 70a3f6f488..e6905743cb 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2360,7 +2360,7 @@ messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4? messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSavedDialogs#1e91fc99 flags:# exclude_pinned:flags.0?true parent_peer:flags.1?InputPeer offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; messages.getSavedHistory#998ab009 flags:# parent_peer:flags.0?InputPeer peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; +messages.deleteSavedHistory#4dc5085f flags:# parent_peer:flags.0?InputPeer peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector<InputDialogPeer> = Bool; From 646b8527179f2397253b02caf2b849dbbd24f88b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 22 May 2025 13:12:59 +0400 Subject: [PATCH 070/310] Correct rights check in monoforums. --- Telegram/SourceFiles/data/data_channel.cpp | 9 ++++++--- Telegram/SourceFiles/data/data_channel.h | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 9250df02a9..51f77db85e 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -343,7 +343,7 @@ void ChannelData::setMonoforumLink(ChannelData *link) { _monoforumLink = link; link->setMonoforumLink(this); session().changes().peerUpdated(this, UpdateFlag::MonoforumLink); - if (isMegagroup() && (link->amCreator() || link->hasAdminRights())) { + if (isMegagroup() && link->canAccessMonoforum()) { setFlags(flags() | Flag::MonoforumAdmin); } } @@ -656,6 +656,10 @@ bool ChannelData::canDeleteStories() const { || (adminRights() & AdminRight::DeleteStories); } +bool ChannelData::canAccessMonoforum() const { + return canPostMessages(); +} + bool ChannelData::canPostPaidMedia() const { return canPostMessages() && (flags() & Flag::PaidMediaAllowed); } @@ -836,9 +840,8 @@ void ChannelData::setAdminRights(ChatAdminRights rights) { UpdateFlag::Rights | UpdateFlag::Admins | UpdateFlag::BannedUsers); if (isBroadcast() && _monoforumLink) { const auto flags = _monoforumLink->flags(); - const auto admin = (amCreator() || hasAdminRights()); _monoforumLink->setFlags((flags & ~Flag::MonoforumAdmin) - | (admin ? Flag::MonoforumAdmin : Flag())); + | (canAccessMonoforum() ? Flag::MonoforumAdmin : Flag())); } } diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 21cbaafd0d..b0362f1d8c 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -396,6 +396,7 @@ public: [[nodiscard]] bool canEditStories() const; [[nodiscard]] bool canDeleteStories() const; [[nodiscard]] bool canPostPaidMedia() const; + [[nodiscard]] bool canAccessMonoforum() const; [[nodiscard]] bool hiddenPreHistory() const; [[nodiscard]] bool canViewMembers() const; [[nodiscard]] bool canViewAdmins() const; From 7dc894384025b86b686286701b44b50b190b6abd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 22 May 2025 13:56:11 +0400 Subject: [PATCH 071/310] Improve monoforum opening. --- .../SourceFiles/data/data_chat_filters.cpp | 4 ++++ Telegram/SourceFiles/data/data_chat_filters.h | 1 + Telegram/SourceFiles/data/data_peer.cpp | 11 ++++++++++ Telegram/SourceFiles/data/data_peer.h | 2 ++ .../dialogs/dialogs_inner_widget.cpp | 21 ++++++++++++------- Telegram/SourceFiles/dialogs/dialogs_row.cpp | 4 +++- .../SourceFiles/dialogs/dialogs_widget.cpp | 13 ++++++------ .../view/history_view_top_bar_widget.cpp | 10 +++++++-- .../info/profile/info_profile_cover.cpp | 4 +--- .../window/window_session_controller.cpp | 2 ++ 10 files changed, 53 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 4a0d0c2331..a2c5f48d89 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -461,6 +461,10 @@ rpl::producer<bool> ChatFilters::tagsEnabledValue() const { return _tagsEnabled.value(); } +rpl::producer<bool> ChatFilters::tagsEnabledChanges() const { + return _tagsEnabled.changes(); +} + void ChatFilters::requestToggleTags(bool value, Fn<void()> fail) { if (_toggleTagsRequestId) { return; diff --git a/Telegram/SourceFiles/data/data_chat_filters.h b/Telegram/SourceFiles/data/data_chat_filters.h index ef4c8deff7..cfba1f9a45 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.h +++ b/Telegram/SourceFiles/data/data_chat_filters.h @@ -213,6 +213,7 @@ public: [[nodiscard]] bool tagsEnabled() const; [[nodiscard]] rpl::producer<bool> tagsEnabledValue() const; + [[nodiscard]] rpl::producer<bool> tagsEnabledChanges() const; void requestToggleTags(bool value, Fn<void()> fail); private: diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 80d81c74bb..44a805e764 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1162,6 +1162,17 @@ not_null<const PeerData*> PeerData::migrateToOrMe() const { return this; } +not_null<PeerData*> PeerData::userpicPaintingPeer() { + if (const auto broadcast = monoforumBroadcast()) { + return broadcast; + } + return this; +} + +not_null<const PeerData*> PeerData::userpicPaintingPeer() const { + return const_cast<PeerData*>(this)->userpicPaintingPeer(); +} + ChannelData *PeerData::monoforumBroadcast() const { const auto monoforum = asMonoforum(); return monoforum ? monoforum->monoforumLink() : nullptr; diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index e185fc58ea..397965e476 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -305,6 +305,8 @@ public: [[nodiscard]] ChannelData *migrateTo() const; [[nodiscard]] not_null<PeerData*> migrateToOrMe(); [[nodiscard]] not_null<const PeerData*> migrateToOrMe() const; + [[nodiscard]] not_null<PeerData*> userpicPaintingPeer(); + [[nodiscard]] not_null<const PeerData*> userpicPaintingPeer() const; // isMonoforum() ? monoforumLink() : nullptr [[nodiscard]] ChannelData *monoforumBroadcast() const; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index deab114692..2905899da1 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -366,7 +366,9 @@ InnerWidget::InnerWidget( rpl::merge( session().settings().archiveCollapsedChanges() | rpl::map_to(false), - session().data().chatsFilters().changed() | rpl::map_to(true) + session().data().chatsFilters().changed() | rpl::map_to(true), + session().data().chatsFilters().tagsEnabledChanges( + ) | rpl::map_to(true) ) | rpl::start_with_next([=](bool refreshHeight) { if (refreshHeight) { _chatsFilterTags.clear(); @@ -379,11 +381,8 @@ InnerWidget::InnerWidget( }, lifetime()); session().data().chatsFilters().tagsEnabledValue( - ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool tags) { + ) | rpl::start_with_next([=](bool tags) { _handleChatListEntryTagRefreshesLifetime.destroy(); - if (_shownList->updateHeights(_narrowRatio)) { - refresh(); - } if (!tags) { return; } @@ -1499,8 +1498,16 @@ bool InnerWidget::isRowActive( not_null<Row*> row, const RowDescriptor &entry) const { const auto key = row->key(); - return (entry.key == key) - || (entry.key.sublist() && key.peer() && key.peer()->isSelf()); + if (entry.key == key) { + return true; + } else if (const auto sublist = entry.key.sublist()) { + if (!sublist->parentChat()) { + // In case we're viewing a Saved Messages sublist, + // we want to highlight the Saved Messages row as active. + return key.history() && key.peer()->isSelf(); + } + } + return false; } bool InnerWidget::isSearchResultActive( diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index bac34785a1..1082a20999 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -320,7 +320,9 @@ Row::~Row() { void Row::recountHeight(float64 narrowRatio, FilterId filterId) { if (const auto history = _id.history()) { const auto hasTags = _id.entry()->hasChatsFilterTags(filterId); - _height = (history->isForum() || history->amMonoforumAdmin()) + const auto wideRow = history->isForum() + || history->amMonoforumAdmin(); + _height = wideRow ? anim::interpolate( hasTags ? st::taggedForumDialogRow.height diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index b311225bf2..ac7bff41db 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -942,8 +942,7 @@ void Widget::chosenRow(const ChosenRow &row) { return; } else if (history && history->peer->amMonoforumAdmin() - && !row.message.fullId - && !controller()->adaptive().isOneColumn()) { + && !row.message.fullId) { const auto monoforum = history->peer->monoforum(); if (controller()->shownMonoforum().current() == monoforum) { controller()->closeMonoforum(); @@ -954,10 +953,12 @@ void Widget::chosenRow(const ChosenRow &row) { controller()->showMonoforum( monoforum, Window::SectionShow().withChildColumn()); - controller()->showThread( - history, - ShowAtUnreadMsgId, - Window::SectionShow::Way::ClearStack); + if (!controller()->adaptive().isOneColumn()) { + controller()->showThread( + history, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } } return; } else if (history) { diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index b93e55033e..7cbf86fcb3 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -929,10 +929,16 @@ void TopBarWidget::refreshInfoButton() { && !rootChatsListBar())) { _info.destroy(); } else if (const auto peer = _activeChat.key.peer()) { + const auto sublist = _activeChat.key.sublist(); + const auto infoPeer = sublist ? sublist->sublistPeer().get() : peer; auto info = object_ptr<Ui::UserpicButton>( this, - peer, - st::topBarInfoButton); + _controller, + infoPeer->userpicPaintingPeer(), + Ui::UserpicButton::Role::Custom, + Ui::UserpicButton::Source::PeerPhoto, + st::topBarInfoButton, + infoPeer->monoforumBroadcast() != nullptr); info->showSavedMessagesOnSelf(true); _info.destroy(); _info = std::move(info); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index e8a1226cfe..2393e6c370 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -628,9 +628,7 @@ Cover::Cover( : object_ptr<Ui::UserpicButton>( this, controller, - (_peer->monoforumBroadcast() - ? _peer->monoforumBroadcast() - : _peer), + _peer->userpicPaintingPeer(), Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, _st.photo, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 73e6e0110f..a505ce3e8c 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1906,6 +1906,7 @@ void SessionController::showForum( } }, _shownForumLifetime); content()->showForum(forum, params); + closeMonoforum(); } void SessionController::closeForum() { @@ -1980,6 +1981,7 @@ void SessionController::showMonoforum( closeMonoforum(); }, _shownMonoforumLifetime); content()->showMonoforum(monoforum, params); + closeForum(); } void SessionController::closeMonoforum() { From 3dbdecf73d624a87fbd34aa443d6605ff92be86a Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 22 May 2025 14:46:00 +0400 Subject: [PATCH 072/310] Make monoforum sender badges float. --- .../admin_log/history_admin_log_inner.h | 2 +- .../history/history_inner_widget.cpp | 111 +++++++++++++++--- .../history/history_inner_widget.h | 16 ++- .../history/view/history_view_element.cpp | 62 +++++++++- .../history/view/history_view_element.h | 30 ++++- .../history/view/history_view_message.cpp | 34 ++---- .../view/history_view_service_message.cpp | 34 ++---- 7 files changed, 208 insertions(+), 81 deletions(-) diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index ed23bdad44..1f571edffc 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -251,7 +251,7 @@ private: // for each found message (in given direction) in the passed history with passed top offset. // // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <EnumItemsDirection direction, typename Method> void enumerateItems(Method method); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index dd6031275a..6d01dc5710 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -916,6 +916,62 @@ void HistoryInner::enumerateDates(Method method) { enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback); } +template <typename Method> +void HistoryInner::enumerateMonoforumSenders(Method method) { + if (!_history->amMonoforumAdmin()) { + return; + } + + const auto skip = (_scrollDateOpacity.animating() || _scrollDateShown) + ? int(base::SafeRound( + (_scrollDateOpacity.value(_scrollDateShown ? 1. : 0.) + * (st::msgServicePadding.bottom() + + st::msgServiceFont->height + + st::msgServicePadding.top() + + st::msgServiceMargin.top())))) + : 0; + + // Find and remember the bottom of an single-day messages pack + // -1 means we didn't find a same-day with previous message yet. + auto lowestInOneBunchItemBottom = -1; + + auto senderCallback = [&](not_null<Element*> view, int itemtop, int itembottom) { + const auto item = view->data(); + if (lowestInOneBunchItemBottom < 0 && view->isInOneBunchWithPrevious()) { + lowestInOneBunchItemBottom = itembottom - view->marginBottom(); + } + + // Call method on a sender for all messages that have it and for those who are not showing it + // because they are in a one day together with the previous message if they are top-most visible. + if (view->displayMonoforumSender() || (!item->isEmpty() && itemtop <= _visibleAreaTop)) { + if (lowestInOneBunchItemBottom < 0) { + lowestInOneBunchItemBottom = itembottom - view->marginBottom(); + } + // Attach sender to the top of the visible area with the same margin as it has in service message. + int senderTop = qMax(itemtop + view->displayedDateHeight(), _visibleAreaTop + skip) + st::msgServiceMargin.top(); + + // Do not let the sender go below the single-sender messages pack bottom line. + int senderHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); + senderTop = qMin(senderTop, lowestInOneBunchItemBottom - senderHeight); + + // Call the template callback function that was passed + // and return if it finished everything it needed. + if (!method(view, itemtop, senderTop)) { + return false; + } + } + + // Forget the found bottom of the pack, search for the next one from scratch. + if (!view->isInOneBunchWithPrevious()) { + lowestInOneBunchItemBottom = -1; + } + + return true; + }; + + enumerateItems<EnumItemsDirection::BottomToTop>(senderCallback); +} + TextSelection HistoryInner::computeRenderSelection( not_null<const SelectedItems*> selected, not_null<Element*> view) const { @@ -1291,14 +1347,6 @@ void HistoryInner::paintEvent(QPaintEvent *e) { const auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top(); - //QDate lastDate; - //if (!_history->isEmpty()) { - // lastDate = _history->blocks.back()->messages.back()->data()->date.date(); - //} - - //// if item top is before this value always show date as a floating date - //int showFloatingBefore = height() - 2 * (_visibleAreaBottom - _visibleAreaTop) - dateHeight; - auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.); enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) { // stop the enumeration if the date is above the painted rect @@ -1312,21 +1360,13 @@ void HistoryInner::paintEvent(QPaintEvent *e) { const auto correctDateTop = itemtop + st::msgServiceMargin.top(); dateInPlace = (dateTop < correctDateTop + dateHeight); } - //bool noFloatingDate = (item->date.date() == lastDate && displayDate); - //if (noFloatingDate) { - // if (itemtop < showFloatingBefore) { - // noFloatingDate = false; - // } - //} // paint the date if it intersects the painted rect if (dateTop < clip.top() + clip.height()) { - auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity; + auto opacity = dateInPlace ? 1. : scrollDateOpacity; if (opacity > 0.) { p.setOpacity(opacity); - const auto dateY = false // noFloatingDate - ? itemtop - : (dateTop - st::msgServiceMargin.top()); + const auto dateY = dateTop - st::msgServiceMargin.top(); if (const auto date = view->Get<HistoryView::DateBadge>()) { date->paint(p, context.st, dateY, _contentWidth, _isChatWide); } else { @@ -1344,6 +1384,38 @@ void HistoryInner::paintEvent(QPaintEvent *e) { }); p.setOpacity(1.); + enumerateMonoforumSenders([&](not_null<Element*> view, int itemtop, int senderTop) { + // stop the enumeration if the sender is above the painted rect + if (senderTop + dateHeight <= clip.top()) { + return false; + } + + const auto displaySender = view->displayMonoforumSender(); + auto senderInPlace = displaySender; + if (senderInPlace) { + const auto correctSenderTop = itemtop + view->displayedDateHeight() + st::msgServiceMargin.top(); + senderInPlace = (senderTop < correctSenderTop + st::msgServiceMargin.top()); + } + + // paint the sender if it intersects the painted rect + if (senderTop < clip.top() + clip.height()) { + const auto senderY = senderTop - st::msgServiceMargin.top(); + if (const auto sender = view->Get<HistoryView::MonoforumSenderBar>()) { + sender->paint(p, context.st, senderY, _contentWidth, _isChatWide, !senderInPlace); + } else { + HistoryView::MonoforumSenderBar::PaintFor( + p, + context.st, + view, + _monoforumSenderUserpicView, + senderY, + _contentWidth, + _isChatWide); + } + } + return true; + }); + _reactionsManager->paint(p, context); } @@ -3566,6 +3638,9 @@ void HistoryInner::toggleScrollDateShown() { void HistoryInner::repaintScrollDateCallback() { int updateTop = _visibleAreaTop; int updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); + if (_history->amMonoforumAdmin()) { + updateHeight *= 2; + } update(0, updateTop, width(), updateHeight); } diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 8d180cf670..1b603babcb 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/dragging_scroll_manager.h" #include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" +#include "ui/userpic_view.h" #include "history/view/history_view_top_bar_widget.h" #include <QtGui/QPainterPath> @@ -279,7 +280,7 @@ private: // for each found message (in given direction) in the passed history with passed top offset. // // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int itembottom)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <bool TopToBottom, typename Method> void enumerateItemsInHistory(History *history, int historytop, Method method); @@ -299,7 +300,7 @@ private: // for each found userpic (from the top to the bottom) using enumerateItems() method. // // Method has "bool (*Method)(not_null<Element*> view, int userpicTop)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <typename Method> void enumerateUserpics(Method method); @@ -307,10 +308,18 @@ private: // for each found date element (from the bottom to the top) using enumerateItems() method. // // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int dateTop)" signature - // if it returns false the enumeration stops immidiately. + // if it returns false the enumeration stops immediately. template <typename Method> void enumerateDates(Method method); + // This function finds all monoforum sender elements that are displayed and calls template method + // for each found date element (from the bottom to the top) using enumerateItems() method. + // + // Method has "bool (*Method)(not_null<Element*> view, int itemtop, int dateTop)" signature + // if it returns false the enumeration stops immediately. + template <typename Method> + void enumerateMonoforumSenders(Method method); + void scrollDateCheck(); void scrollDateHideByTimer(); bool canHaveFromUserpics() const; @@ -458,6 +467,7 @@ private: int _contentWidth = 0; int _historyPaddingTop = 0; int _revealHeight = 0; + Ui::PeerUserpicView _monoforumSenderUserpicView; // Save visible area coords for painting / pressing userpics. int _visibleAreaTop = 0; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index b89b170eb6..cef5cf4793 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -480,7 +480,7 @@ void DateBadge::paint( void MonoforumSenderBar::init( not_null<PeerData*> parentChat, not_null<PeerData*> peer) { - author = peer; + sender = peer; text.setText(st::semiboldTextStyle, peer->name()); const auto skip = st::monoforumBarUserpicSkip; const auto userpic = st::msgServicePadding.top() @@ -503,8 +503,52 @@ void MonoforumSenderBar::paint( not_null<const Ui::ChatStyle*> st, int y, int w, - bool chatWide) const { - Expects(author != nullptr); + bool chatWide, + bool skipPatternLine) const { + Paint(p, st, sender, text, width, view, y, w, chatWide, skipPatternLine); +} + +void MonoforumSenderBar::PaintFor( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<Element*> itemView, + Ui::PeerUserpicView &userpicView, + int y, + int w, + bool chatWide) { + const auto sublist = itemView->data()->savedSublist(); + const auto sender = (sublist && sublist->parentChat()) + ? sublist->sublistPeer().get() + : nullptr; + if (!sender || sender->isMonoforum()) { + return; + } + auto text = Ui::Text::String(st::semiboldTextStyle, sender->name()); + const auto skip = st::monoforumBarUserpicSkip; + const auto userpic = st::msgServicePadding.top() + + st::msgServiceFont->height + + st::msgServicePadding.bottom() + - 2 * skip; + const auto width = skip + + userpic + + skip * 2 + + text.maxWidth() + + st::msgServicePadding.right(); + Paint(p, st, sender, text, width, userpicView, y, w, chatWide, true); +} + +void MonoforumSenderBar::Paint( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<PeerData*> sender, + const Ui::Text::String &text, + int width, + Ui::PeerUserpicView &view, + int y, + int w, + bool chatWide, + bool skipPatternLine) { + Expects(sender != nullptr); int left = st::msgServiceMargin.left(); const auto maxwidth = chatWide @@ -523,7 +567,7 @@ void MonoforumSenderBar::paint( QRect(left, y + st::msgServiceMargin.top(), use, h)); const auto skip = st::monoforumBarUserpicSkip; - { + if (!skipPatternLine) { auto pen = st->msgServiceBg()->p; pen.setWidthF(skip); pen.setCapStyle(Qt::RoundCap); @@ -540,7 +584,7 @@ void MonoforumSenderBar::paint( - 2 * skip; const auto available = use - (skip + userpic + skip * 2 + st::msgServicePadding.right()); - author->paintUserpic(p, view, left + skip, y + st::msgServiceMargin.top() + skip, userpic); + sender->paintUserpic(p, view, left + skip, y + st::msgServiceMargin.top() + skip, userpic); p.setFont(st::msgServiceFont); p.setPen(st->msgServiceFg()); @@ -1448,6 +1492,14 @@ bool Element::isInOneDayWithPrevious() const { return !data()->isEmpty() && !displayDate(); } +bool Element::displayMonoforumSender() const { + return Has<MonoforumSenderBar>(); +} + +bool Element::isInOneBunchWithPrevious() const { + return !data()->isEmpty() && !displayMonoforumSender(); +} + void Element::recountAttachToPreviousInBlocks() { if (isHidden() || data()->isEmpty()) { if (const auto next = nextDisplayedInBlocks()) { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index e7c5ac94f9..f3a05a8ed2 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -268,13 +268,36 @@ struct MonoforumSenderBar : RuntimeComponent<MonoforumSenderBar, Element> { not_null<const Ui::ChatStyle*> st, int y, int w, - bool chatWide) const; + bool chatWide, + bool skipPatternLine) const; + static void PaintFor( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<Element*> itemView, + Ui::PeerUserpicView &userpicView, + int y, + int w, + bool chatWide); - PeerData *author = nullptr; + PeerData *sender = nullptr; Ui::Text::String text; ClickHandlerPtr link; mutable Ui::PeerUserpicView view; int width = 0; + +private: + static void Paint( + Painter &p, + not_null<const Ui::ChatStyle*> st, + not_null<PeerData*> sender, + const Ui::Text::String &text, + int width, + Ui::PeerUserpicView &view, + int y, + int w, + bool chatWide, + bool skipPatternLine); + }; // Any HistoryView::Element can have this Component for @@ -438,6 +461,9 @@ public: [[nodiscard]] bool displayDate() const; [[nodiscard]] bool isInOneDayWithPrevious() const; + [[nodiscard]] bool displayMonoforumSender() const; + [[nodiscard]] bool isInOneBunchWithPrevious() const; + virtual void draw(Painter &p, const PaintContext &context) const = 0; [[nodiscard]] virtual PointState pointState(QPoint point) const = 0; [[nodiscard]] virtual TextState textState( diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 49a7afafce..43e97b6125 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1133,40 +1133,22 @@ void Message::draw(Painter &p, const PaintContext &context) const { if (const auto bar = Get<UnreadBar>()) { auto unreadbarh = bar->height(); - auto dateh = 0; + auto aboveh = 0; if (const auto date = Get<DateBadge>()) { - dateh = date->height(); + aboveh += date->height(); } - if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) { - p.translate(0, dateh); + if (const auto sender = Get<MonoforumSenderBar>()) { + aboveh += sender->height(); + } + if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { + p.translate(0, aboveh); bar->paint( p, context, 0, width(), delegate()->elementIsChatWide()); - p.translate(0, -dateh); - } - } - - if (const auto monoforumBar = Get<MonoforumSenderBar>()) { - auto barh = monoforumBar->height(); - auto skip = 0; - if (const auto date = Get<DateBadge>()) { - skip += date->height(); - } - if (const auto bar = Get<UnreadBar>()) { - skip += bar->height(); - } - if (context.clip.intersects(QRect(0, skip, width(), barh))) { - p.translate(0, skip); - monoforumBar->paint( - p, - context.st, - 0, - width(), - delegate()->elementIsChatWide()); - p.translate(0, -skip); + p.translate(0, -aboveh); } } diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 9acf977d62..d215be432a 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -547,40 +547,22 @@ void Service::draw(Painter &p, const PaintContext &context) const { const auto st = context.st; if (const auto bar = Get<UnreadBar>()) { auto unreadbarh = bar->height(); - auto dateh = 0; + auto aboveh = 0; if (const auto date = Get<DateBadge>()) { - dateh = date->height(); + aboveh += date->height(); } - if (context.clip.intersects(QRect(0, dateh, width(), unreadbarh))) { - p.translate(0, dateh); + if (const auto sender = Get<MonoforumSenderBar>()) { + aboveh += sender->height(); + } + if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { + p.translate(0, aboveh); bar->paint( p, context, 0, width(), delegate()->elementIsChatWide()); - p.translate(0, -dateh); - } - } - - if (const auto monoforumBar = Get<MonoforumSenderBar>()) { - auto barh = monoforumBar->height(); - auto skip = 0; - if (const auto date = Get<DateBadge>()) { - skip += date->height(); - } - if (const auto bar = Get<UnreadBar>()) { - skip += bar->height(); - } - if (context.clip.intersects(QRect(0, skip, width(), barh))) { - p.translate(0, skip); - monoforumBar->paint( - p, - context.st, - 0, - width(), - delegate()->elementIsChatWide()); - p.translate(0, -skip); + p.translate(0, -aboveh); } } From fdbdeeb95694cfd0f9a22634dc0195126a8010c2 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 May 2025 14:06:46 +0400 Subject: [PATCH 073/310] Start new tabs for monoforums. --- Telegram/CMakeLists.txt | 2 + .../chat_helpers/ttl_media_layer_widget.cpp | 7 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 60 +-- .../dialogs/ui/dialogs_topics_view.cpp | 2 +- .../admin_log/history_admin_log_inner.cpp | 5 +- .../admin_log/history_admin_log_inner.h | 2 +- .../history/history_inner_widget.cpp | 20 +- .../history/history_inner_widget.h | 5 +- .../SourceFiles/history/history_widget.cpp | 165 +++++--- Telegram/SourceFiles/history/history_widget.h | 8 + .../view/history_view_chat_section.cpp | 103 +++-- .../history/view/history_view_chat_section.h | 5 + .../history/view/history_view_element.cpp | 20 +- .../history/view/history_view_element.h | 16 +- .../history/view/history_view_list_widget.cpp | 10 +- .../history/view/history_view_list_widget.h | 6 +- .../history/view/history_view_message.cpp | 47 ++- .../view/history_view_service_message.cpp | 6 +- .../view/history_view_subsection_tabs.cpp | 384 ++++++++++++++++++ .../view/history_view_subsection_tabs.h | 84 ++++ .../info/profile/info_profile_actions.cpp | 7 +- Telegram/SourceFiles/mainwidget.cpp | 4 +- .../business/settings_shortcut_messages.cpp | 2 +- Telegram/SourceFiles/ui/chat/chat.style | 31 ++ .../SourceFiles/window/section_widget.cpp | 2 + Telegram/SourceFiles/window/section_widget.h | 3 + .../window/window_session_controller.cpp | 30 ++ .../window/window_session_controller.h | 14 + 28 files changed, 869 insertions(+), 181 deletions(-) create mode 100644 Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_subsection_tabs.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 85a7703fd6..3184b11ae4 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -888,6 +888,8 @@ PRIVATE history/view/history_view_sponsored_click_handler.h history/view/history_view_sticker_toast.cpp history/view/history_view_sticker_toast.h + history/view/history_view_subsection_tabs.cpp + history/view/history_view_subsection_tabs.h history/view/history_view_text_helper.cpp history/view/history_view_text_helper.h history/view/history_view_transcribe_button.cpp diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp index 583fed29ae..65940c6b00 100644 --- a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp @@ -52,7 +52,7 @@ public: bool elementAnimationsPaused() override; not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override; HistoryView::Context elementContext() override; - bool elementIsChatWide() override; + HistoryView::ElementChatMode elementChatMode() override; private: const not_null<QWidget*> _parent; @@ -83,8 +83,9 @@ HistoryView::Context PreviewDelegate::elementContext() { return HistoryView::Context::TTLViewer; } -bool PreviewDelegate::elementIsChatWide() { - return _chatWide.current(); +HistoryView::ElementChatMode PreviewDelegate::elementChatMode() { + using Mode = HistoryView::ElementChatMode; + return _chatWide.current() ? Mode::Wide : Mode::Default; } class PreviewWrap final : public Ui::RpWidget { diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index ac7bff41db..4ecbf32115 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -940,27 +940,27 @@ void Widget::chosenRow(const ChosenRow &row) { } } return; - } else if (history - && history->peer->amMonoforumAdmin() - && !row.message.fullId) { - const auto monoforum = history->peer->monoforum(); - if (controller()->shownMonoforum().current() == monoforum) { - controller()->closeMonoforum(); - //} else if (row.newWindow) { // #TODO monoforum - // controller()->showInNewWindow( - // Window::SeparateId(Window::SeparateType::Forum, history)); - } else { - controller()->showMonoforum( - monoforum, - Window::SectionShow().withChildColumn()); - if (!controller()->adaptive().isOneColumn()) { - controller()->showThread( - history, - ShowAtUnreadMsgId, - Window::SectionShow::Way::ClearStack); - } - } - return; + //} else if (history + // && history->peer->amMonoforumAdmin() + // && !row.message.fullId) { + // const auto monoforum = history->peer->monoforum(); + // if (controller()->shownMonoforum().current() == monoforum) { + // controller()->closeMonoforum(); + // //} else if (row.newWindow) { // #TODO monoforum + // // controller()->showInNewWindow( + // // Window::SeparateId(Window::SeparateType::Forum, history)); + // } else { + // controller()->showMonoforum( + // monoforum, + // Window::SectionShow().withChildColumn()); + // if (!controller()->adaptive().isOneColumn()) { + // controller()->showThread( + // history, + // ShowAtUnreadMsgId, + // Window::SectionShow::Way::ClearStack); + // } + // } + // return; } else if (history) { const auto peer = history->peer; const auto showAtMsgId = controller()->uniqueChatsInSearchResults() @@ -999,13 +999,17 @@ void Widget::chosenRow(const ChosenRow &row) { using namespace Window; auto params = SectionShow(SectionShow::Way::Forward); params.dropSameFromStack = true; - using namespace HistoryView; - controller()->showSection( - std::make_shared<ChatMemento>(ChatViewId{ - .history = sublist->owningHistory(), - .sublist = sublist, - }), - params); + params.highlightPart.text = _searchState.query; + if (!params.highlightPart.empty()) { + params.highlightPartOffsetHint = kSearchQueryOffsetHint; + } + if (false && row.newWindow) { // #TODO monoforum + controller()->showInNewWindow( + Window::SeparateId(sublist), + row.message.fullId.msg); + } else { + controller()->showThread(sublist, row.message.fullId.msg, params); + } } if (row.filteredRow && !session().supportMode()) { if (_subsectionTopBar) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index ba42d90cc0..4ce796c92e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -152,7 +152,7 @@ void TopicsView::prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint) { Ui::Text::SingleCustomEmoji( manager->peerUserpicEmojiData(peer), u"@"_q) - ).append(peer->shortName()); + ).append(' ').append(peer->shortName()); title.key = key; title.version = peer->nameVersion(); title.unread = unread; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 61a599cac4..0d917654d4 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -740,8 +740,9 @@ void InnerWidget::elementSearchInList( void InnerWidget::elementHandleViaClick(not_null<UserData*> bot) { } -bool InnerWidget::elementIsChatWide() { - return _isChatWide; +HistoryView::ElementChatMode InnerWidget::elementChatMode() { + using Mode = HistoryView::ElementChatMode; + return _isChatWide ? Mode::Wide : Mode::Default; } not_null<Ui::PathShiftGradient*> InnerWidget::elementPathShiftGradient() { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h index 1f571edffc..9f58afb921 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.h @@ -131,7 +131,7 @@ public: const QString &query, const FullMsgId &context) override; void elementHandleViaClick(not_null<UserData*> bot) override; - bool elementIsChatWide() override; + HistoryView::ElementChatMode elementChatMode() override; not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override; void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction( diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 6d01dc5710..933de0d20c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -241,8 +241,9 @@ public: _widget->elementHandleViaClick(bot); } } - bool elementIsChatWide() override { - return _widget ? _widget->elementIsChatWide() : false; + HistoryView::ElementChatMode elementChatMode() override { + using Mode = HistoryView::ElementChatMode; + return _widget ? _widget->elementChatMode() : Mode::Default; } not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override { Expects(_widget != nullptr); @@ -808,7 +809,11 @@ bool HistoryInner::canHaveFromUserpics() const { } else if (const auto channel = _peer->asBroadcast()) { return channel->signatureProfiles(); } - return true; + return !_removeFromUserpics; +} + +void HistoryInner::toggleRemoveFromUserpics(bool remove) { + _removeFromUserpics = remove; } template <typename Method> @@ -3930,8 +3935,13 @@ void HistoryInner::elementHandleViaClick(not_null<UserData*> bot) { _widget->insertBotCommand('@' + bot->username()); } -bool HistoryInner::elementIsChatWide() { - return _isChatWide; +HistoryView::ElementChatMode HistoryInner::elementChatMode() { + using Mode = HistoryView::ElementChatMode; + return _isChatWide + ? Mode::Wide + : _removeFromUserpics + ? Mode::Narrow + : Mode::Default; } not_null<Ui::PathShiftGradient*> HistoryInner::elementPathShiftGradient() { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 1b603babcb..0da2dc7657 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -35,6 +35,7 @@ struct SelectionModeResult; struct StateRequest; enum class CursorState : char; enum class PointState : char; +enum class ElementChatMode : char; class EmptyPainter; class Element; class TranslateTracker; @@ -165,7 +166,7 @@ public: const QString &query, const FullMsgId &context); void elementHandleViaClick(not_null<UserData*> bot); - bool elementIsChatWide(); + HistoryView::ElementChatMode elementChatMode(); not_null<Ui::PathShiftGradient*> elementPathShiftGradient(); void elementReplyTo(const FullReplyTo &to); void elementStartInteraction(not_null<const Element*> view); @@ -193,6 +194,7 @@ public: void setChooseReportReason(Data::ReportInput reportInput); void clearChooseReportReason(); + void toggleRemoveFromUserpics(bool remove); // -1 if should not be visible, -2 if bad history() [[nodiscard]] int itemTop(const HistoryItem *item) const; @@ -493,6 +495,7 @@ private: const std::unique_ptr<Ui::PathShiftGradient> _pathGradient; QPainterPath _highlightPathCache; bool _isChatWide = false; + bool _removeFromUserpics = false; base::flat_set<not_null<const HistoryItem*>> _animatedStickersPlayed; base::flat_map<not_null<PeerData*>, Ui::PeerUserpicView> _userpics; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4007594a05..a295569910 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -117,6 +117,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_reply.h" #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_sticker_toast.h" +#include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" @@ -236,6 +237,7 @@ HistoryWidget::HistoryWidget( , _api(&controller->session().mtp()) , _updateEditTimeLeftDisplay([=] { updateField(); }) , _fieldBarCancel(this, st::historyReplyCancel) +, _topBars(std::make_unique<Ui::RpWidget>(this)) , _topBar(this, controller) , _scroll( this, @@ -1713,6 +1715,7 @@ void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { void HistoryWidget::orderWidgets() { _voiceRecordBar->raise(); _send->raise(); + _topBars->raise(); if (_businessBotStatus) { _businessBotStatus->bar().raise(); } @@ -1740,6 +1743,9 @@ void HistoryWidget::orderWidgets() { if (_chooseTheme) { _chooseTheme->raise(); } + if (_subsectionTabs) { + _subsectionTabs->raise(); + } _topShadow->raise(); if (_autocomplete) { _autocomplete->raise(); @@ -2467,6 +2473,11 @@ void HistoryWidget::showHistory( _fieldDisabled = nullptr; _silent.destroy(); updateBotKeyboard(); + + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + controller()->saveSubsectionTabs(base::take(_subsectionTabs)); + } } else { Assert(_list == nullptr); } @@ -2501,7 +2512,7 @@ void HistoryWidget::showHistory( _peer = session().data().peer(peerId); _contactStatus = std::make_unique<ContactStatus>( controller(), - this, + _topBars.get(), _peer, false); _contactStatus->bar().heightValue( @@ -2514,7 +2525,7 @@ void HistoryWidget::showHistory( if (const auto user = _peer->asUser()) { _paysStatus = std::make_unique<PaysStatus>( controller(), - this, + _topBars.get(), user); _paysStatus->bar().heightValue( ) | rpl::start_with_next([=] { @@ -2522,7 +2533,7 @@ void HistoryWidget::showHistory( }, _paysStatus->bar().lifetime()); _businessBotStatus = std::make_unique<BusinessBotStatus>( controller(), - this, + _topBars.get(), user); _businessBotStatus->bar().heightValue( ) | rpl::start_with_next([=] { @@ -3194,29 +3205,12 @@ void HistoryWidget::updateControlsVisibility() { } else if (!_firstLoadRequest && _scroll->isHidden()) { _scroll->show(); } - if (_pinnedBar) { - _pinnedBar->show(); - } + _topBars->show(); if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) { _sponsoredMessageBar->toggle(true, anim::type::normal); } - if (_translateBar) { - _translateBar->show(); - } - if (_groupCallBar) { - _groupCallBar->show(); - } - if (_requestsBar) { - _requestsBar->show(); - } - if (_paysStatus) { - _paysStatus->show(); - } - if (_contactStatus) { - _contactStatus->show(); - } - if (_businessBotStatus) { - _businessBotStatus->show(); + if (_subsectionTabs) { + _subsectionTabs->show(); } if (isChoosingTheme() || (!editingMessage() @@ -4431,20 +4425,12 @@ void HistoryWidget::hideChildWidgets() { if (_tabbedPanel) { _tabbedPanel->hideFast(); } - if (_pinnedBar) { - _pinnedBar->hide(); - } if (_sponsoredMessageBar) { _sponsoredMessageBar->toggle(false, anim::type::instant); } - if (_translateBar) { - _translateBar->hide(); - } - if (_groupCallBar) { - _groupCallBar->hide(); - } - if (_requestsBar) { - _requestsBar->hide(); + _topBars->hide(); + if (_subsectionTabs) { + _subsectionTabs->hide(); } if (_voiceRecordBar) { _voiceRecordBar->hideFast(); @@ -4455,15 +4441,6 @@ void HistoryWidget::hideChildWidgets() { if (_chooseTheme) { _chooseTheme->hide(); } - if (_paysStatus) { - _paysStatus->hide(); - } - if (_contactStatus) { - _contactStatus->hide(); - } - if (_businessBotStatus) { - _businessBotStatus->hide(); - } hideChildren(); } @@ -4747,6 +4724,8 @@ MsgId HistoryWidget::msgId() const { void HistoryWidget::showAnimated( Window::SlideDirection direction, const Window::SectionSlideParams ¶ms) { + validateSubsectionTabs(); + _showAnimation = nullptr; // If we show pinned bar here, we don't want it to change the @@ -4791,6 +4770,11 @@ void HistoryWidget::showAnimated( activate(); } +void HistoryWidget::showFast() { + validateSubsectionTabs(); + show(); +} + void HistoryWidget::showFinished() { _cornerButtons.finishAnimations(); if (_pinnedBar) { @@ -6419,40 +6403,50 @@ void HistoryWidget::resizeEvent(QResizeEvent *e) { } void HistoryWidget::updateControlsGeometry() { - _topBar->resizeToWidth(width()); + const auto width = this->width(); + + _topBar->resizeToWidth(width); _topBar->moveToLeft(0, 0); - _voiceRecordBar->resizeToWidth(width()); + + const auto tabsLeftSkip = _subsectionTabs + ? _subsectionTabs->leftSkip() + : 0; + const auto innerWidth = width - tabsLeftSkip; + + _voiceRecordBar->resizeToWidth(width); moveFieldControls(); - const auto groupCallTop = _topBar->bottomNoMargins(); + _topBars->move(tabsLeftSkip, _topBar->bottomNoMargins() + + (_subsectionTabs ? _subsectionTabs->topSkip() : 0)); + const auto groupCallTop = 0; if (_groupCallBar) { _groupCallBar->move(0, groupCallTop); - _groupCallBar->resizeToWidth(width()); + _groupCallBar->resizeToWidth(innerWidth); } const auto requestsTop = groupCallTop + (_groupCallBar ? _groupCallBar->height() : 0); if (_requestsBar) { _requestsBar->move(0, requestsTop); - _requestsBar->resizeToWidth(width()); + _requestsBar->resizeToWidth(innerWidth); } const auto pinnedBarTop = requestsTop + (_requestsBar ? _requestsBar->height() : 0); if (_pinnedBar) { _pinnedBar->move(0, pinnedBarTop); - _pinnedBar->resizeToWidth(width()); + _pinnedBar->resizeToWidth(innerWidth); } const auto sponsoredMessageBarTop = pinnedBarTop + (_pinnedBar ? _pinnedBar->height() : 0); if (_sponsoredMessageBar) { _sponsoredMessageBar->move(0, sponsoredMessageBarTop); - _sponsoredMessageBar->resizeToWidth(width()); + _sponsoredMessageBar->resizeToWidth(innerWidth); } const auto translateTop = sponsoredMessageBarTop + (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0); if (_translateBar) { _translateBar->move(0, translateTop); - _translateBar->resizeToWidth(width()); + _translateBar->resizeToWidth(innerWidth); } const auto paysStatusTop = translateTop + (_translateBar ? _translateBar->height() : 0); @@ -6462,17 +6456,19 @@ void HistoryWidget::updateControlsGeometry() { const auto contactStatusTop = paysStatusTop + (_paysStatus ? _paysStatus->bar().height() : 0); if (_contactStatus) { - _contactStatus->bar().move(0, contactStatusTop); + _contactStatus->bar().move(tabsLeftSkip, contactStatusTop); } const auto businessBotTop = contactStatusTop + (_contactStatus ? _contactStatus->bar().height() : 0); if (_businessBotStatus) { - _businessBotStatus->bar().move(0, businessBotTop); + _businessBotStatus->bar().move(tabsLeftSkip, businessBotTop); } - const auto scrollAreaTop = businessBotTop + const auto scrollAreaTop = _topBars->y() + + businessBotTop + (_businessBotStatus ? _businessBotStatus->bar().height() : 0); + _topBars->resize(innerWidth, scrollAreaTop - _topBars->y()); if (_scroll->y() != scrollAreaTop) { - _scroll->moveToLeft(0, scrollAreaTop); + _scroll->moveToLeft(tabsLeftSkip, scrollAreaTop); if (_autocomplete) { _autocomplete->setBoundings(_scroll->geometry()); } @@ -6502,7 +6498,7 @@ void HistoryWidget::updateControlsGeometry() { _topShadow->setGeometryToLeft( topShadowLeft, _topBar->bottomNoMargins(), - width() - topShadowLeft - topShadowRight, + width - topShadowLeft - topShadowRight, st::lineWidth); } @@ -6704,7 +6700,12 @@ void HistoryWidget::updateHistoryGeometry( return; } - auto newScrollHeight = height() - _topBar->height(); + const auto newScrollWidth = width() + - (_subsectionTabs ? _subsectionTabs->leftSkip() : 0); + const auto subsectionTabsTop = _topBar->bottomNoMargins(); + auto newScrollHeight = height() + - subsectionTabsTop + - (_subsectionTabs ? _subsectionTabs->topSkip() : 0); if (_translateBar) { newScrollHeight -= _translateBar->height(); } @@ -6760,10 +6761,10 @@ void HistoryWidget::updateHistoryGeometry( } const auto wasScrollTop = _scroll->scrollTop(); const auto wasAtBottom = (wasScrollTop == _scroll->scrollTopMax()); - const auto needResize = (_scroll->width() != width()) + const auto needResize = (_scroll->width() != newScrollWidth) || (_scroll->height() != newScrollHeight); if (needResize) { - _scroll->resize(width(), newScrollHeight); + _scroll->resize(newScrollWidth, newScrollHeight); // on initial updateListSize we didn't put the _scroll->scrollTop // correctly yet so visibleAreaUpdated() call will erase it // with the new (undefined) value @@ -6781,6 +6782,12 @@ void HistoryWidget::updateHistoryGeometry( _cornerButtons.updatePositions(); controller()->floatPlayerAreaUpdated(); } + if (_subsectionTabs) { + const auto scrollBottom = _scroll->y() + newScrollHeight; + const auto areaHeight = scrollBottom - subsectionTabsTop; + _subsectionTabs->setBoundingRect( + { 0, subsectionTabsTop, width(), areaHeight }); + } updateListSize(); _updateHistoryGeometryRequired = false; @@ -7625,7 +7632,7 @@ void HistoryWidget::setupTranslateBar() { Expects(_history != nullptr); _translateBar = std::make_unique<HistoryView::TranslateBar>( - this, + _topBars.get(), controller(), _history); @@ -7700,7 +7707,7 @@ void HistoryWidget::checkPinnedBarState() { } clearHidingPinnedBar(); - _pinnedBar = std::make_unique<Ui::PinnedBar>(this, [=] { + _pinnedBar = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); @@ -7921,7 +7928,7 @@ void HistoryWidget::setupGroupCallBar() { return; } _groupCallBar = std::make_unique<Ui::GroupCallBar>( - this, + _topBars.get(), HistoryView::GroupCallBarContentByPeer( peer, st::historyGroupCallUserpics.size, @@ -7974,7 +7981,7 @@ void HistoryWidget::setupRequestsBar() { return; } _requestsBar = std::make_unique<Ui::RequestsBar>( - this, + _topBars.get(), HistoryView::RequestsBarContentByPeer( peer, st::historyRequestsUserpics.size, @@ -8087,7 +8094,7 @@ void HistoryWidget::checkSponsoredMessageBar() { void HistoryWidget::createSponsoredMessageBar() { _sponsoredMessageBar = base::make_unique_q<Ui::SlideWrap<>>( - this, + _topBars.get(), object_ptr<Ui::RpWidget>(this)); _sponsoredMessageBar->entity()->resizeToWidth(_scroll->width()); @@ -8250,6 +8257,34 @@ void HistoryWidget::showPremiumToast(not_null<DocumentData*> document) { _stickerToast->showFor(document); } +void HistoryWidget::validateSubsectionTabs() { + if (!_history || !HistoryView::SubsectionTabs::UsedFor(_history)) { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + return; + } else if (_subsectionTabs) { + return; + } + _subsectionTabs = controller()->restoreSubsectionTabsFor(this, _history); + if (!_subsectionTabs) { + _subsectionTabs = std::make_unique<HistoryView::SubsectionTabs>( + controller(), + this, + _history); + } + _subsectionTabs->removeRequests() | rpl::start_with_next([=] { + _subsectionTabs = nullptr; + updateControlsGeometry(); + }, _subsectionTabsLifetime); + _subsectionTabs->layoutRequests() | rpl::start_with_next([=] { + _list->toggleRemoveFromUserpics(_subsectionTabs->leftSkip() > 0); + updateControlsGeometry(); + orderWidgets(); + }, _subsectionTabsLifetime); + updateControlsGeometry(); + orderWidgets(); +} + void HistoryWidget::checkCharsCount() { _fieldCharsCountManager.setCount(Ui::ComputeFieldCharacterCount(_field)); checkCharsLimitation(); @@ -9439,5 +9474,7 @@ HistoryWidget::~HistoryWidget() { session().data().itemVisibilitiesUpdated(); } + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; setTabbedPanel(nullptr); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index c11ecdc7c0..e9f536e403 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -107,6 +107,7 @@ class Element; class PinnedTracker; class TranslateBar; class ComposeSearch; +class SubsectionTabs; struct SelectedQuote; } // namespace HistoryView @@ -183,6 +184,7 @@ public: void showAnimated( Window::SlideDirection direction, const Window::SectionSlideParams ¶ms); + void showFast(); void finishAnimating(); void doneShow(); @@ -684,6 +686,8 @@ private: void switchToSearch(QString query); + void validateSubsectionTabs(); + void checkCharsCount(); void checkCharsLimitation(); @@ -707,6 +711,8 @@ private: object_ptr<Ui::IconButton> _fieldBarCancel; + std::unique_ptr<Ui::RpWidget> _topBars; + std::unique_ptr<HistoryView::TranslateBar> _translateBar; int _translateBarHeight = 0; @@ -821,6 +827,8 @@ private: const std::unique_ptr<VoiceRecordBar> _voiceRecordBar; const std::unique_ptr<ForwardPanel> _forwardPanel; std::unique_ptr<HistoryView::ComposeSearch> _composeSearch; + std::unique_ptr<HistoryView::SubsectionTabs> _subsectionTabs; + rpl::lifetime _subsectionTabsLifetime; bool _cmdStartShown = false; object_ptr<Ui::InputField> _field; base::unique_qptr<Ui::RpWidget> _fieldDisabled; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 7545a3e79f..a833cb6cd4 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_contact_status.h" #include "history/view/history_view_scheduled_section.h" #include "history/view/history_view_service_message.h" +#include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_pinned_tracker.h" #include "history/view/history_view_pinned_section.h" #include "history/view/history_view_translate_bar.h" @@ -237,6 +238,7 @@ ChatWidget::ChatWidget( : nullptr) , _topBar(this, controller) , _topBarShadow(this) +, _topBars(std::make_unique<Ui::RpWidget>(this)) , _composeControls(std::make_unique<ComposeControls>( this, ComposeControlsDescriptor{ @@ -256,7 +258,8 @@ ChatWidget::ChatWidget( }) | rpl::type_erased() : rpl::single(false), })) -, _translateBar(std::make_unique<TranslateBar>(this, controller, _history)) +, _translateBar( + std::make_unique<TranslateBar>(_topBars.get(), controller, _history)) , _scroll(std::make_unique<Ui::ScrollArea>( this, controller->chatStyle()->value(lifetime(), st::historyScroll), @@ -444,6 +447,10 @@ ChatWidget::~ChatWidget() { if (_repliesRootId) { controller()->sendingAnimation().clear(); } + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + controller()->saveSubsectionTabs(base::take(_subsectionTabs)); + } if (_topic) { if (_topic->creating()) { _emptyPainter = nullptr; @@ -471,9 +478,10 @@ void ChatWidget::orderWidgets() { if (_pinnedBar) { _pinnedBar->raise(); } - if (_topBar) { - _topBar->raise(); + if (_subsectionTabs) { + _subsectionTabs->raise(); } + _topBar->raise(); _topBarShadow->raise(); _composeControls->raisePanels(); } @@ -499,7 +507,7 @@ void ChatWidget::setupRootView() { if (_topic || !_repliesRootId) { return; } - _repliesRootView = std::make_unique<Ui::PinnedBar>(this, [=] { + _repliesRootView = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); @@ -577,7 +585,9 @@ void ChatWidget::setupTopicViewer() { void ChatWidget::subscribeToTopic() { Expects(_topic != nullptr); - _topicReopenBar = std::make_unique<TopicReopenBar>(this, _topic); + _topicReopenBar = std::make_unique<TopicReopenBar>( + _topBars.get(), + _topic); _topicReopenBar->bar().setVisible(!animatingShow()); _topicReopenBarHeight = _topicReopenBar->bar().height(); _topicReopenBar->bar().heightValue( @@ -1509,6 +1519,37 @@ void ChatWidget::edit( doSetInnerFocus(); } +void ChatWidget::validateSubsectionTabs() { + if (!HistoryView::SubsectionTabs::UsedFor(_history)) { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + return; + } else if (_subsectionTabs) { + return; + } + const auto thread = _topic ? (Data::Thread*)_topic : _sublist; + _subsectionTabs = controller()->restoreSubsectionTabsFor(this, thread); + if (!_subsectionTabs) { + _subsectionTabs = std::make_unique<HistoryView::SubsectionTabs>( + controller(), + this, + thread); + } + _subsectionTabs->removeRequests() | rpl::start_with_next([=] { + _subsectionTabs = nullptr; + updateControlsGeometry(); + }, _subsectionTabsLifetime); + _subsectionTabs->layoutRequests() | rpl::start_with_next([=] { + _inner->overrideChatMode((_subsectionTabs->leftSkip() > 0) + ? ElementChatMode::Narrow + : std::optional<ElementChatMode>()); + updateControlsGeometry(); + orderWidgets(); + }, _subsectionTabsLifetime); + updateControlsGeometry(); + orderWidgets(); +} + void ChatWidget::refreshJoinGroupButton() { if (!_repliesRootId) { return; @@ -1937,7 +1978,7 @@ void ChatWidget::checkPinnedBarState() { } clearHidingPinnedBar(); - _pinnedBar = std::make_unique<Ui::PinnedBar>(this, [=] { + _pinnedBar = std::make_unique<Ui::PinnedBar>(_topBars.get(), [=] { return controller()->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }, controller()->gifPauseLevelChanged()); @@ -2252,13 +2293,10 @@ QPixmap ChatWidget::grabForShowAnimation(const Window::SectionSlideParams ¶m if (params.withTopBarShadow) { _topBarShadow->show(); } - if (_repliesRootView) { - _repliesRootView->hide(); + _topBars->hide(); + if (_subsectionTabs) { + _subsectionTabs->hide(); } - if (_pinnedBar) { - _pinnedBar->hide(); - } - _translateBar->hide(); return result; } @@ -2529,13 +2567,20 @@ void ChatWidget::updateControlsGeometry() { : 0; _topBar->resizeToWidth(contentWidth); _topBarShadow->resize(contentWidth, st::lineWidth); + const auto tabsLeftSkip = _subsectionTabs + ? _subsectionTabs->leftSkip() + : 0; + const auto innerWidth = contentWidth - tabsLeftSkip; + const auto subsectionTabsTop = _topBar->bottomNoMargins(); + _topBars->move(tabsLeftSkip, subsectionTabsTop + + (_subsectionTabs ? _subsectionTabs->topSkip() : 0)); if (_repliesRootView) { - _repliesRootView->resizeToWidth(contentWidth); + _repliesRootView->resizeToWidth(innerWidth); } - auto top = _topBar->height() + _repliesRootViewHeight; + auto top = _repliesRootViewHeight; if (_pinnedBar) { _pinnedBar->move(0, top); - _pinnedBar->resizeToWidth(contentWidth); + _pinnedBar->resizeToWidth(innerWidth); top += _pinnedBarHeight; } if (_topicReopenBar) { @@ -2543,7 +2588,7 @@ void ChatWidget::updateControlsGeometry() { top += _topicReopenBar->bar().height(); } _translateBar->move(0, top); - _translateBar->resizeToWidth(contentWidth); + _translateBar->resizeToWidth(innerWidth); top += _translateBarHeight; auto bottom = height(); @@ -2563,15 +2608,18 @@ void ChatWidget::updateControlsGeometry() { bottom -= _composeControls->heightCurrent(); } + _topBars->resize(innerWidth, top); + top += _topBars->y(); + const auto scrollHeight = bottom - top; - const auto scrollSize = QSize(contentWidth, scrollHeight); + const auto scrollSize = QSize(innerWidth, scrollHeight); if (_scroll->size() != scrollSize) { _skipScrollEvent = true; _scroll->resize(scrollSize); _inner->resizeToWidth(scrollSize.width(), _scroll->height()); _skipScrollEvent = false; } - _scroll->move(0, top); + _scroll->move(tabsLeftSkip, top); if (!_scroll->isHidden()) { if (newScrollTop) { _scroll->scrollToY(*newScrollTop); @@ -2581,6 +2629,13 @@ void ChatWidget::updateControlsGeometry() { _composeControls->move(0, bottom); _composeControls->setAutocompleteBoundingRect(_scroll->geometry()); + if (_subsectionTabs) { + const auto scrollBottom = _scroll->y() + scrollHeight; + const auto areaHeight = scrollBottom - subsectionTabsTop; + _subsectionTabs->setBoundingRect( + { 0, subsectionTabsTop, width(), areaHeight }); + } + _cornerButtons.updatePositions(); } @@ -2700,15 +2755,9 @@ void ChatWidget::showFinishedHook() { _composeControls->showFinished(); } _inner->showFinished(); - if (_repliesRootView) { - _repliesRootView->show(); - } - if (_pinnedBar) { - _pinnedBar->show(); - } - _translateBar->show(); - if (_topicReopenBar) { - _topicReopenBar->bar().show(); + _topBars->show(); + if (_subsectionTabs) { + _subsectionTabs->show(); } // We should setup the drag area only after diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index a62a30a2e9..a4fb8fb012 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -73,6 +73,7 @@ class TopicReopenBar; class EmptyPainter; class PinnedTracker; class TranslateBar; +class SubsectionTabs; struct ChatViewId { not_null<History*> history; @@ -372,6 +373,7 @@ private: Api::SendOptions options, std::optional<MsgId> localMessageId); + void validateSubsectionTabs() override; void setupEmptyPainter(); void refreshJoinGroupButton(); [[nodiscard]] bool emptyShown() const; @@ -396,6 +398,7 @@ private: QPointer<ListWidget> _inner; object_ptr<TopBarWidget> _topBar; object_ptr<Ui::PlainShadow> _topBarShadow; + std::unique_ptr<Ui::RpWidget> _topBars; std::unique_ptr<ComposeControls> _composeControls; std::unique_ptr<ComposeSearch> _composeSearch; std::unique_ptr<Ui::FlatButton> _joinGroup; @@ -404,6 +407,8 @@ private: std::unique_ptr<Ui::FlatButton> _openChatButton; std::unique_ptr<Ui::RpWidget> _aboutHiddenAuthor; std::unique_ptr<EmptyPainter> _emptyPainter; + std::unique_ptr<SubsectionTabs> _subsectionTabs; + rpl::lifetime _subsectionTabsLifetime; bool _canSendTexts = false; bool _skipScrollEvent = false; bool _synteticScrollEvent = false; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index cef5cf4793..3e7377f7ea 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -262,8 +262,8 @@ void DefaultElementDelegate::elementHandleViaClick( not_null<UserData*> bot) { } -bool DefaultElementDelegate::elementIsChatWide() { - return false; +ElementChatMode DefaultElementDelegate::elementChatMode() { + return ElementChatMode::Default; } void DefaultElementDelegate::elementReplyTo(const FullReplyTo &to) { @@ -410,7 +410,7 @@ void UnreadBar::paint( const PaintContext &context, int y, int w, - bool chatWide) const { + ElementChatMode mode) const { const auto previousTranslation = p.transform().dx(); if (previousTranslation != 0) { p.translate(-previousTranslation, 0); @@ -434,7 +434,7 @@ void UnreadBar::paint( p.setPen(st->historyUnreadBarFg()); int maxwidth = w; - if (chatWide) { + if (mode == ElementChatMode::Wide) { maxwidth = qMin( maxwidth, st::msgMaxWidth @@ -609,9 +609,9 @@ void ServicePreMessage::init(PreparedServiceText string) { } } -int ServicePreMessage::resizeToWidth(int newWidth, bool chatWide) { +int ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) { width = newWidth; - if (chatWide) { + if (mode == ElementChatMode::Wide) { accumulate_min( width, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); @@ -644,7 +644,7 @@ void ServicePreMessage::paint( Painter &p, const PaintContext &context, QRect g, - bool chatWide) const { + ElementChatMode mode) const { const auto top = g.top() - height - st::msgMargin.top(); p.translate(0, top); @@ -987,7 +987,8 @@ not_null<PurchasedTag*> Element::enforcePurchasedTag() { int Element::AdditionalSpaceForSelectionCheckbox( not_null<const Element*> view, QRect countedGeometry) { - if (!view->hasOutLayout() || view->delegate()->elementIsChatWide()) { + if (!view->hasOutLayout() + || view->delegate()->elementChatMode() == ElementChatMode::Wide) { return 0; } if (countedGeometry.isEmpty()) { @@ -1698,7 +1699,8 @@ bool Element::hasOutLayout() const { } bool Element::hasRightLayout() const { - return hasOutLayout() && !_delegate->elementIsChatWide(); + return hasOutLayout() + && (_delegate->elementChatMode() != ElementChatMode::Wide); } bool Element::drawBubble() const { diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index f3a05a8ed2..ea4d9f0f19 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -78,6 +78,12 @@ struct SelectionModeResult { float64 progress = 0.0; }; +enum class ElementChatMode : char { + Default, + Wide, + Narrow, // monoforum with left tabs +}; + class Element; class ElementDelegate { public: @@ -114,7 +120,7 @@ public: const QString &query, const FullMsgId &context) = 0; virtual void elementHandleViaClick(not_null<UserData*> bot) = 0; - virtual bool elementIsChatWide() = 0; + virtual ElementChatMode elementChatMode() = 0; virtual not_null<Ui::PathShiftGradient*> elementPathShiftGradient() = 0; virtual void elementReplyTo(const FullReplyTo &to) = 0; virtual void elementStartInteraction(not_null<const Element*> view) = 0; @@ -169,7 +175,7 @@ public: const QString &query, const FullMsgId &context) override; void elementHandleViaClick(not_null<UserData*> bot) override; - bool elementIsChatWide() override; + ElementChatMode elementChatMode() override; void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction(not_null<const Element*> view) override; void elementStartPremium( @@ -233,7 +239,7 @@ struct UnreadBar : RuntimeComponent<UnreadBar, Element> { const PaintContext &context, int y, int w, - bool chatWide) const; + ElementChatMode mode) const; QString text; int width = 0; @@ -305,13 +311,13 @@ private: struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> { void init(PreparedServiceText string); - int resizeToWidth(int newWidth, bool chatWide); + int resizeToWidth(int newWidth, ElementChatMode mode); void paint( Painter &p, const PaintContext &context, QRect g, - bool chatWide) const; + ElementChatMode mode) const; [[nodiscard]] ClickHandlerPtr textState( QPoint point, const StateRequest &request, diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 697b96f7ea..7a86adb72c 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1898,8 +1898,10 @@ void ListWidget::elementHandleViaClick(not_null<UserData*> bot) { _delegate->listHandleViaClick(bot); } -bool ListWidget::elementIsChatWide() { - return _overrideIsChatWide.value_or(_isChatWide); +ElementChatMode ListWidget::elementChatMode() { + return _overrideChatMode.value_or(_isChatWide + ? ElementChatMode::Wide + : ElementChatMode::Default); } not_null<Ui::PathShiftGradient*> ListWidget::elementPathShiftGradient() { @@ -4284,8 +4286,8 @@ void ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) { } } -void ListWidget::overrideIsChatWide(bool isWide) { - _overrideIsChatWide = isWide; +void ListWidget::overrideChatMode(std::optional<ElementChatMode> mode) { + _overrideChatMode = mode; } ListWidget::~ListWidget() { diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.h b/Telegram/SourceFiles/history/view/history_view_list_widget.h index 71db29cd53..8f4a354b8c 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.h @@ -428,7 +428,7 @@ public: const QString &query, const FullMsgId &context) override; void elementHandleViaClick(not_null<UserData*> bot) override; - bool elementIsChatWide() override; + ElementChatMode elementChatMode() override; not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override; void elementReplyTo(const FullReplyTo &to) override; void elementStartInteraction(not_null<const Element*> view) override; @@ -443,7 +443,7 @@ public: bool elementHideTopicButton(not_null<const Element*> view) override; void setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w); - void overrideIsChatWide(bool isWide); + void overrideChatMode(std::optional<ElementChatMode> mode); ~ListWidget(); @@ -834,7 +834,7 @@ private: bool _refreshingViewer = false; bool _showFinished = false; bool _resizePending = false; - std::optional<bool> _overrideIsChatWide; + std::optional<ElementChatMode> _overrideChatMode; // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 43e97b6125..47ec86cb44 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1142,18 +1142,13 @@ void Message::draw(Painter &p, const PaintContext &context) const { } if (context.clip.intersects(QRect(0, aboveh, width(), unreadbarh))) { p.translate(0, aboveh); - bar->paint( - p, - context, - 0, - width(), - delegate()->elementIsChatWide()); + bar->paint(p, context, 0, width(), delegate()->elementChatMode()); p.translate(0, -aboveh); } } if (const auto service = Get<ServicePreMessage>()) { - service->paint(p, context, g, delegate()->elementIsChatWide()); + service->paint(p, context, g, delegate()->elementChatMode()); } if (isHidden()) { @@ -1549,8 +1544,8 @@ void Message::draw(Painter &p, const PaintContext &context) const { constexpr auto kMaxHeightRatio = 3.5; constexpr auto kStrokeWidth = 2.; constexpr auto kWaveWidth = 10.; - const auto isLeftSize = (!context.outbg) - || delegate()->elementIsChatWide(); + const auto isLeftSize = !context.outbg + || (delegate()->elementChatMode() == ElementChatMode::Wide); const auto ratio = std::min(context.gestureHorizontal.ratio, 1.); const auto reachRatio = context.gestureHorizontal.reachRatio; const auto size = st::historyFastShareSize; @@ -1635,7 +1630,8 @@ void Message::draw(Painter &p, const PaintContext &context) const { } const auto o = ScopedPainterOpacity(p, progress); const auto &st = st::msgSelectionCheck; - const auto right = delegate()->elementIsChatWide() + const auto right = (delegate()->elementChatMode() + == ElementChatMode::Wide) ? std::min( int(_bubbleWidthLimit + st::msgPhotoSkip @@ -2465,7 +2461,7 @@ bool Message::hasFromPhoto() const { case Context::AdminLog: return true; case Context::Monoforum: - return delegate()->elementIsChatWide(); + return (delegate()->elementChatMode() == ElementChatMode::Wide); case Context::History: case Context::ChatPreview: case Context::TTLViewer: @@ -2484,8 +2480,10 @@ bool Message::hasFromPhoto() const { || item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost())) { return false; - } else if (delegate()->elementIsChatWide()) { - return true; + } + const auto mode = delegate()->elementChatMode(); + if (mode != ElementChatMode::Default) { + return (mode == ElementChatMode::Wide); } else if (item->history()->peer->isVerifyCodes()) { return !hasOutLayout(); } else if (item->Has<HistoryMessageForwarded>()) { @@ -4385,12 +4383,15 @@ QRect Message::countGeometry() const { ? media->width() : width(); const auto outbg = hasOutLayout(); + const auto useMoreSpace = (delegate()->elementChatMode() + == ElementChatMode::Narrow); + const auto wideSkip = useMoreSpace + ? st::msgMargin.left() + : st::msgMargin.right(); const auto availableWidth = width() - st::msgMargin.left() - - (centeredView ? st::msgMargin.left() : st::msgMargin.right()); - auto contentLeft = hasRightLayout() - ? st::msgMargin.right() - : st::msgMargin.left(); + - (centeredView ? st::msgMargin.left() : wideSkip); + auto contentLeft = hasRightLayout() ? wideSkip : st::msgMargin.left(); auto contentWidth = availableWidth; if (hasFromPhoto()) { contentLeft += st::msgPhotoSkip; @@ -4411,7 +4412,8 @@ QRect Message::countGeometry() const { contentWidth = mediaWidth; } } - if (contentWidth < availableWidth && !delegate()->elementIsChatWide()) { + if (contentWidth < availableWidth + && delegate()->elementChatMode() != ElementChatMode::Wide) { if (outbg) { contentLeft += availableWidth - contentWidth; } else if (centeredView) { @@ -4500,7 +4502,7 @@ int Message::resizeContentGetHeight(int newWidth) { auto newHeight = minHeight(); if (const auto service = Get<ServicePreMessage>()) { - service->resizeToWidth(newWidth, delegate()->elementIsChatWide()); + service->resizeToWidth(newWidth, delegate()->elementChatMode()); } const auto botTop = item->isFakeAboutView() @@ -4515,9 +4517,14 @@ int Message::resizeContentGetHeight(int newWidth) { // This code duplicates countGeometry() but also resizes media. const auto centeredView = item->isFakeAboutView() || (context() == Context::Replies && item->isDiscussionPost()); + const auto useMoreSpace = (delegate()->elementChatMode() + == ElementChatMode::Narrow); + const auto wideSkip = useMoreSpace + ? st::msgMargin.left() + : st::msgMargin.right(); auto contentWidth = newWidth - st::msgMargin.left() - - (centeredView ? st::msgMargin.left() : st::msgMargin.right()); + - (centeredView ? st::msgMargin.left() : wideSkip); if (hasFromPhoto()) { if (const auto size = rightActionSize()) { contentWidth -= size->width() + (st::msgPhotoSkip - st::historyFastShareSize); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index d215be432a..02470dc925 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -423,7 +423,7 @@ bool Service::consumeHorizontalScroll(QPoint position, int delta) { QRect Service::countGeometry() const { auto result = QRect(0, 0, width(), height()); - if (delegate()->elementIsChatWide()) { + if (delegate()->elementChatMode() == ElementChatMode::Wide) { result.setWidth(qMin(result.width(), st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } auto margins = st::msgServiceMargin; @@ -469,7 +469,7 @@ QSize Service::performCountCurrentSize(int newWidth) { + media->resizeGetHeight(newWidth) + st::msgServiceMargin.bottom(); } else if (!text().isEmpty()) { - if (delegate()->elementIsChatWide()) { + if (delegate()->elementChatMode() == ElementChatMode::Wide) { accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins @@ -561,7 +561,7 @@ void Service::draw(Painter &p, const PaintContext &context) const { context, 0, width(), - delegate()->elementIsChatWide()); + delegate()->elementChatMode()); p.translate(0, -aboveh); } } diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp new file mode 100644 index 0000000000..9d942cb026 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -0,0 +1,384 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_subsection_tabs.h" + +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" +#include "data/data_forum.h" +#include "data/data_forum_topic.h" +#include "data/data_saved_messages.h" +#include "data/data_saved_sublist.h" +#include "data/data_session.h" +#include "data/data_thread.h" +#include "dialogs/dialogs_main_list.h" +#include "history/history.h" +#include "lang/lang_keys.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/shadow.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" + +namespace HistoryView { +namespace { + +constexpr auto kDefaultLimit = 10; + +} // namespace + +SubsectionTabs::SubsectionTabs( + not_null<Window::SessionController*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread) +: _controller(controller) +, _history(thread->owningHistory()) +, _active(thread) +, _around(thread) +, _beforeLimit(kDefaultLimit) +, _afterLimit(kDefaultLimit) { + track(); + refreshSlice(); + setupHorizontal(parent); +} + +SubsectionTabs::~SubsectionTabs() { + delete base::take(_horizontal); + delete base::take(_vertical); + delete base::take(_shadow); +} + +void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { + delete base::take(_vertical); + _horizontal = Ui::CreateChild<Ui::RpWidget>(parent); + _horizontal->show(); + + if (!_shadow) { + _shadow = Ui::CreateChild<Ui::PlainShadow>(parent); + _shadow->show(); + } + + const auto toggle = Ui::CreateChild<Ui::IconButton>( + _horizontal, + st::chatTabsToggle); + toggle->show(); + toggle->setClickedCallback([=] { + toggleModes(); + }); + toggle->move(0, 0); + const auto scroll = Ui::CreateChild<Ui::ScrollArea>( + _horizontal, + st::chatTabsScroll, + true); + scroll->show(); + const auto tabs = scroll->setOwnedWidget( + object_ptr<Ui::SettingsSlider>(scroll, st::chatTabsSlider)); + tabs->sectionActivated() | rpl::start_with_next([=](int active) { + if (active >= 0 + && active < _slice.size() + && _active != _slice[active]) { + auto params = Window::SectionShow(); + params.way = Window::SectionShow::Way::ClearStack; + params.animated = anim::type::instant; + _controller->showThread(_slice[active], {}, params); + } + }, tabs->lifetime()); + + _horizontal->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto togglew = toggle->width(); + const auto height = size.height(); + scroll->setGeometry(togglew, 0, size.width() - togglew, height); + }, scroll->lifetime()); + + _horizontal->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(_horizontal).fillRect(clip, st::windowBg); + }, _horizontal->lifetime()); + + _refreshed.events_starting_with_copy( + rpl::empty + ) | rpl::start_with_next([=] { + auto sections = std::vector<TextWithEntities>(); + const auto manager = &_history->owner().customEmojiManager(); + auto activeIndex = -1; + for (const auto &thread : _slice) { + if (thread == _active) { + activeIndex = int(sections.size()); + } + if (const auto topic = thread->asTopic()) { + sections.push_back(topic->titleWithIcon()); + } else if (const auto sublist = thread->asSublist()) { + const auto peer = sublist->sublistPeer(); + sections.push_back(TextWithEntities().append( + Ui::Text::SingleCustomEmoji( + manager->peerUserpicEmojiData(peer), + u"@"_q) + ).append(' ').append(peer->shortName())); + } else { + sections.push_back(tr::lng_filters_all_short( + tr::now, + Ui::Text::WithEntities)); + } + } + tabs->setSections(sections, Core::TextContext({ + .session = &_history->session(), + })); + tabs->fitWidthToSections(); + tabs->setActiveSectionFast(activeIndex); + _horizontal->resize( + tabs->width(), + std::max(toggle->height(), tabs->height())); + }, _horizontal->lifetime()); +} + +void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { + delete base::take(_horizontal); + _vertical = Ui::CreateChild<Ui::RpWidget>(parent); + _vertical->show(); + + if (!_shadow) { + _shadow = Ui::CreateChild<Ui::PlainShadow>(parent); + _shadow->show(); + } + + const auto toggle = Ui::CreateChild<Ui::IconButton>( + _vertical, + st::chatTabsToggle); + toggle->show(); + const auto active = &st::chatTabsToggleActive; + toggle->setIconOverride(active, active); + toggle->setClickedCallback([=] { + toggleModes(); + }); + toggle->move(0, 0); + const auto scroll = Ui::CreateChild<Ui::ScrollArea>(_vertical); + scroll->show(); + + _vertical->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto toggleh = toggle->height(); + const auto width = size.width(); + scroll->setGeometry(0, toggleh, width, size.height() - toggleh); + }, scroll->lifetime()); + + _vertical->paintRequest() | rpl::start_with_next([=](QRect clip) { + QPainter(_vertical).fillRect(clip, st::windowBg); + }, _vertical->lifetime()); + + _refreshed.events_starting_with_copy( + rpl::empty + ) | rpl::start_with_next([=] { + _vertical->resize(std::max(toggle->width(), 0), 0); + }, _vertical->lifetime()); +} + +void SubsectionTabs::toggleModes() { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + setupVertical(_horizontal->parentWidget()); + } else { + setupHorizontal(_vertical->parentWidget()); + } + _layoutRequests.fire({}); +} + +rpl::producer<> SubsectionTabs::removeRequests() const { + if (const auto forum = _history->peer->forum()) { + return forum->destroyed(); + } else if (const auto monoforum = _history->peer->monoforum()) { + return monoforum->destroyed(); + } else { + Unexpected("Peer in SubsectionTabs::removeRequests."); + } +} + +void SubsectionTabs::extractToParent(not_null<Ui::RpWidget*> parent) { + Expects((_horizontal || _vertical) && _shadow); + + if (_vertical) { + _vertical->hide(); + _vertical->setParent(parent); + } else { + _horizontal->hide(); + _horizontal->setParent(parent); + } + _shadow->hide(); + _shadow->setParent(parent); +} + +void SubsectionTabs::setBoundingRect(QRect boundingRect) { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + _horizontal->setGeometry( + boundingRect.x(), + boundingRect.y(), + boundingRect.width(), + _horizontal->height()); + _shadow->setGeometry( + boundingRect.x(), + _horizontal->y() + _horizontal->height(), + boundingRect.width(), + st::lineWidth); + } else { + _vertical->setGeometry( + boundingRect.x(), + boundingRect.y(), + _vertical->width(), + boundingRect.height()); + _shadow->setGeometry( + _vertical->x() + _vertical->width(), + boundingRect.y(), + st::lineWidth, + boundingRect.height()); + } +} + +rpl::producer<> SubsectionTabs::layoutRequests() const { + return _layoutRequests.events(); +} + +int SubsectionTabs::leftSkip() const { + return _vertical ? _vertical->width() : 0; +} + +int SubsectionTabs::topSkip() const { + return _horizontal ? _horizontal->height() : 0; +} + +void SubsectionTabs::raise() { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + _horizontal->raise(); + } else { + _vertical->raise(); + } + _shadow->raise(); +} + +void SubsectionTabs::show() { + setVisible(true); +} + +void SubsectionTabs::hide() { + setVisible(false); +} + +void SubsectionTabs::setVisible(bool shown) { + Expects((_horizontal || _vertical) && _shadow); + + if (_horizontal) { + _horizontal->setVisible(shown); + } else { + _vertical->setVisible(shown); + } + _shadow->setVisible(shown); +} + +void SubsectionTabs::track() { + if (const auto forum = _history->peer->forum()) { + forum->topicDestroyed( + ) | rpl::start_with_next([=](not_null<Data::ForumTopic*> topic) { + if (_around == topic) { + _around = _history; + refreshSlice(); + } + }, _lifetime); + } else if (const auto monoforum = _history->peer->monoforum()) { + monoforum->sublistDestroyed( + ) | rpl::start_with_next([=](not_null<Data::SavedSublist*> sublist) { + if (_around == sublist) { + _around = _history; + refreshSlice(); + } + }, _lifetime); + } else { + Unexpected("Peer in SubsectionTabs::track."); + } +} + +void SubsectionTabs::refreshSlice() { + const auto forum = _history->peer->forum(); + const auto monoforum = _history->peer->monoforum(); + Assert(forum || monoforum); + + const auto list = forum + ? forum->topicsList() + : monoforum->chatsList(); + auto slice = std::vector<not_null<Data::Thread*>>(); + const auto guard = gsl::finally([&] { + if (_slice != slice) { + _slice = std::move(slice); + _refreshed.fire({}); + } + }); + if (!list) { + slice.push_back(_history); + return; + } + const auto &chats = list->indexed()->all(); + auto i = (_around == _history) + ? chats.end() + : ranges::find(chats, _around, [](not_null<Dialogs::Row*> row) { + return not_null(row->thread()); + }); + if (i == chats.end()) { + i = chats.begin(); + } + const auto takeBefore = std::min(_beforeLimit, int(i - chats.begin())); + const auto takeAfter = std::min(_afterLimit, int(chats.end() - i)); + const auto from = i - takeBefore; + const auto till = i + takeAfter; + _beforeSkipped = std::max(0, int(from - chats.begin())); + _afterSkipped = list->loaded() + ? std::max(0, int(chats.end() - till)) + : std::optional<int>(); + if (from == chats.begin()) { + slice.push_back(_history); + } + for (auto i = from; i != till; ++i) { + slice.push_back((*i)->thread()); + } +} + +bool SubsectionTabs::switchTo( + not_null<Data::Thread*> thread, + not_null<Ui::RpWidget*> parent) { + Expects((_horizontal || _vertical) && _shadow); + + if (thread->owningHistory() != _history) { + return false; + } + if (_vertical) { + _vertical->setParent(parent); + _vertical->show(); + } else { + _horizontal->setParent(parent); + _horizontal->show(); + } + _shadow->setParent(parent); + _shadow->show(); + return true; +} + +bool SubsectionTabs::UsedFor(not_null<Data::Thread*> thread) { + const auto history = thread->owningHistory(); + if (history->amMonoforumAdmin()) { + return true; + } + const auto channel = history->peer->asChannel(); + return channel + && channel->isForum() + && ((channel->flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug(); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h new file mode 100644 index 0000000000..d896b1778c --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -0,0 +1,84 @@ +/* +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 + +class History; + +namespace Data { +class Thread; +} // namespace Data + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace HistoryView { + +class SubsectionTabs final { +public: + SubsectionTabs( + not_null<Window::SessionController*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread); + ~SubsectionTabs(); + + [[nodiscard]] bool switchTo( + not_null<Data::Thread*> thread, + not_null<Ui::RpWidget*> parent); + + [[nodiscard]] static bool UsedFor(not_null<Data::Thread*> thread); + + [[nodiscard]] rpl::producer<> removeRequests() const; + + void extractToParent(not_null<Ui::RpWidget*> parent); + + void setBoundingRect(QRect boundingRect); + [[nodiscard]] rpl::producer<> layoutRequests() const; + [[nodiscard]] int leftSkip() const; + [[nodiscard]] int topSkip() const; + + void raise(); + void show(); + void hide(); + +private: + void track(); + void setupHorizontal(not_null<QWidget*> parent); + void setupVertical(not_null<QWidget*> parent); + void toggleModes(); + void setVisible(bool shown); + void refreshSlice(); + + const not_null<Window::SessionController*> _controller; + const not_null<History*> _history; + + Ui::RpWidget *_horizontal = nullptr; + Ui::RpWidget *_vertical = nullptr; + Ui::RpWidget *_shadow = nullptr; + + std::vector<not_null<Data::Thread*>> _slice; + + not_null<Data::Thread*> _active; + not_null<Data::Thread*> _around; + int _beforeLimit = 0; + int _afterLimit = 0; + std::optional<int> _beforeSkipped; + std::optional<int> _afterSkipped; + + rpl::event_stream<> _layoutRequests; + rpl::event_stream<> _refreshed; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index dd760a3e07..6a902916eb 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -2193,9 +2193,10 @@ Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( }) | rpl::distinct_until_changed(); auto viewDirect = [=] { if (const auto linked = channel->monoforumLink()) { - if (const auto monoforum = linked->monoforum()) { - window->showMonoforum(monoforum); - } + window->showPeerHistory(linked); + //if (const auto monoforum = linked->monoforum()) { + // window->showMonoforum(monoforum); + //} } }; AddMainButton( // #TODO monoforum diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index e2de892e69..b759bdcfd4 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1516,7 +1516,7 @@ void MainWidget::showHistory( : Window::SlideDirection::FromRight, animationParams); } else { - _history->show(); + _history->showFast(); crl::on_main(this, [=] { _controller->widget()->setInnerFocus(); }); @@ -1536,6 +1536,8 @@ void MainWidget::showHistory( } floatPlayerCheckVisibility(); + + controller()->dropSubsectionTabs(); } void MainWidget::showMessage( diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index db5a4d99ad..8963cff5b3 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -373,7 +373,7 @@ ShortcutMessages::ShortcutMessages( this, &controller->session(), static_cast<ListDelegate*>(this)); - _inner->overrideIsChatWide(false); + _inner->overrideChatMode(ElementChatMode::Default); _scroll->sizeValue() | rpl::filter([](QSize size) { return !size.isEmpty(); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index ef30b941de..76b725da93 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1253,3 +1253,34 @@ newPeerUserpicsPadding: margins(0px, 3px, 0px, 0px); newPeerWidth: 320px; swipeBackSize: 150px; + +chatTabsToggle: IconButton(defaultIconButton) { + width: 56px; + height: 36px; + icon: icon {{ "top_bar_profile-flip_horizontal", menuIconFg }}; + iconOver: icon {{ "top_bar_profile-flip_horizontal", menuIconFgOver }}; + ripple: emptyRippleAnimation; +} +chatTabsToggleActive: icon {{ "top_bar_profile-flip_horizontal", windowActiveTextFg }}; +chatTabsScroll: ScrollArea(defaultScrollArea) { + barHidden: true; +} +chatTabsSlider: SettingsSlider(defaultSettingsSlider) { + padding: 0px; + height: 36px; + barTop: 33px; + barSkip: 0px; + barStroke: 6px; + barRadius: 2px; + barFg: transparent; + barSnapToLabel: true; + strictSkip: 18px; + labelTop: 9px; + labelStyle: semiboldTextStyle; + labelFg: windowSubTextFg; + labelFgActive: lightButtonFg; + rippleBottomSkip: 1px; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + ripple: defaultRippleAnimation; +} diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index 5a72a69847..95944d2f90 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -279,6 +279,7 @@ void SectionWidget::setGeometryWithTopMoved( void SectionWidget::showAnimated( SlideDirection direction, const SectionSlideParams ¶ms) { + validateSubsectionTabs(); if (_showAnimation) { return; } @@ -309,6 +310,7 @@ std::shared_ptr<SectionMemento> SectionWidget::createMemento() { } void SectionWidget::showFast() { + validateSubsectionTabs(); show(); showFinished(); } diff --git a/Telegram/SourceFiles/window/section_widget.h b/Telegram/SourceFiles/window/section_widget.h index 24761f0971..b1d6f41cbc 100644 --- a/Telegram/SourceFiles/window/section_widget.h +++ b/Telegram/SourceFiles/window/section_widget.h @@ -194,6 +194,9 @@ public: return nullptr; } + virtual void validateSubsectionTabs() { + } + static void PaintBackground( not_null<SessionController*> controller, not_null<Ui::ChatTheme*> theme, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index a505ce3e8c..b32b8c6b04 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL //#include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_chat_section.h" #include "history/view/history_view_scheduled_section.h" +#include "history/view/history_view_subsection_tabs.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" #include "data/stickers/data_custom_emoji.h" @@ -3440,8 +3441,37 @@ std::shared_ptr<ChatHelpers::Show> SessionController::uiShow() { return _cachedShow; } +void SessionController::saveSubsectionTabs( + std::unique_ptr<HistoryView::SubsectionTabs> tabs) { + _savedSubsectionTabsLifetime.destroy(); + _savedSubsectionTabs = std::move(tabs); + _savedSubsectionTabs->extractToParent(widget()); + _savedSubsectionTabs->removeRequests() | rpl::start_with_next([=] { + _savedSubsectionTabs = nullptr; + }, _savedSubsectionTabsLifetime); +} + +auto SessionController::restoreSubsectionTabsFor( + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread) +-> std::unique_ptr<HistoryView::SubsectionTabs> { + if (!_savedSubsectionTabs) { + return nullptr; + } else if (_savedSubsectionTabs->switchTo(thread, parent)) { + _savedSubsectionTabsLifetime.destroy(); + return base::take(_savedSubsectionTabs); + } + return nullptr; +} + +void SessionController::dropSubsectionTabs() { + _savedSubsectionTabsLifetime.destroy(); + base::take(_savedSubsectionTabs); +} + SessionController::~SessionController() { resetFakeUnreadWhileOpened(); + dropSubsectionTabs(); } bool CheckAndJumpToNearChatsFilter( diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 391fbbafd3..151fa7de7a 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -78,6 +78,10 @@ class SavedSublist; class WallPaper; } // namespace Data +namespace HistoryView { +class SubsectionTabs; +} // namespace HistoryView + namespace HistoryView::Reactions { class CachedIconFactory; } // namespace HistoryView::Reactions @@ -659,6 +663,14 @@ public: [[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow() override; + void saveSubsectionTabs( + std::unique_ptr<HistoryView::SubsectionTabs> tabs); + [[nodiscard]] auto restoreSubsectionTabsFor( + not_null<Ui::RpWidget*> parent, + not_null<Data::Thread*> thread) + -> std::unique_ptr<HistoryView::SubsectionTabs>; + void dropSubsectionTabs(); + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } @@ -774,6 +786,8 @@ private: base::has_weak_ptr _storyOpenGuard; QString _premiumRef; + std::unique_ptr<HistoryView::SubsectionTabs> _savedSubsectionTabs; + rpl::lifetime _savedSubsectionTabsLifetime; rpl::lifetime _lifetime; From 72b57924b726fc97b47296bffff09babf819554e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 May 2025 17:33:47 +0400 Subject: [PATCH 074/310] Correctly load tab slices. --- .../SourceFiles/history/history_widget.cpp | 16 --- .../view/history_view_subsection_tabs.cpp | 130 +++++++++++++++++- .../view/history_view_subsection_tabs.h | 6 + .../ui/widgets/discrete_sliders.cpp | 29 +++- .../SourceFiles/ui/widgets/discrete_sliders.h | 11 +- 5 files changed, 164 insertions(+), 28 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index a295569910..6b5ee03337 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -7657,10 +7657,6 @@ void HistoryWidget::setupTranslateBar() { }, _translateBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _translateBar->hide(); - } } void HistoryWidget::setupPinnedTracker() { @@ -7803,10 +7799,6 @@ void HistoryWidget::checkPinnedBarState() { }, _pinnedBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _pinnedBar->hide(); - } } void HistoryWidget::clearHidingPinnedBar() { @@ -7966,10 +7958,6 @@ void HistoryWidget::setupGroupCallBar() { }, _groupCallBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _groupCallBar->hide(); - } } void HistoryWidget::setupRequestsBar() { @@ -8013,10 +8001,6 @@ void HistoryWidget::setupRequestsBar() { }, _requestsBar->lifetime()); orderWidgets(); - - if (_showAnimation) { - _requestsBar->hide(); - } } void HistoryWidget::requestMessageData(MsgId msgId) { diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 9d942cb026..a0b99c395c 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -30,7 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { -constexpr auto kDefaultLimit = 10; +constexpr auto kDefaultLimit = 5;AssertIsDebug()// 10; } // namespace @@ -91,6 +91,54 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { } }, tabs->lifetime()); + scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) { + const auto pixelDelta = e->pixelDelta(); + const auto angleDelta = e->angleDelta(); + if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) { + return false; + } + const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y(); + scroll->scrollToX(scroll->scrollLeft() - y); + return true; + }); + + rpl::merge( + scroll->scrolls(), + _scrollCheckRequests.events(), + scroll->widthValue() | rpl::skip(1) | rpl::map_to(rpl::empty) + ) | rpl::start_with_next([=] { + const auto width = scroll->width(); + const auto left = scroll->scrollLeft(); + const auto max = scroll->scrollLeftMax(); + const auto availableLeft = left; + const auto availableRight = (max - left); + if (max <= 2 * width && _afterAvailable > 0) { + _beforeLimit *= 2; + _afterLimit *= 2; + } + if (availableLeft < width + && _beforeSkipped.value_or(0) > 0 + && !_slice.empty()) { + _around = _slice.front(); + refreshSlice(); + } else if (availableRight < width) { + if (_afterAvailable > 0) { + _around = _slice.back(); + refreshSlice(); + } else if (!_afterSkipped.has_value()) { + _loading = true; + loadMore(); + } + } + }, _horizontal->lifetime()); + + dataChanged() | rpl::start_with_next([=] { + if (_loading) { + _loading = false; + refreshSlice(); + } + }, _horizontal->lifetime()); + _horizontal->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto togglew = toggle->width(); @@ -127,14 +175,62 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { Ui::Text::WithEntities)); } } + const auto paused = [=] { + return _controller->isGifPausedAtLeastFor( + Window::GifPauseReason::Any); + }; + + auto scrollSavingThread = (Data::Thread*)nullptr; + auto scrollSavingShift = 0; + auto scrollSavingIndex = -1; + if (const auto count = tabs->sectionsCount()) { + const auto scrollLeft = scroll->scrollLeft(); + auto indexLeft = tabs->lookupSectionLeft(0); + for (auto index = 0; index != count; ++index) { + const auto nextLeft = (index + 1 != count) + ? tabs->lookupSectionLeft(index + 1) + : (indexLeft + scrollLeft + 1); + if (indexLeft <= scrollLeft && nextLeft > scrollLeft) { + scrollSavingThread = _sectionsSlice[index]; + scrollSavingShift = scrollLeft - indexLeft; + break; + } + indexLeft = nextLeft; + } + scrollSavingIndex = scrollSavingThread + ? int(ranges::find(_slice, not_null(scrollSavingThread)) + - begin(_slice)) + : -1; + if (scrollSavingIndex == _slice.size()) { + scrollSavingIndex = -1; + for (auto index = 0; index != count; ++index) { + const auto thread = _sectionsSlice[index]; + if (ranges::contains(_slice, thread)) { + scrollSavingThread = thread; + scrollSavingShift = scrollLeft + - tabs->lookupSectionLeft(index); + scrollSavingIndex = index; + break; + } + } + } + } + tabs->setSections(sections, Core::TextContext({ .session = &_history->session(), - })); + }), paused); tabs->fitWidthToSections(); tabs->setActiveSectionFast(activeIndex); + _sectionsSlice = _slice; _horizontal->resize( - tabs->width(), + _horizontal->width(), std::max(toggle->height(), tabs->height())); + if (scrollSavingIndex >= 0) { + scroll->scrollToX(tabs->lookupSectionLeft(scrollSavingIndex) + + scrollSavingShift); + } + + _scrollCheckRequests.fire({}); }, _horizontal->lifetime()); } @@ -179,6 +275,26 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { }, _vertical->lifetime()); } +void SubsectionTabs::loadMore() { + if (const auto forum = _history->peer->forum()) { + forum->requestTopics(); + } else if (const auto monoforum = _history->peer->monoforum()) { + monoforum->loadMore(); + } else { + Unexpected("Peer in SubsectionTabs::loadMore."); + } +} + +rpl::producer<> SubsectionTabs::dataChanged() const { + if (const auto forum = _history->peer->forum()) { + return forum->chatsListChanges(); + } else if (const auto monoforum = _history->peer->monoforum()) { + return monoforum->chatsListChanges(); + } else { + Unexpected("Peer in SubsectionTabs::dataChanged."); + } +} + void SubsectionTabs::toggleModes() { Expects((_horizontal || _vertical) && _shadow); @@ -323,6 +439,8 @@ void SubsectionTabs::refreshSlice() { }); if (!list) { slice.push_back(_history); + _beforeSkipped = _afterSkipped = 0; + _afterAvailable = 0; return; } const auto &chats = list->indexed()->all(); @@ -339,9 +457,8 @@ void SubsectionTabs::refreshSlice() { const auto from = i - takeBefore; const auto till = i + takeAfter; _beforeSkipped = std::max(0, int(from - chats.begin())); - _afterSkipped = list->loaded() - ? std::max(0, int(chats.end() - till)) - : std::optional<int>(); + _afterAvailable = std::max(0, int(chats.end() - till)); + _afterSkipped = list->loaded() ? _afterAvailable : std::optional<int>(); if (from == chats.begin()) { slice.push_back(_history); } @@ -358,6 +475,7 @@ bool SubsectionTabs::switchTo( if (thread->owningHistory() != _history) { return false; } + _active = thread; if (_vertical) { _vertical->setParent(parent); _vertical->show(); diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index d896b1778c..fe5054dbe5 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -57,6 +57,8 @@ private: void toggleModes(); void setVisible(bool shown); void refreshSlice(); + void loadMore(); + [[nodiscard]] rpl::producer<> dataChanged() const; const not_null<Window::SessionController*> _controller; const not_null<History*> _history; @@ -66,16 +68,20 @@ private: Ui::RpWidget *_shadow = nullptr; std::vector<not_null<Data::Thread*>> _slice; + std::vector<not_null<Data::Thread*>> _sectionsSlice; not_null<Data::Thread*> _active; not_null<Data::Thread*> _around; int _beforeLimit = 0; int _afterLimit = 0; + int _afterAvailable = 0; + bool _loading = false; std::optional<int> _beforeSkipped; std::optional<int> _afterSkipped; rpl::event_stream<> _layoutRequests; rpl::event_stream<> _refreshed; + rpl::event_stream<> _scrollCheckRequests; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp index d55da801d3..3c80be1720 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp @@ -50,6 +50,7 @@ void DiscreteSlider::setActiveSectionFast(int index) { void DiscreteSlider::finishAnimating() { _a_left.stop(); + _a_width.stop(); update(); _callbackAfterMs = 0; if (_timerId >= 0) { @@ -64,10 +65,24 @@ void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) { } } +int DiscreteSlider::sectionsCount() const { + return int(_sections.size()); +} + +int DiscreteSlider::lookupSectionLeft(int index) const { + Expects(index >= 0 && index < _sections.size()); + + return _sections[index].left; +} + void DiscreteSlider::setSelectOnPress(bool selectOnPress) { _selectOnPress = selectOnPress; } +bool DiscreteSlider::paused() const { + return _paused && _paused(); +} + std::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() { return _sections; } @@ -97,7 +112,8 @@ void DiscreteSlider::setSections(const std::vector<QString> &labels) { void DiscreteSlider::setSections( const std::vector<TextWithEntities> &labels, - Text::MarkedContext context) { + Text::MarkedContext context, + Fn<bool()> paused) { Assert(!labels.empty()); context.repaint = [this] { update(); }; @@ -106,6 +122,7 @@ void DiscreteSlider::setSections( for (const auto &label : labels) { _sections.push_back(Section(label, getLabelStyle(), context)); } + _paused = std::move(paused); refresh(); } @@ -122,7 +139,9 @@ void DiscreteSlider::refresh() { } DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const { - const auto raw = _sections.empty() ? nullptr : &_sections[_selected]; + const auto raw = (_sections.empty() || _selected < 0) + ? nullptr + : &_sections[_selected]; if (!raw) { return { 0, 0 }; } @@ -193,7 +212,7 @@ void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) { } void DiscreteSlider::setSelectedSection(int index) { - if (index < 0 || index >= _sections.size()) { + if (index >= int(_sections.size())) { return; } @@ -414,9 +433,10 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { : section.width; const auto activeLeft = section.left + (section.width - activeWidth) / 2; + const auto divider = std::max(std::min(activeWidth, range.width), 1); const auto active = 1. - std::clamp( - std::abs(range.left - activeLeft) / float64(range.width), + std::abs(range.left - activeLeft) / float64(divider), 0., 1.); if (section.ripple) { @@ -467,6 +487,7 @@ void SettingsSlider::paintEvent(QPaintEvent *e) { .position = QPoint(labelLeft, _st.labelTop), .outerWidth = width(), .availableWidth = section.label.maxWidth(), + .paused = paused(), }); } return true; diff --git a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h index 4e9476bb02..6c7e6405bb 100644 --- a/Telegram/SourceFiles/ui/widgets/discrete_sliders.h +++ b/Telegram/SourceFiles/ui/widgets/discrete_sliders.h @@ -37,7 +37,8 @@ public: void setSections(const std::vector<QString> &labels); void setSections( const std::vector<TextWithEntities> &labels, - Text::MarkedContext context = {}); + Text::MarkedContext context = {}, + Fn<bool()> paused = nullptr); int activeSection() const { return _activeIndex; } @@ -51,6 +52,9 @@ public: return _sectionActivated.events(); } + [[nodiscard]] int sectionsCount() const; + [[nodiscard]] int lookupSectionLeft(int index) const; + protected: void timerEvent(QTimerEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -98,7 +102,9 @@ protected: void setSelectOnPress(bool selectOnPress); - std::vector<Section> §ionsRef(); + [[nodiscard]] std::vector<Section> §ionsRef(); + + [[nodiscard]] bool paused() const; private: void activateCallback(); @@ -109,6 +115,7 @@ private: void setSelectedSection(int index); std::vector<Section> _sections; + Fn<bool()> _paused; int _activeIndex = 0; bool _selectOnPress = true; bool _snapToLabel = false; From e0e69ce740e0f66f1efcc8fa3954f7b3d3bfcba2 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 23 May 2025 21:12:03 +0400 Subject: [PATCH 075/310] Support vertical tabs somehow. --- .../view/history_view_subsection_tabs.cpp | 442 +++++++++++++++++- Telegram/SourceFiles/ui/chat/chat.style | 40 ++ 2 files changed, 469 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index a0b99c395c..cdc024c3d5 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -16,21 +16,304 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_thread.h" +#include "data/data_user.h" #include "dialogs/dialogs_main_list.h" #include "history/history.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" namespace HistoryView { namespace { -constexpr auto kDefaultLimit = 5;AssertIsDebug()// 10; +constexpr auto kDefaultLimit = 5; AssertIsDebug()// 10; +constexpr auto kMaxNameLines = 3; + +class VerticalSlider final : public Ui::RpWidget { +public: + explicit VerticalSlider(not_null<QWidget*> parent); + + struct Section { + std::shared_ptr<Ui::DynamicImage> userpic; + QString text; + }; + + void setSections(std::vector<Section> sections, Fn<bool()> paused); + void setActiveSectionFast(int active); + + void fitHeightToSections(); + + [[nodiscard]] rpl::producer<int> sectionActivated() const { + return _sectionActivated.events(); + } + + [[nodiscard]] int sectionsCount() const; + [[nodiscard]] int lookupSectionTop(int index) const; + +private: + struct Tab { + std::shared_ptr<Ui::DynamicImage> userpic; + Ui::Text::String text; + std::unique_ptr<Ui::RippleAnimation> ripple; + int top = 0; + int height = 0; + bool subscribed = false; + }; + struct Range { + int top = 0; + int height = 0; + }; + + void paintEvent(QPaintEvent *e) override; + void timerEvent(QTimerEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + + void startRipple(int index); + [[nodiscard]] int getIndexFromPosition(QPoint position) const; + [[nodiscard]] QImage prepareRippleMask(int index, const Tab &tab); + + void activateCallback(); + [[nodiscard]] Range getFinalActiveRange() const; + + const style::ChatTabsVertical &_st; + Ui::RoundRect _bar; + std::vector<Tab> _tabs; + int _active = -1; + int _pressed = -1; + Ui::Animations::Simple _activeTop; + Ui::Animations::Simple _activeHeight; + + int _timerId = -1; + crl::time _callbackAfterMs = 0; + + rpl::event_stream<int> _sectionActivated; + Fn<bool()> _paused; + +}; + +VerticalSlider::VerticalSlider(not_null<QWidget*> parent) +: RpWidget(parent) +, _st(st::chatTabsVertical) +, _bar(_st.barRadius, _st.barFg) { + setCursor(style::cur_pointer); +} + +void VerticalSlider::setSections( + std::vector<Section> sections, + Fn<bool()> paused) { + auto old = base::take(_tabs); + _tabs.reserve(sections.size()); + + for (auto §ion : sections) { + const auto i = ranges::find(old, section.userpic, &Tab::userpic); + if (i != end(old)) { + _tabs.push_back(std::move(*i)); + old.erase(i); + } else { + _tabs.push_back({ .userpic = std::move(section.userpic), }); + } + _tabs.back().text = Ui::Text::String( + _st.nameStyle, + section.text, + kDefaultTextOptions, + _st.nameWidth); + } + for (const auto &was : old) { + if (was.subscribed) { + was.userpic->subscribeToUpdates(nullptr); + } + } +} + +void VerticalSlider::setActiveSectionFast(int active) { + _active = active; + _activeTop.stop(); + _activeHeight.stop(); +} + +void VerticalSlider::fitHeightToSections() { + auto top = 0; + for (auto &tab : _tabs) { + tab.top = top; + tab.height = _st.baseHeight + std::min( + _st.nameStyle.font->height * kMaxNameLines, + tab.text.countHeight(_st.nameWidth, true)); + top += tab.height; + } + resize(_st.width, top); +} + +int VerticalSlider::sectionsCount() const { + return int(_tabs.size()); +} + +int VerticalSlider::lookupSectionTop(int index) const { + Expects(index >= 0 && index < _tabs.size()); + + return _tabs[index].top; +} + +VerticalSlider::Range VerticalSlider::getFinalActiveRange() const { + return (_active >= 0) + ? Range{ _tabs[_active].top, _tabs[_active].height } + : Range(); +} + +void VerticalSlider::paintEvent(QPaintEvent *e) { + const auto finalRange = getFinalActiveRange(); + const auto range = Range{ + int(base::SafeRound(_activeTop.value(finalRange.top))), + int(base::SafeRound(_activeHeight.value(finalRange.height))), + }; + + auto p = QPainter(this); + auto clip = e->rect(); + const auto drawRect = [&](QRect rect) { + _bar.paint(p, rect); + }; + const auto nameLeft = (_st.width - _st.nameWidth) / 2; + for (auto &tab : _tabs) { + if (!clip.intersects(QRect(0, tab.top, width(), tab.height))) { + continue; + } + const auto divider = std::max(std::min(tab.height, range.height), 1); + const auto active = 1. + - std::clamp( + std::abs(range.top - tab.top) / float64(divider), + 0., + 1.); + if (tab.ripple) { + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + tab.ripple->paint(p, 0, tab.top, width(), &color); + if (tab.ripple->empty()) { + tab.ripple.reset(); + } + } + + if (!tab.subscribed) { + tab.subscribed = true; + tab.userpic->subscribeToUpdates([=] { update(); }); + } + const auto &image = tab.userpic->image(_st.userpicSize); + const auto userpicLeft = (width() - _st.userpicSize) / 2; + p.drawImage(userpicLeft, tab.top + _st.userpicTop, image); + p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active)); + tab.text.draw(p, { + .position = QPoint(nameLeft, tab.top + _st.nameTop), + .outerWidth = width(), + .availableWidth = _st.nameWidth, + .align = style::al_top, + .paused = _paused && _paused(), + }); + } + if (range.height > 0) { + const auto add = _st.barStroke / 2; + drawRect(myrtlrect(-add, range.top, _st.barStroke, range.height)); + } +} + +void VerticalSlider::timerEvent(QTimerEvent *e) { + activateCallback(); +} + +void VerticalSlider::startRipple(int index) { + if (!_st.ripple.showDuration) { + return; + } + auto &tab = _tabs[index]; + if (!tab.ripple) { + auto mask = prepareRippleMask(index, tab); + tab.ripple = std::make_unique<Ui::RippleAnimation>( + _st.ripple, + std::move(mask), + [this] { update(); }); + } + const auto point = mapFromGlobal(QCursor::pos()); + tab.ripple->add(point - QPoint(0, tab.top)); +} + +QImage VerticalSlider::prepareRippleMask(int index, const Tab &tab) { + return Ui::RippleAnimation::RectMask(QSize(width(), tab.height)); +} + +int VerticalSlider::getIndexFromPosition(QPoint position) const { + const auto count = int(_tabs.size()); + for (auto i = 0; i != count; ++i) { + const auto &tab = _tabs[i]; + if (position.y() < tab.top + tab.height) { + return i; + } + } + return count - 1; +} + +void VerticalSlider::mousePressEvent(QMouseEvent *e) { + for (auto i = 0, count = int(_tabs.size()); i != count; ++i) { + auto &tab = _tabs[i]; + if (tab.top <= e->y() && e->y() < tab.top + tab.height) { + startRipple(i); + _pressed = i; + break; + } + } +} + +void VerticalSlider::mouseReleaseEvent(QMouseEvent *e) { + const auto pressed = std::exchange(_pressed, -1); + if (pressed < 0) { + return; + } + + const auto index = getIndexFromPosition(e->pos()); + if (pressed < _tabs.size()) { + if (_tabs[pressed].ripple) { + _tabs[pressed].ripple->lastStop(); + } + } + if (index == pressed) { + if (_active != index) { + _callbackAfterMs = crl::now() + _st.duration; + activateCallback(); + + const auto from = getFinalActiveRange(); + _active = index; + const auto to = getFinalActiveRange(); + const auto updater = [this] { update(); }; + _activeTop.start(updater, from.top, to.top, _st.duration); + _activeHeight.start( + updater, + from.height, + to.height, + _st.duration); + } + } +} + +void VerticalSlider::activateCallback() { + if (_timerId >= 0) { + killTimer(_timerId); + _timerId = -1; + } + auto ms = crl::now(); + if (ms >= _callbackAfterMs) { + _sectionActivated.fire_copy(_active); + } else { + _timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer); + } +} } // namespace @@ -47,6 +330,13 @@ SubsectionTabs::SubsectionTabs( track(); refreshSlice(); setupHorizontal(parent); + + dataChanged() | rpl::start_with_next([=] { + if (_loading) { + _loading = false; + refreshSlice(); + } + }, _lifetime); } SubsectionTabs::~SubsectionTabs() { @@ -63,6 +353,8 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { if (!_shadow) { _shadow = Ui::CreateChild<Ui::PlainShadow>(parent); _shadow->show(); + } else { + _shadow->raise(); } const auto toggle = Ui::CreateChild<Ui::IconButton>( @@ -132,13 +424,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { } }, _horizontal->lifetime()); - dataChanged() | rpl::start_with_next([=] { - if (_loading) { - _loading = false; - refreshSlice(); - } - }, _horizontal->lifetime()); - _horizontal->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto togglew = toggle->width(); @@ -147,7 +432,11 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { }, scroll->lifetime()); _horizontal->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(_horizontal).fillRect(clip, st::windowBg); + QPainter(_horizontal).fillRect( + clip.intersected( + _horizontal->rect().marginsRemoved( + { 0, 0, 0, st::lineWidth })), + st::windowBg); }, _horizontal->lifetime()); _refreshed.events_starting_with_copy( @@ -254,8 +543,52 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { toggleModes(); }); toggle->move(0, 0); - const auto scroll = Ui::CreateChild<Ui::ScrollArea>(_vertical); + const auto scroll = Ui::CreateChild<Ui::ScrollArea>( + _vertical, + st::chatTabsScroll); scroll->show(); + const auto tabs = scroll->setOwnedWidget( + object_ptr<VerticalSlider>(scroll)); + tabs->sectionActivated() | rpl::start_with_next([=](int active) { + if (active >= 0 + && active < _slice.size() + && _active != _slice[active]) { + auto params = Window::SectionShow(); + params.way = Window::SectionShow::Way::ClearStack; + params.animated = anim::type::instant; + _controller->showThread(_slice[active], {}, params); + } + }, tabs->lifetime()); + + rpl::merge( + scroll->scrolls(), + _scrollCheckRequests.events(), + scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty) + ) | rpl::start_with_next([=] { + const auto height = scroll->height(); + const auto top = scroll->scrollTop(); + const auto max = scroll->scrollTopMax(); + const auto availableTop = top; + const auto availableBottom = (max - top); + if (max <= 2 * height && _afterAvailable > 0) { + _beforeLimit *= 2; + _afterLimit *= 2; + } + if (availableTop < height + && _beforeSkipped.value_or(0) > 0 + && !_slice.empty()) { + _around = _slice.front(); + refreshSlice(); + } else if (availableBottom < height) { + if (_afterAvailable > 0) { + _around = _slice.back(); + refreshSlice(); + } else if (!_afterSkipped.has_value()) { + _loading = true; + loadMore(); + } + } + }, _vertical->lifetime()); _vertical->sizeValue( ) | rpl::start_with_next([=](QSize size) { @@ -271,7 +604,90 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { _refreshed.events_starting_with_copy( rpl::empty ) | rpl::start_with_next([=] { - _vertical->resize(std::max(toggle->width(), 0), 0); + auto sections = std::vector<VerticalSlider::Section>(); + auto activeIndex = -1; + for (const auto &thread : _slice) { + if (thread == _active) { + activeIndex = int(sections.size()); + } + if (const auto topic = thread->asTopic()) { + sections.push_back({ + .userpic = (topic->iconId() + ? Ui::MakeEmojiThumbnail( + &topic->owner(), + Data::SerializeCustomEmojiId(topic->iconId())) + : Ui::MakeUserpicThumbnail( + _controller->session().user())), + .text = topic->title(), + }); + } else if (const auto sublist = thread->asSublist()) { + const auto peer = sublist->sublistPeer(); + sections.push_back({ + .userpic = Ui::MakeUserpicThumbnail(peer), + .text = peer->shortName(), + }); + } else { + sections.push_back({ + .userpic = Ui::MakeUserpicThumbnail( + _controller->session().user()), + .text = tr::lng_filters_all_short(tr::now), + }); + } + } + const auto paused = [=] { + return _controller->isGifPausedAtLeastFor( + Window::GifPauseReason::Any); + }; + + auto scrollSavingThread = (Data::Thread*)nullptr; + auto scrollSavingShift = 0; + auto scrollSavingIndex = -1; + if (const auto count = tabs->sectionsCount()) { + const auto scrollTop = scroll->scrollTop(); + auto indexTop = tabs->lookupSectionTop(0); + for (auto index = 0; index != count; ++index) { + const auto nextTop = (index + 1 != count) + ? tabs->lookupSectionTop(index + 1) + : (indexTop + scrollTop + 1); + if (indexTop <= scrollTop && nextTop > scrollTop) { + scrollSavingThread = _sectionsSlice[index]; + scrollSavingShift = scrollTop - indexTop; + break; + } + indexTop = nextTop; + } + scrollSavingIndex = scrollSavingThread + ? int(ranges::find(_slice, not_null(scrollSavingThread)) + - begin(_slice)) + : -1; + if (scrollSavingIndex == _slice.size()) { + scrollSavingIndex = -1; + for (auto index = 0; index != count; ++index) { + const auto thread = _sectionsSlice[index]; + if (ranges::contains(_slice, thread)) { + scrollSavingThread = thread; + scrollSavingShift = scrollTop + - tabs->lookupSectionTop(index); + scrollSavingIndex = index; + break; + } + } + } + } + + tabs->setSections(sections, paused); + tabs->fitHeightToSections(); + tabs->setActiveSectionFast(activeIndex); + _sectionsSlice = _slice; + _vertical->resize( + std::max(toggle->width(), tabs->width()), + _vertical->height()); + if (scrollSavingIndex >= 0) { + scroll->scrollToY(tabs->lookupSectionTop(scrollSavingIndex) + + scrollSavingShift); + } + + _scrollCheckRequests.fire({}); }, _vertical->lifetime()); } @@ -341,7 +757,7 @@ void SubsectionTabs::setBoundingRect(QRect boundingRect) { _horizontal->height()); _shadow->setGeometry( boundingRect.x(), - _horizontal->y() + _horizontal->height(), + _horizontal->y() + _horizontal->height() - st::lineWidth, boundingRect.width(), st::lineWidth); } else { @@ -367,7 +783,7 @@ int SubsectionTabs::leftSkip() const { } int SubsectionTabs::topSkip() const { - return _horizontal ? _horizontal->height() : 0; + return _horizontal ? (_horizontal->height() - st::lineWidth) : 0; } void SubsectionTabs::raise() { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 76b725da93..6de023c332 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1284,3 +1284,43 @@ chatTabsSlider: SettingsSlider(defaultSettingsSlider) { rippleBgActive: lightButtonBgOver; ripple: defaultRippleAnimation; } + +ChatTabsVertical { + barStroke: pixels; + barRadius: pixels; + barFg: color; + nameStyle: TextStyle; + nameWidth: pixels; + nameTop: pixels; + nameFg: color; + nameFgActive: color; + userpicTop: pixels; + userpicSize: pixels; + baseHeight: pixels; + width: pixels; + ripple: RippleAnimation; + rippleBg: color; + rippleBgActive: color; + duration: int; +} + +chatTabsVertical: ChatTabsVertical { + barStroke: 8px; + barRadius: 4px; + barFg: sliderBgActive; + nameStyle: TextStyle(defaultTextStyle) { + font: font(10px); + } + nameWidth: 46px; + nameTop: 46px; + nameFg: windowSubTextFg; + nameFgActive: lightButtonFg; + userpicTop: 8px; + userpicSize: 36px; + baseHeight: 56px; + width: 56px; + ripple: defaultRippleAnimation; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + duration: 150; +} From 126749f04c51936e4ec8a2018a09fe4a0dd01f1b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 26 May 2025 12:11:05 +0400 Subject: [PATCH 076/310] Fix build with new MSVC. --- Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index b94f000768..f375f65349 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -390,8 +390,8 @@ void EditCaptionBox( return; } auto text = TextWithEntities{ - base::take(textWithTags.text), - ConvertTextTagsToEntities(base::take(textWithTags.tags)), + std::move(textWithTags.text), + ConvertTextTagsToEntities(std::move(textWithTags.tags)), }; if (item->isUploading()) { item->setText(std::move(text)); From 1d2648229857ebe43566da37b67db8501cde6622 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 26 May 2025 13:22:26 +0400 Subject: [PATCH 077/310] Update API scheme on layer 204. --- Telegram/SourceFiles/api/api_unread_things.cpp | 1 + Telegram/SourceFiles/menu/menu_send.cpp | 3 ++- Telegram/SourceFiles/mtproto/scheme/api.tl | 12 ++++++------ Telegram/SourceFiles/window/window_peer_menu.cpp | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/api/api_unread_things.cpp b/Telegram/SourceFiles/api/api_unread_things.cpp index dac998dfdc..be597c954d 100644 --- a/Telegram/SourceFiles/api/api_unread_things.cpp +++ b/Telegram/SourceFiles/api/api_unread_things.cpp @@ -144,6 +144,7 @@ void UnreadThings::requestReactions( MTP_flags(topic ? Flag::f_top_msg_id : Flag()), history->peer->input, MTP_int(topic ? topic->rootId() : 0), + MTPInputPeer(), // saved_peer_id MTP_int(offsetId), MTP_int(addOffset), MTP_int(limit), diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 118bcee597..5d7ca9d396 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -930,7 +930,8 @@ void SetupUnreadReactionsMenu( peer->session().api().request(MTPmessages_ReadReactions( MTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)), peer->input, - MTP_int(rootId) + MTP_int(rootId), + MTPInputPeer() // saved_peer_id )).done([=](const MTPmessages_AffectedHistory &result) { const auto offset = peer->session().api().applyAffectedHistory( peer, diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index e6905743cb..1955440642 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -348,7 +348,7 @@ updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; updateLangPackTooLong#46560264 lang_code:string = Update; updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; -updateChannelReadMessagesContents#ea29055d flags:# channel_id:long top_msg_id:flags.0?int messages:Vector<int> = Update; +updateChannelReadMessagesContents#25f324f7 flags:# channel_id:long top_msg_id:flags.0?int saved_peer_id:flags.1?Peer messages:Vector<int> = Update; updateContactsReset#7084a7be = Update; updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; updateDialogUnreadMark#b658f23e flags:# unread:flags.0?true peer:DialogPeer saved_peer_id:flags.1?Peer = Update; @@ -385,7 +385,7 @@ updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJ updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector<BotCommand> = Update; updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector<long> = Update; updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; -updateMessageReactions#5e1b3cb8 flags:# peer:Peer msg_id:int top_msg_id:flags.0?int reactions:MessageReactions = Update; +updateMessageReactions#1e297bfa flags:# peer:Peer msg_id:int top_msg_id:flags.0?int saved_peer_id:flags.1?Peer reactions:MessageReactions = Update; updateAttachMenuBots#17b7a20b = Update; updateWebViewResultSent#1592b79d query_id:long = Update; updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update; @@ -1715,7 +1715,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; -monoForumDialog#7d25fd43 flags:# unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int draft:flags.1?DraftMessage = SavedDialog; +monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; @@ -2294,7 +2294,7 @@ messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messag messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; -messages.unpinAllMessages#ee22b9a8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.unpinAllMessages#62dd747 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory; messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages; messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed; @@ -2325,8 +2325,8 @@ messages.setChatAvailableReactions#864b2581 flags:# peer:InputPeer available_rea messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool; messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector<int> text:flags.1?Vector<TextWithEntities> to_lang:string = messages.TranslatedText; -messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.getUnreadReactions#bd7f90ac flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.readReactions#9ec44f93 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory; messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 39f8100ffd..bef9e3c6c9 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3069,7 +3069,8 @@ void UnpinAllMessages( api->request(MTPmessages_UnpinAllMessages( MTP_flags(topicRootId ? Flag::f_top_msg_id : Flag()), history->peer->input, - MTP_int(topicRootId.bare) + MTP_int(topicRootId.bare), + MTPInputPeer() // saved_peer_id )).done([=](const MTPmessages_AffectedHistory &result) { const auto peer = history->peer; const auto offset = api->applyAffectedHistory(peer, result); From 0e5419c60b2ff4ae5b04d1b1d0432945e0bc91a6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 26 May 2025 14:01:04 +0400 Subject: [PATCH 078/310] Fix opening forums with tabs. --- .../dialogs/dialogs_inner_widget.cpp | 7 +++++++ .../SourceFiles/history/history_widget.cpp | 4 +++- .../view/history_view_chat_section.cpp | 3 ++- .../view/history_view_top_bar_widget.cpp | 21 ++++++++++++------- .../view/history_view_translate_bar.cpp | 2 +- .../window/window_session_controller.cpp | 4 ++++ 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 2905899da1..84b340b7bd 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_search_tags.h" #include "dialogs/dialogs_quick_action.h" #include "history/view/history_view_context_menu.h" +#include "history/view/history_view_subsection_tabs.h" #include "history/history.h" #include "history/history_item.h" #include "core/application.h" @@ -1500,6 +1501,12 @@ bool InnerWidget::isRowActive( const auto key = row->key(); if (entry.key == key) { return true; + } else if (const auto topic = entry.key.topic()) { + if (const auto history = key.history()) { + return (history->peer == topic->channel()) + && HistoryView::SubsectionTabs::UsedFor(history); + } + return false; } else if (const auto sublist = entry.key.sublist()) { if (!sublist->parentChat()) { // In case we're viewing a Saved Messages sublist, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 6b5ee03337..8a88024743 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6466,7 +6466,9 @@ void HistoryWidget::updateControlsGeometry() { const auto scrollAreaTop = _topBars->y() + businessBotTop + (_businessBotStatus ? _businessBotStatus->bar().height() : 0); - _topBars->resize(innerWidth, scrollAreaTop - _topBars->y()); + _topBars->resize( + innerWidth, + scrollAreaTop - _topBars->y() + st::lineWidth); if (_scroll->y() != scrollAreaTop) { _scroll->moveToLeft(tabsLeftSkip, scrollAreaTop); if (_autocomplete) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index a833cb6cd4..6829d8a432 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -468,6 +468,7 @@ ChatWidget::~ChatWidget() { } void ChatWidget::orderWidgets() { + _topBars->raise(); _translateBar->raise(); if (_topicReopenBar) { _topicReopenBar->bar().raise(); @@ -2608,7 +2609,7 @@ void ChatWidget::updateControlsGeometry() { bottom -= _composeControls->heightCurrent(); } - _topBars->resize(innerWidth, top); + _topBars->resize(innerWidth, top + st::lineWidth); top += _topBars->y(); const auto scrollHeight = bottom - top; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 7cbf86fcb3..d965afcf18 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" // GroupCall::input. #include "data/data_folder.h" #include "data/data_forum.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_stories.h" @@ -55,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_forum_topic.h" #include "data/data_send_action.h" +#include "dialogs/dialogs_main_list.h" #include "chat_helpers/emoji_interactions.h" #include "base/unixtime.h" #include "support/support_helper.h" @@ -488,14 +490,16 @@ void TopBarWidget::paintTopBar(Painter &p) { const auto sublist = _activeChat.key.sublist(); const auto topic = _activeChat.key.topic(); const auto history = _activeChat.key.history(); - const auto namePeer = history + const auto broadcastForMonoforum = history + ? history->peer->monoforumBroadcast() + : nullptr; + const auto namePeer = broadcastForMonoforum + ? broadcastForMonoforum + : history ? history->peer.get() : sublist ? sublist->sublistPeer().get() : nullptr; - const auto broadcastForMonoforum = history - ? history->peer->monoforumBroadcast() - : nullptr; if (topic && _activeChat.section == Section::Replies) { p.setPen(st::dialogsNameFg); topic->chatListNameText().drawElided( @@ -519,12 +523,9 @@ void TopBarWidget::paintTopBar(Painter &p) { } } else if (folder || (peer && (peer->sharedMediaInfo() || peer->isVerifyCodes())) - || broadcastForMonoforum || (_activeChat.section == Section::Scheduled) || (_activeChat.section == Section::Pinned)) { - auto text = broadcastForMonoforum - ? broadcastForMonoforum->name() + u" Messages"_q AssertIsDebug() - : (_activeChat.section == Section::Scheduled) + auto text = (_activeChat.section == Section::Scheduled) ? ((peer && peer->isSelf()) ? tr::lng_reminder_messages(tr::now) : tr::lng_scheduled_messages(tr::now)) @@ -1690,6 +1691,10 @@ void TopBarWidget::updateOnlineDisplay() { text = tr::lng_group_status(tr::now); } } + } else if (const auto monoforum = peer->monoforum()) { + const auto chats = monoforum->chatsList(); + const auto count = chats->fullSize().current(); + text = tr::lng_filters_chats_count(tr::now, lt_count, count); } else if (const auto channel = peer->asChannel()) { if (channel->isMegagroup() && channel->canViewMembers() diff --git a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp index 576c1ac3d1..11e9cc8d7a 100644 --- a/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_translate_bar.cpp @@ -232,7 +232,7 @@ TranslateBar::TranslateBar( : _controller(controller) , _history(history) , _wrap(parent, object_ptr<Ui::AbstractButton>(parent)) -, _shadow(std::make_unique<Ui::PlainShadow>(_wrap.parentWidget())) { +, _shadow(std::make_unique<Ui::PlainShadow>(parent)) { _wrap.hide(anim::type::instant); _shadow->hide(); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index b32b8c6b04..d0ba08a2c7 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1878,6 +1878,10 @@ void SessionController::showForum( const SectionShow ¶ms) { if (showForumInDifferentWindow(forum, params)) { return; + } else if (HistoryView::SubsectionTabs::UsedFor( + forum->owner().history(forum->channel()))) { + showPeerHistory(forum->channel(), params); + return; } _shownForumLifetime.destroy(); if (_shownForum.current() != forum) { From 8512154b451c581eea1351ff6da131eaf931d2d9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 May 2025 13:54:56 +0400 Subject: [PATCH 079/310] Implement better horizontal/vertical tabs. --- .../boxes/peers/edit_forum_topic_box.cpp | 9 +- Telegram/SourceFiles/data/data_channel.cpp | 5 + Telegram/SourceFiles/data/data_channel.h | 1 + .../SourceFiles/data/data_forum_topic.cpp | 6 +- Telegram/SourceFiles/dialogs/dialogs.style | 4 +- .../dialogs/dialogs_inner_widget.cpp | 14 +- .../view/history_view_subsection_tabs.cpp | 658 +++++------------- .../view/history_view_subsection_tabs.h | 7 + Telegram/SourceFiles/ui/chat/chat.style | 29 +- .../ui/controls/subsection_tabs_slider.cpp | 487 +++++++++++++ .../ui/controls/subsection_tabs_slider.h | 163 +++++ .../SourceFiles/ui/dynamic_thumbnails.cpp | 42 +- Telegram/SourceFiles/ui/dynamic_thumbnails.h | 4 +- Telegram/cmake/td_ui.cmake | 2 + 14 files changed, 908 insertions(+), 523 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp create mode 100644 Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h diff --git a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp index 1e6ab6206b..45ad6b1931 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_forum_topic_box.cpp @@ -93,11 +93,12 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) { const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal) ? st::normalForumTopicIcon : st::defaultForumTopicIcon; + const auto general = Data::IsForumGeneralIconTitle(_icon.title); if (_image.isNull()) { - _image = Data::IsForumGeneralIconTitle(_icon.title) + _image = general ? Data::ForumTopicGeneralIconFrame( st.size, - Data::ParseForumGeneralIconColor(_icon.colorId)) + QColor(255, 255, 255)) : Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st); } const auto full = (_tag == Data::CustomEmojiSizeTag::Normal) @@ -106,7 +107,9 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) { const auto esize = full / style::DevicePixelRatio(); const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize); const auto skip = (customSize - st.size) / 2; - p.drawImage(context.position + QPoint(skip, skip), _image); + p.drawImage(context.position + QPoint(skip, skip), general + ? style::colorizeImage(_image, context.textColor) + : _image); } void DefaultIconEmoji::unload() { diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 51f77db85e..6684631180 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -408,6 +408,11 @@ void ChannelData::setPendingRequestsCount( } } +bool ChannelData::useSubsectionTabs() const { + return isForum() + && ((flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug(); +} + ChatRestrictionsInfo ChannelData::KickedRestrictedRights( not_null<PeerData*> participant) { using Flag = ChatRestriction; diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index b0362f1d8c..776edb0aa5 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -279,6 +279,7 @@ public: [[nodiscard]] bool paidMessagesAvailable() const { return flags() & Flag::PaidMessagesAvailable; } + [[nodiscard]] bool useSubsectionTabs() const; [[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights( not_null<PeerData*> participant); diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index 12232d8538..799190714d 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -152,10 +152,10 @@ QImage ForumTopicGeneralIconFrame(int size, const QColor &color) { result.setDevicePixelRatio(ratio); result.fill(Qt::transparent); - const auto use = size * 0.8; - const auto skip = size * 0.1; + const auto use = size * 1.; + const auto skip = size * 0.; auto p = QPainter(&result); - svg.render(&p, QRectF(skip, 0, use, use)); + svg.render(&p, QRectF(skip, skip, use, use)); p.end(); return style::colorizeImage(result, color); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index a5c63d5e35..babbf4027a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -500,8 +500,8 @@ dialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) } dialogsSearchInHeight: 38px; -dialogsSearchInPhotoSize: 26px; -dialogsSearchInPhotoPadding: 12px; +dialogsSearchInPhotoSize: 28px; +dialogsSearchInPhotoPadding: 10px; dialogsSearchInSkip: 10px; dialogsSearchInNameTop: 9px; dialogsSearchInDownTop: 15px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 84b340b7bd..9892c01598 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -4218,12 +4218,20 @@ void InnerWidget::updateSearchIn() { : _openedForum ? _openedForum->channel().get() : nullptr; + const auto paused = [window = _controller] { + return window->isGifPausedAtLeastFor(Window::GifPauseReason::Any); + }; + const auto textFg = [] { + return st::windowSubTextFg->c; + }; const auto topicIcon = !topic ? nullptr : topic->iconId() ? Ui::MakeEmojiThumbnail( &topic->owner(), - Data::SerializeCustomEmojiId(topic->iconId())) + Data::SerializeCustomEmojiId(topic->iconId()), + paused, + textFg) : Ui::MakeEmojiThumbnail( &topic->owner(), Data::TopicIconEmojiEntity({ @@ -4233,7 +4241,9 @@ void InnerWidget::updateSearchIn() { .colorId = (topic->isGeneral() ? Data::ForumGeneralIconColor(st::windowSubTextFg->c) : topic->colorId()), - })); + }), + paused, + textFg); const auto peerIcon = peer ? Ui::MakeUserpicThumbnail(peer) : sublist diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index cdc024c3d5..a4b4e64700 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/controls/subsection_tabs_slider.h" #include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -36,284 +37,6 @@ namespace HistoryView { namespace { constexpr auto kDefaultLimit = 5; AssertIsDebug()// 10; -constexpr auto kMaxNameLines = 3; - -class VerticalSlider final : public Ui::RpWidget { -public: - explicit VerticalSlider(not_null<QWidget*> parent); - - struct Section { - std::shared_ptr<Ui::DynamicImage> userpic; - QString text; - }; - - void setSections(std::vector<Section> sections, Fn<bool()> paused); - void setActiveSectionFast(int active); - - void fitHeightToSections(); - - [[nodiscard]] rpl::producer<int> sectionActivated() const { - return _sectionActivated.events(); - } - - [[nodiscard]] int sectionsCount() const; - [[nodiscard]] int lookupSectionTop(int index) const; - -private: - struct Tab { - std::shared_ptr<Ui::DynamicImage> userpic; - Ui::Text::String text; - std::unique_ptr<Ui::RippleAnimation> ripple; - int top = 0; - int height = 0; - bool subscribed = false; - }; - struct Range { - int top = 0; - int height = 0; - }; - - void paintEvent(QPaintEvent *e) override; - void timerEvent(QTimerEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - - void startRipple(int index); - [[nodiscard]] int getIndexFromPosition(QPoint position) const; - [[nodiscard]] QImage prepareRippleMask(int index, const Tab &tab); - - void activateCallback(); - [[nodiscard]] Range getFinalActiveRange() const; - - const style::ChatTabsVertical &_st; - Ui::RoundRect _bar; - std::vector<Tab> _tabs; - int _active = -1; - int _pressed = -1; - Ui::Animations::Simple _activeTop; - Ui::Animations::Simple _activeHeight; - - int _timerId = -1; - crl::time _callbackAfterMs = 0; - - rpl::event_stream<int> _sectionActivated; - Fn<bool()> _paused; - -}; - -VerticalSlider::VerticalSlider(not_null<QWidget*> parent) -: RpWidget(parent) -, _st(st::chatTabsVertical) -, _bar(_st.barRadius, _st.barFg) { - setCursor(style::cur_pointer); -} - -void VerticalSlider::setSections( - std::vector<Section> sections, - Fn<bool()> paused) { - auto old = base::take(_tabs); - _tabs.reserve(sections.size()); - - for (auto §ion : sections) { - const auto i = ranges::find(old, section.userpic, &Tab::userpic); - if (i != end(old)) { - _tabs.push_back(std::move(*i)); - old.erase(i); - } else { - _tabs.push_back({ .userpic = std::move(section.userpic), }); - } - _tabs.back().text = Ui::Text::String( - _st.nameStyle, - section.text, - kDefaultTextOptions, - _st.nameWidth); - } - for (const auto &was : old) { - if (was.subscribed) { - was.userpic->subscribeToUpdates(nullptr); - } - } -} - -void VerticalSlider::setActiveSectionFast(int active) { - _active = active; - _activeTop.stop(); - _activeHeight.stop(); -} - -void VerticalSlider::fitHeightToSections() { - auto top = 0; - for (auto &tab : _tabs) { - tab.top = top; - tab.height = _st.baseHeight + std::min( - _st.nameStyle.font->height * kMaxNameLines, - tab.text.countHeight(_st.nameWidth, true)); - top += tab.height; - } - resize(_st.width, top); -} - -int VerticalSlider::sectionsCount() const { - return int(_tabs.size()); -} - -int VerticalSlider::lookupSectionTop(int index) const { - Expects(index >= 0 && index < _tabs.size()); - - return _tabs[index].top; -} - -VerticalSlider::Range VerticalSlider::getFinalActiveRange() const { - return (_active >= 0) - ? Range{ _tabs[_active].top, _tabs[_active].height } - : Range(); -} - -void VerticalSlider::paintEvent(QPaintEvent *e) { - const auto finalRange = getFinalActiveRange(); - const auto range = Range{ - int(base::SafeRound(_activeTop.value(finalRange.top))), - int(base::SafeRound(_activeHeight.value(finalRange.height))), - }; - - auto p = QPainter(this); - auto clip = e->rect(); - const auto drawRect = [&](QRect rect) { - _bar.paint(p, rect); - }; - const auto nameLeft = (_st.width - _st.nameWidth) / 2; - for (auto &tab : _tabs) { - if (!clip.intersects(QRect(0, tab.top, width(), tab.height))) { - continue; - } - const auto divider = std::max(std::min(tab.height, range.height), 1); - const auto active = 1. - - std::clamp( - std::abs(range.top - tab.top) / float64(divider), - 0., - 1.); - if (tab.ripple) { - const auto color = anim::color( - _st.rippleBg, - _st.rippleBgActive, - active); - tab.ripple->paint(p, 0, tab.top, width(), &color); - if (tab.ripple->empty()) { - tab.ripple.reset(); - } - } - - if (!tab.subscribed) { - tab.subscribed = true; - tab.userpic->subscribeToUpdates([=] { update(); }); - } - const auto &image = tab.userpic->image(_st.userpicSize); - const auto userpicLeft = (width() - _st.userpicSize) / 2; - p.drawImage(userpicLeft, tab.top + _st.userpicTop, image); - p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active)); - tab.text.draw(p, { - .position = QPoint(nameLeft, tab.top + _st.nameTop), - .outerWidth = width(), - .availableWidth = _st.nameWidth, - .align = style::al_top, - .paused = _paused && _paused(), - }); - } - if (range.height > 0) { - const auto add = _st.barStroke / 2; - drawRect(myrtlrect(-add, range.top, _st.barStroke, range.height)); - } -} - -void VerticalSlider::timerEvent(QTimerEvent *e) { - activateCallback(); -} - -void VerticalSlider::startRipple(int index) { - if (!_st.ripple.showDuration) { - return; - } - auto &tab = _tabs[index]; - if (!tab.ripple) { - auto mask = prepareRippleMask(index, tab); - tab.ripple = std::make_unique<Ui::RippleAnimation>( - _st.ripple, - std::move(mask), - [this] { update(); }); - } - const auto point = mapFromGlobal(QCursor::pos()); - tab.ripple->add(point - QPoint(0, tab.top)); -} - -QImage VerticalSlider::prepareRippleMask(int index, const Tab &tab) { - return Ui::RippleAnimation::RectMask(QSize(width(), tab.height)); -} - -int VerticalSlider::getIndexFromPosition(QPoint position) const { - const auto count = int(_tabs.size()); - for (auto i = 0; i != count; ++i) { - const auto &tab = _tabs[i]; - if (position.y() < tab.top + tab.height) { - return i; - } - } - return count - 1; -} - -void VerticalSlider::mousePressEvent(QMouseEvent *e) { - for (auto i = 0, count = int(_tabs.size()); i != count; ++i) { - auto &tab = _tabs[i]; - if (tab.top <= e->y() && e->y() < tab.top + tab.height) { - startRipple(i); - _pressed = i; - break; - } - } -} - -void VerticalSlider::mouseReleaseEvent(QMouseEvent *e) { - const auto pressed = std::exchange(_pressed, -1); - if (pressed < 0) { - return; - } - - const auto index = getIndexFromPosition(e->pos()); - if (pressed < _tabs.size()) { - if (_tabs[pressed].ripple) { - _tabs[pressed].ripple->lastStop(); - } - } - if (index == pressed) { - if (_active != index) { - _callbackAfterMs = crl::now() + _st.duration; - activateCallback(); - - const auto from = getFinalActiveRange(); - _active = index; - const auto to = getFinalActiveRange(); - const auto updater = [this] { update(); }; - _activeTop.start(updater, from.top, to.top, _st.duration); - _activeHeight.start( - updater, - from.height, - to.height, - _st.duration); - } - } -} - -void VerticalSlider::activateCallback() { - if (_timerId >= 0) { - killTimer(_timerId); - _timerId = -1; - } - auto ms = crl::now(); - if (ms >= _callbackAfterMs) { - _sectionActivated.fire_copy(_active); - } else { - _timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer); - } -} } // namespace @@ -370,18 +93,13 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { st::chatTabsScroll, true); scroll->show(); - const auto tabs = scroll->setOwnedWidget( - object_ptr<Ui::SettingsSlider>(scroll, st::chatTabsSlider)); - tabs->sectionActivated() | rpl::start_with_next([=](int active) { - if (active >= 0 - && active < _slice.size() - && _active != _slice[active]) { - auto params = Window::SectionShow(); - params.way = Window::SectionShow::Way::ClearStack; - params.animated = anim::type::instant; - _controller->showThread(_slice[active], {}, params); - } - }, tabs->lifetime()); + const auto slider = scroll->setOwnedWidget( + object_ptr<Ui::HorizontalSlider>(scroll)); + setupSlider(scroll, slider, false); + + _horizontal->resize( + _horizontal->width(), + std::max(toggle->height(), slider->height())); scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) { const auto pixelDelta = e->pixelDelta(); @@ -394,36 +112,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { return true; }); - rpl::merge( - scroll->scrolls(), - _scrollCheckRequests.events(), - scroll->widthValue() | rpl::skip(1) | rpl::map_to(rpl::empty) - ) | rpl::start_with_next([=] { - const auto width = scroll->width(); - const auto left = scroll->scrollLeft(); - const auto max = scroll->scrollLeftMax(); - const auto availableLeft = left; - const auto availableRight = (max - left); - if (max <= 2 * width && _afterAvailable > 0) { - _beforeLimit *= 2; - _afterLimit *= 2; - } - if (availableLeft < width - && _beforeSkipped.value_or(0) > 0 - && !_slice.empty()) { - _around = _slice.front(); - refreshSlice(); - } else if (availableRight < width) { - if (_afterAvailable > 0) { - _around = _slice.back(); - refreshSlice(); - } else if (!_afterSkipped.has_value()) { - _loading = true; - loadMore(); - } - } - }, _horizontal->lifetime()); - _horizontal->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto togglew = toggle->width(); @@ -438,89 +126,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { { 0, 0, 0, st::lineWidth })), st::windowBg); }, _horizontal->lifetime()); - - _refreshed.events_starting_with_copy( - rpl::empty - ) | rpl::start_with_next([=] { - auto sections = std::vector<TextWithEntities>(); - const auto manager = &_history->owner().customEmojiManager(); - auto activeIndex = -1; - for (const auto &thread : _slice) { - if (thread == _active) { - activeIndex = int(sections.size()); - } - if (const auto topic = thread->asTopic()) { - sections.push_back(topic->titleWithIcon()); - } else if (const auto sublist = thread->asSublist()) { - const auto peer = sublist->sublistPeer(); - sections.push_back(TextWithEntities().append( - Ui::Text::SingleCustomEmoji( - manager->peerUserpicEmojiData(peer), - u"@"_q) - ).append(' ').append(peer->shortName())); - } else { - sections.push_back(tr::lng_filters_all_short( - tr::now, - Ui::Text::WithEntities)); - } - } - const auto paused = [=] { - return _controller->isGifPausedAtLeastFor( - Window::GifPauseReason::Any); - }; - - auto scrollSavingThread = (Data::Thread*)nullptr; - auto scrollSavingShift = 0; - auto scrollSavingIndex = -1; - if (const auto count = tabs->sectionsCount()) { - const auto scrollLeft = scroll->scrollLeft(); - auto indexLeft = tabs->lookupSectionLeft(0); - for (auto index = 0; index != count; ++index) { - const auto nextLeft = (index + 1 != count) - ? tabs->lookupSectionLeft(index + 1) - : (indexLeft + scrollLeft + 1); - if (indexLeft <= scrollLeft && nextLeft > scrollLeft) { - scrollSavingThread = _sectionsSlice[index]; - scrollSavingShift = scrollLeft - indexLeft; - break; - } - indexLeft = nextLeft; - } - scrollSavingIndex = scrollSavingThread - ? int(ranges::find(_slice, not_null(scrollSavingThread)) - - begin(_slice)) - : -1; - if (scrollSavingIndex == _slice.size()) { - scrollSavingIndex = -1; - for (auto index = 0; index != count; ++index) { - const auto thread = _sectionsSlice[index]; - if (ranges::contains(_slice, thread)) { - scrollSavingThread = thread; - scrollSavingShift = scrollLeft - - tabs->lookupSectionLeft(index); - scrollSavingIndex = index; - break; - } - } - } - } - - tabs->setSections(sections, Core::TextContext({ - .session = &_history->session(), - }), paused); - tabs->fitWidthToSections(); - tabs->setActiveSectionFast(activeIndex); - _sectionsSlice = _slice; - _horizontal->resize( - _horizontal->width(), - std::max(toggle->height(), tabs->height())); - if (scrollSavingIndex >= 0) { - scroll->scrollToX(tabs->lookupSectionLeft(scrollSavingIndex) - + scrollSavingShift); - } - - _scrollCheckRequests.fire({}); - }, _horizontal->lifetime()); } void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { @@ -547,48 +152,14 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { _vertical, st::chatTabsScroll); scroll->show(); - const auto tabs = scroll->setOwnedWidget( - object_ptr<VerticalSlider>(scroll)); - tabs->sectionActivated() | rpl::start_with_next([=](int active) { - if (active >= 0 - && active < _slice.size() - && _active != _slice[active]) { - auto params = Window::SectionShow(); - params.way = Window::SectionShow::Way::ClearStack; - params.animated = anim::type::instant; - _controller->showThread(_slice[active], {}, params); - } - }, tabs->lifetime()); - rpl::merge( - scroll->scrolls(), - _scrollCheckRequests.events(), - scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty) - ) | rpl::start_with_next([=] { - const auto height = scroll->height(); - const auto top = scroll->scrollTop(); - const auto max = scroll->scrollTopMax(); - const auto availableTop = top; - const auto availableBottom = (max - top); - if (max <= 2 * height && _afterAvailable > 0) { - _beforeLimit *= 2; - _afterLimit *= 2; - } - if (availableTop < height - && _beforeSkipped.value_or(0) > 0 - && !_slice.empty()) { - _around = _slice.front(); - refreshSlice(); - } else if (availableBottom < height) { - if (_afterAvailable > 0) { - _around = _slice.back(); - refreshSlice(); - } else if (!_afterSkipped.has_value()) { - _loading = true; - loadMore(); - } - } - }, _vertical->lifetime()); + const auto slider = scroll->setOwnedWidget( + object_ptr<Ui::VerticalSlider>(scroll)); + setupSlider(scroll, slider, true); + + _vertical->resize( + std::max(toggle->width(), slider->width()), + _vertical->height()); _vertical->sizeValue( ) | rpl::start_with_next([=](QSize size) { @@ -600,61 +171,149 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { _vertical->paintRequest() | rpl::start_with_next([=](QRect clip) { QPainter(_vertical).fillRect(clip, st::windowBg); }, _vertical->lifetime()); +} + +void SubsectionTabs::setupSlider( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider, + bool vertical) { + slider->sectionActivated() | rpl::start_with_next([=](int active) { + if (active >= 0 + && active < _slice.size() + && _active != _slice[active]) { + auto params = Window::SectionShow(); + params.way = Window::SectionShow::Way::ClearStack; + params.animated = anim::type::instant; + _controller->showThread(_slice[active], {}, params); + } + }, slider->lifetime()); + + rpl::merge( + scroll->scrolls(), + _scrollCheckRequests.events(), + scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty) + ) | rpl::start_with_next([=] { + const auto full = vertical ? scroll->height() : scroll->width(); + const auto scrollValue = vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + const auto scrollMax = vertical + ? scroll->scrollTopMax() + : scroll->scrollLeftMax(); + const auto availableFrom = scrollValue; + const auto availableTill = (scrollMax - scrollValue); + if (scrollMax <= 2 * full && _afterAvailable > 0) { + _beforeLimit *= 2; + _afterLimit *= 2; + } + if (availableFrom < full + && _beforeSkipped.value_or(0) > 0 + && !_slice.empty()) { + _around = _slice.front(); + refreshSlice(); + } else if (availableTill < full) { + if (_afterAvailable > 0) { + _around = _slice.back(); + refreshSlice(); + } else if (!_afterSkipped.has_value()) { + _loading = true; + loadMore(); + } + } + }, scroll->lifetime()); _refreshed.events_starting_with_copy( rpl::empty ) | rpl::start_with_next([=] { - auto sections = std::vector<VerticalSlider::Section>(); - auto activeIndex = -1; - for (const auto &thread : _slice) { - if (thread == _active) { - activeIndex = int(sections.size()); - } - if (const auto topic = thread->asTopic()) { - sections.push_back({ - .userpic = (topic->iconId() - ? Ui::MakeEmojiThumbnail( - &topic->owner(), - Data::SerializeCustomEmojiId(topic->iconId())) - : Ui::MakeUserpicThumbnail( - _controller->session().user())), - .text = topic->title(), - }); - } else if (const auto sublist = thread->asSublist()) { - const auto peer = sublist->sublistPeer(); - sections.push_back({ - .userpic = Ui::MakeUserpicThumbnail(peer), - .text = peer->shortName(), - }); - } else { - sections.push_back({ - .userpic = Ui::MakeUserpicThumbnail( - _controller->session().user()), - .text = tr::lng_filters_all_short(tr::now), - }); - } - } + const auto manager = &_history->owner().customEmojiManager(); const auto paused = [=] { return _controller->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }; + auto sections = std::vector<Ui::SubsectionTab>(); + auto activeIndex = -1; + for (const auto &thread : _slice) { + const auto index = int(sections.size()); + if (thread == _active) { + activeIndex = index; + } + const auto textFg = [=] { + return anim::color( + st::windowSubTextFg, + st::windowActiveTextFg, + slider->buttonActive(slider->buttonAt(index))); + }; + if (const auto topic = thread->asTopic()) { + if (vertical) { + sections.push_back({ + .text = { topic->title() }, + .userpic = (topic->iconId() + ? Ui::MakeEmojiThumbnail( + &topic->owner(), + Data::SerializeCustomEmojiId(topic->iconId()), + paused, + textFg) + : Ui::MakeEmojiThumbnail( + &topic->owner(), + Data::TopicIconEmojiEntity({ + .title = (topic->isGeneral() + ? Data::ForumGeneralIconTitle() + : topic->title()), + .colorId = (topic->isGeneral() + ? Data::ForumGeneralIconColor( + st::windowSubTextFg->c) + : topic->colorId()), + }), + paused, + textFg)), + }); + } else { + sections.push_back({ + .text = topic->titleWithIcon(), + }); + } + } else if (const auto sublist = thread->asSublist()) { + const auto peer = sublist->sublistPeer(); + if (vertical) { + sections.push_back({ + .text = peer->shortName(), + .userpic = Ui::MakeUserpicThumbnail(peer), + }); + } else { + sections.push_back({ + .text = TextWithEntities().append( + Ui::Text::SingleCustomEmoji( + manager->peerUserpicEmojiData(peer), + u"@"_q) + ).append(' ').append(peer->shortName()), + }); + } + } else { + sections.push_back({ + .text = tr::lng_filters_all_short(tr::now), + .userpic = Ui::MakeAllSubsectionsThumbnail(textFg), + }); + } + } auto scrollSavingThread = (Data::Thread*)nullptr; auto scrollSavingShift = 0; auto scrollSavingIndex = -1; - if (const auto count = tabs->sectionsCount()) { - const auto scrollTop = scroll->scrollTop(); - auto indexTop = tabs->lookupSectionTop(0); + if (const auto count = slider->sectionsCount()) { + const auto scrollValue = vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + auto indexPosition = slider->lookupSectionPosition(0); for (auto index = 0; index != count; ++index) { - const auto nextTop = (index + 1 != count) - ? tabs->lookupSectionTop(index + 1) - : (indexTop + scrollTop + 1); - if (indexTop <= scrollTop && nextTop > scrollTop) { + const auto nextPosition = (index + 1 != count) + ? slider->lookupSectionPosition(index + 1) + : (indexPosition + scrollValue + 1); + if (indexPosition <= scrollValue && nextPosition > scrollValue) { scrollSavingThread = _sectionsSlice[index]; - scrollSavingShift = scrollTop - indexTop; + scrollSavingShift = scrollValue - indexPosition; break; } - indexTop = nextTop; + indexPosition = nextPosition; } scrollSavingIndex = scrollSavingThread ? int(ranges::find(_slice, not_null(scrollSavingThread)) @@ -666,8 +325,8 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { const auto thread = _sectionsSlice[index]; if (ranges::contains(_slice, thread)) { scrollSavingThread = thread; - scrollSavingShift = scrollTop - - tabs->lookupSectionTop(index); + scrollSavingShift = scrollValue + - slider->lookupSectionPosition(index); scrollSavingIndex = index; break; } @@ -675,20 +334,27 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { } } - tabs->setSections(sections, paused); - tabs->fitHeightToSections(); - tabs->setActiveSectionFast(activeIndex); + slider->setSections({ + .tabs = std::move(sections), + .context = Core::TextContext({ + .session = &_history->session(), + }), + }, paused); + slider->setActiveSectionFast(activeIndex); + _sectionsSlice = _slice; - _vertical->resize( - std::max(toggle->width(), tabs->width()), - _vertical->height()); if (scrollSavingIndex >= 0) { - scroll->scrollToY(tabs->lookupSectionTop(scrollSavingIndex) - + scrollSavingShift); + const auto position = scrollSavingShift + + slider->lookupSectionPosition(scrollSavingIndex); + if (vertical) { + scroll->scrollToY(position); + } else { + scroll->scrollToX(position); + } } _scrollCheckRequests.fire({}); - }, _vertical->lifetime()); + }, scroll->lifetime()); } void SubsectionTabs::loadMore() { @@ -910,9 +576,7 @@ bool SubsectionTabs::UsedFor(not_null<Data::Thread*> thread) { return true; } const auto channel = history->peer->asChannel(); - return channel - && channel->isForum() - && ((channel->flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug(); + return channel && channel->useSubsectionTabs(); } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index fe5054dbe5..65da19a8b4 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -19,6 +19,8 @@ class SessionController; namespace Ui { class RpWidget; +class ScrollArea; +class SubsectionSlider; } // namespace Ui namespace HistoryView { @@ -60,6 +62,11 @@ private: void loadMore(); [[nodiscard]] rpl::producer<> dataChanged() const; + void setupSlider( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider, + bool vertical); + const not_null<Window::SessionController*> _controller; const not_null<History*> _history; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 6de023c332..0bf7da504d 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1255,7 +1255,7 @@ newPeerWidth: 320px; swipeBackSize: 150px; chatTabsToggle: IconButton(defaultIconButton) { - width: 56px; + width: 64px; height: 36px; icon: icon {{ "top_bar_profile-flip_horizontal", menuIconFg }}; iconOver: icon {{ "top_bar_profile-flip_horizontal", menuIconFgOver }}; @@ -1285,6 +1285,13 @@ chatTabsSlider: SettingsSlider(defaultSettingsSlider) { ripple: defaultRippleAnimation; } +ChatTabsOutline { + radius: pixels; + stroke: pixels; + fg: color; + skip: pixels; +} + ChatTabsVertical { barStroke: pixels; barRadius: pixels; @@ -1311,16 +1318,26 @@ chatTabsVertical: ChatTabsVertical { nameStyle: TextStyle(defaultTextStyle) { font: font(10px); } - nameWidth: 46px; - nameTop: 46px; + nameWidth: 54px; + nameTop: 42px; nameFg: windowSubTextFg; nameFgActive: lightButtonFg; userpicTop: 8px; - userpicSize: 36px; - baseHeight: 56px; - width: 56px; + userpicSize: 28px; + baseHeight: 50px; + width: 64px; ripple: defaultRippleAnimation; rippleBg: windowBgOver; rippleBgActive: lightButtonBgOver; duration: 150; } + +chatTabsOutlineHorizontal: ChatTabsOutline { + stroke: 8px; + radius: 4px; + fg: sliderBgActive; + skip: 8px; +} + +chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) { +} diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp new file mode 100644 index 0000000000..d6bf6b1ca7 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -0,0 +1,487 @@ +/* +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 "ui/controls/subsection_tabs_slider.h" + +#include "base/call_delayed.h" +#include "ui/effects/ripple_animation.h" +#include "ui/dynamic_image.h" +#include "styles/style_chat.h" +#include "styles/style_filter_icons.h" + +namespace Ui { +namespace { + +constexpr auto kMaxNameLines = 3; + +class VerticalButton final : public SubsectionButton { +public: + VerticalButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data); + +private: + void paintEvent(QPaintEvent *e) override; + + void dataUpdatedHook() override; + + void updateSize(); + + const style::ChatTabsVertical &_st; + Text::String _text; + bool _subscribed = false; + +}; + +class HorizontalButton final : public SubsectionButton { +public: + HorizontalButton( + not_null<QWidget*> parent, + const style::SettingsSlider &st, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data); + +private: + void paintEvent(QPaintEvent *e) override; + + void dataUpdatedHook() override; + void updateSize(); + + const style::SettingsSlider &_st; + Text::String _text; + +}; + +VerticalButton::VerticalButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data) +: SubsectionButton(parent, delegate, std::move(data)) +, _st(st::chatTabsVertical) +, _text(_st.nameStyle, _data.text, kDefaultTextOptions, _st.nameWidth) { + updateSize(); +} + +void VerticalButton::dataUpdatedHook() { + _text.setMarkedText(_st.nameStyle, _data.text, kDefaultTextOptions); + updateSize(); +} + +void VerticalButton::updateSize() { + resize(_st.width, _st.baseHeight + std::min( + _st.nameStyle.font->height * kMaxNameLines, + _text.countHeight(_st.nameWidth, true))); +} + +void VerticalButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto active = _delegate->buttonActive(this); + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + paintRipple(p, QPoint(0, 0), &color); + + if (!_subscribed) { + _subscribed = true; + _data.userpic->subscribeToUpdates([=] { update(); }); + } + const auto &image = _data.userpic->image(_st.userpicSize); + const auto userpicLeft = (width() - _st.userpicSize) / 2; + p.drawImage(userpicLeft, _st.userpicTop, image); + p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active)); + + const auto textLeft = (width() - _st.nameWidth) / 2; + _text.draw(p, { + .position = QPoint(textLeft, _st.nameTop), + .outerWidth = width(), + .availableWidth = _st.nameWidth, + .align = style::al_top, + .paused = _delegate->buttonPaused(), + }); +} + +HorizontalButton::HorizontalButton( + not_null<QWidget*> parent, + const style::SettingsSlider &st, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data) +: SubsectionButton(parent, delegate, std::move(data)) +, _st(st) { + dataUpdatedHook(); +} + +void HorizontalButton::updateSize() { + resize(_st.strictSkip + _text.maxWidth(), _st.height); +} + +void HorizontalButton::dataUpdatedHook() { + auto context = _delegate->buttonContext(); + context.repaint = [=] { update(); }; + _text.setMarkedText( + _st.labelStyle, + _data.text, + kDefaultTextOptions, + context); + updateSize(); +} + +void HorizontalButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto active = _delegate->buttonActive(this); + + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + paintRipple(p, QPoint(0, 0), &color); + + p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active)); + _text.draw(p, { + .position = QPoint(_st.strictSkip / 2, _st.labelTop), + .outerWidth = width(), + .availableWidth = _text.maxWidth(), + .paused = _delegate->buttonPaused(), + }); +} + +} // namespace + +SubsectionButton::SubsectionButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data) +: RippleButton(parent, st::defaultRippleAnimationBgOver) +, _delegate(delegate) +, _data(std::move(data)) { +} + +SubsectionButton::~SubsectionButton() = default; + +void SubsectionButton::setData(SubsectionTab &&data) { + _data = std::move(data); + dataUpdatedHook(); + update(); +} + +DynamicImage *SubsectionButton::userpic() const { + return _data.userpic.get(); +} + +void SubsectionButton::setActiveShown(float64 activeShown) { + if (_activeShown != activeShown) { + _activeShown = activeShown; + update(); + } +} + +SubsectionSlider::SubsectionSlider(not_null<QWidget*> parent, bool vertical) +: RpWidget(parent) +, _vertical(vertical) +, _barSt(vertical + ? st::chatTabsOutlineVertical + : st::chatTabsOutlineHorizontal) +, _bar(CreateChild<RpWidget>(this)) +, _barRect(_barSt.radius, _barSt.fg) { + setupBar(); +} + +SubsectionSlider::~SubsectionSlider() = default; + +void SubsectionSlider::setupBar() { + _bar->setAttribute(Qt::WA_TransparentForMouseEvents); + sizeValue() | rpl::start_with_next([=](QSize size) { + const auto thickness = _barSt.stroke - (_barSt.stroke / 2); + _bar->setGeometry( + 0, + _vertical ? 0 : (size.height() - thickness), + _vertical ? thickness : size.width(), + _vertical ? size.height() : thickness); + }, _bar->lifetime()); + _bar->paintRequest() | rpl::start_with_next([=](QRect clip) { + const auto start = -_barSt.stroke / 2; + const auto finalRange = getFinalActiveRange(); + const auto currentRange = getCurrentActiveRange(); + const auto from = currentRange.from + _barSt.skip; + const auto size = currentRange.size - 2 * _barSt.skip; + if (size <= 0) { + return; + } + const auto rect = myrtlrect( + _vertical ? start : from, + _vertical ? from : 0, + _vertical ? _barSt.stroke : size, + _vertical ? size : _barSt.stroke); + if (rect.intersects(clip)) { + auto p = QPainter(_bar); + _barRect.paint(p, rect); + } + }, _bar->lifetime()); +} + +void SubsectionSlider::setSections( + SubsectionTabs sections, + Fn<bool()> paused) { + Expects(!sections.tabs.empty()); + + _context = sections.context; + _paused = std::move(paused); + _fixedCount = sections.fixed; + _pinnedCount = sections.pinned; + _reorderAllowed = sections.reorder; + + auto old = base::take(_tabs); + _tabs.reserve(sections.tabs.size()); + + auto size = 0; + for (auto &data : sections.tabs) { + const auto i = data.userpic + ? ranges::find( + old, + data.userpic.get(), + &SubsectionButton::userpic) + : old.empty() + ? end(old) + : (end(old) - 1); + if (i != end(old)) { + _tabs.push_back(std::move(*i)); + old.erase(i); + _tabs.back()->setData(std::move(data)); + } else { + _tabs.push_back(makeButton(std::move(data))); + _tabs.back()->show(); + } + _tabs.back()->move(_vertical ? 0 : size, _vertical ? size : 0); + + const auto index = int(_tabs.size()) - 1; + _tabs.back()->setClickedCallback([=] { + activate(index); + }); + size += _vertical ? _tabs.back()->height() : _tabs.back()->width(); + } + + if (!_tabs.empty()) { + resize( + _vertical ? _tabs.front()->width() : size, + _vertical ? size : _tabs.front()->height()); + } + + _bar->raise(); +} + +void SubsectionSlider::activate(int index) { + if (_active == index) { + return; + } + const auto old = _active; + const auto was = getFinalActiveRange(); + _active = index; + const auto now = getFinalActiveRange(); + const auto callback = [=] { + _bar->update(); + for (auto i = std::min(old, index); i != std::max(old, index); ++i) { + if (i >= 0 && i < int(_tabs.size())) { + _tabs[i]->update(); + } + } + }; + const auto duration = st::chatTabsSlider.duration; + _activeFrom.start(callback, was.from, now.from, duration); + _activeSize.start(callback, was.size, now.size, duration); + base::call_delayed(duration, this, [=] { + if (_active == index) { + _sectionActivated.fire_copy(index); + } + }); +} + +void SubsectionSlider::setActiveSectionFast(int active) { + Expects(active < int(_tabs.size())); + + _active = active; + _activeFrom.stop(); + _activeSize.stop(); + _bar->update(); +} + +int SubsectionSlider::sectionsCount() const { + return int(_tabs.size()); +} + +rpl::producer<int> SubsectionSlider::sectionActivated() const { + return _sectionActivated.events(); +} + +int SubsectionSlider::lookupSectionPosition(int index) const { + Expects(index >= 0 && index < _tabs.size()); + + return _vertical ? _tabs[index]->y() : _tabs[index]->x(); +} + +void SubsectionSlider::paintEvent(QPaintEvent *e) { +} + +int SubsectionSlider::lookupSectionIndex(QPoint position) const { + Expects(!_tabs.empty()); + + const auto count = sectionsCount(); + if (_vertical) { + for (auto i = 0; i != count; ++i) { + const auto tab = _tabs[i].get(); + if (position.y() < tab->y() + tab->height()) { + return i; + } + } + } else { + for (auto i = 0; i != count; ++i) { + const auto tab = _tabs[i].get(); + if (position.x() < tab->x() + tab->width()) { + return i; + } + } + } + return count - 1; +} + +SubsectionSlider::Range SubsectionSlider::getFinalActiveRange() const { + if (_active < 0 || _active >= _tabs.size()) { + return {}; + } + const auto tab = _tabs[_active].get(); + return Range{ + .from = _vertical ? tab->y() : tab->x(), + .size = _vertical ? tab->height() : tab->width(), + }; +} + +SubsectionSlider::Range SubsectionSlider::getCurrentActiveRange() const { + const auto finalRange = getFinalActiveRange(); + return { + .from = int(base::SafeRound(_activeFrom.value(finalRange.from))), + .size = int(base::SafeRound(_activeSize.value(finalRange.size))), + }; +} + +bool SubsectionSlider::buttonPaused() { + return _paused && _paused(); +} + +float64 SubsectionSlider::buttonActive(not_null<SubsectionButton*> button) { + const auto finalRange = getFinalActiveRange(); + const auto currentRange = getCurrentActiveRange(); + const auto from = _vertical ? button->y() : button->x(); + const auto size = _vertical ? button->height() : button->width(); + const auto checkSize = std::min(size, currentRange.size); + return (checkSize > 0) + ? (1. - (std::abs(currentRange.from - from) / float64(checkSize))) + : 0.; +} + +Text::MarkedContext SubsectionSlider::buttonContext() { + return _context; +} + +not_null<SubsectionButton*> SubsectionSlider::buttonAt(int index) { + Expects(index >= 0 && index < _tabs.size()); + + return _tabs[index].get(); +} + +VerticalSlider::VerticalSlider(not_null<QWidget*> parent) +: SubsectionSlider(parent, true) +, _st(st::chatTabsVertical) { +} + +VerticalSlider::~VerticalSlider() = default; + +std::unique_ptr<SubsectionButton> VerticalSlider::makeButton( + SubsectionTab &&data) { + return std::make_unique<VerticalButton>( + this, + static_cast<SubsectionButtonDelegate*>(this), + std::move(data)); +} + +HorizontalSlider::HorizontalSlider(not_null<QWidget*> parent) +: SubsectionSlider(parent, false) +, _st(st::chatTabsSlider) { +} + +HorizontalSlider::~HorizontalSlider() = default; + +std::unique_ptr<SubsectionButton> HorizontalSlider::makeButton( + SubsectionTab &&data) { + return std::make_unique<HorizontalButton>( + this, + _st, + static_cast<SubsectionButtonDelegate*>(this), + std::move(data)); +} + +std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail( + Fn<QColor()> textColor) { + class Image final : public DynamicImage { + public: + Image(Fn<QColor()> textColor) : _textColor(std::move(textColor)) { + Expects(_textColor != nullptr); + } + + std::shared_ptr<DynamicImage> clone() { + return std::make_shared<Image>(_textColor); + } + + QImage image(int size) { + const auto ratio = style::DevicePixelRatio(); + const auto full = size * ratio; + const auto color = _textColor(); + if (_cache.size() != QSize(full, full)) { + _cache = QImage( + QSize(full, full), + QImage::Format_ARGB32_Premultiplied); + _cache.fill(Qt::TransparentMode); + } else if (_color == color) { + return _cache; + } + _color = color; + if (_mask.isNull()) { + _mask = st::foldersAll.instance(QColor(255, 255, 255)); + } + const auto position = ratio * QPoint( + (size - (_mask.width() / ratio)) / 2, + (size - (_mask.height() / ratio)) / 2); + if (_mask.width() <= full && _mask.height() <= full) { + style::colorizeImage(_mask, color, &_cache, QRect(), position); + } else { + _cache = style::colorizeImage(_mask, color).scaled( + full, + full, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + _cache.setDevicePixelRatio(ratio); + } + return _cache; + } + void subscribeToUpdates(Fn<void()> callback) { + if (!callback) { + _cache = QImage(); + _mask = QImage(); + } + } + + private: + Fn<QColor()> _textColor; + QImage _mask; + QImage _cache; + QColor _color; + + }; + return std::make_shared<Image>(std::move(textColor)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h new file mode 100644 index 0000000000..80842d4169 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -0,0 +1,163 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/round_rect.h" +#include "ui/rp_widget.h" +#include "ui/widgets/buttons.h" + +namespace style { +struct ChatTabsVertical; +struct ChatTabsOutline; +} // namespace style + +namespace Ui { + +class DynamicImage; +class RippleAnimation; +class SubsectionButton; + +struct SubsectionTab { + TextWithEntities text; + std::shared_ptr<DynamicImage> userpic; + int counter = 0; + bool muted = false; + bool mention = false; + bool reaciton = false; +}; + +struct SubsectionTabs { + std::vector<SubsectionTab> tabs; + Text::MarkedContext context; + int fixed = 0; + int pinned = 0; + bool reorder = false; +}; + +class SubsectionButtonDelegate { +public: + virtual bool buttonPaused() = 0; + virtual float64 buttonActive(not_null<SubsectionButton*> button) = 0; + virtual Text::MarkedContext buttonContext() = 0; +}; + +class SubsectionButton : public RippleButton { +public: + SubsectionButton( + not_null<QWidget*> parent, + not_null<SubsectionButtonDelegate*> delegate, + SubsectionTab &&data); + ~SubsectionButton(); + + void setData(SubsectionTab &&data); + [[nodiscard]] DynamicImage *userpic() const; + + void setActiveShown(float64 activeShown); + +protected: + virtual void dataUpdatedHook() = 0; + + const not_null<SubsectionButtonDelegate*> _delegate; + SubsectionTab _data; + float64 _activeShown = 0.; + +}; + +class SubsectionSlider + : public RpWidget + , public SubsectionButtonDelegate { +public: + ~SubsectionSlider(); + + void setSections( + SubsectionTabs sections, + Fn<bool()> paused); + void setActiveSectionFast(int active); + + [[nodiscard]] int sectionsCount() const; + [[nodiscard]] rpl::producer<int> sectionActivated() const; + [[nodiscard]] int lookupSectionPosition(int index) const; + + bool buttonPaused() override; + float64 buttonActive(not_null<SubsectionButton*> button) override; + Text::MarkedContext buttonContext() override; + [[nodiscard]] not_null<SubsectionButton*> buttonAt(int index); + +protected: + struct Range { + int from = 0; + int size = 0; + }; + + SubsectionSlider(not_null<QWidget*> parent, bool vertical); + void setupBar(); + + void paintEvent(QPaintEvent *e) override; + + [[nodiscard]] int lookupSectionIndex(QPoint position) const; + [[nodiscard]] Range getFinalActiveRange() const; + [[nodiscard]] Range getCurrentActiveRange() const; + void activate(int index); + + [[nodiscard]] virtual std::unique_ptr<SubsectionButton> makeButton( + SubsectionTab &&data) = 0; + + const bool _vertical = false; + + const style::ChatTabsOutline &_barSt; + RpWidget *_bar = nullptr; + RoundRect _barRect; + + std::vector<std::unique_ptr<SubsectionButton>> _tabs; + int _active = -1; + int _pressed = -1; + Animations::Simple _activeFrom; + Animations::Simple _activeSize; + + //int _buttonIndexHint = 0; + + Text::MarkedContext _context; + int _fixedCount = 0; + int _pinnedCount = 0; + bool _reorderAllowed = false; + + rpl::event_stream<int> _sectionActivated; + Fn<bool()> _paused; + +}; + +class VerticalSlider final : public SubsectionSlider { +public: + explicit VerticalSlider(not_null<QWidget*> parent); + ~VerticalSlider(); + +private: + std::unique_ptr<SubsectionButton> makeButton( + SubsectionTab &&data) override; + + const style::ChatTabsVertical &_st; + +}; + +class HorizontalSlider final : public SubsectionSlider { +public: + explicit HorizontalSlider(not_null<QWidget*> parent); + ~HorizontalSlider(); + +private: + std::unique_ptr<SubsectionButton> makeButton( + SubsectionTab &&data) override; + + const style::SettingsSlider &_st; + +}; + +[[nodiscard]] std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail( + Fn<QColor()> textColor); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp index f6538867a9..42dec878d3 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp @@ -196,7 +196,11 @@ private: class EmojiThumbnail final : public DynamicImage { public: - EmojiThumbnail(not_null<Data::Session*> owner, const QString &data); + EmojiThumbnail( + not_null<Data::Session*> owner, + const QString &data, + Fn<bool()> paused, + Fn<QColor()> textColor); std::shared_ptr<DynamicImage> clone() override; @@ -207,6 +211,8 @@ private: const not_null<Data::Session*> _owner; const QString _data; std::unique_ptr<Ui::Text::CustomEmoji> _emoji; + Fn<bool()> _paused; + Fn<QColor()> _textColor; QImage _frame; }; @@ -581,9 +587,13 @@ void IconThumbnail::subscribeToUpdates(Fn<void()> callback) { EmojiThumbnail::EmojiThumbnail( not_null<Data::Session*> owner, - const QString &data) + const QString &data, + Fn<bool()> paused, + Fn<QColor()> textColor) : _owner(owner) -, _data(data) { +, _data(data) +, _paused(std::move(paused)) +, _textColor(std::move(textColor)) { } void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) { @@ -598,7 +608,11 @@ void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) { } std::shared_ptr<DynamicImage> EmojiThumbnail::clone() { - return std::make_shared<EmojiThumbnail>(_owner, _data); + return std::make_shared<EmojiThumbnail>( + _owner, + _data, + _paused, + _textColor); } QImage EmojiThumbnail::image(int size) { @@ -614,12 +628,16 @@ QImage EmojiThumbnail::image(int size) { } _frame.fill(Qt::transparent); + const auto esize = Text::AdjustCustomEmojiSize( + Emoji::GetSizeLarge() / style::DevicePixelRatio()); + const auto eskip = (size - esize) / 2; + auto p = Painter(&_frame); _emoji->paint(p, { - .textColor = st::windowBoldFg->c, + .textColor = _textColor ? _textColor() : st::windowBoldFg->c, .now = crl::now(), - .position = QPoint(0, 0), - .paused = false, + .position = QPoint(eskip, eskip), + .paused = _paused && _paused(), }); p.end(); @@ -665,8 +683,14 @@ std::shared_ptr<DynamicImage> MakeIconThumbnail(const style::icon &icon) { std::shared_ptr<DynamicImage> MakeEmojiThumbnail( not_null<Data::Session*> owner, - const QString &data) { - return std::make_shared<EmojiThumbnail>(owner, data); + const QString &data, + Fn<bool()> paused, + Fn<QColor()> textColor) { + return std::make_shared<EmojiThumbnail>( + owner, + data, + std::move(paused), + std::move(textColor)); } std::shared_ptr<DynamicImage> MakePhotoThumbnail( diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.h b/Telegram/SourceFiles/ui/dynamic_thumbnails.h index 08ae74052a..6e003bbe35 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.h +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.h @@ -33,7 +33,9 @@ class DynamicImage; const style::icon &icon); [[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail( not_null<Data::Session*> owner, - const QString &data); + const QString &data, + Fn<bool()> paused = false, + Fn<QColor()> textColor = nullptr); [[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnail( not_null<PhotoData*> photo, FullMsgId fullId); diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index c6de18b209..f39e17f834 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -386,6 +386,8 @@ PRIVATE ui/controls/send_as_button.h ui/controls/send_button.cpp ui/controls/send_button.h + ui/controls/subsection_tabs_slider.cpp + ui/controls/subsection_tabs_slider.h ui/controls/swipe_handler.cpp ui/controls/swipe_handler.h ui/controls/swipe_handler_data.h From 5943052cd1f4695377f487b14b36828a1faaf842 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 May 2025 16:59:38 +0400 Subject: [PATCH 080/310] Show badges in new tabs. --- Telegram/SourceFiles/dialogs/dialogs_common.h | 3 + .../view/history_view_subsection_tabs.cpp | 105 ++++++++++++++---- .../view/history_view_subsection_tabs.h | 22 +++- .../ui/controls/subsection_tabs_slider.cpp | 91 ++++++++++++++- .../ui/controls/subsection_tabs_slider.h | 6 +- 5 files changed, 197 insertions(+), 30 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_common.h b/Telegram/SourceFiles/dialogs/dialogs_common.h index c1e1cd755d..34b844d296 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_common.h +++ b/Telegram/SourceFiles/dialogs/dialogs_common.h @@ -93,6 +93,9 @@ struct BadgesState { friend inline constexpr auto operator<=>( BadgesState, BadgesState) = default; + friend inline constexpr bool operator==( + BadgesState, + BadgesState) = default; [[nodiscard]] bool empty() const { return !unread && !mention && !reaction; diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index a4b4e64700..932504952b 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -180,11 +180,11 @@ void SubsectionTabs::setupSlider( slider->sectionActivated() | rpl::start_with_next([=](int active) { if (active >= 0 && active < _slice.size() - && _active != _slice[active]) { + && _active != _slice[active].thread) { auto params = Window::SectionShow(); params.way = Window::SectionShow::Way::ClearStack; params.animated = anim::type::instant; - _controller->showThread(_slice[active], {}, params); + _controller->showThread(_slice[active].thread, {}, params); } }, slider->lifetime()); @@ -209,11 +209,11 @@ void SubsectionTabs::setupSlider( if (availableFrom < full && _beforeSkipped.value_or(0) > 0 && !_slice.empty()) { - _around = _slice.front(); + _around = _slice.front().thread; refreshSlice(); } else if (availableTill < full) { if (_afterAvailable > 0) { - _around = _slice.back(); + _around = _slice.back().thread; refreshSlice(); } else if (!_afterSkipped.has_value()) { _loading = true; @@ -232,9 +232,9 @@ void SubsectionTabs::setupSlider( }; auto sections = std::vector<Ui::SubsectionTab>(); auto activeIndex = -1; - for (const auto &thread : _slice) { + for (const auto &item : _slice) { const auto index = int(sections.size()); - if (thread == _active) { + if (item.thread == _active) { activeIndex = index; } const auto textFg = [=] { @@ -243,23 +243,24 @@ void SubsectionTabs::setupSlider( st::windowActiveTextFg, slider->buttonActive(slider->buttonAt(index))); }; - if (const auto topic = thread->asTopic()) { + if (const auto topic = item.thread->asTopic()) { if (vertical) { + const auto general = topic->isGeneral(); sections.push_back({ - .text = { topic->title() }, - .userpic = (topic->iconId() + .text = { item.name }, + .userpic = (item.iconId ? Ui::MakeEmojiThumbnail( &topic->owner(), - Data::SerializeCustomEmojiId(topic->iconId()), + Data::SerializeCustomEmojiId(item.iconId), paused, textFg) : Ui::MakeEmojiThumbnail( &topic->owner(), Data::TopicIconEmojiEntity({ - .title = (topic->isGeneral() + .title = (general ? Data::ForumGeneralIconTitle() - : topic->title()), - .colorId = (topic->isGeneral() + : item.name), + .colorId = (general ? Data::ForumGeneralIconColor( st::windowSubTextFg->c) : topic->colorId()), @@ -272,7 +273,7 @@ void SubsectionTabs::setupSlider( .text = topic->titleWithIcon(), }); } - } else if (const auto sublist = thread->asSublist()) { + } else if (const auto sublist = item.thread->asSublist()) { const auto peer = sublist->sublistPeer(); if (vertical) { sections.push_back({ @@ -294,6 +295,8 @@ void SubsectionTabs::setupSlider( .userpic = Ui::MakeAllSubsectionsThumbnail(textFg), }); } + auto §ion = sections.back(); + section.badges = item.badges; } auto scrollSavingThread = (Data::Thread*)nullptr; @@ -309,21 +312,24 @@ void SubsectionTabs::setupSlider( ? slider->lookupSectionPosition(index + 1) : (indexPosition + scrollValue + 1); if (indexPosition <= scrollValue && nextPosition > scrollValue) { - scrollSavingThread = _sectionsSlice[index]; + scrollSavingThread = _sectionsSlice[index].thread; scrollSavingShift = scrollValue - indexPosition; break; } indexPosition = nextPosition; } scrollSavingIndex = scrollSavingThread - ? int(ranges::find(_slice, not_null(scrollSavingThread)) - - begin(_slice)) + ? int(ranges::find( + _slice, + not_null(scrollSavingThread), + &Item::thread + ) - begin(_slice)) : -1; if (scrollSavingIndex == _slice.size()) { scrollSavingIndex = -1; for (auto index = 0; index != count; ++index) { - const auto thread = _sectionsSlice[index]; - if (ranges::contains(_slice, thread)) { + const auto thread = _sectionsSlice[index].thread; + if (ranges::contains(_slice, thread, &Item::thread)) { scrollSavingThread = thread; scrollSavingShift = scrollValue - slider->lookupSectionPosition(index); @@ -483,6 +489,7 @@ void SubsectionTabs::setVisible(bool shown) { } void SubsectionTabs::track() { + using Event = Data::Session::ChatListEntryRefresh; if (const auto forum = _history->peer->forum()) { forum->topicDestroyed( ) | rpl::start_with_next([=](not_null<Data::ForumTopic*> topic) { @@ -491,6 +498,19 @@ void SubsectionTabs::track() { refreshSlice(); } }, _lifetime); + + forum->topicsList()->unreadStateChanges( + ) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + + forum->owner().chatListEntryRefreshes( + ) | rpl::filter([=](const Event &event) { + const auto topic = event.filterId ? nullptr : event.key.topic(); + return (topic && topic->forum() == forum); + }) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); } else if (const auto monoforum = _history->peer->monoforum()) { monoforum->sublistDestroyed( ) | rpl::start_with_next([=](not_null<Data::SavedSublist*> sublist) { @@ -499,12 +519,29 @@ void SubsectionTabs::track() { refreshSlice(); } }, _lifetime); + + monoforum->chatsList()->unreadStateChanges( + ) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + + monoforum->owner().chatListEntryRefreshes( + ) | rpl::filter([=](const Event &event) { + const auto sublist = event.filterId + ? nullptr + : event.key.sublist(); + return (sublist && sublist->parent() == monoforum); + }) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); } else { Unexpected("Peer in SubsectionTabs::track."); } } void SubsectionTabs::refreshSlice() { + _refreshScheduled = false; + const auto forum = _history->peer->forum(); const auto monoforum = _history->peer->monoforum(); Assert(forum || monoforum); @@ -512,15 +549,25 @@ void SubsectionTabs::refreshSlice() { const auto list = forum ? forum->topicsList() : monoforum->chatsList(); - auto slice = std::vector<not_null<Data::Thread*>>(); + auto slice = std::vector<Item>(); + slice.reserve(_slice.size() + 10); const auto guard = gsl::finally([&] { if (_slice != slice) { _slice = std::move(slice); _refreshed.fire({}); } }); + const auto push = [&](not_null<Data::Thread*> thread) { + const auto topic = thread->asTopic(); + slice.push_back({ + .thread = thread, + .badges = thread->chatListBadgesState(), + .iconId = topic ? topic->iconId() : DocumentId(), + .name = thread->chatListName(), + }); + }; if (!list) { - slice.push_back(_history); + push(_history); _beforeSkipped = _afterSkipped = 0; _afterAvailable = 0; return; @@ -542,13 +589,25 @@ void SubsectionTabs::refreshSlice() { _afterAvailable = std::max(0, int(chats.end() - till)); _afterSkipped = list->loaded() ? _afterAvailable : std::optional<int>(); if (from == chats.begin()) { - slice.push_back(_history); + push(_history); } for (auto i = from; i != till; ++i) { - slice.push_back((*i)->thread()); + push((*i)->thread()); } } +void SubsectionTabs::scheduleRefresh() { + if (_refreshScheduled) { + return; + } + _refreshScheduled = true; + InvokeQueued(_shadow, [=] { + if (_refreshScheduled) { + refreshSlice(); + } + }); +} + bool SubsectionTabs::switchTo( not_null<Data::Thread*> thread, not_null<Ui::RpWidget*> parent) { diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index 65da19a8b4..d198a2fd79 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "dialogs/dialogs_common.h" + class History; namespace Data { @@ -53,12 +55,27 @@ public: void hide(); private: + struct Item { + not_null<Data::Thread*> thread; + Dialogs::BadgesState badges; + DocumentId iconId = 0; + QString name; + + friend inline constexpr auto operator<=>( + const Item &, + const Item &) = default; + friend inline constexpr bool operator==( + const Item &, + const Item &) = default; + }; + void track(); void setupHorizontal(not_null<QWidget*> parent); void setupVertical(not_null<QWidget*> parent); void toggleModes(); void setVisible(bool shown); void refreshSlice(); + void scheduleRefresh(); void loadMore(); [[nodiscard]] rpl::producer<> dataChanged() const; @@ -74,8 +91,8 @@ private: Ui::RpWidget *_vertical = nullptr; Ui::RpWidget *_shadow = nullptr; - std::vector<not_null<Data::Thread*>> _slice; - std::vector<not_null<Data::Thread*>> _sectionsSlice; + std::vector<Item> _slice; + std::vector<Item> _sectionsSlice; not_null<Data::Thread*> _active; not_null<Data::Thread*> _around; @@ -83,6 +100,7 @@ private: int _afterLimit = 0; int _afterAvailable = 0; bool _loading = false; + bool _refreshScheduled = false; std::optional<int> _beforeSkipped; std::optional<int> _afterSkipped; diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index d6bf6b1ca7..15691b6e21 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -8,9 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/subsection_tabs_slider.h" #include "base/call_delayed.h" +#include "dialogs/dialogs_three_state_icon.h" #include "ui/effects/ripple_animation.h" #include "ui/dynamic_image.h" +#include "ui/unread_badge_paint.h" #include "styles/style_chat.h" +#include "styles/style_dialogs.h" #include "styles/style_filter_icons.h" namespace Ui { @@ -105,6 +108,41 @@ void VerticalButton::paintEvent(QPaintEvent *e) { .align = style::al_top, .paused = _delegate->buttonPaused(), }); + + const auto &state = _data.badges; + const auto top = _st.userpicTop / 2; + auto right = width() - textLeft; + UnreadBadgeStyle st; + if (state.unread) { + st.muted = state.unreadMuted; + const auto counter = (state.unreadCounter <= 0) + ? QString() + : ((state.mention || state.reaction) + && (state.unreadCounter > 999)) + ? (u"99+"_q) + : (state.unreadCounter > 999999) + ? (u"99999+"_q) + : QString::number(state.unreadCounter); + const auto badge = PaintUnreadBadge(p, counter, right, top, st); + right -= badge.width() + st.padding; + } + if (state.mention || state.reaction) { + UnreadBadgeStyle st; + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.muted = state.mention + ? state.mentionMuted + : state.reactionMuted; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = PaintUnreadBadge(p, counter, right, top, st); + (state.mention + ? st::dialogsUnreadMention.icon + : st::dialogsUnreadReaction.icon).paintInCenter(p, badge); + right -= badge.width() + st.padding + st::dialogsUnreadPadding; + } } HorizontalButton::HorizontalButton( @@ -118,7 +156,28 @@ HorizontalButton::HorizontalButton( } void HorizontalButton::updateSize() { - resize(_st.strictSkip + _text.maxWidth(), _st.height); + auto width = _st.strictSkip + _text.maxWidth(); + + const auto &state = _data.badges; + UnreadBadgeStyle st; + if (state.unread) { + const auto counter = (state.unreadCounter <= 0) + ? QString() + : QString::number(state.unreadCounter); + const auto badge = CountUnreadBadgeSize(counter, st); + width += badge.width() + st.padding; + } + if (state.mention || state.reaction) { + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = CountUnreadBadgeSize(counter, st); + width += badge.width() + st.padding + st::dialogsUnreadPadding; + } + resize(width, _st.height); } void HorizontalButton::dataUpdatedHook() { @@ -149,6 +208,36 @@ void HorizontalButton::paintEvent(QPaintEvent *e) { .availableWidth = _text.maxWidth(), .paused = _delegate->buttonPaused(), }); + + auto right = width() - _st.strictSkip + (_st.strictSkip / 2); + UnreadBadgeStyle st; + const auto &state = _data.badges; + const auto badgeTop = (height() - st.size) / 2; + if (state.unread) { + st.muted = state.unreadMuted; + const auto counter = (state.unreadCounter <= 0) + ? QString() + : QString::number(state.unreadCounter); + const auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st); + right -= badge.width() + st.padding; + } + if (state.mention || state.reaction) { + UnreadBadgeStyle st; + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.muted = state.mention + ? state.mentionMuted + : state.reactionMuted; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st); + (state.mention + ? st::dialogsUnreadMention.icon + : st::dialogsUnreadReaction.icon).paintInCenter(p, badge); + right -= badge.width() + st.padding + st::dialogsUnreadPadding; + } } } // namespace diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h index 80842d4169..391c51964c 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "dialogs/dialogs_common.h" #include "ui/round_rect.h" #include "ui/rp_widget.h" #include "ui/widgets/buttons.h" @@ -25,10 +26,7 @@ class SubsectionButton; struct SubsectionTab { TextWithEntities text; std::shared_ptr<DynamicImage> userpic; - int counter = 0; - bool muted = false; - bool mention = false; - bool reaciton = false; + Dialogs::BadgesState badges; }; struct SubsectionTabs { From d7c964afc5fb555e18da06a5ad0740d50441c0fa Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 May 2025 18:05:36 +0400 Subject: [PATCH 081/310] Show "Messages" badge for monoforum. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/api/api_chat_invite.h | 2 +- Telegram/SourceFiles/boxes/peer_list_box.cpp | 3 ++ .../dialogs/dialogs_inner_widget.cpp | 5 ++ .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 7 ++- Telegram/SourceFiles/history/history.cpp | 3 ++ .../view/history_view_top_bar_widget.cpp | 1 + .../info/profile/info_profile_badge.cpp | 19 ++++--- .../info/profile/info_profile_badge.h | 3 +- .../info/profile/info_profile_values.cpp | 5 ++ .../info/profile/info_profile_values.h | 2 +- Telegram/SourceFiles/ui/unread_badge.cpp | 52 ++++++++++++------- Telegram/SourceFiles/ui/unread_badge.h | 15 ++++-- 13 files changed, 85 insertions(+), 33 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 0ad0b60ee9..39db6ae517 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -169,6 +169,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_status" = "group"; "lng_scam_badge" = "SCAM"; "lng_fake_badge" = "FAKE"; +"lng_direct_badge" = "MESSAGES"; "lng_remember" = "Remember this choice"; diff --git a/Telegram/SourceFiles/api/api_chat_invite.h b/Telegram/SourceFiles/api/api_chat_invite.h index 94eeab5e92..123ccb1f8d 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.h +++ b/Telegram/SourceFiles/api/api_chat_invite.h @@ -14,7 +14,7 @@ class ChannelData; namespace Info::Profile { class Badge; -enum class BadgeType; +enum class BadgeType : uchar; } // namespace Info::Profile namespace Main { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 10feb5e527..5ff2323d27 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -810,6 +810,9 @@ int PeerListRow::paintNameIconGetWidth( ? st::dialogsPremiumIcon.over : st::dialogsPremiumIcon.icon), .scam = &(selected ? st::dialogsScamFgOver : st::dialogsScamFg), + .direct = &(selected + ? st::windowSubTextFgOver + : st::windowSubTextFg), .premiumFg = &(selected ? st::dialogsVerifiedIconBgOver : st::dialogsVerifiedIconBg), diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 9892c01598..8775b85569 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1606,6 +1606,11 @@ void InnerWidget::paintPeerSearchResult( : context.selected ? &st::dialogsScamFgOver : &st::dialogsScamFg), + .direct = (context.active + ? &st::dialogsDraftFgActive + : context.selected + ? &st::windowSubTextFgOver + : &st::windowSubTextFg), .premiumFg = (context.active ? &st::dialogsVerifiedIconBgActive : context.selected diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index f2d58e995e..924b7c0f4d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -750,6 +750,11 @@ void PaintRow( : context.selected ? &st::dialogsScamFgOver : &st::dialogsScamFg), + .direct = (context.active + ? &st::dialogsDraftFgActive + : context.selected + ? &st::windowSubTextFgOver + : &st::windowSubTextFg), .premiumFg = (context.active ? &st::dialogsVerifiedIconBgActive : context.selected @@ -923,7 +928,7 @@ const style::icon *ChatTypeIcon( st::dialogsChannelIcon, context.active, context.selected); - } else if (peer->isForum() || peer->amMonoforumAdmin()) { + } else if (peer->isForum()) { return &ThreeStateIcon( st::dialogsForumIcon, context.active, diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 3c40fbe973..3f4540c60c 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2363,6 +2363,9 @@ bool History::chatListMessageKnown() const { } const QString &History::chatListName() const { + if (const auto broadcast = peer->monoforumBroadcast()) { + return broadcast->name(); + } return peer->name(); } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index d965afcf18..532f384480 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -604,6 +604,7 @@ void TopBarWidget::paintTopBar(Painter &p) { .verified = &st::dialogsVerifiedIcon, .premium = &st::dialogsPremiumIcon.icon, .scam = &st::attentionButtonFg, + .direct = &st::windowSubTextFg, .premiumFg = &st::dialogsVerifiedIconBg, .customEmojiRepaint = [=] { update(); }, .now = now, diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp index b87931baea..8cdfa8c12f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp @@ -131,9 +131,14 @@ void Badge::setContent(Content content) { }, _view->lifetime()); } break; case BadgeType::Scam: - case BadgeType::Fake: { - const auto fake = (_content.badge == BadgeType::Fake); - const auto size = Ui::ScamBadgeSize(fake); + case BadgeType::Fake: + case BadgeType::Direct: { + const auto type = (_content.badge == BadgeType::Direct) + ? Ui::TextBadgeType::Direct + : (_content.badge == BadgeType::Fake) + ? Ui::TextBadgeType::Fake + : Ui::TextBadgeType::Scam; + const auto size = Ui::TextBadgeSize(type); const auto skip = st::infoVerifiedCheckPosition.x(); _view->resize( size.width() + 2 * skip, @@ -141,12 +146,14 @@ void Badge::setContent(Content content) { _view->paintRequest( ) | rpl::start_with_next([=, badge = _view.data()]{ Painter p(badge); - Ui::DrawScamBadge( - fake, + Ui::DrawTextBadge( + type, p, badge->rect().marginsRemoved({ skip, skip, skip, skip }), badge->width(), - st::attentionButtonFg); + (type == Ui::TextBadgeType::Direct + ? st::windowSubTextFg + : st::attentionButtonFg)); }, _view->lifetime()); } break; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.h b/Telegram/SourceFiles/info/profile/info_profile_badge.h index 9f8d782684..387eeadbca 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_badge.h +++ b/Telegram/SourceFiles/info/profile/info_profile_badge.h @@ -35,13 +35,14 @@ namespace Info::Profile { class EmojiStatusPanel; -enum class BadgeType { +enum class BadgeType : uchar { None = 0x00, Verified = 0x01, BotVerified = 0x02, Premium = 0x04, Scam = 0x08, Fake = 0x10, + Direct = 0x20, }; inline constexpr bool is_flag_type(BadgeType) { return true; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 725ad58ad7..16e8b231f7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -93,6 +93,9 @@ void StripExternalLinks(TextWithEntities &text) { } // namespace rpl::producer<QString> NameValue(not_null<PeerData*> peer) { + if (const auto broadcast = peer->monoforumBroadcast()) { + return NameValue(broadcast); + } return peer->session().changes().peerFlagsValue( peer, UpdateFlag::Name @@ -659,6 +662,8 @@ rpl::producer<BadgeType> BadgeValueFromFlags(Peer peer) { ? BadgeType::Scam : (value & Flag::Fake) ? BadgeType::Fake + : peer->isMonoforum() + ? BadgeType::Direct : (value & Flag::Verified) ? BadgeType::Verified : premium diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index dab3cf5065..52f0148bd4 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -130,7 +130,7 @@ struct LinkWithUrl { [[nodiscard]] rpl::producer<bool> CanViewParticipantsValue( not_null<ChannelData*> megagroup); -enum class BadgeType; +enum class BadgeType : uchar; [[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer); [[nodiscard]] rpl::producer<EmojiStatusId> EmojiStatusIdValue( not_null<PeerData*> peer); diff --git a/Telegram/SourceFiles/ui/unread_badge.cpp b/Telegram/SourceFiles/ui/unread_badge.cpp index d53f617da9..8730b46c58 100644 --- a/Telegram/SourceFiles/ui/unread_badge.cpp +++ b/Telegram/SourceFiles/ui/unread_badge.cpp @@ -71,10 +71,17 @@ void UnreadBadge::paintEvent(QPaintEvent *e) { unreadSt); } -QSize ScamBadgeSize(bool fake) { - const auto phrase = fake - ? tr::lng_fake_badge(tr::now) - : tr::lng_scam_badge(tr::now); +QString TextBadgeText(TextBadgeType type) { + switch (type) { + case TextBadgeType::Fake: return tr::lng_fake_badge(tr::now); + case TextBadgeType::Scam: return tr::lng_scam_badge(tr::now); + case TextBadgeType::Direct: return tr::lng_direct_badge(tr::now); + } + Unexpected("Type in TextBadgeText."); +} + +QSize TextBadgeSize(TextBadgeType type) { + const auto phrase = TextBadgeText(type); const auto phraseWidth = st::dialogsScamFont->width(phrase); const auto width = st::dialogsScamPadding.left() + phraseWidth @@ -85,7 +92,7 @@ QSize ScamBadgeSize(bool fake) { return { width, height }; } -void DrawScamFakeBadge( +void DrawTextBadge( Painter &p, QRect rect, int outerWidth, @@ -107,16 +114,14 @@ void DrawScamFakeBadge( phraseWidth); } -void DrawScamBadge( - bool fake, +void DrawTextBadge( + TextBadgeType type, Painter &p, QRect rect, int outerWidth, const style::color &color) { - const auto phrase = fake - ? tr::lng_fake_badge(tr::now) - : tr::lng_scam_badge(tr::now); - DrawScamFakeBadge( + const auto phrase = TextBadgeText(type); + DrawTextBadge( p, rect, outerWidth, @@ -133,8 +138,9 @@ int PeerBadge::drawGetWidth(Painter &p, Descriptor &&descriptor) { Expects(descriptor.customEmojiRepaint != nullptr); const auto peer = descriptor.peer; - if (descriptor.scam && (peer->isScam() || peer->isFake())) { - return drawScamOrFake(p, descriptor); + if ((descriptor.scam && (peer->isScam() || peer->isFake())) + || descriptor.direct && peer->isMonoforum()) { + return drawTextBadge(p, descriptor); } const auto verifyCheck = descriptor.verified && peer->isVerified(); const auto premiumMark = descriptor.premium @@ -177,10 +183,16 @@ int PeerBadge::drawGetWidth(Painter &p, Descriptor &&descriptor) { return 0; } -int PeerBadge::drawScamOrFake(Painter &p, const Descriptor &descriptor) { - const auto phrase = descriptor.peer->isScam() - ? tr::lng_scam_badge(tr::now) - : tr::lng_fake_badge(tr::now); +int PeerBadge::drawTextBadge(Painter &p, const Descriptor &descriptor) { + const auto type = [&] { + if (descriptor.peer->isScam()) { + return TextBadgeType::Scam; + } else if (descriptor.peer->isFake()) { + return TextBadgeType::Fake; + } + return TextBadgeType::Direct; + }(); + const auto phrase = TextBadgeText(type); const auto phraseWidth = st::dialogsScamFont->width(phrase); const auto width = st::dialogsScamPadding.left() + phraseWidth @@ -197,11 +209,13 @@ int PeerBadge::drawScamOrFake(Painter &p, const Descriptor &descriptor) { rectForName.y() + (rectForName.height() - height) / 2, width, height); - DrawScamFakeBadge( + DrawTextBadge( p, rect, descriptor.outerWidth, - *descriptor.scam, + *((type == TextBadgeType::Direct) + ? descriptor.direct + : descriptor.scam), phrase, phraseWidth); return st::dialogsScamSkip + width; diff --git a/Telegram/SourceFiles/ui/unread_badge.h b/Telegram/SourceFiles/ui/unread_badge.h index 5fd7990c4c..974df00ea8 100644 --- a/Telegram/SourceFiles/ui/unread_badge.h +++ b/Telegram/SourceFiles/ui/unread_badge.h @@ -58,6 +58,7 @@ public: const style::icon *verified = nullptr; const style::icon *premium = nullptr; const style::color *scam = nullptr; + const style::color *direct = nullptr; const style::color *premiumFg = nullptr; Fn<void()> customEmojiRepaint; crl::time now = 0; @@ -84,7 +85,7 @@ private: struct EmojiStatus; struct BotVerifiedData; - int drawScamOrFake(Painter &p, const Descriptor &descriptor); + int drawTextBadge(Painter &p, const Descriptor &descriptor); int drawVerifyCheck(Painter &p, const Descriptor &descriptor); int drawPremiumEmojiStatus(Painter &p, const Descriptor &descriptor); int drawPremiumStar(Painter &p, const Descriptor &descriptor); @@ -94,9 +95,15 @@ private: }; -QSize ScamBadgeSize(bool fake); -void DrawScamBadge( - bool fake, +enum class TextBadgeType : uchar { + Scam, + Fake, + Direct, +}; + +QSize TextBadgeSize(TextBadgeType type); +void DrawTextBadge( + TextBadgeType, Painter &p, QRect rect, int outerWidth, From 2a153214f671fab5d0c2e57a4b3325650030c394 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 May 2025 18:31:47 +0400 Subject: [PATCH 082/310] Support polls with 12 options. --- Telegram/SourceFiles/boxes/create_poll_box.cpp | 10 +++++++--- Telegram/SourceFiles/data/data_poll.h | 2 +- Telegram/SourceFiles/main/main_app_config.cpp | 6 ++++++ Telegram/SourceFiles/main/main_app_config.h | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index fab9731cc8..926656d8b6 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "history/view/history_view_schedule_box.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "menu/menu_send.h" #include "ui/controls/emoji_button.h" @@ -510,7 +511,8 @@ Options::Options( } bool Options::full() const { - return (_list.size() == kMaxOptionsCount); + const auto limit = _controller->session().appConfig().pollOptionsLimit(); + return (_list.size() >= limit); } bool Options::hasOptions() const { @@ -1028,8 +1030,10 @@ object_ptr<Ui::RpWidget> CreatePollBox::setupContent() { setCloseByEscape(!count); setCloseByOutsideClick(!count); }) | rpl::map([=](int count) { - return (count < kMaxOptionsCount) - ? tr::lng_polls_create_limit(tr::now, lt_count, kMaxOptionsCount - count) + const auto appConfig = &_controller->session().appConfig(); + const auto max = appConfig->pollOptionsLimit(); + return (count < max) + ? tr::lng_polls_create_limit(tr::now, lt_count, max - count) : tr::lng_polls_create_maximum(tr::now); }) | rpl::after_next([=] { container->resizeToWidth(container->widthNoMargins()); diff --git a/Telegram/SourceFiles/data/data_poll.h b/Telegram/SourceFiles/data/data_poll.h index 49dd021734..03fadc862c 100644 --- a/Telegram/SourceFiles/data/data_poll.h +++ b/Telegram/SourceFiles/data/data_poll.h @@ -75,7 +75,7 @@ struct PollData { int totalVoters = 0; int version = 0; - static constexpr auto kMaxOptions = 10; + static constexpr auto kMaxOptions = 32; private: bool applyResultToAnswers( diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index f5ace98778..5c4a5b02cd 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -140,6 +140,12 @@ int AppConfig::giftResaleReceiveThousandths() const { return get<int>(u"stars_stargift_resale_commission_permille"_q, 800); } +int AppConfig::pollOptionsLimit() const { + return get<int>( + u"poll_answers_max"_q, + _account->mtp().isTestMode() ? 12 : 10); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index dbf26522cf..0fa0e3c495 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -82,6 +82,8 @@ public: [[nodiscard]] int giftResalePriceMin() const; [[nodiscard]] int giftResaleReceiveThousandths() const; + [[nodiscard]] int pollOptionsLimit() const; + void refresh(bool force = false); private: From 4c8ff1c7ec34b84f6150a6885476d1c6ea8818fb Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 27 May 2025 18:41:30 +0400 Subject: [PATCH 083/310] Disable polls in monoforums, enable in Saved Messages. --- .../SourceFiles/data/data_chat_participant_status.cpp | 6 ++++++ Telegram/SourceFiles/data/data_peer.cpp | 9 +++++---- Telegram/SourceFiles/data/data_peer_values.cpp | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 2a85f44d13..b3e318d076 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -156,6 +156,12 @@ bool CanSendAnyOf( } return false; } else if (const auto channel = peer->asChannel()) { + if (channel->isMonoforum()) { + rights &= ~ChatRestriction::SendPolls; + if (!rights) { + return false; + } + } using Flag = ChannelDataFlag; const auto allowed = channel->amIn() || ((channel->flags() & Flag::HasLink) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 44a805e764..1f8e0ad696 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -663,10 +663,11 @@ bool PeerData::canPinMessages() const { bool PeerData::canCreatePolls() const { if (const auto user = asUser()) { - return user->isBot() - && !user->isSupport() - && !user->isRepliesChat() - && !user->isVerifyCodes(); + return user->isSelf() + || (user->isBot() + && !user->isSupport() + && !user->isRepliesChat() + && !user->isVerifyCodes()); } return Data::CanSend(this, ChatRestriction::SendPolls); } diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 0c435d5347..1d65c3b2ec 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -274,6 +274,12 @@ inline auto DefaultRestrictionValue( | Flag::Forbidden | Flag::Creator | Flag::Broadcast; + if (channel->isMonoforum()) { + rights &= ~ChatRestriction::SendPolls; + if (!rights) { + return rpl::single(false); + } + } return rpl::combine( PeerFlagsValue(channel, mask), AdminRightValue( From fdce4bada78e1a11a119b0d8f67a2faf1982dde2 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 May 2025 14:56:49 +0400 Subject: [PATCH 084/310] Implement nice topic mode editing. --- .../animations/edit_peers/topics.tgs | Bin 0 -> 8162 bytes .../animations/edit_peers/topics_list.tgs | Bin 0 -> 2613 bytes .../animations/edit_peers/topics_tabs.tgs | Bin 0 -> 3808 bytes Telegram/Resources/langs/lang.strings | 7 + .../Resources/qrc/telegram/animations.qrc | 3 + .../boxes/peers/edit_peer_info_box.cpp | 58 +++-- .../boxes/peers/toggle_topics_box.cpp | 226 ++++++++++++++++++ .../boxes/peers/toggle_topics_box.h | 20 ++ Telegram/SourceFiles/data/data_channel.cpp | 2 +- Telegram/SourceFiles/data/data_session.cpp | 7 +- .../SourceFiles/history/history_widget.cpp | 16 +- .../view/history_view_chat_section.cpp | 16 +- Telegram/SourceFiles/info/info.style | 10 + .../info_statistics_inner_widget.cpp | 1 - Telegram/SourceFiles/mtproto/scheme/api.tl | 1 + .../SourceFiles/settings/settings_common.cpp | 11 +- .../SourceFiles/settings/settings_common.h | 3 +- .../window/window_session_controller.cpp | 21 +- Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_lottie | 2 +- 20 files changed, 373 insertions(+), 33 deletions(-) create mode 100644 Telegram/Resources/animations/edit_peers/topics.tgs create mode 100644 Telegram/Resources/animations/edit_peers/topics_list.tgs create mode 100644 Telegram/Resources/animations/edit_peers/topics_tabs.tgs create mode 100644 Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp create mode 100644 Telegram/SourceFiles/boxes/peers/toggle_topics_box.h diff --git a/Telegram/Resources/animations/edit_peers/topics.tgs b/Telegram/Resources/animations/edit_peers/topics.tgs new file mode 100644 index 0000000000000000000000000000000000000000..a5552a4acb86a3ec730cbd662b9e8b03ea0338fd GIT binary patch literal 8162 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%0AsK!3Qpn)v4svT3Zsanqq_1o*xfZh%rf^d zo%HoU`jRoDc&zYourcxniG}MPbf+pK<F~~JpC8X3g|Az$$HAHgC*E*n^<BzHj#$t; z-g(4SRWIVVdxzfcZV~f)u^Y&Q82z-0m<`Zp9G-7dx^)@lXA}&^M)CLw^hXrHVUDUX z6c7{*$JYfhXow*tHGyD9#rI21$zf(>WTMpEn?|TNEWx9fNt1e^Hprb5TxPZ(Jw|a! ze!4Dlh%(pf@X5oG=LUilsqfO36$ocqc*pg;0sa@y+U4y2bB1cTh>kmaHqfnVtYG=b zn5}O{rub5tm=wORQqaJiHfV-jha((pSoJWD5TE<>N*g4{K>MJG039|85ABevi5Vx? zp~HyuEvv6m3bXG4+ryhK-1Kop5HFZxO<i0};h}~bZ$N-pUKn30QixxZF}y(^cWhv; zZ=#hVK}|w!8NSml1%&z9ZPu5$2p25YL&zx(XkRP0Tp3_yS0M0pabgT_Y9YZWbYLeH z(STF>-oag}h`VZ|o|$B2T}GOww3m^c^(VKD7*Y5?|DW)?z!|qmS3m<m$AibK;tyk{ zgQ8|KC|Dzbb_R&bq-!zvRNjCqY+QLfeB!3$B}hbqW=$7kh+saDB+pgE@S4Y<i2Ki> z1Gi|@VI!H#G*qw30e+ClbY|Z4lwm9vA;Cx98l8zjx7Kha(0kf{K0w^RZn6JXB+m-J z%zWe=v^y{(#>UcRF+V|D?Fg9cnMf{5ODv?K6(I1rsu;*>P)Es8V*d$=qtF@{Q2?hj zEW_nAay&gZZUcaXJBvkmPp2BHZx@D2oqfTV0_|fCSK@pgtF_8}d}&_Dh`-oNNPap( z2%?5&Q-3laMASO)514#lMsaw39Aeb8yrS)*59n~Ibdnf4wp^ja_cPhkbV#h8z2*pZ zNth9-##CFDjPiHwjO0S}X1Z;K4={3aA(4Gd{=Ym4saTQlRs7r#L6~y!@p%Y`rEOHK zaZhuUwIlr7do=7VzJSc8?oY>uX*7f!c(l#=SJkg*=&V7#h-Y|&Xq$G%Bj2x5gdulv zkoSyzDSwEv@C(=hveV!QQHH;x34n}kN>X@egp7+_p}}=)QFyqio70>94>p-FTU;^g z)acRpe1&h6UXY-d(peQmnP5O~p4W1#qO2mk^x;tS@j?E1G}$29$#!CV>lZ9YT_DsF zA+#?Ok^{T4TvIj{RM)#5;ir$()2|m0^k=7Q#+u4gb2jov7W<S}K(|KW>w+QVm+?#F zylRiHVk2orM`NNP$cas^O?f$3)?TtJtl5*8w$k8dc?RrKv#Je*ZfuPn^Yy~)P!=FA zFP2l#)LGFb_nKuhSsd%JWIf5twE5Kx;BFFh7<G~@)v=++cgRyd4_J_iP<~=Y6sMwL z4v~}&W~#Zt3)i6wBjuhrx0<Eh4+{DNWI|;`JmCZ%6%c?Tm{9DrYt=04?Xic>a8X`O z7=WVxXk!U^;~D@`dF-?ZK5%ZYX~YT#=dr(cQo?t&nK|=0M*ZjT__y$MZnf}se_A#u zR7w=MX0SWOx~mNi5GK?H%f>JbY>&V*0JH41cI|yCH_;w6WOY#kD?&bh-=<54<KVWY zxMzGxco$@5Ax}-|o5V~>5t0!Bz`=tQ4j5wWsw6O<GIS%GhDQ({32v%Dabl;oL6IQL zmZH{@7(p(*LhZcSHf4gAPzi=X5U@U34WJKYl!tP#Bhv~AE=*s6{ILbDtMOQRE~WG< z+w>`z@X{I8UPtmPSOI4rq!IE_dbJsVFoSSWI!cj}B0R<pkCd0ps3-~Wq=%mrtR-;c zfj?~Oa)0QuH=sl>+U@j2i1De2>xxN)vv9by<*Ee>3E>D$NK6Vj$dCbILlF<YxAqT> zaWA`~Cuh)Vsw|T*^7@rz&htmwR0CMsvxdgZi7usgH-g(q;5n&KiYP-40BO5{CmK*_ zgY8IJVlOU(YI10J(vWb%aRvVcUQqe_KcjW>n<b2Hze^4wKXeaYjc`#d7m;POsmmo{ z4xVm({gLIjCR@Mi++)F!jYsPb!Lflyn9OxtJx)IUpv3x4?lt}YmjZOz0`zDa+q}n2 zl7%NE;iNeyI!%@6NNe=>X3ab~miMo7(?}BD5C8g`*|N(o&&Z1rC1?WDd*p`@9k9j7 zc}DLgG40?3Eji(?c__k5XC8=tQjI@jhOZf=K!*>*ih--8^+A12EEj^?VQzF!c}|^! z7O$uYO;=%|F`Hc{B^ujqU)|dRhjt#=+x}RlMj9xFBph^jA*%_wG<Ea?85M<%bXAYb ze@RBHr8pg$WJI*M7cU<KSAzsW0*v!oV>7k*h-RGIBZ^;Lka~BDDIOOctOl@bB$UXp zj@4J0;V;;Gmzf>*-VO8Fzhd;+!J{@;lIl!}mW>f*TsxEa{Ox`>b5H1J89$E1zRbr( zAfXiRjt6|Rk&C+&ccuFhfp~EH5TN@odhmb&_<UJ9#hCtd@<X@bj(zuL)DM*4SFe>= zm`eXgx30&1F_^X8N><cU&+#1wpqxT?g<%No_gzN1QG~@RTzh1ZqQ6sX*BnYQ-Z=sf zMHUgB0RnzwY%@VUeMdVxMHpT+x8}Y5#+i{3G1>Wik?QyUG`v(j<u_jQM?awp?!wdf zx}feh6CNrWpN0e4>$VRcg?s3Q_P3tIzAd&lxL#3H4{-Z#7h&{oxUn6IXFJMthzsZE z%TD=L*T>t2>W5<DxBBMZv|uWH9P;79kRuvb4p2PrbSh98ACQ{XS05X?i<@k|B5l*c zQpM$}DR={xC#S9l|NTD4E9!myQC__Db~CkSONVGKpOj*<Tev5>oY>k*P*o^A*HwRZ zR&a8}MLP@%{MlTsU9#OXm!8jT=c1>Midd3X!!}O<GN5ZD2zSaEyDU+Oy=J(h_!xW^ zV@%y$;M%V88qdxyGe(pN9kKYqTk8-aF^2PFW9SIlIycVmH(g!|n0^RBP#OcRcD`NU znrEqN^uF!WN%#fF7{IQTL=%^>``|*3Ogw2^y4$Z_{9Nz!<0K}aw7uolsbS^Gk`se} z^(+n5ahD$L1f^HU1nVMUwlfCE#8*k+iVz<OZh@3_UF)q|u!(-2950BHzwf`IJ!IX6 zcezt+8BEP)AeK@x!SsT)d6H&p8{kvFa>J5)d}+=Fl8ZpNs;q<{BrNBx=q+B*3%7B3 zo`hxYoWRZsOtTOGlawyy7|N_c(AG?=G?18OX(l6=;5irS`d9J&ABT*~Es*cS_RgBo z`<5z(z2qU);EorT$@TH&#dmLjV9>QPyfyVf#5iwJHS<hxjrZy67>siC=wlgHEJh)j z5v)ZyIKey$Q5`86QDK#nL36XGDU}8h&7x>K6&RVRyL0VBg?_|NZZ|Zv^9u>JXwO2& z<wkE2&%Tyx^61z1+kIzriya5rD16c?pZSuYRfI3HqP6y6j};OITpq6BpKJ_IrF*wM zKo|h7mJnINE4;Bws9=R<zaEd~Xm)K75^`CmwG2a=|D>!o;graY6|!Vct2%sf7As$0 z;{5`wl&!~xF#Kp!0|fsxGjP9T-du>8){hD~CV}r74)-CTBWDjY8>861&l$ILjc~Qr za9FmMtZwPD%k!PcmY%6~H+lV9)?C}cLfe6OU684nI%(|yIk9n#+WFB+{IhO-+(@ML zEy*+vZR;M>#aLcmg?}Nq^S3+soATSo(R~41w@=!Jk8}3q<KzbKGnBBnPR`Qf%;b`Q zXI&24>$CmjbgEWv1&kRUpj=EBUH}`BT*o%|rKV!lGR9;cZAQU?d7rDle!UJSE+j)i zLtH9p4k)+0GWvK`UQhmUt?hT)zi`a|d<3MSCkL)>m8V744UZ#c8H(`I?xzr*uQ<1f z?xv&&Z<G{?PHDY6CbPP1iGl4r5gkA8P0!Rf@1qO&F;_1g1$If3f24OJ&2r{M7ht1Y znU@z}tAFiqQ?=euvkJX=PCj+<<(T}^GBTQes?e^(Q70pC-$3yoCrj5P|IMm%B0h4W z&7;4qfo^O=J^oysN3L(mYWj+xi~RSr=hu0#5=L9x2*#BZ<M*<B)RCB{KxRZv17Lmp z_%D+L)cpzd+;n@Tv2O=8^x{w5#>G|^i?-#b(r9k-9RY`R`QNa##c$GpZ-dU+Cw=Xt z?=3z|o{Irmsh{4bdvK6PxJq|u6Aby)M94}5DF=gO1?Uhiqw_rg^QoHb{dG7;^!4Gf zddnQ{LrDndp0;dK96>^bK*p@6ge0BWMGi?D12hw-kXFx~FrL57!JbScznZ^;h+7W@ zUHa!mP&Y?S|68V9ydHr?sdVWN^rrTm<s^c8ENoX`FxoLu;kWarxF2?|9J-hxdg_pn zDoeLzSg5}<j%;u~|GxH76>=3u#Vqu+h>-F?bD|#75i@)ztKb%5k(FLcu@yqFoK`$$ z|3(^i2qffO4y$UxINF6A4C%t{wlb0E=0p!YBlzHwQ}5D<ubJ<C4D+>a;_NNEDaX89 z)tz%kRj`X-#~WQwp{8!o{q^X$-n)=S`NH{Z3n2dm$njm>6X4P~opHfOBo=wQ$o#vm zv3x3Cxz|Wmtfj7ArlPM^%u+>g%yb)g<rCjjsI8=5kFHTUr(M1D-$=7&othLc8_r|L zU2V+ZQbM#w)^Gl0SBx2pAQnW9R599$dd8uB{X|dw%`)GCaaR%dA(Vl+GU}QD2JJuF zMFp!(hJ#6DpwyN{{9dE`-_bg*lhlxt^6fo|Si*!9*)A}#^~b`uVca_B=s!%%{2?9^ z9GwnhzaFZtb4-J<UJhW9jYPaZH*WF$KF|NDI#X<S<iGl$(yohlb?Mx9x+nPX)&@US z#(fR@Ln}qbN0)?&!38Us7rYFGZ=nj<Hk@PVlpMVL6>&7sYDR$1fz$n!xV&Dr%0AEQ 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 RE&BXq<PMDBGOUG$`ad#ixBmbD literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/edit_peers/topics_list.tgs b/Telegram/Resources/animations/edit_peers/topics_list.tgs new file mode 100644 index 0000000000000000000000000000000000000000..d85bf7ff85b8c8d13dab2064f5bca21fba58501d GIT binary patch literal 2613 zcmV-53d;2#iwFP!000021MOQ&ZyPxh{wqPB*#zGY-{vydIm{ryF7^@#3ym$uMr_HD zG?NVs|M#sblHFvhB`cG7kz|Y@YF2fzSo~O3e6=6D{rg>aAzAllchOmHx$0PVz3ncX zVBPIQcVXeVfv3d6N1&x*-A#V3@1fRy^Kf^&+b`Ew+s*A2RKL2q>MlTfx!bMwp#9xr z2mFifa&!Oi9{~b)%TKFqdQoI=*H>i5{!@2hvf|&C_p9BTpVn7ze!1MNcTn+Rxm~UI z-9=#CZcFkXK+R+KfImx8^a0fN(s*W|FV-`$czOa;HuQ3LvwT>khOwdU{T7to?vsh@ zWC^|4-E>9wpcX_x@SWg3NJhU4mZ^Rqp*>^@Ymh07Pcl|P4QEpgDI=rbszRfspVgZ5 zky>l05-21gs|LM&tGJ)ne5e@gE#@YTSSK0WpVHjoZ~z0?Tg*-RSTPsXAt+L1`33kH z;(N67HEey0X+K5${LW8?V`^jFgU{%A3s{S1i+1e*C!K0K4E@3Qx88B+SPP1BK-e=a zd9*axkp~qT8VXD+E`mc^LVFQuO(WGy#X)05YiL*36*QM#RT`z65C*KVL|7iYL`IY* zGn@el%cTi5!*lO^T_qG{0X!&X!ib@nWJJS2Gl2o!9s2>a6Kd?|NGBXd`GnJ`&^P7L z43C9upXb`z%J@tVz00yg_kF<zM(VP=*l+(>Ve+D6_;$I!c_Se!-9VbUUf%6iPwcTt zn<tYN_?DG{Ji-d{K3n-~{slj=#qlkMS0hmZG|44pl}KABD{Ayp%YACP_@eaOqI(!Z zQaXSioJnxwRH7|WHs#TZ=tW(T2sLVHDbk6wI*P2CMOK|N9T<an%9!lXfr3!_6vz(4 zPjcx(n{H9Yq=11shHz0Smr1UDi>r<nT2R54oTPy?Do+BDf&O3)(`aK#KQXlEuB%Tp z*UcvR(X`g)wnwAxvGYUjfjA3MGtY(7IS!@p6|K4Tnnf;TLc@_Azz#VjKoo&q;12>G zG6Jw=igd%ZB04)t;LgX2c3i1A%0q^Mz#r71x75u=fWv2VjyaE>bsqh_hox^L$kjpV zs=#w~Nc!qUr~@*z+QP|jG_#1BbHdR&LtY!mUtMUr>I_Z9S|t-ficoVYdU}*^0#8u{ zL9N_9mz+K%Pi(a(K~}UUIW}r<lqcOubrrI$eM#-mOy72{uXT)LTAFEFZ%iw}T+DC# zHR<-?g;{amz&5TaxYNMJXq08)fV9V?k^@~ewB_2VrUrNhgg(A8A}tWW4y>1JAO%`8 zm8E?)tLTG#;m7ve&>WdFklEZK>{6eLE>}0?Y8$d(Hu0GI+FAFt-+Mwp%q`)NgBXX? z(prZ?oJe{v8mbpf<wV(0)r3r%kr9p}>}C;m=OC<M>54Hy&w^-S_Jv!ac<_$RXc+>* zVs~`uPA?_WMh>)}d#@tV$9n5$d+Wb@e}W}+JL-s2<xc}_q6JYjm5I`^@u!+3&BQqF zPhpBMA$xxhZ1}+?Hp%|F+P+`C!5X{6#W+>DzU!!o`aA+~fPZrFL`YU|q!7SMCmFxr z6PKF#`S$Mai2gkuSv(x*N8QU=ZtEBCoDdBDFH*bJ8*#+C-G>9Q-SU7%X8&uuT<@+o z+xzZmar6u)_jByoA{{j4b0%f*oMulniDJ*2QzF%#68*Z`?UwJMk+<8`pSP=zCyt5Q z?rF4pf~0oL=@&RN`c}h`cCI+p!txh4Nf4DZ^U{`^reH0`qAb$HjXX=nS_7<?<KJlm zi=b!%0Jf$wZ8Q^xh>;rMQX`0Z<QiT?O{2t|gz>NMR=-_Gl%{ec+y!Pwlp|V0Q5I?9 z+U$s>kTlqn2>y6mHUK5oE)+&L9D_Ct0LBqNQG9SHB?QGu1poRL?c4RizD{L*1^`{F z&(I)VV_B98#LkIyC?q9m82&yR1q4MlxvbBjNG{grAnlv-SSRsILamBfl|BjQ7wihY zU7yU;MfRsWGmxX{3bUashZ>2mid&Xa)}u}$`Qz<42$;}#Q(mkjPt#Q>59P5=;`_ld zhhkRcPr~`Sy?B}J#mwUWi}x40-Cw-D*{oMTZ($$PoxXLJ)-fgLVX=Z)oL|5C&O&h? z@nBEUEtTTY<Ds7zBMt-{oz^Mk=+G}xWJD3GgFvq_&u|gzD``;4deWal77|Z=W7!6z zk_-|`IUw{9ev<1j1|}#nu*T!chMK2?r7H=FFvwt1A1Vivm7|M{e3gfaS!$)00@aL~ zX|7Boo~~d!r4!J`Y$pgIMsN`Y2r)!!5FmUQfi=BrQC#?BV%0d|RXXY<p42K?S&Ubd z7LRMI1P9#!=s;b*F>g#dG`Z>zSAx24jj69|@P-&Lk+NoLO~<>+l7HCjAJZ%i&P3Ni z<V>WPQ7e@I=w;MMeZ;P?Q_x1TYV?aE_M7t20f562Qq9@73@kxa`wgadglf$07KQ-& z>LT|fsvsd-ON>Z2UcG|0Snm_|Q*k_8@&U%s1puAojiNhb<cM^iYd~5cM{uI8z&AvL zByl9_W4+a=pEw{7noKUI=1mw2!&~FQf(UhCzzrRU8wcJDHrs#~$C;xbMvxOsswL=B zxpxL-Ge<;C94F|w3~!YU_RV4oBNh%@sf&PF5{nV(gL(&z;r%#X1Oh{mA0C)&^7$Ju zz$#=+yIEmSa4A4l;2j`qF*+b^*@y#|80~04O(Z=IS|aQ5Jo|{^b*la<Q*|?sJ~v5+ z@XRD_+DZB!tL4>d+nqe`lQZXiIA`MrKmX4ud&qs@m|D#yMtW0EFokg>i`JTkiwKQ5 zQs*oq%TuQ?8=e&AB=~s&@#@`;cMlHu#hz_j&;IA?Zgsgo<4{|sev^|ucK=0SxWn&` zx=-ln!=R)$G(XcDf@q&U^xS3o5g9HsBybi_GlS7fx<G!R@Hn<5Jo1)&9;cG@Sz1$~ zLVCVB`4>3(;ET^Vd3<h*<>s$&&c3>tOXb$%r-6=n^-?~md2p^xyu?b2`Su;GPQ7h} zxls6>smL2ISk(mG-ZCdu!|q^I*uIkZiDIKU22&c9I(>+x;xsfAGnDKt)3~0JK0ig` z15F=1!Fzfd1E$KMY^P<<9qo-^RYh_d`uif+K0g5PeL=n_2xWfamn8{%Jif~kRLI~D zgq<O{7H-5JkEztlTo}!vd;r^2U^)u@XAZ+*gOg#J4n{wY#)-k`aWqug6d~BuRDMDv zq*qX&Cw_>UB~u{}%RtyEp<<4TJ`ansr@}(NZZ%$Hp87Y;Qk(79I13CZLUfq`FSo1J zdiES}mj;;O`s&zv?CiDJu=KKyR%7QZ=_V?COe$byFL{LhZmZ{PQ=}3zitq)5#b_i- ze`*FuN%wT-uIl8FEO5}C`HLaZ{wdP$o2ySSNZZx*FU{W_ojSc$czR1I>(HsIlikti XkKh*hZ=sBOz7zigZ~PuXYBc}=ISB++ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/edit_peers/topics_tabs.tgs b/Telegram/Resources/animations/edit_peers/topics_tabs.tgs new file mode 100644 index 0000000000000000000000000000000000000000..3a240b4c62e16edd0c96f7964c241498ae32e8b0 GIT binary patch literal 3808 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 Wec#Y(`G8Aue*ZrVH{(b?Q2+pB&sf3$ literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 39db6ae517..2cb115b74b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3216,6 +3216,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_feature_transcribe" = "Voice-to-Text Conversion"; "lng_feature_autotranslate" = "Autotranslation of Messages"; +"lng_edit_topics_enable" = "Enable Topics"; +"lng_edit_topics_about" = "The group chat will be divided into topics created by admins or users."; +"lng_edit_topics_layout" = "Topics layout"; +"lng_edit_topics_layout_about" = "Choose how topics appear for all members."; +"lng_edit_topics_tabs" = "Tabs"; +"lng_edit_topics_list" = "List"; + "lng_giveaway_new_title" = "Boosts via Gifts"; "lng_giveaway_new_about" = "Get more boosts for your channel by gifting Premium to your subscribers."; "lng_giveaway_new_about_group" = "Get more boosts for your group by gifting Premium to your members."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index ae94041686..09702ee5d0 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -33,6 +33,9 @@ <file alias="hello_status.tgs">../../animations/hello_status.tgs</file> <file alias="starref_link.tgs">../../animations/starref_link.tgs</file> <file alias="media_forbidden.tgs">../../animations/media_forbidden.tgs</file> + <file alias="topics.tgs">../../animations/edit_peers/topics.tgs</file> + <file alias="topics_tabs.tgs">../../animations/edit_peers/topics_tabs.tgs</file> + <file alias="topics_list.tgs">../../animations/edit_peers/topics_list.tgs</file> <file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file> <file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file> diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 7cfffe5c15..c53412162a 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_requests_box.h" #include "boxes/peers/edit_peer_reactions.h" #include "boxes/peers/replace_boost_box.h" +#include "boxes/peers/toggle_topics_box.h" #include "boxes/peers/verify_peers_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/edit_privacy_box.h" // EditDirectMessagesPriceBox @@ -377,6 +378,7 @@ private: std::optional<QString> description; std::optional<bool> hiddenPreHistory; std::optional<bool> forum; + std::optional<bool> forumTabs; std::optional<bool> autotranslate; std::optional<bool> signatures; std::optional<bool> signatureProfiles; @@ -481,6 +483,7 @@ private: std::optional<HistoryVisibility> _historyVisibilitySavedValue; std::optional<EditPeerTypeData> _typeDataSavedValue; std::optional<bool> _forumSavedValue; + std::optional<bool> _forumTabsSavedValue; std::optional<bool> _autotranslateSavedValue; std::optional<bool> _signaturesSavedValue; std::optional<bool> _signatureProfilesSavedValue; @@ -1104,21 +1107,30 @@ void Controller::fillDirectMessagesButton() { void Controller::fillForumButton() { Expects(_controls.buttonsLayout != nullptr); + _forumSavedValue = _peer->isForum(); + _forumTabsSavedValue = !_peer->isChannel() + || !_peer->isForum() + || _peer->asChannel()->useSubsectionTabs(); + + const auto changes = std::make_shared<rpl::event_stream<>>(); + const auto label = [=] { + return !*_forumSavedValue + ? tr::lng_manage_monoforum_off(tr::now) + : *_forumTabsSavedValue + ? tr::lng_edit_topics_tabs(tr::now) + : tr::lng_edit_topics_list(tr::now); + }; const auto button = _controls.forumToggle = _controls.buttonsLayout->add( EditPeerInfoBox::CreateButton( _controls.buttonsLayout, tr::lng_forum_topics_switch(), - rpl::single(QString()), + changes->events_starting_with({}) | rpl::map(label), [] {}, st::manageGroupTopicsButton, { &st::menuIconTopics })); - const auto unlocks = std::make_shared<rpl::event_stream<bool>>(); - button->toggleOn( - rpl::single(_peer->isForum()) | rpl::then(unlocks->events()) - )->toggledValue( - ) | rpl::start_with_next([=](bool toggled) { - if (_controls.forumToggleLocked && toggled) { - unlocks->fire(false); + + button->setClickedCallback(crl::guard(this, [=] { + if (!*_forumSavedValue && _controls.forumToggleLocked) { if (_discussionLinkSavedValue && *_discussionLinkSavedValue) { ShowForumForDiscussionError(_navigation); } else { @@ -1130,13 +1142,21 @@ void Controller::fillForumButton() { Ui::Text::RichLangValue)); } } else { - _forumSavedValue = toggled; - if (toggled) { - _savingData.hiddenPreHistory = false; - } - refreshHistoryVisibility(); + _navigation->uiShow()->show(Box( + Ui::ToggleTopicsBox, + *_forumSavedValue, + *_forumTabsSavedValue, + crl::guard(this, [=](bool topics, bool topicsTabs) { + _forumSavedValue = topics; + _forumTabsSavedValue = !topics || topicsTabs; + if (topics) { + _savingData.hiddenPreHistory = false; + } + changes->fire({}); + refreshHistoryVisibility(); + }))); } - }, _controls.buttonsLayout->lifetime()); + })); refreshForumToggleLocked(); } @@ -2143,6 +2163,7 @@ bool Controller::validateForum(Saving &to) const { return true; } to.forum = _forumSavedValue; + to.forumTabs = _forumTabsSavedValue; return true; } @@ -2589,8 +2610,13 @@ void Controller::togglePreHistoryHidden( void Controller::saveForum() { const auto channel = _peer->asChannel(); + const auto nowForum = _peer->isForum(); + const auto nowForumTabs = !channel + || !nowForum + || channel->useSubsectionTabs(); if (!_savingData.forum - || *_savingData.forum == _peer->isForum()) { + || (*_savingData.forum == nowForum + && *_savingData.forumTabs == nowForumTabs)) { return continueSave(); } else if (!channel) { const auto saveForChannel = [=](not_null<ChannelData*> channel) { @@ -2608,7 +2634,7 @@ void Controller::saveForum() { _api.request(MTPchannels_ToggleForum( channel->inputChannel, MTP_bool(*_savingData.forum), - MTP_bool(channel->flags() & ChannelDataFlag::ForumTabs) + MTP_bool(*_savingData.forum && *_savingData.forumTabs) )).done([=](const MTPUpdates &result) { const auto weak = base::make_weak(this); channel->session().api().applyUpdates(result); diff --git a/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp new file mode 100644 index 0000000000..b2fed66b25 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp @@ -0,0 +1,226 @@ +/* +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/peers/toggle_topics_box.h" + +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "settings/settings_common.h" +#include "ui/effects/ripple_animation.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" +#include "ui/vertical_list.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace Ui { +namespace { + +enum class LayoutType { + Tabs, + List +}; + +class LayoutButton final : public Ui::RippleButton { +public: + LayoutButton( + QWidget *parent, + LayoutType type, + std::shared_ptr<Ui::RadioenumGroup<LayoutType>> group); + +private: + void paintEvent(QPaintEvent *e) override; + + QImage prepareRippleMask() const override; + + Ui::FlatLabel _text; + Ui::Animations::Simple _activeAnimation; + bool _active = false; + +}; + +LayoutButton::LayoutButton( + QWidget *parent, + LayoutType type, + std::shared_ptr<Ui::RadioenumGroup<LayoutType>> group) +: RippleButton(parent, st::defaultRippleAnimationBgOver) +, _text(this, st::topicsLayoutButtonLabel) +, _active(group->current() == type) { + _text.setText(type == LayoutType::Tabs + ? tr::lng_edit_topics_tabs(tr::now) + : tr::lng_edit_topics_list(tr::now)); + const auto iconColorOverride = [=] { + return anim::color( + st::windowSubTextFg, + st::windowActiveTextFg, + _activeAnimation.value(_active ? 1. : 0.)); + }; + const auto iconSize = st::topicsLayoutButtonIconSize; + auto [iconWidget, iconAnimate] = Settings::CreateLottieIcon( + this, + { + .name = (type == LayoutType::Tabs + ? u"topics_tabs"_q + : u"topics_list"_q), + .color = &st::windowSubTextFg, + .sizeOverride = { iconSize, iconSize }, + .colorizeUsingAlpha = true, + }, + st::topicsLayoutButtonIconPadding, + iconColorOverride); + const auto icon = iconWidget.release(); + setClickedCallback([=] { + group->setValue(type); + }); + group->value() | rpl::start_with_next([=](LayoutType value) { + const auto active = (value == type); + _text.setTextColorOverride(active + ? st::windowFgActive->c + : std::optional<QColor>()); + + if (_active == active) { + return; + } + _active = active; + _text.update(); + if (_active) { + iconAnimate(anim::repeat::once); + } + _activeAnimation.start([=] { + icon->update(); + }, _active ? 0. : 1., _active ? 0. : 1., st::fadeWrapDuration); + }, lifetime()); + + _text.paintRequest() | rpl::start_with_next([=](QRect clip) { + if (_active) { + auto p = QPainter(&_text); + auto hq = PainterHighQualityEnabler(p); + const auto radius = _text.height() / 2.; + p.setPen(Qt::NoPen); + p.setBrush(st::windowBgActive); + p.drawRoundedRect(_text.rect(), radius, radius); + } + }, _text.lifetime()); + + const auto padding = st::topicsLayoutButtonPadding; + const auto skip = st::topicsLayoutButtonSkip; + const auto text = _text.height(); + + resize( + padding.left() + icon->width() + padding.right(), + padding.top() + icon->height() + skip + text + padding.bottom()); + icon->move(padding.left(), padding.top()); + _text.move( + (width() - _text.width()) / 2, + padding.top() + icon->height() + skip); +} + +void LayoutButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto rippleBg = anim::color( + st::windowBgOver, + st::lightButtonBgOver, + _activeAnimation.value(_active ? 1. : 0.)); + paintRipple(p, QPoint(), &rippleBg); +} + +QImage LayoutButton::prepareRippleMask() const { + return Ui::RippleAnimation::RoundRectMask(size(), st::boxRadius); +} + +} // namespace + +void ToggleTopicsBox( + not_null<Ui::GenericBox*> box, + bool enabled, + bool tabs, + Fn<void(bool enabled, bool tabs)> callback) { + box->setTitle(tr::lng_forum_topics_switch()); + box->setWidth(st::boxWideWidth); + + const auto container = box->verticalLayout(); + + Settings::AddDividerTextWithLottie(container, { + .lottie = u"topics"_q, + .lottieSize = st::settingsFilterIconSize, + .lottieMargins = st::settingsFilterIconPadding, + .showFinished = box->showFinishes(), + .about = tr::lng_edit_topics_about( + Ui::Text::RichLangValue + ), + .aboutMargins = st::settingsFilterDividerLabelPadding, + }); + + Ui::AddSkip(container); + + const auto toggle = container->add( + object_ptr<Ui::SettingsButton>( + container, + tr::lng_edit_topics_enable(), + st::settingsButtonNoIcon)); + toggle->toggleOn(rpl::single(enabled)); + + const auto group = std::make_shared<Ui::RadioenumGroup<LayoutType>>(tabs + ? LayoutType::Tabs + : LayoutType::List); + + const auto layoutWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto layout = layoutWrap->entity(); + + Ui::AddSubsectionTitle(layout, tr::lng_edit_topics_layout()); + const auto buttons = layout->add( + object_ptr<Ui::RpWidget>(layout), + QMargins(0, 0, 0, st::defaultVerticalListSkip * 2)); + + const auto tabsButton = Ui::CreateChild<LayoutButton>( + buttons, + LayoutType::Tabs, + group); + const auto listButton = Ui::CreateChild<LayoutButton>( + buttons, + LayoutType::List, + group); + + buttons->resize(container->width(), tabsButton->height()); + buttons->widthValue() | rpl::start_with_next([=](int outer) { + const auto skip = st::boxRowPadding.left() - st::boxRadius; + tabsButton->moveToLeft(skip, 0, outer); + listButton->moveToRight(skip, 0, outer); + }, buttons->lifetime()); + + Ui::AddDividerText( + layout, + tr::lng_edit_topics_layout_about(Ui::Text::RichLangValue)); + + layoutWrap->toggle(enabled, anim::type::instant); + toggle->toggledChanges( + ) | rpl::start_with_next([=](bool checked) { + layoutWrap->toggle(checked, anim::type::normal); + }, layoutWrap->lifetime()); + + box->addButton(tr::lng_settings_save(), [=] { + const auto enabledValue = toggle->toggled(); + const auto tabsValue = (group->current() == LayoutType::Tabs); + callback(enabledValue, tabsValue); + box->closeBox(); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/peers/toggle_topics_box.h b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.h new file mode 100644 index 0000000000..0bd4ad3685 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.h @@ -0,0 +1,20 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/layers/generic_box.h" + +namespace Ui { + +void ToggleTopicsBox( + not_null<Ui::GenericBox*> box, + bool enabled, + bool tabs, + Fn<void(bool enabled, bool tabs)> callback); + +} // namespace Ui diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 6684631180..43182a6ad6 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -410,7 +410,7 @@ void ChannelData::setPendingRequestsCount( bool ChannelData::useSubsectionTabs() const { return isForum() - && ((flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug(); + && (flags() & ChannelDataFlag::ForumTabs); } ChatRestrictionsInfo ChannelData::KickedRestrictedRights( diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index ec41e8ee17..f9a3127f56 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -979,12 +979,13 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) { | Flag::CallNotEmpty | Flag::Forbidden | (!minimal - ? (Flag::Left | Flag::Creator | Flag::ForumTabs) + ? (Flag::Left | Flag::Creator) : Flag()) | Flag::NoForwards | Flag::JoinToWrite | Flag::RequestToJoin | Flag::Forum + | Flag::ForumTabs | ((!minimal && !data.is_stories_hidden_min()) ? Flag::StoriesHidden : Flag()) @@ -1016,8 +1017,7 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) { : Flag()) | (!minimal ? ((data.is_left() ? Flag::Left : Flag()) - | (data.is_creator() ? Flag::Creator : Flag()) - | (data.is_forum_tabs() ? Flag::ForumTabs : Flag())) + | (data.is_creator() ? Flag::Creator : Flag())) : Flag()) | (data.is_noforwards() ? Flag::NoForwards : Flag()) | (data.is_join_to_send() ? Flag::JoinToWrite : Flag()) @@ -1025,6 +1025,7 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) { | ((data.is_forum() && data.is_megagroup()) ? Flag::Forum : Flag()) + | (data.is_forum_tabs() ? Flag::ForumTabs : Flag()) | ((!minimal && !data.is_stories_hidden_min() && data.is_stories_hidden()) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 8a88024743..0630c14547 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2623,6 +2623,7 @@ void HistoryWidget::showHistory( channel->flagsValue( ) | rpl::start_with_next([=] { refreshJoinChannelText(); + validateSubsectionTabs(); }, _list->lifetime()); } else { refreshJoinChannelText(); @@ -8245,8 +8246,18 @@ void HistoryWidget::showPremiumToast(not_null<DocumentData*> document) { void HistoryWidget::validateSubsectionTabs() { if (!_history || !HistoryView::SubsectionTabs::UsedFor(_history)) { - _subsectionTabsLifetime.destroy(); - _subsectionTabs = nullptr; + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + updateControlsGeometry(); + if (const auto forum = _history->asForum()) { + controller()->showForum(forum, { + Window::SectionShow::Way::Backward, + anim::type::normal, + anim::activation::background, + }); + } + } return; } else if (_subsectionTabs) { return; @@ -8259,6 +8270,7 @@ void HistoryWidget::validateSubsectionTabs() { _history); } _subsectionTabs->removeRequests() | rpl::start_with_next([=] { + _subsectionTabsLifetime.destroy(); _subsectionTabs = nullptr; updateControlsGeometry(); }, _subsectionTabsLifetime); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 6829d8a432..70e7d71042 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -928,6 +928,7 @@ void ChatWidget::setupComposeControls() { channel->flagsValue() ) | rpl::start_with_next([=] { refreshJoinGroupButton(); + validateSubsectionTabs(); }, lifetime()); } else { refreshJoinGroupButton(); @@ -1522,8 +1523,18 @@ void ChatWidget::edit( void ChatWidget::validateSubsectionTabs() { if (!HistoryView::SubsectionTabs::UsedFor(_history)) { - _subsectionTabsLifetime.destroy(); - _subsectionTabs = nullptr; + if (_subsectionTabs) { + _subsectionTabsLifetime.destroy(); + _subsectionTabs = nullptr; + updateControlsGeometry(); + if (const auto forum = _history->asForum()) { + controller()->showForum(forum, { + Window::SectionShow::Way::Backward, + anim::type::normal, + anim::activation::background, + }); + } + } return; } else if (_subsectionTabs) { return; @@ -1537,6 +1548,7 @@ void ChatWidget::validateSubsectionTabs() { thread); } _subsectionTabs->removeRequests() | rpl::start_with_next([=] { + _subsectionTabsLifetime.destroy(); _subsectionTabs = nullptr; updateControlsGeometry(); }, _subsectionTabsLifetime); diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 3097de7a26..a9446d9c08 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1177,3 +1177,13 @@ infoGiftTooltip: ImportantTooltip(defaultImportantTooltip) { padding: margins(8px, 2px, 8px, 3px); } infoGiftTooltipFont: font(11px semibold); + +topicsLayoutButtonLabel: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + margin: margins(10px, 4px, 10px, 4px); + textFg: windowSubTextFg; +} +topicsLayoutButtonIconPadding: margins(4px, 0px, 4px, 0px); +topicsLayoutButtonIconSize: 140px; +topicsLayoutButtonPadding: margins(4px, 0px, 4px, 12px); +topicsLayoutButtonSkip: 0px; diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp index 5412b5886b..f02f2f07c6 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_inner_widget.cpp @@ -957,4 +957,3 @@ void InnerWidget::showFinished() { } } // namespace Info::Statistics - diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 1955440642..fc937d352b 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2506,6 +2506,7 @@ channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; +channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index 465a70735c..68bceccf38 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -242,7 +242,8 @@ void AddDividerTextWithLottie( LottieIcon CreateLottieIcon( not_null<QWidget*> parent, Lottie::IconDescriptor &&descriptor, - style::margins padding) { + style::margins padding, + Fn<QColor()> colorOverride) { Expects(!descriptor.frame); // I'm not sure it considers limitFps. descriptor.limitFps = true; @@ -262,7 +263,9 @@ LottieIcon CreateLottieIcon( const auto looped = raw->lifetime().make_state<bool>(true); const auto start = [=] { - icon->animate([=] { raw->update(); }, 0, icon->framesCount() - 1); + icon->animate([=] { + raw->update(); + }, 0, icon->framesCount() - 1); }; const auto animate = [=](anim::repeat repeat) { *looped = (repeat == anim::repeat::loop); @@ -272,7 +275,9 @@ LottieIcon CreateLottieIcon( ) | rpl::start_with_next([=] { auto p = QPainter(raw); const auto left = (raw->width() - width) / 2; - icon->paint(p, left, padding.top()); + icon->paint(p, left, padding.top(), colorOverride + ? colorOverride() + : std::optional<QColor>()); if (!icon->animating() && icon->frameIndex() > 0 && *looped) { start(); } diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 2c2c31b64b..6a9e214951 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -204,7 +204,8 @@ struct LottieIcon { [[nodiscard]] LottieIcon CreateLottieIcon( not_null<QWidget*> parent, Lottie::IconDescriptor &&descriptor, - style::margins padding = {}); + style::margins padding = {}, + Fn<QColor()> colorOverride = nullptr); struct SliderWithLabel { object_ptr<Ui::RpWidget> widget; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index d0ba08a2c7..5e123fd458 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_file_origin.h" +#include "data/data_flags.h" #include "data/data_folder.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -1896,10 +1897,11 @@ void SessionController::showForum( if (_shownForum.current() != forum) { return; } - forum->destroyed( - ) | rpl::start_with_next([=, history = forum->history()] { + const auto history = forum->history(); + const auto closeAndShowHistory = [=](bool showOnlyIfEmpty) { const auto now = activeChatCurrent().owningHistory(); - const auto showHistory = !now || (now == history); + const auto showHistory = !now + || (!showOnlyIfEmpty && (now == history)); const auto weak = base::make_weak(this); closeForum(); if (weak && showHistory) { @@ -1909,6 +1911,19 @@ void SessionController::showForum( anim::activation::background, }); } + }; + forum->destroyed( + ) | rpl::start_with_next([=] { + closeAndShowHistory(false); + }, _shownForumLifetime); + using FlagChange = Data::Flags<ChannelDataFlags>::Change; + forum->channel()->flagsValue( + ) | rpl::start_with_next([=](FlagChange change) { + if (change.diff & ChannelDataFlag::ForumTabs) { + if (HistoryView::SubsectionTabs::UsedFor(history)) { + closeAndShowHistory(true); + } + } }, _shownForumLifetime); content()->showForum(forum, params); closeMonoforum(); diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index f39e17f834..7bdd372fd6 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -58,6 +58,8 @@ PRIVATE boxes/peers/edit_peer_history_visibility_box.cpp boxes/peers/edit_peer_history_visibility_box.h + boxes/peers/toggle_topics_box.cpp + boxes/peers/toggle_topics_box.h calls/group/ui/calls_group_recording_box.cpp calls/group/ui/calls_group_recording_box.h diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 4038a11f63..4fc3ac0ea5 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 4038a11f635311073f6d55786490920b043cb319 +Subproject commit 4fc3ac0ea52f271cc9b108481f83d56fd76ab0ed From 90b2c077a65752c4013d9b78e75186a93a5d6084 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 May 2025 17:20:51 +0400 Subject: [PATCH 085/310] Don't flood with bot requests in monoforums. --- Telegram/SourceFiles/api/api_chat_participants.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index 478390798f..af60bdfe3d 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -494,8 +494,15 @@ void ChatParticipants::requestBots(not_null<ChannelData*> channel) { LOG(("API Error: " "channels.channelParticipantsNotModified received!")); }); - }).fail([=] { + }).fail([=](const MTP::Error &error) { _botsRequests.remove(channel); + if (error.type() == u"CHANNEL_MONOFORUM_UNSUPPORTED"_q) { + channel->mgInfo->bots.clear(); + channel->mgInfo->botStatus = -1; + channel->session().changes().peerUpdated( + channel, + Data::PeerUpdate::Flag::FullInfo); + } }).send(); _botsRequests[channel] = requestId; From c3860cfe72e31c86bb9a5937a0a56de8b5370d52 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 May 2025 17:21:04 +0400 Subject: [PATCH 086/310] Improve monoforum top bar status. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/chat_helpers/chat_helpers.style | 4 ++-- .../SourceFiles/history/view/history_view_top_bar_widget.cpp | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2cb115b74b..7ce0e8ad1c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -164,6 +164,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_chat_status_members_online" = "{members_count}, {online_count}"; "lng_chat_status_subscribers#one" = "{count} subscriber"; "lng_chat_status_subscribers#other" = "{count} subscribers"; +"lng_chat_status_direct" = "Direct messages"; "lng_channel_status" = "channel"; "lng_group_status" = "group"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index b0a98edbfb..c4c689c4b1 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -872,8 +872,8 @@ historyGiftToChannel: IconButton(defaultIconButton) { ripple: universalRippleAnimation; } historyDirectMessage: IconButton(historyGiftToChannel) { - icon: icon{{ "menu/chat_bubble", windowActiveTextFg }}; - iconOver: icon{{ "menu/chat_bubble", windowActiveTextFg }}; + icon: icon{{ "menu/chat_discuss", windowActiveTextFg }}; + iconOver: icon{{ "menu/chat_discuss", windowActiveTextFg }}; } historyUnblock: FlatButton(historyComposeButton) { color: attentionButtonFg; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 532f384480..2ea31c2dc1 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1696,6 +1696,8 @@ void TopBarWidget::updateOnlineDisplay() { const auto chats = monoforum->chatsList(); const auto count = chats->fullSize().current(); text = tr::lng_filters_chats_count(tr::now, lt_count, count); + } else if (peer->isMonoforum()) { + text = tr::lng_chat_status_direct(tr::now); } else if (const auto channel = peer->asChannel()) { if (channel->isMegagroup() && channel->canViewMembers() From 853757e61191ea293d0afa7df84018f3a472728c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 May 2025 17:38:21 +0400 Subject: [PATCH 087/310] Fix tabs transfer between chat widgets. --- Telegram/SourceFiles/history/history_widget.cpp | 17 ++++++++++++++++- Telegram/SourceFiles/history/history_widget.h | 1 + .../history/view/history_view_chat_section.cpp | 14 +++++++++++++- .../history/view/history_view_chat_section.h | 1 + 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0630c14547..13119ca1cc 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2474,6 +2474,7 @@ void HistoryWidget::showHistory( _silent.destroy(); updateBotKeyboard(); + _subsectionCheckLifetime.destroy(); if (_subsectionTabs) { _subsectionTabsLifetime.destroy(); controller()->saveSubsectionTabs(base::take(_subsectionTabs)); @@ -2623,7 +2624,6 @@ void HistoryWidget::showHistory( channel->flagsValue( ) | rpl::start_with_next([=] { refreshJoinChannelText(); - validateSubsectionTabs(); }, _list->lifetime()); } else { refreshJoinChannelText(); @@ -8245,6 +8245,21 @@ void HistoryWidget::showPremiumToast(not_null<DocumentData*> document) { } void HistoryWidget::validateSubsectionTabs() { + if (!_subsectionCheckLifetime + && _history + && _history->peer->isMegagroup()) { + _subsectionCheckLifetime = _history->peer->asChannel()->flagsValue( + ) | rpl::skip( + 1 + ) | rpl::filter([=](Data::Flags<ChannelDataFlags>::Change change) { + const auto mask = ChannelDataFlag::Forum + | ChannelDataFlag::ForumTabs + | ChannelDataFlag::MonoforumAdmin; + return change.diff & mask; + }) | rpl::start_with_next([=] { + validateSubsectionTabs(); + }); + } if (!_history || !HistoryView::SubsectionTabs::UsedFor(_history)) { if (_subsectionTabs) { _subsectionTabsLifetime.destroy(); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index e9f536e403..3c7cc4e82e 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -829,6 +829,7 @@ private: std::unique_ptr<HistoryView::ComposeSearch> _composeSearch; std::unique_ptr<HistoryView::SubsectionTabs> _subsectionTabs; rpl::lifetime _subsectionTabsLifetime; + rpl::lifetime _subsectionCheckLifetime; bool _cmdStartShown = false; object_ptr<Ui::InputField> _field; base::unique_qptr<Ui::RpWidget> _fieldDisabled; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 70e7d71042..cbd9d6c8a4 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -928,7 +928,6 @@ void ChatWidget::setupComposeControls() { channel->flagsValue() ) | rpl::start_with_next([=] { refreshJoinGroupButton(); - validateSubsectionTabs(); }, lifetime()); } else { refreshJoinGroupButton(); @@ -1522,6 +1521,19 @@ void ChatWidget::edit( } void ChatWidget::validateSubsectionTabs() { + if (!_subsectionCheckLifetime && _history->peer->isMegagroup()) { + _subsectionCheckLifetime = _history->peer->asChannel()->flagsValue( + ) | rpl::skip( + 1 + ) | rpl::filter([=](Data::Flags<ChannelDataFlags>::Change change) { + const auto mask = ChannelDataFlag::Forum + | ChannelDataFlag::ForumTabs + | ChannelDataFlag::MonoforumAdmin; + return change.diff & mask; + }) | rpl::start_with_next([=] { + validateSubsectionTabs(); + }); + } if (!HistoryView::SubsectionTabs::UsedFor(_history)) { if (_subsectionTabs) { _subsectionTabsLifetime.destroy(); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index a4fb8fb012..f62beb39d5 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -409,6 +409,7 @@ private: std::unique_ptr<EmptyPainter> _emptyPainter; std::unique_ptr<SubsectionTabs> _subsectionTabs; rpl::lifetime _subsectionTabsLifetime; + rpl::lifetime _subsectionCheckLifetime; bool _canSendTexts = false; bool _skipScrollEvent = false; bool _synteticScrollEvent = false; From d0e5ea78a5403716f12284385bbd7866d1a6c02d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 May 2025 18:25:41 +0400 Subject: [PATCH 088/310] Improve topics layout management. --- Telegram/Resources/langs/lang.strings | 1 + .../boxes/peers/edit_peer_info_box.cpp | 4 +-- .../boxes/peers/toggle_topics_box.cpp | 8 ++++-- .../dialogs/ui/dialogs_topics_view.cpp | 7 ++++- .../dialogs/ui/dialogs_topics_view.h | 1 + Telegram/SourceFiles/history/history_item.cpp | 28 +++++++++++++------ .../view/history_view_top_bar_widget.cpp | 4 ++- 7 files changed, 37 insertions(+), 16 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7ce0e8ad1c..0253369e8d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2078,6 +2078,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_changed_title" = "{from} changed group name to «{title}»"; "lng_action_changed_title_channel" = "Channel name was changed to «{title}»"; "lng_action_created_chat" = "{from} created the group «{title}»"; +"lng_action_created_monoforum" = "Direct messages were enabled in this channel."; "lng_action_ttl_changed" = "{from} set messages to auto-delete in {duration}"; "lng_action_ttl_changed_you" = "You set messages to auto-delete in {duration}"; "lng_action_ttl_changed_channel" = "Messages in this channel will be automatically deleted after {duration}"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index c53412162a..9ae378d7db 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -1087,7 +1087,7 @@ void Controller::fillDirectMessagesButton() { tr::lng_manage_monoforum(), std::move(label), [=] { showEditDirectMessagesBox(); }, - { &st::menuIconChatBubble }); + { .icon = &st::menuIconChatBubble, .newBadge = true }); } // //void Controller::fillInviteLinkButton() { @@ -1127,7 +1127,7 @@ void Controller::fillForumButton() { changes->events_starting_with({}) | rpl::map(label), [] {}, st::manageGroupTopicsButton, - { &st::menuIconTopics })); + { .icon = &st::menuIconTopics, .newBadge = true })); button->setClickedCallback(crl::guard(this, [=] { if (!*_forumSavedValue && _controls.forumToggleLocked) { diff --git a/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp index b2fed66b25..4d6d5454dd 100644 --- a/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/toggle_topics_box.cpp @@ -81,6 +81,7 @@ LayoutButton::LayoutButton( const auto icon = iconWidget.release(); setClickedCallback([=] { group->setValue(type); + iconAnimate(anim::repeat::once); }); group->value() | rpl::start_with_next([=](LayoutType value) { const auto active = (value == type); @@ -93,9 +94,6 @@ LayoutButton::LayoutButton( } _active = active; _text.update(); - if (_active) { - iconAnimate(anim::repeat::once); - } _activeAnimation.start([=] { icon->update(); }, _active ? 0. : 1., _active ? 0. : 1., st::fadeWrapDuration); @@ -170,6 +168,10 @@ void ToggleTopicsBox( st::settingsButtonNoIcon)); toggle->toggleOn(rpl::single(enabled)); + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + const auto group = std::make_shared<Ui::RadioenumGroup<LayoutType>>(tabs ? LayoutType::Tabs : LayoutType::List); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index 4ce796c92e..9286e40cdb 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -110,6 +110,7 @@ void TopicsView::prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint) { _jumpToTopic = false; } } + _allLoaded = _forum->topicsList()->loaded(); } void TopicsView::prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint) { @@ -182,6 +183,7 @@ void TopicsView::prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint) { _jumpToTopic = false; } } + _allLoaded = _monoforum->chatsList()->loaded(); } int TopicsView::jumpToTopicWidth() const { @@ -207,10 +209,13 @@ void TopicsView::paint( rect.setWidth(rect.width() - _lastTopicJumpGeometry.rightCut); auto skipBig = _jumpToTopic && !context.active; if (_titles.empty()) { + const auto text = (_monoforum && _allLoaded) + ? tr::lng_filters_no_chats(tr::now) + : tr::lng_contacts_loading(tr::now); p.drawText( rect.x(), rect.y() + st::normalFont->ascent, - tr::lng_contacts_loading(tr::now)); + text); return; } for (const auto &title : _titles) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h index 7c41337fad..9dd5d3aa7b 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h @@ -121,6 +121,7 @@ private: JumpToLastGeometry _lastTopicJumpGeometry; int _version = -1; bool _jumpToTopic = false; + bool _allLoaded = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 04c3f1076b..672b322412 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -4632,20 +4632,30 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { auto prepareChatCreate = [this](const MTPDmessageActionChatCreate &action) { auto result = PreparedServiceText(); - result.links.push_back(fromLink()); - result.text = tr::lng_action_created_chat( - tr::now, - lt_from, - fromLinkText(), // Link 1. - lt_title, - { .text = qs(action.vtitle()) }, - Ui::Text::WithEntities); + if (_history->peer->isMonoforum()) { + result.text = tr::lng_action_created_monoforum( + tr::now, + Ui::Text::WithEntities); + } else { + result.links.push_back(fromLink()); + result.text = tr::lng_action_created_chat( + tr::now, + lt_from, + fromLinkText(), // Link 1. + lt_title, + { .text = qs(action.vtitle()) }, + Ui::Text::WithEntities); + } return result; }; auto prepareChannelCreate = [this](const MTPDmessageActionChannelCreate &action) { auto result = PreparedServiceText(); - if (isPost()) { + if (_history->peer->isMonoforum()) { + result.text = tr::lng_action_created_monoforum( + tr::now, + Ui::Text::WithEntities); + } else if (isPost()) { result.text = tr::lng_action_created_channel( tr::now, Ui::Text::WithEntities); diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 2ea31c2dc1..b825a2b19e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1695,7 +1695,9 @@ void TopBarWidget::updateOnlineDisplay() { } else if (const auto monoforum = peer->monoforum()) { const auto chats = monoforum->chatsList(); const auto count = chats->fullSize().current(); - text = tr::lng_filters_chats_count(tr::now, lt_count, count); + text = (count > 0) + ? tr::lng_filters_chats_count(tr::now, lt_count, count) + : tr::lng_filters_no_chats(tr::now); } else if (peer->isMonoforum()) { text = tr::lng_chat_status_direct(tr::now); } else if (const auto channel = peer->asChannel()) { From 5b15f377cdf2eb9c4ad76bda1aa475b45ac74abf Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 29 May 2025 19:02:56 +0400 Subject: [PATCH 089/310] Improve set direct message price box. --- .../animations/edit_peers/direct_messages.tgs | Bin 0 -> 7075 bytes Telegram/Resources/langs/lang.strings | 7 ++-- .../Resources/qrc/telegram/animations.qrc | 1 + .../SourceFiles/boxes/edit_privacy_box.cpp | 38 +++++++++++++----- .../boxes/peers/edit_peer_info_box.cpp | 2 +- 5 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 Telegram/Resources/animations/edit_peers/direct_messages.tgs diff --git a/Telegram/Resources/animations/edit_peers/direct_messages.tgs b/Telegram/Resources/animations/edit_peers/direct_messages.tgs new file mode 100644 index 0000000000000000000000000000000000000000..3a0806e1ae327429470389ba0ed9e1e464e1d282 GIT binary patch literal 7075 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 N{{wloPVZ&=006|l#rpsN literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 0253369e8d..9efcdd8784 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1891,9 +1891,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_monoforum" = "Direct Messages"; "lng_manage_monoforum_off" = "Off"; "lng_manage_monoforum_free" = "Free"; -"lng_manage_monoforum_allow" = "Allow Direct Messages"; -"lng_manage_monoforum_about" = "Allow users to write direct private messages to your channel, with the option to charge a fee for every message."; -"lng_manage_monoforum_price_about" = "Charge users for the ability to write a direct message to your channel. Your channel will receive {percent} of the selected fee ({amount}) for each incoming message."; +"lng_manage_monoforum_allow" = "Allow Channel Messages"; +"lng_manage_monoforum_price" = "Price for each message"; +"lng_manage_monoforum_about" = "Allow users to send messages to your channel, with the option to charge a fee for each message."; +"lng_manage_monoforum_price_about" = "Your channel will receive {percent} of the selected fee ({amount}) for each incoming message."; "lng_manage_history_visibility_title" = "Chat history for new members"; "lng_manage_history_visibility_shown" = "Visible"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 09702ee5d0..98917d9924 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -36,6 +36,7 @@ <file alias="topics.tgs">../../animations/edit_peers/topics.tgs</file> <file alias="topics_tabs.tgs">../../animations/edit_peers/topics_tabs.tgs</file> <file alias="topics_list.tgs">../../animations/edit_peers/topics_list.tgs</file> + <file alias="direct_messages.tgs">../../animations/edit_peers/direct_messages.tgs</file> <file alias="dice_idle.tgs">../../animations/dice/dice_idle.tgs</file> <file alias="dart_idle.tgs">../../animations/dice/dart_idle.tgs</file> diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 2047539943..88ebe4ccbb 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -511,8 +511,9 @@ auto PrivacyExceptionsBoxController::createRow(not_null<History*> history) current->moveToLeft((outer - current->width()) / 2, 0, outer); }; const auto updateByValue = [=](int value) { - current->setText( - tr::lng_action_gift_for_stars(tr::now, lt_count, value)); + current->setText(value > 0 + ? tr::lng_action_gift_for_stars(tr::now, lt_count, value) + : tr::lng_manage_monoforum_free(tr::now)); state->index = 0; auto maxIndex = valuesCount - 1; @@ -1178,7 +1179,9 @@ rpl::producer<int> SetupChargeSlider( const auto chargeStars = savedValue.value_or(defaultValue); state->stars = chargeStars; - Ui::AddSubsectionTitle(container, (group || broadcast) + Ui::AddSubsectionTitle(container, broadcast + ? tr::lng_manage_monoforum_price() + : group ? tr::lng_rights_charge_price() : tr::lng_messages_privacy_price()); @@ -1251,17 +1254,32 @@ void EditDirectMessagesPriceBox( std::optional<int> savedValue, Fn<void(std::optional<int>)> callback) { box->setTitle(tr::lng_manage_monoforum()); + box->setWidth(st::boxWideWidth); - const auto toggle = box->addRow(object_ptr<Ui::SettingsButton>( + const auto container = box->verticalLayout(); + + Settings::AddDividerTextWithLottie(container, { + .lottie = u"direct_messages"_q, + .lottieSize = st::settingsFilterIconSize, + .lottieMargins = st::settingsFilterIconPadding, + .showFinished = box->showFinishes(), + .about = tr::lng_manage_monoforum_about( + Ui::Text::RichLangValue + ), + .aboutMargins = st::settingsFilterDividerLabelPadding, + }); + + Ui::AddSkip(container); + + const auto toggle = container->add(object_ptr<Ui::SettingsButton>( box, tr::lng_manage_monoforum_allow(), - st::settingsButtonNoIcon - ), {})->toggleOn(rpl::single(savedValue.has_value())); - Ui::AddSkip(box->verticalLayout()); + st::settingsButtonNoIcon)); + toggle->toggleOn(rpl::single(savedValue.has_value())); - Ui::AddDividerText( - box->verticalLayout(), - tr::lng_manage_monoforum_about()); + Ui::AddSkip(container); + Ui::AddDivider(container); + Ui::AddSkip(container); const auto wrap = box->addRow( object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 9ae378d7db..4604c4f6a7 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -1087,7 +1087,7 @@ void Controller::fillDirectMessagesButton() { tr::lng_manage_monoforum(), std::move(label), [=] { showEditDirectMessagesBox(); }, - { .icon = &st::menuIconChatBubble, .newBadge = true }); + { .icon = &st::menuIconChats, .newBadge = true }); } // //void Controller::fillInviteLinkButton() { From abe1962002e88da502810ec306636edd1eb66cc7 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 30 May 2025 12:22:59 +0400 Subject: [PATCH 090/310] Show context menu for topics in new tabs. --- Telegram/Resources/langs/lang.strings | 2 +- Telegram/SourceFiles/dialogs/dialogs_key.h | 1 + .../view/history_view_subsection_tabs.cpp | 31 +++++++++++++++++++ .../view/history_view_subsection_tabs.h | 5 +++ .../ui/controls/subsection_tabs_slider.cpp | 21 +++++++++++++ .../ui/controls/subsection_tabs_slider.h | 10 ++++++ .../SourceFiles/window/window_peer_menu.cpp | 17 ++++++++-- 7 files changed, 83 insertions(+), 4 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 9efcdd8784..877674edd9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -170,7 +170,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_status" = "group"; "lng_scam_badge" = "SCAM"; "lng_fake_badge" = "FAKE"; -"lng_direct_badge" = "MESSAGES"; +"lng_direct_badge" = "DIRECT"; "lng_remember" = "Remember this choice"; diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index 58b0284dcc..de4f99f3ad 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -113,6 +113,7 @@ struct EntryState { Replies, SavedSublist, ContextMenu, + SubsectionTabsMenu, ShortcutMessages, }; diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 932504952b..f8d664352b 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -23,13 +23,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "ui/controls/subsection_tabs_slider.h" #include "ui/effects/ripple_animation.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" +#include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" @@ -188,6 +192,12 @@ void SubsectionTabs::setupSlider( } }, slider->lifetime()); + slider->sectionContextMenu() | rpl::start_with_next([=](int index) { + if (index >= 0 && index < _slice.size()) { + showThreadContextMenu(_slice[index].thread); + } + }, slider->lifetime()); + rpl::merge( scroll->scrolls(), _scrollCheckRequests.events(), @@ -363,6 +373,27 @@ void SubsectionTabs::setupSlider( }, scroll->lifetime()); } +void SubsectionTabs::showThreadContextMenu(not_null<Data::Thread*> thread) { + _menu = nullptr; + _menu = base::make_unique_q<Ui::PopupMenu>( + _horizontal ? _horizontal : _vertical, + st::popupMenuExpandedSeparator); + + const auto addAction = Ui::Menu::CreateAddActionCallback(_menu); + Window::FillDialogsEntryMenu( + _controller, + Dialogs::EntryState{ + .key = Dialogs::Key{ thread }, + .section = Dialogs::EntryState::Section::SubsectionTabsMenu, + }, + addAction); + if (_menu->empty()) { + _menu = nullptr; + } else { + _menu->popup(QCursor::pos()); + } +} + void SubsectionTabs::loadMore() { if (const auto forum = _history->peer->forum()) { forum->requestTopics(); diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index d198a2fd79..200afe3b35 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/unique_qptr.h" #include "dialogs/dialogs_common.h" class History; @@ -21,6 +22,7 @@ class SessionController; namespace Ui { class RpWidget; +class PopupMenu; class ScrollArea; class SubsectionSlider; } // namespace Ui @@ -83,10 +85,13 @@ private: not_null<Ui::ScrollArea*> scroll, not_null<Ui::SubsectionSlider*> slider, bool vertical); + void showThreadContextMenu(not_null<Data::Thread*> thread); const not_null<Window::SessionController*> _controller; const not_null<History*> _history; + base::unique_qptr<Ui::PopupMenu> _menu; + Ui::RpWidget *_horizontal = nullptr; Ui::RpWidget *_vertical = nullptr; Ui::RpWidget *_shadow = nullptr; diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index 15691b6e21..fbf6df2d0a 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -270,6 +270,10 @@ void SubsectionButton::setActiveShown(float64 activeShown) { } } +void SubsectionButton::contextMenuEvent(QContextMenuEvent *e) { + _delegate->buttonContextMenu(this, e); +} + SubsectionSlider::SubsectionSlider(not_null<QWidget*> parent, bool vertical) : RpWidget(parent) , _vertical(vertical) @@ -407,6 +411,10 @@ rpl::producer<int> SubsectionSlider::sectionActivated() const { return _sectionActivated.events(); } +rpl::producer<int> SubsectionSlider::sectionContextMenu() const { + return _sectionContextMenu.events(); +} + int SubsectionSlider::lookupSectionPosition(int index) const { Expects(index >= 0 && index < _tabs.size()); @@ -472,6 +480,19 @@ float64 SubsectionSlider::buttonActive(not_null<SubsectionButton*> button) { : 0.; } +void SubsectionSlider::buttonContextMenu( + not_null<SubsectionButton*> button, + not_null<QContextMenuEvent*> e) { + const auto i = ranges::find( + _tabs, + button.get(), + &std::unique_ptr<SubsectionButton>::get); + Assert(i != end(_tabs)); + + _sectionContextMenu.fire(int(i - begin(_tabs))); + e->accept(); +} + Text::MarkedContext SubsectionSlider::buttonContext() { return _context; } diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h index 391c51964c..30d35aa190 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -42,6 +42,9 @@ public: virtual bool buttonPaused() = 0; virtual float64 buttonActive(not_null<SubsectionButton*> button) = 0; virtual Text::MarkedContext buttonContext() = 0; + virtual void buttonContextMenu( + not_null<SubsectionButton*> button, + not_null<QContextMenuEvent*> e) = 0; }; class SubsectionButton : public RippleButton { @@ -60,6 +63,8 @@ public: protected: virtual void dataUpdatedHook() = 0; + void contextMenuEvent(QContextMenuEvent *e) override; + const not_null<SubsectionButtonDelegate*> _delegate; SubsectionTab _data; float64 _activeShown = 0.; @@ -79,10 +84,14 @@ public: [[nodiscard]] int sectionsCount() const; [[nodiscard]] rpl::producer<int> sectionActivated() const; + [[nodiscard]] rpl::producer<int> sectionContextMenu() const; [[nodiscard]] int lookupSectionPosition(int index) const; bool buttonPaused() override; float64 buttonActive(not_null<SubsectionButton*> button) override; + void buttonContextMenu( + not_null<SubsectionButton*> button, + not_null<QContextMenuEvent*> e) override; Text::MarkedContext buttonContext() override; [[nodiscard]] not_null<SubsectionButton*> buttonAt(int index); @@ -125,6 +134,7 @@ protected: bool _reorderAllowed = false; rpl::event_stream<int> _sectionActivated; + rpl::event_stream<int> _sectionContextMenu; Fn<bool()> _paused; }; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index bef9e3c6c9..026b1246dc 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -495,6 +495,10 @@ void Filler::addToggleTopicClosed() { void Filler::addTogglePin() { if ((!_sublist && !_peer) || (_topic && !_topic->canTogglePinned())) { return; + } else if (_request.section == Section::SubsectionTabsMenu + && !_sublist + && !_topic) { + return; } const auto controller = _controller; const auto filterId = _request.filterId; @@ -602,6 +606,10 @@ void Filler::addToggleFolder() { || !history->owner().chatsFilters().has() || !history->inChatList()) { return; + } else if (_request.section == Section::SubsectionTabsMenu + && !_sublist + && !_topic) { + return; } _addAction(PeerMenuCallback::Args{ .text = tr::lng_filters_menu_add(tr::now), @@ -689,7 +697,9 @@ void Filler::addNewWindow() { } void Filler::addToggleArchive() { - if (!_peer || _topic) { + if (!_peer + || _topic + || _request.section == Section::SubsectionTabsMenu) { return; } const auto peer = _peer; @@ -721,7 +731,7 @@ void Filler::addToggleArchive() { } void Filler::addClearHistory() { - if (_topic) { + if (_topic || _peer->isMonoforum()) { return; } const auto channel = _peer->asChannel(); @@ -1261,7 +1271,8 @@ void Filler::fill() { case Section::Profile: fillProfileActions(); break; case Section::Replies: fillRepliesActions(); break; case Section::Scheduled: fillScheduledActions(); break; - case Section::ContextMenu: fillContextMenuActions(); break; + case Section::ContextMenu: + case Section::SubsectionTabsMenu: fillContextMenuActions(); break; default: Unexpected("_request.section in Filler::fill."); } } From 3278de9ba1554b91e4b2417fa53f2c30c1810fb2 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 30 May 2025 15:00:33 +0400 Subject: [PATCH 091/310] Support mark as read/unread in sublists. --- Telegram/SourceFiles/apiwrap.cpp | 26 +++++++ Telegram/SourceFiles/apiwrap.h | 6 ++ .../boxes/moderate_messages_box.cpp | 76 ++++++++++++++++--- .../SourceFiles/boxes/moderate_messages_box.h | 7 ++ Telegram/SourceFiles/boxes/peer_list_box.cpp | 10 +-- Telegram/SourceFiles/data/data_changes.cpp | 37 +++++++++ Telegram/SourceFiles/data/data_changes.h | 36 +++++++++ Telegram/SourceFiles/data/data_forum.cpp | 47 ++++++------ Telegram/SourceFiles/data/data_histories.cpp | 19 +++++ Telegram/SourceFiles/data/data_histories.h | 4 + Telegram/SourceFiles/data/data_peer.cpp | 4 + Telegram/SourceFiles/data/data_peer.h | 1 + .../SourceFiles/data/data_saved_messages.cpp | 42 +++++++++- .../SourceFiles/data/data_saved_messages.h | 1 + .../SourceFiles/data/data_saved_sublist.cpp | 19 +++++ .../SourceFiles/data/data_saved_sublist.h | 2 +- Telegram/SourceFiles/data/data_thread.cpp | 11 +++ Telegram/SourceFiles/data/data_thread.h | 1 + .../SourceFiles/dialogs/dialogs_entry.cpp | 4 + Telegram/SourceFiles/history/history.cpp | 28 +++++-- Telegram/SourceFiles/history/history.h | 1 + .../history/history_inner_widget.cpp | 19 +++-- Telegram/SourceFiles/history/history_item.cpp | 11 ++- .../history/history_unread_things.cpp | 11 +++ .../view/history_view_chat_preview.cpp | 5 +- .../view/history_view_chat_section.cpp | 75 +++++++++++++++--- .../history/view/history_view_chat_section.h | 2 + .../view/history_view_subsection_tabs.cpp | 5 +- .../view/history_view_top_bar_widget.cpp | 2 +- .../info/profile/info_profile_cover.cpp | 2 +- .../ui/controls/userpic_button.cpp | 4 +- .../SourceFiles/ui/controls/userpic_button.h | 3 +- .../SourceFiles/window/window_peer_menu.cpp | 61 ++++++++++----- .../SourceFiles/window/window_peer_menu.h | 3 + .../window/window_session_controller.cpp | 3 + 35 files changed, 497 insertions(+), 91 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 5f4a5fda8b..873fc58b3a 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -1373,6 +1373,32 @@ void ApiWrap::deleteAllFromParticipantSend( }).send(); } +void ApiWrap::deleteSublistHistory( + not_null<ChannelData*> channel, + not_null<PeerData*> sublistPeer) { + deleteSublistHistorySend(channel, sublistPeer); +} + +void ApiWrap::deleteSublistHistorySend( + not_null<ChannelData*> parentChat, + not_null<PeerData*> sublistPeer) { + request(MTPmessages_DeleteSavedHistory( + MTP_flags(MTPmessages_DeleteSavedHistory::Flag::f_parent_peer), + parentChat->input, + sublistPeer->input, + MTP_int(0), // max_id + MTP_int(0), // min_date + MTP_int(0) // max_date + )).done([=](const MTPmessages_AffectedHistory &result) { + const auto offset = applyAffectedHistory(parentChat, result); + if (offset > 0) { + deleteSublistHistorySend(parentChat, sublistPeer); + } else if (const auto monoforum = parentChat->monoforum()) { + monoforum->applySublistDeleted(sublistPeer); + } + }).send(); +} + void ApiWrap::scheduleStickerSetRequest(uint64 setId, uint64 access) { if (!_stickerSetRequests.contains(setId)) { _stickerSetRequests.emplace(setId, StickerSetRequest{ access }); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 529114b0a1..a4835adbae 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -231,6 +231,9 @@ public: void deleteAllFromParticipant( not_null<ChannelData*> channel, not_null<PeerData*> from); + void deleteSublistHistory( + not_null<ChannelData*> parentChat, + not_null<PeerData*> sublistPeer); void requestWebPageDelayed(not_null<WebPageData*> page); void clearWebPageRequest(not_null<WebPageData*> page); @@ -539,6 +542,9 @@ private: void deleteAllFromParticipantSend( not_null<ChannelData*> channel, not_null<PeerData*> from); + void deleteSublistHistorySend( + not_null<ChannelData*> parentChat, + not_null<PeerData*> sublistPeer); void uploadAlbumMedia( not_null<HistoryItem*> item, diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index fbe6ee72b4..ae25f7a687 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_participant_status.h" #include "data/data_histories.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" @@ -565,15 +566,7 @@ bool CanCreateModerateMessagesBox(const HistoryItemsList &items) { && !options.participants.empty(); } -void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { - const auto container = box->verticalLayout(); - - const auto maybeUser = peer->asUser(); - const auto isBot = maybeUser && maybeUser->isBot(); - - Ui::AddSkip(container); - Ui::AddSkip(container); - +void SafeSubmitOnEnter(not_null<Ui::GenericBox*> box) { base::install_event_filter(box, [=](not_null<QEvent*> event) { if (event->type() == QEvent::KeyPress) { if (const auto k = static_cast<QKeyEvent*>(event.get())) { @@ -587,12 +580,24 @@ void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { }, .confirmText = tr::lng_box_yes(), .cancelText = tr::lng_box_no(), - })); + })); } } } return base::EventFilterResult::Continue; }); +} + +void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { + const auto container = box->verticalLayout(); + + const auto maybeUser = peer->asUser(); + const auto isBot = maybeUser && maybeUser->isBot(); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + SafeSubmitOnEnter(box); const auto userpic = Ui::CreateChild<Ui::UserpicButton>( container, @@ -754,3 +759,54 @@ void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { }, st::attentionBoxButton); box->addButton(tr::lng_cancel(), close); } + +void DeleteSublistBox( + not_null<Ui::GenericBox*> box, + not_null<Data::SavedSublist*> sublist) { + const auto container = box->verticalLayout(); + + const auto weak = base::make_weak(sublist.get()); + const auto peer = sublist->sublistPeer(); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + SafeSubmitOnEnter(box); + + const auto userpic = Ui::CreateChild<Ui::UserpicButton>( + container, + peer, + st::mainMenuUserpic); + Ui::IconWithTitle( + container, + userpic, + Ui::CreateChild<Ui::FlatLabel>( + container, + tr::lng_profile_delete_conversation() | Ui::Text::ToBold(), + box->getDelegate()->style().title)); + + Ui::AddSkip(container); + Ui::AddSkip(container); + + box->addRow( + object_ptr<Ui::FlatLabel>( + container, + tr::lng_sure_delete_history( + lt_contact, + rpl::single(peer->name())), + st::boxLabel)); + + Ui::AddSkip(container); + + const auto close = crl::guard(box, [=] { box->closeBox(); }); + box->addButton(tr::lng_box_delete(), [=] { + const auto strong = weak.get(); + const auto parentChat = strong ? strong->parentChat() : nullptr; + if (!parentChat) { + return; + } + peer->session().api().deleteSublistHistory(parentChat, peer); + close(); + }, st::attentionBoxButton); + box->addButton(tr::lng_cancel(), close); +} diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.h b/Telegram/SourceFiles/boxes/moderate_messages_box.h index eb24026125..64e544e4b3 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.h +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class PeerData; +namespace Data { +class SavedSublist; +} // namespace Data + namespace Ui { class GenericBox; } // namespace Ui @@ -21,3 +25,6 @@ void CreateModerateMessagesBox( [[nodiscard]] bool CanCreateModerateMessagesBox(const HistoryItemsList &); void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer); +void DeleteSublistBox( + not_null<Ui::GenericBox*> box, + not_null<Data::SavedSublist*> sublist); diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 5ff2323d27..d57ba16954 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -714,7 +714,7 @@ void PeerListRow::elementsPaint( } QString PeerListRow::generateName() { - return peer()->name(); + return peer()->userpicPaintingPeer()->name(); } QString PeerListRow::generateShortName() { @@ -724,12 +724,12 @@ QString PeerListRow::generateShortName() { ? tr::lng_replies_messages(tr::now) : _isVerifyCodesChat ? tr::lng_verification_codes(tr::now) - : peer()->shortName(); + : peer()->userpicPaintingPeer()->shortName(); } Ui::PeerUserpicView &PeerListRow::ensureUserpicView() { - if (!_userpic.cloud && peer()->hasUserpic()) { - _userpic = peer()->createUserpicView(); + if (!_userpic.cloud && peer()->userpicPaintingPeer()->hasUserpic()) { + _userpic = peer()->userpicPaintingPeer()->createUserpicView(); } return _userpic; } @@ -738,7 +738,7 @@ PaintRoundImageCallback PeerListRow::generatePaintUserpicCallback( bool forceRound) { const auto saved = !_savedMessagesStatus.isEmpty(); const auto replies = _isRepliesMessagesChat; - const auto peer = this->peer(); + const auto peer = this->peer()->userpicPaintingPeer(); auto userpic = saved ? Ui::PeerUserpicView() : ensureUserpicView(); if (forceRound && peer->isForum()) { return ForceRoundUserpicCallback(peer); diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 773c50d5d4..6f2a554da9 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -204,6 +204,42 @@ void Changes::topicRemoved(not_null<ForumTopic*> topic) { _topicChanges.drop(topic); } +void Changes::sublistUpdated( + not_null<SavedSublist*> sublist, + SublistUpdate::Flags flags) { + const auto drop = (flags & SublistUpdate::Flag::Destroyed); + _sublistChanges.updated(sublist, flags, drop); + if (!drop) { + scheduleNotifications(); + } +} + +rpl::producer<SublistUpdate> Changes::sublistUpdates( + SublistUpdate::Flags flags) const { + return _sublistChanges.updates(flags); +} + +rpl::producer<SublistUpdate> Changes::sublistUpdates( + not_null<SavedSublist*> sublist, + SublistUpdate::Flags flags) const { + return _sublistChanges.updates(sublist, flags); +} + +rpl::producer<SublistUpdate> Changes::sublistFlagsValue( + not_null<SavedSublist*> sublist, + SublistUpdate::Flags flags) const { + return _sublistChanges.flagsValue(sublist, flags); +} + +rpl::producer<SublistUpdate> Changes::realtimeSublistUpdates( + SublistUpdate::Flag flag) const { + return _sublistChanges.realtimeUpdates(flag); +} + +void Changes::sublistRemoved(not_null<SavedSublist*> sublist) { + _sublistChanges.drop(sublist); +} + void Changes::messageUpdated( not_null<HistoryItem*> item, MessageUpdate::Flags flags) { @@ -323,6 +359,7 @@ void Changes::sendNotifications() { _messageChanges.sendNotifications(); _entryChanges.sendNotifications(); _topicChanges.sendNotifications(); + _sublistChanges.sendNotifications(); _storyChanges.sendNotifications(); } diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 22cfec1e66..f3215d2599 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -38,6 +38,7 @@ inline constexpr int CountBit(Flag Last = Flag::LastUsedBit) { namespace Data { class ForumTopic; +class SavedSublist; class Story; struct NameUpdate { @@ -184,6 +185,25 @@ struct TopicUpdate { }; +struct SublistUpdate { + enum class Flag : uint32 { + None = 0, + + UnreadView = (1U << 1), + UnreadReactions = (1U << 2), + CloudDraft = (1U << 3), + Destroyed = (1U << 4), + + LastUsedBit = (1U << 4), + }; + using Flags = base::flags<Flag>; + friend inline constexpr auto is_flag_type(Flag) { return true; } + + not_null<SavedSublist*> sublist; + Flags flags = 0; + +}; + struct MessageUpdate { enum class Flag : uint32 { None = 0, @@ -305,6 +325,21 @@ public: TopicUpdate::Flag flag) const; void topicRemoved(not_null<ForumTopic*> topic); + void sublistUpdated( + not_null<SavedSublist*> sublist, + SublistUpdate::Flags flags); + [[nodiscard]] rpl::producer<SublistUpdate> sublistUpdates( + SublistUpdate::Flags flags) const; + [[nodiscard]] rpl::producer<SublistUpdate> sublistUpdates( + not_null<SavedSublist*> sublist, + SublistUpdate::Flags flags) const; + [[nodiscard]] rpl::producer<SublistUpdate> sublistFlagsValue( + not_null<SavedSublist*> sublist, + SublistUpdate::Flags flags) const; + [[nodiscard]] rpl::producer<SublistUpdate> realtimeSublistUpdates( + SublistUpdate::Flag flag) const; + void sublistRemoved(not_null<SavedSublist*> sublist); + void messageUpdated( not_null<HistoryItem*> item, MessageUpdate::Flags flags); @@ -396,6 +431,7 @@ private: Manager<PeerData, PeerUpdate> _peerChanges; Manager<History, HistoryUpdate> _historyChanges; Manager<ForumTopic, TopicUpdate> _topicChanges; + Manager<SavedSublist, SublistUpdate> _sublistChanges; Manager<HistoryItem, MessageUpdate> _messageChanges; Manager<Dialogs::Entry, EntryUpdate> _entryChanges; Manager<Story, StoryUpdate> _storyChanges; diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index d422b01947..921cd0ca15 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -175,30 +175,31 @@ void Forum::applyTopicDeleted(MsgId rootId) { _topicsDeleted.emplace(rootId); const auto i = _topics.find(rootId); - if (i != end(_topics)) { - const auto raw = i->second.get(); - Core::App().notifications().clearFromTopic(raw); - owner().removeChatListEntry(raw); - - if (ranges::contains(_lastTopics, not_null(raw))) { - reorderLastTopics(); - } - - _topicDestroyed.fire(raw); - session().changes().topicUpdated( - raw, - Data::TopicUpdate::Flag::Destroyed); - session().changes().entryUpdated( - raw, - Data::EntryUpdate::Flag::Destroyed); - _topics.erase(i); - - _history->destroyMessagesByTopic(rootId); - session().storage().unload(Storage::SharedMediaUnloadThread( - _history->peer->id, - rootId)); - _history->setForwardDraft(rootId, PeerId(), {}); + if (i == end(_topics)) { + return; } + const auto raw = i->second.get(); + Core::App().notifications().clearFromTopic(raw); + owner().removeChatListEntry(raw); + + if (ranges::contains(_lastTopics, not_null(raw))) { + reorderLastTopics(); + } + + _topicDestroyed.fire(raw); + session().changes().topicUpdated( + raw, + Data::TopicUpdate::Flag::Destroyed); + session().changes().entryUpdated( + raw, + Data::EntryUpdate::Flag::Destroyed); + _topics.erase(i); + + _history->destroyMessagesByTopic(rootId); + session().storage().unload(Storage::SharedMediaUnloadThread( + _history->peer->id, + rootId)); + _history->setForwardDraft(rootId, PeerId(), {}); } void Forum::reorderLastTopics() { diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 44dd7b86f7..47804bba96 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_text_entities.h" #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -500,6 +501,24 @@ void Histories::changeDialogUnreadMark( )).send(); } +void Histories::changeSublistUnreadMark( + not_null<Data::SavedSublist*> sublist, + bool unread) { + const auto parent = sublist->parentChat(); + if (!parent) { + return; + } + sublist->setUnreadMark(unread); + + using Flag = MTPmessages_MarkDialogUnread::Flag; + session().api().request(MTPmessages_MarkDialogUnread( + MTP_flags(Flag::f_parent_peer + | (unread ? Flag::f_unread : Flag(0))), + parent->input, + MTP_inputDialogPeer(sublist->sublistPeer()->input) + )).send(); +} + void Histories::requestFakeChatListMessage( not_null<History*> history) { if (_fakeChatListRequests.contains(history)) { diff --git a/Telegram/SourceFiles/data/data_histories.h b/Telegram/SourceFiles/data/data_histories.h index eb8645c5a6..fd6ee38108 100644 --- a/Telegram/SourceFiles/data/data_histories.h +++ b/Telegram/SourceFiles/data/data_histories.h @@ -26,6 +26,7 @@ namespace Data { class Session; class Folder; struct WebPageDraft; +class SavedSublist; [[nodiscard]] MTPInputReplyTo ReplyToForMTP( not_null<History*> history, @@ -71,6 +72,9 @@ public: Fn<void()> callback = nullptr); void dialogEntryApplied(not_null<History*> history); void changeDialogUnreadMark(not_null<History*> history, bool unread); + void changeSublistUnreadMark( + not_null<Data::SavedSublist*> sublist, + bool unread); void requestFakeChatListMessage(not_null<History*> history); void requestGroupAround(not_null<HistoryItem*> item); diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 1f8e0ad696..4556022257 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1174,6 +1174,10 @@ not_null<const PeerData*> PeerData::userpicPaintingPeer() const { return const_cast<PeerData*>(this)->userpicPaintingPeer(); } +bool PeerData::userpicForceForumShape() const { + return monoforumBroadcast() != nullptr; +} + ChannelData *PeerData::monoforumBroadcast() const { const auto monoforum = asMonoforum(); return monoforum ? monoforum->monoforumLink() : nullptr; diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 397965e476..8de2297985 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -307,6 +307,7 @@ public: [[nodiscard]] not_null<const PeerData*> migrateToOrMe() const; [[nodiscard]] not_null<PeerData*> userpicPaintingPeer(); [[nodiscard]] not_null<const PeerData*> userpicPaintingPeer() const; + [[nodiscard]] bool userpicForceForumShape() const; // isMonoforum() ? monoforumLink() : nullptr [[nodiscard]] ChannelData *monoforumBroadcast() const; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index ef67795445..94d02148c4 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_messages.h" #include "apiwrap.h" +#include "core/application.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_user.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_unread_things.h" #include "main/main_session.h" +#include "window/notifications_manager.h" namespace Data { namespace { @@ -50,11 +52,13 @@ SavedMessages::SavedMessages( SavedMessages::~SavedMessages() { auto &changes = session().changes(); - for (const auto &[peer, sublist] : _sublists) { - _owningHistory->setForwardDraft(MsgId(), peer->id, {}); + if (_owningHistory) { + for (const auto &[peer, sublist] : _sublists) { + _owningHistory->setForwardDraft(MsgId(), peer->id, {}); - const auto raw = sublist.get(); - changes.entryRemoved(raw); + const auto raw = sublist.get(); + changes.entryRemoved(raw); + } } } @@ -308,6 +312,36 @@ void SavedMessages::apply(const MTPDupdateSavedDialogPinned &update) { }); } +void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) { + const auto i = _sublists.find(sublistPeer); + if (i == end(_sublists)) { + return; + } + const auto raw = i->second.get(); + //Core::App().notifications().clearFromTopic(raw); // #TODO monoforum + owner().removeChatListEntry(raw); + + if (ranges::contains(_lastSublists, not_null(raw))) { + reorderLastSublists(); + } + + _sublistDestroyed.fire(raw); + session().changes().sublistUpdated( + raw, + Data::SublistUpdate::Flag::Destroyed); + session().changes().entryUpdated( + raw, + Data::EntryUpdate::Flag::Destroyed); + _sublists.erase(i); + + const auto history = owningHistory(); + history->destroyMessagesBySublist(sublistPeer); + //session().storage().unload(Storage::SharedMediaUnloadThread( + // _history->peer->id, + // rootId)); + history->setForwardDraft(MsgId(), sublistPeer->id, {}); +} + void SavedMessages::reorderLastSublists() { if (!_parentChat) { return; diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 206b905f21..8b5530db79 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -50,6 +50,7 @@ public: void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); + void applySublistDeleted(not_null<PeerData*> sublistPeer); void listMessageChanged(HistoryItem *from, HistoryItem *to); [[nodiscard]] int recentSublistsListVersion() const; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index e4e420a75e..29bf6d5a13 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -480,6 +480,15 @@ void SavedSublist::setUnreadCount(std::optional<int> count) { } } +void SavedSublist::setUnreadMark(bool unread) { + if (unreadMark() == unread) { + return; + } + const auto notifier = unreadStateChangeNotifier( + !unreadCountCurrent()); + Thread::setUnreadMarkFlag(unread); +} + bool SavedSublist::unreadCountKnown() const { return !inMonoforum() || _unreadCount.current().has_value(); } @@ -620,6 +629,9 @@ void SavedSublist::readTill( if (!IsServerMsgId(tillId)) { return; } + if (unreadMark()) { + owner().histories().changeSublistUnreadMark(this, false); + } const auto was = computeInboxReadTillFull(); const auto now = tillId; if (now < was) { @@ -713,6 +725,7 @@ void SavedSublist::applyMonoforumDialog( data.vread_inbox_max_id().v, data.vunread_count().v); setOutboxReadTill(data.vread_outbox_max_id().v); + setUnreadMark(data.is_unread_mark()); applyMaybeLast(topItem); } @@ -1016,10 +1029,16 @@ Dialogs::UnreadState SavedSublist::unreadStateFor( int count, bool known) const { auto result = Dialogs::UnreadState(); + const auto mark = !count && unreadMark(); const auto muted = this->muted(); result.messages = count; result.chats = count ? 1 : 0; + result.marks = mark ? 1 : 0; + result.reactions = unreadReactions().has() ? 1 : 0; + result.messagesMuted = muted ? result.messages : 0; result.chatsMuted = muted ? result.chats : 0; + result.marksMuted = muted ? result.marks : 0; + result.reactionsMuted = muted ? result.reactions : 0; result.known = known; return result; } diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 0708e1a7a0..095fdacf1e 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -58,13 +58,13 @@ public: [[nodiscard]] rpl::producer<> changes() const; [[nodiscard]] std::optional<int> fullCount() const; [[nodiscard]] rpl::producer<int> fullCountValue() const; - [[nodiscard]] rpl::producer<std::optional<int>> maybeFullCount() const; void loadFullCount(); [[nodiscard]] bool unreadCountKnown() const; [[nodiscard]] int unreadCountCurrent() const; [[nodiscard]] int displayedUnreadCount() const; [[nodiscard]] rpl::producer<std::optional<int>> unreadCountValue() const; + void setUnreadMark(bool unread); void applyMonoforumDialog( const MTPDmonoForumDialog &dialog, diff --git a/Telegram/SourceFiles/data/data_thread.cpp b/Telegram/SourceFiles/data/data_thread.cpp index dcf9b85f51..702aed3639 100644 --- a/Telegram/SourceFiles/data/data_thread.cpp +++ b/Telegram/SourceFiles/data/data_thread.cpp @@ -95,6 +95,17 @@ HistoryUnreadThings::ConstProxy Thread::unreadReactions() const { }; } +bool Thread::canToggleUnread(bool nowUnread) const { + if ((asTopic() || asForum()) && !nowUnread) { + return false; + } else if (asSublist() && owningHistory()->peer->isSelf()) { + return false; + } else if (asHistory() && peer()->amMonoforumAdmin()) { + return false; + } + return true; +} + const base::flat_set<MsgId> &Thread::unreadMentionsIds() const { if (!_unreadThings) { static const auto Result = base::flat_set<MsgId>(); diff --git a/Telegram/SourceFiles/data/data_thread.h b/Telegram/SourceFiles/data/data_thread.h index 74462a1944..09a33f27da 100644 --- a/Telegram/SourceFiles/data/data_thread.h +++ b/Telegram/SourceFiles/data/data_thread.h @@ -80,6 +80,7 @@ public: [[nodiscard]] HistoryUnreadThings::ConstProxy unreadReactions() const; virtual void hasUnreadMentionChanged(bool has) = 0; virtual void hasUnreadReactionChanged(bool has) = 0; + bool canToggleUnread(bool nowUnread) const; void removeNotification(not_null<HistoryItem*> item); void clearNotifications(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp index 747cb519af..f863378136 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.cpp @@ -287,6 +287,10 @@ void Entry::notifyUnreadStateChange(const UnreadState &wasState) { } } } + } else if (const auto sublist = asSublist()) { + session().changes().sublistUpdated( + sublist, + Data::SublistUpdate::Flag::UnreadView); } updateChatListEntryPostponed(); } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 3f4540c60c..fc99f35daf 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -398,14 +398,18 @@ void History::applyCloudDraft(MsgId topicRootId, PeerId monoforumPeerId) { createLocalDraftFromCloud(topicRootId, monoforumPeerId); if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { thread->updateChatListSortPosition(); - if (!topicRootId) { - session().changes().historyUpdated( - this, - UpdateFlag::CloudDraft); - } else { + if (topicRootId) { session().changes().topicUpdated( thread->asTopic(), Data::TopicUpdate::Flag::CloudDraft); + } else if (monoforumPeerId) { + session().changes().sublistUpdated( + thread->asSublist(), + Data::SublistUpdate::Flag::CloudDraft); + } else { + session().changes().historyUpdated( + this, + UpdateFlag::CloudDraft); } } } @@ -633,6 +637,20 @@ void History::destroyMessagesByTopic(MsgId topicRootId) { } } +void History::destroyMessagesBySublist(not_null<PeerData*> sublistPeer) { + auto toDestroy = std::vector<not_null<HistoryItem*>>(); + toDestroy.reserve(_items.size()); + const auto peerId = sublistPeer->id; + for (const auto &message : _items) { + if (message->sublistPeerId() == peerId) { + toDestroy.push_back(message.get()); + } + } + for (const auto item : toDestroy) { + item->destroy(); + } +} + void History::unpinMessagesFor(MsgId topicRootId) { if (!topicRootId) { session().storage().remove( diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 4a009a4d20..e58ed77a1a 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -139,6 +139,7 @@ public: void destroyMessage(not_null<HistoryItem*> item); void destroyMessagesByDates(TimeId minDate, TimeId maxDate); void destroyMessagesByTopic(MsgId topicRootId); + void destroyMessagesBySublist(not_null<PeerData*> sublistPeer); void unpinMessagesFor(MsgId topicRootId); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 933de0d20c..89743aa851 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -82,6 +82,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_document.h" #include "data/data_channel.h" @@ -4963,9 +4964,17 @@ auto HistoryInner::DelegateMixin() bool CanSendReply(not_null<const HistoryItem*> item) { const auto peer = item->history()->peer; - const auto topic = item->topic(); - return topic - ? Data::CanSendAnything(topic) - : (Data::CanSendAnything(peer) - && (!peer->isChannel() || peer->asChannel()->amIn())); + if (const auto topic = item->topic()) { + return Data::CanSendAnything(topic); + } else if (!Data::CanSendAnything(peer)) { + return false; + } else if (const auto channel = peer->asChannel()) { + if (const auto sublist = item->savedSublist()) { + if (sublist->sublistPeer() == peer) { + return false; + } + } + return channel->amIn(); + } + return true; } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 672b322412..cc6e4e8bb1 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2151,9 +2151,13 @@ void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) { } } if (reaction) { + const auto sublist = this->savedSublist(); const auto toHistory = history->unreadReactions().add(id, type); const auto toTopic = topic && topic->unreadReactions().add(id, type); - if (toHistory || toTopic) { + const auto toSublist = sublist + && sublist->parentChat() + && sublist->unreadReactions().add(id, type); + if (toHistory || toTopic || toSublist) { if (type == HistoryUnreadThings::AddType::New) { changes->messageUpdated( this, @@ -2170,6 +2174,11 @@ void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) { topic, Data::TopicUpdate::Flag::UnreadReactions); } + if (toSublist) { + changes->sublistUpdated( + sublist, + Data::SublistUpdate::Flag::UnreadReactions); + } } } } diff --git a/Telegram/SourceFiles/history/history_unread_things.cpp b/Telegram/SourceFiles/history/history_unread_things.cpp index 4cca97437e..1ba1678f3a 100644 --- a/Telegram/SourceFiles/history/history_unread_things.cpp +++ b/Telegram/SourceFiles/history/history_unread_things.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_unread_things.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -38,6 +39,12 @@ template <typename Update> return UpdateFlag<Data::TopicUpdate>(type); } +[[nodiscard]] Data::SublistUpdate::Flag SublistUpdateFlag(Type type) { + Expects(type == Type::Reactions); + + return Data::SublistUpdate::Flag::UnreadReactions; +} + } // namespace void Proxy::setCount(int count) { @@ -224,6 +231,10 @@ void Proxy::notifyUpdated() { topic->session().changes().topicUpdated( topic, TopicUpdateFlag(_type)); + } else if (const auto sublist = _thread->asSublist()) { + sublist->session().changes().sublistUpdated( + sublist, + SublistUpdateFlag(_type)); } } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp index 6b62e06a93..05ad41d477 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -366,8 +366,9 @@ void Item::setupTop() { ? nullptr : Ui::CreateChild<Ui::UserpicButton>( _top.get(), - _thread->peer(), - st::previewUserpic); + _thread->peer()->userpicPaintingPeer(), + st::previewUserpic, + _thread->peer()->userpicForceForumShape()); if (userpic) { userpic->showSavedMessagesOnSelf(true); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index cbd9d6c8a4..32ef2bf028 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -47,6 +47,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/delete_messages_box.h" #include "boxes/send_files_box.h" #include "boxes/premium_limits_box.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "base/call_delayed.h" @@ -58,6 +59,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "main/main_session_settings.h" #include "data/components/scheduled_messages.h" +#include "data/data_histories.h" #include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -619,9 +621,7 @@ void ChatWidget::subscribeToTopic() { _topic->destroyed( ) | rpl::start_with_next([=] { - controller()->showBackFromStack(Window::SectionShow( - anim::type::normal, - anim::activation::background)); + closeCurrent(); }, _topicLifetime); if (!_topic->creating()) { @@ -635,6 +635,17 @@ void ChatWidget::subscribeToTopic() { _cornerButtons.updateUnreadThingsVisibility(); } +void ChatWidget::closeCurrent() { + const auto thread = controller()->windowId().chat(); + if ((_sublist && thread == _sublist) || (_topic && thread == _topic)) { + controller()->window().close(); + } else { + controller()->showBackFromStack(Window::SectionShow( + anim::type::normal, + anim::activation::background)); + } +} + void ChatWidget::subscribeToPinnedMessages() { using EntryUpdateFlag = Data::EntryUpdate::Flag; session().changes().entryUpdates( @@ -2496,9 +2507,7 @@ void ChatWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { refreshUnreadCountBadge(count); }, lifetime()); - refreshUnreadCountBadge(_replies->unreadCountKnown() - ? _replies->unreadCountCurrent() - : std::optional<int>()); + unreadCountUpdated(); const auto isTopic = (_topic != nullptr); const auto isTopicCreating = isTopic && _topic->creating(); @@ -2533,14 +2542,62 @@ void ChatWidget::setReplies(std::shared_ptr<Data::RepliesList> replies) { void ChatWidget::subscribeToSublist() { Expects(_sublist != nullptr); + // Must be done before unreadCountUpdated(), or we auto-close. + if (_sublist->unreadMark()) { + _sublist->owner().histories().changeSublistUnreadMark( + _sublist, + false); + } + _sublist->unreadCountValue( ) | rpl::start_with_next([=](std::optional<int> count) { refreshUnreadCountBadge(count); }, lifetime()); - refreshUnreadCountBadge(_sublist->unreadCountKnown() - ? _sublist->unreadCountCurrent() - : std::optional<int>()); + using Flag = Data::SublistUpdate::Flag; + session().changes().sublistUpdates( + _sublist, + Flag::UnreadView | Flag::UnreadReactions | Flag::CloudDraft + ) | rpl::start_with_next([=](const Data::SublistUpdate &update) { + if (update.flags & Flag::UnreadView) { + unreadCountUpdated(); + } + if (update.flags & Flag::UnreadReactions) { + _cornerButtons.updateUnreadThingsVisibility(); + } + if (update.flags & Flag::CloudDraft) { + _composeControls->applyCloudDraft(); + } + }, lifetime()); + + _sublist->destroyed( + ) | rpl::start_with_next([=] { + closeCurrent(); + }, lifetime()); + + unreadCountUpdated(); +} + +void ChatWidget::unreadCountUpdated() { + if (_sublist && _sublist->unreadMark()) { + crl::on_main(this, [=] { + const auto guard = Ui::MakeWeak(this); + controller()->showPeerHistory(_sublist->owningHistory()); + if (guard) { + closeCurrent(); + } + }); + } else { + refreshUnreadCountBadge(_replies + ? (_replies->unreadCountKnown() + ? _replies->unreadCountCurrent() + : std::optional<int>()) + : _sublist + ? (_sublist->unreadCountKnown() + ? _sublist->unreadCountCurrent() + : std::optional<int>()) + : std::optional<int>()); + } } void ChatWidget::restoreState(not_null<ChatMemento*> memento) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index f62beb39d5..52bc50f832 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -241,6 +241,8 @@ private: int limitAfter); void onScroll(); + void closeCurrent(); + void unreadCountUpdated(); void updateInnerVisibleArea(); void updateControlsGeometry(); void updateAdaptiveLayout(); diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index f8d664352b..dec9438836 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -590,9 +590,12 @@ void SubsectionTabs::refreshSlice() { }); const auto push = [&](not_null<Data::Thread*> thread) { const auto topic = thread->asTopic(); + const auto sublist = thread->asSublist(); slice.push_back({ .thread = thread, - .badges = thread->chatListBadgesState(), + .badges = ((topic || sublist) + ? thread->chatListBadgesState() + : Dialogs::BadgesState()), .iconId = topic ? topic->iconId() : DocumentId(), .name = thread->chatListName(), }); diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index b825a2b19e..8273f9604e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -940,7 +940,7 @@ void TopBarWidget::refreshInfoButton() { Ui::UserpicButton::Role::Custom, Ui::UserpicButton::Source::PeerPhoto, st::topBarInfoButton, - infoPeer->monoforumBroadcast() != nullptr); + infoPeer->userpicForceForumShape()); info->showSavedMessagesOnSelf(true); _info.destroy(); _info = std::move(info); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 2393e6c370..84786a9ad0 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -632,7 +632,7 @@ Cover::Cover( Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, _st.photo, - _peer->monoforumBroadcast() != nullptr)) + _peer->userpicForceForumShape())) , _changePersonal((role == Role::Info || topic || !_peer->isUser() diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 967ad72fa6..fa7e2c70f7 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -202,10 +202,12 @@ UserpicButton::UserpicButton( UserpicButton::UserpicButton( QWidget *parent, not_null<PeerData*> peer, - const style::UserpicButton &st) + const style::UserpicButton &st, + bool forceForumShape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _peer(peer) +, _forceForumShape(forceForumShape) , _role(Role::Custom) , _source(Source::PeerPhoto) { Expects(_role != Role::OpenPhoto); diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index 6bd96f330e..959f980333 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -74,7 +74,8 @@ public: UserpicButton( QWidget *parent, not_null<PeerData*> peer, // Role::Custom, Source::PeerPhoto - const style::UserpicButton &st); + const style::UserpicButton &st, + bool forceForumShape = false); ~UserpicButton(); enum class ChosenType { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 026b1246dc..84f738b55a 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -499,6 +499,8 @@ void Filler::addTogglePin() { && !_sublist && !_topic) { return; + } else if (_sublist && !_peer->isSelf()) { + return; } const auto controller = _controller; const auto filterId = _request.filterId; @@ -526,7 +528,7 @@ void Filler::addTogglePin() { } void Filler::addToggleMuteSubmenu(bool addSeparator) { - if (!_thread || _thread->peer()->isSelf()) { + if (!_thread || _thread->peer()->isSelf() || _thread->asSublist()) { return; } PeerMenuAddMuteSubmenuAction(_controller, _thread, _addAction); @@ -550,16 +552,18 @@ void Filler::addSupportInfo() { } void Filler::addInfo() { - if (_peer - && (_peer->isSelf() - || _peer->isRepliesChat() - || _peer->isVerifyCodes())) { + const auto sublist = _thread ? _thread->asSublist() : nullptr; + const auto infoPeer = sublist ? sublist->sublistPeer().get() : _peer; + if (infoPeer + && (infoPeer->isSelf() + || infoPeer->isRepliesChat() + || infoPeer->isVerifyCodes())) { return; } else if (!_thread) { return; } else if (_controller->adaptive().isThreeColumn()) { const auto thread = _controller->activeChatCurrent().thread(); - if (thread && thread == _thread) { + if (thread && !thread->asSublist() && thread == _thread) { if (Core::App().settings().thirdSectionInfoEnabled() || Core::App().settings().tabbedReplacedWithInfo()) { return; @@ -570,16 +574,16 @@ void Filler::addInfo() { const auto weak = base::make_weak(_thread); const auto text = _thread->asTopic() ? tr::lng_context_view_topic(tr::now) - : (_peer->isChat() || _peer->isMegagroup()) + : (infoPeer->isChat() || infoPeer->isMegagroup()) ? tr::lng_context_view_group(tr::now) - : _peer->isUser() + : infoPeer->isUser() ? tr::lng_context_view_profile(tr::now) : tr::lng_context_view_channel(tr::now); _addAction(text, [=] { if (const auto strong = weak.get()) { controller->showPeerInfo(strong); } - }, _peer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); + }, infoPeer->isUser() ? &st::menuIconProfile : &st::menuIconInfo); } void Filler::addStoryArchive() { @@ -624,12 +628,9 @@ void Filler::addToggleFolder() { void Filler::addToggleUnreadMark() { const auto peer = _peer; - const auto history = _request.key.history(); - if (!_thread) { - return; - } const auto unread = IsUnreadThread(_thread); - if ((_thread->asTopic() || peer->isForum()) && !unread) { + const auto history = _request.key.history(); + if (!_thread || !_thread->canToggleUnread(unread)) { return; } const auto weak = base::make_weak(_thread); @@ -643,6 +644,8 @@ void Filler::addToggleUnreadMark() { } if (unread) { MarkAsReadThread(thread); + } else if (const auto sublist = thread->asSublist()) { + peer->owner().histories().changeSublistUnreadMark(sublist, true); } else if (history) { peer->owner().histories().changeDialogUnreadMark(history, true); } @@ -751,14 +754,16 @@ void Filler::addClearHistory() { } void Filler::addDeleteChat() { - if (_topic || _peer->isChannel()) { + if (_topic || (!_sublist && _peer->isChannel())) { return; } _addAction({ - .text = (_peer->isUser() + .text = ((_peer->isUser() || _sublist) ? tr::lng_profile_delete_conversation(tr::now) : tr::lng_profile_clear_and_exit(tr::now)), - .handler = DeleteAndLeaveHandler(_controller, _peer), + .handler = (_sublist + ? DeleteSublistHandler(_controller, _sublist) + : DeleteAndLeaveHandler(_controller, _peer)), .icon = &st::menuIconDeleteAttention, .isAttention = true, }); @@ -766,7 +771,7 @@ void Filler::addDeleteChat() { void Filler::addLeaveChat() { const auto channel = _peer->asChannel(); - if (_topic || !channel || !channel->amIn()) { + if (_topic || _sublist || !channel || !channel->amIn()) { return; } _addAction({ @@ -1263,7 +1268,7 @@ void Filler::addSendGift() { void Filler::fill() { if (_folder) { fillArchiveActions(); - } else if (_sublist) { + } else if (_sublist && _peer->isSelf()) { fillSavedSublistActions(); } else switch (_request.section) { case Section::ChatsList: fillChatsListActions(); break; @@ -3232,6 +3237,19 @@ Fn<void()> DeleteAndLeaveHandler( }; } +Fn<void()> DeleteSublistHandler( + not_null<Window::SessionController*> controller, + not_null<Data::SavedSublist*> sublist) { + const auto weak = base::make_weak(sublist.get()); + return [=] { + if (const auto strong = weak.get()) { + if (!controller->showFrozenError()) { + controller->show(Box(DeleteSublistBox, strong)); + } + } + }; +} + void FillDialogsEntryMenu( not_null<SessionController*> controller, Dialogs::EntryState request, @@ -3345,8 +3363,7 @@ void MarkAsReadThread(not_null<Data::Thread*> thread) { if (!IsUnreadThread(thread)) { return; } else if (const auto forum = thread->asForum()) { - forum->enumerateTopics([]( - not_null<Data::ForumTopic*> topic) { + forum->enumerateTopics([](not_null<Data::ForumTopic*> topic) { MarkAsReadThread(topic); }); } else if (const auto history = thread->asHistory()) { @@ -3356,6 +3373,8 @@ void MarkAsReadThread(not_null<Data::Thread*> thread) { } } else if (const auto topic = thread->asTopic()) { topic->readTillEnd(); + } else if (const auto sublist = thread->asSublist()) { + sublist->readTillEnd(); } } diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index fbc677bdb6..06c5ce626c 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -148,6 +148,9 @@ Fn<void()> ClearHistoryHandler( Fn<void()> DeleteAndLeaveHandler( not_null<Window::SessionController*> controller, not_null<PeerData*> peer); +Fn<void()> DeleteSublistHandler( + not_null<Window::SessionController*> controller, + not_null<Data::SavedSublist*> sublist); object_ptr<Ui::BoxContent> PrepareChooseRecipientBox( not_null<Main::Session*> session, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 5e123fd458..6b41f48329 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1309,6 +1309,9 @@ void SessionNavigation::showPeerInfo( const SectionShow ¶ms) { if (const auto topic = thread->asTopic()) { showSection(std::make_shared<Info::Memento>(topic), params); + } else if (const auto sublist = thread->asSublist() + ; sublist && sublist->parentChat()) { + showPeerInfo(sublist->sublistPeer()->id, params); } else { showPeerInfo(thread->peer()->id, params); } From 0d43f16db23d67c41f84734d8359497e946fe4cd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 30 May 2025 16:26:06 +0400 Subject: [PATCH 092/310] Remove unsupported actions from monoforum menu. --- Telegram/SourceFiles/boxes/moderate_messages_box.cpp | 8 +++++--- Telegram/SourceFiles/data/data_channel.cpp | 7 ++++++- Telegram/SourceFiles/data/data_peer.cpp | 9 ++++++++- Telegram/SourceFiles/window/window_peer_menu.cpp | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index ae25f7a687..29f7a22c0e 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -591,6 +591,7 @@ void SafeSubmitOnEnter(not_null<Ui::GenericBox*> box) { void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { const auto container = box->verticalLayout(); + const auto userpicPeer = peer->userpicPaintingPeer(); const auto maybeUser = peer->asUser(); const auto isBot = maybeUser && maybeUser->isBot(); @@ -601,8 +602,9 @@ void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { const auto userpic = Ui::CreateChild<Ui::UserpicButton>( container, - peer, - st::mainMenuUserpic); + userpicPeer, + st::mainMenuUserpic, + peer->userpicForceForumShape()); userpic->showSavedMessagesOnSelf(true); Ui::IconWithTitle( container, @@ -614,7 +616,7 @@ void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { : maybeUser ? tr::lng_profile_delete_conversation() | Ui::Text::ToBold() : rpl::single( - peer->name() + userpicPeer->name() ) | Ui::Text::ToBold() | rpl::type_erased(), box->getDelegate()->style().title)); diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 43182a6ad6..8365be635b 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -652,6 +652,9 @@ bool ChannelData::canPostStories() const { } bool ChannelData::canEditStories() const { + if (isMonoforum()) { + return false; + } return amCreator() || (adminRights() & AdminRight::EditStories); } @@ -678,7 +681,9 @@ bool ChannelData::hiddenPreHistory() const { } bool ChannelData::canAddMembers() const { - return isMegagroup() + return isMonoforum() + ? false + : isMegagroup() ? !amRestricted(ChatRestriction::AddParticipants) : ((adminRights() & AdminRight::InviteByLinkOrAdd) || amCreator()); } diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 4556022257..7af00bad92 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1206,6 +1206,8 @@ int PeerData::nameVersion() const { const QString &PeerData::name() const { if (const auto to = migrateTo()) { return to->name(); + } else if (const auto broadcast = monoforumBroadcast()) { + return broadcast->name(); } return _name; } @@ -1213,6 +1215,10 @@ const QString &PeerData::name() const { const QString &PeerData::shortName() const { if (const auto user = asUser()) { return user->firstName.isEmpty() ? user->lastName : user->firstName; + } else if (const auto to = migrateTo()) { + return to->shortName(); + } else if (const auto broadcast = monoforumBroadcast()) { + return broadcast->shortName(); } return _name; } @@ -1554,7 +1560,8 @@ bool PeerData::canRevokeFullHistory() const { } else if (const auto megagroup = asMegagroup()) { return megagroup->amCreator() && megagroup->membersCountKnown() - && megagroup->canDelete(); + && megagroup->canDelete() + && !megagroup->isMonoforum(); } return false; } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 84f738b55a..0f48fb48f7 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1087,6 +1087,9 @@ void Filler::addManageChat() { void Filler::addBoostChat() { if (const auto channel = _peer->asChannel()) { + if (channel->isMonoforum()) { + return; + } const auto text = channel->isMegagroup() ? tr::lng_boost_group_button(tr::now) : tr::lng_boost_channel_button(tr::now); @@ -1101,6 +1104,9 @@ void Filler::addBoostChat() { void Filler::addViewStatistics() { if (const auto channel = _peer->asChannel()) { + if (channel->isMonoforum()) { + return; + } const auto controller = _controller; const auto weak = base::make_weak(_thread); const auto peer = _peer; @@ -1219,7 +1225,7 @@ void Filler::addThemeEdit() { } void Filler::addTTLSubmenu(bool addSeparator) { - if (_thread->asTopic()) { + if (_thread->asTopic() || !_peer || _peer->isMonoforum()) { return; // #TODO later forum } const auto validator = TTLMenu::TTLValidator( @@ -1346,6 +1352,7 @@ void Filler::addViewAsMessages() { void Filler::addViewAsTopics() { if (!_peer || !_peer->isForum() + || (_peer->asChannel()->flags() & ChannelDataFlag::ForumTabs) || !_controller->adaptive().isOneColumn()) { return; } From 50b761fab2dc704c2dd9a3bb93d6b16bdb8934b1 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 30 May 2025 17:40:09 +0400 Subject: [PATCH 093/310] Remove showing monoforums inside dialogs widget. --- .../dialogs/dialogs_inner_widget.cpp | 48 +----- .../dialogs/dialogs_inner_widget.h | 3 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 150 ++---------------- Telegram/SourceFiles/dialogs/dialogs_widget.h | 14 +- .../view/history_view_top_bar_widget.cpp | 4 - .../info/saved/info_saved_sublists_widget.cpp | 2 +- Telegram/SourceFiles/mainwidget.cpp | 12 -- Telegram/SourceFiles/mainwidget.h | 3 - .../window/window_session_controller.cpp | 61 +------ .../window/window_session_controller.h | 8 - Telegram/lib_ui | 2 +- 11 files changed, 24 insertions(+), 283 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 8775b85569..344bebd455 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -781,55 +781,11 @@ void InnerWidget::changeOpenedForum(Data::Forum *forum) { } } -void InnerWidget::changeOpenedMonoforum(Data::SavedMessages *monoforum) { - if (_savedSublists == monoforum) { - return; - } - stopReorderPinned(); - clearSelection(); - - if (monoforum) { - saveChatsFilterScrollState(_filterId); - } - _filterId = monoforum - ? 0 - : _controller->activeChatsFilterCurrent(); - if (_openedForum) { - // If we close it inside forum destruction we should not schedule. - session().data().forumIcons().scheduleUserpicsReset(_openedForum); - } - _savedSublists = monoforum; - _st = &st::defaultDialogRow; - refreshShownList(); - - _openedForumLifetime.destroy(); - if (monoforum) { - rpl::merge( - monoforum->chatsListChanges(), - monoforum->chatsListLoadedEvents() - ) | rpl::start_with_next([=] { - refresh(); - }, _openedForumLifetime); - } - - refreshWithCollapsedRows(true); - if (_loadMoreCallback) { - _loadMoreCallback(); - } - - if (!monoforum) { - restoreChatsFilterScrollState(_filterId); - } -} - -void InnerWidget::showSavedSublists(ChannelData *parentChat) { - Expects(!parentChat || parentChat->monoforum()); +void InnerWidget::showSavedSublists() { Expects(!_geometryInited); Expects(!_savedSublists); - _savedSublists = parentChat - ? parentChat->monoforum() - : &session().data().savedMessages(); + _savedSublists = &session().data().savedMessages(); stopReorderPinned(); clearSelection(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index f840a99b43..d2a21405cb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -141,8 +141,7 @@ public: void changeOpenedFolder(Data::Folder *folder); void changeOpenedForum(Data::Forum *forum); - void changeOpenedMonoforum(Data::SavedMessages *monoforum); - void showSavedSublists(ChannelData *parentChat); + void showSavedSublists(); void selectSkip(int32 direction); void selectSkipPage(int32 pixels, int32 direction); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 4ecbf32115..06b1ac9294 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -79,7 +79,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_download_manager.h" #include "data/data_chat_filters.h" -#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" @@ -420,8 +419,6 @@ Widget::Widget( ) | rpl::filter([=](const Data::HistoryUpdate &update) { if (_openedForum) { return (update.history == _openedForum->history()); - } else if (_openedMonoforum) { - return (update.history->peer == _openedMonoforum->parentChat()); } else if (_openedFolder) { return (update.history->folder() == _openedFolder) && !update.history->isPinnedDialog(FilterId()); @@ -600,7 +597,6 @@ Widget::Widget( _search->setFocusFast(); if (_childList) { controller->closeForum(); - controller->closeMonoforum(); } }); @@ -623,8 +619,6 @@ Widget::Widget( searchMore(); } else if (_openedForum && state == WidgetState::Default) { _openedForum->requestTopics(); - } else if (_openedMonoforum && state == WidgetState::Default) { - _openedMonoforum->loadMore(); } else { const auto folder = _inner->shownFolder(); if (!folder || !folder->chatsList()->loaded()) { @@ -673,16 +667,7 @@ Widget::Widget( ) | rpl::filter(!rpl::mappers::_1) | rpl::start_with_next([=] { if (_openedForum) { changeOpenedForum(nullptr, anim::type::normal); - } else if (_childList && !_childList->openedMonoforum()) { - closeChildList(anim::type::normal); - } - }, lifetime()); - - controller->shownMonoforum().changes( - ) | rpl::filter(!rpl::mappers::_1) | rpl::start_with_next([=] { - if (_openedMonoforum) { - changeOpenedMonoforum(nullptr, anim::type::normal); - } else if (_childList && !_childList->openedForum()) { + } else if (_childList) { closeChildList(anim::type::normal); } }, lifetime()); @@ -811,7 +796,7 @@ void Widget::setupSwipeBack() { } }); } - if (controller()->shownForum().current()) { // #TODO monoforum + if (controller()->shownForum().current()) { if (!isRightToLeft) { return Ui::Controls::SwipeHandlerFinishData(); } @@ -932,7 +917,8 @@ void Widget::chosenRow(const ChosenRow &row) { controller()->showForum( forum, Window::SectionShow().withChildColumn()); - if (forum->channel()->viewForumAsMessages()) { + if (controller()->shownForum().current() == forum + && forum->channel()->viewForumAsMessages()) { controller()->showThread( history, ShowAtUnreadMsgId, @@ -940,27 +926,6 @@ void Widget::chosenRow(const ChosenRow &row) { } } return; - //} else if (history - // && history->peer->amMonoforumAdmin() - // && !row.message.fullId) { - // const auto monoforum = history->peer->monoforum(); - // if (controller()->shownMonoforum().current() == monoforum) { - // controller()->closeMonoforum(); - // //} else if (row.newWindow) { // #TODO monoforum - // // controller()->showInNewWindow( - // // Window::SeparateId(Window::SeparateType::Forum, history)); - // } else { - // controller()->showMonoforum( - // monoforum, - // Window::SectionShow().withChildColumn()); - // if (!controller()->adaptive().isOneColumn()) { - // controller()->showThread( - // history, - // ShowAtUnreadMsgId, - // Window::SectionShow::Way::ClearStack); - // } - // } - // return; } else if (history) { const auto peer = history->peer; const auto showAtMsgId = controller()->uniqueChatsInSearchResults() @@ -995,21 +960,6 @@ void Widget::chosenRow(const ChosenRow &row) { } controller()->openFolder(folder); hideChildList(); - } else if (const auto sublist = row.key.sublist()) { - using namespace Window; - auto params = SectionShow(SectionShow::Way::Forward); - params.dropSameFromStack = true; - params.highlightPart.text = _searchState.query; - if (!params.highlightPart.empty()) { - params.highlightPartOffsetHint = kSearchQueryOffsetHint; - } - if (false && row.newWindow) { // #TODO monoforum - controller()->showInNewWindow( - Window::SeparateId(sublist), - row.message.fullId.msg); - } else { - controller()->showThread(sublist, row.message.fullId.msg, params); - } } if (row.filteredRow && !session().supportMode()) { if (_subsectionTopBar) { @@ -1084,8 +1034,7 @@ void Widget::setupTopBarSuggestions(not_null<Ui::VerticalLayout*> dialogs) { ) | rpl::filter(_1 == nullptr) | rpl::map([=] { auto on = rpl::combine( controller()->activeChatsFilter(), - _openedFolderOrForumOrMonoforumChanges.events_starting_with( - false), + _openedFolderOrForumChanges.events_starting_with(false), widthValue() | rpl::map( _1 >= st::columnMinimalWidthLeft ) | rpl::distinct_until_changed(), @@ -1094,11 +1043,11 @@ void Widget::setupTopBarSuggestions(not_null<Ui::VerticalLayout*> dialogs) { _jumpToDate->toggledValue() ) | rpl::map([=]( FilterId id, - bool folderOrForumOrMonoforum, + bool folderOrForum, bool wide, bool search, bool searchInPeer) { - return !folderOrForumOrMonoforum + return !folderOrForum && wide && !search && !searchInPeer @@ -1155,8 +1104,7 @@ void Widget::updateFrozenAccountBar() { void Widget::updateTopBarSuggestions() { if (_topBarSuggestion) { - _openedFolderOrForumOrMonoforumChanges.fire( - _openedFolder || _openedForum || _openedMonoforum); + _openedFolderOrForumChanges.fire(_openedFolder || _openedForum); } } @@ -1594,7 +1542,7 @@ void Widget::updateControlsVisibility(bool fast) { if (_chatFilters) { _chatFilters->setVisible(!_openedForum); } - if (_openedFolder || _openedForum || _openedMonoforum) { + if (_openedFolder || _openedForum) { _subsectionTopBar->show(); if (_forumTopShadow) { _forumTopShadow->show(); @@ -1973,29 +1921,6 @@ void Widget::changeOpenedForum(Data::Forum *forum, anim::type animated) { }, (forum != nullptr), animated); } -void Widget::changeOpenedMonoforum( - Data::SavedMessages *monoforum, - anim::type animated) { - if (_openedMonoforum == monoforum) { - return; - } - changeOpenedSubsection([&] { - cancelSearch({ .forceFullCancel = true }); - closeChildList(anim::type::instant); - _openedMonoforum = monoforum; - _searchState.tab = monoforum - ? ChatSearchTab::ThisPeer - : ChatSearchTab::MyMessages; - _searchWithPostsPreview = computeSearchWithPostsPreview(); - _api.request(base::take(_topicSearchRequest)).cancel(); - _inner->changeOpenedMonoforum(monoforum); - storiesToggleExplicitExpand(false); - updateFrozenAccountBar(); - updateTopBarSuggestions(); - updateStoriesVisibility(); - }, (monoforum != nullptr), animated); -} - void Widget::hideChildList() { if (_childList) { controller()->closeForum(); @@ -2003,7 +1928,7 @@ void Widget::hideChildList() { } void Widget::refreshTopBars() { - if (_openedFolder || _openedForum || _openedMonoforum) { + if (_openedFolder || _openedForum) { if (!_subsectionTopBar) { _subsectionTopBar.create(this, controller()); if (_stories) { @@ -2033,12 +1958,10 @@ void Widget::refreshTopBars() { } const auto history = _openedForum ? _openedForum->history().get() - : _openedMonoforum - ? session().data().history(_openedMonoforum->parentChat()).get() : nullptr; _subsectionTopBar->setActiveChat( HistoryView::TopBarWidget::ActiveChat{ - .key = ((_openedForum || _openedMonoforum) + .key = (_openedForum ? Dialogs::Key(history) : Dialogs::Key(_openedFolder)), .section = Dialogs::EntryState::Section::ChatsList, @@ -2204,10 +2127,6 @@ Data::Forum *Widget::openedForum() const { return _openedForum; } -Data::SavedMessages *Widget::openedMonoforum() const { - return _openedMonoforum; -} - void Widget::jumpToTop(bool belowPinned) { if (session().supportMode()) { return; @@ -2506,15 +2425,6 @@ void Widget::escape() { } else if (initial != forum) { controller()->showForum(initial); } - } else if (const auto monoforum - = controller()->shownMonoforum().current()) { - const auto id = controller()->windowId(); // #TODO monoforum - const auto initial = (Data::SavedMessages*)nullptr; - if (!initial) { - controller()->closeMonoforum(); - } else if (initial != monoforum) { - controller()->showMonoforum(initial); - } } else if (controller()->openedFolder().current()) { if (!controller()->windowId().folder()) { controller()->closeFolder(); @@ -3379,33 +3289,12 @@ void Widget::showForum( return; } cancelSearch({ .forceFullCancel = true }); - openChildList(forum, nullptr, params); -} - -void Widget::showMonoforum( - not_null<Data::SavedMessages*> monoforum, - const Window::SectionShow ¶ms) { - if (_openedMonoforum == monoforum) { - return; - } - const auto nochat = !controller()->mainSectionShown(); - if (!params.childColumn - || (Core::App().settings().dialogsWidthRatio(nochat) == 0.) - || (_layout != Layout::Main) - || OptionForumHideChatsList.value()) { - changeOpenedMonoforum(monoforum, params.animated); - return; - } - cancelSearch({ .forceFullCancel = true }); - openChildList(nullptr, monoforum, params); + openChildList(forum, params); } void Widget::openChildList( - Data::Forum *forum, - Data::SavedMessages *monoforum, + not_null<Data::Forum*> forum, const Window::SectionShow ¶ms) { - Expects(forum || monoforum); - auto slide = Window::SectionSlideParams(); const auto animated = !_childList && (params.animated == anim::type::normal); @@ -3426,13 +3315,8 @@ void Widget::openChildList( this, controller(), Layout::Child); - if (forum) { - _childList->showForum(forum, copy); - _childListPeerId = forum->channel()->id; - } else { - _childList->showMonoforum(monoforum, copy); - _childListPeerId = monoforum->parentChat()->id; - } + _childList->showForum(forum, copy); + _childListPeerId = forum->channel()->id; } _childListShadow = std::make_unique<Ui::RpWidget>(this); @@ -4289,10 +4173,6 @@ PeerData *Widget::searchInPeer() const { ? nullptr : _openedForum ? _openedForum->channel().get() - : _openedMonoforum - ? (_openedMonoforum->parentChat() - ? _openedMonoforum->parentChat() - : (PeerData*)session().user().get()) : _searchState.inChat.sublist() ? _searchState.inChat.sublist()->owningHistory()->peer.get() : _searchState.inChat.peer(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index b2584462f2..ab4685101f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -23,7 +23,6 @@ class Error; namespace Data { class Forum; -class SavedMessages; enum class StorySourcesList : uchar; struct ReactionId; } // namespace Data @@ -109,14 +108,10 @@ public: void showForum( not_null<Data::Forum*> forum, const Window::SectionShow ¶ms); - void showMonoforum( - not_null<Data::SavedMessages*> monoforum, - const Window::SectionShow ¶ms); void setInnerFocus(bool unfocusSearch = false); [[nodiscard]] bool searchHasFocus() const; [[nodiscard]] Data::Forum *openedForum() const; - [[nodiscard]] Data::SavedMessages *openedMonoforum() const; void jumpToTop(bool belowPinned = false); void raiseWithTooltip(); @@ -254,9 +249,6 @@ private: anim::type animated); void changeOpenedFolder(Data::Folder *folder, anim::type animated); void changeOpenedForum(Data::Forum *forum, anim::type animated); - void changeOpenedMonoforum( - Data::SavedMessages *monoforum, - anim::type animated); void hideChildList(); void destroyChildListCanvas(); [[nodiscard]] QPixmap grabForFolderSlideAnimation(); @@ -266,8 +258,7 @@ private: Window::SlideDirection direction); void openChildList( - Data::Forum *forum, - Data::SavedMessages *monoforum, + not_null<Data::Forum*> forum, const Window::SectionShow ¶ms); void closeChildList(anim::type animated); @@ -343,7 +334,7 @@ private: Ui::SlideWrap<Ui::RpWidget> *_topBarSuggestion = nullptr; rpl::event_stream<int> _topBarSuggestionHeightChanged; rpl::event_stream<bool> _searchStateForTopBarSuggestion; - rpl::event_stream<bool> _openedFolderOrForumOrMonoforumChanges; + rpl::event_stream<bool> _openedFolderOrForumChanges; object_ptr<Ui::ElasticScroll> _scroll; QPointer<InnerWidget> _inner; @@ -369,7 +360,6 @@ private: Data::Folder *_openedFolder = nullptr; Data::Forum *_openedForum = nullptr; - Data::SavedMessages *_openedMonoforum = nullptr; SearchState _searchState; History *_searchInMigrated = nullptr; rpl::lifetime _searchTagsLifetime; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 8273f9604e..3cb26ace66 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -773,10 +773,6 @@ void TopBarWidget::backClicked() { && _activeChat.key.history() && _activeChat.key.history()->isForum()) { _controller->closeForum(); - } else if (_activeChat.section == Section::ChatsList - && _activeChat.key.history() - && _activeChat.key.history()->amMonoforumAdmin()) { - _controller->closeMonoforum(); } else { _controller->showBackFromStack(); } diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index 0ab5a23af5..06143d2c13 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -58,7 +58,7 @@ SublistsWidget::SublistsWidget( this, controller->parentController(), rpl::single(Dialogs::InnerWidget::ChildListShown()))); - _list->showSavedSublists(nullptr); + _list->showSavedSublists(); _list->setNarrowRatio(0.); _list->chosenRow() | rpl::start_with_next([=](Dialogs::ChosenRow row) { diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index b759bdcfd4..587172d8f6 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1594,18 +1594,6 @@ void MainWidget::showForum( } } -void MainWidget::showMonoforum( - not_null<Data::SavedMessages*> monoforum, - const SectionShow ¶ms) { - Expects(_dialogs != nullptr); - - _dialogs->showMonoforum(monoforum, params); - - if (params.activation != anim::activation::background) { - _controller->window().hideSettingsAndLayer(); - } -} - PeerData *MainWidget::peer() const { return _history->peer(); } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 54bb67513c..fc9281f9fe 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -199,9 +199,6 @@ public: not_null<const HistoryItem*> item, const SectionShow ¶ms); void showForum(not_null<Data::Forum*> forum, const SectionShow ¶ms); - void showMonoforum( - not_null<Data::SavedMessages*> monoforum, - const SectionShow ¶ms); bool notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 6b41f48329..523777e882 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -605,15 +605,10 @@ void SessionNavigation::showPeerByLinkResolved( showPeerInfo(peer, params); } else if (resolveType == ResolveType::HashtagSearch) { searchMessages(info.text, peer->owner().history(peer)); - } else if ((peer->isForum() || peer->amMonoforumAdmin()) - && resolveType != ResolveType::Boost) { + } else if (peer->isForum() && resolveType != ResolveType::Boost) { const auto itemId = info.messageId; if (!itemId) { - if (peer->isForum()) { - parentController()->showForum(peer->forum(), params); - } else { - parentController()->showMonoforum(peer->monoforum(), params); - } + parentController()->showForum(peer->forum(), params); } else if (const auto item = peer->owner().message(peer, itemId)) { showMessageByLinkResolved(item, info); } else { @@ -1929,7 +1924,6 @@ void SessionController::showForum( } }, _shownForumLifetime); content()->showForum(forum, params); - closeMonoforum(); } void SessionController::closeForum() { @@ -1980,57 +1974,6 @@ const rpl::variable<Data::Forum*> &SessionController::shownForum() const { return _shownForum; } -void SessionController::showMonoforum( - not_null<Data::SavedMessages*> monoforum, - const SectionShow ¶ms) { - // if (showForumInDifferentWindow(forum, params)) { - // return; - // } - _shownMonoforumLifetime.destroy(); - if (_shownMonoforum.current() != monoforum) { - resetFakeUnreadWhileOpened(); - } - if (monoforum - && _activeChatEntry.current().key.peer() - && adaptive().isOneColumn()) { - clearSectionStack(params); - } - _shownMonoforum = monoforum.get(); - if (_shownMonoforum.current() != monoforum) { - return; - } - monoforum->destroyed( - ) | rpl::start_with_next([=] { - closeMonoforum(); - }, _shownMonoforumLifetime); - content()->showMonoforum(monoforum, params); - closeForum(); -} - -void SessionController::closeMonoforum() { - if (const auto monoforum = _shownMonoforum.current()) { - const auto id = windowId(); - if (id.type == SeparateType::SavedSublist) { - const auto initial = id.parentChat; - if (!initial - || !initial->monoforum() - || initial == monoforum->parentChat()) { - Core::App().closeWindow(_window); - } else { - showMonoforum(initial->monoforum()); - } - return; - } - } - _shownMonoforumLifetime.destroy(); - _shownMonoforum = nullptr; -} - -auto SessionController::shownMonoforum() const --> const rpl::variable<Data::SavedMessages*> & { - return _shownMonoforum; -} - void SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) { if (windowId().type == SeparateType::SharedMedia) { return; diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 151fa7de7a..86b1e6f3be 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -415,12 +415,6 @@ public: void closeForum(); const rpl::variable<Data::Forum*> &shownForum() const; - void showMonoforum( - not_null<Data::SavedMessages*> monoforum, - const SectionShow ¶ms = SectionShow::Way::ClearStack); - void closeMonoforum(); - const rpl::variable<Data::SavedMessages*> &shownMonoforum() const; - void setActiveChatEntry(Dialogs::RowDescriptor row); void setActiveChatEntry(Dialogs::Key key); Dialogs::RowDescriptor activeChatEntryCurrent() const; @@ -770,8 +764,6 @@ private: rpl::variable<Data::Folder*> _openedFolder; rpl::variable<Data::Forum*> _shownForum; rpl::lifetime _shownForumLifetime; - rpl::variable<Data::SavedMessages*> _shownMonoforum; - rpl::lifetime _shownMonoforumLifetime; rpl::event_stream<> _filtersMenuChanged; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 369d8b5172..2b0d129b52 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 369d8b5172b6f70a3a9b2363cfa0f4fc5f620f56 +Subproject commit 2b0d129b528bb56a71ebcf1f8c0a4d8b19123e34 From 6068678fa18949231802ca3c11db2b60068087f0 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 30 May 2025 18:33:47 +0400 Subject: [PATCH 094/310] Improve separate window support. --- Telegram/SourceFiles/core/application.cpp | 16 ++++++++++++---- Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 6 ++++-- .../SourceFiles/window/notifications_manager.cpp | 8 +++++--- Telegram/SourceFiles/window/window_peer_menu.cpp | 8 +++++--- .../SourceFiles/window/window_separate_id.cpp | 13 +++---------- Telegram/SourceFiles/window/window_separate_id.h | 6 +----- .../window/window_session_controller.cpp | 3 +-- 7 files changed, 31 insertions(+), 29 deletions(-) diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 593d6f4a0b..53e01dd106 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "data/data_abstract_structure.h" +#include "data/data_channel.h" #include "data/data_forum.h" #include "data/data_message_reactions.h" #include "data/data_session.h" @@ -1377,8 +1378,9 @@ Window::Controller *Application::windowForShowingHistory( Window::Controller *Application::windowForShowingForum( not_null<Data::Forum*> forum) const { + const auto tabs = forum->channel()->useSubsectionTabs(); const auto id = Window::SeparateId( - Window::SeparateType::Forum, + tabs ? Window::SeparateType::Chat : Window::SeparateType::Forum, forum->history()); if (const auto separate = separateWindowFor(id)) { return separate; @@ -1386,9 +1388,15 @@ Window::Controller *Application::windowForShowingForum( auto result = (Window::Controller*)nullptr; enumerateWindows([&](not_null<Window::Controller*> window) { if (const auto controller = window->sessionController()) { - const auto current = controller->shownForum().current(); - if (forum == current) { - result = window; + if (tabs) { + if (controller->windowId() == id) { + result = window; + } + } else { + const auto current = controller->shownForum().current(); + if (forum == current) { + result = window; + } } } }); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 06b1ac9294..6b6227b9a3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -911,8 +911,10 @@ void Widget::chosenRow(const ChosenRow &row) { if (controller()->shownForum().current() == forum) { controller()->closeForum(); } else if (row.newWindow) { - controller()->showInNewWindow( - Window::SeparateId(Window::SeparateType::Forum, history)); + const auto type = forum->channel()->useSubsectionTabs() + ? Window::SeparateType::Chat + : Window::SeparateType::Forum; + controller()->showInNewWindow(Window::SeparateId(type, history)); } else { controller()->showForum( forum, diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index f34c3a1084..0038a41783 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1176,9 +1176,11 @@ Window::SessionController *Manager::openNotificationMessage( } }); - const auto separateId = topic - ? Window::SeparateId(Window::SeparateType::Forum, history) - : Window::SeparateId(history->peer); + const auto separateId = !topic + ? Window::SeparateId(history->peer) + : history->peer->asChannel()->useSubsectionTabs() + ? Window::SeparateId(Window::SeparateType::Chat, topic) + : Window::SeparateId(Window::SeparateType::Forum, history); const auto separate = Core::App().separateWindowFor(separateId); const auto itemId = openExactlyMessage ? messageId : ShowAtUnreadMsgId; if (openSeparated && !separate && !topic) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 0f48fb48f7..477b1b6707 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -669,7 +669,7 @@ void Filler::addNewWindow() { if (const auto sublist = weak.get()) { controller->showInNewWindow(SeparateId( SeparateType::SavedSublist, - sublist->owner().history(sublist->sublistPeer()))); + sublist)); } }, &st::menuIconNewWindow); AddSeparatorAndShiftUp(_addAction); @@ -690,7 +690,9 @@ void Filler::addNewWindow() { _addAction(tr::lng_context_new_window(tr::now), [=] { Ui::PreventDelayedActivation(); if (const auto strong = weak.get()) { - const auto forum = !strong->asTopic() && peer->isForum(); + const auto forum = !strong->asTopic() + && peer->isForum() + && !peer->asChannel()->useSubsectionTabs(); controller->showInNewWindow(SeparateId( forum ? SeparateType::Forum : SeparateType::Chat, strong)); @@ -2472,7 +2474,7 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox( return true; } const auto id = SeparateId( - (peer->isForum() + ((peer->isForum() && !peer->asChannel()->useSubsectionTabs()) ? SeparateType::Forum : SeparateType::Chat), thread); diff --git a/Telegram/SourceFiles/window/window_separate_id.cpp b/Telegram/SourceFiles/window/window_separate_id.cpp index c6b0f0da62..f6ed9155fd 100644 --- a/Telegram/SourceFiles/window/window_separate_id.cpp +++ b/Telegram/SourceFiles/window/window_separate_id.cpp @@ -31,14 +31,10 @@ SeparateId::SeparateId(SeparateType type, not_null<Main::Session*> session) , account(&session->account()) { } -SeparateId::SeparateId( - SeparateType type, - not_null<Data::Thread*> thread, - ChannelData *parentChat) +SeparateId::SeparateId(SeparateType type, not_null<Data::Thread*> thread) : type(type) , account(&thread->session().account()) -, thread(thread) -, parentChat((type == SeparateType::SavedSublist) ? parentChat : nullptr) { +, thread(thread) { } SeparateId::SeparateId(not_null<Data::Thread*> thread) @@ -77,12 +73,9 @@ Data::Folder *SeparateId::folder() const { } Data::SavedSublist *SeparateId::sublist() const { - const auto monoforum = parentChat ? parentChat->monoforum() : nullptr; return (type != SeparateType::SavedSublist) ? nullptr - : monoforum - ? monoforum->sublist(thread->peer()).get() - : thread->owner().savedMessages().sublist(thread->peer()).get(); + : thread->asSublist(); } bool SeparateId::hasChatsList() const { diff --git a/Telegram/SourceFiles/window/window_separate_id.h b/Telegram/SourceFiles/window/window_separate_id.h index e36e8c2a98..dbc79397a5 100644 --- a/Telegram/SourceFiles/window/window_separate_id.h +++ b/Telegram/SourceFiles/window/window_separate_id.h @@ -46,10 +46,7 @@ struct SeparateId { SeparateId(std::nullptr_t); SeparateId(not_null<Main::Account*> account); SeparateId(SeparateType type, not_null<Main::Session*> session); - SeparateId( - SeparateType type, - not_null<Data::Thread*> thread, - ChannelData *parentChat = nullptr); + SeparateId(SeparateType type, not_null<Data::Thread*> thread); SeparateId(not_null<Data::Thread*> thread); SeparateId(not_null<PeerData*> peer); SeparateId( @@ -60,7 +57,6 @@ struct SeparateId { Storage::SharedMediaType sharedMediaType = {}; Main::Account *account = nullptr; Data::Thread *thread = nullptr; // For types except Main and Archive. - ChannelData *parentChat = nullptr; [[nodiscard]] bool valid() const { return account != nullptr; } diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 523777e882..1fe5d152cc 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1877,8 +1877,7 @@ void SessionController::showForum( const SectionShow ¶ms) { if (showForumInDifferentWindow(forum, params)) { return; - } else if (HistoryView::SubsectionTabs::UsedFor( - forum->owner().history(forum->channel()))) { + } else if (forum->channel()->useSubsectionTabs()) { showPeerHistory(forum->channel(), params); return; } From ffe6786ad1a309fbd650edc54ff7d78961391496 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 30 May 2025 21:10:18 +0400 Subject: [PATCH 095/310] Support unread reactions in monoforums. --- Telegram/SourceFiles/api/api_unread_things.cpp | 13 +++++++++---- .../SourceFiles/data/data_saved_messages.cpp | 6 ++++++ Telegram/SourceFiles/data/data_saved_messages.h | 1 + .../SourceFiles/data/data_saved_sublist.cpp | 1 + Telegram/SourceFiles/history/history.cpp | 17 ++++++++++++++--- Telegram/SourceFiles/history/history.h | 4 +++- Telegram/SourceFiles/history/history_item.cpp | 5 +++++ .../history/view/history_view_chat_section.cpp | 6 ++++-- Telegram/SourceFiles/menu/menu_send.cpp | 11 ++++++++--- .../window/notifications_manager.cpp | 13 ++++++------- .../SourceFiles/window/window_peer_menu.cpp | 6 ++++-- 11 files changed, 61 insertions(+), 22 deletions(-) diff --git a/Telegram/SourceFiles/api/api_unread_things.cpp b/Telegram/SourceFiles/api/api_unread_things.cpp index be597c954d..32deab3c88 100644 --- a/Telegram/SourceFiles/api/api_unread_things.cpp +++ b/Telegram/SourceFiles/api/api_unread_things.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "main/main_session.h" #include "history/history.h" @@ -31,7 +32,9 @@ UnreadThings::UnreadThings(not_null<ApiWrap*> api) : _api(api) { bool UnreadThings::trackMentions(Data::Thread *thread) const { const auto peer = thread ? thread->peer().get() : nullptr; - return peer && (peer->isChat() || peer->isMegagroup()); + return peer + && (peer->isChat() || peer->isMegagroup()) + && !peer->isMonoforum(); } bool UnreadThings::trackReactions(Data::Thread *thread) const { @@ -93,7 +96,7 @@ void UnreadThings::cancelRequests(not_null<Data::Thread*> thread) { void UnreadThings::requestMentions( not_null<Data::Thread*> thread, int loaded) { - if (_mentionsRequests.contains(thread)) { + if (_mentionsRequests.contains(thread) || thread->asSublist()) { return; } const auto offsetId = std::max( @@ -138,13 +141,15 @@ void UnreadThings::requestReactions( const auto maxId = 0; const auto minId = 0; const auto history = thread->owningHistory(); + const auto sublist = thread->asSublist(); const auto topic = thread->asTopic(); using Flag = MTPmessages_GetUnreadReactions::Flag; const auto requestId = _api->request(MTPmessages_GetUnreadReactions( - MTP_flags(topic ? Flag::f_top_msg_id : Flag()), + MTP_flags((topic ? Flag::f_top_msg_id : Flag()) + | (sublist ? Flag::f_saved_peer_id : Flag())), history->peer->input, MTP_int(topic ? topic->rootId() : 0), - MTPInputPeer(), // saved_peer_id + (sublist ? sublist->sublistPeer()->input : MTPInputPeer()), MTP_int(offsetId), MTP_int(addOffset), MTP_int(limit), diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 94d02148c4..ae5d4e3218 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -128,6 +128,12 @@ void SavedMessages::loadMore() { _loadMore.call(); } +void SavedMessages::clearAllUnreadReactions() { + for (const auto &[peer, sublist] : _sublists) { + sublist->unreadReactions().clear(); + } +} + void SavedMessages::sendLoadMore() { if (_loadMoreRequestId || _chatsList.loaded()) { return; diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 8b5530db79..9b20b8920a 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -47,6 +47,7 @@ public: void preloadSublists(); void loadMore(); + void clearAllUnreadReactions(); void apply(const MTPDupdatePinnedSavedDialogs &update); void apply(const MTPDupdateSavedDialogPinned &update); diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 29bf6d5a13..50f75e051b 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -725,6 +725,7 @@ void SavedSublist::applyMonoforumDialog( data.vread_inbox_max_id().v, data.vunread_count().v); setOutboxReadTill(data.vread_outbox_max_id().v); + unreadReactions().setCount(data.vunread_reactions_count().v); setUnreadMark(data.is_unread_mark()); applyMaybeLast(topItem); } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index fc99f35daf..bee130f501 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -836,11 +836,19 @@ void History::clearUnreadMentionsFor(MsgId topicRootId) { } } -void History::clearUnreadReactionsFor(MsgId topicRootId) { +void History::clearUnreadReactionsFor( + MsgId topicRootId, + Data::SavedSublist *sublist) { const auto forum = peer->forum(); - if (!topicRootId) { + const auto monoforum = peer->monoforum(); + const auto sublistPeerId = sublist ? sublist->sublistPeer()->id : 0; + if ((!topicRootId && !sublist) + || (!topicRootId && forum) + || (!sublist && monoforum)) { if (forum) { forum->clearAllUnreadReactions(); + } else if (monoforum) { + monoforum->clearAllUnreadReactions(); } unreadReactions().clear(); return; @@ -848,6 +856,8 @@ void History::clearUnreadReactionsFor(MsgId topicRootId) { if (const auto topic = forum->topicFor(topicRootId)) { topic->unreadReactions().clear(); } + } else if (monoforum) { + sublist->unreadReactions().clear(); } const auto &ids = unreadReactionsIds(); if (ids.empty()) { @@ -859,7 +869,8 @@ void History::clearUnreadReactionsFor(MsgId topicRootId) { items.reserve(ids.size()); for (const auto &id : ids) { if (const auto item = owner->message(peerId, id)) { - if (item->topicRootId() == topicRootId) { + if ((topicRootId && item->topicRootId() == topicRootId) + || (sublist && item->sublistPeerId() == sublistPeerId)) { items.emplace(id); } } diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index e58ed77a1a..2bddc0eef0 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -284,7 +284,9 @@ public: void clearLastKeyboard(); void clearUnreadMentionsFor(MsgId topicRootId); - void clearUnreadReactionsFor(MsgId topicRootId); + void clearUnreadReactionsFor( + MsgId topicRootId, + Data::SavedSublist *sublist); Data::Draft *draft(Data::DraftKey key) const; void setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index cc6e4e8bb1..231a8d7a09 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1476,6 +1476,9 @@ void HistoryItem::markReactionsRead() { if (const auto topic = this->topic()) { topic->updateChatListEntry(); topic->unreadReactions().erase(id); + } else if (const auto sublist = this->savedSublist()) { + sublist->updateChatListEntry(); + sublist->unreadReactions().erase(id); } } @@ -2195,6 +2198,8 @@ void HistoryItem::destroyHistoryEntry() { history()->unreadReactions().erase(id); if (const auto topic = this->topic()) { topic->unreadReactions().erase(id); + } else if (const auto sublist = this->savedSublist()) { + sublist->unreadReactions().erase(id); } } if (isRegular() && _history->peer->isMegagroup()) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 32ef2bf028..3d164d2946 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2198,7 +2198,7 @@ void ChatWidget::cornerButtonsShowAtPosition( Data::Thread *ChatWidget::cornerButtonsThread() { return _sublist - ? nullptr + ? static_cast<Data::Thread*>(_sublist) : _topic ? static_cast<Data::Thread*>(_topic) : _history; @@ -2233,7 +2233,9 @@ bool ChatWidget::cornerButtonsUnreadMayBeShown() { } bool ChatWidget::cornerButtonsHas(CornerButtonType type) { - return _topic || (type == CornerButtonType::Down); + return _topic + || (_sublist && type == CornerButtonType::Reactions) + || (type == CornerButtonType::Down); } void ChatWidget::showAtStart() { diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 5d7ca9d396..4125ac0891 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_message_reactions.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "main/main_session.h" #include "apiwrap.h" @@ -924,14 +925,16 @@ void SetupUnreadReactionsMenu( return; } const auto topic = thread->asTopic(); + const auto sublist = thread->asSublist(); const auto peer = thread->peer(); const auto rootId = topic ? topic->rootId() : 0; using Flag = MTPmessages_ReadReactions::Flag; peer->session().api().request(MTPmessages_ReadReactions( - MTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)), + MTP_flags((rootId ? Flag::f_top_msg_id : Flag(0)) + | (sublist ? Flag::f_saved_peer_id : Flag(0))), peer->input, MTP_int(rootId), - MTPInputPeer() // saved_peer_id + sublist ? sublist->sublistPeer()->input : MTPInputPeer() )).done([=](const MTPmessages_AffectedHistory &result) { const auto offset = peer->session().api().applyAffectedHistory( peer, @@ -940,7 +943,9 @@ void SetupUnreadReactionsMenu( resend(weakThread, done, resend); } else { done(); - peer->owner().history(peer)->clearUnreadReactionsFor(rootId); + peer->owner().history(peer)->clearUnreadReactionsFor( + rootId, + sublist); } }).fail(done).send(); }; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 0038a41783..36f27e4dc0 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1167,6 +1167,9 @@ Window::SessionController *Manager::openNotificationMessage( && item->isRegular() && (item->out() || (item->mentionsMe() && !history->peer->isUser())); const auto topic = item ? item->topic() : nullptr; + const auto sublist = (item && item->history()->amMonoforumAdmin()) + ? item->savedSublist() + : nullptr; const auto guard = gsl::finally([&] { if (topic) { @@ -1223,13 +1226,9 @@ Window::SessionController *Manager::openNotificationMessage( if (window) { window->widget()->showFromTray(); if (topic) { - using namespace HistoryView; - window->showSection( - std::make_shared<ChatMemento>(ChatViewId{ - .history = history, - .repliesRootId = topic->rootId(), - }, itemId), - SectionShow::Way::Forward); + window->showTopic(topic, itemId, SectionShow::Way::Forward); + } else if (sublist) { + window->showSublist(sublist, itemId, SectionShow::Way::Forward); } else { window->showPeerHistory( history->peer->id, diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 477b1b6707..183526a0a7 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3090,12 +3090,14 @@ void UnpinAllMessages( const auto sendRequest = [=](auto self) -> void { const auto history = strong->owningHistory(); const auto topicRootId = strong->topicRootId(); + const auto sublist = strong->asSublist(); using Flag = MTPmessages_UnpinAllMessages::Flag; api->request(MTPmessages_UnpinAllMessages( - MTP_flags(topicRootId ? Flag::f_top_msg_id : Flag()), + MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag()) + | (sublist ? Flag::f_saved_peer_id : Flag())), history->peer->input, MTP_int(topicRootId.bare), - MTPInputPeer() // saved_peer_id + sublist ? sublist->sublistPeer()->input : MTPInputPeer() )).done([=](const MTPmessages_AffectedHistory &result) { const auto peer = history->peer; const auto offset = api->applyAffectedHistory(peer, result); From dfc1ec3ccf273666db53236fd5ec1b3e3f73a23c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 2 Jun 2025 15:00:36 +0400 Subject: [PATCH 096/310] Support shared media / pins for sublists. --- Telegram/SourceFiles/apiwrap.cpp | 99 ++++++--- Telegram/SourceFiles/apiwrap.h | 5 + .../SourceFiles/boxes/pin_messages_box.cpp | 18 +- .../SourceFiles/core/local_url_handlers.cpp | 1 + .../data/data_document_resolver.cpp | 5 +- .../SourceFiles/data/data_document_resolver.h | 3 +- Telegram/SourceFiles/data/data_forum.cpp | 8 +- .../data/data_history_messages.cpp | 4 +- Telegram/SourceFiles/data/data_peer.cpp | 22 +- Telegram/SourceFiles/data/data_peer.h | 5 + .../SourceFiles/data/data_saved_messages.cpp | 198 +++++++++++++----- .../SourceFiles/data/data_saved_messages.h | 38 +++- .../SourceFiles/data/data_saved_sublist.cpp | 6 +- .../data/data_search_controller.cpp | 25 ++- .../SourceFiles/data/data_search_controller.h | 3 + Telegram/SourceFiles/data/data_session.cpp | 1 + .../SourceFiles/data/data_shared_media.cpp | 7 + Telegram/SourceFiles/data/data_shared_media.h | 5 + Telegram/SourceFiles/data/data_sparse_ids.cpp | 7 +- Telegram/SourceFiles/data/data_sparse_ids.h | 6 +- .../dialogs/dialogs_inner_widget.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 86 ++++++-- Telegram/SourceFiles/history/history.h | 2 +- Telegram/SourceFiles/history/history_item.cpp | 16 ++ .../SourceFiles/history/history_widget.cpp | 6 +- .../view/history_view_chat_section.cpp | 47 +++-- .../history/view/history_view_chat_section.h | 1 + .../view/history_view_pinned_section.cpp | 2 + .../view/history_view_pinned_tracker.cpp | 1 + .../view/history_view_top_bar_widget.cpp | 2 +- .../info_common_groups_widget.cpp | 2 +- .../SourceFiles/info/info_content_widget.cpp | 11 +- .../SourceFiles/info/info_content_widget.h | 5 + Telegram/SourceFiles/info/info_controller.cpp | 17 ++ Telegram/SourceFiles/info/info_controller.h | 7 + Telegram/SourceFiles/info/info_memento.cpp | 49 +++++ Telegram/SourceFiles/info/info_memento.h | 9 + .../info/media/info_media_buttons.cpp | 8 +- .../info/media/info_media_buttons.h | 1 + .../info/media/info_media_inner_widget.cpp | 6 + .../info/media/info_media_list_widget.cpp | 10 +- .../info/media/info_media_list_widget.h | 1 + .../info/media/info_media_provider.cpp | 20 +- .../info/media/info_media_provider.h | 1 + .../info/media/info_media_widget.cpp | 19 +- .../info/media/info_media_widget.h | 2 + .../info/members/info_members_widget.cpp | 2 +- .../peer_gifts/info_peer_gifts_widget.cpp | 2 +- .../profile/info_profile_inner_widget.cpp | 7 +- .../info/profile/info_profile_inner_widget.h | 2 + .../info/profile/info_profile_values.cpp | 2 + .../info/profile/info_profile_values.h | 1 + .../info/profile/info_profile_widget.cpp | 14 +- .../info/profile/info_profile_widget.h | 2 + .../info_requests_list_widget.cpp | 2 +- .../info/saved/info_saved_sublists_widget.cpp | 3 +- .../info_similar_peers_widget.cpp | 2 +- .../inline_bots/inline_bot_result.cpp | 4 +- Telegram/SourceFiles/iv/iv_instance.cpp | 7 +- .../main/main_session_settings.cpp | 65 ++++-- .../SourceFiles/main/main_session_settings.h | 5 +- .../media/player/media_player_instance.cpp | 7 + .../media/player/media_player_instance.h | 1 + .../media/view/media_view_open_common.h | 12 +- .../media/view/media_view_overlay_widget.cpp | 30 ++- .../media/view/media_view_overlay_widget.h | 2 + .../linux/notifications_manager_linux.cpp | 24 +++ .../linux/notifications_manager_linux.h | 1 + .../platform/mac/notifications_manager_mac.h | 1 + .../platform/mac/notifications_manager_mac.mm | 51 ++++- .../win/notifications_manager_win.cpp | 37 +++- .../platform/win/notifications_manager_win.h | 1 + .../details/storage_settings_scheme.cpp | 1 + .../storage/storage_shared_media.cpp | 49 ++++- .../storage/storage_shared_media.h | 32 ++- .../window/notifications_manager.cpp | 72 ++++++- .../window/notifications_manager.h | 15 ++ .../window/notifications_manager_default.cpp | 33 ++- .../window/notifications_manager_default.h | 10 +- .../SourceFiles/window/window_peer_menu.cpp | 10 +- .../SourceFiles/window/window_peer_menu.h | 1 + .../window/window_session_controller.cpp | 16 +- .../window/window_session_controller.h | 1 + 83 files changed, 1105 insertions(+), 221 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 873fc58b3a..4f8cef0a22 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2978,17 +2978,27 @@ void ApiWrap::resolveJumpToDate( Fn<void(not_null<PeerData*>, MsgId)> callback) { if (const auto peer = chat.peer()) { const auto topic = chat.topic(); - const auto rootId = topic ? topic->rootId() : 0; - resolveJumpToHistoryDate(peer, rootId, date, std::move(callback)); + const auto sublist = chat.sublist(); + const auto rootId = topic ? topic->rootId() : MsgId(); + const auto monoforumPeerId = sublist + ? sublist->sublistPeer()->id + : PeerId(); + resolveJumpToHistoryDate( + peer, + rootId, + monoforumPeerId, + date, + std::move(callback)); } } template <typename Callback> void ApiWrap::requestMessageAfterDate( - not_null<PeerData*> peer, - MsgId topicRootId, - const QDate &date, - Callback &&callback) { + not_null<PeerData*> peer, + MsgId topicRootId, + PeerId monoforumPeerId, + const QDate &date, + Callback &&callback) { // API returns a message with date <= offset_date. // 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. @@ -3011,7 +3021,7 @@ void ApiWrap::requestMessageAfterDate( return &messages.vmessages().v; }; const auto list = result.match([&]( - const MTPDmessages_messages &data) { + const MTPDmessages_messages &data) { return handleMessages(data); }, [&](const MTPDmessages_messagesSlice &data) { return handleMessages(data); @@ -3054,6 +3064,18 @@ void ApiWrap::requestMessageAfterDate( MTP_int(maxId), MTP_int(minId), MTP_long(historyHash))); + } else if (monoforumPeerId) { + send(MTPmessages_GetSavedHistory( + MTP_flags(MTPmessages_GetSavedHistory::Flag::f_parent_peer), + peer->input, + session().data().peer(monoforumPeerId)->input, + MTP_int(offsetId), + MTP_int(offsetDate), + MTP_int(addOffset), + MTP_int(limit), + MTP_int(maxId), + MTP_int(minId), + MTP_long(historyHash))); } else { send(MTPmessages_GetHistory( peer->input, @@ -3070,28 +3092,41 @@ void ApiWrap::requestMessageAfterDate( void ApiWrap::resolveJumpToHistoryDate( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, const QDate &date, Fn<void(not_null<PeerData*>, MsgId)> callback) { if (const auto channel = peer->migrateTo()) { return resolveJumpToHistoryDate( channel, topicRootId, + monoforumPeerId, date, std::move(callback)); } const auto jumpToDateInPeer = [=] { - requestMessageAfterDate(peer, topicRootId, date, [=](MsgId itemId) { - callback(peer, itemId); - }); + requestMessageAfterDate( + peer, + topicRootId, + monoforumPeerId, + date, + [=](MsgId itemId) { callback(peer, itemId); }); }; - if (const auto chat = topicRootId ? nullptr : peer->migrateFrom()) { - requestMessageAfterDate(chat, 0, date, [=](MsgId itemId) { - if (itemId) { - callback(chat, itemId); - } else { - jumpToDateInPeer(); - } - }); + const auto migrated = (topicRootId || monoforumPeerId) + ? nullptr + : peer->migrateFrom(); + if (migrated) { + requestMessageAfterDate( + migrated, + MsgId(), + PeerId(), + date, + [=](MsgId itemId) { + if (itemId) { + callback(migrated, itemId); + } else { + jumpToDateInPeer(); + } + }); } else { jumpToDateInPeer(); } @@ -3140,12 +3175,14 @@ void ApiWrap::requestHistory( void ApiWrap::requestSharedMedia( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, MsgId messageId, SliceType slice) { const auto key = SharedMediaRequest{ peer, topicRootId, + monoforumPeerId, type, messageId, slice, @@ -3157,6 +3194,7 @@ void ApiWrap::requestSharedMedia( const auto prepared = Api::PrepareSearchRequest( peer, topicRootId, + monoforumPeerId, type, QString(), messageId, @@ -3179,7 +3217,12 @@ void ApiWrap::requestSharedMedia( messageId, slice, result); - sharedMediaDone(peer, topicRootId, type, std::move(parsed)); + sharedMediaDone( + peer, + topicRootId, + monoforumPeerId, + type, + std::move(parsed)); finish(); }).fail([=] { _sharedMediaRequests.remove(key); @@ -3192,16 +3235,19 @@ void ApiWrap::requestSharedMedia( void ApiWrap::sharedMediaDone( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, Api::SearchResult &&parsed) { const auto topic = peer->forumTopicFor(topicRootId); - if (topicRootId && !topic) { + const auto sublist = peer->monoforumSublistFor(monoforumPeerId); + if ((topicRootId && !topic) || (monoforumPeerId && !sublist)) { return; } const auto hasMessages = !parsed.messageIds.empty(); _session->storage().add(Storage::SharedMediaAddSlice( peer->id, topicRootId, + monoforumPeerId, type, std::move(parsed.messageIds), parsed.noSkipRange, @@ -3212,6 +3258,9 @@ void ApiWrap::sharedMediaDone( if (topic) { topic->setHasPinnedMessages(true); } + if (sublist) { + sublist->setHasPinnedMessages(true); + } } } @@ -3245,16 +3294,12 @@ void ApiWrap::sendAction(const SendAction &action) { && !action.options.shortcutId && !action.replaceMediaOf) { const auto topicRootId = action.replyTo.topicRootId; - const auto monoforumPeerId = action.replyTo.monoforumPeerId; const auto topic = topicRootId ? action.history->peer->forumTopicFor(topicRootId) : nullptr; - const auto monoforum = monoforumPeerId - ? action.history->peer->monoforum() - : nullptr; - const auto sublist = monoforum - ? monoforum->sublistLoaded( - action.history->owner().peer(monoforumPeerId)) + const auto monoforumPeerId = action.replyTo.monoforumPeerId; + const auto sublist = monoforumPeerId + ? action.history->peer->monoforumSublistFor(monoforumPeerId) : nullptr; if (topic) { topic->readTillEnd(); diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index a4835adbae..cb6ed34c2d 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -289,6 +289,7 @@ public: void requestSharedMedia( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Storage::SharedMediaType type, MsgId messageId, SliceType slice); @@ -505,18 +506,21 @@ private: void resolveJumpToHistoryDate( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, const QDate &date, Fn<void(not_null<PeerData*>, MsgId)> callback); template <typename Callback> void requestMessageAfterDate( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, const QDate &date, Callback &&callback); void sharedMediaDone( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, Api::SearchResult &&parsed); void globalMediaDone( @@ -665,6 +669,7 @@ private: struct SharedMediaRequest { not_null<PeerData*> peer; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaType mediaType = {}; MsgId aroundId = 0; SliceType sliceType = {}; diff --git a/Telegram/SourceFiles/boxes/pin_messages_box.cpp b/Telegram/SourceFiles/boxes/pin_messages_box.cpp index 163e9dedc6..07d041a7d1 100644 --- a/Telegram/SourceFiles/boxes/pin_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/pin_messages_box.cpp @@ -24,10 +24,15 @@ namespace { [[nodiscard]] bool IsOldForPin( MsgId id, not_null<PeerData*> peer, - MsgId topicRootId) { + MsgId topicRootId, + PeerId monoforumPeerId) { const auto normal = peer->migrateToOrMe(); const auto migrated = normal->migrateFrom(); - const auto top = Data::ResolveTopPinnedId(normal, topicRootId, migrated); + const auto top = Data::ResolveTopPinnedId( + normal, + topicRootId, + monoforumPeerId, + migrated); if (!top) { return false; } else if (peer == migrated) { @@ -53,7 +58,14 @@ void PinMessageBox( const auto peer = item->history()->peer; const auto msgId = item->id; const auto topicRootId = item->topic() ? item->topicRootId() : MsgId(); - const auto pinningOld = IsOldForPin(msgId, peer, topicRootId); + const auto monoforumPeerId = item->history()->peer->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId(); + const auto pinningOld = IsOldForPin( + msgId, + peer, + topicRootId, + monoforumPeerId); const auto state = box->lifetime().make_state<State>(); const auto api = box->lifetime().make_state<MTP::Sender>( &peer->session().mtp()); diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 832276e05f..64e33b14ca 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -847,6 +847,7 @@ bool OpenMediaTimestamp( document, context, context ? context->topicRootId() : MsgId(0), + context ? context->sublistPeerId() : PeerId(0), false, time)); } else if (document->isSong() || document->isVoiceMessage()) { diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp index 4c901daa24..13b30f0bd2 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.cpp +++ b/Telegram/SourceFiles/data/data_document_resolver.cpp @@ -187,7 +187,8 @@ void ResolveDocument( Window::SessionController *controller, not_null<DocumentData*> document, HistoryItem *item, - MsgId topicRootId) { + MsgId topicRootId, + PeerId monoforumPeerId) { if (document->isNull()) { return; } @@ -202,7 +203,7 @@ void ResolveDocument( controller->openDocument( document, true, - { msgId, topicRootId }); + { msgId, topicRootId, monoforumPeerId }); } }; diff --git a/Telegram/SourceFiles/data/data_document_resolver.h b/Telegram/SourceFiles/data/data_document_resolver.h index 4988297aaa..de9327312f 100644 --- a/Telegram/SourceFiles/data/data_document_resolver.h +++ b/Telegram/SourceFiles/data/data_document_resolver.h @@ -31,6 +31,7 @@ void ResolveDocument( Window::SessionController *controller, not_null<DocumentData*> document, HistoryItem *item, - MsgId topicRootId); + MsgId topicRootId, + PeerId monoforumPeerId); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index 921cd0ca15..ffc92a9847 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -72,7 +72,10 @@ Forum::~Forum() { auto &changes = session().changes(); const auto peerId = _history->peer->id; for (const auto &[rootId, topic] : _topics) { - storage.unload(Storage::SharedMediaUnloadThread(peerId, rootId)); + storage.unload(Storage::SharedMediaUnloadThread( + peerId, + rootId, + PeerId())); _history->setForwardDraft(rootId, PeerId(), {}); const auto raw = topic.get(); @@ -198,7 +201,8 @@ void Forum::applyTopicDeleted(MsgId rootId) { _history->destroyMessagesByTopic(rootId); session().storage().unload(Storage::SharedMediaUnloadThread( _history->peer->id, - rootId)); + rootId, + PeerId())); _history->setForwardDraft(rootId, PeerId(), {}); } diff --git a/Telegram/SourceFiles/data/data_history_messages.cpp b/Telegram/SourceFiles/data/data_history_messages.cpp index 5d5dffa30b..6711059297 100644 --- a/Telegram/SourceFiles/data/data_history_messages.cpp +++ b/Telegram/SourceFiles/data/data_history_messages.cpp @@ -152,6 +152,7 @@ rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer( auto createSimpleViewer = [=]( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter) { @@ -161,11 +162,10 @@ rpl::producer<SparseIdsMergedSlice> HistoryMergedViewer( return HistoryViewer(chosen, simpleKey, limitBefore, limitAfter); }; const auto peerId = history->peer->id; - const auto topicRootId = MsgId(); const auto migratedPeerId = migrateFrom ? migrateFrom->id : PeerId(0); using Key = SparseIdsMergedSlice::Key; return SparseIdsMergedSlice::CreateViewer( - Key(peerId, topicRootId, migratedPeerId, universalAroundId), + Key(peerId, MsgId(), PeerId(), migratedPeerId, universalAroundId), limitBefore, limitAfter, std::move(createSimpleViewer)); diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 7af00bad92..8f639faf0f 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1474,6 +1474,16 @@ Data::SavedMessages *PeerData::monoforum() const { return nullptr; } +Data::SavedSublist *PeerData::monoforumSublistFor( + PeerId sublistPeerId) const { + if (!sublistPeerId) { + return nullptr; + } else if (const auto monoforum = this->monoforum()) { + return monoforum->sublistLoaded(owner().peer(sublistPeerId)); + } + return nullptr; +} + bool PeerData::allowsForwarding() const { if (isUser()) { return true; @@ -1807,12 +1817,14 @@ void SetTopPinnedMessageId( session.settings().setHiddenPinnedMessageId( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId 0); session.saveSettingsDelayed(); } session.storage().add(Storage::SharedMediaAddExisting( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, messageId, { messageId, ServerMaxMsgId })); @@ -1822,22 +1834,25 @@ void SetTopPinnedMessageId( FullMsgId ResolveTopPinnedId( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated) { const auto slice = peer->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( peer->id, topicRootId, + monoforumPeerId, Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, 1)); - const auto old = (!topicRootId && migrated) + const auto old = (!topicRootId && !monoforumPeerId && migrated) ? migrated->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( migrated->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, @@ -1859,22 +1874,25 @@ FullMsgId ResolveTopPinnedId( FullMsgId ResolveMinPinnedId( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated) { const auto slice = peer->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( peer->id, topicRootId, + monoforumPeerId, Storage::SharedMediaType::Pinned, 1), 1, 1)); - const auto old = (!topicRootId && migrated) + const auto old = (!topicRootId && !monoforumPeerId && migrated) ? migrated->session().storage().snapshot( Storage::SharedMediaQuery( Storage::SharedMediaKey( migrated->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, 1), 1, diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 8de2297985..104e8ba10b 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -38,6 +38,7 @@ class ForumTopic; class Session; class GroupCall; class SavedMessages; +class SavedSublist; struct ReactionId; class WallPaper; @@ -260,6 +261,8 @@ public: [[nodiscard]] Data::ForumTopic *forumTopicFor(MsgId rootId) const; [[nodiscard]] Data::SavedMessages *monoforum() const; + [[nodiscard]] Data::SavedSublist *monoforumSublistFor( + PeerId sublistPeerId) const; [[nodiscard]] Data::PeerNotifySettings ¬ify() { return _notify; @@ -616,10 +619,12 @@ void SetTopPinnedMessageId( [[nodiscard]] FullMsgId ResolveTopPinnedId( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated = nullptr); [[nodiscard]] FullMsgId ResolveMinPinnedId( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated = nullptr); } // namespace Data diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index ae5d4e3218..ab3ac5f4eb 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "data/data_changes.h" #include "data/data_channel.h" +#include "data/data_histories.h" #include "data/data_user.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" @@ -18,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_unread_things.h" #include "main/main_session.h" +#include "storage/storage_facade.h" +#include "storage/storage_shared_media.h" #include "window/notifications_manager.h" namespace Data { @@ -29,6 +32,7 @@ constexpr auto kListPerPage = 100; constexpr auto kListFirstPerPage = 20; constexpr auto kLoadedSublistsMinCount = 20; constexpr auto kShowSublistNamesCount = 5; +constexpr auto kStalePerRequest = 100; } // namespace @@ -50,16 +54,36 @@ SavedMessages::SavedMessages( } } -SavedMessages::~SavedMessages() { +void SavedMessages::clear() { + for (const auto &request : base::take(_sublistRequests)) { + if (request.second.id != _staleRequestId) { + owner().histories().cancelRequest(request.second.id); + } + } + if (const auto requestId = base::take(_staleRequestId)) { + session().api().request(requestId).cancel(); + } + + auto &storage = session().storage(); auto &changes = session().changes(); if (_owningHistory) { - for (const auto &[peer, sublist] : _sublists) { + for (const auto &[peer, sublist] : base::take(_sublists)) { + storage.unload(Storage::SharedMediaUnloadThread( + _owningHistory->peer->id, + MsgId(), + peer->id)); _owningHistory->setForwardDraft(MsgId(), peer->id, {}); const auto raw = sublist.get(); + changes.sublistRemoved(raw); changes.entryRemoved(raw); } } + _owningHistory = nullptr; +} + +SavedMessages::~SavedMessages() { + clear(); } bool SavedMessages::supported() const { @@ -108,6 +132,90 @@ SavedSublist *SavedMessages::sublistLoaded(not_null<PeerData*> peer) { return (i != end(_sublists)) ? i->second.get() : nullptr; } +void SavedMessages::requestSomeStale() { + if (_staleRequestId + || (!_offset.id && _loadMoreRequestId) + || _stalePeers.empty() + || !_parentChat) { + return; + } + const auto type = Histories::RequestType::History; + auto peers = std::vector<not_null<PeerData*>>(); + auto peerIds = QVector<MTPInputPeer>(); + peers.reserve(std::min(int(_stalePeers.size()), kStalePerRequest)); + peerIds.reserve(std::min(int(_stalePeers.size()), kStalePerRequest)); + for (auto i = begin(_stalePeers); i != end(_stalePeers);) { + const auto peer = *i; + i = _stalePeers.erase(i); + + peers.push_back(peer); + peerIds.push_back(peer->input); + if (peerIds.size() == kStalePerRequest) { + break; + } + } + if (peerIds.empty()) { + return; + } + const auto call = [=] { + for (const auto &peer : peers) { + finishSublistRequest(peer); + } + }; + auto &histories = owner().histories(); + _staleRequestId = histories.sendRequest(_owningHistory, type, [=]( + Fn<void()> finish) { + using Flag = MTPmessages_GetSavedDialogsByID::Flag; + return session().api().request( + MTPmessages_GetSavedDialogsByID( + MTP_flags(Flag::f_parent_peer), + _parentChat->input, + MTP_vector<MTPInputPeer>(peerIds)) + ).done([=](const MTPmessages_SavedDialogs &result) { + _staleRequestId = 0; + applyReceivedSublists(result); + call(); + finish(); + }).fail([=] { + _staleRequestId = 0; + call(); + finish(); + }).send(); + }); + for (const auto &peer : peers) { + _sublistRequests[peer].id = _staleRequestId; + } +} + +void SavedMessages::finishSublistRequest(not_null<PeerData*> peer) { + if (const auto request = _sublistRequests.take(peer)) { + for (const auto &callback : request->callbacks) { + callback(); + } + } +} + +void SavedMessages::requestSublist( + not_null<PeerData*> peer, + Fn<void()> done) { + if (!_parentChat) { + return; + } + auto &request = _sublistRequests[peer]; + if (done) { + request.callbacks.push_back(std::move(done)); + } + if (!request.id + && _stalePeers.emplace(peer).second + && (_stalePeers.size() == 1)) { + crl::on_main(&session(), [peer = _parentChat] { + if (const auto monoforum = peer->monoforum()) { + monoforum->requestSomeStale(); + } + }); + } +} + rpl::producer<> SavedMessages::chatsListChanges() const { return _chatsListChanges.events(); } @@ -146,18 +254,28 @@ void SavedMessages::sendLoadMore() { MTP_flags(Flag::f_exclude_pinned | (_parentChat ? Flag::f_parent_peer : Flag(0))), _parentChat ? _parentChat->input : MTPInputPeer(), - MTP_int(_offsetDate), - MTP_int(_offsetId), - _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), - MTP_int(_offsetId ? kListPerPage : kListFirstPerPage), + MTP_int(_offset.date), + MTP_int(_offset.id), + _offset.peer ? _offset.peer->input : MTP_inputPeerEmpty(), + MTP_int(_offset.id ? kListPerPage : kListFirstPerPage), MTP_long(0)) // hash ).done([=](const MTPmessages_SavedDialogs &result) { - apply(result, false); + const auto applied = applyReceivedSublists(result); + if (applied.allLoaded || _offset == applied.offset) { + _chatsList.setLoaded(); + } else if (_offset.date > 0 && applied.offset.date > _offset.date) { + LOG(("API Error: Bad order in messages.savedDialogs.")); + _chatsList.setLoaded(); + } else { + _offset = applied.offset; + } + _loadMoreRequestId = 0; _chatsListChanges.fire({}); if (_chatsList.loaded()) { _chatsListLoadedEvents.fire({}); } reorderLastSublists(); + requestSomeStale(); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { markUnsupported(); @@ -174,7 +292,9 @@ void SavedMessages::loadPinned() { _pinnedRequestId = _owner->session().api().request( MTPmessages_GetPinnedSavedDialogs() ).done([=](const MTPmessages_SavedDialogs &result) { - apply(result, true); + _pinnedRequestId = 0; + _pinnedLoaded = true; + applyReceivedSublists(result, true); _chatsListChanges.fire({}); }).fail([=](const MTP::Error &error) { if (error.type() == u"SAVED_DIALOGS_UNSUPPORTED"_q) { @@ -186,11 +306,11 @@ void SavedMessages::loadPinned() { }).send(); } -void SavedMessages::apply( - const MTPmessages_SavedDialogs &result, +SavedMessages::ApplyResult SavedMessages::applyReceivedSublists( + const MTPmessages_SavedDialogs &dialogs, bool pinned) { auto list = (const QVector<MTPSavedDialog>*)nullptr; - result.match([](const MTPDmessages_savedDialogsNotModified &) { + dialogs.match([](const MTPDmessages_savedDialogsNotModified &) { LOG(("API Error: messages.savedDialogsNotModified.")); }, [&](const auto &data) { _owner->processUsers(data.vusers()); @@ -200,22 +320,11 @@ void SavedMessages::apply( NewMessageType::Existing); list = &data.vdialogs().v; }); - if (pinned) { - _pinnedRequestId = 0; - _pinnedLoaded = true; - } else { - _loadMoreRequestId = 0; - } if (!list) { - if (!pinned) { - _chatsList.setLoaded(); - } - return; + return { .allLoaded = true }; } auto lastValid = false; - auto offsetDate = TimeId(); - auto offsetId = MsgId(); - auto offsetPeer = (PeerData*)nullptr; + auto result = ApplyResult(); const auto parentPeerId = _parentChat ? _parentChat->id : _owner->session().userPeerId(); @@ -224,9 +333,9 @@ void SavedMessages::apply( const auto peer = _owner->peer(peerFromMTP(data.vpeer())); const auto topId = MsgId(data.vtop_message().v); if (const auto item = _owner->message(parentPeerId, topId)) { - offsetPeer = peer; - offsetDate = item->date(); - offsetId = topId; + result.offset.peer = peer; + result.offset.date = item->date(); + result.offset.id = topId; lastValid = true; const auto entry = sublist(peer); const auto entryPinned = pinned || data.is_pinned(); @@ -239,9 +348,9 @@ void SavedMessages::apply( const auto peer = _owner->peer(peerFromMTP(data.vpeer())); const auto topId = MsgId(data.vtop_message().v); if (const auto item = _owner->message(parentPeerId, topId)) { - offsetPeer = peer; - offsetDate = item->date(); - offsetId = topId; + result.offset.peer = peer; + result.offset.date = item->date(); + result.offset.id = topId; lastValid = true; sublist(peer)->applyMonoforumDialog(data, item); } else { @@ -252,20 +361,14 @@ void SavedMessages::apply( if (pinned) { } else if (!lastValid) { LOG(("API Error: Unknown message in the end of a slice.")); - _chatsList.setLoaded(); - } else if (result.type() == mtpc_messages_savedDialogs) { - _chatsList.setLoaded(); - } else if ((_offsetDate > 0 && offsetDate > _offsetDate) - || (offsetDate == _offsetDate - && offsetId == _offsetId - && offsetPeer == _offsetPeer)) { - LOG(("API Error: Bad order in messages.savedDialogs.")); - _chatsList.setLoaded(); - } else { - _offsetDate = offsetDate; - _offsetId = offsetId; - _offsetPeer = offsetPeer; + result.allLoaded = true; + } else if (dialogs.type() == mtpc_messages_savedDialogs) { + result.allLoaded = true; } + if (!_stalePeers.empty()) { + requestSomeStale(); + } + return result; } void SavedMessages::sendLoadMoreRequests() { @@ -324,7 +427,7 @@ void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) { return; } const auto raw = i->second.get(); - //Core::App().notifications().clearFromTopic(raw); // #TODO monoforum + Core::App().notifications().clearFromSublist(raw); owner().removeChatListEntry(raw); if (ranges::contains(_lastSublists, not_null(raw))) { @@ -342,9 +445,10 @@ void SavedMessages::applySublistDeleted(not_null<PeerData*> sublistPeer) { const auto history = owningHistory(); history->destroyMessagesBySublist(sublistPeer); - //session().storage().unload(Storage::SharedMediaUnloadThread( - // _history->peer->id, - // rootId)); + session().storage().unload(Storage::SharedMediaUnloadThread( + _owningHistory->peer->id, + MsgId(), + sublistPeer->id)); history->setForwardDraft(MsgId(), sublistPeer->id, {}); } diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 9b20b8920a..c7568326c5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -18,6 +18,16 @@ namespace Data { class Session; class SavedSublist; +struct SavedMessagesOffsets { + TimeId date = 0; + MsgId id = 0; + PeerData *peer = nullptr; + + friend inline constexpr auto operator<=>( + SavedMessagesOffsets, + SavedMessagesOffsets) = default; +}; + class SavedMessages final { public: explicit SavedMessages( @@ -37,6 +47,7 @@ public: [[nodiscard]] not_null<Dialogs::MainList*> chatsList(); [[nodiscard]] not_null<SavedSublist*> sublist(not_null<PeerData*> peer); [[nodiscard]] SavedSublist *sublistLoaded(not_null<PeerData*> peer); + void requestSublist(not_null<PeerData*> peer, Fn<void()> done = nullptr); [[nodiscard]] rpl::producer<> chatsListChanges() const; [[nodiscard]] rpl::producer<> chatsListLoadedEvents() const; @@ -59,13 +70,31 @@ public: [[nodiscard]] auto recentSublists() const -> const std::vector<not_null<SavedSublist*>> &; + void clear(); + [[nodiscard]] rpl::lifetime &lifetime(); private: + struct SublistRequest { + mtpRequestId id = 0; + std::vector<Fn<void()>> callbacks; + }; + struct ApplyResult { + SavedMessagesOffsets offset; + bool allLoaded = false; + }; + void loadPinned(); - void apply(const MTPmessages_SavedDialogs &result, bool pinned); + ApplyResult applyReceivedSublists( + const MTPmessages_SavedDialogs &result, + SavedMessagesOffsets &updateOffsets); + ApplyResult applyReceivedSublists( + const MTPmessages_SavedDialogs &result, + bool pinned = false); void reorderLastSublists(); + void requestSomeStale(); + void finishSublistRequest(not_null<PeerData*> peer); void sendLoadMore(); void sendLoadMoreRequests(); @@ -80,13 +109,14 @@ private: base::flat_map< not_null<PeerData*>, std::unique_ptr<SavedSublist>> _sublists; + base::flat_map<not_null<PeerData*>, SublistRequest> _sublistRequests; + base::flat_set<not_null<PeerData*>> _stalePeers; + mtpRequestId _staleRequestId = 0; mtpRequestId _loadMoreRequestId = 0; mtpRequestId _pinnedRequestId = 0; - TimeId _offsetDate = 0; - MsgId _offsetId = 0; - PeerData *_offsetPeer = nullptr; + SavedMessagesOffsets _offset; SingleQueuedInvokation _loadMore; bool _loadMoreScheduled = false; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 50f75e051b..ce0a3f4147 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_sublist.h" #include "apiwrap.h" +#include "core/application.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_drafts.h" @@ -22,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_unread_things.h" #include "main/main_session.h" +#include "window/notifications_manager.h" namespace Data { namespace { @@ -221,7 +223,7 @@ void SavedSublist::applyItemRemoved(MsgId id) { void SavedSublist::requestChatListMessage() { if (!chatListMessageKnown()) { - //forum()->requestTopic(_rootId); // #TODO monoforum + parent()->requestSublist(sublistPeer()); } } @@ -648,7 +650,7 @@ void SavedSublist::readTill( _readRequestTimer.callOnce(0); } } - // Core::App().notifications().clearIncomingFromSublist(this); // #TODO monoforum + Core::App().notifications().clearIncomingFromSublist(this); } void SavedSublist::sendReadTillRequest() { diff --git a/Telegram/SourceFiles/data/data_search_controller.cpp b/Telegram/SourceFiles/data/data_search_controller.cpp index 4a2418eb9d..6b43192805 100644 --- a/Telegram/SourceFiles/data/data_search_controller.cpp +++ b/Telegram/SourceFiles/data/data_search_controller.cpp @@ -132,6 +132,7 @@ GlobalMediaResult ParseGlobalMediaResult( std::optional<SearchRequest> PrepareSearchRequest( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Storage::SharedMediaType type, const QString &query, MsgId messageId, @@ -168,11 +169,14 @@ std::optional<SearchRequest> PrepareSearchRequest( int64(0x3FFFFFFF))); using Flag = MTPmessages_Search::Flag; return MTPmessages_Search( - MTP_flags(topicRootId ? Flag::f_top_msg_id : Flag(0)), + MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag(0)) + | (monoforumPeerId ? Flag::f_saved_peer_id : Flag(0))), peer->input, MTP_string(query), MTP_inputPeerEmpty(), - MTPInputPeer(), // saved_peer_id + (monoforumPeerId + ? peer->owner().peer(monoforumPeerId)->input + : MTPInputPeer()), MTPVector<MTPReaction>(), // saved_reaction MTP_int(topicRootId), filter, @@ -369,12 +373,14 @@ rpl::producer<SparseIdsMergedSlice> SearchController::idsSlice( auto createSimpleViewer = [=]( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter) { return simpleIdsSlice( peerId, topicRootId, + monoforumPeerId, simpleKey, query, limitBefore, @@ -384,6 +390,7 @@ rpl::producer<SparseIdsMergedSlice> SearchController::idsSlice( SparseIdsMergedSlice::Key( query.peerId, query.topicRootId, + query.monoforumPeerId, query.migratedPeerId, aroundId), limitBefore, @@ -394,6 +401,7 @@ rpl::producer<SparseIdsMergedSlice> SearchController::idsSlice( rpl::producer<SparseIdsSlice> SearchController::simpleIdsSlice( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId aroundId, const Query &query, int limitBefore, @@ -402,8 +410,12 @@ rpl::producer<SparseIdsSlice> SearchController::simpleIdsSlice( Expects(IsServerMsgId(aroundId) || (aroundId == 0)); Expects((aroundId != 0) || (limitBefore == 0 && limitAfter == 0)); - Expects((query.peerId == peerId && query.topicRootId == topicRootId) - || (query.migratedPeerId == peerId && MsgId(0) == topicRootId)); + Expects((query.peerId == peerId + && query.topicRootId == topicRootId + && query.monoforumPeerId == monoforumPeerId) + || (query.migratedPeerId == peerId + && MsgId(0) == topicRootId + && PeerId(0) == monoforumPeerId)); auto it = _cache.find(query); if (it == _cache.end()) { @@ -437,7 +449,9 @@ rpl::producer<SparseIdsSlice> SearchController::simpleIdsSlice( _session->data().itemRemoved( ) | rpl::filter([=](not_null<const HistoryItem*> item) { return (item->history()->peer->id == peerId) - && (!topicRootId || item->topicRootId() == topicRootId); + && (!topicRootId || item->topicRootId() == topicRootId) + && (!monoforumPeerId + || item->sublistPeerId() == monoforumPeerId); }) | rpl::filter([=](not_null<const HistoryItem*> item) { return builder->removeOne(item->id); }) | rpl::start_with_next(pushNextSnapshot, lifetime); @@ -510,6 +524,7 @@ void SearchController::requestMore( auto prepared = PrepareSearchRequest( listData->peer, query.topicRootId, + query.monoforumPeerId, query.type, query.query, key.aroundId, diff --git a/Telegram/SourceFiles/data/data_search_controller.h b/Telegram/SourceFiles/data/data_search_controller.h index c73fb7b390..e7fb3f8e9f 100644 --- a/Telegram/SourceFiles/data/data_search_controller.h +++ b/Telegram/SourceFiles/data/data_search_controller.h @@ -61,6 +61,7 @@ struct GlobalMediaResult { [[nodiscard]] std::optional<SearchRequest> PrepareSearchRequest( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Storage::SharedMediaType type, const QString &query, MsgId messageId, @@ -92,6 +93,7 @@ public: PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; PeerId migratedPeerId = 0; MediaType type = MediaType::kCount; QString query; @@ -151,6 +153,7 @@ private: rpl::producer<SparseIdsSlice> simpleIdsSlice( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId aroundId, const Query &query, int limitBefore, diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index f9a3127f56..292dffc405 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -404,6 +404,7 @@ void Session::clear() { channel->setFlags(channel->flags() & ~(ChannelDataFlag::Forum | ChannelDataFlag::MonoforumAdmin)); } + _savedMessages->clear(); _sendActionManager->clear(); diff --git a/Telegram/SourceFiles/data/data_shared_media.cpp b/Telegram/SourceFiles/data/data_shared_media.cpp index 6834190bf7..5e0c3735d2 100644 --- a/Telegram/SourceFiles/data/data_shared_media.cpp +++ b/Telegram/SourceFiles/data/data_shared_media.cpp @@ -110,11 +110,13 @@ rpl::producer<SparseIdsSlice> SharedMediaViewer( auto requestMediaAround = [ peer = session->data().peer(key.peerId), topicRootId = key.topicRootId, + monoforumPeerId = key.monoforumPeerId, type = key.type ](const SparseIdsSliceBuilder::AroundData &data) { peer->session().api().requestSharedMedia( peer, topicRootId, + monoforumPeerId, type, data.aroundId, data.direction); @@ -131,6 +133,7 @@ rpl::producer<SparseIdsSlice> SharedMediaViewer( ) | rpl::filter([=](const SliceUpdate &update) { return (update.peerId == key.peerId) && (update.topicRootId == key.topicRootId) + && (update.monoforumPeerId == key.monoforumPeerId) && (update.type == key.type); }) | rpl::filter([=](const SliceUpdate &update) { return builder->applyUpdate(update.data); @@ -151,6 +154,8 @@ rpl::producer<SparseIdsSlice> SharedMediaViewer( return (update.peerId == key.peerId) && (!update.topicRootId || update.topicRootId == key.topicRootId) + && (!update.monoforumPeerId + || update.monoforumPeerId == key.monoforumPeerId) && update.types.test(key.type); }) | rpl::filter([=] { return builder->removeAll(); @@ -236,6 +241,7 @@ rpl::producer<SparseIdsMergedSlice> SharedMediaMergedViewer( auto createSimpleViewer = [=]( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter) { @@ -244,6 +250,7 @@ rpl::producer<SparseIdsMergedSlice> SharedMediaMergedViewer( Storage::SharedMediaKey( peerId, topicRootId, + monoforumPeerId, key.type, simpleKey), limitBefore, diff --git a/Telegram/SourceFiles/data/data_shared_media.h b/Telegram/SourceFiles/data/data_shared_media.h index d9d2d8c63f..8f53b4026c 100644 --- a/Telegram/SourceFiles/data/data_shared_media.h +++ b/Telegram/SourceFiles/data/data_shared_media.h @@ -74,11 +74,13 @@ public: Key( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, PeerId migratedPeerId, Type type, UniversalMsgId universalId) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , migratedPeerId(migratedPeerId) , type(type) , universalId(universalId) { @@ -91,6 +93,7 @@ public: PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; PeerId migratedPeerId = 0; Type type = Type::kCount; UniversalMsgId universalId; @@ -120,6 +123,7 @@ public: return { key.peerId, key.topicRootId, + key.monoforumPeerId, key.migratedPeerId, v::is<MessageId>(key.universalId) ? v::get<MessageId>(key.universalId) @@ -130,6 +134,7 @@ public: return { key.peerId, key.topicRootId, + key.monoforumPeerId, key.migratedPeerId, ServerMaxMsgId - 1 }; diff --git a/Telegram/SourceFiles/data/data_sparse_ids.cpp b/Telegram/SourceFiles/data/data_sparse_ids.cpp index b48881ca3a..787c0148a3 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.cpp +++ b/Telegram/SourceFiles/data/data_sparse_ids.cpp @@ -377,7 +377,10 @@ rpl::producer<SparseIdsMergedSlice> SparseIdsMergedSlice::CreateViewer( int limitBefore, int limitAfter, Fn<SimpleViewerFunction> simpleViewer) { - Expects(!key.topicRootId || !key.migratedPeerId); + Expects(!key.topicRootId + || (!key.monoforumPeerId && !key.migratedPeerId)); + Expects(!key.monoforumPeerId + || (!key.topicRootId && !key.migratedPeerId)); Expects(IsServerMsgId(key.universalId) || (key.universalId == 0) || (IsServerMsgId(ServerMaxMsgId + key.universalId) && key.migratedPeerId != 0)); @@ -388,6 +391,7 @@ rpl::producer<SparseIdsMergedSlice> SparseIdsMergedSlice::CreateViewer( auto partViewer = simpleViewer( key.peerId, key.topicRootId, + key.monoforumPeerId, SparseIdsMergedSlice::PartKey(key), limitBefore, limitAfter @@ -405,6 +409,7 @@ rpl::producer<SparseIdsMergedSlice> SparseIdsMergedSlice::CreateViewer( auto migratedViewer = simpleViewer( key.migratedPeerId, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId SparseIdsMergedSlice::MigratedKey(key), limitBefore, limitAfter); diff --git a/Telegram/SourceFiles/data/data_sparse_ids.h b/Telegram/SourceFiles/data/data_sparse_ids.h index 6b762e2dbe..3328ee5a40 100644 --- a/Telegram/SourceFiles/data/data_sparse_ids.h +++ b/Telegram/SourceFiles/data/data_sparse_ids.h @@ -33,11 +33,13 @@ public: Key( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, PeerId migratedPeerId, UniversalMsgId universalId) : peerId(peerId) , topicRootId(topicRootId) - , migratedPeerId(topicRootId ? 0 : migratedPeerId) + , monoforumPeerId(monoforumPeerId) + , migratedPeerId((topicRootId || monoforumPeerId) ? 0 : migratedPeerId) , universalId(universalId) { } @@ -47,6 +49,7 @@ public: PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; PeerId migratedPeerId = 0; UniversalMsgId universalId = 0; }; @@ -72,6 +75,7 @@ public: using SimpleViewerFunction = rpl::producer<SparseIdsSlice>( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SparseIdsSlice::Key simpleKey, int limitBefore, int limitAfter); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 344bebd455..6eb211320a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1909,7 +1909,7 @@ RowDescriptor InnerWidget::computeChatPreviewRow() const { auto result = computeChosenRow(); if (const auto peer = result.key.peer()) { const auto topicId = _pressedTopicJump - ? _pressedTopicJumpRootId + ? _pressedTopicJumpRootId // #TODO monoforums : 0; if (const auto topic = peer->forumTopicFor(topicId)) { return { topic, FullMsgId() }; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index bee130f501..8061df17fb 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -651,8 +651,38 @@ void History::destroyMessagesBySublist(not_null<PeerData*> sublistPeer) { } } -void History::unpinMessagesFor(MsgId topicRootId) { - if (!topicRootId) { +void History::unpinMessagesFor(MsgId topicRootId, PeerId monoforumPeerId) { + if (topicRootId) { + session().storage().remove( + Storage::SharedMediaRemoveAll( + peer->id, + topicRootId, + Storage::SharedMediaType::Pinned)); + if (const auto topic = peer->forumTopicFor(topicRootId)) { + topic->setHasPinnedMessages(false); + } + for (const auto &item : _items) { + if (item->isPinned() && item->topicRootId() == topicRootId) { + item->setIsPinned(false); + } + } + } else if (monoforumPeerId) { + session().storage().remove( + Storage::SharedMediaRemoveAll( + peer->id, + monoforumPeerId, + Storage::SharedMediaType::Pinned)); + if (const auto sublist = peer->monoforumSublistFor( + monoforumPeerId)) { + sublist->setHasPinnedMessages(false); + } + for (const auto &item : _items) { + if (item->isPinned() + && item->sublistPeerId() == monoforumPeerId) { + item->setIsPinned(false); + } + } + } else { session().storage().remove( Storage::SharedMediaRemoveAll( peer->id, @@ -668,20 +698,6 @@ void History::unpinMessagesFor(MsgId topicRootId) { item->setIsPinned(false); } } - } else { - session().storage().remove( - Storage::SharedMediaRemoveAll( - peer->id, - topicRootId, - Storage::SharedMediaType::Pinned)); - if (const auto topic = peer->forumTopicFor(topicRootId)) { - topic->setHasPinnedMessages(false); - } - for (const auto &item : _items) { - if (item->isPinned() && item->topicRootId() == topicRootId) { - item->setIsPinned(false); - } - } } } @@ -898,6 +914,7 @@ not_null<HistoryItem*> History::addNewToBack( storage.add(Storage::SharedMediaAddExisting( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId types, item->id, { from, till })); @@ -909,6 +926,7 @@ not_null<HistoryItem*> History::addNewToBack( storage.add(Storage::SharedMediaAddExisting( peer->id, topic->rootId(), + PeerId(), // monoforumPeerId types, item->id, { item->id, item->id})); @@ -916,6 +934,18 @@ not_null<HistoryItem*> History::addNewToBack( topic->setHasPinnedMessages(true); } } + if (const auto sublist = item->savedSublist()) { + storage.add(Storage::SharedMediaAddExisting( + peer->id, + MsgId(), // topicRootId + item->sublistPeerId(), + types, + item->id, + { item->id, item->id })); + if (pinned) { + sublist->setHasPinnedMessages(true); + } + } } } if (item->from()->id) { @@ -1182,7 +1212,8 @@ void History::applyServiceChanges( if (id && item) { session().storage().add(Storage::SharedMediaAddSlice( peer->id, - MsgId(0), + MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, { id }, { id, ServerMaxMsgId })); @@ -1191,11 +1222,22 @@ void History::applyServiceChanges( session().storage().add(Storage::SharedMediaAddSlice( peer->id, topic->rootId(), + PeerId(), // monoforumPeerId Storage::SharedMediaType::Pinned, { id }, { id, ServerMaxMsgId })); topic->setHasPinnedMessages(true); } + if (const auto sublist = item->savedSublist()) { + session().storage().add(Storage::SharedMediaAddSlice( + peer->id, + MsgId(), // topicRootId + item->sublistPeerId(), + Storage::SharedMediaType::Pinned, + { id }, + { id, ServerMaxMsgId })); + sublist->setHasPinnedMessages(true); + } } }, [&](const MTPDmessageReplyStoryHeader &data) { LOG(("API Error: story reply in messageActionPinMessage.")); @@ -1470,6 +1512,7 @@ void History::addEdgesToSharedMedia() { session().storage().add(Storage::SharedMediaAddSlice( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId type, {}, { from, till })); @@ -1683,6 +1726,7 @@ void History::addToSharedMedia( session().storage().add(Storage::SharedMediaAddSlice( peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId type, std::move(medias[i]), { from, till })); @@ -3162,11 +3206,9 @@ void History::forceFullResize() { Data::Thread *History::threadFor(MsgId topicRootId, PeerId monoforumPeerId) { return topicRootId ? peer->forumTopicFor(topicRootId) - : !monoforumPeerId - ? static_cast<Data::Thread*>(this) - : peer->monoforum() - ? peer->monoforum()->sublistLoaded(owner().peer(monoforumPeerId)) - : nullptr; + : monoforumPeerId + ? peer->monoforumSublistFor(monoforumPeerId) + : static_cast<Data::Thread*>(this); } const Data::Thread *History::threadFor( diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 2bddc0eef0..cd6e707372 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -141,7 +141,7 @@ public: void destroyMessagesByTopic(MsgId topicRootId); void destroyMessagesBySublist(not_null<PeerData*> sublistPeer); - void unpinMessagesFor(MsgId topicRootId); + void unpinMessagesFor(MsgId topicRootId, PeerId monoforumPeerId); not_null<HistoryItem*> addNewMessage( MsgId id, diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 231a8d7a09..3daa1a5500 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1513,6 +1513,7 @@ void HistoryItem::setIsPinned(bool pinned) { storage.add(Storage::SharedMediaAddExisting( _history->peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId Storage::SharedMediaType::Pinned, id, { id, id })); @@ -1521,11 +1522,22 @@ void HistoryItem::setIsPinned(bool pinned) { storage.add(Storage::SharedMediaAddExisting( _history->peer->id, topic->rootId(), + PeerId(), // monoforumPeerId Storage::SharedMediaType::Pinned, id, { id, id })); topic->setHasPinnedMessages(true); } + if (const auto sublist = this->savedSublist()) { + storage.add(Storage::SharedMediaAddExisting( + _history->peer->id, + MsgId(0), // topicRootId + sublistPeerId(), + Storage::SharedMediaType::Pinned, + id, + { id, id })); + sublist->setHasPinnedMessages(true); + } } else { _flags &= ~MessageFlag::Pinned; if (_flags & MessageFlag::StoryItem) { @@ -2238,6 +2250,7 @@ void HistoryItem::addToSharedMediaIndex() { _history->session().storage().add(Storage::SharedMediaAddNew( _history->peer->id, topicRootId(), + sublistPeerId(), types, id)); if (types.test(Storage::SharedMediaType::Pinned)) { @@ -2245,6 +2258,9 @@ void HistoryItem::addToSharedMediaIndex() { if (const auto topic = this->topic()) { topic->setHasPinnedMessages(true); } + if (const auto sublist = this->savedSublist()) { + sublist->setHasPinnedMessages(true); + } } } } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 13119ca1cc..7d41300c38 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6038,7 +6038,7 @@ bool HistoryWidget::showSendingFilesError( return true; } -MsgId HistoryWidget::resolveReplyToTopicRootId() { +MsgId HistoryWidget::resolveReplyToTopicRootId() { // #TODO monoforums Expects(_peer != nullptr); const auto replyToInfo = replyTo(); @@ -7601,6 +7601,7 @@ void HistoryWidget::updatePinnedViewer() { _minPinnedId = Data::ResolveMinPinnedId( _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId _migrated ? _migrated->peer.get() : nullptr); } if (_pinnedClickedId @@ -7680,6 +7681,7 @@ void HistoryWidget::checkPinnedBarState() { const auto currentPinnedId = Data::ResolveTopPinnedId( _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId _migrated ? _migrated->peer.get() : nullptr); const auto universalPinnedId = !currentPinnedId ? int32(0) @@ -7713,6 +7715,7 @@ void HistoryWidget::checkPinnedBarState() { auto pinnedRefreshed = Info::Profile::SharedMediaCountValue( _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId nullptr, Storage::SharedMediaType::Pinned ) | rpl::distinct_until_changed( @@ -8593,6 +8596,7 @@ void HistoryWidget::hidePinnedMessage() { controller(), _peer, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId crl::guard(this, callback)); } } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 3d164d2946..39afe08693 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -151,7 +151,7 @@ void ChatMemento::setFromTopic(not_null<Data::ForumTopic*> topic) { } -Data::ForumTopic *ChatMemento::topicForRemoveRequests() const { +Data::ForumTopic *ChatMemento::topicForRemoveRequests() const {// #TODO monoforums return _id.repliesRootId ? _id.history->peer->forumTopicFor(_id.repliesRootId) : nullptr; @@ -233,6 +233,9 @@ ChatWidget::ChatWidget( , _topic(lookupTopic()) , _areComments(computeAreComments()) , _sublist(_id.sublist) +, _monoforumPeerId((_sublist && _sublist->parentChat()) + ? _sublist->sublistPeer()->id + : PeerId()) , _sendAction(_repliesRootId ? _history->owner().sendActionManager().repliesPainter( _history, @@ -772,7 +775,7 @@ void ChatWidget::setupComposeControls() { _composeControls->setHistory({ .history = _history.get(), .topicRootId = _topic ? _topic->rootId() : MsgId(), - .monoforumPeerId = _sublist ? _sublist->sublistPeer()->id : PeerId(), + .monoforumPeerId = _monoforumPeerId, .showSlowmodeError = [=] { return showSlowmodeError(); }, .sendActionFactory = [=] { return prepareSendAction({}); }, .slowmodeSecondsLeft = SlowmodeSecondsLeft(_peer), @@ -1781,20 +1784,17 @@ SendMenu::Details ChatWidget::sendMenuDetails() const { } FullReplyTo ChatWidget::replyTo() const { - const auto monoforumPeerId = (_sublist && _sublist->parentChat()) - ? _sublist->sublistPeer()->id - : PeerId(); if (auto custom = _composeControls->replyingToMessage()) { const auto item = custom.messageId ? session().data().message(custom.messageId) : nullptr; const auto sublistPeerId = item ? item->sublistPeerId() : PeerId(); if (!item - || !monoforumPeerId - || (sublistPeerId == monoforumPeerId)) { + || !_monoforumPeerId + || (sublistPeerId == _monoforumPeerId)) { // Never answer to a message in a wrong monoforum peer id. custom.topicRootId = _repliesRootId; - custom.monoforumPeerId = monoforumPeerId; + custom.monoforumPeerId = _monoforumPeerId; return custom; } } @@ -1803,7 +1803,7 @@ FullReplyTo ChatWidget::replyTo() const { ? FullMsgId(_peer->id, _repliesRootId) : FullMsgId()), .topicRootId = _repliesRootId, - .monoforumPeerId = monoforumPeerId, + .monoforumPeerId = _monoforumPeerId, }; } @@ -1850,7 +1850,10 @@ void ChatWidget::updatePinnedViewer() { _pinnedClickedId = FullMsgId(); } if (_pinnedClickedId && !_minPinnedId) { - _minPinnedId = Data::ResolveMinPinnedId(_peer, _repliesRootId); + _minPinnedId = Data::ResolveMinPinnedId( + _peer, + _repliesRootId, + _monoforumPeerId); } if (_pinnedClickedId && _minPinnedId && _minPinnedId >= _pinnedClickedId) { // After click on the last pinned message we should the top one. @@ -1955,6 +1958,7 @@ void ChatWidget::setupPinnedTracker() { Storage::SharedMediaKey( _topic->channel()->id, _repliesRootId, + _monoforumPeerId, Storage::SharedMediaType::Pinned, ServerMaxMsgId - 1), 1, @@ -1968,10 +1972,15 @@ void ChatWidget::setupPinnedTracker() { const auto peerId = _peer->id; const auto hiddenId = settings.hiddenPinnedMessageId( peerId, - _repliesRootId); + _repliesRootId, + _monoforumPeerId); const auto last = result.size() ? result[result.size() - 1] : 0; if (hiddenId && hiddenId != last) { - settings.setHiddenPinnedMessageId(peerId, _repliesRootId, 0); + settings.setHiddenPinnedMessageId( + peerId, + _repliesRootId, + _monoforumPeerId, + 0); _history->session().saveSettingsDelayed(); } } @@ -1987,10 +1996,12 @@ void ChatWidget::checkPinnedBarState() { ? MsgId(0) : _peer->session().settings().hiddenPinnedMessageId( _peer->id, - _repliesRootId); + _repliesRootId, + _monoforumPeerId); const auto currentPinnedId = Data::ResolveTopPinnedId( _peer, - _repliesRootId); + _repliesRootId, + _monoforumPeerId); const auto universalPinnedId = !currentPinnedId ? MsgId(0) : currentPinnedId.msg; @@ -2021,6 +2032,7 @@ void ChatWidget::checkPinnedBarState() { auto pinnedRefreshed = Info::Profile::SharedMediaCountValue( _peer, _repliesRootId, + _monoforumPeerId, nullptr, Storage::SharedMediaType::Pinned ) | rpl::distinct_until_changed( @@ -2187,6 +2199,7 @@ void ChatWidget::hidePinnedMessage() { controller(), _peer, _repliesRootId, + _monoforumPeerId, crl::guard(this, callback)); } } @@ -3193,7 +3206,9 @@ void ChatWidget::listShowPremiumToast(not_null<DocumentData*> document) { void ChatWidget::listOpenPhoto( not_null<PhotoData*> photo, FullMsgId context) { - controller()->openPhoto(photo, { context, _repliesRootId }); + controller()->openPhoto( + photo, + { context, _repliesRootId, _monoforumPeerId }); } void ChatWidget::listOpenDocument( @@ -3203,7 +3218,7 @@ void ChatWidget::listOpenDocument( controller()->openDocument( document, showInMediaView, - { context, _repliesRootId }); + { context, _repliesRootId, _monoforumPeerId }); } void ChatWidget::listPaintEmpty( diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index 52bc50f832..593553ce25 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -394,6 +394,7 @@ private: rpl::variable<bool> _areComments = false; Data::SavedSublist *_sublist = nullptr; + PeerId _monoforumPeerId; std::shared_ptr<SendActionPainter> _sendAction; std::shared_ptr<Ui::ChatTheme> _theme; diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index b8c380c5c1..cf82e31e2b 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -195,6 +195,7 @@ void PinnedWidget::setupClearButton() { controller(), _history->peer, _thread->topicRootId(), + _thread->monoforumPeerId(), crl::guard(this, callback)); } else { Window::UnpinAllMessages(controller(), _thread); @@ -517,6 +518,7 @@ rpl::producer<Data::MessagesSlice> PinnedWidget::listSource( SparseIdsMergedSlice::Key( _history->peer->id, _thread->topicRootId(), + _thread->monoforumPeerId(), _migratedPeer ? _migratedPeer->id : 0, messageId), Storage::SharedMediaType::Pinned), diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp index 24abaa03fe..fcc7226566 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_tracker.cpp @@ -86,6 +86,7 @@ void PinnedTracker::refreshViewer() { SparseIdsMergedSlice::Key( peer->id, _thread->topicRootId(), + _thread->monoforumPeerId(), _migratedPeer ? _migratedPeer->id : 0, _viewerAroundId), Storage::SharedMediaType::Pinned), diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 3cb26ace66..9f7a8ac8be 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -751,7 +751,7 @@ void TopBarWidget::infoClicked() { _controller->showSection(std::make_shared<Info::Memento>(topic)); } else if (const auto sublist = key.sublist()) { _controller->showSection(std::make_shared<Info::Memento>( - sublist->owningHistory()->peer, + sublist, Info::Section(Storage::SharedMediaType::Photo))); } else if (key.peer()->savedSublistsInfo()) { _controller->showSection(std::make_shared<Info::Memento>( diff --git a/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp b/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp index f81019bb77..46ad5a394a 100644 --- a/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp +++ b/Telegram/SourceFiles/info/common_groups/info_common_groups_widget.cpp @@ -21,7 +21,7 @@ namespace Info { namespace CommonGroups { Memento::Memento(not_null<UserData*> user) -: ContentMemento(user, nullptr, PeerId()) { +: ContentMemento(user, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/info_content_widget.cpp b/Telegram/SourceFiles/info/info_content_widget.cpp index f392e132ed..efbfbdc14a 100644 --- a/Telegram/SourceFiles/info/info_content_widget.cpp +++ b/Telegram/SourceFiles/info/info_content_widget.cpp @@ -307,6 +307,7 @@ QRect ContentWidget::floatPlayerAvailableRect() const { void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { const auto peer = _controller->key().peer(); const auto topic = _controller->key().topic(); + const auto sublist = _controller->key().sublist(); if (!peer && !topic) { return; } @@ -316,6 +317,8 @@ void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) { Dialogs::EntryState{ .key = (topic ? Dialogs::Key{ topic } + : sublist + ? Dialogs::Key{ sublist } : Dialogs::Key{ peer->owner().history(peer) }), .section = Dialogs::EntryState::Section::Profile, }, @@ -465,6 +468,8 @@ void ContentWidget::setupSwipeHandler(not_null<Ui::RpWidget*> widget) { Key ContentMemento::key() const { if (const auto topic = this->topic()) { return Key(topic); + } else if (const auto sublist = this->sublist()) { + return Key(sublist); } else if (const auto peer = this->peer()) { return Key(peer); } else if (const auto poll = this->poll()) { @@ -489,12 +494,14 @@ Key ContentMemento::key() const { ContentMemento::ContentMemento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId) : _peer(peer) -, _migratedPeerId((!topic && peer->migrateFrom()) +, _migratedPeerId((!topic && !sublist && peer->migrateFrom()) ? peer->migrateFrom()->id : 0) -, _topic(topic) { +, _topic(topic) +, _sublist(sublist) { if (_topic) { _peer->owner().itemIdChanged( ) | rpl::start_with_next([=](const Data::Session::IdChange &change) { diff --git a/Telegram/SourceFiles/info/info_content_widget.h b/Telegram/SourceFiles/info/info_content_widget.h index 359f46787a..29c376dfa4 100644 --- a/Telegram/SourceFiles/info/info_content_widget.h +++ b/Telegram/SourceFiles/info/info_content_widget.h @@ -210,6 +210,7 @@ public: ContentMemento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId); explicit ContentMemento(Settings::Tag settings); explicit ContentMemento(Downloads::Tag downloads); @@ -240,6 +241,9 @@ public: Data::ForumTopic *topic() const { return _topic; } + Data::SavedSublist *sublist() const { + return _sublist; + } UserData *settingsSelf() const { return _settingsSelf; } @@ -311,6 +315,7 @@ private: PeerData * const _peer = nullptr; const PeerId _migratedPeerId = 0; Data::ForumTopic *_topic = nullptr; + Data::SavedSublist *_sublist = nullptr; UserData * const _settingsSelf = nullptr; PeerData * const _storiesPeer = nullptr; Stories::Tab _storiesTab = {}; diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index b1ec130dc5..49054d63f1 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/search_field_controller.h" #include "data/data_shared_media.h" +#include "history/history.h" #include "info/info_content_widget.h" #include "info/info_memento.h" #include "info/global_media/info_global_media_widget.h" @@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_download_manager.h" @@ -35,6 +37,9 @@ Key::Key(not_null<PeerData*> peer) : _value(peer) { Key::Key(not_null<Data::ForumTopic*> topic) : _value(topic) { } +Key::Key(not_null<Data::SavedSublist*> sublist) : _value(sublist) { +} + Key::Key(Settings::Tag settings) : _value(settings) { } @@ -69,6 +74,8 @@ PeerData *Key::peer() const { return *peer; } else if (const auto topic = this->topic()) { return topic->channel(); + } else if (const auto sublist = this->sublist()) { + return sublist->owningHistory()->peer; } return nullptr; } @@ -81,6 +88,14 @@ Data::ForumTopic *Key::topic() const { return nullptr; } +Data::SavedSublist *Key::sublist() const { + if (const auto sublist = std::get_if<not_null<Data::SavedSublist*>>( + &_value)) { + return *sublist; + } + return nullptr; +} + UserData *Key::settingsSelf() const { if (const auto tag = std::get_if<Settings::Tag>(&_value)) { return tag->self; @@ -195,6 +210,7 @@ rpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource( SparseIdsMergedSlice::Key( peer()->id, topicId, + sublist() ? sublist()->sublistPeer()->id : PeerId(), migratedPeerId(), aroundId), section().mediaType()), @@ -487,6 +503,7 @@ rpl::producer<SparseIdsMergedSlice> Controller::mediaSource( SparseIdsMergedSlice::Key( query.peerId, query.topicRootId, + query.monoforumPeerId, query.migratedPeerId, aroundId), query.type), diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index c82a8b9f5a..5b29ac1ce6 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -18,6 +18,7 @@ struct WhoReadList; namespace Data { class ForumTopic; +class SavedSublist; } // namespace Data namespace Ui { @@ -94,6 +95,7 @@ class Key { public: explicit Key(not_null<PeerData*> peer); explicit Key(not_null<Data::ForumTopic*> topic); + explicit Key(not_null<Data::SavedSublist*> sublist); Key(Settings::Tag settings); Key(Downloads::Tag downloads); Key(Stories::Tag stories); @@ -108,6 +110,7 @@ public: PeerData *peer() const; Data::ForumTopic *topic() const; + Data::SavedSublist *sublist() const; UserData *settingsSelf() const; bool isDownloads() const; bool isGlobalMedia() const; @@ -135,6 +138,7 @@ private: std::variant< not_null<PeerData*>, not_null<Data::ForumTopic*>, + not_null<Data::SavedSublist*>, Settings::Tag, Downloads::Tag, Stories::Tag, @@ -225,6 +229,9 @@ public: [[nodiscard]] Data::ForumTopic *topic() const { return key().topic(); } + [[nodiscard]] Data::SavedSublist *sublist() const { + return key().sublist(); + } [[nodiscard]] UserData *settingsSelf() const { return key().settingsSelf(); } diff --git a/Telegram/SourceFiles/info/info_memento.cpp b/Telegram/SourceFiles/info/info_memento.cpp index 40f7733dd6..945bda8736 100644 --- a/Telegram/SourceFiles/info/info_memento.cpp +++ b/Telegram/SourceFiles/info/info_memento.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "main/main_session.h" @@ -48,6 +49,14 @@ Memento::Memento(not_null<Data::ForumTopic*> topic, Section section) : Memento(DefaultStack(topic, section)) { } +Memento::Memento(not_null<Data::SavedSublist*> sublist) +: Memento(sublist, Section::Type::Profile) { +} + +Memento::Memento(not_null<Data::SavedSublist*> sublist, Section section) +: Memento(DefaultStack(sublist, section)) { +} + Memento::Memento(Settings::Tag settings, Section section) : Memento(DefaultStack(settings, section)) { } @@ -66,9 +75,12 @@ Memento::Memento( Memento::Memento(std::vector<std::shared_ptr<ContentMemento>> stack) : _stack(std::move(stack)) { auto topics = base::flat_set<not_null<Data::ForumTopic*>>(); + auto sublists = base::flat_set<not_null<Data::SavedSublist*>>(); for (auto &entry : _stack) { if (const auto topic = entry->topic()) { topics.emplace(topic); + } else if (const auto sublist = entry->sublist()) { + sublists.emplace(sublist); } } for (const auto &topic : topics) { @@ -86,6 +98,21 @@ Memento::Memento(std::vector<std::shared_ptr<ContentMemento>> stack) } }, _lifetime); } + for (const auto &sublist : sublists) { + sublist->destroyed( + ) | rpl::start_with_next([=] { + for (auto i = begin(_stack); i != end(_stack);) { + if (i->get()->sublist() == sublist) { + i = _stack.erase(i); + } else { + ++i; + } + } + if (_stack.empty()) { + _removeRequests.fire({}); + } + }, _lifetime); + } } std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( @@ -104,6 +131,14 @@ std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( return result; } +std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( + not_null<Data::SavedSublist*> sublist, + Section section) { + auto result = std::vector<std::shared_ptr<ContentMemento>>(); + result.push_back(DefaultContent(sublist, section)); + return result; +} + std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack( Settings::Tag settings, Section section) { @@ -205,6 +240,20 @@ std::shared_ptr<ContentMemento> Memento::DefaultContent( Unexpected("Wrong section type in Info::Memento::DefaultContent()"); } +std::shared_ptr<ContentMemento> Memento::DefaultContent( + not_null<Data::SavedSublist*> sublist, + Section section) { + switch (section.type()) { + case Section::Type::Profile: + return std::make_shared<Profile::Memento>(sublist); + case Section::Type::Media: + return std::make_shared<Media::Memento>( + sublist, + section.mediaType()); + } + Unexpected("Wrong section type in Info::Memento::DefaultContent()"); +} + object_ptr<Window::SectionWidget> Memento::createWidget( QWidget *parent, not_null<Window::SessionController*> controller, diff --git a/Telegram/SourceFiles/info/info_memento.h b/Telegram/SourceFiles/info/info_memento.h index dc50f2f89c..fd0e5946aa 100644 --- a/Telegram/SourceFiles/info/info_memento.h +++ b/Telegram/SourceFiles/info/info_memento.h @@ -23,6 +23,7 @@ enum class SharedMediaType : signed char; namespace Data { class ForumTopic; +class SavedSublist; struct ReactionId; } // namespace Data @@ -49,6 +50,8 @@ public: Memento(not_null<PeerData*> peer, Section section); explicit Memento(not_null<Data::ForumTopic*> topic); Memento(not_null<Data::ForumTopic*> topic, Section section); + explicit Memento(not_null<Data::SavedSublist*> sublist); + Memento(not_null<Data::SavedSublist*> sublist, Section section); Memento(Settings::Tag settings, Section section); Memento(not_null<PollData*> poll, FullMsgId contextId); Memento( @@ -94,6 +97,9 @@ private: static std::vector<std::shared_ptr<ContentMemento>> DefaultStack( not_null<Data::ForumTopic*> topic, Section section); + static std::vector<std::shared_ptr<ContentMemento>> DefaultStack( + not_null<Data::SavedSublist*> sublist, + Section section); static std::vector<std::shared_ptr<ContentMemento>> DefaultStack( Settings::Tag settings, Section section); @@ -111,6 +117,9 @@ private: static std::shared_ptr<ContentMemento> DefaultContent( not_null<Data::ForumTopic*> topic, Section section); + static std::shared_ptr<ContentMemento> DefaultContent( + not_null<Data::SavedSublist*> sublist, + Section section); std::vector<std::shared_ptr<ContentMemento>> _stack; rpl::event_stream<> _removeRequests; diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.cpp b/Telegram/SourceFiles/info/media/info_media_buttons.cpp index 9d91702861..5a2b78e952 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.cpp +++ b/Telegram/SourceFiles/info/media/info_media_buttons.cpp @@ -147,12 +147,18 @@ not_null<Ui::SettingsButton*> AddButton( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Type type, Ui::MultiSlideTracker &tracker) { auto result = AddCountedButton( parent, - Profile::SharedMediaCountValue(peer, topicRootId, migrated, type), + Profile::SharedMediaCountValue( + peer, + topicRootId, + monoforumPeerId, + migrated, + type), MediaText(type), tracker)->entity(); const auto separateId = SeparateId(peer, topicRootId, type); diff --git a/Telegram/SourceFiles/info/media/info_media_buttons.h b/Telegram/SourceFiles/info/media/info_media_buttons.h index e8927c0970..a8a24feb47 100644 --- a/Telegram/SourceFiles/info/media/info_media_buttons.h +++ b/Telegram/SourceFiles/info/media/info_media_buttons.h @@ -42,6 +42,7 @@ using Type = Storage::SharedMediaType; not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Type type, Ui::MultiSlideTracker &tracker); diff --git a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp index 6f2bedac48..faccefcd74 100644 --- a/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_inner_widget.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/info_controller.h" #include "data/data_forum_topic.h" #include "data/data_peer.h" +#include "data/data_saved_sublist.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/shadow.h" #include "ui/widgets/buttons.h" @@ -79,7 +80,11 @@ void InnerWidget::createTypeButtons() { auto tracker = Ui::MultiSlideTracker(); const auto peer = _controller->key().peer(); const auto topic = _controller->key().topic(); + const auto sublist = _controller->key().sublist(); const auto topicRootId = topic ? topic->rootId() : MsgId(); + const auto monoforumPeerId = sublist + ? sublist->sublistPeer()->id + : PeerId(); const auto migrated = _controller->migrated(); const auto addMediaButton = [&]( Type buttonType, @@ -92,6 +97,7 @@ void InnerWidget::createTypeButtons() { _controller, peer, topicRootId, + monoforumPeerId, migrated, buttonType, tracker); diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp index 681405968c..d4d99d138a 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_download_manager.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/history.h" @@ -512,7 +513,7 @@ void ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) { : Data::StoriesContext{ Data::StoriesContextSaved() }; _controller->parentController()->openPhoto( photo, - { id, topicRootId() }, + { id, topicRootId(), monoforumPeerId() }, _controller->storiesPeer() ? &context : nullptr); } @@ -527,7 +528,7 @@ void ListWidget::openDocument( _controller->parentController()->openDocument( document, showInMediaView, - { id, topicRootId() }, + { id, topicRootId(), monoforumPeerId() }, _controller->storiesPeer() ? &context : nullptr); } @@ -796,6 +797,11 @@ MsgId ListWidget::topicRootId() const { return topic ? topic->rootId() : MsgId(0); } +PeerId ListWidget::monoforumPeerId() const { + const auto sublist = _controller->key().sublist(); + return sublist ? sublist->sublistPeer()->id : PeerId(0); +} + QMargins ListWidget::padding() const { return st::infoMediaMargin; } diff --git a/Telegram/SourceFiles/info/media/info_media_list_widget.h b/Telegram/SourceFiles/info/media/info_media_list_widget.h index 5b486c1de7..4f4a78e966 100644 --- a/Telegram/SourceFiles/info/media/info_media_list_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_list_widget.h @@ -158,6 +158,7 @@ private: void setupSelectRestriction(); [[nodiscard]] MsgId topicRootId() const; + [[nodiscard]] PeerId monoforumPeerId() const; QMargins padding() const; bool isItemLayout( diff --git a/Telegram/SourceFiles/info/media/info_media_provider.cpp b/Telegram/SourceFiles/info/media/info_media_provider.cpp index 62f7fdfc4a..88f47bc755 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.cpp +++ b/Telegram/SourceFiles/info/media/info_media_provider.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_peer_values.h" #include "data/data_document.h" +#include "data/data_saved_sublist.h" #include "styles/style_info.h" #include "styles/style_overview.h" @@ -40,7 +41,10 @@ Provider::Provider(not_null<AbstractController*> controller) , _peer(_controller->key().peer()) , _topicRootId(_controller->key().topic() ? _controller->key().topic()->rootId() - : 0) + : MsgId()) +, _monoforumPeerId(_controller->key().sublist() + ? _controller->key().sublist()->sublistPeer()->id + : PeerId()) , _migrated(_controller->migrated()) , _type(_controller->section().mediaType()) , _slice(sliceKey(_universalAroundId)) { @@ -331,13 +335,23 @@ SparseIdsMergedSlice::Key Provider::sliceKey( UniversalMsgId universalId) const { using Key = SparseIdsMergedSlice::Key; if (!_topicRootId && _migrated) { - return Key(_peer->id, _topicRootId, _migrated->id, universalId); + return Key( + _peer->id, + _topicRootId, + _monoforumPeerId, + _migrated->id, + universalId); } if (universalId < 0) { // Convert back to plain id for non-migrated histories. universalId = universalId + ServerMaxMsgId; } - return Key(_peer->id, _topicRootId, 0, universalId); + return Key( + _peer->id, + _topicRootId, + _monoforumPeerId, + PeerId(), + universalId); } void Provider::itemRemoved(not_null<const HistoryItem*> item) { diff --git a/Telegram/SourceFiles/info/media/info_media_provider.h b/Telegram/SourceFiles/info/media/info_media_provider.h index b544e2b8d7..ed09954e83 100644 --- a/Telegram/SourceFiles/info/media/info_media_provider.h +++ b/Telegram/SourceFiles/info/media/info_media_provider.h @@ -105,6 +105,7 @@ private: const not_null<PeerData*> _peer; const MsgId _topicRootId = 0; + const PeerId _monoforumPeerId = 0; PeerData * const _migrated = nullptr; const Type _type = Type::Photo; diff --git a/Telegram/SourceFiles/info/media/info_media_widget.cpp b/Telegram/SourceFiles/info/media/info_media_widget.cpp index e01d44f533..58d9e8e95e 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.cpp +++ b/Telegram/SourceFiles/info/media/info_media_widget.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/media/info_media_widget.h" +#include "history/history.h" #include "info/media/info_media_inner_widget.h" #include "info/info_controller.h" #include "main/main_session.h" @@ -17,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "lang/lang_keys.h" #include "styles/style_info.h" @@ -70,6 +72,7 @@ Memento::Memento(not_null<Controller*> controller) ? controller->storiesPeer() : controller->parentController()->session().user()), controller->topic(), + controller->sublist(), controller->migratedPeerId(), (controller->section().type() == Section::Type::Downloads ? Type::File @@ -79,23 +82,31 @@ Memento::Memento(not_null<Controller*> controller) } Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type) -: Memento(peer, nullptr, migratedPeerId, type) { +: Memento(peer, nullptr, nullptr, migratedPeerId, type) { } Memento::Memento(not_null<Data::ForumTopic*> topic, Type type) -: Memento(topic->channel(), topic, PeerId(), type) { +: Memento(topic->channel(), topic, nullptr, PeerId(), type) { +} + +Memento::Memento(not_null<Data::SavedSublist*> sublist, Type type) +: Memento(sublist->owningHistory()->peer, nullptr, sublist, PeerId(), type) { } Memento::Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Type type) -: ContentMemento(peer, topic, migratedPeerId) +: ContentMemento(peer, topic, sublist, migratedPeerId) , _type(type) { _searchState.query.type = type; _searchState.query.peerId = peer->id; - _searchState.query.topicRootId = topic ? topic->rootId() : 0; + _searchState.query.topicRootId = topic ? topic->rootId() : MsgId(); + _searchState.query.monoforumPeerId = sublist + ? sublist->sublistPeer()->id + : PeerId(); _searchState.query.migratedPeerId = migratedPeerId; if (migratedPeerId) { _searchState.migratedList = Storage::SparseIdsList(); diff --git a/Telegram/SourceFiles/info/media/info_media_widget.h b/Telegram/SourceFiles/info/media/info_media_widget.h index b7c53879b3..1a84139190 100644 --- a/Telegram/SourceFiles/info/media/info_media_widget.h +++ b/Telegram/SourceFiles/info/media/info_media_widget.h @@ -35,6 +35,7 @@ public: explicit Memento(not_null<Controller*> controller); Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type); Memento(not_null<Data::ForumTopic*> topic, Type type); + Memento(not_null<Data::SavedSublist*> sublist, Type type); using SearchState = Api::DelayedSearchController::SavedState; @@ -92,6 +93,7 @@ private: Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Type type); diff --git a/Telegram/SourceFiles/info/members/info_members_widget.cpp b/Telegram/SourceFiles/info/members/info_members_widget.cpp index 0a00a7344d..07609a430e 100644 --- a/Telegram/SourceFiles/info/members/info_members_widget.cpp +++ b/Telegram/SourceFiles/info/members/info_members_widget.cpp @@ -26,7 +26,7 @@ Memento::Memento(not_null<Controller*> controller) } Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId) -: ContentMemento(peer, nullptr, migratedPeerId) { +: ContentMemento(peer, nullptr, nullptr, migratedPeerId) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 47981cdf73..86b7d7b527 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -678,7 +678,7 @@ void InnerWidget::restoreState(not_null<Memento*> memento) { } Memento::Memento(not_null<PeerData*> peer) -: ContentMemento(peer, nullptr, PeerId()) { +: ContentMemento(peer, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index aa8b1862fe..5c9b86ce47 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo.h" #include "data/data_file_origin.h" #include "data/data_user.h" +#include "data/data_saved_sublist.h" #include "main/main_session.h" #include "apiwrap.h" #include "api/api_peer_photo.h" @@ -46,6 +47,7 @@ InnerWidget::InnerWidget( , _peer(_controller->key().peer()) , _migrated(_controller->migrated()) , _topic(_controller->key().topic()) +, _sublist(_controller->key().sublist()) , _content(setupContent(this, origin)) { _content->heightValue( ) | rpl::start_with_next([this](int height) { @@ -82,7 +84,7 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent( AddDetails(result, _controller, _peer, _topic, origin); result->add(setupSharedMedia(result.data())); - if (_topic) { + if (_topic || _sublist) { return result; } { @@ -147,7 +149,8 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia( content, _controller, _peer, - _topic ? _topic->rootId() : 0, + _topic ? _topic->rootId() : MsgId(), + _sublist ? _sublist->sublistPeer()->id : PeerId(), _migrated, type, tracker); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h index a1b257801b..65936bcae5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { class ForumTopic; +class SavedSublist; class PhotoMedia; } // namespace Data @@ -74,6 +75,7 @@ private: const not_null<PeerData*> _peer; PeerData * const _migrated = nullptr; Data::ForumTopic * const _topic = nullptr; + Data::SavedSublist * const _sublist = nullptr; PeerData *_reactionGroup = nullptr; diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 16e8b231f7..ac25b6c436 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -543,6 +543,7 @@ rpl::producer<int> KickedCountValue(not_null<ChannelData*> channel) { rpl::producer<int> SharedMediaCountValue( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Storage::SharedMediaType type) { auto aroundId = 0; @@ -553,6 +554,7 @@ rpl::producer<int> SharedMediaCountValue( SparseIdsMergedSlice::Key( peer->id, topicRootId, + monoforumPeerId, migrated ? migrated->id : 0, aroundId), type), diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index 52f0148bd4..16e9ed5d64 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -113,6 +113,7 @@ struct LinkWithUrl { [[nodiscard]] rpl::producer<int> SharedMediaCountValue( not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, PeerData *migrated, Storage::SharedMediaType type); [[nodiscard]] rpl::producer<int> CommonGroupsCountValue( diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp index 6f62b98975..cf235c55df 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_widget.h" #include "dialogs/ui/dialogs_stories_content.h" +#include "history/history.h" #include "info/profile/info_profile_inner_widget.h" #include "info/profile/info_profile_members.h" #include "ui/widgets/scroll_area.h" @@ -15,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" #include "data/data_user.h" #include "lang/lang_keys.h" #include "info/info_controller.h" @@ -25,6 +27,7 @@ Memento::Memento(not_null<Controller*> controller) : Memento( controller->peer(), controller->topic(), + controller->sublist(), controller->migratedPeerId(), { v::null }) { } @@ -33,20 +36,25 @@ Memento::Memento( not_null<PeerData*> peer, PeerId migratedPeerId, Origin origin) -: Memento(peer, nullptr, migratedPeerId, origin) { +: Memento(peer, nullptr, nullptr, migratedPeerId, origin) { } Memento::Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Origin origin) -: ContentMemento(peer, topic, migratedPeerId) +: ContentMemento(peer, topic, sublist, migratedPeerId) , _origin(origin) { } Memento::Memento(not_null<Data::ForumTopic*> topic) -: ContentMemento(topic->channel(), topic, 0) { +: ContentMemento(topic->channel(), topic, nullptr, 0) { +} + +Memento::Memento(not_null<Data::SavedSublist*> sublist) +: ContentMemento(sublist->owningHistory()->peer, nullptr, sublist, 0) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.h b/Telegram/SourceFiles/info/profile/info_profile_widget.h index b6e4da66a9..6d1bfcefbf 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.h +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.h @@ -35,6 +35,7 @@ public: PeerId migratedPeerId, Origin origin = { v::null }); explicit Memento(not_null<Data::ForumTopic*> topic); + explicit Memento(not_null<Data::SavedSublist*> sublist); object_ptr<ContentWidget> createWidget( QWidget *parent, @@ -56,6 +57,7 @@ private: Memento( not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, PeerId migratedPeerId, Origin origin); diff --git a/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp index 2fae27d580..b006dbbe28 100644 --- a/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp +++ b/Telegram/SourceFiles/info/requests_list/info_requests_list_widget.cpp @@ -188,7 +188,7 @@ std::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() { } Memento::Memento(not_null<PeerData*> peer) -: ContentMemento(peer, nullptr, PeerId()) { +: ContentMemento(peer, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp index 06143d2c13..298ef5f3e1 100644 --- a/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp +++ b/Telegram/SourceFiles/info/saved/info_saved_sublists_widget.cpp @@ -29,7 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Saved { SublistsMemento::SublistsMemento(not_null<Main::Session*> session) -: ContentMemento(session->user(), nullptr, PeerId()) { +: ContentMemento(session->user(), nullptr, nullptr, PeerId()) { } Section SublistsMemento::section() const { @@ -113,6 +113,7 @@ void SublistsWidget::setupOtherTypes() { controller(), peer, MsgId(), // topicRootId + PeerId(), // monoforumPeerId nullptr, // migrated buttonType, tracker); diff --git a/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp b/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp index 3646b5b683..003166ff98 100644 --- a/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp +++ b/Telegram/SourceFiles/info/similar_peers/info_similar_peers_widget.cpp @@ -438,7 +438,7 @@ std::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() { } Memento::Memento(not_null<PeerData*> peer) -: ContentMemento(peer, nullptr, PeerId()) { +: ContentMemento(peer, nullptr, nullptr, PeerId()) { } Section Memento::section() const { diff --git a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp index 0b5c01cd39..b79c168013 100644 --- a/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp +++ b/Telegram/SourceFiles/inline_bots/inline_bot_result.cpp @@ -347,9 +347,9 @@ bool Result::onChoose(Layout::ItemBase *layout) { Media::View::OpenRequest Result::openRequest() { using namespace Media::View; if (_document) { - return OpenRequest(nullptr, _document, nullptr, MsgId()); + return OpenRequest(nullptr, _document, nullptr, MsgId(), PeerId()); } else if (_photo) { - return OpenRequest(nullptr, _photo, nullptr, MsgId()); + return OpenRequest(nullptr, _photo, nullptr, MsgId(), PeerId()); } return {}; } diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index b4fbc4e844..09a7e2e383 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -894,6 +894,7 @@ void Instance::show( : nullptr; const auto item = (HistoryItem*)nullptr; const auto topicRootId = MsgId(0); + const auto monoforumPeerId = PeerId(0); if (event.context.startsWith("-photo")) { const auto id = event.context.mid(6).toULongLong(); const auto photo = _shownSession->data().photo(id); @@ -902,7 +903,8 @@ void Instance::show( controller, photo, item, - topicRootId + topicRootId, + monoforumPeerId }); } } else if (event.context.startsWith("-video")) { @@ -913,7 +915,8 @@ void Instance::show( controller, video, item, - topicRootId + topicRootId, + monoforumPeerId }); } } diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 7d13cde46c..58b7578c1b 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -40,7 +40,7 @@ QByteArray SessionSettings::serialize() const { + sizeof(qint32) * 11 + (_mutePeriods.size() * sizeof(quint64)) + sizeof(qint32) * 2 - + _hiddenPinnedMessages.size() * (sizeof(quint64) * 3) + + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4) + sizeof(qint32) + _groupEmojiSectionHidden.size() * sizeof(quint64) + sizeof(qint32) * 2; @@ -68,32 +68,33 @@ QByteArray SessionSettings::serialize() const { << qint32(_archiveInMainMenu.current() ? 1 : 0) << qint32(_skipArchiveInSearch.current() ? 1 : 0) << qint32(0) // old _mediaLastPlaybackPosition.size()); - << qint32(0) // very old _hiddenPinnedMessages.size()); + << qint32(0) // very very old _hiddenPinnedMessages.size()); << qint32(_dialogsFiltersEnabled ? 1 : 0) << qint32(_supportAllSilent ? 1 : 0) << qint32(_photoEditorHintShowsCount) - << qint32(0) // old _hiddenPinnedMessages.size()); + << qint32(0) // very old _hiddenPinnedMessages.size()); << qint32(_mutePeriods.size()); for (const auto &period : _mutePeriods) { stream << quint64(period); } stream << qint32(0) // old _skipPremiumStickersSet - << qint32(_hiddenPinnedMessages.size()); - for (const auto &[key, value] : _hiddenPinnedMessages) { - stream - << SerializePeerId(key.peerId) - << qint64(key.topicRootId.bare) - << qint64(value.bare); - } - stream + << qint32(0) // old _hiddenPinnedMessages.size()); << qint32(_groupEmojiSectionHidden.size()); for (const auto &peerId : _groupEmojiSectionHidden) { stream << SerializePeerId(peerId); } stream << qint32(_lastNonPremiumLimitDownload) - << qint32(_lastNonPremiumLimitUpload); + << qint32(_lastNonPremiumLimitUpload) + << qint32(_hiddenPinnedMessages.size()); + for (const auto &[key, value] : _hiddenPinnedMessages) { + stream + << SerializePeerId(key.peerId) + << qint64(key.topicRootId.bare) + << SerializePeerId(key.monoforumPeerId) + << qint64(value.bare); + } } Ensures(result.size() == size); @@ -401,6 +402,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { auto count = qint32(0); stream >> count; if (stream.status() == QDataStream::Ok) { + // Legacy. for (auto i = 0; i != count; ++i) { auto keyPeerId = quint64(); auto keyTopicRootId = qint64(); @@ -438,6 +440,33 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { >> lastNonPremiumLimitDownload >> lastNonPremiumLimitUpload; } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto keyPeerId = quint64(); + auto keyTopicRootId = qint64(); + auto keyMonoforumPeerId = quint64(); + auto value = qint64(); + stream + >> keyPeerId + >> keyTopicRootId + >> keyMonoforumPeerId + >> value; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()")); + return; + } + hiddenPinnedMessages.emplace(ThreadId{ + DeserializePeerId(keyPeerId), + keyTopicRootId, + DeserializePeerId(keyMonoforumPeerId), + }, value); + } + } + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for SessionSettings::addFromSerialized()")); @@ -595,16 +624,22 @@ rpl::producer<bool> SessionSettings::skipArchiveInSearchChanges() const { MsgId SessionSettings::hiddenPinnedMessageId( PeerId peerId, - MsgId topicRootId) const { - const auto i = _hiddenPinnedMessages.find({ peerId, topicRootId }); + MsgId topicRootId, + PeerId monoforumPeerId) const { + const auto i = _hiddenPinnedMessages.find({ + peerId, + topicRootId, + monoforumPeerId, + }); return (i != end(_hiddenPinnedMessages)) ? i->second : 0; } void SessionSettings::setHiddenPinnedMessageId( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId msgId) { - const auto id = ThreadId{ peerId, topicRootId }; + const auto id = ThreadId{ peerId, topicRootId, monoforumPeerId }; if (msgId) { _hiddenPinnedMessages[id] = msgId; } else { diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index c171968e2c..b88244aaa3 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -110,10 +110,12 @@ public: [[nodiscard]] MsgId hiddenPinnedMessageId( PeerId peerId, - MsgId topicRootId = 0) const; + MsgId topicRootId = 0, + PeerId monoforumPeerId = 0) const; void setHiddenPinnedMessageId( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, MsgId msgId); [[nodiscard]] bool dialogsFiltersEnabled() const { @@ -149,6 +151,7 @@ private: struct ThreadId { PeerId peerId; MsgId topicRootId; + PeerId monoforumPeerId; friend inline constexpr auto operator<=>( ThreadId, diff --git a/Telegram/SourceFiles/media/player/media_player_instance.cpp b/Telegram/SourceFiles/media/player/media_player_instance.cpp index de1932b7aa..c73c34e5ef 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.cpp +++ b/Telegram/SourceFiles/media/player/media_player_instance.cpp @@ -85,6 +85,7 @@ struct Instance::ShuffleData { std::vector<UniversalMsgId> playedIds; History *history = nullptr; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; History *migrated = nullptr; bool scheduled = false; int indexInPlayedIds = 0; @@ -247,6 +248,7 @@ void Instance::setHistory( if (history) { data->history = history->migrateToOrMe(); data->topicRootId = 0; + data->monoforumPeerId = 0; data->migrated = data->history->migrateFrom(); setSession(data, &history->session()); } else { @@ -349,6 +351,7 @@ bool Instance::validPlaylist(not_null<const Data*> data) const { const auto inSameDomain = [](const Key &a, const Key &b) { return (a.peerId == b.peerId) && (a.topicRootId == b.topicRootId) + && (a.monoforumPeerId == b.monoforumPeerId) && (a.migratedPeerId == b.migratedPeerId); }; const auto countDistanceInData = [&](const Key &a, const Key &b) { @@ -422,6 +425,7 @@ auto Instance::playlistKey(not_null<const Data*> data) const (item->isScheduled() ? SparseIdsMergedSlice::kScheduledTopicId : data->topicRootId), + data->monoforumPeerId, data->migrated ? data->migrated->peer->id : 0, universalId); } @@ -479,6 +483,7 @@ auto Instance::playlistOtherKey(not_null<const Data*> data) const return SliceKey( data->history->peer->id, data->topicRootId, + data->monoforumPeerId, data->migrated ? data->migrated->peer->id : 0, (data->playlistSlice->skippedBefore() == 0 ? ServerMaxMsgId - 1 @@ -905,6 +910,7 @@ void Instance::validateShuffleData(not_null<Data*> data) { && (key->topicRootId == SparseIdsMergedSlice::kScheduledTopicId); if (raw->history != data->history || raw->topicRootId != data->topicRootId + || raw->monoforumPeerId != data->monoforumPeerId || raw->migrated != data->migrated || raw->scheduled != scheduled) { raw->history = data->history; @@ -962,6 +968,7 @@ void Instance::validateShuffleData(not_null<Data*> data) { SliceKey( raw->history->peer->id, raw->topicRootId, + raw->monoforumPeerId, raw->migrated ? raw->migrated->peer->id : 0, last), data->overview), diff --git a/Telegram/SourceFiles/media/player/media_player_instance.h b/Telegram/SourceFiles/media/player/media_player_instance.h index ebe7d3d4cb..baf0599869 100644 --- a/Telegram/SourceFiles/media/player/media_player_instance.h +++ b/Telegram/SourceFiles/media/player/media_player_instance.h @@ -194,6 +194,7 @@ private: rpl::event_stream<> playlistChanges; History *history = nullptr; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; History *migrated = nullptr; Main::Session *session = nullptr; bool isPlaying = false; diff --git a/Telegram/SourceFiles/media/view/media_view_open_common.h b/Telegram/SourceFiles/media/view/media_view_open_common.h index 76cfd7748a..f512ca45b3 100644 --- a/Telegram/SourceFiles/media/view/media_view_open_common.h +++ b/Telegram/SourceFiles/media/view/media_view_open_common.h @@ -30,11 +30,13 @@ public: Window::SessionController *controller, not_null<PhotoData*> photo, HistoryItem *item, - MsgId topicRootId) + MsgId topicRootId, + PeerId monoforumPeerId) : _controller(controller) , _photo(photo) , _item(item) - , _topicRootId(topicRootId) { + , _topicRootId(topicRootId) + , _monoforumPeerId(monoforumPeerId) { } OpenRequest( Window::SessionController *controller, @@ -50,12 +52,14 @@ public: not_null<DocumentData*> document, HistoryItem *item, MsgId topicRootId, + PeerId monoforumPeerId, bool continueStreaming = false, crl::time startTime = 0) : _controller(controller) , _document(document) , _item(item) , _topicRootId(topicRootId) + , _monoforumPeerId(monoforumPeerId) , _continueStreaming(continueStreaming) , _startTime(startTime) { } @@ -92,6 +96,9 @@ public: [[nodiscard]] MsgId topicRootId() const { return _topicRootId; } + [[nodiscard]] PeerId monoforumPeerId() const { + return _monoforumPeerId; + } [[nodiscard]] DocumentData *document() const { return _document; @@ -129,6 +136,7 @@ private: PeerData *_peer = nullptr; HistoryItem *_item = nullptr; MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; std::optional<Data::CloudTheme> _cloudTheme = std::nullopt; bool _continueStreaming = false; crl::time _startTime = 0; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1ea8c3f560..99f7bc2825 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -366,6 +366,7 @@ struct OverlayWidget::PipWrap { struct OverlayWidget::ItemContext { not_null<HistoryItem*> item; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; }; struct OverlayWidget::StoriesContext { @@ -2675,7 +2676,8 @@ void OverlayWidget::handleDocumentClick() { findWindow(), _document, _message, - _topicRootId); + _topicRootId, + _monoforumPeerId); if (_document && _document->loading() && !_radial.animating()) { _radial.start(_documentMedia->progress()); } @@ -2921,13 +2923,22 @@ void OverlayWidget::showMediaOverview() { const auto topic = _topicRootId ? _history->peer->forumTopicFor(_topicRootId) : nullptr; + const auto sublist = _monoforumPeerId + ? _history->peer->monoforumSublistFor(_monoforumPeerId) + : nullptr; if (_topicRootId && !topic) { return; + } else if (_monoforumPeerId && !sublist) { + return; } window->showSection(_topicRootId ? std::make_shared<Info::Memento>( topic, Info::Section(*overviewType)) + : _monoforumPeerId + ? std::make_shared<Info::Memento>( + sublist, + Info::Section(*overviewType)) : std::make_shared<Info::Memento>( _history->peer, Info::Section(*overviewType))); @@ -3017,6 +3028,7 @@ auto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> { return SharedMediaKey{ _history->peer->id, MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId _migrated ? _migrated->peer->id : 0, SharedMediaType::ChatPhoto, _photo @@ -3032,6 +3044,7 @@ auto OverlayWidget::sharedMediaKey() const -> std::optional<SharedMediaKey> { (isScheduled ? SparseIdsMergedSlice::kScheduledTopicId : _topicRootId), + (isScheduled ? PeerId() : _monoforumPeerId), _migrated ? _migrated->peer->id : 0, type, (_message->history() == _history @@ -4609,6 +4622,7 @@ void OverlayWidget::switchToPip() { const auto document = _document; const auto messageId = _message ? _message->fullId() : FullMsgId(); const auto topicRootId = _topicRootId; + const auto monoforumPeerId = _monoforumPeerId; const auto closeAndContinue = [=] { _showAsPip = false; show(OpenRequest( @@ -4616,6 +4630,7 @@ void OverlayWidget::switchToPip() { document, document->owner().message(messageId), topicRootId, + monoforumPeerId, true)); }; _showAsPip = true; @@ -5699,9 +5714,9 @@ OverlayWidget::Entity OverlayWidget::entityForCollage(int index) const { return { v::null, nullptr }; } if (const auto document = std::get_if<DocumentData*>(&items[index])) { - return { *document, _message, _topicRootId }; + return { *document, _message, _topicRootId, _monoforumPeerId }; } else if (const auto photo = std::get_if<PhotoData*>(&items[index])) { - return { *photo, _message, _topicRootId }; + return { *photo, _message, _topicRootId, _monoforumPeerId }; } return { v::null, nullptr }; } @@ -5712,12 +5727,12 @@ OverlayWidget::Entity OverlayWidget::entityForItemId(const FullMsgId &itemId) co if (const auto item = _session->data().message(itemId)) { if (const auto media = item->media()) { if (const auto photo = media->photo()) { - return { photo, item, _topicRootId }; + return { photo, item, _topicRootId, _monoforumPeerId }; } else if (const auto document = media->document()) { - return { document, item, _topicRootId }; + return { document, item, _topicRootId, _monoforumPeerId }; } } - return { v::null, item, _topicRootId }; + return { v::null, item, _topicRootId, _monoforumPeerId }; } return { v::null, nullptr }; } @@ -5744,6 +5759,9 @@ void OverlayWidget::setContext( _history = _message->history(); _peer = _history->peer; _topicRootId = _peer->isForum() ? item->topicRootId : MsgId(); + _monoforumPeerId = _peer->amMonoforumAdmin() + ? item->monoforumPeerId + : PeerId(); setStoriesPeer(nullptr); } else if (const auto peer = std::get_if<not_null<PeerData*>>(&context)) { _peer = *peer; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index fb8efce90e..8787be47e4 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -170,6 +170,7 @@ private: not_null<DocumentData*>> data; HistoryItem *item = nullptr; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; }; enum class SavePhotoVideo { None, @@ -674,6 +675,7 @@ private: History *_migrated = nullptr; History *_history = nullptr; // if conversation photos or files overview MsgId _topicRootId = 0; + PeerId _monoforumPeerId = 0; PeerData *_peer = nullptr; UserData *_user = nullptr; // if user profile photos overview diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index d49fecb814..8303e0d033 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/sandbox.h" #include "core/core_settings.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" +#include "data/data_peer.h" #include "history/history.h" #include "history/history_item.h" #include "main/main_session.h" @@ -156,6 +158,7 @@ public: void clearAll(); void clearFromItem(not_null<HistoryItem*> item); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void clearNotification(NotificationId id); @@ -367,6 +370,8 @@ Manager::Private::Private(not_null<Manager*> manager) .sessionId = dict.lookup_value("session").get_uint64(), .peerId = PeerId(dict.lookup_value("peer").get_uint64()), .topicRootId = dict.lookup_value("topic").get_int64(), + .monoforumPeerId = dict.lookup_value( + "monoforumpeer").get_uint64(), }, .msgId = dict.lookup_value("msgid").get_int64(), }; @@ -531,6 +536,7 @@ void Manager::Private::showNotification( .sessionId = peer->session().uniqueId(), .peerId = peer->id, .topicRootId = info.topicRootId, + .monoforumPeerId = info.monoforumPeerId, }; const auto notificationId = NotificationId{ .contextId = key, @@ -591,6 +597,10 @@ void Manager::Private::showNotification( GLib::Variant::new_string("topic"), GLib::Variant::new_variant( GLib::Variant::new_int64(info.topicRootId.bare))), + GLib::Variant::new_dict_entry( + GLib::Variant::new_string("monoforumpeer"), + GLib::Variant::new_variant( + GLib::Variant::new_uint64(info.monoforumPeerId.value))), GLib::Variant::new_dict_entry( GLib::Variant::new_string("msgid"), GLib::Variant::new_variant( @@ -809,6 +819,7 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) { .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), + .monoforumPeerId = item->sublistPeerId(), }); if (i != _notifications.cend() && i->second.remove(item->id) @@ -825,6 +836,15 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) { }); } +void Manager::Private::clearFromSublist( + not_null<Data::SavedSublist*> sublist) { + _notifications.remove(ContextId{ + .sessionId = sublist->session().uniqueId(), + .peerId = sublist->owningHistory()->peer->id, + .monoforumPeerId = sublist->sublistPeer()->id, + }); +} + void Manager::Private::clearFromHistory(not_null<History*> history) { const auto sessionId = history->session().uniqueId(); const auto peerId = history->peer->id; @@ -889,6 +909,10 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { _private->clearFromTopic(topic); } +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + _private->clearFromSublist(sublist); +} + void Manager::doClearFromHistory(not_null<History*> history) { _private->clearFromHistory(history); } diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h index 8ab17f55b7..d2e740750f 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.h @@ -24,6 +24,7 @@ protected: void doClearAllFast() override; void doClearFromItem(not_null<HistoryItem*> item) override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; bool doSkipToast() const override; diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h index 2ffc5d6cb6..6af0292384 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.h @@ -25,6 +25,7 @@ protected: void doClearAllFast() override; void doClearFromItem(not_null<HistoryItem*> item) override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; QString accountNameSeparator() override; diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm index 3be39ef841..7ab8462891 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm @@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/platform/mac/base_utilities_mac.h" #include "base/random.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" +#include "data/data_peer.h" #include "history/history.h" #include "history/history_item.h" #include "ui/empty_userpic.h" @@ -131,6 +133,12 @@ using Manager = Platform::Notifications::Manager; return; } const auto notificationTopicRootId = [topicObject longLongValue]; + NSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@"monoforumpeer"]; + if (!monoforumPeerObject) { + LOG(("App Error: A notification with unknown monoforum peer was received")); + return; + } + const auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue]; NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"]; const auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL; @@ -140,6 +148,7 @@ using Manager = Platform::Notifications::Manager; .sessionId = notificationSessionId, .peerId = PeerId(notificationPeerId), .topicRootId = MsgId(notificationTopicRootId), + .monoforumPeerId = PeerId(notificationMonoforumPeerId), }, .msgId = notificationMsgId, }; @@ -210,6 +219,7 @@ public: void clearAll(); void clearFromItem(not_null<HistoryItem*> item); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void updateDelegate(); @@ -237,6 +247,9 @@ private: struct ClearFromTopic { ContextId contextId; }; + struct ClearFromSublist { + ContextId contextId; + } struct ClearFromHistory { ContextId partialContextId; }; @@ -250,6 +263,7 @@ private: using ClearTask = std::variant< ClearFromItem, ClearFromTopic, + ClearFromSublist, ClearFromHistory, ClearFromSession, ClearAll, @@ -311,6 +325,8 @@ void Manager::Private::showNotification( @"peer", [NSNumber numberWithLongLong:info.topicRootId.bare], @"topic", + [NSNumber numberWithUnsignedLongLong:info.monoforumPeerId.value], + @"monoforumpeer", [NSNumber numberWithLongLong:info.itemId.bare], @"msgid", [NSNumber numberWithUnsignedLongLong:_managerId], @@ -351,6 +367,7 @@ void Manager::Private::clearingThreadLoop() { auto clearAll = false; auto clearFromItems = base::flat_set<NotificationId>(); auto clearFromTopics = base::flat_set<ContextId>(); + auto clearFromSublists = base::flat_set<ContextId>(); auto clearFromHistories = base::flat_set<ContextId>(); auto clearFromSessions = base::flat_set<uint64>(); { @@ -368,6 +385,8 @@ void Manager::Private::clearingThreadLoop() { clearFromItems.emplace(value.id); }, [&](const ClearFromTopic &value) { clearFromTopics.emplace(value.contextId); + }, [&](const ClearFromSublist &value) { + clearFromSublists.emplace(value.contextId); }, [&](const ClearFromHistory &value) { clearFromHistories.emplace(value.partialContextId); }, [&](const ClearFromSession &value) { @@ -395,21 +414,35 @@ void Manager::Private::clearingThreadLoop() { return true; } const auto notificationTopicRootId = [topicObject longLongValue]; + NSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@"monoforumpeer"]; + if (!monoforumPeerObject) { + return true; + } + const auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue]; NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"]; const auto msgId = msgObject ? [msgObject longLongValue] : 0LL; const auto partialContextId = ContextId{ .sessionId = notificationSessionId, .peerId = PeerId(notificationPeerId), }; - const auto contextId = ContextId{ + const auto contextId = notificationTopicRootId + ? ContextId{ .sessionId = notificationSessionId, .peerId = PeerId(notificationPeerId), .topicRootId = MsgId(notificationTopicRootId), - }; + } + : notificationMonoforumPeerId + ? ContextId{ + .sessionId = notificationSessionId, + .peerId = PeerId(notificationPeerId), + .monoforumPeerId = PeerId(notificationMonoforumPeerId), + } + : partialContextId; const auto id = NotificationId{ contextId, MsgId(msgId) }; return clearFromSessions.contains(notificationSessionId) || clearFromHistories.contains(partialContextId) || clearFromTopics.contains(contextId) + || clearFromSublists.contains(contextId) || (msgId && clearFromItems.contains(id)); }; @@ -450,6 +483,7 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) { .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), + .monoforumPeerId = item->sublistPeerId(), }, item->id }); } @@ -461,6 +495,15 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) { } }); } +void Manager::Private::clearFromSublist( + not_null<Data::SavedSublist*> sublist) { + putClearTask(ClearFromSublist{ ContextId{ + .sessionId = sublist->session().uniqueId(), + .peerId = sublist->owningHistory()->peer->id, + .monoforumPeerId = sublist->sublistPeer()->id, + } }); +} + void Manager::Private::clearFromHistory(not_null<History*> history) { putClearTask(ClearFromHistory{ ContextId{ .sessionId = history->session().uniqueId(), @@ -511,6 +554,10 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { _private->clearFromTopic(topic); } +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + _private->clearFromSublist(sublist); +} + void Manager::doClearFromHistory(not_null<History*> history) { _private->clearFromHistory(history); } diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp index 9a4f028af4..d132ed4f32 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.cpp @@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/win/windows_dlls.h" #include "platform/win/specific_win.h" #include "data/data_forum_topic.h" +#include "data/data_saved_sublist.h" +#include "data/data_peer.h" #include "history/history.h" #include "history/history_item.h" #include "core/application.h" @@ -433,6 +435,7 @@ public: void clearAll(); void clearFromItem(not_null<HistoryItem*> item); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void beforeNotificationActivated(NotificationId id); @@ -508,6 +511,7 @@ void Manager::Private::clearFromItem(not_null<HistoryItem*> item) { .sessionId = item->history()->session().uniqueId(), .peerId = item->history()->peer->id, .topicRootId = item->topicRootId(), + .monoforumPeerId = item->sublistPeerId(), }); if (i == _notifications.cend()) { return; @@ -544,6 +548,27 @@ void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) { } } +void Manager::Private::clearFromSublist( + not_null<Data::SavedSublist*> sublist) { + if (!_notifier) { + return; + } + + const auto i = _notifications.find(ContextId{ + .sessionId = sublist->session().uniqueId(), + .peerId = sublist->owningHistory()->peer->id, + .monoforumPeerId = sublist->sublistPeer()->id, + }); + if (i != _notifications.cend()) { + const auto temp = base::take(i->second); + _notifications.erase(i); + + for (const auto &[msgId, notification] : temp) { + tryHide(notification); + } + } +} + void Manager::Private::clearFromHistory(not_null<History*> history) { if (!_notifier) { return; @@ -626,7 +651,9 @@ void Manager::Private::handleActivation(const ToastActivation &activation) { .contextId = ContextId{ .sessionId = parsed.value("session").toULongLong(), .peerId = PeerId(parsed.value("peer").toULongLong()), - .topicRootId = MsgId(parsed.value("topic").toLongLong()) + .topicRootId = MsgId(parsed.value("topic").toLongLong()), + .monoforumPeerId = PeerId( + parsed.value("monoforumpeer").toULongLong()), }, .msgId = MsgId(parsed.value("msg").toLongLong()), }; @@ -694,16 +721,18 @@ bool Manager::Private::showNotificationInTryCatch( .sessionId = peer->session().uniqueId(), .peerId = peer->id, .topicRootId = info.topicRootId, + .monoforumPeerId = info.monoforumPeerId, }; const auto notificationId = NotificationId{ .contextId = key, .msgId = info.itemId, }; - const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&msg=%5"_q + const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&monoforumpeer=%5&msg=%6"_q .arg(GetCurrentProcessId()) .arg(key.sessionId) .arg(key.peerId.value) .arg(info.topicRootId.bare) + .arg(info.monoforumPeerId.value) .arg(info.itemId.bare); const auto modern = Platform::IsWindows10OrGreater(); @@ -897,6 +926,10 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { _private->clearFromTopic(topic); } +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + _private->clearFromSublist(sublist); +} + void Manager::doClearFromHistory(not_null<History*> history) { _private->clearFromHistory(history); } diff --git a/Telegram/SourceFiles/platform/win/notifications_manager_win.h b/Telegram/SourceFiles/platform/win/notifications_manager_win.h index 5e99c760a2..7f9a6ce8ef 100644 --- a/Telegram/SourceFiles/platform/win/notifications_manager_win.h +++ b/Telegram/SourceFiles/platform/win/notifications_manager_win.h @@ -31,6 +31,7 @@ protected: void doClearAllFast() override; void doClearFromItem(not_null<HistoryItem*> item) override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; void onBeforeNotificationActivated(NotificationId id) override; diff --git a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp index d9421433e6..ebef828a98 100644 --- a/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp +++ b/Telegram/SourceFiles/storage/details/storage_settings_scheme.cpp @@ -1084,6 +1084,7 @@ bool ReadSetting( context.sessionSettings().setHiddenPinnedMessageId( DeserializePeerId(i.key()), MsgId(0), // topicRootId + PeerId(0), // monoforumPeerId MsgId(i.value())); } context.legacyRead = true; diff --git a/Telegram/SourceFiles/storage/storage_shared_media.cpp b/Telegram/SourceFiles/storage/storage_shared_media.cpp index 0326ad3566..89ce8f6d3b 100644 --- a/Telegram/SourceFiles/storage/storage_shared_media.cpp +++ b/Telegram/SourceFiles/storage/storage_shared_media.cpp @@ -27,6 +27,7 @@ auto SharedMedia::enforceLists(Key key) return SharedMediaSliceUpdate( key.peerId, key.topicRootId, + key.monoforumPeerId, type, update); }) | rpl::start_to_stream(_sliceUpdated, _lifetime); @@ -50,10 +51,20 @@ void SharedMedia::add(SharedMediaAddNew &&query) { if (topicIt != end(_lists)) { addByIt(topicIt); } + const auto monoforumPeerIt = query.monoforumPeerId + ? _lists.find({ query.peerId, MsgId(), query.monoforumPeerId }) + : end(_lists); + if (monoforumPeerIt != end(_lists)) { + addByIt(monoforumPeerIt); + } } void SharedMedia::add(SharedMediaAddExisting &&query) { - auto peerIt = enforceLists({ query.peerId, query.topicRootId }); + auto peerIt = enforceLists({ + query.peerId, + query.topicRootId, + query.monoforumPeerId, + }); for (auto index = 0; index != kSharedMediaTypeCount; ++index) { auto type = static_cast<SharedMediaType>(index); if (query.types.test(type)) { @@ -67,7 +78,11 @@ void SharedMedia::add(SharedMediaAddExisting &&query) { void SharedMedia::add(SharedMediaAddSlice &&query) { Expects(IsValidSharedMediaType(query.type)); - auto peerIt = enforceLists({ query.peerId, query.topicRootId }); + auto peerIt = enforceLists({ + query.peerId, + query.topicRootId, + query.monoforumPeerId, + }); auto index = static_cast<int>(query.type); peerIt->second[index].addSlice( std::move(query.messageIds), @@ -90,11 +105,17 @@ void SharedMedia::remove(SharedMediaRemoveOne &&query) { } void SharedMedia::remove(SharedMediaRemoveAll &&query) { - auto peerIt = _lists.lower_bound({ query.peerId, query.topicRootId }); + auto peerIt = _lists.lower_bound({ + query.peerId, + query.topicRootId, + query.monoforumPeerId, + }); while (peerIt != end(_lists) && peerIt->first.peerId == query.peerId && (!query.topicRootId - || peerIt->first.topicRootId == query.topicRootId)) { + || peerIt->first.topicRootId == query.topicRootId) + && (!query.monoforumPeerId + || peerIt->first.monoforumPeerId == query.monoforumPeerId)) { for (auto index = 0; index != kSharedMediaTypeCount; ++index) { auto type = static_cast<SharedMediaType>(index); if (query.types.test(type)) { @@ -118,13 +139,17 @@ void SharedMedia::invalidate(SharedMediaInvalidateBottom &&query) { } void SharedMedia::unload(SharedMediaUnloadThread &&query) { - _lists.erase({ query.peerId, query.topicRootId }); + _lists.erase({ query.peerId, query.topicRootId, query.monoforumPeerId }); } rpl::producer<SharedMediaResult> SharedMedia::query(SharedMediaQuery &&query) const { Expects(IsValidSharedMediaType(query.key.type)); - auto peerIt = _lists.find({ query.key.peerId, query.key.topicRootId }); + auto peerIt = _lists.find({ + query.key.peerId, + query.key.topicRootId, + query.key.monoforumPeerId, + }); if (peerIt != _lists.end()) { auto index = static_cast<int>(query.key.type); return peerIt->second[index].query(SparseIdsListQuery( @@ -141,7 +166,11 @@ rpl::producer<SharedMediaResult> SharedMedia::query(SharedMediaQuery &&query) co SharedMediaResult SharedMedia::snapshot(const SharedMediaQuery &query) const { Expects(IsValidSharedMediaType(query.key.type)); - auto peerIt = _lists.find({ query.key.peerId, query.key.topicRootId }); + auto peerIt = _lists.find({ + query.key.peerId, + query.key.topicRootId, + query.key.monoforumPeerId, + }); if (peerIt != _lists.end()) { auto index = static_cast<int>(query.key.type); return peerIt->second[index].snapshot(SparseIdsListQuery( @@ -155,7 +184,11 @@ SharedMediaResult SharedMedia::snapshot(const SharedMediaQuery &query) const { bool SharedMedia::empty(const SharedMediaKey &key) const { Expects(IsValidSharedMediaType(key.type)); - auto peerIt = _lists.find({ key.peerId, key.topicRootId }); + auto peerIt = _lists.find({ + key.peerId, + key.topicRootId, + key.monoforumPeerId, + }); if (peerIt != _lists.end()) { auto index = static_cast<int>(key.type); return peerIt->second[index].empty(); diff --git a/Telegram/SourceFiles/storage/storage_shared_media.h b/Telegram/SourceFiles/storage/storage_shared_media.h index 697bd284c3..09f5cce406 100644 --- a/Telegram/SourceFiles/storage/storage_shared_media.h +++ b/Telegram/SourceFiles/storage/storage_shared_media.h @@ -42,16 +42,19 @@ struct SharedMediaAddNew { SharedMediaAddNew( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaTypesMask types, MsgId messageId) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , messageId(messageId) , types(types) { } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; MsgId messageId = 0; SharedMediaTypesMask types; @@ -61,11 +64,13 @@ struct SharedMediaAddExisting { SharedMediaAddExisting( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaTypesMask types, MsgId messageId, MsgRange noSkipRange) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , messageId(messageId) , noSkipRange(noSkipRange) , types(types) { @@ -73,6 +78,7 @@ struct SharedMediaAddExisting { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; MsgId messageId = 0; MsgRange noSkipRange; SharedMediaTypesMask types; @@ -83,12 +89,14 @@ struct SharedMediaAddSlice { SharedMediaAddSlice( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, std::vector<MsgId> &&messageIds, MsgRange noSkipRange, std::optional<int> count = std::nullopt) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , messageIds(std::move(messageIds)) , noSkipRange(noSkipRange) , type(type) @@ -97,6 +105,7 @@ struct SharedMediaAddSlice { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; std::vector<MsgId> messageIds; MsgRange noSkipRange; SharedMediaType type = SharedMediaType::kCount; @@ -135,9 +144,18 @@ struct SharedMediaRemoveAll { , topicRootId(topicRootId) , types(types) { } + SharedMediaRemoveAll( + PeerId peerId, + PeerId monoforumPeerId, + SharedMediaTypesMask types = SharedMediaTypesMask::All()) + : peerId(peerId) + , monoforumPeerId(monoforumPeerId) + , types(types) { + } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaTypesMask types; }; @@ -154,10 +172,12 @@ struct SharedMediaKey { SharedMediaKey( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, MsgId messageId) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , type(type) , messageId(messageId) { } @@ -168,6 +188,7 @@ struct SharedMediaKey { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaType type = SharedMediaType::kCount; MsgId messageId = 0; @@ -195,16 +216,19 @@ struct SharedMediaSliceUpdate { SharedMediaSliceUpdate( PeerId peerId, MsgId topicRootId, + PeerId monoforumPeerId, SharedMediaType type, const SparseIdsSliceUpdate &data) : peerId(peerId) , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) , type(type) , data(data) { } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; SharedMediaType type = SharedMediaType::kCount; SparseIdsSliceUpdate data; }; @@ -212,13 +236,16 @@ struct SharedMediaSliceUpdate { struct SharedMediaUnloadThread { SharedMediaUnloadThread( PeerId peerId, - MsgId topicRootId) + MsgId topicRootId, + PeerId monoforumPeerId) : peerId(peerId) - , topicRootId(topicRootId) { + , topicRootId(topicRootId) + , monoforumPeerId(monoforumPeerId) { } PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; }; class SharedMedia { @@ -245,6 +272,7 @@ private: struct Key { PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; friend inline constexpr auto operator<=>(Key, Key) = default; }; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 36f27e4dc0..58dc24f52a 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/notify/data_notify_settings.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document_media.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" @@ -349,6 +350,15 @@ void System::registerThread(not_null<Data::Thread*> thread) { clearFromTopic(topic); }, i->second); } + } else if (const auto sublist = thread->asSublist()) { + const auto &[i, ok] = _watchedSublists.emplace( + sublist, + rpl::lifetime()); + if (ok) { + sublist->destroyed() | rpl::start_with_next([=] { + clearFromSublist(sublist); + }, i->second); + } } } @@ -426,6 +436,7 @@ void System::clearAll() { _waiters.clear(); _settingWaiters.clear(); _watchedTopics.clear(); + _watchedSublists.clear(); } void System::clearFromTopic(not_null<Data::ForumTopic*> topic) { @@ -445,6 +456,23 @@ void System::clearFromTopic(not_null<Data::ForumTopic*> topic) { showNext(); } +void System::clearFromSublist(not_null<Data::SavedSublist*> sublist) { + if (_manager) { + _manager->clearFromSublist(sublist); + } + + sublist->clearNotifications(); + _whenMaps.remove(sublist); + _whenAlerts.remove(sublist); + _waiters.remove(sublist); + _settingWaiters.remove(sublist); + + _watchedSublists.remove(sublist); + + _waitTimer.cancel(); + showNext(); +} + void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) { for (auto i = _whenMaps.begin(); i != _whenMaps.end();) { const auto thread = i->first; @@ -460,6 +488,8 @@ void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) { _settingWaiters.remove(thread); if (const auto topic = thread->asTopic()) { _watchedTopics.remove(topic); + } else if (const auto sublist = thread->asSublist()) { + _watchedSublists.remove(sublist); } } const auto clearFrom = [&](auto &map) { @@ -468,6 +498,8 @@ void System::clearForThreadIf(Fn<bool(not_null<Data::Thread*>)> predicate) { if (predicate(thread)) { if (const auto topic = thread->asTopic()) { _watchedTopics.remove(topic); + } else if (const auto sublist = thread->asSublist()) { + _watchedSublists.remove(sublist); } i = map.erase(i); } else { @@ -517,6 +549,15 @@ void System::clearIncomingFromTopic(not_null<Data::ForumTopic*> topic) { _whenAlerts.remove(topic); } +void System::clearIncomingFromSublist( + not_null<Data::SavedSublist*> sublist) { + if (_manager) { + _manager->clearFromSublist(sublist); + } + sublist->clearIncomingNotifications(); + _whenAlerts.remove(sublist); +} + void System::clearFromItem(not_null<HistoryItem*> item) { if (_manager) { _manager->clearFromItem(item); @@ -533,6 +574,7 @@ void System::clearAllFast() { _waiters.clear(); _settingWaiters.clear(); _watchedTopics.clear(); + _watchedSublists.clear(); } void System::checkDelayed() { @@ -1114,10 +1156,14 @@ void Manager::notificationActivated( history->peer, id.msgId); const auto topic = item ? item->topic() : nullptr; + const auto sublist = item ? item->savedSublist() : nullptr; if (!options.draft.text.isEmpty()) { const auto topicRootId = topic ? topic->rootId() : id.contextId.topicRootId; + const auto monoforumPeerId = (sublist && sublist->parentChat()) + ? sublist->sublistPeer()->id + : id.contextId.monoforumPeerId; const auto replyToId = (id.msgId > 0 && !history->peer->isUser() && id.msgId != topicRootId) @@ -1129,6 +1175,7 @@ void Manager::notificationActivated( FullReplyTo{ .messageId = replyToId, .topicRootId = topicRootId, + .monoforumPeerId = monoforumPeerId, }, MessageCursor{ length, @@ -1167,13 +1214,13 @@ Window::SessionController *Manager::openNotificationMessage( && item->isRegular() && (item->out() || (item->mentionsMe() && !history->peer->isUser())); const auto topic = item ? item->topic() : nullptr; - const auto sublist = (item && item->history()->amMonoforumAdmin()) - ? item->savedSublist() - : nullptr; + const auto sublist = item ? item->savedSublist() : nullptr; const auto guard = gsl::finally([&] { if (topic) { system()->clearFromTopic(topic); + } else if (sublist && sublist->parentChat()) { + system()->clearFromSublist(sublist); } else { system()->clearFromHistory(history); } @@ -1256,6 +1303,10 @@ void Manager::notificationReplied( const auto topicRootId = topic ? topic->rootId() : id.contextId.topicRootId; + const auto sublist = item ? item->savedSublist() : nullptr; + const auto monoforumPeerId = (sublist && sublist->parentChat()) + ? sublist->sublistPeer()->id + : id.contextId.monoforumPeerId; auto message = Api::MessageToSend(Api::SendAction(history)); message.textWithTags = reply; @@ -1268,6 +1319,7 @@ void Manager::notificationReplied( message.action.replyTo = { .messageId = { replyToId ? history->peer->id : 0, replyToId }, .topicRootId = topic ? topic->rootId() : 0, + .monoforumPeerId = monoforumPeerId, }; message.action.clearDraft = false; history->session().api().sendMessage(std::move(message)); @@ -1293,16 +1345,21 @@ void NativeManager::doShowNotification(NotificationFields &&fields) { && !reactionFrom && (item->out() || peer->isSelf()) && item->isFromScheduled(); - const auto topicWithChat = [&] { + const auto subWithChat = [&] { const auto name = peer->name(); const auto topic = item->topic(); - return topic ? (topic->title() + u" ("_q + name + ')') : name; + const auto sublist = item->savedSublist(); + return topic + ? (topic->title() + u" ("_q + name + ')') + : (sublist && sublist->parentChat()) + ? (sublist->sublistPeer()->shortName() + u" ("_q + name + ')') + : name; }; const auto title = options.hideNameAndPhoto ? AppName.utf16() : (scheduled && peer->isSelf()) ? tr::lng_notification_reminder(tr::now) - : topicWithChat(); + : subWithChat(); const auto fullTitle = addTargetAccountName(title, &peer->session()); const auto subtitle = reactionFrom ? (reactionFrom != peer ? reactionFrom->name() : QString()) @@ -1341,6 +1398,9 @@ void NativeManager::doShowNotification(NotificationFields &&fields) { doShowNativeNotification({ .peer = item->history()->peer, .topicRootId = item->topicRootId(), + .monoforumPeerId = (item->history()->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId()), .itemId = item->id, .title = scheduled ? WrapFromScheduled(fullTitle) : fullTitle, .subtitle = subtitle, diff --git a/Telegram/SourceFiles/window/notifications_manager.h b/Telegram/SourceFiles/window/notifications_manager.h index 9caa1dd264..b91afcfca0 100644 --- a/Telegram/SourceFiles/window/notifications_manager.h +++ b/Telegram/SourceFiles/window/notifications_manager.h @@ -17,6 +17,7 @@ class History; namespace Data { class Session; class ForumTopic; +class SavedSublist; class Thread; struct ItemNotification; enum class ItemNotificationType; @@ -109,8 +110,10 @@ public: void checkDelayed(); void schedule(Data::ItemNotification notification); void clearFromTopic(not_null<Data::ForumTopic*> topic); + void clearFromSublist(not_null<Data::SavedSublist*> sublist); void clearFromHistory(not_null<History*> history); void clearIncomingFromTopic(not_null<Data::ForumTopic*> topic); + void clearIncomingFromSublist(not_null<Data::SavedSublist*> sublist); void clearIncomingFromHistory(not_null<History*> history); void clearFromSession(not_null<Main::Session*> session); void clearFromItem(not_null<HistoryItem*> item); @@ -221,6 +224,9 @@ private: base::flat_map< not_null<Data::ForumTopic*>, rpl::lifetime> _watchedTopics; + base::flat_map< + not_null<Data::SavedSublist*>, + rpl::lifetime> _watchedSublists; int _lastForwardedCount = 0; uint64 _lastHistorySessionId = 0; @@ -237,6 +243,7 @@ public: uint64 sessionId = 0; PeerId peerId = 0; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; friend inline auto operator<=>( const ContextId&, @@ -279,6 +286,9 @@ public: void clearFromTopic(not_null<Data::ForumTopic*> topic) { doClearFromTopic(topic); } + void clearFromSublist(not_null<Data::SavedSublist*> sublist) { + doClearFromSublist(sublist); + } void clearFromHistory(not_null<History*> history) { doClearFromHistory(history); } @@ -341,6 +351,8 @@ protected: virtual void doClearAllFast() = 0; virtual void doClearFromItem(not_null<HistoryItem*> item) = 0; virtual void doClearFromTopic(not_null<Data::ForumTopic*> topic) = 0; + virtual void doClearFromSublist( + not_null<Data::SavedSublist*> sublist) = 0; virtual void doClearFromHistory(not_null<History*> history) = 0; virtual void doClearFromSession(not_null<Main::Session*> session) = 0; [[nodiscard]] virtual bool doSkipToast() const = 0; @@ -377,6 +389,7 @@ public: struct NotificationInfo { not_null<PeerData*> peer; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; MsgId itemId = 0; QString title; QString subtitle; @@ -426,6 +439,8 @@ protected: } void doClearFromTopic(not_null<Data::ForumTopic*> topic) override { } + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override { + } void doClearFromHistory(not_null<History*> history) override { } void doClearFromSession(not_null<Main::Session*> session) override { diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index 44bf48131b..dcfd89dec5 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/ui_utility.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_forum_topic.h" #include "data/stickers/data_custom_emoji.h" @@ -236,6 +237,7 @@ void Manager::showNextFromQueue() { this, queued.history, queued.topicRootId, + queued.monoforumPeerId, queued.peer, queued.author, queued.item, @@ -383,7 +385,25 @@ void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) { } } for (const auto ¬ification : _notifications) { - if (notification->unlinkHistory(history, topicRootId)) { + if (notification->unlinkHistory(history, topicRootId, PeerId())) { + _positionsOutdated = true; + } + } + showNextFromQueue(); +} + +void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) { + const auto history = sublist->owningHistory(); + const auto sublistPeerId = sublist->sublistPeer()->id; + for (auto i = _queuedNotifications.begin(); i != _queuedNotifications.cend();) { + if (i->history == history && i->monoforumPeerId == sublistPeerId) { + i = _queuedNotifications.erase(i); + } else { + ++i; + } + } + for (const auto ¬ification : _notifications) { + if (notification->unlinkHistory(history, MsgId(), sublistPeerId)) { _positionsOutdated = true; } } @@ -618,6 +638,7 @@ Notification::Notification( not_null<Manager*> manager, not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, not_null<PeerData*> peer, const QString &author, HistoryItem *item, @@ -633,6 +654,8 @@ Notification::Notification( , _history(history) , _topic(history->peer->forumTopicFor(topicRootId)) , _topicRootId(topicRootId) +, _sublist(history->peer->monoforumSublistFor(monoforumPeerId)) +, _monoforumPeerId(monoforumPeerId) , _userpicView(_peer->createUserpicView()) , _author(author) , _reaction(reaction) @@ -1149,10 +1172,14 @@ void Notification::changeHeight(int newHeight) { manager()->changeNotificationHeight(this, newHeight); } -bool Notification::unlinkHistory(History *history, MsgId topicRootId) { +bool Notification::unlinkHistory( + History *history, + MsgId topicRootId, + PeerId monoforumPeerId) { const auto unlink = _history && (history == _history || !history) - && (topicRootId == _topicRootId || !topicRootId); + && (topicRootId == _topicRootId || !topicRootId) + && (monoforumPeerId == _monoforumPeerId || !monoforumPeerId); if (unlink) { hideFast(); _history = nullptr; diff --git a/Telegram/SourceFiles/window/notifications_manager_default.h b/Telegram/SourceFiles/window/notifications_manager_default.h index 7c81282355..219ce8d83a 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.h +++ b/Telegram/SourceFiles/window/notifications_manager_default.h @@ -70,6 +70,7 @@ private: void doClearAll() override; void doClearAllFast() override; void doClearFromTopic(not_null<Data::ForumTopic*> topic) override; + void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override; void doClearFromHistory(not_null<History*> history) override; void doClearFromSession(not_null<Main::Session*> session) override; void doClearFromItem(not_null<HistoryItem*> item) override; @@ -111,6 +112,7 @@ private: not_null<History*> history; MsgId topicRootId = 0; + PeerId monoforumPeerId = 0; not_null<PeerData*> peer; Data::ReactionId reaction; QString author; @@ -203,6 +205,7 @@ public: not_null<Manager*> manager, not_null<History*> history, MsgId topicRootId, + PeerId monoforumPeerId, not_null<PeerData*> peer, const QString &author, HistoryItem *item, @@ -231,7 +234,10 @@ public: // Called only by Manager. bool unlinkItem(HistoryItem *del); - bool unlinkHistory(History *history = nullptr, MsgId topicRootId = 0); + bool unlinkHistory( + History *history = nullptr, + MsgId topicRootId = 0, + PeerId monoforumPeerId = 0); bool unlinkSession(not_null<Main::Session*> session); bool checkLastInput( bool hasReplyingNotifications, @@ -285,6 +291,8 @@ private: History *_history = nullptr; Data::ForumTopic *_topic = nullptr; MsgId _topicRootId = 0; + Data::SavedSublist *_sublist = nullptr; + PeerId _monoforumPeerId = 0; Ui::PeerUserpicView _userpicView; QString _author; Data::ReactionId _reaction; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 183526a0a7..fc68d80a5a 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -3040,14 +3040,18 @@ void HidePinnedBar( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Fn<void()> onHidden) { const auto callback = crl::guard(navigation, [=](Fn<void()> &&close) { close(); auto &session = peer->session(); - const auto migrated = topicRootId ? nullptr : peer->migrateFrom(); + const auto migrated = (topicRootId || monoforumPeerId) + ? nullptr + : peer->migrateFrom(); const auto top = Data::ResolveTopPinnedId( peer, topicRootId, + monoforumPeerId, migrated); const auto universal = !top ? MsgId(0) @@ -3058,6 +3062,7 @@ void HidePinnedBar( session.settings().setHiddenPinnedMessageId( peer->id, topicRootId, + monoforumPeerId, universal); session.saveSettingsDelayed(); if (onHidden) { @@ -3091,6 +3096,7 @@ void UnpinAllMessages( const auto history = strong->owningHistory(); const auto topicRootId = strong->topicRootId(); const auto sublist = strong->asSublist(); + const auto monoforumPeerId = strong->monoforumPeerId(); using Flag = MTPmessages_UnpinAllMessages::Flag; api->request(MTPmessages_UnpinAllMessages( MTP_flags((topicRootId ? Flag::f_top_msg_id : Flag()) @@ -3104,7 +3110,7 @@ void UnpinAllMessages( if (offset > 0) { self(self); } else { - history->unpinMessagesFor(topicRootId); + history->unpinMessagesFor(topicRootId, monoforumPeerId); } }).send(); }; diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 06c5ce626c..15435b34ba 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -218,6 +218,7 @@ void HidePinnedBar( not_null<Window::SessionNavigation*> navigation, not_null<PeerData*> peer, MsgId topicRootId, + PeerId monoforumPeerId, Fn<void()> onHidden); void UnpinAllMessages( not_null<Window::SessionNavigation*> navigation, diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 1fe5d152cc..5a49bab04d 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2927,8 +2927,12 @@ void SessionController::openPhoto( if (openSharedStory(item) || openFakeItemStory(message.id, stories)) { return; } - _window->openInMediaView( - Media::View::OpenRequest(this, photo, item, message.topicRootId)); + _window->openInMediaView(Media::View::OpenRequest( + this, + photo, + item, + message.topicRootId, + message.monoforumPeerId)); } void SessionController::openPhoto( @@ -2963,11 +2967,17 @@ void SessionController::openDocument( document, item, message.topicRootId, + message.monoforumPeerId, false, usedTimestamp)); return; } - Data::ResolveDocument(this, document, item, message.topicRootId); + Data::ResolveDocument( + this, + document, + item, + message.topicRootId, + message.monoforumPeerId); } bool SessionController::openSharedStory(HistoryItem *item) { diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 86b1e6f3be..fdeef9a221 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -523,6 +523,7 @@ public: struct MessageContext { FullMsgId id; MsgId topicRootId; + PeerId monoforumPeerId; }; void openPhoto( not_null<PhotoData*> photo, From cd05586d51170ffce14d633efb421ce336b9e4e6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 2 Jun 2025 16:43:39 +0400 Subject: [PATCH 097/310] Fix display of pinned messages in sublists. --- .../SourceFiles/boxes/pin_messages_box.cpp | 4 +- .../view/history_view_chat_section.cpp | 40 +++++++++++++------ .../history/view/history_view_chat_section.h | 1 + .../view/history_view_context_menu.cpp | 6 ++- .../view/history_view_pinned_section.cpp | 4 ++ .../view/history_view_pinned_section.h | 1 + Telegram/SourceFiles/mainwidget.cpp | 2 + Telegram/SourceFiles/window/section_memento.h | 5 +++ 8 files changed, 48 insertions(+), 15 deletions(-) diff --git a/Telegram/SourceFiles/boxes/pin_messages_box.cpp b/Telegram/SourceFiles/boxes/pin_messages_box.cpp index 07d041a7d1..9ba027e20c 100644 --- a/Telegram/SourceFiles/boxes/pin_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/pin_messages_box.cpp @@ -83,7 +83,9 @@ void PinMessageBox( object->setAllowTextLines(); state->pinForPeer = Ui::MakeWeak(object.data()); return object; - } else if (!pinningOld && (peer->isChat() || peer->isMegagroup())) { + } else if (!pinningOld + && (peer->isChat() || peer->isMegagroup()) + && !peer->isMonoforum()) { auto object = object_ptr<Ui::Checkbox>( box, tr::lng_pinned_notify(tr::now), diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 39afe08693..9d2b01041f 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -151,12 +151,16 @@ void ChatMemento::setFromTopic(not_null<Data::ForumTopic*> topic) { } -Data::ForumTopic *ChatMemento::topicForRemoveRequests() const {// #TODO monoforums +Data::ForumTopic *ChatMemento::topicForRemoveRequests() const { return _id.repliesRootId ? _id.history->peer->forumTopicFor(_id.repliesRootId) : nullptr; } +Data::SavedSublist *ChatMemento::sublistForRemoveRequests() const { + return _id.sublist; +} + void ChatMemento::setReadInformation( MsgId inboxReadTillId, int unreadCount, @@ -656,7 +660,8 @@ void ChatWidget::subscribeToPinnedMessages() { ) | rpl::start_with_next([=](const Data::EntryUpdate &update) { if (_pinnedTracker && (update.flags & EntryUpdateFlag::HasPinnedMessages) - && (_topic == update.entry.get())) { + && (_topic == update.entry.get() + || _sublist == update.entry.get())) { checkPinnedBarState(); } }, lifetime()); @@ -1833,7 +1838,7 @@ void ChatWidget::refreshUnreadCountBadge(std::optional<int> count) { } void ChatWidget::updatePinnedViewer() { - if (_scroll->isHidden() || !_topic || !_pinnedTracker) { + if (_scroll->isHidden() || (!_topic && !_sublist) || !_pinnedTracker) { return; } const auto visibleBottom = _scroll->scrollTop() + _scroll->height(); @@ -1866,7 +1871,7 @@ void ChatWidget::updatePinnedViewer() { void ChatWidget::checkLastPinnedClickedIdReset( int wasScrollTop, int nowScrollTop) { - if (_scroll->isHidden() || !_topic) { + if (_scroll->isHidden() || (!_topic && !_sublist)) { return; } if (wasScrollTop < nowScrollTop && _pinnedClickedId) { @@ -1948,15 +1953,16 @@ void ChatWidget::setupTranslateBar() { } void ChatWidget::setupPinnedTracker() { - Expects(_topic != nullptr); + Expects(_topic || _sublist); - _pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(_topic); + const auto thread = _topic ? (Data::Thread*)_topic : _sublist; + _pinnedTracker = std::make_unique<HistoryView::PinnedTracker>(thread); _pinnedBar = nullptr; SharedMediaViewer( - &_topic->session(), + &session(), Storage::SharedMediaKey( - _topic->channel()->id, + _peer->id, _repliesRootId, _monoforumPeerId, Storage::SharedMediaType::Pinned, @@ -1966,7 +1972,7 @@ void ChatWidget::setupPinnedTracker() { ) | rpl::filter([=](const SparseIdsSlice &result) { return result.fullCount().has_value(); }) | rpl::start_with_next([=](const SparseIdsSlice &result) { - _topic->setHasPinnedMessages(*result.fullCount() != 0); + thread->setHasPinnedMessages(*result.fullCount() != 0); if (result.skippedAfter() == 0) { auto &settings = _history->session().settings(); const auto peerId = _peer->id; @@ -1985,7 +1991,7 @@ void ChatWidget::setupPinnedTracker() { } } checkPinnedBarState(); - }, _topicLifetime); + }, lifetime()); } void ChatWidget::checkPinnedBarState() { @@ -2138,8 +2144,9 @@ void ChatWidget::refreshPinnedBarButton(bool many, HistoryItem *item) { if (!id.message) { return; } + const auto thread = _topic ? (Data::Thread*)_topic : _sublist; controller()->showSection( - std::make_shared<PinnedMemento>(_topic, id.message.msg)); + std::make_shared<PinnedMemento>(thread, id.message.msg)); }; const auto context = [copy = _inner](FullMsgId itemId) { if (const auto raw = copy.data()) { @@ -2591,6 +2598,7 @@ void ChatWidget::subscribeToSublist() { }, lifetime()); unreadCountUpdated(); + subscribeToPinnedMessages(); } void ChatWidget::unreadCountUpdated() { @@ -2782,7 +2790,10 @@ void ChatWidget::updateInnerVisibleArea() { } void ChatWidget::updatePinnedVisibility() { - if (!_loaded || !_repliesRootId) { + if (_sublist) { + setPinnedVisibility(true); + return; + } else if (!_loaded || !_repliesRootId) { return; } else if (!_topic && (!_repliesRoot || _repliesRoot->isEmpty())) { setPinnedVisibility(!_repliesRoot); @@ -2803,7 +2814,10 @@ void ChatWidget::updatePinnedVisibility() { } void ChatWidget::setPinnedVisibility(bool shown) { - if (animatingShow() || !_repliesRootId) { + if (animatingShow()) { + } else if (_sublist) { + _repliesRootVisible = shown; + } else if (!_repliesRootId) { return; } else if (!_topic) { if (!_repliesRootViewInitScheduled) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.h b/Telegram/SourceFiles/history/view/history_view_chat_section.h index 593553ce25..a92c17ab3a 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.h +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.h @@ -503,6 +503,7 @@ public: } Data::ForumTopic *topicForRemoveRequests() const override; + Data::SavedSublist *sublistForRemoveRequests() const override; [[nodiscard]] not_null<ListMemento*> list() { return &_list; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index b28099fe58..d156b8fc73 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -756,8 +756,12 @@ bool AddPinMessageAction( return false; } const auto topic = item->topic(); + const auto sublist = item->savedSublist(); if (context != Context::History && context != Context::Pinned) { - if (context != Context::Replies || !topic) { + if ((context != Context::Replies || !topic) + && (context != Context::Monoforum + || !sublist + || !item->history()->amMonoforumAdmin())) { return false; } } diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp index cf82e31e2b..446d957617 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.cpp @@ -90,6 +90,10 @@ Data::ForumTopic *PinnedMemento::topicForRemoveRequests() const { return _thread->asTopic(); } +Data::SavedSublist *PinnedMemento::sublistForRemoveRequests() const { + return _thread->asSublist(); +} + PinnedWidget::PinnedWidget( QWidget *parent, not_null<Window::SessionController*> controller, diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 75956e403d..be7a515d72 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -225,6 +225,7 @@ public: } Data::ForumTopic *topicForRemoveRequests() const override; + Data::SavedSublist *sublistForRemoveRequests() const override; private: const not_null<Data::Thread*> _thread; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 587172d8f6..65001c5cdb 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -223,6 +223,8 @@ StackItemSection::StackItemSection( rpl::producer<> StackItemSection::sectionRemoveRequests() const { if (const auto topic = _memento->topicForRemoveRequests()) { return rpl::merge(_memento->removeRequests(), topic->destroyed()); + } else if (const auto sublist = _memento->sublistForRemoveRequests()) { + return rpl::merge(_memento->removeRequests(), sublist->destroyed()); } return _memento->removeRequests(); } diff --git a/Telegram/SourceFiles/window/section_memento.h b/Telegram/SourceFiles/window/section_memento.h index e794d024eb..d139928a99 100644 --- a/Telegram/SourceFiles/window/section_memento.h +++ b/Telegram/SourceFiles/window/section_memento.h @@ -13,6 +13,7 @@ class LayerWidget; namespace Data { class ForumTopic; +class SavedSublist; } // namespace Data namespace Window { @@ -41,6 +42,10 @@ public: [[nodiscard]] virtual Data::ForumTopic *topicForRemoveRequests() const { return nullptr; } + [[nodiscard]] virtual auto sublistForRemoveRequests() const + -> Data::SavedSublist* { + return nullptr; + } [[nodiscard]] virtual rpl::producer<> removeRequests() const { return rpl::never<>(); } From 6c80d443b9dfc4af379e3882a86bc35841814e6b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 2 Jun 2025 17:18:01 +0400 Subject: [PATCH 098/310] Better entry point for Direct Messages. --- Telegram/Resources/langs/lang.strings | 1 + .../SourceFiles/data/data_saved_sublist.cpp | 20 +------------------ .../info/profile/info_profile_actions.cpp | 18 ----------------- .../SourceFiles/window/window_peer_menu.cpp | 20 +++++++++++++++++++ 4 files changed, 22 insertions(+), 37 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 877674edd9..37b72d4b3b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1491,6 +1491,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_hide_participants_about" = "Switch this on to hide the list of members in this group. Admins will remain visible."; "lng_profile_view_channel" = "View Channel"; "lng_profile_view_discussion" = "View discussion"; +"lng_profile_direct_messages" = "Direct messages"; "lng_profile_join_channel" = "Join Channel"; "lng_profile_join_group" = "Join Group"; "lng_profile_apply_to_join_group" = "Apply to Join Group"; diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index ce0a3f4147..594a1d1935 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -445,7 +445,6 @@ void SavedSublist::setInboxReadTill( && _inboxReadTillId >= _list.front()) { unreadCount = 0; } - const auto wasUnreadCount = _unreadCount; if (_unreadCount.current() != unreadCount && (changed || unreadCount.has_value())) { setUnreadCount(unreadCount); @@ -593,24 +592,7 @@ std::optional<int> SavedSublist::computeUnreadCountLocally( } void SavedSublist::requestUnreadCount() { - if (_reloadUnreadCountRequestId) { - return; - } - //const auto weak = base::make_weak(this); // #TODO monoforum - //const auto session = &_parent->session(); - //const auto apply = [weak](MsgId readTill, int unreadCount) { - // if (const auto strong = weak.get()) { - // strong->setInboxReadTill(readTill, unreadCount); - // } - //}; - //_reloadUnreadCountRequestId = session->api().request( - // ... - //).done([=](const ... &result) { - // if (weak) { - // _reloadUnreadCountRequestId = 0; - // } - // ... - //}).send(); + parent()->requestSublist(sublistPeer()); } void SavedSublist::readTill(not_null<HistoryItem*> item) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 6a902916eb..8be1249d40 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -2188,24 +2188,6 @@ Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( std::move(viewChannel), tracker); - auto viewDirectVisible = channel->flagsValue() | rpl::map([=] { - return channel->monoforumLink() != nullptr; - }) | rpl::distinct_until_changed(); - auto viewDirect = [=] { - if (const auto linked = channel->monoforumLink()) { - window->showPeerHistory(linked); - //if (const auto monoforum = linked->monoforum()) { - // window->showMonoforum(monoforum); - //} - } - }; - AddMainButton( // #TODO monoforum - _wrap, - rpl::single(u"View Direct Messages"_q), - std::move(viewDirectVisible), - std::move(viewDirect), - tracker); - return tracker; } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index fc68d80a5a..2a5da8ae11 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -293,6 +293,7 @@ private: void addThemeEdit(); void addBlockUser(); void addViewDiscussion(); + void addDirectMessages(); void addToggleTopicClosed(); void addExportChat(); void addTranslate(); @@ -872,6 +873,23 @@ void Filler::addViewDiscussion() { }, &st::menuIconDiscussion); } +void Filler::addDirectMessages() { + const auto channel = _peer->asBroadcast(); + if (!channel) { + return; + } + const auto monoforum = channel->broadcastMonoforum(); + if (!monoforum || !monoforum->amMonoforumAdmin()) { + return; + } + const auto navigation = _controller; + _addAction(tr::lng_profile_direct_messages(tr::now), [=] { + navigation->showPeerHistory( + monoforum, + Window::SectionShow::Way::Forward); + }, &st::menuIconChatDiscuss); +} + void Filler::addExportChat() { if (_thread->asTopic() || !_peer->canExportChatHistory()) { return; @@ -1461,6 +1479,7 @@ void Filler::fillHistoryActions() { addCreatePoll(); addThemeEdit(); addViewDiscussion(); + addDirectMessages(); addExportChat(); addTranslate(); addReport(); @@ -1485,6 +1504,7 @@ void Filler::fillProfileActions() { addManageTopic(); addToggleTopicClosed(); addViewDiscussion(); + addDirectMessages(); addExportChat(); addToggleFolder(); addBlockUser(); From dd8fdfc3d43eb76a2b0e17a6bf93f641c42899bb Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 2 Jun 2025 18:19:16 +0400 Subject: [PATCH 099/310] Allow forwarding polls to monoforums. --- .../data/data_chat_participant_status.cpp | 6 --- Telegram/SourceFiles/data/data_peer.cpp | 2 + .../SourceFiles/data/data_peer_values.cpp | 6 --- .../dialogs/dialogs_inner_widget.cpp | 41 +++++++++++-------- .../dialogs/dialogs_inner_widget.h | 6 ++- .../SourceFiles/dialogs/dialogs_widget.cpp | 15 ++++++- .../dialogs/ui/dialogs_message_view.cpp | 9 +++- .../SourceFiles/history/history_widget.cpp | 7 +++- .../view/history_view_subsection_tabs.cpp | 1 + 9 files changed, 60 insertions(+), 33 deletions(-) diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index b3e318d076..2a85f44d13 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -156,12 +156,6 @@ bool CanSendAnyOf( } return false; } else if (const auto channel = peer->asChannel()) { - if (channel->isMonoforum()) { - rights &= ~ChatRestriction::SendPolls; - if (!rights) { - return false; - } - } using Flag = ChannelDataFlag; const auto allowed = channel->amIn() || ((channel->flags() & Flag::HasLink) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 8f639faf0f..7359ef5af4 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -668,6 +668,8 @@ bool PeerData::canCreatePolls() const { && !user->isSupport() && !user->isRepliesChat() && !user->isVerifyCodes()); + } else if (isMonoforum()) { + return false; } return Data::CanSend(this, ChatRestriction::SendPolls); } diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 1d65c3b2ec..0c435d5347 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -274,12 +274,6 @@ inline auto DefaultRestrictionValue( | Flag::Forbidden | Flag::Creator | Flag::Broadcast; - if (channel->isMonoforum()) { - rights &= ~ChatRestriction::SendPolls; - if (!rights) { - return rpl::single(false); - } - } return rpl::combine( PeerFlagsValue(channel, mask), AdminRightValue( diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 6eb211320a..312c830d1e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1464,11 +1464,7 @@ bool InnerWidget::isRowActive( } return false; } else if (const auto sublist = entry.key.sublist()) { - if (!sublist->parentChat()) { - // In case we're viewing a Saved Messages sublist, - // we want to highlight the Saved Messages row as active. - return key.history() && key.peer()->isSelf(); - } + return key.history() && key.history() == sublist->owningHistory(); } return false; } @@ -1909,9 +1905,14 @@ RowDescriptor InnerWidget::computeChatPreviewRow() const { auto result = computeChosenRow(); if (const auto peer = result.key.peer()) { const auto topicId = _pressedTopicJump - ? _pressedTopicJumpRootId // #TODO monoforums - : 0; - if (const auto topic = peer->forumTopicFor(topicId)) { + ? _pressedTopicJumpRootId + : MsgId(); + const auto sublistPeerId = _pressedTopicJump + ? _pressedSublistJumpPeerId + : PeerId(); + if (const auto sublist = peer->monoforumSublistFor(sublistPeerId)) { + return { sublist, FullMsgId() }; + } else if (const auto topic = peer->forumTopicFor(topicId)) { return { topic, FullMsgId() }; } } @@ -2422,6 +2423,7 @@ void InnerWidget::mousePressReleased( auto collapsedPressed = _collapsedPressed; setCollapsedPressed(-1); const auto pressedTopicRootId = _pressedTopicJumpRootId; + const auto pressedSublistPeerId = _pressedSublistJumpPeerId; const auto pressedTopicJump = _pressedTopicJump; const auto pressedRightButton = _pressedRightButton; auto pressed = _pressed; @@ -2505,7 +2507,10 @@ void InnerWidget::mousePressReleased( } else if (pressedRightButton && peerSearchPressed >= 0) { showSponsoredMenu(peerSearchPressed, globalPosition); } else { - chooseRow(modifiers, pressedTopicRootId); + chooseRow( + modifiers, + pressedTopicRootId, + pressedSublistPeerId); } } } @@ -2557,6 +2562,9 @@ void InnerWidget::setPressed( : nullptr; const auto item = history ? history->chatListMessage() : nullptr; _pressedTopicJumpRootId = item ? item->topicRootId() : MsgId(); + _pressedSublistJumpPeerId = item + ? item->sublistPeerId() + : PeerId(); } } } @@ -2603,6 +2611,9 @@ void InnerWidget::setFilteredPressed( : nullptr; const auto item = history ? history->chatListMessage() : nullptr; _pressedTopicJumpRootId = item ? item->topicRootId() : MsgId(); + _pressedSublistJumpPeerId = item + ? item->sublistPeerId() + : PeerId(); } } } @@ -4763,7 +4774,8 @@ bool InnerWidget::isUserpicPressOnWide() const { bool InnerWidget::chooseRow( Qt::KeyboardModifiers modifiers, - MsgId pressedTopicRootId) { + MsgId pressedTopicRootId, + PeerId pressedSublistPeerId) { if (chooseHashtag()) { return true; } else if (_selectedMorePosts) { @@ -4805,12 +4817,9 @@ bool InnerWidget::chooseRow( if (!chosen.message.fullId) { if (const auto history = chosen.key.history()) { if (history->peer->forum()) { - if (pressedTopicRootId) { - chosen.message.fullId = { - history->peer->id, - pressedTopicRootId, - }; - } + chosen.topicJumpRootId = pressedTopicRootId; + } else if (history->peer->amMonoforumAdmin()) { + chosen.sublistJumpPeerId = pressedSublistPeerId; } } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index d2a21405cb..c3c4a6b033 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -84,6 +84,8 @@ enum class ChatTypeFilter : uchar; struct ChosenRow { Key key; Data::MessagePosition message; + MsgId topicJumpRootId; + PeerId sublistJumpPeerId; QByteArray sponsoredRandomId; bool userpicClick : 1 = false; bool filteredRow : 1 = false; @@ -163,7 +165,8 @@ public: void chatPreviewShown(bool shown, RowDescriptor row = {}); bool chooseRow( Qt::KeyboardModifiers modifiers = {}, - MsgId pressedTopicRootId = {}); + MsgId pressedTopicRootId = {}, + PeerId pressedSublistPeerId = {}); void scrollToEntry(const RowDescriptor &entry); @@ -543,6 +546,7 @@ private: Row *_selected = nullptr; Row *_pressed = nullptr; MsgId _pressedTopicJumpRootId; + PeerId _pressedSublistJumpPeerId; bool _selectedTopicJump = false; bool _pressedTopicJump = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 6b6227b9a3..ce0912c934 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -860,7 +860,10 @@ void Widget::chosenRow(const ChosenRow &row) { const auto history = row.key.history(); const auto topicJump = history - ? history->peer->forumTopicFor(row.message.fullId.msg) + ? history->peer->forumTopicFor(row.topicJumpRootId) + : nullptr; + const auto sublistJump = history + ? history->peer->monoforumSublistFor(row.sublistJumpPeerId) : nullptr; if (topicJump) { @@ -880,6 +883,16 @@ void Widget::chosenRow(const ChosenRow &row) { Window::SectionShow::Way::ClearStack); } return; + } else if (sublistJump) { + if (row.newWindow) { + controller()->showInNewWindow(Window::SeparateId(sublistJump)); + } else { + controller()->showThread( + sublistJump, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } + return; } else if (const auto topic = row.key.topic()) { auto params = Window::SectionShow( Window::SectionShow::Way::ClearStack); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index 87fd7232cb..955377480f 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -267,11 +267,18 @@ int MessageView::countWidth() const { auto result = 0; if (!_senderCache.isEmpty()) { result += _senderCache.maxWidth(); - if (!_imagesCache.empty()) { + if (!_imagesCache.empty() && !_leftIcon) { result += st::dialogsMiniPreviewSkip + st::dialogsMiniPreviewRight; } } + if (_leftIcon) { + const auto w = _leftIcon->icon.icon.width(); + result += w + + (_imagesCache.empty() + ? _leftIcon->skipText + : _leftIcon->skipMedia); + } if (!_imagesCache.empty()) { result += (_imagesCache.size() * (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip)) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 7d41300c38..f7367cba1b 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2126,7 +2126,10 @@ void HistoryWidget::setupDirectMessageButton() { }, _directMessage->lifetime()); _directMessage->setClickedCallback([=] { if (const auto channel = _peer ? _peer->asChannel() : nullptr) { - if (const auto monoforum = channel->monoforumLink()) { + if (channel->invitePeekExpires()) { + controller()->showToast( + tr::lng_channel_invite_private(tr::now)); + } else if (const auto monoforum = channel->monoforumLink()) { controller()->showPeerHistory( monoforum, Window::SectionShow::Way::Forward); @@ -6038,7 +6041,7 @@ bool HistoryWidget::showSendingFilesError( return true; } -MsgId HistoryWidget::resolveReplyToTopicRootId() { // #TODO monoforums +MsgId HistoryWidget::resolveReplyToTopicRootId() { Expects(_peer != nullptr); const auto replyToInfo = replyTo(); diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index dec9438836..b69a8604b5 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -660,6 +660,7 @@ bool SubsectionTabs::switchTo( } _shadow->setParent(parent); _shadow->show(); + _refreshed.fire({}); return true; } From 7f7b764f7b1ddeb95d5afe2b5bbfe26a441c74c3 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 3 Jun 2025 10:27:39 +0400 Subject: [PATCH 100/310] Allow ton:// links in webapps. --- Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp | 3 ++- Telegram/lib_webview | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 92664d402e..aa99af3ee8 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -1459,7 +1459,8 @@ bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) { if (Core::InternalPassportLink(local)) { return true; } else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive) - && !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)) { + && !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive) + && !local.startsWith(u"ton://"_q, Qt::CaseInsensitive)) { return false; } const auto bot = _bot; diff --git a/Telegram/lib_webview b/Telegram/lib_webview index b9f9e981c8..04c45d069f 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit b9f9e981c81a78120a023822d2aa908d38b6795f +Subproject commit 04c45d069fc0088740b9637bc5da414ee82be198 From f4582ddf36f60582271573db923521454227b68a Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 3 Jun 2025 14:16:07 +0400 Subject: [PATCH 101/310] Correctly mark monoforum chats as read. --- Telegram/SourceFiles/data/data_histories.cpp | 1 + .../SourceFiles/data/data_saved_messages.cpp | 21 +++++++ .../SourceFiles/data/data_saved_messages.h | 5 ++ .../SourceFiles/data/data_saved_sublist.cpp | 23 ++++++- .../SourceFiles/data/data_saved_sublist.h | 1 + Telegram/SourceFiles/history/history.cpp | 62 +++++++++++++++++++ Telegram/SourceFiles/history/history.h | 7 ++- .../SourceFiles/history/history_widget.cpp | 3 +- .../view/history_view_subsection_tabs.cpp | 20 ++++-- 9 files changed, 135 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 47804bba96..ad0f7b7283 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -711,6 +711,7 @@ void Histories::sendReadRequest(not_null<History*> history, State &state) { } else { Assert(!state->sentReadTill || state->sentReadTill > tillId); } + history->validateMonoforumUnread(tillId); sendReadRequests(); finish(); }; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index ab3ac5f4eb..8108530c3b 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -521,6 +521,27 @@ auto SavedMessages::recentSublists() const return _lastSublists; } +void SavedMessages::markUnreadCountsUnknown(MsgId readTillId) { + for (const auto &[peer, sublist] : _sublists) { + if (sublist->unreadCountCurrent() > 0) { + sublist->setInboxReadTill(readTillId, std::nullopt); + } + } +} + +void SavedMessages::updateUnreadCounts( + MsgId readTillId, + const base::flat_map<not_null<SavedSublist*>, int> &counts) { + for (const auto &[peer, sublist] : _sublists) { + const auto raw = sublist.get(); + const auto i = counts.find(raw); + const auto count = (i != end(counts)) ? i->second : 0; + if (raw->unreadCountCurrent() != count) { + raw->setInboxReadTill(readTillId, count); + } + } +} + rpl::producer<> SavedMessages::destroyed() const { if (!_parentChat) { return rpl::never<>(); diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index c7568326c5..34800518b5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -70,6 +70,11 @@ public: [[nodiscard]] auto recentSublists() const -> const std::vector<not_null<SavedSublist*>> &; + void markUnreadCountsUnknown(MsgId readTillId); + void updateUnreadCounts( + MsgId readTillId, + const base::flat_map<not_null<SavedSublist*>, int> &counts); + void clear(); [[nodiscard]] rpl::lifetime &lifetime(); diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index 594a1d1935..c8271b6824 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_saved_sublist.h" +#include "api/api_unread_things.h" #include "apiwrap.h" #include "core/application.h" #include "data/data_changes.h" @@ -61,7 +62,7 @@ SavedSublist::~SavedSublist() { if (_readRequestTimer.isActive()) { sendReadTillRequest(); } - // session().api().unreadThings().cancelRequests(this); + session().api().unreadThings().cancelRequests(this); } bool SavedSublist::inMonoforum() const { @@ -444,6 +445,9 @@ void SavedSublist::setInboxReadTill( && !_list.empty() && _inboxReadTillId >= _list.front()) { unreadCount = 0; + } else if (_lastServerMessage.value_or(nullptr) + && (*_lastServerMessage)->id <= newReadTillId) { + unreadCount = 0; } if (_unreadCount.current() != unreadCount && (changed || unreadCount.has_value())) { @@ -646,10 +650,11 @@ void SavedSublist::sendReadTillRequest() { const auto api = &_parent->session().api(); api->request(base::take(_readRequestId)).cancel(); + _sentReadTill = computeInboxReadTillFull(); _readRequestId = api->request(MTPmessages_ReadSavedHistory( parentChat->input, sublistPeer()->input, - MTP_int(computeInboxReadTillFull()) + MTP_int(_sentReadTill.bare) )).done(crl::guard(this, [=] { _readRequestId = 0; reloadUnreadCountIfNeeded(); @@ -708,6 +713,20 @@ void SavedSublist::applyMonoforumDialog( setInboxReadTill( data.vread_inbox_max_id().v, data.vunread_count().v); + if (!unreadCountKnown() && !_readRequestId) { + // We got read_inbox_max_id < than our current inboxReadTillId, + // we need either to send a read request with this new value, + // or to downgrade inboxReadTillId locally. + if (_sentReadTill < computeInboxReadTillFull()) { + sendReadTillRequest(); + } else { + // Just if nothing else helps. + _inboxReadTillId = 0; + setInboxReadTill( + data.vread_inbox_max_id().v, + data.vunread_count().v); + } + } setOutboxReadTill(data.vread_outbox_max_id().v); unreadReactions().setCount(data.vunread_reactions_count().v); setUnreadMark(data.is_unread_mark()); diff --git a/Telegram/SourceFiles/data/data_saved_sublist.h b/Telegram/SourceFiles/data/data_saved_sublist.h index 095fdacf1e..1361f207fb 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.h +++ b/Telegram/SourceFiles/data/data_saved_sublist.h @@ -184,6 +184,7 @@ private: base::Timer _readRequestTimer; mtpRequestId _readRequestId = 0; + MsgId _sentReadTill = 0; mtpRequestId _reloadUnreadCountRequestId = 0; diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 8061df17fb..b419b414b4 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -3108,8 +3108,70 @@ void History::applyDialogTopMessage(MsgId topMessageId) { } } +void History::tryMarkMonoforumIntervalRead( + MsgId wasInboxReadBefore, + MsgId nowInboxReadBefore) { + if (!amMonoforumAdmin() || (nowInboxReadBefore <= wasInboxReadBefore)) { + return; + } else if (loadedAtBottom() && nowInboxReadBefore >= minMsgId()) { + // Count for each sublist how many messages are still not read. + auto counts = base::flat_map<not_null<Data::SavedSublist*>, int>(); + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (!item->isRegular() || item->id < nowInboxReadBefore) { + continue; + } + if (const auto sublist = item->savedSublist()) { + ++counts[sublist]; + } + } + } + if (const auto monoforum = peer->monoforum()) { + monoforum->updateUnreadCounts(nowInboxReadBefore - 1, counts); + } + } else if (minMsgId() <= wasInboxReadBefore + && maxMsgId() >= nowInboxReadBefore) { + // Count for each sublist how many messages were read. + for (const auto &block : blocks) { + for (const auto &message : block->messages) { + const auto item = message->data(); + if (!item->isRegular() || item->id < wasInboxReadBefore) { + continue; + } else if (item->id >= nowInboxReadBefore) { + break; + } + if (const auto sublist = item->savedSublist()) { + const auto unread = sublist->unreadCountCurrent(); + if (unread > 0) { + sublist->setInboxReadTill(item->id, unread - 1); + } + } + } + } + } else { + // We can't invalidate sublist unread counts here, because no read + // request was yet sent to the server (so it can't return correct + // values yet), we need to do that after we send read request. + _flags |= Flag::MonoforumUnreadInvalidatePending; + } +} + +void History::validateMonoforumUnread(MsgId readTillId) { + if (!(_flags & Flag::MonoforumUnreadInvalidatePending)) { + return; + } + _flags &= ~Flag::MonoforumUnreadInvalidatePending; + if (!amMonoforumAdmin()) { + return; + } else if (const auto monoforum = peer->monoforum()) { + monoforum->markUnreadCountsUnknown(readTillId); + } +} + void History::setInboxReadTill(MsgId upTo) { if (_inboxReadBefore) { + tryMarkMonoforumIntervalRead(*_inboxReadBefore, upTo + 1); accumulate_max(*_inboxReadBefore, upTo + 1); } else { _inboxReadBefore = upTo + 1; diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index cd6e707372..7ebacdfc82 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -430,6 +430,10 @@ public: // Interface for Data::Histories. void setInboxReadTill(MsgId upTo); std::optional<int> countStillUnreadLocal(MsgId readTillId) const; + void tryMarkMonoforumIntervalRead( + MsgId wasInboxReadBefore, + MsgId nowInboxReadBefore); + void validateMonoforumUnread(MsgId readTillId); [[nodiscard]] bool isTopPromoted() const; @@ -466,7 +470,7 @@ public: private: friend class HistoryBlock; - enum class Flag : uchar { + enum class Flag : ushort { HasPendingResizedItems = (1 << 0), PendingAllItemsResize = (1 << 1), IsTopPromoted = (1 << 2), @@ -475,6 +479,7 @@ private: FakeUnreadWhileOpened = (1 << 5), HasPinnedMessages = (1 << 6), ResolveChatListMessage = (1 << 7), + MonoforumUnreadInvalidatePending = (1 << 8), }; using Flags = base::flags<Flag>; friend inline constexpr auto is_flag_type(Flag) { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index f7367cba1b..2d883e5704 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -3632,10 +3632,11 @@ void HistoryWidget::unreadCountUpdated() { }); } else { const auto hideCounter = _history->isForum() - || _history->amMonoforumAdmin() || !_history->trackUnreadMessages(); _cornerButtons.updateJumpDownVisibility(hideCounter ? 0 + : _history->amMonoforumAdmin() + ? _history->chatListUnreadState().messages : _history->chatListBadgesState().unreadCounter); } } diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index b69a8604b5..886044ed20 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -349,7 +349,6 @@ void SubsectionTabs::setupSlider( } } } - slider->setSections({ .tabs = std::move(sections), .context = Core::TextContext({ @@ -591,11 +590,24 @@ void SubsectionTabs::refreshSlice() { const auto push = [&](not_null<Data::Thread*> thread) { const auto topic = thread->asTopic(); const auto sublist = thread->asSublist(); + const auto badges = [&] { + if (!topic && !sublist) { + return Dialogs::BadgesState(); + } else if (thread->chatListUnreadState().known) { + return thread->chatListBadgesState(); + } + const auto i = ranges::find(_slice, thread, &Item::thread); + if (i != end(_slice)) { + // While the unread count is unknown (possibly loading) + // we can preserve the old badges state, because it won't + // glitch that way when we stop knowing it for a moment. + return i->badges; + } + return thread->chatListBadgesState(); + }(); slice.push_back({ .thread = thread, - .badges = ((topic || sublist) - ? thread->chatListBadgesState() - : Dialogs::BadgesState()), + .badges = badges, .iconId = topic ? topic->iconId() : DocumentId(), .name = thread->chatListName(), }); From d156de05a54190b6ca20c4db41f3576eadd4cd53 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 3 Jun 2025 14:31:23 +0400 Subject: [PATCH 102/310] Allow replying in monoforum while not in it. --- Telegram/SourceFiles/history/history.cpp | 5 +---- Telegram/SourceFiles/history/history_inner_widget.cpp | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index b419b414b4..77000409e0 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2896,10 +2896,7 @@ bool History::shouldBeInChatList() const { } else if (isPinnedDialog(FilterId())) { return true; } else if (const auto channel = peer->asChannel()) { - if (channel->isMonoforum()) { - return !lastMessageKnown() - || (lastMessage() != nullptr); - } else if (!channel->amIn()) { + if (!channel->amIn()) { return isTopPromoted(); } } else if (const auto chat = peer->asChat()) { diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 89743aa851..ede9c81b08 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -4970,9 +4970,7 @@ bool CanSendReply(not_null<const HistoryItem*> item) { return false; } else if (const auto channel = peer->asChannel()) { if (const auto sublist = item->savedSublist()) { - if (sublist->sublistPeer() == peer) { - return false; - } + return (sublist->sublistPeer() != peer); } return channel->amIn(); } From 41ed487d5ef0e0cf7445bad70d6e97ee9cf10228 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 3 Jun 2025 14:59:59 +0400 Subject: [PATCH 103/310] Improve opening ChatWidget at the end. --- .../history/view/history_view_chat_section.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 9d2b01041f..ec2049dbb1 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2663,12 +2663,10 @@ void ChatWidget::recountChatWidth() { void ChatWidget::updateControlsGeometry() { const auto contentWidth = width(); - const auto newScrollTop = _scroll->isHidden() + const auto newScrollDelta = _scroll->isHidden() ? std::nullopt : _scroll->scrollTop() - ? base::make_optional(_scroll->scrollTop() - + topDelta() - + _scrollTopDelta) + ? base::make_optional(topDelta() + _scrollTopDelta) : 0; _topBar->resizeToWidth(contentWidth); _topBarShadow->resize(contentWidth, st::lineWidth); @@ -2726,6 +2724,9 @@ void ChatWidget::updateControlsGeometry() { } _scroll->move(tabsLeftSkip, top); if (!_scroll->isHidden()) { + const auto newScrollTop = (newScrollDelta && _scroll->scrollTop()) + ? (_scroll->scrollTop() + *newScrollDelta) + : std::optional<int>(); if (newScrollTop) { _scroll->scrollToY(*newScrollTop); } From dfb66001045e11d37679ce59231b2a5651201e40 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 3 Jun 2025 16:13:42 +0400 Subject: [PATCH 104/310] Fix loading of horizontal avatar strip. --- .../SourceFiles/history/view/history_view_subsection_tabs.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 886044ed20..5757d7b89e 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -201,7 +201,9 @@ void SubsectionTabs::setupSlider( rpl::merge( scroll->scrolls(), _scrollCheckRequests.events(), - scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty) + (vertical + ? scroll->heightValue() + : scroll->widthValue()) | rpl::skip(1) | rpl::map_to(rpl::empty) ) | rpl::start_with_next([=] { const auto full = vertical ? scroll->height() : scroll->width(); const auto scrollValue = vertical From d775760f988c0b1cda08398d4804733ff66506e8 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 3 Jun 2025 17:17:36 +0400 Subject: [PATCH 105/310] Support nice monoforum userpics. --- .../SourceFiles/boxes/add_contact_box.cpp | 2 +- .../boxes/moderate_messages_box.cpp | 2 +- Telegram/SourceFiles/data/data_peer.cpp | 22 ++++- Telegram/SourceFiles/data/data_peer.h | 19 ++-- .../history/history_item_components.cpp | 2 +- .../view/history_view_chat_preview.cpp | 2 +- .../view/history_view_top_bar_widget.cpp | 2 +- .../info/profile/info_profile_cover.cpp | 2 +- .../main/main_session_settings.cpp | 7 +- .../settings/settings_websites.cpp | 26 ++--- .../ui/controls/subsection_tabs_slider.cpp | 1 + .../ui/controls/userpic_button.cpp | 72 +++++++------- .../SourceFiles/ui/controls/userpic_button.h | 11 ++- .../SourceFiles/ui/dynamic_thumbnails.cpp | 23 ++--- Telegram/SourceFiles/ui/empty_userpic.cpp | 94 +++++++++++++++++++ Telegram/SourceFiles/ui/empty_userpic.h | 11 +++ Telegram/SourceFiles/ui/userpic_view.cpp | 17 ++-- Telegram/SourceFiles/ui/userpic_view.h | 13 ++- .../window/notifications_manager_default.cpp | 2 +- 19 files changed, 221 insertions(+), 109 deletions(-) diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 020550774f..792eff79b4 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -559,7 +559,7 @@ void GroupInfoBox::prepare() { &_navigation->parentController()->window(), Ui::UserpicButton::Role::ChoosePhoto, st::defaultUserpicButton, - (_type == Type::Forum)); + (_type == Type::Forum) ? Ui::PeerUserpicShape::Forum : Ui::PeerUserpicShape::Auto); _photo->showCustomOnChosen(); _title.create( this, diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index 29f7a22c0e..071b9dd684 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -604,7 +604,7 @@ void DeleteChatBox(not_null<Ui::GenericBox*> box, not_null<PeerData*> peer) { container, userpicPeer, st::mainMenuUserpic, - peer->userpicForceForumShape()); + peer->userpicShape()); userpic->showSavedMessagesOnSelf(true); Ui::IconWithTitle( container, diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 7359ef5af4..86fb382f0e 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -427,20 +427,30 @@ QImage *PeerData::userpicCloudImage(Ui::PeerUserpicView &view) const { void PeerData::paintUserpic( Painter &p, Ui::PeerUserpicView &view, - const PaintUserpicContext &context) const { + PaintUserpicContext context) const { if (const auto broadcast = monoforumBroadcast()) { + if (context.shape == Ui::PeerUserpicShape::Auto) { + context.shape = Ui::PeerUserpicShape::Monoforum; + } broadcast->paintUserpic(p, view, context); return; } const auto size = context.size; const auto cloud = userpicCloudImage(view); const auto ratio = style::DevicePixelRatio(); + if (context.shape == Ui::PeerUserpicShape::Auto) { + context.shape = isForum() + ? Ui::PeerUserpicShape::Forum + : isMonoforum() + ? Ui::PeerUserpicShape::Monoforum + : Ui::PeerUserpicShape::Circle; + } Ui::ValidateUserpicCache( view, cloud, cloud ? nullptr : ensureEmptyUserpic().get(), size * ratio, - context.forumLayout); + context.shape); p.drawImage(QRect(context.position, QSize(size, size)), view.cached); } @@ -1176,8 +1186,12 @@ not_null<const PeerData*> PeerData::userpicPaintingPeer() const { return const_cast<PeerData*>(this)->userpicPaintingPeer(); } -bool PeerData::userpicForceForumShape() const { - return monoforumBroadcast() != nullptr; +Ui::PeerUserpicShape PeerData::userpicShape() const { + return isForum() + ? Ui::PeerUserpicShape::Forum + : isMonoforum() + ? Ui::PeerUserpicShape::Monoforum + : Ui::PeerUserpicShape::Circle; } ChannelData *PeerData::monoforumBroadcast() const { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 104e8ba10b..207b7361e2 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -186,6 +186,12 @@ struct PeerBarDetails { int paysPerMessage = 0; }; +struct PaintUserpicContext { + QPoint position; + int size = 0; + Ui::PeerUserpicShape shape = Ui::PeerUserpicShape::Auto; +}; + class PeerData { protected: PeerData(not_null<Data::Session*> owner, PeerId id); @@ -310,7 +316,7 @@ public: [[nodiscard]] not_null<const PeerData*> migrateToOrMe() const; [[nodiscard]] not_null<PeerData*> userpicPaintingPeer(); [[nodiscard]] not_null<const PeerData*> userpicPaintingPeer() const; - [[nodiscard]] bool userpicForceForumShape() const; + [[nodiscard]] Ui::PeerUserpicShape userpicShape() const; // isMonoforum() ? monoforumLink() : nullptr [[nodiscard]] ChannelData *monoforumBroadcast() const; @@ -348,15 +354,10 @@ public: bool hasVideo); void setUserpicPhoto(const MTPPhoto &data); - struct PaintUserpicContext { - QPoint position; - int size = 0; - bool forumLayout = false; - }; void paintUserpic( Painter &p, Ui::PeerUserpicView &view, - const PaintUserpicContext &context) const; + PaintUserpicContext context) const; void paintUserpic( Painter &p, Ui::PeerUserpicView &view, @@ -367,7 +368,9 @@ public: paintUserpic(p, view, { .position = { x, y }, .size = size, - .forumLayout = !forceCircle && (isForum() || isMonoforum()), + .shape = (forceCircle + ? Ui::PeerUserpicShape::Circle + : Ui::PeerUserpicShape::Auto), }); } void paintUserpicLeft( diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 28eea2027d..edb6a099da 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -196,7 +196,7 @@ bool HiddenSenderInfo::paintCustomUserpic( image.isNull() ? nullptr : &image, image.isNull() ? &emptyUserpic : nullptr, size * style::DevicePixelRatio(), - false); + Ui::PeerUserpicShape::Circle); p.drawImage(QRect(x, y, size, size), view.cached); return valid; } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp index 05ad41d477..5e9d3eecdd 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_preview.cpp @@ -368,7 +368,7 @@ void Item::setupTop() { _top.get(), _thread->peer()->userpicPaintingPeer(), st::previewUserpic, - _thread->peer()->userpicForceForumShape()); + _thread->peer()->userpicShape()); if (userpic) { userpic->showSavedMessagesOnSelf(true); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index 9f7a8ac8be..e45537d964 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -936,7 +936,7 @@ void TopBarWidget::refreshInfoButton() { Ui::UserpicButton::Role::Custom, Ui::UserpicButton::Source::PeerPhoto, st::topBarInfoButton, - infoPeer->userpicForceForumShape()); + infoPeer->userpicShape()); info->showSavedMessagesOnSelf(true); _info.destroy(); _info = std::move(info); diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 84786a9ad0..05a11f3008 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -632,7 +632,7 @@ Cover::Cover( Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, _st.photo, - _peer->userpicForceForumShape())) + _peer->userpicShape())) , _changePersonal((role == Role::Info || topic || !_peer->isUser() diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 58b7578c1b..88f60b5af8 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -39,11 +39,10 @@ QByteArray SessionSettings::serialize() const { + Serialize::bytearraySize(autoDownload) + sizeof(qint32) * 11 + (_mutePeriods.size() * sizeof(quint64)) - + sizeof(qint32) * 2 - + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4) - + sizeof(qint32) + + sizeof(qint32) * 3 + _groupEmojiSectionHidden.size() * sizeof(quint64) - + sizeof(qint32) * 2; + + sizeof(qint32) * 3 + + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4); auto result = QByteArray(); result.reserve(size); diff --git a/Telegram/SourceFiles/settings/settings_websites.cpp b/Telegram/SourceFiles/settings/settings_websites.cpp index 836f20ab4d..7f495998f0 100644 --- a/Telegram/SourceFiles/settings/settings_websites.cpp +++ b/Telegram/SourceFiles/settings/settings_websites.cpp @@ -119,7 +119,7 @@ void InfoBox( data.bot, st::websiteBigUserpic)), st::sessionBigCoverPadding)->entity(); - userpic->forceForumShape(true); + userpic->overrideShape(Ui::PeerUserpicShape::Forum); userpic->setAttribute(Qt::WA_TransparentForMouseEvents); const auto nameWrap = box->addRow( @@ -224,25 +224,11 @@ PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) { const auto peer = _data.bot; auto userpic = _userpic = peer->createUserpicView(); return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { - const auto ratio = style::DevicePixelRatio(); - if (const auto cloud = peer->userpicCloudImage(userpic)) { - Ui::ValidateUserpicCache( - userpic, - cloud, - nullptr, - size * ratio, - true); - p.drawImage(QRect(x, y, size, size), userpic.cached); - } else { - if (_emptyUserpic.isNull()) { - _emptyUserpic = PeerData::GenerateUserpicImage( - peer, - _userpic, - size * ratio, - size * ratio * Ui::ForumUserpicRadiusMultiplier()); - } - p.drawImage(QRect(x, y, size, size), _emptyUserpic); - } + peer->paintUserpic(p, _userpic, { + .position = QPoint(x, y), + .size = size, + .shape = Ui::PeerUserpicShape::Forum, + }); }; } diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index fbf6df2d0a..eae444f628 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -107,6 +107,7 @@ void VerticalButton::paintEvent(QPaintEvent *e) { .availableWidth = _st.nameWidth, .align = style::al_top, .paused = _delegate->buttonPaused(), + .elisionLines = kMaxNameLines, }); const auto &state = _data.badges; diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index fa7e2c70f7..ed382472df 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -160,12 +160,12 @@ UserpicButton::UserpicButton( not_null<Window::Controller*> window, Role role, const style::UserpicButton &st, - bool forceForumShape) + PeerUserpicShape shape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(window->sessionController()) , _window(window) -, _forceForumShape(forceForumShape) +, _shape(shape) , _role(role) { Expects(_role == Role::ChangePhoto || _role == Role::ChoosePhoto); @@ -181,13 +181,13 @@ UserpicButton::UserpicButton( Role role, Source source, const style::UserpicButton &st, - bool forceForumShape) + PeerUserpicShape shape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _controller(controller) , _window(&controller->window()) , _peer(peer) -, _forceForumShape(forceForumShape) +, _shape(shape) , _role(role) , _source(source) { if (_source == Source::Custom) { @@ -203,11 +203,11 @@ UserpicButton::UserpicButton( QWidget *parent, not_null<PeerData*> peer, const style::UserpicButton &st, - bool forceForumShape) + PeerUserpicShape shape) : RippleButton(parent, st.changeButton.ripple) , _st(st) , _peer(peer) -, _forceForumShape(forceForumShape) +, _shape(shape) , _role(Role::Custom) , _source(Source::PeerPhoto) { Expects(_role != Role::OpenPhoto); @@ -407,7 +407,7 @@ void UserpicButton::choosePhotoLocally() { CameraBox, _window, _peer, - _forceForumShape, + (_shape == PeerUserpicShape::Forum), callback(ChosenType::Set))); }, &st::menuIconPhotoSet); } @@ -648,7 +648,8 @@ void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) { auto size = QSize{ _st.photoSize, _st.photoSize }; const auto ratio = style::DevicePixelRatio(); request.outer = request.resize = size * ratio; - if (useForumShape()) { + if (_shape == PeerUserpicShape::Monoforum) { + } else if (useForumShape()) { const auto radius = int(_st.photoSize * Ui::ForumUserpicRadiusMultiplier()); if (_roundingCorners[0].width() != radius * ratio) { @@ -661,7 +662,24 @@ void UserpicButton::paintUserpicFrame(Painter &p, QPoint photoPosition) { } request.mask = _ellipseMask; } - p.drawImage(QRect(photoPosition, size), _streamed->frame(request)); + auto frame = _streamed->frame(request); + + if (_shape == PeerUserpicShape::Monoforum) { + if (_monoforumMask.isNull()) { + _monoforumMask = MonoforumShapeMask(request.resize); + } + constexpr auto format = QImage::Format_ARGB32_Premultiplied; + if (frame.format() != format) { + frame = std::move(frame).convertToFormat(format); + } + auto q = QPainter(&frame); + q.setCompositionMode(QPainter::CompositionMode_DestinationIn); + q.drawImage( + QRect(QPoint(), frame.size() / frame.devicePixelRatio()), + _monoforumMask); + q.end(); + } + p.drawImage(QRect(photoPosition, size), frame); if (!paused) { _streamed->markFrameShown(); } @@ -892,9 +910,8 @@ void UserpicButton::processNewPeerPhoto() { } bool UserpicButton::useForumShape() const { - return _forceForumShape - || (_peer && _peer->isForum()) - || (_peer && _peer->isMonoforum()); + return (_shape == PeerUserpicShape::Forum) + || (_peer && _peer->isForum() && _shape == PeerUserpicShape::Auto); } void UserpicButton::grabOldUserpic() { @@ -946,8 +963,8 @@ void UserpicButton::switchChangePhotoOverlay( } } -void UserpicButton::forceForumShape(bool force) { - _forceForumShape = force; +void UserpicButton::overrideShape(PeerUserpicShape shape) { + _shape = shape; prepare(); } @@ -1083,28 +1100,11 @@ void UserpicButton::prepareUserpicPixmap() { _userpic = CreateSquarePixmap(size, [&](Painter &p) { if (_userpicHasImage) { if (_showPeerUserpic) { - if (useForumShape()) { - const auto ratio = style::DevicePixelRatio(); - if (const auto cloud = _peer->userpicCloudImage(_userpicView)) { - Ui::ValidateUserpicCache( - _userpicView, - cloud, - nullptr, - size * ratio, - true); - p.drawImage(QRect(0, 0, size, size), _userpicView.cached); - } else { - const auto empty = PeerData::GenerateUserpicImage( - _peer, - _userpicView, - size * ratio, - (size * ratio) - * Ui::ForumUserpicRadiusMultiplier()); - p.drawImage(QRect(0, 0, size, size), empty); - } - } else { - _peer->paintUserpic(p, _userpicView, 0, 0, size); - } + _peer->paintUserpic(p, _userpicView, { + .position = QPoint(), + .size = size, + .shape = _shape, + }); } else if (_nonPersonalView) { using Size = Data::PhotoSize; if (const auto full = _nonPersonalView->image(Size::Large)) { diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index 959f980333..f126fac07a 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -62,7 +62,7 @@ public: not_null<::Window::Controller*> window, Role role, const style::UserpicButton &st, - bool forceForumShape = false); + PeerUserpicShape shape = PeerUserpicShape::Auto); UserpicButton( QWidget *parent, not_null<::Window::SessionController*> controller, @@ -70,12 +70,12 @@ public: Role role, Source source, const style::UserpicButton &st, - bool forceForumShape = false); + PeerUserpicShape shape = PeerUserpicShape::Auto); UserpicButton( QWidget *parent, not_null<PeerData*> peer, // Role::Custom, Source::PeerPhoto const style::UserpicButton &st, - bool forceForumShape = false); + PeerUserpicShape shape = PeerUserpicShape::Auto); ~UserpicButton(); enum class ChosenType { @@ -96,7 +96,7 @@ public: bool enabled, Fn<void(ChosenImage)> chosen); void showSavedMessagesOnSelf(bool enabled); - void forceForumShape(bool force); + void overrideShape(PeerUserpicShape shape); // Role::ChoosePhoto or Role::ChangePhoto [[nodiscard]] rpl::producer<ChosenImage> chosenImages() const { @@ -163,8 +163,9 @@ private: ::Window::SessionController *_controller = nullptr; ::Window::Controller *_window = nullptr; PeerData *_peer = nullptr; - bool _forceForumShape = false; + PeerUserpicShape _shape = PeerUserpicShape::Auto; PeerUserpicView _userpicView; + QImage _monoforumMask; std::shared_ptr<Data::PhotoMedia> _nonPersonalView; Role _role = Role::ChangePhoto; bool _notShownYet = true; diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp index 42dec878d3..32f164776f 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.cpp @@ -250,22 +250,13 @@ QImage PeerUserpic::image(int size) { auto p = Painter(&_frame); auto &view = _subscribed->view; - if (!_forceRound) { - _peer->paintUserpic(p, view, 0, 0, size); - } else if (const auto cloud = _peer->userpicCloudImage(view)) { - const auto full = size * style::DevicePixelRatio(); - Ui::ValidateUserpicCache(view, cloud, nullptr, full, false); - p.drawImage(QRect(0, 0, size, size), view.cached); - } else { - const auto full = size * style::DevicePixelRatio(); - const auto r = full / 2.; - const auto empty = PeerData::GenerateUserpicImage( - _peer, - view, - full, - r); - p.drawImage(QRect(0, 0, size, size), empty); - } + _peer->paintUserpic(p, view, { + .position = QPoint(), + .size = size, + .shape = (_forceRound + ? Ui::PeerUserpicShape::Circle + : Ui::PeerUserpicShape::Auto), + }); } return _frame; } diff --git a/Telegram/SourceFiles/ui/empty_userpic.cpp b/Telegram/SourceFiles/ui/empty_userpic.cpp index ea38baa05d..e731062b67 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.cpp +++ b/Telegram/SourceFiles/ui/empty_userpic.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" // style::IconButton #include "styles/style_info.h" // st::topBarCall +#include <QtCore/QMutex> #include <QtSvg/QSvgRenderer> namespace Ui { @@ -366,6 +367,17 @@ void EmptyUserpic::paintSquare( }); } +void EmptyUserpic::paintMonoforum( + QPainter &p, + int x, + int y, + int outerWidth, + int size) const { + paint(p, x, y, outerWidth, size, [&] { + PaintMonoforumShape(p, QRect(x, y, size, size)); + }); +} + void EmptyUserpic::PaintSavedMessages( QPainter &p, int x, @@ -649,4 +661,86 @@ void EmptyUserpic::fillString(const QString &name) { EmptyUserpic::~EmptyUserpic() = default; +void PaintMonoforumShape(QPainter &p, QRect rect) { + p.drawEllipse(rect); + + auto path = QPainterPath(); + path.moveTo( + rect.x() + rect.width() * 0.5, + rect.y() + rect.height() * 0.5); + path.arcTo( + QRectF( + rect.x() - rect.width() * 0.5, + rect.y(), + rect.width(), + rect.height()), + 0, + -90); + path.arcTo( + QRectF( + rect.x() - rect.width() * 0.25, + rect.y() - rect.height() * 2, + rect.width() * 0.5, + rect.height() * 3), + -90, + 45); + path.lineTo( + rect.x() + rect.width() * 0.5, + rect.y() + rect.height() * 0.5); + p.drawPath(path); +} + +QImage MonoforumShapeMask(QSize size) { + auto result = QImage(size, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + + QPainter p(&result); + PainterHighQualityEnabler hq(p); + p.setBrush(Qt::white); + p.setPen(Qt::NoPen); + + PaintMonoforumShape(p, QRect(QPoint(), size)); + + p.end(); + + return result; +} + +const QImage &MonoforumShapeMaskCached(QSize size) { + const auto key = (uint64(uint32(size.width())) << 32) + | uint64(uint32(size.height())); + + static auto Masks = base::flat_map<uint64, QImage>(); + static auto Mutex = QMutex(); + auto lock = QMutexLocker(&Mutex); + const auto i = Masks.find(key); + if (i != end(Masks)) { + return i->second; + } + lock.unlock(); + + auto mask = MonoforumShapeMask(size); + + lock.relock(); + return Masks.emplace(key, std::move(mask)).first->second; +} + +QImage ApplyMonoforumShape(QImage image) { + const auto size = image.size(); + auto mask = MonoforumShapeMaskCached(size); + + constexpr auto format = QImage::Format_ARGB32_Premultiplied; + if (image.format() != format) { + image = std::move(image).convertToFormat(format); + } + auto p = QPainter(&image); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.drawImage( + QRect(QPoint(), image.size() / image.devicePixelRatio()), + mask); + p.end(); + + return image; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/empty_userpic.h b/Telegram/SourceFiles/ui/empty_userpic.h index fba1fd88ad..6e2e903cec 100644 --- a/Telegram/SourceFiles/ui/empty_userpic.h +++ b/Telegram/SourceFiles/ui/empty_userpic.h @@ -46,6 +46,12 @@ public: int y, int outerWidth, int size) const; + void paintMonoforum( + QPainter &p, + int x, + int y, + int outerWidth, + int size) const; [[nodiscard]] QPixmap generate(int size); [[nodiscard]] std::pair<uint64, uint64> uniqueKey() const; @@ -147,4 +153,9 @@ private: }; +void PaintMonoforumShape(QPainter &p, QRect rect); +[[nodiscard]] QImage MonoforumShapeMask(QSize size); +[[nodiscard]] const QImage &MonoforumShapeMaskCached(QSize size); +[[nodiscard]] QImage ApplyMonoforumShape(QImage image); + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/userpic_view.cpp b/Telegram/SourceFiles/ui/userpic_view.cpp index f14ac76e80..ff5f336d42 100644 --- a/Telegram/SourceFiles/ui/userpic_view.cpp +++ b/Telegram/SourceFiles/ui/userpic_view.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/userpic_view.h" #include "ui/empty_userpic.h" +#include "ui/painter.h" #include "ui/image/image_prepare.h" namespace Ui { @@ -25,14 +26,14 @@ void ValidateUserpicCache( const QImage *cloud, const EmptyUserpic *empty, int size, - bool forum) { + PeerUserpicShape shape) { Expects(cloud != nullptr || empty != nullptr); const auto full = QSize(size, size); const auto version = style::PaletteVersion(); - const auto forumValue = forum ? 1 : 0; + const auto shapeValue = static_cast<uint32>(shape) & 3; const auto regenerate = (view.cached.size() != QSize(size, size)) - || (view.forum != forumValue) + || (view.shape != shapeValue) || (cloud && !view.empty.null()) || (empty && empty != view.empty.get()) || (empty && view.paletteVersion != version); @@ -40,7 +41,7 @@ void ValidateUserpicCache( return; } view.empty = empty; - view.forum = forumValue; + view.shape = shapeValue; view.paletteVersion = version; if (cloud) { @@ -48,7 +49,9 @@ void ValidateUserpicCache( full, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - if (forum) { + if (shape == PeerUserpicShape::Monoforum) { + view.cached = Ui::ApplyMonoforumShape(std::move(view.cached)); + } else if (shape == PeerUserpicShape::Forum) { view.cached = Images::Round( std::move(view.cached), Images::CornersMask(size @@ -64,7 +67,9 @@ void ValidateUserpicCache( view.cached.fill(Qt::transparent); auto p = QPainter(&view.cached); - if (forum) { + if (shape == PeerUserpicShape::Monoforum) { + empty->paintMonoforum(p, 0, 0, size, size); + } else if (shape == PeerUserpicShape::Forum) { empty->paintRounded( p, 0, diff --git a/Telegram/SourceFiles/ui/userpic_view.h b/Telegram/SourceFiles/ui/userpic_view.h index 6e50127d74..0bd9cbcedc 100644 --- a/Telegram/SourceFiles/ui/userpic_view.h +++ b/Telegram/SourceFiles/ui/userpic_view.h @@ -17,6 +17,13 @@ class EmptyUserpic; [[nodiscard]] float64 ForumUserpicRadiusMultiplier(); +enum class PeerUserpicShape : uint8 { + Auto, + Circle, + Forum, + Monoforum, +}; + struct PeerUserpicView { [[nodiscard]] bool null() const { return cached.isNull() && !cloud && empty.null(); @@ -25,8 +32,8 @@ struct PeerUserpicView { QImage cached; std::shared_ptr<QImage> cloud; base::weak_ptr<const EmptyUserpic> empty; - uint32 paletteVersion : 31 = 0; - uint32 forum : 1 = 0; + uint32 paletteVersion : 30 = 0; + uint32 shape : 2 = 0; }; [[nodiscard]] bool PeerUserpicLoading(const PeerUserpicView &view); @@ -36,6 +43,6 @@ void ValidateUserpicCache( const QImage *cloud, const EmptyUserpic *empty, int size, - bool forum); + PeerUserpicShape shape); } // namespace Ui diff --git a/Telegram/SourceFiles/window/notifications_manager_default.cpp b/Telegram/SourceFiles/window/notifications_manager_default.cpp index dcfd89dec5..e5a0a78ac1 100644 --- a/Telegram/SourceFiles/window/notifications_manager_default.cpp +++ b/Telegram/SourceFiles/window/notifications_manager_default.cpp @@ -656,7 +656,7 @@ Notification::Notification( , _topicRootId(topicRootId) , _sublist(history->peer->monoforumSublistFor(monoforumPeerId)) , _monoforumPeerId(monoforumPeerId) -, _userpicView(_peer->createUserpicView()) +, _userpicView(_peer->userpicPaintingPeer()->createUserpicView()) , _author(author) , _reaction(reaction) , _item(item) From 902da901004bf8e4eb8eab511194ff4d6e6daf09 Mon Sep 17 00:00:00 2001 From: GitHub Action <action@github.com> Date: Sun, 1 Jun 2025 00:42:13 +0000 Subject: [PATCH 106/310] Update User-Agent for DNS to Chrome 136.0.0.0. --- .../SourceFiles/mtproto/details/mtproto_domain_resolver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp index 6d09b676ca..56c6375672 100644 --- a/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp +++ b/Telegram/SourceFiles/mtproto/details/mtproto_domain_resolver.cpp @@ -65,7 +65,7 @@ QByteArray DnsUserAgent() { static const auto kResult = QByteArray( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/135.0.0.0 Safari/537.36"); + "Chrome/136.0.0.0 Safari/537.36"); return kResult; } From a4e4502d5091b7a675075c6959af8aa52c247462 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Wed, 4 Jun 2025 06:30:17 +0000 Subject: [PATCH 107/310] Add missing dependencies to macOS packaged action --- .github/workflows/mac_packaged.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 7dcae8d6d8..99fcf24f52 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -69,7 +69,7 @@ jobs: run: | brew update brew upgrade || true - brew install ada-url autoconf automake boost cmake ffmpeg libtool openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz + brew install ada-url autoconf automake boost cmake ffmpeg jpeg-xl libavif libheif libtool openal-soft openh264 openssl opus ninja pkg-config python qt yasm xz sudo xcode-select -s /Applications/Xcode.app/Contents/Developer xcodebuild -version > CACHE_KEY.txt From 8f7195d3b2f3bed45636501e56e9b71ef1c94493 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Wed, 4 Jun 2025 11:02:22 +0400 Subject: [PATCH 108/310] Fix macOS action --- .github/workflows/mac.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index bf461603b6..395d430ca5 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -40,7 +40,7 @@ jobs: macos: name: MacOS - runs-on: macos-latest + runs-on: macos-13 strategy: matrix: From a330a3f2ebfae08107bc09e2e7fb4c19e7ef6835 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 15:44:45 +0400 Subject: [PATCH 109/310] Nicer empty monoforum for non-admins. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/data/data_channel.cpp | 5 -- Telegram/SourceFiles/history/history.cpp | 6 +- Telegram/SourceFiles/history/history.h | 2 +- .../history/history_inner_widget.cpp | 4 ++ .../history/view/history_view_about_view.cpp | 55 +++++++++++++++---- 6 files changed, 54 insertions(+), 20 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 37b72d4b3b..77a065e20f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5297,6 +5297,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_non_premium_message_toast" = "**{user}** only accepts messages from contacts and {link} subscribers."; "lng_send_non_premium_message_toast_link" = "Telegram Premium"; +"lng_send_charges_stars_channel" = "{channel} charges {amount} per message to its admin."; +"lng_send_free_channel" = "Send a direct message to the administrator of {channel}."; "lng_send_charges_stars_text" = "{user} charges {amount} for each message."; "lng_send_charges_stars_go" = "Buy Stars"; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 8365be635b..bb41dd4b4e 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -947,11 +947,6 @@ void ChannelData::growSlowmodeLastMessage(TimeId when) { } int ChannelData::starsPerMessage() const { - if (const auto broadcast = monoforumBroadcast()) { - if (!amMonoforumAdmin()) { - return broadcast->starsPerMessage(); - } - } return _starsPerMessage; } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 77000409e0..1bb9b4fb23 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2992,7 +2992,7 @@ void History::dialogEntryApplied() { return; } if (!chatListMessage()) { - clear(ClearType::Unload); + clear(ClearType::Unload, true); addNewerSlice(QVector<MTPMessage>()); addOlderSlice(QVector<MTPMessage>()); if (const auto channel = peer->asChannel()) { @@ -3762,7 +3762,7 @@ std::vector<MsgId> History::collectMessagesFromParticipantToDelete( return result; } -void History::clear(ClearType type) { +void History::clear(ClearType type, bool markEmpty) { _unreadBarView = nullptr; _firstUnreadView = nullptr; removeJoinedMessage(); @@ -3772,7 +3772,7 @@ void History::clear(ClearType type) { owner().notifyHistoryUnloaded(this); lastKeyboardInited = false; if (type == ClearType::Unload) { - _loadedAtTop = _loadedAtBottom = false; + _loadedAtTop = _loadedAtBottom = markEmpty; } else { // Leave the 'sending' messages in local messages. auto local = base::flat_set<not_null<HistoryItem*>>(); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 7ebacdfc82..86c14ccc37 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -110,7 +110,7 @@ public: DeleteChat, ClearHistory, }; - void clear(ClearType type); + void clear(ClearType type, bool markEmpty = false); void clearUpTill(MsgId availableMinId); void applyGroupAdminChanges(const base::flat_set<UserId> &changes); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index ede9c81b08..0d2c3e520b 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -4542,6 +4542,10 @@ void HistoryInner::refreshAboutView(bool force) { session().api().requestFullPeer(user); } } + } else if (const auto monoforum = _peer->asChannel()) { + if (monoforum->isMonoforum() && !monoforum->amMonoforumAdmin()) { + refresh(); + } } } diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 486bcd984f..3ad2dcf1ee 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "countries/countries_instance.h" #include "data/business/data_business_common.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_channel.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" @@ -61,6 +62,7 @@ public: enum class Type { PremiumRequired, StarsCharged, + FreeDirect, }; EmptyChatLockedBox(not_null<Element*> parent, Type type); @@ -421,7 +423,9 @@ int EmptyChatLockedBox::buttonSkip() { } rpl::producer<QString> EmptyChatLockedBox::button() { - return (_type == Type::PremiumRequired) + return (_type == Type::FreeDirect) + ? nullptr + : (_type == Type::PremiumRequired) ? tr::lng_send_non_premium_go() : tr::lng_send_charges_stars_go(); } @@ -512,6 +516,9 @@ bool AboutView::refresh() { return true; } const auto user = _history->peer->asUser(); + const auto monoforum = _history->peer->isMonoforum() + ? _history->peer->asChannel() + : nullptr; const auto info = user ? user->botInfo.get() : nullptr; if (!info) { if (user @@ -539,6 +546,14 @@ bool AboutView::refresh() { makeIntro(user); } return true; + } else if (monoforum && _history->isDisplayedEmpty()) { + if (_item) { + return false; + } + setItem( + makeStarsPerMessage(monoforum->starsPerMessageChecked()), + nullptr); + return true; } if (_item) { setItem({}, nullptr); @@ -813,28 +828,46 @@ AdminLog::OwnedItem AboutView::makePremiumRequired() { } AdminLog::OwnedItem AboutView::makeStarsPerMessage(int stars) { + auto name = Ui::Text::Bold(_history->peer->shortName()); + auto cost = Ui::Text::IconEmoji( + &st::starIconEmoji + ).append(Ui::Text::Bold(Lang::FormatCountDecimal(stars))); const auto item = _history->makeMessage({ .id = _history->nextNonHistoryEntryId(), .flags = (MessageFlag::FakeAboutView | MessageFlag::FakeHistoryItem | MessageFlag::Local), .from = _history->peer->id, - }, PreparedServiceText{ tr::lng_send_charges_stars_text( - tr::now, - lt_user, - Ui::Text::Bold(_history->peer->shortName()), - lt_amount, - Ui::Text::IconEmoji( - &st::starIconEmoji - ).append(Ui::Text::Bold(Lang::FormatCountDecimal(stars))), - Ui::Text::RichLangValue), + }, PreparedServiceText{ !_history->peer->isMonoforum() + ? tr::lng_send_charges_stars_text( + tr::now, + lt_user, + std::move(name), + lt_amount, + std::move(cost), + Ui::Text::RichLangValue) + : stars + ? tr::lng_send_charges_stars_channel( + tr::now, + lt_channel, + std::move(name), + lt_amount, + std::move(cost), + Ui::Text::RichLangValue) + : tr::lng_send_free_channel( + tr::now, + lt_channel, + std::move(name), + Ui::Text::RichLangValue), }); auto result = AdminLog::OwnedItem(_delegate, item); result->overrideMedia(std::make_unique<ServiceBox>( result.get(), std::make_unique<EmptyChatLockedBox>( result.get(), - EmptyChatLockedBox::Type::StarsCharged))); + (stars + ? EmptyChatLockedBox::Type::StarsCharged + : EmptyChatLockedBox::Type::FreeDirect)))); return result; } From 8dc151e14dfe751292a46b81e0ad543236141258 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 16:21:21 +0400 Subject: [PATCH 110/310] Fix build on Windows. --- Telegram/SourceFiles/settings/settings_main.cpp | 2 +- .../SourceFiles/ui/controls/round_video_recorder.cpp | 11 ----------- cmake | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 496e5e2933..1d65911e3f 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -547,7 +547,7 @@ void SetupValidatePasswordSuggestion( 0, 0, 0)); - const auto label = content->add( + content->add( object_ptr<Ui::FlatLabel>( content, tr::lng_settings_suggestion_password_about(), diff --git a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp index dfd17f42f4..36a03c66ee 100644 --- a/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp +++ b/Telegram/SourceFiles/ui/controls/round_video_recorder.cpp @@ -915,10 +915,6 @@ void RoundVideoRecorder::Private::drawLogoOnYUV420P( not_null<AVFrame*> frame) { const auto width = frame->width; const auto height = frame->height; - const auto centerX = width / 2; - const auto centerY = height / 2; - const auto radius = std::min(centerX, centerY); - const auto radiusSquared = radius * radius; const auto logoBottom = height - kLogoSize + kLogoYShift; const auto logoStartX = kLogoXShift; @@ -933,20 +929,13 @@ void RoundVideoRecorder::Private::drawLogoOnYUV420P( const auto ySkip = frame->linesize[0] - width; const auto uvWidth = width / 2; - const auto uvHeight = height / 2; auto uData = frame->data[1]; auto vData = frame->data[2]; const auto uvSkip = frame->linesize[1] - uvWidth; auto yMaskIndex = 0; for (auto y = 0; y < height; ++y) { - const auto dy = y - centerY; - const auto dySquared = dy * dy; - for (auto x = 0; x < width; ++x) { - const auto dx = x - centerX; - const auto distanceSquared = dx * dx + dySquared; - if (_circleMask[yMaskIndex]) { *yData = static_cast<uint8_t>(*yData * kOverlayOpacity + 16 * kOverlayOpaque); diff --git a/cmake b/cmake index fd6f14f2de..dd3a6bcaaa 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit fd6f14f2deb9cc67c144c529e3267b67f99ba624 +Subproject commit dd3a6bcaaa37f4d873bbdba840f858696a58735b From 28e7afa41290d67e4a153d566928c93c3af66642 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 16:48:37 +0400 Subject: [PATCH 111/310] Even nicer empty chat. --- Telegram/Resources/icons/chat/large_messages.png | Bin 0 -> 1184 bytes .../Resources/icons/chat/large_messages@2x.png | Bin 0 -> 2303 bytes .../Resources/icons/chat/large_messages@3x.png | Bin 0 -> 3524 bytes .../history/view/history_view_about_view.cpp | 8 ++++++-- Telegram/SourceFiles/ui/chat/chat.style | 2 ++ 5 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 Telegram/Resources/icons/chat/large_messages.png create mode 100644 Telegram/Resources/icons/chat/large_messages@2x.png create mode 100644 Telegram/Resources/icons/chat/large_messages@3x.png diff --git a/Telegram/Resources/icons/chat/large_messages.png b/Telegram/Resources/icons/chat/large_messages.png new file mode 100644 index 0000000000000000000000000000000000000000..f2b2fda9d0633da89dbe047b3a0859565433221e GIT binary patch literal 1184 zcmV;R1Yi4!P)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NG3Q0skR9Fe^SW75vQ5Zgli}H-* zkw@N-Oh_@10g*>h%0!e$3?xj<#ej?`8Ho&-$)glWF=FD8S18Jx%K#~_i+jIo-TUu# z*52owz0cWaa_`xb{lC`#eS58cy|%&Ne4IVt?1BHR2aM{!Hzp>gpr9ZpCnqv8GAJl0 zI5_z8^Yi=Hb9{Wfu&^*cKYwv?p+2FFkscl%jg5^52M0e@JR2JuRaI54uC6wMS#D-D z8k?G$E-x=tFfj=^Iy$PXthAh5vloLhH#cX3qfr?f8}sq;(G0BEAT>4h>gq}(0fq3x z!^7z4XhjGO#)O0fRH{OHn@r#o5)z^TUI@k9IXOA83EFI<t*tFDFE0T|tvWh7YBsSQ zj-H+#wG8;U+}vC{KrJ<WdwWYsN#U_n$tx=>ma^LC8y_E6MS_B`5Zi}nrS;3pi=Uq# z!I=|W$Wc*IL9!zRtoUVRWmYpwgtD@-tVY`zKoEg}f#y<4gd!p$%mwLXCy3_e=9ZQg zD{1fV@2ymU-R|ApT}Gn5z8=Z?`ugOYq#knA7R_N|w`LmFUI?wNtu-|@od35d5yGx6 z_;gp#&dylQ+uK_v3=9k;Cnxhj5~1_+bKc<ynF!%1!SZ{0dUkbn@mLa}#l=P5;RqQE zFH0L99!~j7N=m5MVU!3>PEJygBLwUo6B83Gb5v9mL7;X~<D{AhYinzE&m^-6rlzI{ zY<+#5kDHm9A(x4epP$di=_tRyzvIxyz+z)#h0J?<dnP=1Wp;K}h}K2z?d>I$_V#um z=hM>@c|`&BuloTF9YBX`8~`649t1+P3W#i&o|cw|0u<WVtm5<dIW;^yEKjw$xv5Nr zS$%zdKR!O>&1|roo}R|X#}im#VIl68@;rlsgA}76=vCU7<o^Et1b~ize0-F%fdxwu zg((Q~qEpOb!H7j)SX$!Z;>=w*aSeBOcURy+CR`E(Fj)5Y_XVS(iW_%BLxTWNTwHv2 zcc&N+V|8^kK`RKQrKLj4p`jrh_7IBuT3cJ&^z`)V>MDLJ{P=qC-MG(Vba3hB%Y~&z z!;X#)3l)h~#>GY2Ekr`0p`kZ7H$-S@X$k*2#Gt=A647zuuT6Txi%++_f?#C%B4dre zNjVG#xIr1K!p_bP*kE*duOMT-2o|O$B_)-Ym*XEuVq&6V02#f#y>ap<l?i`NOoCOD ziX%i(Q4t0dLza<|q3QlrTU$FaGNK7iRd8Nj9x92(TD`Enz3t}arV7J`pv=rnoB%Q0 z^3@N<?(XjF?Cj3Y&gbW6o^gMFKaZuukn!o5jGZeiEG!@(0P_pKb*`_ku@_MoeumW5 y)nU4%r>EoU`1<;a4qjefrU0kl?16u)2YvzId`I}3DuE6F0000<MNUMnLSTZX_zOD# literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_messages@2x.png b/Telegram/Resources/icons/chat/large_messages@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4a0821ec60db83f67492399a57aef34bbfcc267 GIT binary patch literal 2303 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 MLeCifUdR6Nzh5F)Y5)KL literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/large_messages@3x.png b/Telegram/Resources/icons/chat/large_messages@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1d5b682e32d250f20f3f00869a54f3bcd7e1a3da GIT binary patch literal 3524 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 R^5WkQ3JKTKs8lx(`430im4*NS literal 0 HcmV?d00001 diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 3ad2dcf1ee..c4028d362a 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -403,7 +403,9 @@ EmptyChatLockedBox::EmptyChatLockedBox(not_null<Element*> parent, Type type) EmptyChatLockedBox::~EmptyChatLockedBox() = default; int EmptyChatLockedBox::width() { - return st::premiumRequiredWidth; + return (_type == Type::PremiumRequired) + ? st::premiumRequiredWidth + : st::starsPerMessageWidth; } int EmptyChatLockedBox::top() { @@ -460,7 +462,9 @@ void EmptyChatLockedBox::draw( p.setBrush(context.st->msgServiceBg()); // ? p.setPen(Qt::NoPen); p.drawEllipse(geometry); - st::premiumRequiredIcon.paintInCenter(p, geometry); + (_type == Type::PremiumRequired + ? st::premiumRequiredIcon + : st::directMessagesIcon).paintInCenter(p, geometry); } void EmptyChatLockedBox::stickerClearLoopPlayed() { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0bf7da504d..3e3c2bbaf8 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1055,6 +1055,8 @@ chatSimilarSkip: 12px; premiumRequiredWidth: 186px; premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; premiumRequiredCircle: 60px; +directMessagesIcon: icon{{ "chat/large_messages", msgServiceFg }}; +starsPerMessageWidth: 226px; repliesEmptyIcon: icon{{ "chat/large_quickreply", msgServiceFg }}; greetingEmptyIcon: icon{{ "chat/large_greeting", msgServiceFg }}; From 6a43107bb27d175454c495e9d0cef03fb30ac11f Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 16:52:44 +0400 Subject: [PATCH 112/310] Fix possible crash in subsection tabs. --- .../history/view/history_view_subsection_tabs.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 5757d7b89e..4d9f888d25 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -341,11 +341,15 @@ void SubsectionTabs::setupSlider( scrollSavingIndex = -1; for (auto index = 0; index != count; ++index) { const auto thread = _sectionsSlice[index].thread; - if (ranges::contains(_slice, thread, &Item::thread)) { + const auto i = ranges::find( + _slice, + thread, + &Item::thread); + if (i != end(_slice)) { scrollSavingThread = thread; scrollSavingShift = scrollValue - slider->lookupSectionPosition(index); - scrollSavingIndex = index; + scrollSavingIndex = int(i - begin(_slice)); break; } } From 66473738d65e09ad494e2e4737a9d28cb747292d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 17:26:19 +0400 Subject: [PATCH 113/310] Add simple shadow to subsection tabs. --- .../view/history_view_subsection_tabs.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 4d9f888d25..a2f30404a5 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -97,10 +97,18 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { st::chatTabsScroll, true); scroll->show(); + const auto shadow = Ui::CreateChild<Ui::PlainShadow>(_horizontal); const auto slider = scroll->setOwnedWidget( object_ptr<Ui::HorizontalSlider>(scroll)); setupSlider(scroll, slider, false); + shadow->showOn(rpl::single( + rpl::empty + ) | rpl::then( + scroll->scrolls() + ) | rpl::map([=] { return scroll->scrollLeft() > 0; })); + shadow->setAttribute(Qt::WA_TransparentForMouseEvents); + _horizontal->resize( _horizontal->width(), std::max(toggle->height(), slider->height())); @@ -121,6 +129,7 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { const auto togglew = toggle->width(); const auto height = size.height(); scroll->setGeometry(togglew, 0, size.width() - togglew, height); + shadow->setGeometry(togglew, 0, st::lineWidth, height); }, scroll->lifetime()); _horizontal->paintRequest() | rpl::start_with_next([=](QRect clip) { @@ -156,11 +165,18 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { _vertical, st::chatTabsScroll); scroll->show(); - + const auto shadow = Ui::CreateChild<Ui::PlainShadow>(_vertical); const auto slider = scroll->setOwnedWidget( object_ptr<Ui::VerticalSlider>(scroll)); setupSlider(scroll, slider, true); + shadow->showOn(rpl::single( + rpl::empty + ) | rpl::then( + scroll->scrolls() + ) | rpl::map([=] { return scroll->scrollTop() > 0; })); + shadow->setAttribute(Qt::WA_TransparentForMouseEvents); + _vertical->resize( std::max(toggle->width(), slider->width()), _vertical->height()); @@ -170,6 +186,7 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) { const auto toggleh = toggle->height(); const auto width = size.width(); scroll->setGeometry(0, toggleh, width, size.height() - toggleh); + shadow->setGeometry(0, toggleh, width, st::lineWidth); }, scroll->lifetime()); _vertical->paintRequest() | rpl::start_with_next([=](QRect clip) { From 158d2a4124b6abeccb9ca88bc0229a8c9f8f1f92 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 17:59:10 +0400 Subject: [PATCH 114/310] Fix possible stack overflow in subsection tabs. --- .../view/history_view_subsection_tabs.cpp | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index a2f30404a5..3f2370b75d 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -231,18 +231,40 @@ void SubsectionTabs::setupSlider( : scroll->scrollLeftMax(); const auto availableFrom = scrollValue; const auto availableTill = (scrollMax - scrollValue); - if (scrollMax <= 2 * full && _afterAvailable > 0) { + if (scrollMax <= 3 * full && _afterAvailable > 0) { _beforeLimit *= 2; _afterLimit *= 2; } + const auto findMiddle = [&] { + Expects(!_slice.empty()); + + auto best = -1; + auto bestDistance = -1; + const auto ideal = scrollValue + (full / 2); + for (auto i = 0, count = int(_slice.size()); i != count; ++i) { + const auto a = slider->lookupSectionPosition(i); + const auto b = (i + 1 == count) + ? (full + scrollMax) + : slider->lookupSectionPosition(i + 1); + const auto middle = (a + b) / 2; + const auto distance = std::abs(middle - ideal); + if (best < 0 || distance < bestDistance) { + best = i; + bestDistance = distance; + } + } + + Ensures(best >= 0); + return best; + }; if (availableFrom < full && _beforeSkipped.value_or(0) > 0 && !_slice.empty()) { - _around = _slice.front().thread; + _around = _slice[findMiddle()].thread; refreshSlice(); } else if (availableTill < full) { if (_afterAvailable > 0) { - _around = _slice.back().thread; + _around = _slice[findMiddle()].thread; refreshSlice(); } else if (!_afterSkipped.has_value()) { _loading = true; From 8d1c2f832d24ad1a8a0efdf482a0713634f916f0 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 17:59:25 +0400 Subject: [PATCH 115/310] Add "Create topic" to new forum view. --- Telegram/SourceFiles/window/window_peer_menu.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 2a5da8ae11..958cb3179b 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -529,7 +529,10 @@ void Filler::addTogglePin() { } void Filler::addToggleMuteSubmenu(bool addSeparator) { - if (!_thread || _thread->peer()->isSelf() || _thread->asSublist()) { + if (!_thread + || _thread->peer()->isSelf() + || _thread->asSublist() + || (_thread->asHistory() && _thread->asHistory()->isForum())) { return; } PeerMenuAddMuteSubmenuAction(_controller, _thread, _addAction); @@ -1470,6 +1473,7 @@ void Filler::fillContextMenuActions() { void Filler::fillHistoryActions() { addToggleMuteSubmenu(true); + addCreateTopic(); addInfo(); addViewAsTopics(); addManageChat(); From 910b6d88791ac953474dd0feaf125afdbaaeca02 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 18:13:21 +0400 Subject: [PATCH 116/310] Fix unread mark badge in new forums layout. --- .../history/view/history_view_subsection_tabs.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 3f2370b75d..66790e7051 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -635,7 +635,7 @@ void SubsectionTabs::refreshSlice() { const auto push = [&](not_null<Data::Thread*> thread) { const auto topic = thread->asTopic(); const auto sublist = thread->asSublist(); - const auto badges = [&] { + auto badges = [&] { if (!topic && !sublist) { return Dialogs::BadgesState(); } else if (thread->chatListUnreadState().known) { @@ -650,6 +650,10 @@ void SubsectionTabs::refreshSlice() { } return thread->chatListBadgesState(); }(); + if (topic) { + // Don't show the small indicators for non-visited unread topics. + badges.unread = false; + } slice.push_back({ .thread = thread, .badges = badges, From 90e445eec991f9d60522976ae24d11e4b061362b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 18:26:30 +0400 Subject: [PATCH 117/310] Don't show notifications from other admins. --- Telegram/SourceFiles/history/history_item.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 3daa1a5500..39c983ac7d 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1713,6 +1713,9 @@ bool HistoryItem::skipNotification() const { if (forwarded->imported) { return true; } + } else if (_history->amMonoforumAdmin() + && from() == _history->peer->monoforumBroadcast()) { + return true; } return false; } From 8654ffb6fb56099639b2f6c4bbe04bce9a5d73d4 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 18:26:48 +0400 Subject: [PATCH 118/310] Don't show "Who Viewed" in monoforums. --- Telegram/SourceFiles/api/api_who_reacted.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index a5186fc6b1..88cd463dac 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -712,7 +712,8 @@ bool WhoReadExists(not_null<HistoryItem*> item) { const auto megagroup = peer->asMegagroup(); if ((!chat && !megagroup) || (megagroup - && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))) { + && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden)) + || megagroup->isMonoforum()) { return false; } const auto &appConfig = peer->session().appConfig(); From a72782e23271b345c6b0ea5d826e4427b7bd34f0 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 18:42:17 +0400 Subject: [PATCH 119/310] Use server provided default stars count for direct. --- .../SourceFiles/boxes/edit_privacy_box.cpp | 46 +++++++++++-------- Telegram/SourceFiles/main/main_app_config.cpp | 4 ++ Telegram/SourceFiles/main/main_app_config.h | 1 + 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 88ebe4ccbb..86816ba15b 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -45,7 +45,6 @@ namespace { constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value; constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value; -constexpr auto kDefaultDirectMessagesPrice = 10; constexpr auto kDefaultPrivateMessagesPrice = 10; using Exceptions = Api::UserPrivacy::Exceptions; @@ -1227,24 +1226,33 @@ rpl::producer<int> SetupChargeSlider( const auto skip = 2 * st::defaultVerticalListSkip; Ui::AddSkip(container, skip); - auto dollars = state->stars.value() | rpl::map([=](int stars) { - const auto ratio = peer->session().appConfig().starsWithdrawRate(); + const auto details = container->add( + object_ptr<Ui::VerticalLayout>(container)); + state->stars.value() | rpl::start_with_next([=](int stars) { + while (details->count()) { + delete details->widgetAt(0); + } + if (!stars) { + Ui::AddDivider(details); + return; + } + const auto &appConfig = peer->session().appConfig(); + const auto percent = appConfig.paidMessageCommission(); + const auto ratio = appConfig.starsWithdrawRate(); const auto dollars = int(base::SafeRound(stars * ratio)); - return '~' + Ui::FillAmountAndCurrency(dollars, u"USD"_q); - }); - const auto percent = peer->session().appConfig().paidMessageCommission(); - Ui::AddDividerText( - container, - (broadcast - ? tr::lng_manage_monoforum_price_about - : group - ? tr::lng_rights_charge_price_about - : tr::lng_messages_privacy_price_about)( - lt_percent, - rpl::single(QString::number(percent / 10.) + '%'), - lt_amount, - std::move(dollars))); - + const auto amount = Ui::FillAmountAndCurrency(dollars, u"USD"_q); + Ui::AddDividerText( + details, + (broadcast + ? tr::lng_manage_monoforum_price_about + : group + ? tr::lng_rights_charge_price_about + : tr::lng_messages_privacy_price_about)( + lt_percent, + rpl::single(QString::number(percent / 10.) + '%'), + lt_amount, + rpl::single('~' + amount))); + }, details->lifetime()); return state->stars.value(); } @@ -1298,7 +1306,7 @@ void EditDirectMessagesPriceBox( inner, channel, savedValue, - kDefaultDirectMessagesPrice, + channel->session().appConfig().paidMessageChannelStarsDefault(), true ) | rpl::start_with_next([=](int stars) { *result = stars; diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 5c4a5b02cd..3e53a82bfb 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -105,6 +105,10 @@ int AppConfig::paidMessageCommission() const { return get<int>(u"stars_paid_message_commission_permille"_q, 850); } +int AppConfig::paidMessageChannelStarsDefault() const { + return get<int>(u"stars_paid_messages_channel_amount_default"_q, 10); +} + int AppConfig::pinnedGiftsLimit() const { return get<int>(u"stargifts_pinned_to_top_limit"_q, 6); } diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 0fa0e3c495..8c460a3963 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -71,6 +71,7 @@ public: [[nodiscard]] bool paidMessagesAvailable() const; [[nodiscard]] int paidMessageStarsMax() const; [[nodiscard]] int paidMessageCommission() const; + [[nodiscard]] int paidMessageChannelStarsDefault() const; [[nodiscard]] int pinnedGiftsLimit() const; From 7dadaa1b283f340de48736d359041d7298eda708 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 19:26:39 +0400 Subject: [PATCH 120/310] Save subsection tabs layout to disk. --- .../history/history_inner_widget.cpp | 2 +- .../SourceFiles/history/history_widget.cpp | 3 +- .../view/history_view_chat_section.cpp | 3 ++ .../view/history_view_subsection_tabs.cpp | 23 ++++++++++- .../view/history_view_subsection_tabs.h | 7 ++++ .../main/main_session_settings.cpp | 40 ++++++++++++++++++- .../SourceFiles/main/main_session_settings.h | 4 ++ 7 files changed, 77 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0d2c3e520b..20a147fca7 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -810,7 +810,7 @@ bool HistoryInner::canHaveFromUserpics() const { } else if (const auto channel = _peer->asBroadcast()) { return channel->signatureProfiles(); } - return !_removeFromUserpics; + return _isChatWide || !_removeFromUserpics; } void HistoryInner::toggleRemoveFromUserpics(bool remove) { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 2d883e5704..397b8bdc88 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6474,7 +6474,7 @@ void HistoryWidget::updateControlsGeometry() { _topBars->resize( innerWidth, scrollAreaTop - _topBars->y() + st::lineWidth); - if (_scroll->y() != scrollAreaTop) { + if (_scroll->y() != scrollAreaTop || _scroll->x() != tabsLeftSkip) { _scroll->moveToLeft(tabsLeftSkip, scrollAreaTop); if (_autocomplete) { _autocomplete->setBoundings(_scroll->geometry()); @@ -8301,6 +8301,7 @@ void HistoryWidget::validateSubsectionTabs() { updateControlsGeometry(); orderWidgets(); }, _subsectionTabsLifetime); + _list->toggleRemoveFromUserpics(_subsectionTabs->leftSkip() > 0); updateControlsGeometry(); orderWidgets(); } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index ec2049dbb1..9c3c948da9 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1590,6 +1590,9 @@ void ChatWidget::validateSubsectionTabs() { updateControlsGeometry(); orderWidgets(); }, _subsectionTabsLifetime); + _inner->overrideChatMode((_subsectionTabs->leftSkip() > 0) + ? ElementChatMode::Narrow + : std::optional<ElementChatMode>()); updateControlsGeometry(); orderWidgets(); } diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 66790e7051..73fb100f1f 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "main/main_session_settings.h" #include "ui/controls/subsection_tabs_slider.h" #include "ui/effects/ripple_animation.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" @@ -56,7 +57,7 @@ SubsectionTabs::SubsectionTabs( , _afterLimit(kDefaultLimit) { track(); refreshSlice(); - setupHorizontal(parent); + setup(parent); dataChanged() | rpl::start_with_next([=] { if (_loading) { @@ -72,6 +73,15 @@ SubsectionTabs::~SubsectionTabs() { delete base::take(_shadow); } +void SubsectionTabs::setup(not_null<Ui::RpWidget*> parent) { + const auto peerId = _history->peer->id; + if (session().settings().verticalSubsectionTabs(peerId)) { + setupVertical(parent); + } else { + setupHorizontal(parent); + } +} + void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) { delete base::take(_vertical); _horizontal = Ui::CreateChild<Ui::RpWidget>(parent); @@ -397,7 +407,7 @@ void SubsectionTabs::setupSlider( slider->setSections({ .tabs = std::move(sections), .context = Core::TextContext({ - .session = &_history->session(), + .session = &session(), }), }, paused); slider->setActiveSectionFast(activeIndex); @@ -466,6 +476,11 @@ void SubsectionTabs::toggleModes() { } else { setupHorizontal(_vertical->parentWidget()); } + const auto peerId = _history->peer->id; + const auto vertical = (_vertical != nullptr); + session().settings().setVerticalSubsectionTabs(peerId, vertical); + session().saveSettingsDelayed(); + _layoutRequests.fire({}); } @@ -703,6 +718,10 @@ void SubsectionTabs::scheduleRefresh() { }); } +Main::Session &SubsectionTabs::session() { + return _history->session(); +} + bool SubsectionTabs::switchTo( not_null<Data::Thread*> thread, not_null<Ui::RpWidget*> parent) { diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index 200afe3b35..15cf7c6d75 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -16,6 +16,10 @@ namespace Data { class Thread; } // namespace Data +namespace Main { +class Session; +} // namespace Main + namespace Window { class SessionController; } // namespace Window @@ -37,6 +41,8 @@ public: not_null<Data::Thread*> thread); ~SubsectionTabs(); + [[nodiscard]] Main::Session &session(); + [[nodiscard]] bool switchTo( not_null<Data::Thread*> thread, not_null<Ui::RpWidget*> parent); @@ -79,6 +85,7 @@ private: void refreshSlice(); void scheduleRefresh(); void loadMore(); + void setup(not_null<Ui::RpWidget*> parent); [[nodiscard]] rpl::producer<> dataChanged() const; void setupSlider( diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 88f60b5af8..7f562e3e48 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -42,7 +42,9 @@ QByteArray SessionSettings::serialize() const { + sizeof(qint32) * 3 + _groupEmojiSectionHidden.size() * sizeof(quint64) + sizeof(qint32) * 3 - + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4); + + _hiddenPinnedMessages.size() * (sizeof(quint64) * 4) + + sizeof(qint32) + + _verticalSubsectionTabs.size() * sizeof(quint64); auto result = QByteArray(); result.reserve(size); @@ -94,6 +96,10 @@ QByteArray SessionSettings::serialize() const { << SerializePeerId(key.monoforumPeerId) << qint64(value.bare); } + stream << qint32(_verticalSubsectionTabs.size()); + for (const auto &peerId : _verticalSubsectionTabs) { + stream << SerializePeerId(peerId); + } } Ensures(result.size() == size); @@ -153,6 +159,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { std::vector<int> appDictionariesEnabled; qint32 appAutoDownloadDictionaries = app.autoDownloadDictionaries() ? 1 : 0; base::flat_map<ThreadId, MsgId> hiddenPinnedMessages; + base::flat_set<PeerId> verticalSubsectionTabs; qint32 dialogsFiltersEnabled = _dialogsFiltersEnabled ? 1 : 0; qint32 supportAllSilent = _supportAllSilent ? 1 : 0; qint32 photoEditorHintShowsCount = _photoEditorHintShowsCount; @@ -466,6 +473,22 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { } } } + if (!stream.atEnd()) { + auto count = qint32(0); + stream >> count; + if (stream.status() == QDataStream::Ok) { + for (auto i = 0; i != count; ++i) { + auto peerId = quint64(); + stream >> peerId; + if (stream.status() != QDataStream::Ok) { + LOG(("App Error: " + "Bad data for SessionSettings::addFromSerialized()")); + return; + } + verticalSubsectionTabs.emplace(DeserializePeerId(peerId)); + } + } + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for SessionSettings::addFromSerialized()")); @@ -512,6 +535,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { _mutePeriods = std::move(mutePeriods); _lastNonPremiumLimitDownload = lastNonPremiumLimitDownload; _lastNonPremiumLimitUpload = lastNonPremiumLimitUpload; + _verticalSubsectionTabs = std::move(verticalSubsectionTabs); if (version < 2) { app.setLastSeenWarningSeen(appLastSeenWarningSeen == 1); @@ -646,6 +670,20 @@ void SessionSettings::setHiddenPinnedMessageId( } } +bool SessionSettings::verticalSubsectionTabs(PeerId peerId) const { + return _verticalSubsectionTabs.contains(peerId); +} + +void SessionSettings::setVerticalSubsectionTabs( + PeerId peerId, + bool vertical) { + if (vertical) { + _verticalSubsectionTabs.emplace(peerId); + } else { + _verticalSubsectionTabs.remove(peerId); + } +} + bool SessionSettings::photoEditorHintShown() const { return _photoEditorHintShowsCount < kPhotoEditorHintMaxShowsCount; } diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index b88244aaa3..ee6218e69a 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -118,6 +118,9 @@ public: PeerId monoforumPeerId, MsgId msgId); + [[nodiscard]] bool verticalSubsectionTabs(PeerId peerId) const; + void setVerticalSubsectionTabs(PeerId peerId, bool vertical); + [[nodiscard]] bool dialogsFiltersEnabled() const { return _dialogsFiltersEnabled; } @@ -167,6 +170,7 @@ private: rpl::variable<bool> _archiveInMainMenu = false; rpl::variable<bool> _skipArchiveInSearch = false; base::flat_map<ThreadId, MsgId> _hiddenPinnedMessages; + base::flat_set<PeerId> _verticalSubsectionTabs; bool _dialogsFiltersEnabled = false; int _photoEditorHintShowsCount = 0; std::vector<TimeId> _mutePeriods; From ee3d70f879bb7578496d83f4aa81e4c02571767a Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 21:05:08 +0400 Subject: [PATCH 121/310] Fix glitching userpics in monoforum. --- .../history/view/history_view_subsection_tabs.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 73fb100f1f..207bd4b1cb 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -283,6 +283,12 @@ void SubsectionTabs::setupSlider( } }, scroll->lifetime()); + using ImagePointer = std::shared_ptr<Ui::DynamicImage>; + struct Cache { + base::flat_map<not_null<PeerData*>, ImagePointer> userpics; + }; + const auto cache = std::make_shared<Cache>(); + _refreshed.events_starting_with_copy( rpl::empty ) | rpl::start_with_next([=] { @@ -291,6 +297,7 @@ void SubsectionTabs::setupSlider( return _controller->isGifPausedAtLeastFor( Window::GifPauseReason::Any); }; + auto updated = Cache(); auto sections = std::vector<Ui::SubsectionTab>(); auto activeIndex = -1; for (const auto &item : _slice) { @@ -337,9 +344,13 @@ void SubsectionTabs::setupSlider( } else if (const auto sublist = item.thread->asSublist()) { const auto peer = sublist->sublistPeer(); if (vertical) { + auto was = cache->userpics[peer]; + auto userpic = updated.userpics[peer] = was + ? was + : Ui::MakeUserpicThumbnail(peer); sections.push_back({ .text = peer->shortName(), - .userpic = Ui::MakeUserpicThumbnail(peer), + .userpic = std::move(userpic), }); } else { sections.push_back({ @@ -359,6 +370,7 @@ void SubsectionTabs::setupSlider( auto §ion = sections.back(); section.badges = item.badges; } + *cache = std::move(updated); auto scrollSavingThread = (Data::Thread*)nullptr; auto scrollSavingShift = 0; From 5c4b1f663880cf51f38854b78fdd4b058913be3d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 21:33:27 +0400 Subject: [PATCH 122/310] Show message author to admins in monoforums. --- Telegram/Resources/langs/lang.strings | 1 + .../chat_helpers/chat_helpers.style | 5 + .../history/history_inner_widget.cpp | 5 +- Telegram/SourceFiles/history/history_item.cpp | 7 +- Telegram/SourceFiles/history/history_item.h | 1 + .../view/history_view_context_menu.cpp | 122 +++++++++++++++++- .../history/view/history_view_context_menu.h | 3 +- 7 files changed, 133 insertions(+), 11 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 77a065e20f..86ddc9cc3e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4252,6 +4252,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_seen_reacted#other" = "{count} Reacted"; "lng_context_seen_reacted_none" = "Nobody Reacted"; "lng_context_seen_reacted_all" = "Show All Reactions"; +"lng_context_sent_by" = "Sent by {user}"; "lng_context_set_as_quick" = "Set As Quick"; "lng_context_filter_by_tag" = "Filter by Tag"; "lng_context_tag_add_name" = "Add Name"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index c4c689c4b1..230c2ac31b 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -269,6 +269,11 @@ whenReadPadding: margins(34px, 3px, 17px, 4px); whenReadIconPosition: point(8px, 0px); whenReadSkip: 3px; whenReadShowPadding: margins(6px, 0px, 6px, 2px); +whoSentItem: Menu(defaultMenu) { + itemPadding: margins(17px, 3px, 17px, 4px); + itemRightSkip: 0px; + itemStyle: whenReadStyle; +} switchPmButton: RoundButton(defaultBoxButton) { width: 320px; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 20a147fca7..3978655b7b 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -3103,7 +3103,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { leaderOrSelf, _controller); } else if (leaderOrSelf) { - HistoryView::MaybeAddWhenEditedForwardedAction(_menu, leaderOrSelf); + HistoryView::MaybeAddWhenEditedForwardedAction( + _menu, + leaderOrSelf, + _controller); } if (_menu->empty()) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 39c983ac7d..58e747454a 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1706,6 +1706,10 @@ bool HistoryItem::isSponsored() const { return _flags & MessageFlag::Sponsored; } +bool HistoryItem::canLookupMessageAuthor() const { + return isRegular() && _history->amMonoforumAdmin() && _from->isChannel(); +} + bool HistoryItem::skipNotification() const { if (isSilent() && (_flags & MessageFlag::IsContactSignUp)) { return true; @@ -1713,8 +1717,7 @@ bool HistoryItem::skipNotification() const { if (forwarded->imported) { return true; } - } else if (_history->amMonoforumAdmin() - && from() == _history->peer->monoforumBroadcast()) { + } else if (canLookupMessageAuthor()) { return true; } return false; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 6a64ccfe4c..55904615ce 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -179,6 +179,7 @@ public: [[nodiscard]] bool isFromScheduled() const; [[nodiscard]] bool isScheduled() const; [[nodiscard]] bool isSponsored() const; + [[nodiscard]] bool canLookupMessageAuthor() const; [[nodiscard]] bool skipNotification() const; [[nodiscard]] bool isUserpicSuggestion() const; [[nodiscard]] BusinessShortcutId shortcutId() const; diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index d156b8fc73..58ea3e210e 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -113,7 +113,8 @@ bool HasEditMessageAction( || (context != Context::History && context != Context::Replies && context != Context::ShortcutMessages - && context != Context::ScheduledTopic)) { + && context != Context::ScheduledTopic + && context != Context::Monoforum)) { return false; } const auto peer = item->history()->peer; @@ -1154,6 +1155,97 @@ void ShowWhoReadInfo( controller->showSection(std::move(memento)); } +[[nodiscard]] rpl::producer<not_null<UserData*>> LookupMessageAuthor( + not_null<HistoryItem*> item) { + struct Author { + UserData *user = nullptr; + std::vector<Fn<void(UserData*)>> callbacks; + }; + struct Authors { + base::flat_map<FullMsgId, Author> map; + }; + static auto Cache = base::flat_map<not_null<Main::Session*>, Authors>(); + + const auto channel = item->history()->peer->asChannel(); + const auto session = &channel->session(); + const auto id = item->fullId(); + if (!Cache.contains(session)) { + Cache.emplace(session); + session->lifetime().add([session] { + Cache.remove(session); + }); + } + + return [channel, id](auto consumer) { + const auto session = &channel->session(); + auto &map = Cache[session].map; + auto i = map.find(id); + if (i == end(map)) { + i = map.emplace(id).first; + const auto finishWith = [=](UserData *user) { + auto &entry = Cache[session].map[id]; + entry.user = user; + for (const auto &callback : base::take(entry.callbacks)) { + callback(user); + } + }; + session->api().request(MTPchannels_GetMessageAuthor( + channel->inputChannel, + MTP_int(id.msg.bare) + )).done([=](const MTPUser &result) { + finishWith(session->data().processUser(result)); + }).fail([=] { + finishWith(nullptr); + }).send(); + } else if (const auto user = i->second.user + ; user || i->second.callbacks.empty()) { + if (user) { + consumer.put_next(not_null(user)); + } + return rpl::lifetime(); + } + + auto lifetime = rpl::lifetime(); + const auto done = [=](UserData *result) { + if (result) { + consumer.put_next(not_null(result)); + } + }; + const auto guard = lifetime.make_state<base::has_weak_ptr>(); + i->second.callbacks.push_back(crl::guard(guard, done)); + return lifetime; + }; +} + +[[nodiscard]] base::unique_qptr<Ui::Menu::ItemBase> MakeMessageAuthorAction( + not_null<Ui::PopupMenu*> menu, + not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller) { + const auto parent = menu->menu(); + const auto user = std::make_shared<UserData*>(nullptr); + const auto action = Ui::Menu::CreateAction( + parent, + tr::lng_contacts_loading(tr::now), + [=] { if (*user) { controller->showPeerInfo(*user); } }); + action->setDisabled(true); + auto lifetime = LookupMessageAuthor( + item + ) | rpl::start_with_next([=](not_null<UserData*> author) { + action->setText( + tr::lng_context_sent_by(tr::now, lt_user, author->name())); + action->setDisabled(false); + *user = author; + }); + auto result = base::make_unique_q<Ui::Menu::Action>( + menu->menu(), + st::whoSentItem, + action, + nullptr, + nullptr); + result->lifetime().add(std::move(lifetime)); + return result; +} + } // namespace ContextMenuRequest::ContextMenuRequest( @@ -1292,7 +1384,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu( if (hasWhoReactedItem) { AddWhoReactedAction(result, list, item, list->controller()); } else if (item) { - MaybeAddWhenEditedForwardedAction(result, item); + MaybeAddWhenEditedForwardedAction(result, item, list->controller()); } return result; @@ -1466,9 +1558,10 @@ void AddSaveSoundForNotifications( }, &st::menuIconSoundAdd); } -void AddWhenEditedForwardedActionHelper( +void AddWhenEditedForwardedAuthorActionHelper( not_null<Ui::PopupMenu*> menu, not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller, bool insertSeparator) { if (const auto forwarded = item->Get<HistoryMessageForwarded>()) { if (!forwarded->story && forwarded->psaType.isEmpty()) { @@ -1489,6 +1582,12 @@ void AddWhenEditedForwardedActionHelper( Api::WhenEdited(item->from(), edited->date))); } } + if (item->canLookupMessageAuthor()) { + if (insertSeparator && !menu->empty()) { + menu->addSeparator(&st::expandedMenuSeparator); + } + menu->addAction(MakeMessageAuthorAction(menu, item, controller)); + } } void AddWhoReactedAction( @@ -1539,7 +1638,11 @@ void AddWhoReactedAction( menu->addSeparator(&st::expandedMenuSeparator); } if (item->history()->peer->isUser()) { - AddWhenEditedForwardedActionHelper(menu, item, false); + AddWhenEditedForwardedAuthorActionHelper( + menu, + item, + controller, + false); menu->addAction(Ui::WhenReadContextAction( menu.get(), Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds), @@ -1551,14 +1654,19 @@ void AddWhoReactedAction( Data::ReactedMenuFactory(&controller->session()), participantChosen, showAllChosen)); - AddWhenEditedForwardedActionHelper(menu, item, true); + AddWhenEditedForwardedAuthorActionHelper( + menu, + item, + controller, + true); } } void MaybeAddWhenEditedForwardedAction( not_null<Ui::PopupMenu*> menu, - not_null<HistoryItem*> item) { - AddWhenEditedForwardedActionHelper(menu, item, true); + not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller) { + AddWhenEditedForwardedAuthorActionHelper(menu, item, controller, true); } void AddEditTagAction( diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.h b/Telegram/SourceFiles/history/view/history_view_context_menu.h index 0e14fa8034..fb26345500 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.h +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.h @@ -88,7 +88,8 @@ void AddWhoReactedAction( not_null<Window::SessionController*> controller); void MaybeAddWhenEditedForwardedAction( not_null<Ui::PopupMenu*> menu, - not_null<HistoryItem*> item); + not_null<HistoryItem*> item, + not_null<Window::SessionController*> controller); void ShowWhoReactedMenu( not_null<base::unique_qptr<Ui::PopupMenu>*> menu, QPoint position, From af061125dd1d59bc2fac24ce49d9d2e6e402249f Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Wed, 4 Jun 2025 15:29:27 +0000 Subject: [PATCH 123/310] Fix Docker build without LTO --- Telegram/build/docker/centos_env/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 816a6df5b1..a2b219c59e 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -512,6 +512,7 @@ RUN git clone -b n6.1.1 --depth=1 https://github.com/FFmpeg/FFmpeg.git \ && ./configure \ --extra-cflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ --extra-cxxflags="-fno-lto -DCONFIG_SAFE_BITSTREAM_READER=1" \ + --extra-ldflags="-lstdc++" \ --disable-debug \ --disable-programs \ --disable-doc \ From 4659d5db5dbf46b262c59223ac7b5d2d4c64f0fd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 4 Jun 2025 21:35:43 +0400 Subject: [PATCH 124/310] Version 5.15. - Send Direct Messages to Channels. - Enable New Tab Layout for Topics. - Create Polls with Up To 12 Options. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/boxes/peer_list_controllers.cpp | 2 +- .../SourceFiles/boxes/send_gif_with_caption_box.cpp | 2 -- Telegram/SourceFiles/core/version.h | 4 ++-- .../history/view/history_view_chat_section.cpp | 3 --- .../history/view/history_view_subsection_tabs.cpp | 6 +++--- .../history/view/history_view_subsection_tabs.h | 4 ++-- .../platform/linux/notifications_manager_linux.cpp | 7 ++++--- .../platform/mac/notifications_manager_mac.mm | 2 +- .../SourceFiles/ui/controls/subsection_tabs_slider.cpp | 5 +---- .../SourceFiles/ui/controls/subsection_tabs_slider.h | 2 -- Telegram/SourceFiles/ui/dynamic_thumbnails.h | 2 +- Telegram/SourceFiles/ui/unread_badge.cpp | 2 +- Telegram/build/version | 10 +++++----- changelog.txt | 6 ++++++ 17 files changed, 36 insertions(+), 39 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 0510faaad4..f72376f831 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="5.14.3.0" /> + Version="5.15.0.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 2ed18d704b..e119d657b5 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 5,14,3,0 - PRODUCTVERSION 5,14,3,0 + FILEVERSION 5,15,0,0 + PRODUCTVERSION 5,15,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "5.14.3.0" + VALUE "FileVersion", "5.15.0.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.14.3.0" + VALUE "ProductVersion", "5.15.0.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 2b9ca09531..f2d7d8b38e 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 5,14,3,0 - PRODUCTVERSION 5,14,3,0 + FILEVERSION 5,15,0,0 + PRODUCTVERSION 5,15,0,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", "5.14.3.0" + VALUE "FileVersion", "5.15.0.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.14.3.0" + VALUE "ProductVersion", "5.15.0.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index c33da2ab35..37ab361a8f 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -1275,7 +1275,7 @@ std::unique_ptr<PeerListRow> ChooseSublistBoxController::createSearchRow( auto ChooseSublistBoxController::createRow( not_null<Data::SavedSublist*> sublist) -> std::unique_ptr<PeerListRow> { - if (const auto skip = _filter && !_filter(sublist)) { + if (_filter && !_filter(sublist)) { return nullptr; } auto result = std::make_unique<PeerListRow>(sublist->sublistPeer()); diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp index f375f65349..7deadbe2f0 100644 --- a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -245,8 +245,6 @@ void CaptionBox( box->setWidth(st::boxWidth); box->getDelegate()->setStyle(st::sendGifBox); - const auto container = box->verticalLayout(); - const auto input = AddInputField(box, controller); box->setFocusCallback([=] { input->setFocus(); diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 4e9087855b..f52d1ee876 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5014003; -constexpr auto AppVersionStr = "5.14.3"; +constexpr auto AppVersion = 5015000; +constexpr auto AppVersionStr = "5.15"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 9c3c948da9..a66c10b819 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2978,9 +2978,6 @@ rpl::producer<Data::MessagesSlice> ChatWidget::sublistSource( Data::MessagePosition aroundId, int limitBefore, int limitAfter) { - const auto messageId = aroundId.fullId.msg - ? aroundId.fullId.msg - : (ServerMaxMsgId - 1); return _sublist->source( aroundId, limitBefore, diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 207bd4b1cb..b070db4ca1 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -41,7 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { -constexpr auto kDefaultLimit = 5; AssertIsDebug()// 10; +constexpr auto kDefaultLimit = 12; } // namespace @@ -349,7 +349,7 @@ void SubsectionTabs::setupSlider( ? was : Ui::MakeUserpicThumbnail(peer); sections.push_back({ - .text = peer->shortName(), + .text = { peer->shortName() }, .userpic = std::move(userpic), }); } else { @@ -363,7 +363,7 @@ void SubsectionTabs::setupSlider( } } else { sections.push_back({ - .text = tr::lng_filters_all_short(tr::now), + .text = { tr::lng_filters_all_short(tr::now) }, .userpic = Ui::MakeAllSubsectionsThumbnail(textFg), }); } diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index 15cf7c6d75..ef775ee8f4 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -69,10 +69,10 @@ private: DocumentId iconId = 0; QString name; - friend inline constexpr auto operator<=>( + friend inline auto operator<=>( const Item &, const Item &) = default; - friend inline constexpr bool operator==( + friend inline bool operator==( const Item &, const Item &) = default; }; diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 8303e0d033..f73fceab05 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -369,9 +369,10 @@ Manager::Private::Private(not_null<Manager*> manager) .contextId = ContextId{ .sessionId = dict.lookup_value("session").get_uint64(), .peerId = PeerId(dict.lookup_value("peer").get_uint64()), - .topicRootId = dict.lookup_value("topic").get_int64(), - .monoforumPeerId = dict.lookup_value( - "monoforumpeer").get_uint64(), + .topicRootId = MsgId( + dict.lookup_value("topic").get_int64()), + .monoforumPeerId = PeerId(dict.lookup_value( + "monoforumpeer").get_uint64()), }, .msgId = dict.lookup_value("msgid").get_int64(), }; diff --git a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm index 7ab8462891..b92cd90f55 100644 --- a/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm +++ b/Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm @@ -249,7 +249,7 @@ private: }; struct ClearFromSublist { ContextId contextId; - } + }; struct ClearFromHistory { ContextId partialContextId; }; diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index eae444f628..2cc4fcabdd 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -300,7 +300,6 @@ void SubsectionSlider::setupBar() { }, _bar->lifetime()); _bar->paintRequest() | rpl::start_with_next([=](QRect clip) { const auto start = -_barSt.stroke / 2; - const auto finalRange = getFinalActiveRange(); const auto currentRange = getCurrentActiveRange(); const auto from = currentRange.from + _barSt.skip; const auto size = currentRange.size - 2 * _barSt.skip; @@ -471,7 +470,6 @@ bool SubsectionSlider::buttonPaused() { } float64 SubsectionSlider::buttonActive(not_null<SubsectionButton*> button) { - const auto finalRange = getFinalActiveRange(); const auto currentRange = getCurrentActiveRange(); const auto from = _vertical ? button->y() : button->x(); const auto size = _vertical ? button->height() : button->width(); @@ -505,8 +503,7 @@ not_null<SubsectionButton*> SubsectionSlider::buttonAt(int index) { } VerticalSlider::VerticalSlider(not_null<QWidget*> parent) -: SubsectionSlider(parent, true) -, _st(st::chatTabsVertical) { +: SubsectionSlider(parent, true) { } VerticalSlider::~VerticalSlider() = default; diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h index 30d35aa190..d793085b04 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -148,8 +148,6 @@ private: std::unique_ptr<SubsectionButton> makeButton( SubsectionTab &&data) override; - const style::ChatTabsVertical &_st; - }; class HorizontalSlider final : public SubsectionSlider { diff --git a/Telegram/SourceFiles/ui/dynamic_thumbnails.h b/Telegram/SourceFiles/ui/dynamic_thumbnails.h index 6e003bbe35..e58c300fab 100644 --- a/Telegram/SourceFiles/ui/dynamic_thumbnails.h +++ b/Telegram/SourceFiles/ui/dynamic_thumbnails.h @@ -34,7 +34,7 @@ class DynamicImage; [[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail( not_null<Data::Session*> owner, const QString &data, - Fn<bool()> paused = false, + Fn<bool()> paused = nullptr, Fn<QColor()> textColor = nullptr); [[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnail( not_null<PhotoData*> photo, diff --git a/Telegram/SourceFiles/ui/unread_badge.cpp b/Telegram/SourceFiles/ui/unread_badge.cpp index 8730b46c58..88f6d376d7 100644 --- a/Telegram/SourceFiles/ui/unread_badge.cpp +++ b/Telegram/SourceFiles/ui/unread_badge.cpp @@ -139,7 +139,7 @@ int PeerBadge::drawGetWidth(Painter &p, Descriptor &&descriptor) { const auto peer = descriptor.peer; if ((descriptor.scam && (peer->isScam() || peer->isFake())) - || descriptor.direct && peer->isMonoforum()) { + || (descriptor.direct && peer->isMonoforum())) { return drawTextBadge(p, descriptor); } const auto verifyCheck = descriptor.verified && peer->isVerified(); diff --git a/Telegram/build/version b/Telegram/build/version index af8ef69b2c..2ffce92c2c 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5014003 -AppVersionStrMajor 5.14 -AppVersionStrSmall 5.14.3 -AppVersionStr 5.14.3 +AppVersion 5015000 +AppVersionStrMajor 5.15 +AppVersionStrSmall 5.15 +AppVersionStr 5.15.0 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.14.3 +AppVersionOriginal 5.15 diff --git a/changelog.txt b/changelog.txt index f9fc66744e..3e4dbdc0f1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +5.15 (04.06.25) + +- Send Direct Messages to Channels. +- Enable New Tab Layout for Topics. +- Create Polls with Up To 12 Options. + 5.14.3 (18.05.25) - Fix stale birthday suggestions removing. From 133d7874e3fb5fec2a15f999b5583f7f364e43e6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 08:53:05 +0400 Subject: [PATCH 125/310] Revert d3d compiler to 10.0.22621.3233. --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index dd3a6bcaaa..2130c73ccc 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit dd3a6bcaaa37f4d873bbdba840f858696a58735b +Subproject commit 2130c73cccfc1acc79675b838c2e211699199858 From 16d5dbe71cadeb67d898012b3d34c3cd1a1ab6c0 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 08:59:07 +0400 Subject: [PATCH 126/310] Fix crash in group chat context menu. Fixes #29387. --- Telegram/SourceFiles/api/api_who_reacted.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 88cd463dac..84ab9b0c2c 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -713,7 +713,7 @@ bool WhoReadExists(not_null<HistoryItem*> item) { if ((!chat && !megagroup) || (megagroup && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden)) - || megagroup->isMonoforum()) { + || (megagroup && megagroup->isMonoforum())) { return false; } const auto &appConfig = peer->session().appConfig(); From 4e5082f6c640095d4b6a723b907a0da621fdb1dd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 09:06:18 +0400 Subject: [PATCH 127/310] Fix comments root view position. Fixes #29389. --- Telegram/SourceFiles/history/view/history_view_chat_section.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index a66c10b819..c9421d8b25 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -306,7 +306,7 @@ ChatWidget::ChatWidget( _topBar->show(); if (_repliesRootView) { - _repliesRootView->move(0, _topBar->height()); + _repliesRootView->move(0, 0); } _topBar->deleteSelectionRequest( From 93164808840efa78933249e9bea8279becb82717 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 09:22:44 +0400 Subject: [PATCH 128/310] Don't generate joined message in monoforums. --- Telegram/SourceFiles/history/history.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 1bb9b4fb23..a1731627a6 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -3440,9 +3440,10 @@ Data::HistoryMessages *History::maybeMessages() { HistoryItem *History::insertJoinedMessage() { const auto channel = peer->asChannel(); if (!channel + || channel->isMonoforum() || _joinedMessage || !channel->amIn() - || (peer->isMegagroup() + || (channel->isMegagroup() && channel->mgInfo->joinedMessageFound)) { return _joinedMessage; } From 0adb3b062f535f11efb74d65982f72819a408f9d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 09:55:16 +0400 Subject: [PATCH 129/310] Use only first name in birthday notification. --- Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp index 2b019dbc9e..400a1f5db0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp @@ -292,7 +292,7 @@ rpl::producer<Ui::SlideWrap<Ui::RpWidget>*> TopBarSuggestionValue( ? tr::lng_dialogs_suggestions_birthday_contact_title( tr::now, lt_text, - { first->name() }, + { first->shortName() }, Ui::Text::RichLangValue) : tr::lng_dialogs_suggestions_birthday_contacts_title( tr::now, From d25356917d4f64fe3482bded74ff30de4e66a575 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Thu, 5 Jun 2025 05:30:52 +0000 Subject: [PATCH 130/310] Stop setting CMAKE_EXE_LINKER_FLAGS in actions --- .github/workflows/linux.yml | 1 - .github/workflows/mac_packaged.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 0421fe6b31..f52621cafb 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -110,7 +110,6 @@ jobs: -D CMAKE_CONFIGURATION_TYPES=Debug \ -D CMAKE_C_FLAGS_DEBUG="-O0" \ -D CMAKE_CXX_FLAGS_DEBUG="-O0" \ - -D CMAKE_EXE_LINKER_FLAGS="-s" \ -D CMAKE_COMPILE_WARNING_AS_ERROR=ON \ -D TDESKTOP_API_TEST=ON \ -D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF \ diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 99fcf24f52..ddef1c7523 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -166,7 +166,6 @@ jobs: -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_C_FLAGS_DEBUG="" \ -DCMAKE_CXX_FLAGS_DEBUG="" \ - -DCMAKE_EXE_LINKER_FLAGS="-s" \ -DTDESKTOP_API_TEST=ON \ $DEFINE From e92adf94a77fba8619bd59d0414bd436ab1de2aa Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 10:45:06 +0400 Subject: [PATCH 131/310] Improve adaptive loading in subsection tabs. --- .../view/history_view_subsection_tabs.cpp | 68 +++++++++++-------- .../view/history_view_subsection_tabs.h | 3 + 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index b070db4ca1..564750bc8d 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -241,45 +241,24 @@ void SubsectionTabs::setupSlider( : scroll->scrollLeftMax(); const auto availableFrom = scrollValue; const auto availableTill = (scrollMax - scrollValue); - if (scrollMax <= 3 * full && _afterAvailable > 0) { + const auto needMore = (scrollMax <= 3 * full && _afterAvailable > 0); + if (needMore) { _beforeLimit *= 2; _afterLimit *= 2; } - const auto findMiddle = [&] { - Expects(!_slice.empty()); - - auto best = -1; - auto bestDistance = -1; - const auto ideal = scrollValue + (full / 2); - for (auto i = 0, count = int(_slice.size()); i != count; ++i) { - const auto a = slider->lookupSectionPosition(i); - const auto b = (i + 1 == count) - ? (full + scrollMax) - : slider->lookupSectionPosition(i + 1); - const auto middle = (a + b) / 2; - const auto distance = std::abs(middle - ideal); - if (best < 0 || distance < bestDistance) { - best = i; - bestDistance = distance; - } - } - - Ensures(best >= 0); - return best; - }; if (availableFrom < full && _beforeSkipped.value_or(0) > 0 && !_slice.empty()) { - _around = _slice[findMiddle()].thread; - refreshSlice(); + refreshAroundMiddle(scroll, slider); } else if (availableTill < full) { if (_afterAvailable > 0) { - _around = _slice[findMiddle()].thread; - refreshSlice(); + refreshAroundMiddle(scroll, slider); } else if (!_afterSkipped.has_value()) { _loading = true; loadMore(); } + } else if (needMore) { + refreshAroundMiddle(scroll, slider); } }, scroll->lifetime()); @@ -641,6 +620,41 @@ void SubsectionTabs::track() { } } +void SubsectionTabs::refreshAroundMiddle( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider) { + Expects(!_slice.empty()); + + const auto full = _vertical ? scroll->height() : scroll->width(); + const auto scrollValue = _vertical + ? scroll->scrollTop() + : scroll->scrollLeft(); + const auto scrollMax = _vertical + ? scroll->scrollTopMax() + : scroll->scrollLeftMax(); + + auto best = -1; + auto bestDistance = -1; + const auto ideal = scrollValue + (full / 2); + for (auto i = 0, count = int(_slice.size()); i != count; ++i) { + const auto a = slider->lookupSectionPosition(i); + const auto b = (i + 1 == count) + ? (full + scrollMax) + : slider->lookupSectionPosition(i + 1); + const auto middle = (a + b) / 2; + const auto distance = std::abs(middle - ideal); + if (best < 0 || distance < bestDistance) { + best = i; + bestDistance = distance; + } + } + + Assert(best >= 0 && best < _slice.size()); + + _around = _slice[best].thread; + refreshSlice(); +} + void SubsectionTabs::refreshSlice() { _refreshScheduled = false; diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index ef775ee8f4..75a37104ea 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -83,6 +83,9 @@ private: void toggleModes(); void setVisible(bool shown); void refreshSlice(); + void refreshAroundMiddle( + not_null<Ui::ScrollArea*> scroll, + not_null<Ui::SubsectionSlider*> slider); void scheduleRefresh(); void loadMore(); void setup(not_null<Ui::RpWidget*> parent); From a08436ecd2899a069947db86e3ef888e7efe36a5 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 11:04:23 +0400 Subject: [PATCH 132/310] Fix unread counters in filters with monoforums. --- Telegram/SourceFiles/history/history.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index a1731627a6..40ba51d43e 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2334,6 +2334,9 @@ History *History::migrateSibling() const { Dialogs::UnreadState History::chatListUnreadState() const { if (const auto forum = peer->forum()) { return AdjustedForumUnreadState(forum->topicsList()->unreadState()); + } else if (const auto monoforum = peer->monoforum()) { + return AdjustedForumUnreadState( + monoforum->chatsList()->unreadState()); } return computeUnreadState(); } From 11986ac6982c59ed425c9ea70c21cda44364ea74 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 11:33:43 +0400 Subject: [PATCH 133/310] Show star in channel direct messages settings. --- .../boxes/peers/edit_peer_info_box.cpp | 94 ++++++++++++------- .../boxes/peers/edit_peer_info_box.h | 7 ++ .../boosts/create_giveaway_box.cpp | 2 +- .../ui/widgets/expandable_peer_list.cpp | 4 +- 4 files changed, 72 insertions(+), 35 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 4604c4f6a7..19611e16e7 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -82,6 +82,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "api/api_invite_links.h" #include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" @@ -133,7 +134,7 @@ void AddButtonWithCount( not_null<Ui::SettingsButton*> AddButtonWithText( not_null<Ui::VerticalLayout*> parent, rpl::producer<QString> &&text, - rpl::producer<QString> &&label, + rpl::producer<TextWithEntities> &&label, Fn<void()> callback, Settings::IconDescriptor &&descriptor) { return parent->add(EditPeerInfoBox::CreateButton( @@ -145,6 +146,20 @@ not_null<Ui::SettingsButton*> AddButtonWithText( std::move(descriptor))); } +not_null<Ui::SettingsButton*> AddButtonWithText( + not_null<Ui::VerticalLayout*> parent, + rpl::producer<QString> &&text, + rpl::producer<QString> &&label, + Fn<void()> callback, + Settings::IconDescriptor &&descriptor) { + return AddButtonWithText( + parent, + std::move(text), + std::move(label) | Ui::Text::ToWithEntities(), + std::move(callback), + std::move(descriptor)); +} + void AddButtonDelete( not_null<Ui::VerticalLayout*> parent, rpl::producer<QString> &&text, @@ -1077,10 +1092,14 @@ void Controller::fillDirectMessagesButton() { auto label = _starsPerDirectMessageSavedValue->value( ) | rpl::map([](int starsPerMessage) { return (starsPerMessage < 0) - ? tr::lng_manage_monoforum_off() + ? tr::lng_manage_monoforum_off(Ui::Text::WithEntities) : !starsPerMessage - ? tr::lng_manage_monoforum_free() - : rpl::single(Lang::FormatCountDecimal(starsPerMessage)); + ? tr::lng_manage_monoforum_free(Ui::Text::WithEntities) + : rpl::single(Ui::Text::IconEmoji( + &st::starIconEmojiColored + ).append(' ').append( + Lang::FormatStarsAmountDecimal( + StarsAmount{ starsPerMessage }))); }) | rpl::flatten_latest(); AddButtonWithText( _controls.buttonsLayout, @@ -2866,6 +2885,22 @@ object_ptr<Ui::SettingsButton> EditPeerInfoBox::CreateButton( Fn<void()> callback, const style::SettingsCountButton &st, Settings::IconDescriptor &&descriptor) { + return CreateButton( + parent, + std::move(text), + std::move(count) | Ui::Text::ToWithEntities(), + std::move(callback), + st, + std::move(descriptor)); +} + +object_ptr<Ui::SettingsButton> EditPeerInfoBox::CreateButton( + not_null<QWidget*> parent, + rpl::producer<QString> &&text, + rpl::producer<TextWithEntities> &&labelText, + Fn<void()> callback, + const style::SettingsCountButton &st, + Settings::IconDescriptor &&descriptor) { auto result = object_ptr<Ui::SettingsButton>( parent, rpl::duplicate(text), @@ -2886,37 +2921,49 @@ object_ptr<Ui::SettingsButton> EditPeerInfoBox::CreateButton( std::move(descriptor)); } - auto labelText = rpl::combine( + const auto label = Ui::CreateChild<Ui::FlatLabel>( + button, + rpl::duplicate(labelText), + st.label); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->show(); + + rpl::combine( rpl::duplicate(text), - std::move(count), + std::move(labelText), button->widthValue() - ) | rpl::map([&st](const QString &text, const QString &count, int width) { + ) | rpl::start_with_next([&st, label]( + const QString &text, + const TextWithEntities &labelText, + int width) { const auto available = width - st.button.padding.left() - (st.button.style.font->spacew * 2) - st.button.style.font->width(text) - st.labelPosition.x(); - const auto required = st.label.style.font->width(count); - return (required > available) - ? st.label.style.font->elided(count, std::max(available, 0)) - : count; - }); + const auto required = label->textMaxWidth(); + label->resizeToWidth(std::min(required, available)); + label->moveToRight( + st.labelPosition.x(), + st.labelPosition.y(), + width); + }, label->lifetime()); if (badge) { rpl::combine( std::move(text), - rpl::duplicate(labelText), + label->widthValue(), button->widthValue() ) | rpl::start_with_next([=]( const QString &text, - const QString &label, + int labelWidth, int width) { const auto space = st.button.style.font->spacew; const auto left = st.button.padding.left() + st.button.style.font->width(text) + space; const auto right = st.labelPosition.x() - + st.label.style.font->width(label) + + labelWidth + (space * 2); const auto available = width - left - right; badge->setVisible(available >= badge->width()); @@ -2930,23 +2977,6 @@ object_ptr<Ui::SettingsButton> EditPeerInfoBox::CreateButton( }, badge->lifetime()); } - const auto label = Ui::CreateChild<Ui::FlatLabel>( - button, - std::move(labelText), - st.label); - label->setAttribute(Qt::WA_TransparentForMouseEvents); - label->show(); - - rpl::combine( - button->widthValue(), - label->widthValue() - ) | rpl::start_with_next([=, &st](int outerWidth, int width) { - label->moveToRight( - st.labelPosition.x(), - st.labelPosition.y(), - outerWidth); - }, label->lifetime()); - return result; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h index b8787afac9..f918547c1b 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.h @@ -46,6 +46,13 @@ public: Fn<void()> callback, const style::SettingsCountButton &st, Settings::IconDescriptor &&descriptor); + [[nodiscard]] static object_ptr<Ui::SettingsButton> CreateButton( + not_null<QWidget*> parent, + rpl::producer<QString> &&text, + rpl::producer<TextWithEntities> &&labelText, + Fn<void()> callback, + const style::SettingsCountButton &st, + Settings::IconDescriptor &&descriptor); protected: void prepare() override; diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp index a5a73614e6..09f5cdeb60 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/create_giveaway_box.cpp @@ -1250,7 +1250,7 @@ void CreateGiveawayBox( rpl::duplicate(creditsValueType), tr::lng_giveaway_additional_credits_about(), tr::lng_giveaway_additional_about() - ) | rpl::map(Ui::Text::WithEntities))); + ) | Ui::Text::ToWithEntities())); Ui::AddSkip(additionalWrap); } diff --git a/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp b/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp index fa43f4b920..a033a17b64 100644 --- a/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp +++ b/Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp @@ -151,8 +151,8 @@ void AddExpandablePeerList( using namespace Info::Profile; auto name = controller->data.bold - ? NameValue(peer) | rpl::map(Ui::Text::Bold) - : NameValue(peer) | rpl::map(Ui::Text::WithEntities); + ? NameValue(peer) | Ui::Text::ToBold() + : NameValue(peer) | Ui::Text::ToWithEntities(); const auto userpic = Ui::CreateChild<Ui::UserpicButton>(line, peer, st); const auto checkbox = Ui::CreateChild<Ui::Checkbox>( From 3cfdc9d8974cb44d6d61f0f4e022383b6bafa15b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 11:40:05 +0400 Subject: [PATCH 134/310] Fix setting group emoji status. --- .../boxes/peers/edit_peer_color_box.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index c1c160ac08..29019c1091 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -574,15 +574,15 @@ void Set( MTP_flags(Flag::f_color | Flag::f_background_emoji_id), MTP_int(values.colorIndex), MTP_long(values.backgroundEmojiId))); - } else if (peer->isMegagroup()) { } else if (const auto channel = peer->asChannel()) { - using Flag = MTPchannels_UpdateColor::Flag; - send(MTPchannels_UpdateColor( - MTP_flags(Flag::f_color | Flag::f_background_emoji_id), - channel->inputChannel, - MTP_int(values.colorIndex), - MTP_long(values.backgroundEmojiId))); - + if (peer->isBroadcast()) { + using Flag = MTPchannels_UpdateColor::Flag; + send(MTPchannels_UpdateColor( + MTP_flags(Flag::f_color | Flag::f_background_emoji_id), + channel->inputChannel, + MTP_int(values.colorIndex), + MTP_long(values.backgroundEmojiId))); + } if (values.statusChanged && (values.statusId || peer->emojiStatusId())) { peer->owner().emojiStatuses().set( From fb2274c58dbfb09a135cd49cfae091f4f4522969 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 12:04:28 +0400 Subject: [PATCH 135/310] Fix glitch in new forum layout opening. --- Telegram/SourceFiles/dialogs/dialogs_widget.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index ce0912c934..1d578857d8 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -872,7 +872,8 @@ void Widget::chosenRow(const ChosenRow &row) { } else if (row.newWindow) { controller()->showInNewWindow(Window::SeparateId(topicJump)); } else { - if (!controller()->adaptive().isOneColumn()) { + if (!controller()->adaptive().isOneColumn() + && !topicJump->channel()->useSubsectionTabs()) { controller()->showForum( topicJump->forum(), Window::SectionShow().withChildColumn()); @@ -931,7 +932,8 @@ void Widget::chosenRow(const ChosenRow &row) { } else { controller()->showForum( forum, - Window::SectionShow().withChildColumn()); + Window::SectionShow( + Window::SectionShow::Way::ClearStack).withChildColumn()); if (controller()->shownForum().current() == forum && forum->channel()->viewForumAsMessages()) { controller()->showThread( From 265b7904a8b8af633ed594178030f7896505b602 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 12:10:31 +0400 Subject: [PATCH 136/310] Fix blockquote/code captions to media in reply bar. --- Telegram/SourceFiles/data/data_media_types.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 2686a39af9..cd0f169ec2 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -922,9 +922,9 @@ ItemPreview MediaPhoto::toPreview(ToPreviewOptions options) const { const auto type = tr::lng_in_dlg_photo(tr::now); const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); + : Dialogs::Ui::DialogsPreviewText(options.translated + ? parent()->translatedText() + : parent()->originalText()); const auto hasMiniImages = !images.empty(); return { .text = WithCaptionNotificationText(type, caption, hasMiniImages), @@ -1194,9 +1194,9 @@ ItemPreview MediaFile::toPreview(ToPreviewOptions options) const { }(); const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); + : Dialogs::Ui::DialogsPreviewText(options.translated + ? parent()->translatedText() + : parent()->originalText()); const auto hasMiniImages = !images.empty(); return { .text = WithCaptionNotificationText(type, caption, hasMiniImages), @@ -2199,9 +2199,9 @@ ItemPreview MediaInvoice::toPreview(ToPreviewOptions options) const { const auto type = ComputeAlbumCountsString(counts); const auto caption = (options.hideCaption || options.ignoreMessageText) ? TextWithEntities() - : options.translated - ? parent()->translatedText() - : parent()->originalText(); + : Dialogs::Ui::DialogsPreviewText(options.translated + ? parent()->translatedText() + : parent()->originalText()); const auto hasMiniImages = !images.empty(); auto nice = Ui::Text::Colorized( Ui::CreditsEmojiSmall(&parent()->history()->session())); From 4b25406d14964a64d222abb05ede95fc532724b3 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 12:19:27 +0400 Subject: [PATCH 137/310] Remove delay when switching subsection tabs. --- .../ui/controls/subsection_tabs_slider.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index 2cc4fcabdd..d1006e53a1 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/controls/subsection_tabs_slider.h" -#include "base/call_delayed.h" #include "dialogs/dialogs_three_state_icon.h" #include "ui/effects/ripple_animation.h" #include "ui/dynamic_image.h" @@ -384,14 +383,13 @@ void SubsectionSlider::activate(int index) { } } }; - const auto duration = st::chatTabsSlider.duration; - _activeFrom.start(callback, was.from, now.from, duration); - _activeSize.start(callback, was.size, now.size, duration); - base::call_delayed(duration, this, [=] { - if (_active == index) { - _sectionActivated.fire_copy(index); - } - }); + const auto weak = Ui::MakeWeak(_bar); + _sectionActivated.fire_copy(index); + if (weak) { + const auto duration = st::chatTabsSlider.duration; + _activeFrom.start(callback, was.from, now.from, duration); + _activeSize.start(callback, was.size, now.size, duration); + } } void SubsectionSlider::setActiveSectionFast(int active) { From 3bc20c35509deeae2c4e9f924fade85b5be8ee75 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 12:47:16 +0400 Subject: [PATCH 138/310] Save last opened subsection within a launch. --- Telegram/SourceFiles/data/data_channel.cpp | 4 ++-- Telegram/SourceFiles/data/data_forum.cpp | 17 +++++++++++++ Telegram/SourceFiles/data/data_forum.h | 5 ++++ .../SourceFiles/data/data_saved_messages.cpp | 14 +++++++++++ .../SourceFiles/data/data_saved_messages.h | 5 ++++ Telegram/SourceFiles/data/data_thread.cpp | 15 ++++++++++++ Telegram/SourceFiles/data/data_thread.h | 2 ++ .../SourceFiles/dialogs/dialogs_widget.cpp | 24 ++++++++++++++++++- .../SourceFiles/history/history_widget.cpp | 4 ++++ .../view/history_view_chat_section.cpp | 6 +++++ .../window/window_session_controller.cpp | 15 ++++++++---- 11 files changed, 103 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index bb41dd4b4e..7a0f6a502a 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -409,8 +409,8 @@ void ChannelData::setPendingRequestsCount( } bool ChannelData::useSubsectionTabs() const { - return isForum() - && (flags() & ChannelDataFlag::ForumTabs); + return amMonoforumAdmin() + || (isForum() && (flags() & ChannelDataFlag::ForumTabs)); } ChatRestrictionsInfo ChannelData::KickedRestrictedRights( diff --git a/Telegram/SourceFiles/data/data_forum.cpp b/Telegram/SourceFiles/data/data_forum.cpp index ffc92a9847..3890b4c0ed 100644 --- a/Telegram/SourceFiles/data/data_forum.cpp +++ b/Telegram/SourceFiles/data/data_forum.cpp @@ -189,6 +189,9 @@ void Forum::applyTopicDeleted(MsgId rootId) { reorderLastTopics(); } + if (_activeSubsectionTopic == raw) { + _activeSubsectionTopic = nullptr; + } _topicDestroyed.fire(raw); session().changes().topicUpdated( raw, @@ -259,6 +262,20 @@ const std::vector<not_null<ForumTopic*>> &Forum::recentTopics() const { return _lastTopics; } +void Forum::saveActiveSubsectionThread(not_null<Thread*> thread) { + if (const auto topic = thread->asTopic()) { + Assert(topic->forum() == this); + _activeSubsectionTopic = topic->creating() ? nullptr : topic; + } else { + Assert(thread == history()); + _activeSubsectionTopic = nullptr; + } +} + +Thread *Forum::activeSubsectionThread() const { + return _activeSubsectionTopic; +} + void Forum::listMessageChanged(HistoryItem *from, HistoryItem *to) { if (from || to) { reorderLastTopics(); diff --git a/Telegram/SourceFiles/data/data_forum.h b/Telegram/SourceFiles/data/data_forum.h index 732ef80097..ae0cc2d1a5 100644 --- a/Telegram/SourceFiles/data/data_forum.h +++ b/Telegram/SourceFiles/data/data_forum.h @@ -96,6 +96,9 @@ public: [[nodiscard]] auto recentTopics() const -> const std::vector<not_null<ForumTopic*>> &; + void saveActiveSubsectionThread(not_null<Thread*> thread); + [[nodiscard]] Thread *activeSubsectionThread() const; + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } @@ -129,6 +132,8 @@ private: std::vector<not_null<ForumTopic*>> _lastTopics; int _lastTopicsVersion = 0; + ForumTopic *_activeSubsectionTopic = nullptr; + rpl::event_stream<> _chatsListChanges; rpl::event_stream<> _chatsListLoadedEvents; diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 8108530c3b..12f3e870b9 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -82,6 +82,20 @@ void SavedMessages::clear() { _owningHistory = nullptr; } +void SavedMessages::saveActiveSubsectionThread(not_null<Thread*> thread) { + if (const auto sublist = thread->asSublist()) { + Assert(sublist->parent() == this); + _activeSubsectionSublist = sublist; + } else { + Assert(thread == _owningHistory); + _activeSubsectionSublist = nullptr; + } +} + +Thread *SavedMessages::activeSubsectionThread() const { + return _activeSubsectionSublist; +} + SavedMessages::~SavedMessages() { clear(); } diff --git a/Telegram/SourceFiles/data/data_saved_messages.h b/Telegram/SourceFiles/data/data_saved_messages.h index 34800518b5..193dfc25d5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.h +++ b/Telegram/SourceFiles/data/data_saved_messages.h @@ -77,6 +77,9 @@ public: void clear(); + void saveActiveSubsectionThread(not_null<Thread*> thread); + Thread *activeSubsectionThread() const; + [[nodiscard]] rpl::lifetime &lifetime(); private: @@ -132,6 +135,8 @@ private: rpl::event_stream<> _chatsListChanges; rpl::event_stream<> _chatsListLoadedEvents; + SavedSublist *_activeSubsectionSublist = nullptr; + bool _pinnedLoaded = false; bool _unsupported = false; diff --git a/Telegram/SourceFiles/data/data_thread.cpp b/Telegram/SourceFiles/data/data_thread.cpp index 702aed3639..47a8f22232 100644 --- a/Telegram/SourceFiles/data/data_thread.cpp +++ b/Telegram/SourceFiles/data/data_thread.cpp @@ -7,9 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_thread.h" +#include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_changes.h" +#include "data/data_channel.h" #include "data/data_peer.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "history/history.h" #include "history/history_item.h" @@ -202,4 +205,16 @@ void Thread::setHasPinnedMessages(bool has) { EntryUpdate::Flag::HasPinnedMessages); } +void Thread::saveMeAsActiveSubsectionThread() { + if (const auto channel = owningHistory()->peer->asChannel()) { + if (channel->useSubsectionTabs()) { + if (const auto forum = channel->forum()) { + forum->saveActiveSubsectionThread(this); + } else if (const auto monoforum = channel->monoforum()) { + monoforum->saveActiveSubsectionThread(this); + } + } + } +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_thread.h b/Telegram/SourceFiles/data/data_thread.h index 09a33f27da..42fd2dca06 100644 --- a/Telegram/SourceFiles/data/data_thread.h +++ b/Telegram/SourceFiles/data/data_thread.h @@ -120,6 +120,8 @@ public: [[nodiscard]] bool hasPinnedMessages() const; void setHasPinnedMessages(bool has); + void saveMeAsActiveSubsectionThread(); + protected: void setUnreadMarkFlag(bool unread); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 1d578857d8..48f82bbd2e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -79,6 +79,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_download_manager.h" #include "data/data_chat_filters.h" +#include "data/data_saved_messages.h" #include "data/data_saved_sublist.h" #include "data/data_stories.h" #include "info/downloads/info_downloads_widget.h" @@ -920,7 +921,8 @@ void Widget::chosenRow(const ChosenRow &row) { && history->isForum() && !row.message.fullId && (!controller()->adaptive().isOneColumn() - || !history->peer->forum()->channel()->viewForumAsMessages())) { + || !history->peer->forum()->channel()->viewForumAsMessages() + || history->peer->forum()->channel()->useSubsectionTabs())) { const auto forum = history->peer->forum(); if (controller()->shownForum().current() == forum) { controller()->closeForum(); @@ -943,6 +945,26 @@ void Widget::chosenRow(const ChosenRow &row) { } } return; + } else if (history + && history->amMonoforumAdmin() + && !row.message.fullId) { + const auto monoforum = history->peer->monoforum(); + if (row.newWindow) { + controller()->showInNewWindow( + Window::SeparateId(Window::SeparateType::Chat, history)); + } else { + if (const auto active = monoforum->activeSubsectionThread()) { + controller()->showThread( + active, + ShowAtUnreadMsgId, + Window::SectionShow::Way::ClearStack); + } else { + controller()->showPeerHistory( + history, + Window::SectionShow::Way::ClearStack); + } + } + return; } else if (history) { const auto peer = history->peer; const auto showAtMsgId = controller()->uniqueChatsInSearchResults() diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 397b8bdc88..b468697738 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4834,6 +4834,10 @@ void HistoryWidget::doneShow() { controller()->widget()->setInnerFocus(); _preserveScrollTop = false; checkSuggestToGigagroup(); + + if (_history) { + _history->saveMeAsActiveSubsectionThread(); + } } void HistoryWidget::cornerButtonsShowAtPosition( diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index c9421d8b25..6bbe275c07 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2880,6 +2880,12 @@ void ChatWidget::showFinishedHook() { // because after that the method showChildren() is called. setupDragArea(); updatePinnedVisibility(); + + if (_topic) { + _topic->saveMeAsActiveSubsectionThread(); + } else if (_sublist) { + _sublist->saveMeAsActiveSubsectionThread(); + } } bool ChatWidget::floatPlayerHandleWheelEvent(QEvent *e) { diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 5a49bab04d..3e3a77260f 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -586,7 +586,8 @@ void SessionNavigation::showPeerByLinkResolved( if (const auto forum = peer->forum()) { if (controller->windowId().hasChatsList() && !controller->adaptive().isOneColumn() - && controller->shownForum().current() != forum) { + && controller->shownForum().current() != forum + && !forum->channel()->useSubsectionTabs()) { controller->showForum(forum); } } @@ -1878,7 +1879,11 @@ void SessionController::showForum( if (showForumInDifferentWindow(forum, params)) { return; } else if (forum->channel()->useSubsectionTabs()) { - showPeerHistory(forum->channel(), params); + if (const auto active = forum->activeSubsectionThread()) { + showThread(active, ShowAtUnreadMsgId, params); + } else { + showPeerHistory(forum->channel(), params); + } return; } _shownForumLifetime.destroy(); @@ -1992,9 +1997,9 @@ void SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) { Data::PeerFlagValue( channel, ChannelData::Flag::Forum - ) | rpl::filter( - rpl::mappers::_1 - ) | rpl::start_with_next([=] { + ) | rpl::filter([=](bool forum) { + return forum && !channel->useSubsectionTabs(); + }) | rpl::start_with_next([=] { clearSectionStack( { anim::type::normal, anim::activation::background }); showForum(channel->forum(), From e9e187c58b26ce8de99d1d774f7ea6e828ea0076 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 12:54:59 +0400 Subject: [PATCH 139/310] Ctrl+Click to open subsection in a new window. --- .../view/history_view_subsection_tabs.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 564750bc8d..9fc513c011 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_subsection_tabs.h" +#include "base/qt/qt_key_modifiers.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" @@ -35,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "window/window_peer_menu.h" +#include "window/window_separate_id.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" @@ -209,13 +211,19 @@ void SubsectionTabs::setupSlider( not_null<Ui::SubsectionSlider*> slider, bool vertical) { slider->sectionActivated() | rpl::start_with_next([=](int active) { + const auto newWindow = base::IsCtrlPressed(); if (active >= 0 && active < _slice.size() - && _active != _slice[active].thread) { - auto params = Window::SectionShow(); - params.way = Window::SectionShow::Way::ClearStack; - params.animated = anim::type::instant; - _controller->showThread(_slice[active].thread, {}, params); + && (newWindow || _active != _slice[active].thread)) { + const auto thread = _slice[active].thread; + if (newWindow) { + _controller->showInNewWindow(Window::SeparateId(thread)); + } else { + auto params = Window::SectionShow(); + params.way = Window::SectionShow::Way::ClearStack; + params.animated = anim::type::instant; + _controller->showThread(thread, ShowAtUnreadMsgId, params); + } } }, slider->lifetime()); From f9acb5d19bb1b6226f7f22cd7940a92ea6abaa5d Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 13:00:14 +0400 Subject: [PATCH 140/310] Fix activation of wrong tab after new window. --- .../SourceFiles/history/view/history_view_subsection_tabs.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index 9fc513c011..6c5187c781 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -218,6 +218,7 @@ void SubsectionTabs::setupSlider( const auto thread = _slice[active].thread; if (newWindow) { _controller->showInNewWindow(Window::SeparateId(thread)); + _refreshed.fire({}); // This should activate current section. } else { auto params = Window::SectionShow(); params.way = Window::SectionShow::Way::ClearStack; From 08681ac1b999cb8bf99a8e84a02e2ca202c37c1c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 13:39:33 +0400 Subject: [PATCH 141/310] Show join requests in new forums layout. --- .../SourceFiles/history/view/history_view_requests_bar.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp b/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp index e10a50b8a7..d0ddb48baf 100644 --- a/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp +++ b/Telegram/SourceFiles/history/view/history_view_requests_bar.cpp @@ -109,7 +109,9 @@ rpl::producer<Ui::RequestsBarContent> RequestsBarContentByPeer( auto state = lifetime.make_state<State>(peer); const auto pushNext = [=](bool now = false) { - if ((!showInForum && peer->isForum()) + if ((!showInForum + && peer->isForum() + && !peer->asChannel()->useSubsectionTabs()) || (std::min(state->current.count, kRecentRequestsLimit) != state->users.size())) { return; From 65cfd6c81c27a528c193e9c79233ba88ea81063c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 13:55:55 +0400 Subject: [PATCH 142/310] Fix new forum layout search and topics list. --- .../SourceFiles/dialogs/dialogs_widget.cpp | 18 +++++++++- .../info/profile/info_profile_actions.cpp | 34 ++++++++++++++++--- .../window/window_session_controller.cpp | 27 +++++++++------ .../window/window_session_controller.h | 1 + 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 48f82bbd2e..e3dc7d97cb 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -3516,7 +3516,10 @@ bool Widget::applySearchState(SearchState state) { showSearchInTopBar(anim::type::normal); } else if (_layout == Layout::Main) { _forumSearchRequested = true; - controller()->showForum(forum); + auto params = Window::SectionShow( + Window::SectionShow::Way::ClearStack); + params.forceTopicsList = true; + controller()->showForum(forum, params); } else { return false; } @@ -4345,6 +4348,19 @@ bool Widget::cancelSearch(CancelSearchOptions options) { } } updateForceDisplayWide(); + if (clearingInChat) { + if (const auto forum = controller()->shownForum().current()) { + if (forum->channel()->useSubsectionTabs()) { + const auto id = controller()->windowId(); + const auto initial = id.forum(); + if (!initial) { + controller()->closeForum(); + } else if (initial != forum) { + controller()->showForum(initial); + } + } + } + } return clearingQuery || clearingInChat || clearSearchFocus; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 8be1249d40..1e0cb0c857 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_folder.h" +#include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_peer_values.h" #include "data/data_session.h" @@ -1040,6 +1041,9 @@ private: not_null<ChannelData*> channel); Ui::MultiSlideTracker fillDiscussionButtons( not_null<ChannelData*> channel); + void addShowTopicsListButton( + Ui::MultiSlideTracker &tracker, + not_null<Data::Forum*> forum); void addReportReaction(Ui::MultiSlideTracker &tracker); void addReportReaction( @@ -2103,23 +2107,37 @@ void DetailsFiller::addReportReaction( } Ui::MultiSlideTracker DetailsFiller::fillTopicButtons() { + Ui::MultiSlideTracker tracker; + addShowTopicsListButton(tracker, _topic->forum()); + return tracker; +} + +void DetailsFiller::addShowTopicsListButton( + Ui::MultiSlideTracker &tracker, + not_null<Data::Forum*> forum) { using namespace rpl::mappers; - Ui::MultiSlideTracker tracker; const auto window = _controller->parentController(); - - const auto forum = _topic->forum(); + const auto channel = forum->channel(); auto showTopicsVisible = rpl::combine( window->adaptive().oneColumnValue(), window->shownForum().value(), _1 || (_2 != forum)); + const auto callback = [=] { + if (const auto forum = channel->forum()) { + if (channel->useSubsectionTabs()) { + window->searchInChat(forum->history()); + } else { + window->showForum(forum); + } + } + }; AddMainButton( _wrap, tr::lng_forum_show_topics_list(), std::move(showTopicsVisible), - [=] { window->showForum(forum); }, + callback, tracker); - return tracker; } Ui::MultiSlideTracker DetailsFiller::fillUserButtons( @@ -2216,6 +2234,12 @@ Ui::MultiSlideTracker DetailsFiller::fillDiscussionButtons( std::move(viewDiscussion), tracker); + if (const auto forum = channel->forum()) { + if (channel->useSubsectionTabs()) { + addShowTopicsListButton(tracker, forum); + } + } + return tracker; } diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 3e3a77260f..736c4c2743 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1876,9 +1876,10 @@ bool SessionController::showForumInDifferentWindow( void SessionController::showForum( not_null<Data::Forum*> forum, const SectionShow ¶ms) { + const auto forced = params.forceTopicsList; if (showForumInDifferentWindow(forum, params)) { return; - } else if (forum->channel()->useSubsectionTabs()) { + } else if (!forced && forum->channel()->useSubsectionTabs()) { if (const auto active = forum->activeSubsectionThread()) { showThread(active, ShowAtUnreadMsgId, params); } else { @@ -1914,20 +1915,26 @@ void SessionController::showForum( }); } }; + content()->showForum(forum, params); + if (_shownForum.current() != forum) { + return; + } + forum->destroyed( ) | rpl::start_with_next([=] { closeAndShowHistory(false); }, _shownForumLifetime); - using FlagChange = Data::Flags<ChannelDataFlags>::Change; - forum->channel()->flagsValue( - ) | rpl::start_with_next([=](FlagChange change) { - if (change.diff & ChannelDataFlag::ForumTabs) { - if (HistoryView::SubsectionTabs::UsedFor(history)) { - closeAndShowHistory(true); + if (!forced) { + using FlagChange = Data::Flags<ChannelDataFlags>::Change; + forum->channel()->flagsValue( + ) | rpl::start_with_next([=](FlagChange change) { + if (change.diff & ChannelDataFlag::ForumTabs) { + if (HistoryView::SubsectionTabs::UsedFor(history)) { + closeAndShowHistory(true); + } } - } - }, _shownForumLifetime); - content()->showForum(forum, params); + }, _shownForumLifetime); + } } void SessionController::closeForum() { diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index fdeef9a221..9af6714b60 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -171,6 +171,7 @@ struct SectionShow { bool thirdColumn = false; bool childColumn = false; bool forbidLayer = false; + bool forceTopicsList = false; bool reapplyLocalDraft = false; bool dropSameFromStack = false; Origin origin; From 9a622ab466d73b434a1782adfc2bab0cb0993bdd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 14:09:03 +0400 Subject: [PATCH 143/310] Add view channel button to monoforum info. --- .../info/profile/info_profile_actions.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 1e0cb0c857..076f9e262b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1044,6 +1044,9 @@ private: void addShowTopicsListButton( Ui::MultiSlideTracker &tracker, not_null<Data::Forum*> forum); + void addViewChannelButton( + Ui::MultiSlideTracker &tracker, + not_null<ChannelData*> channel); void addReportReaction(Ui::MultiSlideTracker &tracker); void addReportReaction( @@ -2182,9 +2185,16 @@ Ui::MultiSlideTracker DetailsFiller::fillUserButtons( Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( not_null<ChannelData*> channel) { + Ui::MultiSlideTracker tracker; + addViewChannelButton(tracker, channel); + return tracker; +} + +void DetailsFiller::addViewChannelButton( + Ui::MultiSlideTracker &tracker, + not_null<ChannelData*> channel) { using namespace rpl::mappers; - Ui::MultiSlideTracker tracker; auto window = _controller->parentController(); auto activePeerValue = window->activeChatValue( ) | rpl::map([](Dialogs::Key key) { @@ -2205,8 +2215,6 @@ Ui::MultiSlideTracker DetailsFiller::fillChannelButtons( std::move(viewChannelVisible), std::move(viewChannel), tracker); - - return tracker; } Ui::MultiSlideTracker DetailsFiller::fillDiscussionButtons( @@ -2238,6 +2246,8 @@ Ui::MultiSlideTracker DetailsFiller::fillDiscussionButtons( if (channel->useSubsectionTabs()) { addShowTopicsListButton(tracker, forum); } + } else if (const auto broadcast = channel->monoforumBroadcast()) { + addViewChannelButton(tracker, broadcast); } return tracker; From 73ea86ceeb3921d2525d23cb939327c93ddac911 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 14:23:47 +0400 Subject: [PATCH 144/310] Improve monoforum chat profiles. --- .../view/history_view_top_bar_widget.cpp | 4 +- .../info/profile/info_profile_actions.cpp | 44 ++++++++++++++++--- .../info/profile/info_profile_actions.h | 5 ++- .../profile/info_profile_inner_widget.cpp | 4 +- .../info/profile/info_profile_widget.cpp | 2 + .../stories/info_stories_inner_widget.cpp | 6 ++- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index e45537d964..fb20a34506 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -750,9 +750,7 @@ void TopBarWidget::infoClicked() { } else if (const auto topic = key.topic()) { _controller->showSection(std::make_shared<Info::Memento>(topic)); } else if (const auto sublist = key.sublist()) { - _controller->showSection(std::make_shared<Info::Memento>( - sublist, - Info::Section(Storage::SharedMediaType::Photo))); + _controller->showSection(std::make_shared<Info::Memento>(sublist)); } else if (key.peer()->savedSublistsInfo()) { _controller->showSection(std::make_shared<Info::Memento>( key.peer(), diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 076f9e262b..31a9468dfb 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_peer_values.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/notify/data_notify_settings.h" @@ -1019,6 +1020,10 @@ public: not_null<Ui::RpWidget*> parent, not_null<PeerData*> peer, Origin origin); + DetailsFiller( + not_null<Controller*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::SavedSublist*> sublist); DetailsFiller( not_null<Controller*> controller, not_null<Ui::RpWidget*> parent, @@ -1070,6 +1075,7 @@ private: not_null<Ui::RpWidget*> _parent; not_null<PeerData*> _peer; Data::ForumTopic *_topic = nullptr; + Data::SavedSublist *_sublist = nullptr; Origin _origin; object_ptr<Ui::VerticalLayout> _wrap; @@ -1169,6 +1175,17 @@ DetailsFiller::DetailsFiller( , _wrap(_parent) { } +DetailsFiller::DetailsFiller( + not_null<Controller*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::SavedSublist*> sublist) +: _controller(controller) +, _parent(parent) +, _peer(sublist->sublistPeer()) +, _sublist(sublist) +, _wrap(_parent) { +} + DetailsFiller::DetailsFiller( not_null<Controller*> controller, not_null<Ui::RpWidget*> parent, @@ -2178,7 +2195,9 @@ Ui::MultiSlideTracker DetailsFiller::fillUserButtons( if (!user->isVerifyCodes()) { addSendMessageButton(); } - addReportReaction(tracker); + if (!_sublist) { + addReportReaction(tracker); + } return tracker; } @@ -2261,7 +2280,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() { } else { add(object_ptr<Ui::BoxContentDivider>(_wrap)); } - if (const auto user = _peer->asUser()) { + if (const auto user = _sublist ? nullptr : _peer->asUser()) { add(setupPersonalChannel(user)); } add(CreateSkipWidget(_wrap)); @@ -2276,7 +2295,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::fill() { } } } - if (!_peer->isSelf()) { + if (!_sublist && !_peer->isSelf()) { add(setupMuteToggle()); } setupMainButtons(); @@ -2739,6 +2758,14 @@ object_ptr<Ui::RpWidget> SetupDetails( return filler.fill(); } +object_ptr<Ui::RpWidget> SetupDetails( + not_null<Controller*> controller, + not_null<Ui::RpWidget*> parent, + not_null<Data::SavedSublist*> sublist) { + DetailsFiller filler(controller, parent, sublist); + return filler.fill(); +} + object_ptr<Ui::RpWidget> SetupDetails( not_null<Controller*> controller, not_null<Ui::RpWidget*> parent, @@ -2988,7 +3015,9 @@ Cover *AddCover( not_null<Ui::VerticalLayout*> container, not_null<Controller*> controller, not_null<PeerData*> peer, - Data::ForumTopic *topic) { + Data::ForumTopic *topic, + Data::SavedSublist *sublist) { + const auto shown = sublist ? sublist->sublistPeer() : peer; const auto result = topic ? container->add(object_ptr<Cover>( container, @@ -2997,13 +3026,13 @@ Cover *AddCover( : container->add(object_ptr<Cover>( container, controller->parentController(), - peer, + shown, [=] { return controller->wrapWidget(); })); result->showSection( ) | rpl::start_with_next([=](Section section) { controller->showSection(topic ? std::make_shared<Info::Memento>(topic, section) - : std::make_shared<Info::Memento>(peer, section)); + : std::make_shared<Info::Memento>(shown, section)); }, result->lifetime()); result->setOnlineCount(rpl::single(0)); return result; @@ -3014,9 +3043,12 @@ void AddDetails( not_null<Controller*> controller, not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, Origin origin) { if (topic) { container->add(SetupDetails(controller, container, topic)); + } else if (sublist) { + container->add(SetupDetails(controller, container, sublist)); } else { container->add(SetupDetails(controller, container, peer, origin)); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.h b/Telegram/SourceFiles/info/profile/info_profile_actions.h index 584f2d7f86..70f5ef054f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.h +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.h @@ -16,6 +16,7 @@ class VerticalLayout; namespace Data { class ForumTopic; +class SavedSublist; } // namespace Data namespace Info { @@ -55,12 +56,14 @@ Cover *AddCover( not_null<Ui::VerticalLayout*> container, not_null<Controller*> controller, not_null<PeerData*> peer, - Data::ForumTopic *topic); + Data::ForumTopic *topic, + Data::SavedSublist *sublist); void AddDetails( not_null<Ui::VerticalLayout*> container, not_null<Controller*> controller, not_null<PeerData*> peer, Data::ForumTopic *topic, + Data::SavedSublist *sublist, Origin origin); } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 5c9b86ce47..5fe27a2eb7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -77,12 +77,12 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent( } auto result = object_ptr<Ui::VerticalLayout>(parent); - _cover = AddCover(result, _controller, _peer, _topic); + _cover = AddCover(result, _controller, _peer, _topic, _sublist); if (_topic && _topic->creating()) { return result; } - AddDetails(result, _controller, _peer, _topic, origin); + AddDetails(result, _controller, _peer, _topic, _sublist, origin); result->add(setupSharedMedia(result.data())); if (_topic || _sublist) { return result; diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp index cf235c55df..6d0c8435fe 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp @@ -110,6 +110,8 @@ void Widget::setInnerFocus() { rpl::producer<QString> Widget::title() { if (controller()->key().topic()) { return tr::lng_info_topic_title(); + } else if (controller()->key().sublist()) { + return tr::lng_info_user_title(); } const auto peer = controller()->key().peer(); if (const auto user = peer->asUser()) { diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index d0ab12cb50..4848fd3355 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -135,8 +135,10 @@ void InnerWidget::createProfileTop() { const auto peer = key.storiesPeer(); startTop(); - Profile::AddCover(_top, _controller, peer, nullptr); - Profile::AddDetails(_top, _controller, peer, nullptr, { v::null }); + + using namespace Profile; + AddCover(_top, _controller, peer, nullptr, nullptr); + AddDetails(_top, _controller, peer, nullptr, nullptr, { v::null }); auto tracker = Ui::MultiSlideTracker(); const auto dividerWrap = _top->add( From dc61faace15b804aa9b1f10c4bf825afdf6bc96c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 14:58:45 +0400 Subject: [PATCH 145/310] Handle disabling direct messages in channel. --- Telegram/SourceFiles/data/data_channel.cpp | 17 ++++++++- Telegram/SourceFiles/data/data_channel.h | 2 ++ .../data/data_chat_participant_status.cpp | 6 ++++ Telegram/SourceFiles/data/data_peer.cpp | 3 ++ .../SourceFiles/data/data_peer_values.cpp | 6 +++- .../dialogs/dialogs_inner_widget.cpp | 6 ++++ .../SourceFiles/history/history_widget.cpp | 35 ++++++++++++++----- Telegram/SourceFiles/history/history_widget.h | 1 + 8 files changed, 66 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 7a0f6a502a..02a94552ec 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -337,7 +337,18 @@ bool ChannelData::discussionLinkKnown() const { } void ChannelData::setMonoforumLink(ChannelData *link) { - if (_monoforumLink || !link) { + if (_monoforumLink) { + if (isBroadcast()) { + _monoforumLink->setMonoforumLink(link ? this : nullptr); + } else if (isMonoforum()) { + if (!link && !monoforumDisabled()) { + setFlags(flags() | Flag::MonoforumDisabled); + } else if (link && monoforumDisabled()) { + setFlags(flags() & ~Flag::MonoforumDisabled); + } + } + return; + } else if (!link) { return; } _monoforumLink = link; @@ -352,6 +363,10 @@ ChannelData *ChannelData::monoforumLink() const { return _monoforumLink; } +bool ChannelData::monoforumDisabled() const { + return flags() & Flag::MonoforumDisabled; +} + void ChannelData::setMembersCount(int newMembersCount) { if (_membersCount != newMembersCount) { if (isMegagroup() diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 776edb0aa5..15b031199b 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -81,6 +81,7 @@ enum class ChannelDataFlag : uint64 { AutoTranslation = (1ULL << 38), Monoforum = (1ULL << 39), MonoforumAdmin = (1ULL << 40), + MonoforumDisabled = (1ULL << 41), ForumTabs = (1ULL << 41), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; @@ -432,6 +433,7 @@ public: void setMonoforumLink(ChannelData *link); [[nodiscard]] ChannelData *monoforumLink() const; + [[nodiscard]] bool monoforumDisabled() const; void ptsInit(int32 pts) { _ptsWaiter.init(pts); diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index 2a85f44d13..de38444757 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -156,6 +156,9 @@ bool CanSendAnyOf( } return false; } else if (const auto channel = peer->asChannel()) { + if (channel->monoforumDisabled()) { + return false; + } using Flag = ChannelDataFlag; const auto allowed = channel->amIn() || ((channel->flags() & Flag::HasLink) @@ -221,6 +224,9 @@ SendError RestrictionError( } const auto all = restricted.isWithEveryone(); const auto channel = peer->asChannel(); + if (channel && channel->monoforumDisabled()) { + return tr::lng_action_direct_messages_disabled(tr::now); + } if (!all && channel) { auto restrictedUntil = channel->restrictedUntil(); if (restrictedUntil > 0 diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 86fb382f0e..46ec093b1e 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1542,6 +1542,9 @@ Data::RestrictionCheckResult PeerData::amRestricted( : Result::Explicit()) : Result::Allowed(); } else if (const auto channel = asChannel()) { + if (channel->monoforumDisabled()) { + return Result::WithEveryone(); + } const auto defaultRestrictions = channel->defaultRestrictions() | (channel->isPublic() ? (ChatRestriction::PinMessages diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 0c435d5347..5739124d45 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -273,7 +273,8 @@ inline auto DefaultRestrictionValue( | Flag::HasLink | Flag::Forbidden | Flag::Creator - | Flag::Broadcast; + | Flag::Broadcast + | Flag::MonoforumDisabled; return rpl::combine( PeerFlagsValue(channel, mask), AdminRightValue( @@ -288,6 +289,9 @@ inline auto DefaultRestrictionValue( bool unrestrictedByBoosts, ChatRestrictions sendRestriction, ChatRestrictions defaultSendRestriction) { + if (flags & Flag::MonoforumDisabled) { + return false; + } const auto notAmInFlags = Flag::Left | Flag::Forbidden; const auto forumRestriction = forbidInForums && (flags & Flag::Forum); diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 312c830d1e..801155646e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -527,7 +527,13 @@ InnerWidget::InnerWidget( RowDescriptor previous, RowDescriptor next) { updateDialogRow(previous); + if (const auto sublist = previous.key.sublist()) { + updateDialogRow({ { sublist->owningHistory() }, {} }); + } updateDialogRow(next); + if (const auto sublist = next.key.sublist()) { + updateDialogRow({ { sublist->owningHistory() }, {} }); + } }, lifetime()); _controller->activeChatsFilter( diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index b468697738..5aedac9535 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1067,9 +1067,20 @@ void HistoryWidget::refreshDirectMessageShown() { return; } const auto channel = _peer->asChannel(); - _directMessage->setVisible(channel - && channel->isBroadcast() - && channel->monoforumLink()); + const auto monoforum = channel ? channel->broadcastMonoforum() : nullptr; + const auto visible = monoforum && !monoforum->monoforumDisabled(); + _directMessage->setVisible(visible); + if (visible) { + using Flags = Data::Flags<ChannelDataFlags>; + _directMessageLifetime = monoforum->flagsValue( + ) | rpl::skip( + 1 + ) | rpl::start_with_next([=](Flags::Change change) { + if (change.diff & ChannelDataFlag::MonoforumDisabled) { + refreshDirectMessageShown(); + } + }); + } } void HistoryWidget::refreshTopBarActiveChat() { @@ -2624,13 +2635,19 @@ void HistoryWidget::showHistory( if (const auto channel = _peer->asChannel()) { channel->updateFull(); if (!channel->isBroadcast()) { - channel->flagsValue( - ) | rpl::start_with_next([=] { + using Flags = Data::Flags<ChannelDataFlags>; + channel->flagsValue() | rpl::skip( + 1 + ) | rpl::start_with_next([=](Flags::Change change) { refreshJoinChannelText(); + if (change.diff & ChannelDataFlag::MonoforumDisabled) { + updateCanSendMessage(); + updateSendRestriction(); + updateHistoryGeometry(); + } }, _list->lifetime()); - } else { - refreshJoinChannelText(); } + refreshJoinChannelText(); } controller()->adaptive().changes( @@ -6645,7 +6662,9 @@ int HistoryWidget::countAutomaticScrollTop() { } Data::SendError HistoryWidget::computeSendRestriction() const { - if (!_canSendMessages && _peer->amMonoforumAdmin()) { + if (!_canSendMessages + && _peer->amMonoforumAdmin() + && !_peer->asChannel()->monoforumDisabled()) { return Data::SendError({ .text = tr::lng_monoforum_choose_to_reply(tr::now), .monoforumAdmin = true, diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 3c7cc4e82e..42f2cf7f8c 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -807,6 +807,7 @@ private: object_ptr<Ui::FlatButton> _muteUnmute; QPointer<Ui::IconButton> _giftToChannel; QPointer<Ui::IconButton> _directMessage; + rpl::lifetime _directMessageLifetime; object_ptr<Ui::FlatButton> _reportMessages; struct { object_ptr<Ui::RoundButton> button = { nullptr }; From 03c24e290660bec0e077ed8f3f9fd96e591a2969 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 15:18:18 +0400 Subject: [PATCH 146/310] Show better monoforum chat info column. --- .../history/view/history_view_top_bar_widget.cpp | 7 +++++++ .../SourceFiles/info/profile/info_profile_widget.cpp | 9 ++++++--- Telegram/SourceFiles/mainwidget.cpp | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index fb20a34506..a04c003cec 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -394,6 +394,10 @@ void TopBarWidget::toggleInfoSection() { (_activeChat.key.topic() ? std::make_shared<Info::Memento>( _activeChat.key.topic()) + : (_activeChat.key.sublist() + && _activeChat.key.sublist()->parentChat()) + ? std::make_shared<Info::Memento>( + _activeChat.key.sublist()) : Info::Memento::Default(_activeChat.key.peer())), Window::SectionShow().withThirdColumn()); } else { @@ -1170,6 +1174,9 @@ void TopBarWidget::updateControlsVisibility() { ? true : (section == Section::Replies) ? (_activeChat.key.topic() != nullptr) + : (section == Section::SavedSublist) + ? (_activeChat.key.sublist() != nullptr + && _activeChat.key.sublist()->parentChat()) : false); updateSearchVisibility(); if (_searchMode) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp index 6d0c8435fe..a11face226 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_widget.cpp @@ -110,8 +110,9 @@ void Widget::setInnerFocus() { rpl::producer<QString> Widget::title() { if (controller()->key().topic()) { return tr::lng_info_topic_title(); - } else if (controller()->key().sublist()) { - return tr::lng_info_user_title(); + } else if (controller()->key().sublist() + && controller()->key().sublist()->parentChat()) { + return tr::lng_profile_direct_messages(); } const auto peer = controller()->key().peer(); if (const auto user = peer->asUser()) { @@ -119,7 +120,9 @@ rpl::producer<QString> Widget::title() { ? tr::lng_info_bot_title() : tr::lng_info_user_title(); } else if (const auto channel = peer->asChannel()) { - return channel->isMegagroup() + return channel->isMonoforum() + ? tr::lng_profile_direct_messages() + : channel->isMegagroup() ? tr::lng_info_group_title() : tr::lng_info_channel_title(); } else if (peer->isChat()) { diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 65001c5cdb..a26693f680 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -2370,6 +2370,9 @@ void MainWidget::updateControlsGeometry() { (thread->asTopic() ? std::make_shared<Info::Memento>( thread->asTopic()) + : thread->asSublist() + ? std::make_shared<Info::Memento>( + thread->asSublist()) : Info::Memento::Default( thread->asHistory()->peer)), params.withThirdColumn()); @@ -2633,6 +2636,9 @@ auto MainWidget::thirdSectionForCurrentMainSection( return std::move(_thirdSectionFromStack); } else if (const auto topic = key.topic()) { return std::make_shared<Info::Memento>(topic); + } else if (const auto sublist = key.sublist() + ; sublist && sublist->parentChat()) { + return std::make_shared<Info::Memento>(sublist); } else if (const auto peer = key.peer()) { return std::make_shared<Info::Memento>( peer, From bfb4652425f1a5240ae514957717ac9661858771 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 16:09:41 +0400 Subject: [PATCH 147/310] Realtime update admin status in members list. --- .../boxes/peers/add_participants_box.cpp | 1 + .../boxes/peers/edit_participants_box.cpp | 50 ++++++++++++------- Telegram/SourceFiles/data/data_changes.cpp | 17 +++++++ Telegram/SourceFiles/data/data_changes.h | 16 ++++++ .../info_profile_members_controllers.cpp | 4 ++ .../info_profile_members_controllers.h | 1 + 6 files changed, 72 insertions(+), 17 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 25853e0f85..ab05d9bccb 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -1522,6 +1522,7 @@ void AddSpecialBoxController::editAdminDone( } _additional.applyAdminLocally(user, rights, rank); + // _adminDoneCallback should call changes().chatAdminUpdated. if (const auto callback = _adminDoneCallback) { callback(user, rights, rank); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index e168e800e0..ea60f38333 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -461,6 +461,7 @@ void ParticipantsAdditionalData::setExternal( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); _admins.erase(user); } _restrictedRights.erase(participant); @@ -538,6 +539,7 @@ void ParticipantsAdditionalData::fillFromChannel( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); _restrictedRights.emplace(user, restricted->second.rights); } } @@ -743,6 +745,7 @@ UserData *ParticipantsAdditionalData::applyRegular(UserId userId) { _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); _restrictedRights.erase(user); _kicked.erase(user); _restrictedBy.erase(user); @@ -761,6 +764,7 @@ PeerData *ParticipantsAdditionalData::applyBanned( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); + _adminRanks.erase(user); } if (data.isKicked()) { _kicked.emplace(participant); @@ -1270,6 +1274,33 @@ void ParticipantsBoxController::prepare() { } else { rebuild(); } + + _peer->session().changes().chatAdminChanges( + ) | rpl::start_with_next([=](const Data::ChatAdminChange &update) { + if (update.peer != _peer) { + return; + } + const auto user = update.user; + const auto rights = ChatAdminRightsInfo(update.rights); + const auto rank = update.rank; + _additional.applyAdminLocally(user, rights, rank); + if (!_additional.isCreator(user) || !user->isSelf()) { + if (!rights.flags) { + if (_role == Role::Admins) { + removeRow(user); + } + } else { + if (_role == Role::Admins) { + prependRow(user); + } else if (_role == Role::Kicked + || _role == Role::Restricted) { + removeRow(user); + } + } + } + recomputeTypeFor(user); + refreshRows(); + }, lifetime()); } void ParticipantsBoxController::unload() { @@ -1800,23 +1831,8 @@ void ParticipantsBoxController::editAdminDone( if (_editParticipantBox) { _editParticipantBox->closeBox(); } - - _additional.applyAdminLocally(user, rights, rank); - if (!_additional.isCreator(user) || !user->isSelf()) { - if (!rights.flags) { - if (_role == Role::Admins) { - removeRow(user); - } - } else { - if (_role == Role::Admins) { - prependRow(user); - } else if (_role == Role::Kicked || _role == Role::Restricted) { - removeRow(user); - } - } - } - recomputeTypeFor(user); - refreshRows(); + const auto flags = rights.flags; + user->session().changes().chatAdminChanged(_peer, user, flags, rank); } void ParticipantsBoxController::showRestricted(not_null<UserData*> user) { diff --git a/Telegram/SourceFiles/data/data_changes.cpp b/Telegram/SourceFiles/data/data_changes.cpp index 6f2a554da9..5356d48065 100644 --- a/Telegram/SourceFiles/data/data_changes.cpp +++ b/Telegram/SourceFiles/data/data_changes.cpp @@ -340,6 +340,23 @@ rpl::producer<StoryUpdate> Changes::realtimeStoryUpdates( return _storyChanges.realtimeUpdates(flag); } +void Changes::chatAdminChanged( + not_null<PeerData*> peer, + not_null<UserData*> user, + ChatAdminRights rights, + QString rank) { + _chatAdminChanges.fire({ + .peer = peer, + .user = user, + .rights = rights, + .rank = std::move(rank), + }); +} + +rpl::producer<ChatAdminChange> Changes::chatAdminChanges() const { + return _chatAdminChanges.events(); +} + void Changes::scheduleNotifications() { if (!_notify) { _notify = true; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index f3215d2599..190f3f554c 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/flags.h" +#include "data/data_chat_participant_status.h" class History; class PeerData; @@ -271,6 +272,13 @@ struct StoryUpdate { }; +struct ChatAdminChange { + not_null<PeerData*> peer; + not_null<UserData*> user; + ChatAdminRights rights; + QString rank; +}; + class Changes final { public: explicit Changes(not_null<Main::Session*> session); @@ -383,6 +391,13 @@ public: [[nodiscard]] rpl::producer<StoryUpdate> realtimeStoryUpdates( StoryUpdate::Flag flag) const; + void chatAdminChanged( + not_null<PeerData*> peer, + not_null<UserData*> user, + ChatAdminRights rights, + QString rank); + [[nodiscard]] rpl::producer<ChatAdminChange> chatAdminChanges() const; + void sendNotifications(); private: @@ -435,6 +450,7 @@ private: Manager<HistoryItem, MessageUpdate> _messageChanges; Manager<Dialogs::Entry, EntryUpdate> _entryChanges; Manager<Story, StoryUpdate> _storyChanges; + rpl::event_stream<ChatAdminChange> _chatAdminChanges; bool _notify = false; diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp index 1987be5d49..619a5206a8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp @@ -39,6 +39,10 @@ void MemberListRow::setType(Type type) { : QString()); } +MemberListRow::Type MemberListRow::type() const { + return _type; +} + bool MemberListRow::rightActionDisabled() const { return true; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h index 37d04cb7c6..c867d731d5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h @@ -34,6 +34,7 @@ public: MemberListRow(not_null<UserData*> user, Type type); void setType(Type type); + [[nodiscard]] Type type() const; bool rightActionDisabled() const override; QMargins rightActionMargins() const override; void refreshStatus() override; From a3308087a56fff1bfc47c14e40c72d1be149b429 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Thu, 5 Jun 2025 11:15:52 +0000 Subject: [PATCH 148/310] Fix static libstdc++ link --- Telegram/build/docker/centos_env/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index a2b219c59e..fd03909ed9 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -302,6 +302,7 @@ RUN git clone -b v0.11.1 --depth=1 https://github.com/libjxl/libjxl.git \ && cmake --build build \ && export DESTDIR=/usr/src/jxl-cache \ && cmake --install build \ + && sed -i 's/-lstdc++//' $DESTDIR/usr/local/lib64/pkgconfig/libjxl*.pc \ && rm $DESTDIR/usr/local/lib64/libjpeg.so* \ && cp build/lib/libjpegli-static.a $DESTDIR/usr/local/lib64/libjpeg.a \ && mkdir build/hwy \ From 3667ef551ccff1da8313d905d9e7624068b0c105 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 16:25:38 +0400 Subject: [PATCH 149/310] Version 5.15.1. - Fix launch on Windows 7. - Fix launch on older Linux distributions. - Fix crash in group chat message right click. - Fix unread counters in channel direct messages. - Don't generate "User joined" message in channel direct messages. - Fix some other glitches in new forums and channel direct messages. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 4 ++-- Telegram/build/version | 8 ++++---- changelog.txt | 9 +++++++++ cmake | 2 +- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index f72376f831..b08ff9cfed 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="5.15.0.0" /> + Version="5.15.1.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index e119d657b5..b520bb44f4 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 5,15,0,0 - PRODUCTVERSION 5,15,0,0 + FILEVERSION 5,15,1,0 + PRODUCTVERSION 5,15,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", "5.15.0.0" + VALUE "FileVersion", "5.15.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.0.0" + VALUE "ProductVersion", "5.15.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index f2d7d8b38e..1d35fc1a97 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 5,15,0,0 - PRODUCTVERSION 5,15,0,0 + FILEVERSION 5,15,1,0 + PRODUCTVERSION 5,15,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", "5.15.0.0" + VALUE "FileVersion", "5.15.1.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.0.0" + VALUE "ProductVersion", "5.15.1.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index f52d1ee876..34b631a09f 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5015000; -constexpr auto AppVersionStr = "5.15"; +constexpr auto AppVersion = 5015001; +constexpr auto AppVersionStr = "5.15.1"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/build/version b/Telegram/build/version index 2ffce92c2c..9ad57c543e 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5015000 +AppVersion 5015001 AppVersionStrMajor 5.15 -AppVersionStrSmall 5.15 -AppVersionStr 5.15.0 +AppVersionStrSmall 5.15.1 +AppVersionStr 5.15.1 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.15 +AppVersionOriginal 5.15.1 diff --git a/changelog.txt b/changelog.txt index 3e4dbdc0f1..403983a166 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,12 @@ +5.15.1 (05.06.25) + +- Fix launch on Windows 7. +- Fix launch on older Linux distributions. +- Fix crash in group chat message right click. +- Fix unread counters in channel direct messages. +- Don't generate "User joined" message in channel direct messages. +- Fix some other glitches in new forums and channel direct messages. + 5.15 (04.06.25) - Send Direct Messages to Channels. diff --git a/cmake b/cmake index 2130c73ccc..c0608b65b6 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 2130c73cccfc1acc79675b838c2e211699199858 +Subproject commit c0608b65b60d52dabbd78ff0752bb9e317c55251 From 759258bb395f4eaa13e90eb2c70037fe0ffb1d11 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 5 Jun 2025 16:49:56 +0300 Subject: [PATCH 150/310] Added support of statistics availability to Credits component. --- .../SourceFiles/data/components/credits.cpp | 25 ++++++++++++++++--- .../SourceFiles/data/components/credits.h | 10 ++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 5ae899d77e..119847e0fc 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -47,10 +47,23 @@ void Credits::load(bool force) { && _lastLoaded + kReloadThreshold > crl::now())) { return; } - _loader = std::make_unique<Api::CreditsStatus>(_session->user()); - _loader->request({}, [=](Data::CreditsStatusSlice slice) { - _loader = nullptr; - apply(slice.balance); + const auto self = _session->user(); + _loader = std::make_unique<rpl::lifetime>(); + _loader->make_state<Api::CreditsStatus>(self)->request({}, [=]( + Data::CreditsStatusSlice slice) { + const auto balance = slice.balance; + const auto apiStats + = _loader->make_state<Api::CreditsEarnStatistics>(self); + const auto finish = [=](bool statsEnabled) { + _statsEnabled = statsEnabled; + apply(balance); + _loader = nullptr; + }; + apiStats->request() | rpl::start_with_error_done([=] { + finish(false); + }, [=] { + finish(true); + }, *_loader); }); } @@ -148,4 +161,8 @@ rpl::producer<> Credits::refreshedByPeerId(PeerId peerId) { ) | rpl::filter(rpl::mappers::_1 == peerId) | rpl::to_empty; } +bool Credits::statsEnabled() const { + return _statsEnabled; +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h index e2d719091c..dac6df8981 100644 --- a/Telegram/SourceFiles/data/components/credits.h +++ b/Telegram/SourceFiles/data/components/credits.h @@ -7,10 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -namespace Api { -class CreditsStatus; -} // namespace Api - namespace Main { class Session; } // namespace Main @@ -39,6 +35,8 @@ public: [[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId); + [[nodiscard]] bool statsEnabled() const; + void applyCurrency(PeerId peerId, uint64 balance); [[nodiscard]] uint64 balanceCurrency(PeerId peerId) const; @@ -54,7 +52,7 @@ private: const not_null<Main::Session*> _session; - std::unique_ptr<Api::CreditsStatus> _loader; + std::unique_ptr<rpl::lifetime> _loader; base::flat_map<PeerId, StarsAmount> _cachedPeerBalances; base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances; @@ -66,6 +64,8 @@ private: crl::time _lastLoaded = 0; float64 _rate = 0.; + bool _statsEnabled = false; + rpl::event_stream<PeerId> _refreshedByPeerId; SingleQueuedInvokation _reload; From 067dcbfbebe4960f0858b0a49692779da4493772 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 5 Jun 2025 17:14:28 +0300 Subject: [PATCH 151/310] Added initial entry point for self statistics of credits. --- Telegram/Resources/langs/lang.strings | 3 + .../info/bot/earn/info_bot_earn_list.cpp | 30 +++--- Telegram/SourceFiles/settings/settings.style | 9 ++ .../SourceFiles/settings/settings_common.cpp | 6 +- .../SourceFiles/settings/settings_credits.cpp | 93 ++++++++++++++----- .../settings/settings_credits_graphics.cpp | 24 ++--- 6 files changed, 116 insertions(+), 49 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 86ddc9cc3e..d1a9784970 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2780,6 +2780,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_more_options" = "More Options"; "lng_credits_balance_me" = "your balance"; "lng_credits_buy_button" = "Buy More Stars"; +"lng_credits_buy_button_short" = "Top Up"; +"lng_credits_stats_button_short" = "Stats"; "lng_credits_gift_button" = "Gift Stars to Friends"; "lng_credits_box_out_title" = "Confirm Your Purchase"; "lng_credits_box_out_sure#one" = "Do you want to buy **\"{text}\"** in **{bot}** for **{count} Star**?"; @@ -6431,6 +6433,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bot_earn_balance_button_locked" = "Withdraw"; "lng_bot_earn_balance_button_buy_ads" = "Buy Ads"; "lng_bot_earn_learn_credits_out_about" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. {link}"; +"lng_self_earn_learn_credits_out_about" = "You can withdraw from 10 Stars using Fragment. {link}"; "lng_bot_earn_out_ph" = "Enter amount to withdraw"; "lng_bot_earn_balance_password_title" = "Two-step verification"; "lng_bot_earn_balance_password_description" = "Please enter your password to collect."; diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp index c8674009f0..ee71143124 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -128,6 +128,13 @@ void InnerWidget::fill() { return _state.availableBalance; }) ); + auto overallBalanceValue = rpl::single( + data.overallRevenue + ) | rpl::then( + _stateUpdated.events() | rpl::map([=] { + return _state.overallRevenue; + }) + ); auto valueToString = [](StarsAmount v) { return Lang::FormatStarsAmountDecimal(v); }; @@ -211,13 +218,7 @@ void InnerWidget::fill() { Ui::AddSkip(container); Ui::AddSkip(container); addOverview( - rpl::single( - data.overallRevenue - ) | rpl::then( - _stateUpdated.events() | rpl::map([=] { - return _state.overallRevenue; - }) - ), + rpl::duplicate(overallBalanceValue), tr::lng_bot_earn_total); Ui::AddSkip(container); Ui::AddSkip(container); @@ -245,17 +246,20 @@ void InnerWidget::fill() { return _state.buyAdsUrl; }) ), - rpl::duplicate(availableBalanceValue), + peer()->isSelf() + ? rpl::duplicate(overallBalanceValue) + : rpl::duplicate(availableBalanceValue), rpl::duplicate(dateValue), _state.isWithdrawalEnabled, - rpl::duplicate( - availableBalanceValue + (peer()->isSelf() + ? rpl::duplicate(overallBalanceValue) + : rpl::duplicate(availableBalanceValue) ) | rpl::map([=](StarsAmount v) { return v ? ToUsd(v, multiplier, kMinorLength) : QString(); })); container->resizeToWidth(container->width()); } - if (BotStarRef::Join::Allowed(peer())) { + if (BotStarRef::Join::Allowed(peer()) && !peer()->isSelf()) { const auto button = BotStarRef::AddViewListButton( container, tr::lng_credits_summary_earn_title(), @@ -267,7 +271,9 @@ void InnerWidget::fill() { Ui::AddSkip(container); Ui::AddDivider(container); } - fillHistory(); + if (!peer()->isSelf()) { + fillHistory(); + } } void InnerWidget::fillHistory() { diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index f28f04e777..6da9e1117f 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -696,3 +696,12 @@ settingsGiftIconEmoji: IconEmoji { icon: icon{{ "settings/mini_gift", windowFg }}; padding: margins(1px, 2px, 1px, 0px); } + +settingsCreditsButtonBuy: RoundButton(inviteLinkCopy) { + icon: icon {{ "settings/add", activeButtonFg, point(0px, 7px) }}; + iconOver: icon {{ "settings/add", activeButtonFgOver, point(0px, 7px) }}; +} +settingsCreditsButtonStats: RoundButton(inviteLinkCopy) { + icon: icon {{ "info/edit/links_share", activeButtonFg }}; + iconOver: icon {{ "info/edit/links_share", activeButtonFgOver }}; +} diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index 68bceccf38..d7f6139220 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -123,7 +123,11 @@ not_null<Button*> AddButtonWithIcon( const style::SettingsButton &st, IconDescriptor &&descriptor) { return container->add( - CreateButtonWithIcon(container, std::move(text), st, std::move(descriptor))); + CreateButtonWithIcon( + container, + std::move(text), + st, + std::move(descriptor))); } void CreateRightLabel( diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 0e132d82f3..9fd0ed483f 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_user.h" +#include "info/bot/earn/info_bot_earn_widget.h" #include "info/bot/starref/info_bot_starref_common.h" #include "info/bot/starref/info_bot_starref_join_widget.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. @@ -433,32 +434,74 @@ void Credits::setupContent() { }; const auto state = content->lifetime().make_state<State>(); - const auto button = content->add( - object_ptr<Ui::RoundButton>( - content, - rpl::conditional( - state->buyStars.loadingValue(), - rpl::single(QString()), - tr::lng_credits_buy_button()), - st::creditsSettingsBigBalanceButton), - st::boxRowPadding); - button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); - const auto show = _controller->uiShow(); - button->setClickedCallback(state->buyStars.handler(show, paid)); - { - using namespace Info::Statistics; - const auto loadingAnimation = InfiniteRadialAnimationWidget( - button, - button->height() / 2); - AddChildToWidgetCenter(button, loadingAnimation); - loadingAnimation->showOn(state->buyStars.loadingValue()); - } const auto paddings = rect::m::sum::h(st::boxRowPadding); - button->widthValue() | rpl::filter([=] { - return (button->widthNoMargins() != (content->width() - paddings)); - }) | rpl::start_with_next([=] { - button->resizeToWidth(content->width() - paddings); - }, button->lifetime()); + if (!_controller->session().credits().statsEnabled()) { + const auto button = content->add( + object_ptr<Ui::RoundButton>( + content, + rpl::conditional( + state->buyStars.loadingValue(), + rpl::single(QString()), + tr::lng_credits_buy_button()), + st::creditsSettingsBigBalanceButton), + st::boxRowPadding); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + const auto show = _controller->uiShow(); + button->setClickedCallback(state->buyStars.handler(show, paid)); + { + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + button, + button->height() / 2); + AddChildToWidgetCenter(button, loadingAnimation); + loadingAnimation->showOn(state->buyStars.loadingValue()); + } + button->widthValue() | rpl::filter([=] { + return button->widthNoMargins() != (content->width() - paddings); + }) | rpl::start_with_next([=] { + button->resizeToWidth(content->width() - paddings); + }, button->lifetime()); + } else { + const auto wrap = content->add( + object_ptr<Ui::FixedHeightWidget>( + content, + st::inviteLinkButton.height), + st::boxRowPadding); + const auto buy = Ui::CreateChild<Ui::RoundButton>( + wrap, + tr::lng_credits_buy_button_short(), + st::settingsCreditsButtonBuy); + buy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + const auto show = _controller->uiShow(); + buy->setClickedCallback(state->buyStars.handler(show, paid)); + { + using namespace Info::Statistics; + const auto loadingAnimation = InfiniteRadialAnimationWidget( + buy, + buy->height() / 2); + AddChildToWidgetCenter(buy, loadingAnimation); + loadingAnimation->showOn(state->buyStars.loadingValue()); + } + const auto stats = Ui::CreateChild<Ui::RoundButton>( + wrap, + tr::lng_credits_stats_button_short(), + st::settingsCreditsButtonStats); + stats->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + const auto self = _controller->session().user(); + const auto controller = _controller->parentController(); + stats->setClickedCallback([=] { + controller->showSection(Info::BotEarn::Make(self)); + }); + + wrap->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2; + buy->setFullWidth(buttonWidth); + stats->setFullWidth(buttonWidth); + buy->moveToLeft(0, 0, width); + stats->moveToRight(0, 0, width); + }, wrap->lifetime()); + } Ui::AddSkip(content); diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index dcbe5c3705..d14773c7d3 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -2789,17 +2789,19 @@ void AddWithdrawalWidget( const auto arrow = Ui::Text::IconEmoji(&st::textMoreIconEmoji); auto about = Ui::CreateLabelWithCustomEmoji( container, - tr::lng_bot_earn_learn_credits_out_about( - lt_link, - tr::lng_channel_earn_about_link( - lt_emoji, - rpl::single(arrow), - Ui::Text::RichLangValue - ) | rpl::map([](TextWithEntities text) { - return Ui::Text::Link( - std::move(text), - tr::lng_bot_earn_balance_about_url(tr::now)); - }), + (peer->isSelf() + ? tr::lng_self_earn_learn_credits_out_about + : tr::lng_bot_earn_learn_credits_out_about)( + lt_link, + tr::lng_channel_earn_about_link( + lt_emoji, + rpl::single(arrow), + Ui::Text::RichLangValue + ) | rpl::map([](TextWithEntities text) { + return Ui::Text::Link( + std::move(text), + tr::lng_bot_earn_balance_about_url(tr::now)); + }), Ui::Text::RichLangValue), Core::TextContext({ .session = session }), st::boxDividerLabel); From 553cc0c6ae5d42c356bfdcd42a58ca8f533b0efd Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 21:25:40 +0400 Subject: [PATCH 152/310] Fix new forum messages sending. --- Telegram/SourceFiles/data/data_channel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 15b031199b..214ac56b86 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -82,7 +82,7 @@ enum class ChannelDataFlag : uint64 { Monoforum = (1ULL << 39), MonoforumAdmin = (1ULL << 40), MonoforumDisabled = (1ULL << 41), - ForumTabs = (1ULL << 41), + ForumTabs = (1ULL << 42), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags<ChannelDataFlag>; From dcbda7b3afea36ed765a9e19ae6ac571436a1d53 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 5 Jun 2025 21:29:21 +0400 Subject: [PATCH 153/310] Version 5.15.2. - Fix sending messages in new forum layout. - Add statistics for user stars. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 4 ++-- Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp | 4 ++-- Telegram/build/version | 8 ++++---- changelog.txt | 5 +++++ 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index b08ff9cfed..d49fd63a41 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="5.15.1.0" /> + Version="5.15.2.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index b520bb44f4..a8844cf0e3 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 5,15,1,0 - PRODUCTVERSION 5,15,1,0 + FILEVERSION 5,15,2,0 + PRODUCTVERSION 5,15,2,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "5.15.1.0" + VALUE "FileVersion", "5.15.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.1.0" + VALUE "ProductVersion", "5.15.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 1d35fc1a97..57e38d9980 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 5,15,1,0 - PRODUCTVERSION 5,15,1,0 + FILEVERSION 5,15,2,0 + PRODUCTVERSION 5,15,2,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", "5.15.1.0" + VALUE "FileVersion", "5.15.2.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.1.0" + VALUE "ProductVersion", "5.15.2.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 34b631a09f..b1b92ca664 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5015001; -constexpr auto AppVersionStr = "5.15.1"; +constexpr auto AppVersion = 5015002; +constexpr auto AppVersionStr = "5.15.2"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp index ee71143124..300a067033 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -247,12 +247,12 @@ void InnerWidget::fill() { }) ), peer()->isSelf() - ? rpl::duplicate(overallBalanceValue) + ? rpl::duplicate(overallBalanceValue) | rpl::type_erased() : rpl::duplicate(availableBalanceValue), rpl::duplicate(dateValue), _state.isWithdrawalEnabled, (peer()->isSelf() - ? rpl::duplicate(overallBalanceValue) + ? rpl::duplicate(overallBalanceValue) | rpl::type_erased() : rpl::duplicate(availableBalanceValue) ) | rpl::map([=](StarsAmount v) { return v ? ToUsd(v, multiplier, kMinorLength) : QString(); diff --git a/Telegram/build/version b/Telegram/build/version index 9ad57c543e..b0ed63ee1a 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5015001 +AppVersion 5015002 AppVersionStrMajor 5.15 -AppVersionStrSmall 5.15.1 -AppVersionStr 5.15.1 +AppVersionStrSmall 5.15.2 +AppVersionStr 5.15.2 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.15.1 +AppVersionOriginal 5.15.2 diff --git a/changelog.txt b/changelog.txt index 403983a166..94044fea23 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +5.15.2 (05.06.25) + +- Fix sending messages in new forum layout. +- Add statistics for user stars. + 5.15.1 (05.06.25) - Fix launch on Windows 7. From af58ffadcb07d84e6099274e20a6f1ec46801912 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Wed, 4 Jun 2025 13:51:17 +0000 Subject: [PATCH 154/310] Use cache action for Docker layers cache --- .github/workflows/linux.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index f52621cafb..ae23948629 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -80,14 +80,26 @@ jobs: - name: Set up Docker Buildx. uses: docker/setup-buildx-action@v3 + - name: Libraries cache. + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/.buildx-cache + key: ${{ runner.OS }}-libs-${{ hashFiles('Telegram/build/docker/centos_env/**') }} + restore-keys: ${{ runner.OS }}-libs- + - name: Libraries. uses: docker/build-push-action@v6 with: context: Telegram/build/docker/centos_env load: ${{ env.ONLY_CACHE == 'false' }} tags: ${{ env.IMAGE_TAG }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=local,src=${{ runner.temp }}/.buildx-cache + cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache-new,mode=max + + - name: Move cache. + run: | + rm -rf ${{ runner.temp }}/.buildx-cache + mv ${{ runner.temp }}/.buildx-cache{-new,} - name: Telegram Desktop build. if: env.ONLY_CACHE == 'false' From 49b056a0ce5597dba7ba90e94461e60766f1324b Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 6 Jun 2025 06:34:47 +0000 Subject: [PATCH 155/310] Update xcb libraries to avoid freedesktop's anongit --- Telegram/build/docker/centos_env/Dockerfile | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index fd03909ed9..cfaee54100 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -353,7 +353,7 @@ RUN git clone -b xcb-util-wm-0.4.2 --depth=1 --recursive --shallow-submodules ht && rm -rf libxcb-wm FROM builder AS xcb-util -RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-util.git \ +RUN git clone -b xcb-util-0.4.1-gitlab --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-util.git \ && cd libxcb-util \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ @@ -364,7 +364,7 @@ RUN git clone -b xcb-util-0.4.1 --depth=1 --recursive --shallow-submodules https FROM builder AS xcb-image COPY --link --from=xcb-util /usr/src/xcb-util-cache / -RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \ +RUN git clone -b xcb-util-image-0.4.1-gitlab --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-image.git \ && cd libxcb-image \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ @@ -373,8 +373,12 @@ RUN git clone -b xcb-util-image-0.4.1 --depth=1 --recursive --shallow-submodules && rm -rf libxcb-image FROM builder AS xcb-keysyms -RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ +RUN git init libxcb-keysyms \ && cd libxcb-keysyms \ + && git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-keysyms.git \ + && git fetch --depth=1 origin ef5cb393d27511ba511c68a54f8ff7b9aab4a384 \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init --recursive --depth=1 \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR=/usr/src/xcb-keysyms-cache install \ @@ -382,8 +386,12 @@ RUN git clone -b xcb-util-keysyms-0.4.1 --depth=1 --recursive --shallow-submodul && rm -rf libxcb-keysyms FROM builder AS xcb-render-util -RUN git clone -b xcb-util-renderutil-0.3.10 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-render-util.git \ +RUN git init libxcb-render-util \ && cd libxcb-render-util \ + && git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-render-util.git \ + && git fetch --depth=1 origin 5ad9853d6ddcac394d42dd2d4e34436b5db9da39 \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init --recursive --depth=1 \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR=/usr/src/xcb-render-util-cache install \ @@ -395,8 +403,12 @@ COPY --link --from=xcb-util /usr/src/xcb-util-cache / COPY --link --from=xcb-image /usr/src/xcb-image-cache / COPY --link --from=xcb-render-util /usr/src/xcb-render-util-cache / -RUN git clone -b xcb-util-cursor-0.1.4 --depth=1 --recursive --shallow-submodules https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \ +RUN git init libxcb-cursor \ && cd libxcb-cursor \ + && git remote add origin https://github.com/gitlab-freedesktop-mirrors/libxcb-cursor.git \ + && git fetch --depth=1 origin 4929f6051658ba5424b41703a1fb63f9db896065 \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init --recursive --depth=1 \ && ./autogen.sh --enable-static --disable-shared \ && make -j$(nproc) \ && make DESTDIR=/usr/src/xcb-cursor-cache install \ From 307a7791df3e908e11b6d506c7f65b354f1f3676 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 6 Jun 2025 06:36:05 +0000 Subject: [PATCH 156/310] Set Implib commit --- Telegram/build/docker/centos_env/Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index cfaee54100..8ef6e92633 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -50,9 +50,13 @@ ENV CMAKE_GENERATOR=Ninja ENV CMAKE_BUILD_TYPE=None ENV CMAKE_BUILD_PARALLEL_LEVEL= -RUN git clone --depth=1 https://github.com/yugr/Implib.so.git \ - && mkdir Implib.so/build \ - && cd Implib.so/build \ +RUN git init Implib.so \ + && cd Implib.so \ + && git remote add origin https://github.com/yugr/Implib.so.git \ + && git fetch --depth=1 origin ecf7bb51a92a0fb16834c5b698570ab25f9f1d21 \ + && git reset --hard FETCH_HEAD \ + && mkdir build \ + && cd build \ && implib() { \ LIBFILE=$(basename $1); \ LIBNAME=$(basename $1 .so); \ From 0c635a05ff8f32eafd0f9620a398def3557479a1 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 6 Jun 2025 06:45:43 +0000 Subject: [PATCH 157/310] Allow overriding jobs count in Dockerfile --- Telegram/build/docker/centos_env/Dockerfile | 2 +- Telegram/build/docker/centos_env/gen_dockerfile.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 8ef6e92633..2f013df80d 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -48,7 +48,7 @@ ENV LDFLAGS='{% if not LTO %}-fuse-ld=lld{% endif %} -static-libstdc++ -static-l ENV CMAKE_GENERATOR=Ninja ENV CMAKE_BUILD_TYPE=None -ENV CMAKE_BUILD_PARALLEL_LEVEL= +ENV CMAKE_BUILD_PARALLEL_LEVEL='{{ JOBS }}' RUN git init Implib.so \ && cd Implib.so \ diff --git a/Telegram/build/docker/centos_env/gen_dockerfile.py b/Telegram/build/docker/centos_env/gen_dockerfile.py index 997b784caf..96269eba6e 100755 --- a/Telegram/build/docker/centos_env/gen_dockerfile.py +++ b/Telegram/build/docker/centos_env/gen_dockerfile.py @@ -4,12 +4,15 @@ from os.path import dirname from jinja2 import Environment, FileSystemLoader def checkEnv(envName, defaultValue): - return bool(len(environ[envName])) if envName in environ else defaultValue + if isinstance(defaultValue, bool): + return bool(len(environ[envName])) if envName in environ else defaultValue + return environ[envName] if envName in environ else defaultValue def main(): print(Environment(loader=FileSystemLoader(dirname(__file__))).get_template("Dockerfile").render( DEBUG=checkEnv("DEBUG", True), LTO=checkEnv("LTO", True), + JOBS=checkEnv("JOBS", ""), )) if __name__ == '__main__': From 6102119673b4d555605d088a26d8bbc8f4c988a8 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Fri, 6 Jun 2025 10:43:02 +0400 Subject: [PATCH 158/310] Update cmake_helpers --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index c0608b65b6..3fa88ebd4a 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit c0608b65b60d52dabbd78ff0752bb9e317c55251 +Subproject commit 3fa88ebd4a7e66cc8fbedeb11af4b8380d8b64a1 From a8fc5a722ff9da81290a559fb874fa0a38fb58ec Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 6 Jun 2025 18:36:03 +0400 Subject: [PATCH 159/310] Fix display of contact status. --- .../SourceFiles/history/history_widget.cpp | 18 ++++++++++++++++++ .../history/view/history_view_chat_section.cpp | 4 ---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 5aedac9535..eb170a4250 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -3230,6 +3230,15 @@ void HistoryWidget::updateControlsVisibility() { if (_sponsoredMessageBar && checkSponsoredMessageBarVisibility()) { _sponsoredMessageBar->toggle(true, anim::type::normal); } + if (_paysStatus) { + _paysStatus->show(); + } + if (_contactStatus) { + _contactStatus->show(); + } + if (_businessBotStatus) { + _businessBotStatus->show(); + } if (_subsectionTabs) { _subsectionTabs->show(); } @@ -4463,6 +4472,15 @@ void HistoryWidget::hideChildWidgets() { if (_chooseTheme) { _chooseTheme->hide(); } + if (_paysStatus) { + _paysStatus->hide(); + } + if (_contactStatus) { + _contactStatus->hide(); + } + if (_businessBotStatus) { + _businessBotStatus->hide(); + } hideChildren(); } diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 6bbe275c07..7f40aac55f 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2119,10 +2119,6 @@ void ChatWidget::checkPinnedBarState() { }, _pinnedBar->lifetime()); orderWidgets(); - - if (animatingShow()) { - _pinnedBar->hide(); - } } void ChatWidget::clearHidingPinnedBar() { From 22f9b1a0b1e3158ca328fcb93738429a51a797ad Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 9 Jun 2025 09:24:46 +0400 Subject: [PATCH 160/310] Hide photo change button for monoforums. --- Telegram/SourceFiles/info/profile/info_profile_cover.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 05a11f3008..f6798194c3 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -830,7 +830,8 @@ void Cover::refreshUploadPhotoOverlay() { if (const auto chat = _peer->asChat()) { return chat->canEditInformation(); } else if (const auto channel = _peer->asChannel()) { - return channel->canEditInformation(); + return channel->canEditInformation() + && !channel->isMonoforum(); } else if (const auto user = _peer->asUser()) { return user->isSelf() || (user->isContact() From 959229f143923be1875659f1facc8f964d36f614 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 9 Jun 2025 09:38:46 +0400 Subject: [PATCH 161/310] Version 5.15.3. - Fix new contact top bar appearance. - Remove change photo button for channel direct messages. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 4 ++-- Telegram/build/version | 8 ++++---- changelog.txt | 5 +++++ cmake | 2 +- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index d49fd63a41..4d39fc2b4c 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="5.15.2.0" /> + Version="5.15.3.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index a8844cf0e3..ac6a617460 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 5,15,2,0 - PRODUCTVERSION 5,15,2,0 + FILEVERSION 5,15,3,0 + PRODUCTVERSION 5,15,3,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "5.15.2.0" + VALUE "FileVersion", "5.15.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.2.0" + VALUE "ProductVersion", "5.15.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 57e38d9980..f98bbfb554 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 5,15,2,0 - PRODUCTVERSION 5,15,2,0 + FILEVERSION 5,15,3,0 + PRODUCTVERSION 5,15,3,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", "5.15.2.0" + VALUE "FileVersion", "5.15.3.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.2.0" + VALUE "ProductVersion", "5.15.3.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index b1b92ca664..1f93f98968 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5015002; -constexpr auto AppVersionStr = "5.15.2"; +constexpr auto AppVersion = 5015003; +constexpr auto AppVersionStr = "5.15.3"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/build/version b/Telegram/build/version index b0ed63ee1a..30496c9d19 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5015002 +AppVersion 5015003 AppVersionStrMajor 5.15 -AppVersionStrSmall 5.15.2 -AppVersionStr 5.15.2 +AppVersionStrSmall 5.15.3 +AppVersionStr 5.15.3 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.15.2 +AppVersionOriginal 5.15.3 diff --git a/changelog.txt b/changelog.txt index 94044fea23..7c612c7dac 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +5.15.3 (09.06.25) + +- Fix new contact top bar appearance. +- Remove change photo button for channel direct messages. + 5.15.2 (05.06.25) - Fix sending messages in new forum layout. diff --git a/cmake b/cmake index 3fa88ebd4a..c0608b65b6 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 3fa88ebd4a7e66cc8fbedeb11af4b8380d8b64a1 +Subproject commit c0608b65b60d52dabbd78ff0752bb9e317c55251 From d4f38b6d660fbd78b35315fbe620e2d5ea54776e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 9 Jun 2025 11:05:47 +0400 Subject: [PATCH 162/310] Version 5.15.3: Revert cmake_helpers downgrade. --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index c0608b65b6..3fa88ebd4a 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit c0608b65b60d52dabbd78ff0752bb9e317c55251 +Subproject commit 3fa88ebd4a7e66cc8fbedeb11af4b8380d8b64a1 From 6d31a4246fdaad37c423a11dc3ddc5c7fa9e2251 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Mon, 9 Jun 2025 14:28:13 +0000 Subject: [PATCH 163/310] Fix default cursor path --- Telegram/build/docker/centos_env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index 2f013df80d..fdc31da804 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -413,7 +413,7 @@ RUN git init libxcb-cursor \ && git fetch --depth=1 origin 4929f6051658ba5424b41703a1fb63f9db896065 \ && git reset --hard FETCH_HEAD \ && git submodule update --init --recursive --depth=1 \ - && ./autogen.sh --enable-static --disable-shared \ + && ./autogen.sh --enable-static --disable-shared --with-cursorpath='~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps' \ && make -j$(nproc) \ && make DESTDIR=/usr/src/xcb-cursor-cache install \ && cd .. \ From 67bd87b50c899ca0f7eee7af96e7cbee90d06611 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Mon, 9 Jun 2025 20:02:23 +0000 Subject: [PATCH 164/310] Prevent non-Qt harfbuzz/libpng from being linked --- Telegram/build/docker/centos_env/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index fdc31da804..e57745ad55 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -35,6 +35,8 @@ RUN sed -i '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/")/ -O0 {% if LTO %}-fno-lto -fno- RUN sed -i 's/NO_DEFAULT_PATH//g; s/PKG_CONFIG_ALLOW_SYSTEM_LIBS/PKG_CONFIG_IS_DUMB/g' /usr/share/cmake/Modules/FindPkgConfig.cmake RUN sed -i 's/set(OpenGL_GL_PREFERENCE "")/set(OpenGL_GL_PREFERENCE "LEGACY")/' /usr/share/cmake/Modules/FindOpenGL.cmake RUN sed -i '/Requires.private: valgrind/d' /usr/lib64/pkgconfig/libdrm.pc +RUN sed -i 's/-lharfbuzz//' /usr/lib64/pkgconfig/harfbuzz.pc +RUN sed -i 's/-lpng16//' /usr/lib64/pkgconfig/libpng16.pc RUN echo set debuginfod enabled on > /opt/rh/$TOOLSET/root/etc/gdbinit.d/00-debuginfod.gdb RUN adduser user From 23133499c77a5fcb2f00b91c300e78d6148b2951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= <dnovomesky@gmail.com> Date: Mon, 2 Jun 2025 16:52:00 +0200 Subject: [PATCH 165/310] Update dav1d, openh264, libwebp, libavif, libde265, libheif --- Telegram/build/docker/centos_env/Dockerfile | 18 ++++++----- Telegram/build/prepare/prepare.py | 34 +++++++++++++-------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index e57745ad55..a68726e311 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -172,7 +172,7 @@ RUN git clone -b v1.5.2 --depth=1 https://github.com/xiph/opus.git \ && rm -rf opus FROM builder AS dav1d -RUN git clone -b 1.4.1 --depth=1 https://github.com/videolan/dav1d.git \ +RUN git clone -b 1.5.1 --depth=1 https://github.com/videolan/dav1d.git \ && cd dav1d \ && meson build \ --buildtype=plain \ @@ -185,7 +185,7 @@ RUN git clone -b 1.4.1 --depth=1 https://github.com/videolan/dav1d.git \ && rm -rf dav1d FROM builder AS openh264 -RUN git clone -b v2.4.1 --depth=1 https://github.com/cisco/openh264.git \ +RUN git clone -b v2.6.0 --depth=1 https://github.com/cisco/openh264.git \ && cd openh264 \ && meson build \ --buildtype=plain \ @@ -196,7 +196,7 @@ RUN git clone -b v2.4.1 --depth=1 https://github.com/cisco/openh264.git \ && rm -rf openh264 FROM builder AS de265 -RUN git clone -b v1.0.15 --depth=1 https://github.com/strukturag/libde265.git \ +RUN git clone -b v1.0.16 --depth=1 https://github.com/strukturag/libde265.git \ && cd libde265 \ && cmake -B build . \ -DCMAKE_BUILD_TYPE=None \ @@ -229,7 +229,7 @@ RUN git init libvpx \ && rm -rf libvpx FROM builder AS webp -RUN git clone -b chrome-m116-5845 --depth=1 https://github.com/webmproject/libwebp.git \ +RUN git clone -b v1.5.0 --depth=1 https://github.com/webmproject/libwebp.git \ && cd libwebp \ && cmake -B build . \ -DWEBP_BUILD_ANIM_UTILS=OFF \ @@ -249,12 +249,12 @@ RUN git clone -b chrome-m116-5845 --depth=1 https://github.com/webmproject/libwe FROM builder AS avif COPY --link --from=dav1d /usr/src/dav1d-cache / -RUN git clone -b v1.0.4 --depth=1 https://github.com/AOMediaCodec/libavif.git \ +RUN git clone -b v1.3.0 --depth=1 https://github.com/AOMediaCodec/libavif.git \ && cd libavif \ - && sed -i 's/BUILD_SHARED_LIBS OR VCPKG_TARGET_TRIPLET/TRUE/' CMakeLists.txt \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ - -DAVIF_CODEC_DAV1D=ON \ + -DAVIF_CODEC_DAV1D=SYSTEM \ + -DAVIF_LIBYUV=OFF \ && cmake --build build \ && DESTDIR=/usr/src/avif-cache cmake --install build \ && cd .. \ @@ -263,7 +263,7 @@ RUN git clone -b v1.0.4 --depth=1 https://github.com/AOMediaCodec/libavif.git \ FROM builder AS heif COPY --link --from=de265 /usr/src/de265-cache / -RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ +RUN git clone -b v1.19.8 --depth=1 https://github.com/strukturag/libheif.git \ && cd libheif \ && cmake -B build . \ -DBUILD_SHARED_LIBS=OFF \ @@ -272,11 +272,13 @@ RUN git clone -b v1.18.2 --depth=1 https://github.com/strukturag/libheif.git \ -DWITH_X265=OFF \ -DWITH_AOM_DECODER=OFF \ -DWITH_AOM_ENCODER=OFF \ + -DWITH_OpenH264_DECODER=OFF \ -DWITH_RAV1E=OFF \ -DWITH_RAV1E_PLUGIN=OFF \ -DWITH_SvtEnc=OFF \ -DWITH_SvtEnc_PLUGIN=OFF \ -DWITH_DAV1D=OFF \ + -DWITH_LIBSHARPYUV=OFF \ -DWITH_EXAMPLES=OFF \ && cmake --build build \ && DESTDIR=/usr/src/heif-cache cmake --install build \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index a859afa93d..3742fd1125 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -749,7 +749,7 @@ win: # Somehow in x86 Debug build dav1d crashes on AV1 10bpc videos. stage('dav1d', """ - git clone -b 1.4.1 https://code.videolan.org/videolan/dav1d.git + git clone -b 1.5.1 https://code.videolan.org/videolan/dav1d.git cd dav1d win32: SET "TARGET=x86" @@ -817,7 +817,7 @@ mac: """) stage('openh264', """ - git clone -b v2.4.1 https://github.com/cisco/openh264.git + git clone -b v2.6.0 https://github.com/cisco/openh264.git cd openh264 win32: SET "TARGET=x86" @@ -878,7 +878,7 @@ mac: """) stage('libavif', """ - git clone -b v1.0.4 https://github.com/AOMediaCodec/libavif.git + git clone -b v1.3.0 https://github.com/AOMediaCodec/libavif.git cd libavif win: cmake . ^ @@ -888,7 +888,8 @@ win: -DCMAKE_POLICY_DEFAULT_CMP0091=NEW ^ -DBUILD_SHARED_LIBS=OFF ^ -DAVIF_ENABLE_WERROR=OFF ^ - -DAVIF_CODEC_DAV1D=ON + -DAVIF_CODEC_DAV1D=SYSTEM ^ + -DAVIF_LIBYUV=OFF cmake --build . --config Debug --parallel cmake --install . --config Debug release: @@ -901,16 +902,15 @@ mac: -D CMAKE_INSTALL_PREFIX:STRING=$USED_PREFIX \\ -D BUILD_SHARED_LIBS=OFF \\ -D AVIF_ENABLE_WERROR=OFF \\ - -D AVIF_CODEC_DAV1D=ON \\ - -D CMAKE_DISABLE_FIND_PACKAGE_libsharpyuv=ON + -D AVIF_CODEC_DAV1D=SYSTEM \\ + -D AVIF_LIBYUV=OFF cmake --build . --config MinSizeRel $MAKE_THREADS_CNT cmake --install . --config MinSizeRel """) stage('libde265', """ - git clone -b v1.0.15 https://github.com/strukturag/libde265.git + git clone -b v1.0.16 https://github.com/strukturag/libde265.git cd libde265 - git cherry-pick 5c5af1e win: cmake . ^ -A %WIN32X64% ^ @@ -943,7 +943,7 @@ mac: """) stage('libwebp', """ - git clone -b v1.4.0 https://github.com/webmproject/libwebp.git + git clone -b v1.5.0 https://github.com/webmproject/libwebp.git cd libwebp win: nmake /f Makefile.vc CFG=debug-static OBJDIR=out RTLIBCFG=static all @@ -983,11 +983,13 @@ mac: """) stage('libheif', """ - git clone -b v1.18.2 https://github.com/strukturag/libheif.git + git clone -b v1.19.8 https://github.com/strukturag/libheif.git cd libheif win: %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' libheif/CMakeLists.txt %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' libheif/CMakeLists.txt + %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/LIBHEIF_EXPORTS/LIBDE265_STATIC_BUILD/g' heifio/CMakeLists.txt + %THIRDPARTY_DIR%\\msys64\\usr\\bin\\sed.exe -i 's/HAVE_VISIBILITY/LIBHEIF_STATIC_BUILD/g' heifio/CMakeLists.txt cmake . ^ -A %WIN32X64% ^ -DCMAKE_INSTALL_PREFIX=%LIBS_DIR%/local ^ @@ -996,10 +998,15 @@ win: -DBUILD_TESTING=OFF ^ -DENABLE_PLUGIN_LOADING=OFF ^ -DWITH_LIBDE265=ON ^ + -DWITH_OpenH264_DECODER=OFF ^ -DWITH_SvtEnc=OFF ^ -DWITH_SvtEnc_PLUGIN=OFF ^ -DWITH_RAV1E=OFF ^ -DWITH_RAV1E_PLUGIN=OFF ^ + -DWITH_LIBSHARPYUV=OFF ^ + -DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE ^ + -DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE ^ + -DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE ^ -DWITH_EXAMPLES=OFF cmake --build . --config Debug --parallel cmake --install . --config Debug @@ -1017,14 +1024,17 @@ mac: -D WITH_AOM_ENCODER=OFF \\ -D WITH_AOM_DECODER=OFF \\ -D WITH_X265=OFF \\ + -D WITH_OpenH264_DECODER=OFF \\ -D WITH_SvtEnc=OFF \\ -D WITH_RAV1E=OFF \\ -D WITH_DAV1D=ON \\ -D WITH_LIBDE265=ON \\ -D LIBDE265_INCLUDE_DIR=$USED_PREFIX/include/ \\ -D LIBDE265_LIBRARY=$USED_PREFIX/lib/libde265.a \\ - -D LIBSHARPYUV_INCLUDE_DIR=$USED_PREFIX/include/webp/ \\ - -D LIBSHARPYUV_LIBRARY=$USED_PREFIX/lib/libsharpyuv.a \\ + -D WITH_LIBSHARPYUV=OFF \\ + -D CMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE \\ + -D CMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE \\ + -D CMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE \\ -D WITH_EXAMPLES=OFF cmake --build . --config MinSizeRel $MAKE_THREADS_CNT cmake --install . --config MinSizeRel From 63e1d6dab6e69a97f3a4f16f9dc3ac401e28811a Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 12 Jun 2025 19:12:12 +0400 Subject: [PATCH 166/310] Fix saved messages sublists updates. --- Telegram/SourceFiles/history/history_item.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 58e747454a..4e93a2bfba 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -3847,6 +3847,11 @@ void HistoryItem::createComponents(CreateConfig &&config) { } } saved->sublistPeerId = config.savedSublistPeer; + if (_history->peer->isSelf()) { + saved->savedMessagesSublist + = _history->owner().savedMessages().sublist( + _history->owner().peer(saved->sublistPeerId)); + } } if (const auto reply = Get<HistoryMessageReply>()) { From 2e4a437d32c6b1141d8ac541ba013e5e4239b38c Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 12 Jun 2025 22:02:21 +0400 Subject: [PATCH 167/310] Version 5.15.4. - Fix updating messages in Saved Messages subchats. - Fix possible issues with mouse cursor on Linux. --- Telegram/Resources/uwp/AppX/AppxManifest.xml | 2 +- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 4 ++-- Telegram/build/version | 8 ++++---- changelog.txt | 5 +++++ 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 4d39fc2b4c..e0aedc42fa 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ <Identity Name="TelegramMessengerLLP.TelegramDesktop" ProcessorArchitecture="ARCHITECTURE" Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A" - Version="5.15.3.0" /> + Version="5.15.4.0" /> <Properties> <DisplayName>Telegram Desktop</DisplayName> <PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName> diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index ac6a617460..0dd49f9757 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 5,15,3,0 - PRODUCTVERSION 5,15,3,0 + FILEVERSION 5,15,4,0 + PRODUCTVERSION 5,15,4,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Telegram FZ-LLC" VALUE "FileDescription", "Telegram Desktop" - VALUE "FileVersion", "5.15.3.0" + VALUE "FileVersion", "5.15.4.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.3.0" + VALUE "ProductVersion", "5.15.4.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index f98bbfb554..a127c2d426 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 5,15,3,0 - PRODUCTVERSION 5,15,3,0 + FILEVERSION 5,15,4,0 + PRODUCTVERSION 5,15,4,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", "5.15.3.0" + VALUE "FileVersion", "5.15.4.0" VALUE "LegalCopyright", "Copyright (C) 2014-2025" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "5.15.3.0" + VALUE "ProductVersion", "5.15.4.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 1f93f98968..06f6ac4f32 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs; constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs; constexpr auto AppName = "Telegram Desktop"_cs; constexpr auto AppFile = "Telegram"_cs; -constexpr auto AppVersion = 5015003; -constexpr auto AppVersionStr = "5.15.3"; +constexpr auto AppVersion = 5015004; +constexpr auto AppVersionStr = "5.15.4"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/build/version b/Telegram/build/version index 30496c9d19..15187380d5 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,7 +1,7 @@ -AppVersion 5015003 +AppVersion 5015004 AppVersionStrMajor 5.15 -AppVersionStrSmall 5.15.3 -AppVersionStr 5.15.3 +AppVersionStrSmall 5.15.4 +AppVersionStr 5.15.4 BetaChannel 0 AlphaVersion 0 -AppVersionOriginal 5.15.3 +AppVersionOriginal 5.15.4 diff --git a/changelog.txt b/changelog.txt index 7c612c7dac..21a96deb3a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +5.15.4 (12.06.25) + +- Fix updating messages in Saved Messages subchats. +- Fix possible issues with mouse cursor on Linux. + 5.15.3 (09.06.25) - Fix new contact top bar appearance. From 02dd0dbbef5548f38fe04095df210c90155d4ae6 Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Sun, 22 Jun 2025 14:58:40 +0000 Subject: [PATCH 168/310] Push Docker image to GHCR again --- .github/workflows/docker.yml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..8c04e71228 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,42 @@ +name: Docker. + +on: + push: + paths: + - '.github/workflows/docker.yml' + - 'Telegram/build/docker/centos_env/**' + +jobs: + docker: + name: Ubuntu + runs-on: ubuntu-latest + if: github.ref_name == github.event.repository.default_branch + + env: + IMAGE_TAG: ghcr.io/${{ github.repository }}/centos_env:latest + + steps: + - name: Clone. + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: First set up. + run: | + sudo apt update + curl -sSL https://install.python-poetry.org | python3 - + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + + - name: Free up some disk space. + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be + with: + tool-cache: true + + - name: Docker image build. + run: | + cd Telegram/build/docker/centos_env + poetry install + DEBUG= LTO= poetry run gen_dockerfile | DOCKER_BUILDKIT=1 docker build -t $IMAGE_TAG - + + - name: Push the Docker image. + run: docker push $IMAGE_TAG From e6ebc19b4f194b81248ffa2e5738e42956d1d4fd Mon Sep 17 00:00:00 2001 From: Ilya Fedin <fedin-ilja2010@ya.ru> Date: Thu, 26 Jun 2025 03:00:48 +0000 Subject: [PATCH 169/310] Switch qt snapcraft part to cmake plugin --- snap/snapcraft.yaml | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 70bf6cb179..1419ed4f88 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -318,7 +318,17 @@ parts: - -./usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/*.so qt: - plugin: nil + source: https://github.com/qt/qt5.git + source-depth: 1 + source-tag: v6.9.1 + source-submodules: + - qtbase + - qtdeclarative + - qtimageformats + - qtshadertools + - qtsvg + - qtwayland + plugin: cmake build-environment: - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s build-packages: @@ -404,28 +414,22 @@ parts: - zlib1g - mesa-vulkan-drivers - xkb-data + cmake-generator: Ninja + cmake-parameters: + - -DCMAKE_BUILD_TYPE=Release + - -DCMAKE_INSTALL_PREFIX=/usr + - -DCMAKE_PREFIX_PATH=$CRAFT_STAGE/usr + - -DINSTALL_LIBDIR=/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR + - -DQT_GENERATE_SBOM=OFF + - -DINPUT_openssl=linked override-pull: | - QT=6.9.1 - - git clone -b v${QT} --depth=1 https://github.com/qt/qt5.git . - git submodule update --init --recursive --depth=1 qtbase qtdeclarative qtwayland qtimageformats qtsvg qtshadertools - + craftctl default + QT="$(grep 'set(QT_REPO_MODULE_VERSION' qtbase/.cmake.conf | sed -r 's/.*"(.*)".*/\1/')" cd qtbase find $CRAFT_STAGE/patches/qtbase_${QT} -type f -print0 | sort -z | xargs -r0 git apply cd ../qtwayland find $CRAFT_STAGE/patches/qtwayland_${QT} -type f -print0 | sort -z | xargs -r0 git apply cd .. - override-build: | - cmake -GNinja -B $CRAFT_PART_BUILD \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr \ - -DCMAKE_PREFIX_PATH=$CRAFT_STAGE/usr \ - -DINSTALL_LIBDIR=/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR \ - -DQT_GENERATE_SBOM=OFF \ - -DINPUT_openssl=linked - - cmake --build . -j$CRAFT_PARALLEL_BUILD_COUNT - DESTDIR="$CRAFT_PART_INSTALL" cmake --install . prime: - -./usr/bin - -./usr/doc From 5a6a5fd4d14f4429d21e1c6926fb9231ae78120a Mon Sep 17 00:00:00 2001 From: Sean Wei <me@sean.taipei> Date: Wed, 18 Jun 2025 15:30:00 -0400 Subject: [PATCH 170/310] Change `const T&&` parameters to `T&&` to enable proper move semantics Previously some constructors/functions used `const T&&`, which prevents calling the move constructor. This commit removes the `const` qualifier so that `std::move` actually performs a move. --- Telegram/SourceFiles/data/stickers/data_stickers.cpp | 2 +- Telegram/SourceFiles/data/stickers/data_stickers.h | 2 +- Telegram/SourceFiles/editor/scene/scene_item_image.cpp | 2 +- Telegram/SourceFiles/editor/scene/scene_item_image.h | 2 +- Telegram/SourceFiles/editor/scene/scene_item_line.cpp | 2 +- Telegram/SourceFiles/editor/scene/scene_item_line.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.cpp b/Telegram/SourceFiles/data/stickers/data_stickers.cpp index 97e0014e89..19c8e9640f 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers.cpp @@ -789,7 +789,7 @@ void Stickers::somethingReceived( void Stickers::setPackAndEmoji( StickersSet &set, StickersPack &&pack, - const std::vector<TimeId> &&dates, + std::vector<TimeId> &&dates, const QVector<MTPStickerPack> &packs) { set.stickers = std::move(pack); set.dates = std::move(dates); diff --git a/Telegram/SourceFiles/data/stickers/data_stickers.h b/Telegram/SourceFiles/data/stickers/data_stickers.h index affff47b8a..cc2c7fbe4f 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers.h @@ -291,7 +291,7 @@ private: void setPackAndEmoji( StickersSet &set, StickersPack &&pack, - const std::vector<TimeId> &&dates, + std::vector<TimeId> &&dates, const QVector<MTPStickerPack> &packs); void somethingReceived( const QVector<MTPStickerSet> &list, diff --git a/Telegram/SourceFiles/editor/scene/scene_item_image.cpp b/Telegram/SourceFiles/editor/scene/scene_item_image.cpp index b3cc544cfd..1a939231bc 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_image.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_image.cpp @@ -10,7 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { ItemImage::ItemImage( - const QPixmap &&pixmap, + QPixmap &&pixmap, ItemBase::Data data) : ItemBase(std::move(data)) , _pixmap(std::move(pixmap)) { diff --git a/Telegram/SourceFiles/editor/scene/scene_item_image.h b/Telegram/SourceFiles/editor/scene/scene_item_image.h index 1754ac279b..320370ea27 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_image.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_image.h @@ -14,7 +14,7 @@ namespace Editor { class ItemImage : public ItemBase { public: ItemImage( - const QPixmap &&pixmap, + QPixmap &&pixmap, ItemBase::Data data); void paint( QPainter *p, diff --git a/Telegram/SourceFiles/editor/scene/scene_item_line.cpp b/Telegram/SourceFiles/editor/scene/scene_item_line.cpp index 0d40e24c7d..0b68b3969d 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_line.cpp +++ b/Telegram/SourceFiles/editor/scene/scene_item_line.cpp @@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Editor { -ItemLine::ItemLine(const QPixmap &&pixmap) +ItemLine::ItemLine(QPixmap &&pixmap) : _pixmap(std::move(pixmap)) , _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) { } diff --git a/Telegram/SourceFiles/editor/scene/scene_item_line.h b/Telegram/SourceFiles/editor/scene/scene_item_line.h index d4746c8e39..f750fd4840 100644 --- a/Telegram/SourceFiles/editor/scene/scene_item_line.h +++ b/Telegram/SourceFiles/editor/scene/scene_item_line.h @@ -13,7 +13,7 @@ namespace Editor { class ItemLine : public NumberedItem { public: - ItemLine(const QPixmap &&pixmap); + ItemLine(QPixmap &&pixmap); QRectF boundingRect() const override; void paint( QPainter *p, From cf4a617f2b271e379961be9ee95db9f6de0ed56f Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli <yagiz@nizipli.com> Date: Fri, 27 Jun 2025 12:56:01 -0400 Subject: [PATCH 171/310] update ada-url to v3.2.4 (#29353) --- Telegram/build/docker/centos_env/Dockerfile | 2 +- Telegram/build/prepare/prepare.py | 2 +- snap/snapcraft.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/build/docker/centos_env/Dockerfile b/Telegram/build/docker/centos_env/Dockerfile index a68726e311..9632ce4644 100644 --- a/Telegram/build/docker/centos_env/Dockerfile +++ b/Telegram/build/docker/centos_env/Dockerfile @@ -817,7 +817,7 @@ RUN git init tg_owt \ && rm -rf tg_owt FROM builder AS ada -RUN git clone -b v3.2.2 --depth=1 https://github.com/ada-url/ada.git \ +RUN git clone -b v3.2.4 --depth=1 https://github.com/ada-url/ada.git \ && cd ada \ && cmake -B build . \ -D ADA_TESTING=OFF \ diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 3742fd1125..b66056016c 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1874,7 +1874,7 @@ release: """) stage('ada', """ - git clone -b v3.2.2 https://github.com/ada-url/ada.git + git clone -b v3.2.4 https://github.com/ada-url/ada.git cd ada win: cmake -B out . ^ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 1419ed4f88..b36f86d3c2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -217,7 +217,7 @@ parts: ada: source: https://github.com/ada-url/ada.git source-depth: 1 - source-tag: v3.2.2 + source-tag: v3.2.4 plugin: cmake build-environment: - LDFLAGS: ${LDFLAGS:+$LDFLAGS} -s From 9832af7cce0a4093421d78092c33931612f5b8d3 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 13 Jun 2025 13:37:04 +0400 Subject: [PATCH 172/310] Show messages from channels in monoforums. --- Telegram/SourceFiles/history/view/history_view_message.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 47ec86cb44..eab953b1e2 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3692,7 +3692,7 @@ bool Message::hasFromName() const { case Context::AdminLog: return true; case Context::Monoforum: - return data()->out(); + return data()->out() || data()->from()->isChannel(); case Context::History: case Context::ChatPreview: case Context::TTLViewer: From 06db13a0ab703aee5323052ae63ec196d9604f2b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 6 Jun 2025 14:52:16 +0400 Subject: [PATCH 173/310] Update API scheme to layer 205. --- .../data/business/data_shortcut_messages.cpp | 1 + .../data/components/scheduled_messages.cpp | 1 + .../data/components/sponsored_messages.cpp | 5 +- .../export/data/export_data_types.cpp | 83 ++++++++++---- .../export/data/export_data_types.h | 96 ++++++++++------ .../export/output/export_output_html.cpp | 104 +++++++++++++++++- .../export/output/export_output_json.cpp | 57 +++++++++- .../admin_log/history_admin_log_item.cpp | 3 + Telegram/SourceFiles/history/history_item.cpp | 33 ++++++ .../history/history_item_helpers.cpp | 2 + .../view/history_view_contact_status.cpp | 11 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 28 +++-- 12 files changed, 349 insertions(+), 75 deletions(-) diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index ca6f362ed2..a195921a4c 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -54,6 +54,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), data.vdate(), data.vaction(), diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index cecdf3606c..9e88a17f78 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -59,6 +59,7 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(), data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(), data.vdate(), data.vaction(), diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index fa5dacf697..ffd8c2f20d 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -263,7 +263,10 @@ void SponsoredMessages::request(not_null<History*> history, Fn<void()> done) { } } request.requestId = _session->api().request( - MTPmessages_GetSponsoredMessages(history->peer->input) + MTPmessages_GetSponsoredMessages( + MTP_flags(0), + history->peer->input, + MTPint()) // msg_id ).done([=](const MTPmessages_sponsoredMessages &result) { parse(history, result); if (done) { diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index d9b66c85f8..8d13561903 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -335,6 +335,10 @@ Utf8String Reaction::TypeToString(const Reaction &reaction) { Unexpected("Type in Reaction::Type."); } +std::vector<TextPart> ParseText(const MTPTextWithEntities &text) { + return ParseText(text.data().vtext(), text.data().ventities().v); +} + Utf8String Reaction::Id(const Reaction &reaction) { auto id = Utf8String(); switch (reaction.type) { @@ -777,17 +781,16 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) { auto result = Poll(); data.vpoll().match([&](const MTPDpoll &poll) { result.id = poll.vid().v; - result.question = ParseString(poll.vquestion().data().vtext()); + result.question = ParseText(poll.vquestion()); result.closed = poll.is_closed(); result.answers = ranges::views::all( poll.vanswers().v ) | ranges::views::transform([](const MTPPollAnswer &answer) { - return answer.match([](const MTPDpollAnswer &answer) { - auto result = Poll::Answer(); - result.text = ParseString(answer.vtext().data().vtext()); - result.option = answer.voption().v; - return result; - }); + const auto &data = answer.data(); + auto result = Poll::Answer(); + result.text = ParseText(data.vtext()); + result.option = data.voption().v; + return result; }) | ranges::to_vector; }); data.vresults().match([&](const MTPDpollResults &results) { @@ -796,25 +799,47 @@ Poll ParsePoll(const MTPDmessageMediaPoll &data) { } if (const auto resultsList = results.vresults()) { for (const auto &single : resultsList->v) { - single.match([&](const MTPDpollAnswerVoters &voters) { - const auto i = ranges::find( - result.answers, - voters.voption().v, - &Poll::Answer::option); - if (i == end(result.answers)) { - return; - } - i->votes = voters.vvoters().v; - if (voters.is_chosen()) { - i->my = true; - } - }); + const auto &voters = single.data(); + const auto i = ranges::find( + result.answers, + voters.voption().v, + &Poll::Answer::option); + if (i == end(result.answers)) { + continue; + } + i->votes = voters.vvoters().v; + if (voters.is_chosen()) { + i->my = true; + } } } }); return result; } +TodoListItem ParseTodoListItem(const MTPTodoItem &item) { + const auto &data = item.data(); + auto result = TodoListItem(); + result.text = ParseText(data.vtitle()); + result.id = data.vid().v; + return result; +} + +TodoList ParseTodoList(const MTPDmessageMediaToDo &data) { + auto result = TodoList(); + data.vtodo().match([&](const MTPDtodoList &data) { + result.title = ParseText(data.vtitle()); + result.othersCanAppend = data.is_others_can_append(); + result.othersCanComplete = data.is_others_can_complete(); + result.items = ranges::views::all( + data.vlist().v + ) | ranges::views::transform( + ParseTodoListItem + ) | ranges::to_vector; + }); + return result; +} + GiveawayStart ParseGiveaway(const MTPDmessageMediaGiveaway &data) { auto result = GiveawayStart{ .untilDate = data.vuntil_date().v, @@ -1367,6 +1392,8 @@ Media ParseMedia( result.ttl = data.vperiod().v; }, [&](const MTPDmessageMediaPoll &data) { result.content = ParsePoll(data); + }, [&](const MTPDmessageMediaToDo &data) { + result.content = ParseTodoList(data); }, [](const MTPDmessageMediaDice &data) { // #TODO dice }, [](const MTPDmessageMediaStory &data) { @@ -1714,6 +1741,22 @@ ServiceAction ParseServiceAction( .stars = int(data.vstars().v), .broadcastAllowed = data.is_broadcast_messages_allowed(), }; + }, [&](const MTPDmessageActionTodoCompletions &data) { + const auto take = [](const MTPVector<MTPint> &list) { + return list.v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; + }; + result.content = ActionTodoCompletions{ + .completed = take(data.vcompleted()), + .incompleted = take(data.vincompleted()), + }; + }, [&](const MTPDmessageActionTodoAppendTasks &data) { + result.content = ActionTodoAppendTasks{ + .items = data.vlist().v + | ranges::views::transform(ParseTodoListItem) + | ranges::to_vector, + }; }, [&](const MTPDmessageActionConferenceCall &data) { auto content = ActionPhoneCall(); using State = ActionPhoneCall::State; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 5a3e2c3ec2..6e7c873b28 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -44,6 +44,39 @@ inline auto NumberToString(Type value, int length = 0, char filler = '0') filler).replace(',', '.'); } +struct TextPart { + enum class Type { + Text, + Unknown, + Mention, + Hashtag, + BotCommand, + Url, + Email, + Bold, + Italic, + Code, + Pre, + TextUrl, + MentionName, + Phone, + Cashtag, + Underline, + Strike, + Blockquote, + BankCard, + Spoiler, + CustomEmoji, + }; + Type type = Type::Text; + Utf8String text; + Utf8String additional; + + [[nodiscard]] static Utf8String UnavailableEmoji() { + return "(unavailable)"; + } +}; + struct UserpicsInfo { int count = 0; }; @@ -198,19 +231,31 @@ struct PaidMedia { struct Poll { struct Answer { - Utf8String text; + std::vector<TextPart> text; QByteArray option; int votes = 0; bool my = false; }; uint64 id = 0; - Utf8String question; + std::vector<TextPart> question; std::vector<Answer> answers; int totalVotes = 0; bool closed = false; }; +struct TodoListItem { + std::vector<TextPart> text; + int id = 0; +}; + +struct TodoList { + bool othersCanAppend = false; + bool othersCanComplete = false; + std::vector<TextPart> title; + std::vector<TodoListItem> items; +}; + struct GiveawayStart { std::vector<QString> countries; std::vector<ChannelId> channels; @@ -370,6 +415,7 @@ struct Media { Game, Invoice, Poll, + TodoList, GiveawayStart, GiveawayResults, PaidMedia, @@ -404,39 +450,6 @@ Media ParseMedia( const QString &folder, TimeId date); -struct TextPart { - enum class Type { - Text, - Unknown, - Mention, - Hashtag, - BotCommand, - Url, - Email, - Bold, - Italic, - Code, - Pre, - TextUrl, - MentionName, - Phone, - Cashtag, - Underline, - Strike, - Blockquote, - BankCard, - Spoiler, - CustomEmoji, - }; - Type type = Type::Text; - Utf8String text; - Utf8String additional; - - [[nodiscard]] static Utf8String UnavailableEmoji() { - return "(unavailable)"; - } -}; - struct ActionChatCreate { Utf8String title; std::vector<UserId> userIds; @@ -676,6 +689,15 @@ struct ActionPaidMessagesPrice { bool broadcastAllowed = false; }; +struct ActionTodoCompletions { + std::vector<int> completed; + std::vector<int> incompleted; +}; + +struct ActionTodoAppendTasks { + std::vector<TodoListItem> items; +}; + struct ServiceAction { std::variant< v::null_t, @@ -723,7 +745,9 @@ struct ServiceAction { ActionPrizeStars, ActionStarGift, ActionPaidMessagesRefunded, - ActionPaidMessagesPrice> content; + ActionPaidMessagesPrice, + ActionTodoCompletions, + ActionTodoAppendTasks> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index ed73f81b0f..edb7d95adb 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -621,7 +621,14 @@ private: [[nodiscard]] QByteArray pushPhotoMedia( const Data::Photo &data, const QString &basePath); - [[nodiscard]] QByteArray pushPoll(const Data::Poll &data); + [[nodiscard]] QByteArray pushPoll( + const Data::Poll &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase); + [[nodiscard]] QByteArray pushTodoList( + const Data::TodoList &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase); [[nodiscard]] QByteArray pushGiveaway( const PeersMap &peers, const Data::GiveawayStart &data); @@ -1395,6 +1402,50 @@ auto HtmlWriter::Wrap::pushMessage( + QString::number(data.stars).toUtf8() + " Telegram Stars."; return result; + }, [&](const ActionTodoCompletions &data) { + auto completed = QByteArrayList(); + for (const auto index : data.completed) { + completed.push_back(QByteArray::number(index)); + } + auto incompleted = QByteArrayList(); + for (const auto index : data.incompleted) { + incompleted.push_back(QByteArray::number(index)); + } + const auto list = [](const QByteArrayList &v) { + return v.isEmpty() + ? QByteArray() + : (v.size() > 1) + ? (v.mid(0, v.size() - 1).join(", ") + " and " + v.back()) + : v.front(); + }; + if (completed.isEmpty() && !incompleted.isEmpty()) { + return serviceFrom + + " marked " + + list(incompleted) + + " as not done yet in " + + wrapReplyToLink("this todo list") + "."; + } else if (!completed.isEmpty() && incompleted.isEmpty()) { + return serviceFrom + + " marked " + + list(completed) + + " as done in " + + wrapReplyToLink("this todo list") + "."; + } + return serviceFrom + + " marked " + + list(completed) + + " as done and " + + list(incompleted) + + " as not done yet in " + + wrapReplyToLink("this todo list") + "."; + }, [&](const ActionTodoAppendTasks &data) { + auto tasks = QByteArrayList(); + for (const auto &task : data.items) { + tasks.push_back(""" + + FormatText(task.text, internalLinksDomain, _base) + + """); + } + return serviceFrom + " added tasks: " + tasks.join(", "); }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { @@ -1721,7 +1772,9 @@ QByteArray HtmlWriter::Wrap::pushMedia( Assert(!message.media.ttl); return pushPhotoMedia(*photo, basePath); } else if (const auto poll = std::get_if<Poll>(&content)) { - return pushPoll(*poll); + return pushPoll(*poll, internalLinksDomain, _base); + } else if (const auto todo = std::get_if<TodoList>(&content)) { + return pushTodoList(*todo, internalLinksDomain, _base); } else if (const auto giveaway = std::get_if<GiveawayStart>(&content)) { return pushGiveaway(peers, *giveaway); } else if (const auto giveaway = std::get_if<GiveawayResults>(&content)) { @@ -1999,13 +2052,19 @@ QByteArray HtmlWriter::Wrap::pushPhotoMedia( return result; } -QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { +QByteArray HtmlWriter::Wrap::pushPoll( + const Data::Poll &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase) { using namespace Data; auto result = pushDiv("media_wrap clearfix"); result.append(pushDiv("media_poll")); result.append(pushDiv("question bold")); - result.append(SerializeString(data.question)); + result.append(FormatText( + data.question, + internalLinksDomain, + relativeLinkBase)); result.append(popTag()); result.append(pushDiv("details")); if (data.closed) { @@ -2036,7 +2095,9 @@ QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { }; for (const auto &answer : data.answers) { result.append(pushDiv("answer")); - result.append("- " + SerializeString(answer.text) + details(answer)); + result.append("- " + + FormatText(answer.text, internalLinksDomain, relativeLinkBase) + + details(answer)); result.append(popTag()); } result.append(pushDiv("total details ")); @@ -2047,6 +2108,38 @@ QByteArray HtmlWriter::Wrap::pushPoll(const Data::Poll &data) { return result; } +QByteArray HtmlWriter::Wrap::pushTodoList( + const Data::TodoList &data, + const QString &internalLinksDomain, + const QString &relativeLinkBase) { + using namespace Data; + + auto result = pushDiv("media_wrap clearfix"); + result.append(pushDiv("media_poll")); + result.append(pushDiv("question bold")); + result.append(FormatText( + data.title, + internalLinksDomain, + relativeLinkBase)); + result.append(popTag()); + result.append(pushDiv("details")); + result.append(SerializeString("To-do List")); + result.append(popTag()); + const auto details = [&](const TodoListItem &item) { + return QByteArray(""); // #TODO todo + }; + for (const auto &item : data.items) { + result.append(pushDiv("answer")); + result.append("- " + + FormatText(item.text, internalLinksDomain, relativeLinkBase) + + details(item)); + result.append(popTag()); + } + result.append(popTag()); + result.append(popTag()); + return result; +} + QByteArray HtmlWriter::Wrap::pushGiveaway( const PeersMap &peers, const Data::GiveawayStart &data) { @@ -2436,6 +2529,7 @@ MediaData HtmlWriter::Wrap::prepareMediaData( result.description = data.description; result.status = Data::FormatMoneyAmount(data.amount, data.currency); }, [](const Poll &data) { + }, [](const TodoList &data) { }, [](const GiveawayStart &data) { }, [](const GiveawayResults &data) { }, [&](const PaidMedia &data) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 7af9bc0b97..ec57f7f83a 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -680,6 +680,34 @@ QByteArray SerializeMessage( pushAction("paid_messages_price_change"); push("price_stars", data.stars); push("is_broadcast_messages_allowed", data.broadcastAllowed); + }, [&](const ActionTodoCompletions &data) { + pushActor(); + pushAction("todo_completions"); + auto completed = QByteArrayList(); + for (const auto index : data.completed) { + completed.push_back(QByteArray::number(index)); + } + auto incompleted = QByteArrayList(); + for (const auto index : data.incompleted) { + incompleted.push_back(QByteArray::number(index)); + } + pushBare("completed", '[' + completed.join(',') + ']'); + pushBare("incompleted", '[' + incompleted.join(',') + ']'); + }, [&](const ActionTodoAppendTasks &data) { + pushActor(); + pushAction("todo_append_tasks"); + const auto items = ranges::views::all( + data.items + ) | ranges::views::transform([&](const TodoListItem &item) { + context.nesting.push_back(Context::kArray); + auto result = SerializeObject(context, { + { "text", SerializeText(context, item.text) }, + { "id", NumberToString(item.id) }, + }); + context.nesting.pop_back(); + return result; + }) | ranges::to_vector; + pushBare("items", SerializeArray(context, items)); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { @@ -807,7 +835,7 @@ QByteArray SerializeMessage( ) | ranges::views::transform([&](const Poll::Answer &answer) { context.nesting.push_back(Context::kArray); auto result = SerializeObject(context, { - { "text", SerializeString(answer.text) }, + { "text", SerializeText(context, answer.text) }, { "voters", NumberToString(answer.votes) }, { "chosen", answer.my ? "true" : "false" }, }); @@ -818,11 +846,36 @@ QByteArray SerializeMessage( context.nesting.pop_back(); pushBare("poll", SerializeObject(context, { - { "question", SerializeString(data.question) }, + { "question", SerializeText(context, data.question) }, { "closed", data.closed ? "true" : "false" }, { "total_voters", NumberToString(data.totalVotes) }, { "answers", serialized } })); + }, [&](const TodoList &data) { + context.nesting.push_back(Context::kObject); + const auto items = ranges::views::all( + data.items + ) | ranges::views::transform([&](const TodoListItem &item) { + context.nesting.push_back(Context::kArray); + auto result = SerializeObject(context, { + { "text", SerializeText(context, item.text) }, + { "id", NumberToString(item.id) }, + }); + context.nesting.pop_back(); + return result; + }) | ranges::to_vector; + const auto serialized = SerializeArray(context, items); + context.nesting.pop_back(); + + pushBare("todo_list", SerializeObject(context, { + { "title", SerializeText(context, data.title) }, + { "others_can_append", data.othersCanAppend ? "true" : "false" }, + { + "others_can_complete", + data.othersCanComplete ? "true" : "false", + }, + { "answers", serialized } + })); }, [&](const GiveawayStart &data) { context.nesting.push_back(Context::kArray); const auto channels = ranges::views::all( diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index 2e3580222e..abe3d949c5 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -131,6 +131,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { const auto reply = PrepareLogReply(data.vreply_to()); const auto removeFlags = Flag::f_out | Flag::f_post + | Flag::f_saved_peer_id | Flag::f_reactions_are_possible | Flag::f_reactions | Flag::f_ttl_period @@ -140,6 +141,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { data.vid(), data.vfrom_id() ? *data.vfrom_id() : MTPPeer(), data.vpeer_id(), + MTPPeer(), // saved_peer_id reply.value_or(MTPMessageReplyHeader()), MTP_int(newDate), data.vaction(), @@ -150,6 +152,7 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { const auto reply = PrepareLogReply(data.vreply_to()); const auto removeFlags = Flag::f_out | Flag::f_post + | Flag::f_saved_peer_id | (reply ? Flag() : Flag::f_reply_to) | Flag::f_replies | Flag::f_edit_date diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 4e93a2bfba..0808d03fd3 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -357,6 +357,8 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia( return std::make_unique<Data::MediaPoll>( item, item->history()->owner().processPoll(media)); + }, [&](const MTPDmessageMediaToDo &media) -> Result { + return nullptr; // #TODO todo }, [&](const MTPDmessageMediaDice &media) -> Result { return std::make_unique<Data::MediaDice>( item, @@ -2102,6 +2104,7 @@ void HistoryItem::applyEditionToHistoryCleared() { MTP_int(id), peerToMTP(PeerId(0)), // from_id peerToMTP(_history->peer->id), + MTPPeer(), // saved_peer_id MTPMessageReplyHeader(), MTP_int(date()), MTP_messageActionHistoryClear(), @@ -4553,6 +4556,24 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { }, [](const MTPDmessageReplyStoryHeader &data) { }); } + + const auto savedSublistPeer = message.vsaved_peer_id() + ? peerFromMTP(*message.vsaved_peer_id()) + : PeerId(); + const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin(); + if (savedSublistPeer || requiresMonoforumPeer) { + UpdateComponents(HistoryMessageSaved::Bit()); + const auto saved = Get<HistoryMessageSaved>(); + saved->sublistPeerId = savedSublistPeer + ? savedSublistPeer + : _from->id; + if (_history->peer->isSelf()) { + saved->savedMessagesSublist + = _history->owner().savedMessages().sublist( + _history->owner().peer(saved->sublistPeerId)); + } + } + setServiceMessageByAction(action); } @@ -5853,6 +5874,16 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &action) { + auto result = PreparedServiceText(); // #TODO todo + return result; + }; + + auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &action) { + auto result = PreparedServiceText(); // #TODO todo + return result; + }; + auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { Unexpected("PhoneCall type in setServiceMessageFromMtp."); }; @@ -5907,6 +5938,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { preparePaidMessagesRefunded, preparePaidMessagesPrice, prepareConferenceCall, + prepareTodoCompletions, + prepareTodoAppendTasks, PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>, PrepareErrorText<MTPDmessageActionEmpty>)); diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 6bf1382f86..9b791852c9 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -897,6 +897,8 @@ MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) { return Result::Good; }, [](const MTPDmessageMediaPoll &) { return Result::Good; + }, [](const MTPDmessageMediaToDo &) { + return Result::Good; }, [](const MTPDmessageMediaDice &) { return Result::Good; }, [](const MTPDmessageMediaStory &data) { diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 13e5ce6786..f5710124bb 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -1222,10 +1222,13 @@ void PaysStatus::setupHandlers() { ) | rpl::start_with_next([=] { const auto user = _user; const auto exception = [=](bool refund) { - using Flag = MTPaccount_AddNoPaidMessagesException::Flag; + using Flag = MTPaccount_ToggleNoPaidMessagesException::Flag; const auto api = &user->session().api(); - api->request(MTPaccount_AddNoPaidMessagesException( - MTP_flags(refund ? Flag::f_refund_charged : Flag()), + const auto require = false; + api->request(MTPaccount_ToggleNoPaidMessagesException( + MTP_flags((refund ? Flag::f_refund_charged : Flag()) + | (require ? Flag::f_require_payment : Flag())), + MTPInputPeer(), // parent_peer // #TODO monoforum user->inputUser )).done([=] { user->clearPaysPerMessage(); @@ -1268,6 +1271,8 @@ void PaysStatus::setupHandlers() { }, box->lifetime()); user->session().api().request(MTPaccount_GetPaidMessagesRevenue( + MTP_flags(0), + MTPInputPeer(), // parent_peer // #TODO monoforum user->inputUser )).done(crl::guard(_inner, [=]( const MTPaccount_PaidMessagesRevenue &result) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index fc937d352b..eacd073501 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -46,6 +46,7 @@ inputMediaDice#e66fbf7b emoticon:string = InputMedia; inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia; inputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector<InputMedia> payload:flags.0?string = InputMedia; +inputMediaTodo#9fc55fde todo:TodoList = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; @@ -117,7 +118,7 @@ chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:f messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message; -messageService#d3d28540 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; +messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; @@ -136,6 +137,7 @@ messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int sto messageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector<long> countries_iso2:flags.1?Vector<string> prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia; messageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector<long> months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia; messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector<MessageExtendedMedia> = MessageMedia; +messageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector<TodoCompletion> = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction; @@ -188,6 +190,8 @@ messageActionStarGiftUnique#2e3ae60e flags:# upgrade:flags.0?true transferred:fl messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction; messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction; messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction; +messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction; +messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?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; @@ -1407,9 +1411,9 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; -sponsoredMessage#4d93a990 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; +sponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage; -messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages; +messages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages; messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages; searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod; @@ -1715,7 +1719,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; -monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog; +monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs; @@ -1983,6 +1987,12 @@ stories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount pendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion; +todoItem#cba9a52f id:int title:TextWithEntities = TodoItem; + +todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList; + +todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2134,8 +2144,8 @@ account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings; account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings; account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; -account.addNoPaidMessagesException#6f688aa7 flags:# refund_charged:flags.0?true user_id:InputUser = Bool; -account.getPaidMessagesRevenue#f1266f38 user_id:InputUser = account.PaidMessagesRevenue; +account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue; +account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getFullUser#b60f5918 id:InputUser = users.UserFull; @@ -2390,13 +2400,15 @@ messages.getPaidReactionPrivacy#472455aa = Updates; messages.viewSponsoredMessage#269e3643 random_id:bytes = Bool; messages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool; messages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult; -messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages; +messages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages; messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector<InlineQueryPeerType> = messages.BotPreparedInlineMessage; messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool; messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector<InputPeer> = messages.SavedDialogs; messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; +messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates; +messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2714,4 +2726,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 204 +// LAYER 205 From a97d1b86699bda8f994792f94d5c329beed0c34f Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 6 Jun 2025 18:24:44 +0400 Subject: [PATCH 174/310] Support task lists view/update/actions. --- Telegram/CMakeLists.txt | 6 + Telegram/Resources/langs/lang.strings | 19 +- Telegram/SourceFiles/api/api_todo_lists.cpp | 204 +++++ Telegram/SourceFiles/api/api_todo_lists.h | 56 ++ Telegram/SourceFiles/api/api_updates.cpp | 4 +- Telegram/SourceFiles/apiwrap.cpp | 13 +- Telegram/SourceFiles/apiwrap.h | 3 + .../SourceFiles/data/data_media_types.cpp | 67 ++ Telegram/SourceFiles/data/data_media_types.h | 28 + Telegram/SourceFiles/data/data_session.cpp | 103 ++- Telegram/SourceFiles/data/data_session.h | 27 +- Telegram/SourceFiles/data/data_todo_list.cpp | 232 ++++++ Telegram/SourceFiles/data/data_todo_list.h | 79 ++ Telegram/SourceFiles/data/data_types.h | 2 + Telegram/SourceFiles/data/data_web_page.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 23 + Telegram/SourceFiles/history/history_item.cpp | 131 +++- Telegram/SourceFiles/history/history_item.h | 8 + .../history/history_item_components.cpp | 65 ++ .../history/history_item_components.h | 21 + .../history/view/history_view_element.cpp | 29 +- .../history/view/history_view_element.h | 7 +- .../view/history_view_service_message.cpp | 42 +- .../history/view/media/history_view_media.cpp | 4 + .../history/view/media/history_view_media.h | 8 + .../view/media/history_view_todo_list.cpp | 712 ++++++++++++++++++ .../view/media/history_view_todo_list.h | 131 ++++ 27 files changed, 1983 insertions(+), 43 deletions(-) create mode 100644 Telegram/SourceFiles/api/api_todo_lists.cpp create mode 100644 Telegram/SourceFiles/api/api_todo_lists.h create mode 100644 Telegram/SourceFiles/data/data_todo_list.cpp create mode 100644 Telegram/SourceFiles/data/data_todo_list.h create mode 100644 Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp create mode 100644 Telegram/SourceFiles/history/view/media/history_view_todo_list.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 3184b11ae4..d23e37b829 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -178,6 +178,8 @@ PRIVATE api/api_statistics_sender.h api/api_text_entities.cpp api/api_text_entities.h + api/api_todo_lists.cpp + api/api_todo_lists.h api/api_toggling_media.cpp api/api_toggling_media.h api/api_transcribes.cpp @@ -649,6 +651,8 @@ PRIVATE data/data_streaming.h data/data_thread.cpp data/data_thread.h + data/data_todo_list.cpp + data/data_todo_list.h data/data_types.cpp data/data_types.h data/data_unread_value.cpp @@ -812,6 +816,8 @@ PRIVATE history/view/media/history_view_story_mention.h history/view/media/history_view_theme_document.cpp history/view/media/history_view_theme_document.h + history/view/media/history_view_todo_list.cpp + history/view/media/history_view_todo_list.h history/view/media/history_view_unique_gift.cpp history/view/media/history_view_unique_gift.h history/view/media/history_view_userpic_suggestion.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d1a9784970..205656b57e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2257,8 +2257,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_action_message_price_paid#other" = "Messages now cost {count} Stars each in this group."; "lng_action_direct_messages_enabled" = "Channel enabled Direct Messages."; "lng_action_direct_messages_paid#one" = "Channel allows Direct Messages for {count} Star each."; -"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each"; +"lng_action_direct_messages_paid#other" = "Channel allows Direct Messages for {count} Stars each."; "lng_action_direct_messages_disabled" = "Channel disabled Direct Messages."; +"lng_action_todo_marked_done" = "{from} marked {tasks} as done."; +"lng_action_todo_marked_done_self" = "You marked {tasks} as done."; +"lng_action_todo_marked_not_done" = "{from} marked {tasks} as not done."; +"lng_action_todo_marked_not_done_self" = "You marked {tasks} as not done."; +"lng_action_todo_added" = "{from} added {tasks} to the list."; +"lng_action_todo_added_self" = "You added {tasks} to the list."; +"lng_action_todo_tasks_fallback#one" = "task"; +"lng_action_todo_tasks_fallback#other" = "{count} tasks"; +"lng_action_todo_tasks_and_one" = "{tasks}, {task}"; +"lng_action_todo_tasks_and_last" = "{tasks} and {task}"; "lng_you_paid_stars#one" = "You paid {count} Star."; "lng_you_paid_stars#other" = "You paid {count} Stars."; @@ -3791,6 +3801,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_in_dlg_sticker_emoji" = "{emoji} Sticker"; "lng_in_dlg_poll" = "Poll"; "lng_in_dlg_story" = "Story"; +"lng_in_dlg_todo_list" = "To-Do List"; "lng_in_dlg_story_expired" = "Expired story"; "lng_in_dlg_media_count#one" = "{count} media"; "lng_in_dlg_media_count#other" = "{count} media"; @@ -5844,6 +5855,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_polls_show_more#other" = "Show more ({count})"; "lng_polls_votes_collapse" = "Collapse"; +"lng_todo_title" = "To-Do List"; +"lng_todo_title_group" = "Group To-Do List"; +"lng_todo_completed#one" = "{count} of {total} completed"; +"lng_todo_completed#other" = "{count} of {total} completed"; +"lng_todo_completed_none" = "None of {total} completed"; + "lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM."; "lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM."; "lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}."; diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp new file mode 100644 index 0000000000..dc5a7841cd --- /dev/null +++ b/Telegram/SourceFiles/api/api_todo_lists.cpp @@ -0,0 +1,204 @@ +/* +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 "api/api_todo_lists.h" + +//#include "api/api_common.h" +//#include "api/api_updates.h" +#include "apiwrap.h" +//#include "base/random.h" +//#include "data/business/data_shortcut_messages.h" +//#include "data/data_changes.h" +//#include "data/data_histories.h" +#include "data/data_todo_list.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +//#include "history/history_item_helpers.h" // ShouldSendSilent +#include "main/main_session.h" + +namespace Api { +namespace { + +constexpr auto kSendTogglesDelay = 3 * crl::time(1000); + +[[nodiscard]] TimeId UnixtimeFromMsgId(mtpMsgId msgId) { + return TimeId(msgId >> 32); +} + +} // namespace + +TodoLists::TodoLists(not_null<ApiWrap*> api) +: _session(&api->session()) +, _api(&api->instance()) +, _sendTimer([=] { sendAccumulatedToggles(false); }) { +} +// +//void TodoLists::create( +// const PollData &data, +// SendAction action, +// Fn<void()> done, +// Fn<void()> fail) { +// _session->api().sendAction(action); +// +// const auto history = action.history; +// const auto peer = history->peer; +// const auto topicRootId = action.replyTo.messageId +// ? action.replyTo.topicRootId +// : 0; +// const auto monoforumPeerId = action.replyTo.monoforumPeerId; +// auto sendFlags = MTPmessages_SendMedia::Flags(0); +// if (action.replyTo) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; +// } +// const auto clearCloudDraft = action.clearDraft; +// if (clearCloudDraft) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; +// history->clearLocalDraft(topicRootId, monoforumPeerId); +// history->clearCloudDraft(topicRootId, monoforumPeerId); +// history->startSavingCloudDraft(topicRootId, monoforumPeerId); +// } +// const auto silentPost = ShouldSendSilent(peer, action.options); +// const auto starsPaid = std::min( +// peer->starsPerMessageChecked(), +// action.options.starsApproved); +// if (silentPost) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_silent; +// } +// if (action.options.scheduled) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; +// } +// if (action.options.shortcutId) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; +// } +// if (action.options.effectId) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_effect; +// } +// if (starsPaid) { +// action.options.starsApproved -= starsPaid; +// sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; +// } +// const auto sendAs = action.options.sendAs; +// if (sendAs) { +// sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; +// } +// auto &histories = history->owner().histories(); +// const auto randomId = base::RandomValue<uint64>(); +// histories.sendPreparedMessage( +// history, +// action.replyTo, +// randomId, +// Data::Histories::PrepareMessage<MTPmessages_SendMedia>( +// MTP_flags(sendFlags), +// peer->input, +// Data::Histories::ReplyToPlaceholder(), +// PollDataToInputMedia(&data), +// MTP_string(), +// MTP_long(randomId), +// MTPReplyMarkup(), +// MTPVector<MTPMessageEntity>(), +// MTP_int(action.options.scheduled), +// (sendAs ? sendAs->input : MTP_inputPeerEmpty()), +// Data::ShortcutIdToMTP(_session, action.options.shortcutId), +// MTP_long(action.options.effectId), +// MTP_long(starsPaid) +// ), [=](const MTPUpdates &result, const MTP::Response &response) { +// if (clearCloudDraft) { +// history->finishSavingCloudDraft( +// topicRootId, +// monoforumPeerId, +// UnixtimeFromMsgId(response.outerMsgId)); +// } +// _session->changes().historyUpdated( +// history, +// (action.options.scheduled +// ? Data::HistoryUpdate::Flag::ScheduledSent +// : Data::HistoryUpdate::Flag::MessageSent)); +// done(); +// }, [=](const MTP::Error &error, const MTP::Response &response) { +// if (clearCloudDraft) { +// history->finishSavingCloudDraft( +// topicRootId, +// monoforumPeerId, +// UnixtimeFromMsgId(response.outerMsgId)); +// } +// fail(); +// }); +//} + +void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) { + auto &entry = _toggles[itemId]; + if (completed) { + if (!entry.completed.emplace(id).second) { + return; + } + } else { + if (!entry.incompleted.emplace(id).second) { + return; + } + } + entry.scheduled = crl::now(); + if (!entry.requestId && !_sendTimer.isActive()) { + _sendTimer.callOnce(kSendTogglesDelay); + } +} + +void TodoLists::sendAccumulatedToggles(bool force) { + const auto now = crl::now(); + auto nearest = crl::time(0); + for (auto &[itemId, entry] : _toggles) { + if (entry.requestId) { + continue; + } + const auto wait = entry.scheduled + kSendTogglesDelay - now; + if (wait <= 0) { + entry.scheduled = 0; + send(itemId, entry); + } else if (!nearest || nearest > wait) { + nearest = wait; + } + } + if (nearest > 0) { + _sendTimer.callOnce(nearest); + } +} + +void TodoLists::send(FullMsgId itemId, Accumulated &entry) { + const auto item = _session->data().message(itemId); + if (!item) { + return; + } + auto completed = entry.completed + | ranges::views::transform([](int id) { return MTP_int(id); }); + auto incompleted = entry.incompleted + | ranges::views::transform([](int id) { return MTP_int(id); }); + entry.requestId = _api.request(MTPmessages_ToggleTodoCompleted( + item->history()->peer->input, + MTP_int(item->id), + MTP_vector_from_range(completed), + MTP_vector_from_range(incompleted) + )).done([=](const MTPUpdates &result) { + _session->api().applyUpdates(result); + finishRequest(itemId); + }).fail([=](const MTP::Error &error) { + finishRequest(itemId); + }).send(); + entry.completed.clear(); + entry.incompleted.clear(); +} + +void TodoLists::finishRequest(FullMsgId itemId) { + auto &entry = _toggles[itemId]; + entry.requestId = 0; + if (entry.completed.empty() && entry.incompleted.empty()) { + _toggles.remove(itemId); + } else { + sendAccumulatedToggles(false); + } +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_todo_lists.h b/Telegram/SourceFiles/api/api_todo_lists.h new file mode 100644 index 0000000000..49891ea0bf --- /dev/null +++ b/Telegram/SourceFiles/api/api_todo_lists.h @@ -0,0 +1,56 @@ +/* +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/timer.h" +#include "mtproto/sender.h" + +class ApiWrap; +class HistoryItem; +struct TodoListData; + +namespace Main { +class Session; +} // namespace Main + +namespace Api { + +struct SendAction; + +class TodoLists final { +public: + explicit TodoLists(not_null<ApiWrap*> api); + + //void create( + // const PollData &data, + // SendAction action, + // Fn<void()> done, + // Fn<void()> fail); + void toggleCompletion(FullMsgId itemId, int id, bool completed); + +private: + struct Accumulated { + base::flat_set<int> completed; + base::flat_set<int> incompleted; + crl::time scheduled = 0; + mtpRequestId requestId = 0; + }; + + void sendAccumulatedToggles(bool force); + void send(FullMsgId itemId, Accumulated &entry); + void finishRequest(FullMsgId itemId); + + const not_null<Main::Session*> _session; + MTP::Sender _api; + + base::flat_map<FullMsgId, Accumulated> _toggles; + base::Timer _sendTimer; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index c3928b6073..aa3934e4d2 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1916,7 +1916,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { // Update web page anyway. session().data().processWebpage(d.vwebpage()); - session().data().sendWebPageGamePollNotifications(); + session().data().sendWebPageGamePollTodoListNotifications(); updateAndApply(d.vpts().v, d.vpts_count().v, update); } break; @@ -1926,7 +1926,7 @@ void Updates::feedUpdate(const MTPUpdate &update) { // Update web page anyway. session().data().processWebpage(d.vwebpage()); - session().data().sendWebPageGamePollNotifications(); + session().data().sendWebPageGamePollTodoListNotifications(); auto channel = session().data().channelLoaded(d.vchannel_id()); if (channel && !_handlingChannelDifference) { diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 4f8cef0a22..cbbbf0e65a 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_polls.h" #include "api/api_sending.h" #include "api/api_text_entities.h" +#include "api/api_todo_lists.h" #include "api/api_self_destruct.h" #include "api/api_sensitive_content.h" #include "api/api_global_privacy.h" @@ -178,6 +179,7 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session) , _confirmPhone(std::make_unique<Api::ConfirmPhone>(this)) , _peerPhoto(std::make_unique<Api::PeerPhoto>(this)) , _polls(std::make_unique<Api::Polls>(this)) +, _todoLists(std::make_unique<Api::TodoLists>(this)) , _chatParticipants(std::make_unique<Api::ChatParticipants>(this)) , _unreadThings(std::make_unique<Api::UnreadThings>(this)) , _ringtones(std::make_unique<Api::Ringtones>(this)) @@ -2574,7 +2576,10 @@ void ApiWrap::refreshFileReference( }); } -void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &result, mtpRequestId req) { +void ApiWrap::gotWebPages( + ChannelData *channel, + const MTPmessages_Messages &result, + mtpRequestId req) { WebPageData::ApplyChanges(_session, channel, result); for (auto i = _webPagesPending.begin(); i != _webPagesPending.cend();) { if (i->second == req) { @@ -2588,7 +2593,7 @@ void ApiWrap::gotWebPages(ChannelData *channel, const MTPmessages_Messages &resu ++i; } } - _session->data().sendWebPageGamePollNotifications(); + _session->data().sendWebPageGamePollTodoListNotifications(); } void ApiWrap::updateStickers() { @@ -4792,6 +4797,10 @@ Api::Polls &ApiWrap::polls() { return *_polls; } +Api::TodoLists &ApiWrap::todoLists() { + return *_todoLists; +} + Api::ChatParticipants &ApiWrap::chatParticipants() { return *_chatParticipants; } diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index cb6ed34c2d..5eabc79096 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -77,6 +77,7 @@ class ConfirmPhone; class PeerPhoto; class PeerColors; class Polls; +class TodoLists; class ChatParticipants; class UnreadThings; class Ringtones; @@ -413,6 +414,7 @@ public: [[nodiscard]] Api::ConfirmPhone &confirmPhone(); [[nodiscard]] Api::PeerPhoto &peerPhoto(); [[nodiscard]] Api::Polls &polls(); + [[nodiscard]] Api::TodoLists &todoLists(); [[nodiscard]] Api::ChatParticipants &chatParticipants(); [[nodiscard]] Api::UnreadThings &unreadThings(); [[nodiscard]] Api::Ringtones &ringtones(); @@ -764,6 +766,7 @@ private: const std::unique_ptr<Api::ConfirmPhone> _confirmPhone; const std::unique_ptr<Api::PeerPhoto> _peerPhoto; const std::unique_ptr<Api::Polls> _polls; + const std::unique_ptr<Api::TodoLists> _todoLists; const std::unique_ptr<Api::ChatParticipants> _chatParticipants; const std::unique_ptr<Api::UnreadThings> _unreadThings; const std::unique_ptr<Api::Ringtones> _ringtones; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index cd0f169ec2..38f10e8885 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_web_page.h" #include "history/view/media/history_view_poll.h" #include "history/view/media/history_view_theme_document.h" +#include "history/view/media/history_view_todo_list.h" #include "history/view/media/history_view_slot_machine.h" #include "history/view/media/history_view_dice.h" #include "history/view/media/history_view_service_box.h" @@ -65,6 +66,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_stories.h" #include "data/data_story.h" +#include "data/data_todo_list.h" #include "data/data_user.h" #include "main/main_session.h" #include "main/main_session_settings.h" @@ -645,6 +647,10 @@ PollData *Media::poll() const { return nullptr; } +TodoListData *Media::todolist() const { + return nullptr; +} + const WallPaper *Media::paper() const { return nullptr; } @@ -2315,6 +2321,67 @@ std::unique_ptr<HistoryView::Media> MediaPoll::createView( return std::make_unique<HistoryView::Poll>(message, _poll); } +MediaTodoList::MediaTodoList( + not_null<HistoryItem*> parent, + not_null<TodoListData*> todolist) +: Media(parent) +, _todolist(todolist) { +} + +MediaTodoList::~MediaTodoList() { +} + +std::unique_ptr<Media> MediaTodoList::clone(not_null<HistoryItem*> parent) { + return std::make_unique<MediaTodoList>(parent, _todolist); +} + +TodoListData *MediaTodoList::todolist() const { + return _todolist; +} + +TextWithEntities MediaTodoList::notificationText() const { + return TextWithEntities() + .append(QChar(0x2611)) + .append(QChar(' ')) + .append(Ui::Text::Colorized(_todolist->title)); +} + +QString MediaTodoList::pinnedTextSubstring() const { + return QChar(171) + _todolist->title.text + QChar(187); +} + +TextForMimeData MediaTodoList::clipboardText() const { + auto result = TextWithEntities(); + result + .append(u"[ "_q) + .append(tr::lng_in_dlg_todo_list(tr::now)) + .append(u" : "_q) + .append(_todolist->title) + .append(u" ]"_q); + for (const auto &item : _todolist->items) { + result.append(u"\n- "_q).append(item.text); + } + return TextForMimeData::Rich(std::move(result)); +} + +bool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) { + return false; +} + +bool MediaTodoList::updateSentMedia(const MTPMessageMedia &media) { + return false; +} + +std::unique_ptr<HistoryView::Media> MediaTodoList::createView( + not_null<HistoryView::Element*> message, + not_null<HistoryItem*> realParent, + HistoryView::Element *replacing) { + return std::make_unique<HistoryView::TodoList>( + message, + _todolist, + replacing); +} + MediaDice::MediaDice(not_null<HistoryItem*> parent, QString emoji, int value) : Media(parent) , _emoji(emoji) diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index a8a0a34ac9..fd4cc54d33 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -196,6 +196,7 @@ public: virtual const GiftCode *gift() const; virtual CloudImage *location() const; virtual PollData *poll() const; + virtual TodoListData *todolist() const; virtual const WallPaper *paper() const; virtual bool paperForBoth() const; virtual FullStoryId storyId() const; @@ -610,6 +611,33 @@ private: }; +class MediaTodoList final : public Media { +public: + MediaTodoList( + not_null<HistoryItem*> parent, + not_null<TodoListData*> todolist); + ~MediaTodoList(); + + std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override; + + TodoListData *todolist() const override; + + TextWithEntities notificationText() const override; + QString pinnedTextSubstring() const override; + TextForMimeData clipboardText() const override; + + bool updateInlineResultMedia(const MTPMessageMedia &media) override; + bool updateSentMedia(const MTPMessageMedia &media) override; + std::unique_ptr<HistoryView::Media> createView( + not_null<HistoryView::Element*> message, + not_null<HistoryItem*> realParent, + HistoryView::Element *replacing = nullptr) override; + +private: + not_null<TodoListData*> _todolist; + +}; + class MediaDice final : public Media { public: MediaDice(not_null<HistoryItem*> parent, QString emoji, int value); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 292dffc405..b173fe8b18 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -75,6 +75,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_premium_limits.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" +#include "data/data_todo_list.h" #include "base/platform/base_platform_info.h" #include "base/unixtime.h" #include "base/call_delayed.h" @@ -1710,6 +1711,16 @@ void Session::requestPollViewRepaint(not_null<const PollData*> poll) { } } +void Session::requestTodoListViewRepaint( + not_null<const TodoListData*> todolist) { + if (const auto i = _todoListViews.find(todolist) + ; i != _todoListViews.end()) { + for (const auto &view : i->second) { + requestViewResize(view); + } + } +} + void Session::documentLoadProgress(not_null<DocumentData*> document) { requestDocumentViewRepaint(document); _documentLoadProgress.fire_copy(document); @@ -4098,6 +4109,39 @@ not_null<PollData*> Session::processPoll(const MTPDmessageMediaPoll &data) { return result; } +not_null<TodoListData*> Session::todoList(TodoListId id) { + auto i = _todoLists.find(id); + if (i == _todoLists.cend()) { + i = _todoLists.emplace( + id, + std::make_unique<TodoListData>(this, id)).first; + } + return i->second.get(); +} + +not_null<TodoListData*> Session::processTodoList( + TodoListId id, + const MTPTodoList &todolist) { + const auto &data = todolist.data(); + const auto result = todoList(id); + const auto changed = result->applyChanges(data); + if (changed) { + notifyTodoListUpdateDelayed(result); + } + return result; +} + +not_null<TodoListData*> Session::processTodoList( + TodoListId id, + const MTPDmessageMediaToDo &data) { + const auto result = processTodoList(id, data.vtodo()); + const auto changed = result->applyCompletions(data.vcompletions()); + if (changed) { + notifyTodoListUpdateDelayed(result); + } + return result; +} + void Session::checkPollsClosings() { const auto now = base::unixtime::now(); auto closest = 0; @@ -4308,6 +4352,24 @@ void Session::unregisterPollView( } } +void Session::registerTodoListView( + not_null<const TodoListData*> todolist, + not_null<ViewElement*> view) { + _todoListViews[todolist].insert(view); +} + +void Session::unregisterTodoListView( + not_null<const TodoListData*> todolist, + not_null<ViewElement*> view) { + const auto i = _todoListViews.find(todolist); + if (i != _todoListViews.end()) { + auto &items = i->second; + if (items.remove(view) && items.empty()) { + _todoListViews.erase(i); + } + } +} + void Session::registerContactView( UserId contactId, not_null<ViewElement*> view) { @@ -4488,37 +4550,54 @@ QString Session::findContactPhone(UserId contactId) const { return QString(); } -bool Session::hasPendingWebPageGamePollNotification() const { +bool Session::hasPendingWebPageGamePollTodoListNotification() const { return !_webpagesUpdated.empty() || !_gamesUpdated.empty() - || !_pollsUpdated.empty(); + || !_pollsUpdated.empty() + || !_todoListsUpdated.empty(); } void Session::notifyWebPageUpdateDelayed(not_null<WebPageData*> page) { - const auto invoke = !hasPendingWebPageGamePollNotification(); + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); _webpagesUpdated.insert(page); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); } } void Session::notifyGameUpdateDelayed(not_null<GameData*> game) { - const auto invoke = !hasPendingWebPageGamePollNotification(); + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); _gamesUpdated.insert(game); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); } } void Session::notifyPollUpdateDelayed(not_null<PollData*> poll) { - const auto invoke = !hasPendingWebPageGamePollNotification(); + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); _pollsUpdated.insert(poll); if (invoke) { - crl::on_main(_session, [=] { sendWebPageGamePollNotifications(); }); + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); } } -void Session::sendWebPageGamePollNotifications() { +void Session::notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist) { + const auto invoke = !hasPendingWebPageGamePollTodoListNotification(); + _todoListsUpdated.insert(todolist); + if (invoke) { + crl::on_main(_session, [=] { + sendWebPageGamePollTodoListNotifications(); + }); + } +} + +void Session::sendWebPageGamePollTodoListNotifications() { auto resize = std::vector<not_null<ViewElement*>>(); for (const auto &page : base::take(_webpagesUpdated)) { _webpageUpdates.fire_copy(page); @@ -4537,6 +4616,12 @@ void Session::sendWebPageGamePollNotifications() { resize.insert(end(resize), begin(i->second), end(i->second)); } } + for (const auto &todolist : base::take(_todoListsUpdated)) { + if (const auto i = _todoListViews.find(todolist) + ; i != _todoListViews.end()) { + resize.insert(end(resize), begin(i->second), end(i->second)); + } + } for (const auto &view : resize) { requestViewResize(view); } diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 2ac7d93d75..62083465bf 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -536,6 +536,7 @@ public: void requestDocumentViewRepaint(not_null<const DocumentData*> document); void markMediaRead(not_null<const DocumentData*> document); void requestPollViewRepaint(not_null<const PollData*> poll); + void requestTodoListViewRepaint(not_null<const TodoListData*> todolist); void photoLoadProgress(not_null<PhotoData*> photo); void photoLoadDone(not_null<PhotoData*> photo); @@ -690,6 +691,14 @@ public: not_null<PollData*> processPoll(const MTPPoll &data); not_null<PollData*> processPoll(const MTPDmessageMediaPoll &data); + [[nodiscard]] not_null<TodoListData*> todoList(TodoListId id); + not_null<TodoListData*> processTodoList( + TodoListId id, + const MTPTodoList &todolist); + not_null<TodoListData*> processTodoList( + TodoListId id, + const MTPDmessageMediaToDo &data); + [[nodiscard]] not_null<CloudImage*> location( const LocationPoint &point); @@ -729,6 +738,12 @@ public: void unregisterPollView( not_null<const PollData*> poll, not_null<ViewElement*> view); + void registerTodoListView( + not_null<const TodoListData*> todolist, + not_null<ViewElement*> view); + void unregisterTodoListView( + not_null<const TodoListData*> todolist, + not_null<ViewElement*> view); void registerContactView( UserId contactId, not_null<ViewElement*> view); @@ -758,8 +773,9 @@ public: void notifyWebPageUpdateDelayed(not_null<WebPageData*> page); void notifyGameUpdateDelayed(not_null<GameData*> game); void notifyPollUpdateDelayed(not_null<PollData*> poll); - [[nodiscard]] bool hasPendingWebPageGamePollNotification() const; - void sendWebPageGamePollNotifications(); + void notifyTodoListUpdateDelayed(not_null<TodoListData*> todolist); + [[nodiscard]] bool hasPendingWebPageGamePollTodoListNotification() const; + void sendWebPageGamePollTodoListNotifications(); [[nodiscard]] rpl::producer<not_null<WebPageData*>> webPageUpdates() const; void channelDifferenceTooLong(not_null<ChannelData*> channel); @@ -1066,6 +1082,9 @@ private: std::unordered_map< PollId, std::unique_ptr<PollData>> _polls; + std::map< + TodoListId, + std::unique_ptr<TodoListData>> _todoLists; std::unordered_map< GameId, std::unique_ptr<GameData>> _games; @@ -1078,6 +1097,9 @@ private: std::unordered_map< not_null<const PollData*>, base::flat_set<not_null<ViewElement*>>> _pollViews; + std::unordered_map< + not_null<const TodoListData*>, + base::flat_set<not_null<ViewElement*>>> _todoListViews; std::unordered_map< UserId, base::flat_set<not_null<HistoryItem*>>> _contactItems; @@ -1094,6 +1116,7 @@ private: base::flat_set<not_null<WebPageData*>> _webpagesUpdated; base::flat_set<not_null<GameData*>> _gamesUpdated; base::flat_set<not_null<PollData*>> _pollsUpdated; + base::flat_set<not_null<TodoListData*>> _todoListsUpdated; rpl::event_stream<not_null<WebPageData*>> _webpageUpdates; rpl::event_stream<not_null<ChannelData*>> _channelDifferenceTooLong; diff --git a/Telegram/SourceFiles/data/data_todo_list.cpp b/Telegram/SourceFiles/data/data_todo_list.cpp new file mode 100644 index 0000000000..1bb1a86859 --- /dev/null +++ b/Telegram/SourceFiles/data/data_todo_list.cpp @@ -0,0 +1,232 @@ +/* +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 "data/data_todo_list.h" + +#include "api/api_text_entities.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "base/call_delayed.h" +#include "history/history_item.h" +#include "main/main_session.h" +#include "api/api_text_entities.h" +#include "ui/text/text_options.h" + +namespace { + +constexpr auto kShortPollTimeout = 30 * crl::time(1000); + +const TodoListItem *ItemById(const std::vector<TodoListItem> &list, int id) { + const auto i = ranges::find(list, id, &TodoListItem::id); + return (i != end(list)) ? &*i : nullptr; +} + +TodoListItem *ItemById(std::vector<TodoListItem> &list, int id) { + return const_cast<TodoListItem*>(ItemById(std::as_const(list), id)); +} + +} // namespace + +TodoListData::TodoListData(not_null<Data::Session*> owner, TodoListId id) +: id(id) +, _owner(owner) { +} + +Data::Session &TodoListData::owner() const { + return *_owner; +} + +Main::Session &TodoListData::session() const { + return _owner->session(); +} + +bool TodoListData::applyChanges(const MTPDtodoList &todolist) { + const auto newTitle = TextWithEntities{ + .text = qs(todolist.vtitle().data().vtext()), + .entities = Api::EntitiesFromMTP( + &session(), + todolist.vtitle().data().ventities().v), + }; + const auto newFlags = (todolist.is_others_can_append() + ? Flag::OthersCanAppend + : Flag()) + | (todolist.is_others_can_complete() ? Flag::OthersCanComplete + : Flag()); + auto newItems = ranges::views::all( + todolist.vlist().v + ) | ranges::views::transform([&](const MTPTodoItem &item) { + return TodoListItemFromMTP(&session(), item); + }) | ranges::views::take( + kMaxOptions + ) | ranges::to_vector; + + const auto changed1 = (title != newTitle) || (_flags != newFlags); + const auto changed2 = (items != newItems); + if (!changed1 && !changed2) { + return false; + } + if (changed1) { + title = newTitle; + _flags = newFlags; + } + if (changed2) { + std::swap(items, newItems); + for (const auto &old : newItems) { + if (const auto current = itemById(old.id)) { + current->completedBy = old.completedBy; + current->completionDate = old.completionDate; + } + } + } + ++version; + return true; +} + +bool TodoListData::applyCompletions( + const MTPVector<MTPTodoCompletion> *completions) { + auto changed = false; + const auto lookup = [&](int id) { + if (!completions) { + return (const MTPDtodoCompletion*)nullptr; + } + const auto proj = [](const MTPTodoCompletion &completion) { + return completion.data().vid().v; + }; + const auto i = ranges::find(completions->v, id, proj); + return (i != completions->v.end()) ? &i->data() : nullptr; + }; + for (auto &item : items) { + const auto completion = lookup(item.id); + const auto by = (completion && completion->vcompleted_by().v) + ? owner().user(UserId(completion->vcompleted_by().v)).get() + : nullptr; + const auto date = completion ? completion->vdate().v : TimeId(); + if (item.completedBy != by || item.completionDate != date) { + item.completedBy = by; + item.completionDate = date; + changed = true; + } + } + if (changed) { + ++version; + } + return changed; +} + +void TodoListData::apply( + not_null<HistoryItem*> item, + const MTPDmessageActionTodoCompletions &data) { + for (const auto &id : data.vcompleted().v) { + if (const auto task = itemById(id.v)) { + task->completedBy = item->from(); + task->completionDate = item->date(); + } + } + for (const auto &id : data.vincompleted().v) { + if (const auto task = itemById(id.v)) { + task->completedBy = nullptr; + task->completionDate = TimeId(); + } + } + owner().notifyTodoListUpdateDelayed(this); +} + +void TodoListData::apply(const MTPDmessageActionTodoAppendTasks &data) { + const auto limit = TodoListData::kMaxOptions; + for (const auto &task : data.vlist().v) { + if (items.size() < limit) { + const auto parsed = TodoListItemFromMTP( + &session(), + task); + if (!itemById(parsed.id)) { + items.push_back(std::move(parsed)); + } + } + } + owner().notifyTodoListUpdateDelayed(this); +} + +TodoListItem *TodoListData::itemById(int id) { + return ItemById(items, id); +} + +const TodoListItem *TodoListData::itemById(int id) const { + return ItemById(items, id); +} + +void TodoListData::setFlags(Flags flags) { + if (_flags != flags) { + _flags = flags; + ++version; + } +} + +TodoListData::Flags TodoListData::flags() const { + return _flags; +} + +bool TodoListData::othersCanAppend() const { + return (_flags & Flag::OthersCanAppend); +} + +bool TodoListData::othersCanComplete() const { + return (_flags & Flag::OthersCanComplete); +} + +MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) { + const auto convert = [&](const TodoListItem &item) { + return MTP_todoItem( + MTP_int(item.id), + MTP_textWithEntities( + MTP_string(item.text.text), + Api::EntitiesToMTP( + &todolist->session(), + item.text.entities))); + }; + auto items = QVector<MTPTodoItem>(); + items.reserve(todolist->items.size()); + ranges::transform( + todolist->items, + ranges::back_inserter(items), + convert); + using Flag = MTPDtodoList::Flag; + const auto flags = Flag() + | (todolist->othersCanAppend() + ? Flag::f_others_can_append + : Flag()) + | (todolist->othersCanComplete() + ? Flag::f_others_can_complete + : Flag()); + return MTP_todoList( + MTP_flags(flags), + MTP_textWithEntities( + MTP_string(todolist->title.text), + Api::EntitiesToMTP( + &todolist->session(), + todolist->title.entities)), + MTP_vector<MTPTodoItem>(items)); +} + +MTPInputMedia TodoListDataToInputMedia( + not_null<const TodoListData*> todolist) { + return MTP_inputMediaTodo(TodoListDataToMTP(todolist)); +} + +TodoListItem TodoListItemFromMTP( + not_null<Main::Session*> session, + const MTPTodoItem &item) { + const auto &data = item.data(); + return { + .text = TextWithEntities{ + .text = qs(data.vtitle().data().vtext()), + .entities = Api::EntitiesFromMTP( + session, + data.vtitle().data().ventities().v), + }, + .id = data.vid().v, + }; +} diff --git a/Telegram/SourceFiles/data/data_todo_list.h b/Telegram/SourceFiles/data/data_todo_list.h new file mode 100644 index 0000000000..3ba84c6c78 --- /dev/null +++ b/Telegram/SourceFiles/data/data_todo_list.h @@ -0,0 +1,79 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Data { +class Session; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +struct TodoListItem { + TextWithEntities text; + PeerData *completedBy = nullptr; + TimeId completionDate = 0; + int id = 0; + + friend inline bool operator==( + const TodoListItem &, + const TodoListItem &) = default; +}; + +struct TodoListData { + TodoListData(not_null<Data::Session*> owner, TodoListId id); + + [[nodiscard]] Data::Session &owner() const; + [[nodiscard]] Main::Session &session() const; + + enum class Flag { + OthersCanAppend = 0x01, + OthersCanComplete = 0x02, + }; + friend inline constexpr bool is_flag_type(Flag) { return true; }; + using Flags = base::flags<Flag>; + + bool applyChanges(const MTPDtodoList &todolist); + bool applyCompletions(const MTPVector<MTPTodoCompletion> *completions); + + void apply( + not_null<HistoryItem*> item, + const MTPDmessageActionTodoCompletions &data); + void apply(const MTPDmessageActionTodoAppendTasks &data); + + [[nodiscard]] TodoListItem *itemById(int id); + [[nodiscard]] const TodoListItem *itemById(int id) const; + + void setFlags(Flags flags); + [[nodiscard]] Flags flags() const; + [[nodiscard]] bool othersCanAppend() const; + [[nodiscard]] bool othersCanComplete() const; + + TodoListId id; + TextWithEntities title; + std::vector<TodoListItem> items; + int version = 0; + + static constexpr auto kMaxOptions = 32; + +private: + bool applyCompletionToItems(const MTPTodoCompletion *result); + + const not_null<Data::Session*> _owner; + Flags _flags = Flags(); + +}; + +[[nodiscard]] MTPTodoList TodoListDataToMTP( + not_null<const TodoListData*> todolist); +[[nodiscard]] MTPInputMedia TodoListDataToInputMedia( + not_null<const TodoListData*> todolist); +[[nodiscard]] TodoListItem TodoListItemFromMTP( + not_null<Main::Session*> session, + const MTPTodoItem &item); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index c1ed9c42f7..c8f373a4f6 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -128,6 +128,7 @@ struct WebPageData; struct GameData; struct BotAppData; struct PollData; +struct TodoListData; using PhotoId = uint64; using VideoId = uint64; @@ -136,6 +137,7 @@ using DocumentId = uint64; using WebPageId = uint64; using GameId = uint64; using PollId = uint64; +using TodoListId = FullMsgId; using WallPaperId = uint64; using CallId = uint64; using BotAppId = uint64; diff --git a/Telegram/SourceFiles/data/data_web_page.cpp b/Telegram/SourceFiles/data/data_web_page.cpp index 74db4d6b5a..508a45d7c0 100644 --- a/Telegram/SourceFiles/data/data_web_page.cpp +++ b/Telegram/SourceFiles/data/data_web_page.cpp @@ -372,7 +372,7 @@ void WebPageData::ApplyChanges( }, [&](const auto &) { }); } - session->data().sendWebPageGamePollNotifications(); + session->data().sendWebPageGamePollTodoListNotifications(); } QString WebPageData::displayedSiteName() const { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 40ba51d43e..367cde6421 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_histories.h" #include "data/data_history_messages.h" +#include "data/data_todo_list.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "api/api_chat_participants.h" @@ -1331,6 +1332,28 @@ void History::applyServiceChanges( Core::App().calls().showConferenceInvite(user, item->id); } } + }, [&](const MTPDmessageActionTodoCompletions &data) { + if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + const auto list = done->msg + ? done->msg + : owner().message(peer, done->msgId); + if (const auto media = list ? list->media() : nullptr) { + if (const auto todolist = media->todolist()) { + todolist->apply(item, data); + } + } + } + }, [&](const MTPDmessageActionTodoAppendTasks &data) { + if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + const auto list = done->msg + ? done->msg + : owner().message(peer, done->msgId); + if (const auto media = list ? list->media() : nullptr) { + if (const auto todolist = media->todolist()) { + todolist->apply(data); + } + } + } }, [](const auto &) { }); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0808d03fd3..18dfcba072 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -62,6 +62,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_group_call.h" // Data::GroupCall::id(). #include "data/data_poll.h" // PollData::publicVotes. +#include "data/data_todo_list.h" #include "data/data_stories.h" #include "data/data_web_page.h" #include "chat_helpers/stickers_gift_box_pack.h" @@ -358,7 +359,9 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia( item, item->history()->owner().processPoll(media)); }, [&](const MTPDmessageMediaToDo &media) -> Result { - return nullptr; // #TODO todo + return std::make_unique<Data::MediaTodoList>( + item, + item->history()->owner().processTodoList(item->fullId(), media)); }, [&](const MTPDmessageMediaDice &media) -> Result { return std::make_unique<Data::MediaDice>( item, @@ -820,6 +823,10 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { return same; } else if (const auto results = Get<HistoryServiceGiveawayResults>()) { return results; + } else if (const auto done = Get<HistoryServiceTodoCompletions>()) { + return done; + } else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) { + return append; } return nullptr; } @@ -877,6 +884,10 @@ void HistoryItem::updateDependentServiceText() { updateServiceText(prepareGameScoreText()); } else if (Has<HistoryServicePayment>()) { updateServiceText(preparePaymentSentText()); + } else if (Has<HistoryServiceTodoCompletions>()) { + updateServiceText(prepareTodoCompletionsText()); + } else if (Has<HistoryServiceTodoAppendTasks>()) { + updateServiceText(prepareTodoAppendTasksText()); } } @@ -4528,12 +4539,32 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { refund->transactionId = qs(data.vcharge().data().vid()); const auto id = fullId(); refund->link = std::make_shared<LambdaClickHandler>([=]( - ClickContext context) { + ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); if (const auto window = my.sessionWindow.get()) { Settings::ShowRefundInfoBox(window, id); } }); + } else if (type == mtpc_messageActionTodoCompletions) { + const auto &data = action.c_messageActionTodoCompletions(); + UpdateComponents(HistoryServiceTodoCompletions::Bit()); + const auto done = Get<HistoryServiceTodoCompletions>(); + done->completed = data.vcompleted().v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; + done->incompleted = data.vincompleted().v + | ranges::views::transform(&MTPint::v) + | ranges::to_vector; + } else if (type == mtpc_messageActionTodoAppendTasks) { + const auto session = &_history->session(); + const auto &data = action.c_messageActionTodoAppendTasks(); + UpdateComponents(HistoryServiceTodoAppendTasks::Bit()); + const auto append = Get<HistoryServiceTodoAppendTasks>(); + append->list = ranges::views::all( + data.vlist().v + ) | ranges::views::transform([&](const MTPTodoItem &item) { + return TodoListItemFromMTP(session, item); + }) | ranges::to_vector; } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -5874,14 +5905,12 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; - auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &action) { - auto result = PreparedServiceText(); // #TODO todo - return result; + auto prepareTodoCompletions = [&](const MTPDmessageActionTodoCompletions &) { + return prepareTodoCompletionsText(); }; - auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &action) { - auto result = PreparedServiceText(); // #TODO todo - return result; + auto prepareTodoAppendTasks = [&](const MTPDmessageActionTodoAppendTasks &) { + return prepareTodoAppendTasksText(); }; auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { @@ -6549,6 +6578,92 @@ PreparedServiceText HistoryItem::prepareCallScheduledText( return result; } +PreparedServiceText HistoryItem::composeTodoIncompleted( + not_null<HistoryServiceTodoCompletions*> done) { + const auto tasks = ComposeTodoTasksList(done->msg, done->incompleted); + if (out()) { + return { + tr::lng_action_todo_marked_not_done_self( + tr::now, + lt_tasks, + tasks, + Ui::Text::WithEntities), + }; + } + return { + .text = tr::lng_action_todo_marked_not_done( + tr::now, + lt_from, + fromLinkText(), + lt_tasks, + tasks, + Ui::Text::WithEntities), + .links = { fromLink() }, + }; +} + +PreparedServiceText HistoryItem::composeTodoCompleted( + not_null<HistoryServiceTodoCompletions*> done) { + const auto tasks = ComposeTodoTasksList(done->msg, done->completed); + if (out()) { + return { + tr::lng_action_todo_marked_done_self( + tr::now, + lt_tasks, + tasks, + Ui::Text::WithEntities), + }; + } + return { + .text = tr::lng_action_todo_marked_done( + tr::now, + lt_from, + fromLinkText(), + lt_tasks, + tasks, + Ui::Text::WithEntities), + .links = { fromLink() }, + }; +} + +PreparedServiceText HistoryItem::prepareTodoCompletionsText() { + auto result = PreparedServiceText(); + const auto done = Get<HistoryServiceTodoCompletions>(); + Assert(done != nullptr); + + return done->completed.empty() + ? composeTodoIncompleted(done) + : composeTodoCompleted(done); +} + +PreparedServiceText HistoryItem::prepareTodoAppendTasksText() { + auto result = PreparedServiceText(); + auto append = Get<HistoryServiceTodoAppendTasks>(); + Assert(append != nullptr); + + const auto tasks = ComposeTodoTasksList(append); + if (out()) { + return { + tr::lng_action_todo_added_self( + tr::now, + lt_tasks, + tasks, + Ui::Text::WithEntities), + }; + } + return { + .text = tr::lng_action_todo_added( + tr::now, + lt_from, + fromLinkText(), + lt_tasks, + tasks, + Ui::Text::WithEntities), + .links = { fromLink() }, + }; + return result; +} + TextWithEntities HistoryItem::fromLinkText() const { return Ui::Text::Link(st::wrap_rtl(_from->name()), 1); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 55904615ce..d869944333 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -23,6 +23,7 @@ struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; struct HistoryMessageForwarded; struct HistoryServiceDependentData; +struct HistoryServiceTodoCompletions; enum class HistorySelfDestructType; struct PreparedServiceText; struct MessageFactcheck; @@ -644,6 +645,13 @@ private: CallId linkCallId); [[nodiscard]] PreparedServiceText prepareCallScheduledText( TimeId scheduleDate); + [[nodiscard]] PreparedServiceText prepareTodoCompletionsText(); + [[nodiscard]] PreparedServiceText prepareTodoAppendTasksText(); + + [[nodiscard]] PreparedServiceText composeTodoIncompleted( + not_null<HistoryServiceTodoCompletions*> done); + [[nodiscard]] PreparedServiceText composeTodoCompleted( + not_null<HistoryServiceTodoCompletions*> done); [[nodiscard]] PreparedServiceText prepareServiceTextForMessage( const MTPMessageMedia &media, diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index edb6a099da..f4381d5756 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -48,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_click_handler.h" #include "data/data_session.h" #include "data/data_stories.h" +#include "data/data_todo_list.h" #include "main/main_session.h" #include "window/window_session_controller.h" #include "api/api_bot.h" @@ -70,6 +71,38 @@ base::options::toggle FastButtonsModeOption({ .description = "Trigger inline keyboard buttons by 1-9 keyboard keys.", }); +[[nodiscard]] TextWithEntities ComposeTodoTasksList( + int fullCount, + const std::vector<TextWithEntities> &names) { + const auto count = int(names.size()); + if (!count) { + return tr::lng_action_todo_tasks_fallback( + tr::now, + lt_count, + fullCount, + Ui::Text::WithEntities); + } else if (count == 1) { + return names.front(); + } + auto full = names.front(); + for (auto i = 1; i != count - 1; ++i) { + full = tr::lng_action_todo_tasks_and_one( + tr::now, + lt_tasks, + full, + lt_task, + names[i], + Ui::Text::WithEntities); + } + return tr::lng_action_todo_tasks_and_last( + tr::now, + lt_tasks, + full, + lt_task, + names.back(), + Ui::Text::WithEntities); +} + } // namespace const char kOptionFastButtonsMode[] = "fast-buttons-mode"; @@ -1225,6 +1258,38 @@ MessageFactcheck FromMTP( return result; } +TextWithEntities ComposeTodoTasksList( + HistoryItem *itemWithList, + const std::vector<int> &ids) { + const auto media = itemWithList ? itemWithList->media() : nullptr; + const auto list = media ? media->todolist() : nullptr; + auto names = std::vector<TextWithEntities>(); + if (list) { + names.reserve(ids.size()); + for (const auto &id : ids) { + const auto i = ranges::find(list->items, id, &TodoListItem::id); + if (i == end(list->items)) { + names.clear(); + break; + } + names.push_back( + TextWithEntities().append('"').append(i->text).append('"')); + } + } + return ComposeTodoTasksList(ids.size(), names); +} + +TextWithEntities ComposeTodoTasksList( + not_null<HistoryServiceTodoAppendTasks*> append) { + auto names = std::vector<TextWithEntities>(); + names.reserve(append->list.size()); + for (const auto &task : append->list) { + names.push_back( + TextWithEntities().append('"').append(task.text).append('"')); + } + return ComposeTodoTasksList(names.size(), names); +} + HistoryDocumentCaptioned::HistoryDocumentCaptioned() : caption(st::msgFileMinWidth - st::msgPadding.left() - st::msgPadding.right()) { } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 6ec434a967..7050237255 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bubble.h" struct WebPageData; +struct TodoListItem; class VoiceSeekClickHandler; class ReplyKeyboard; @@ -661,6 +662,26 @@ struct HistoryServiceTopicInfo } }; +struct HistoryServiceTodoCompletions +: RuntimeComponent<HistoryServiceTodoCompletions, HistoryItem> +, HistoryServiceDependentData { + std::vector<int> completed; + std::vector<int> incompleted; +}; + +[[nodiscard]] TextWithEntities ComposeTodoTasksList( + HistoryItem *itemWithList, + const std::vector<int> &ids); + +struct HistoryServiceTodoAppendTasks +: RuntimeComponent<HistoryServiceTodoAppendTasks, HistoryItem> +, HistoryServiceDependentData { + std::vector<TodoListItem> list; +}; + +[[nodiscard]] TextWithEntities ComposeTodoTasksList( + not_null<HistoryServiceTodoAppendTasks*> append); + struct HistoryServiceGameScore : RuntimeComponent<HistoryServiceGameScore, HistoryItem> , HistoryServiceDependentData { diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 3e7377f7ea..cd5d3bc339 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -598,12 +598,15 @@ void MonoforumSenderBar::Paint( }); } -void ServicePreMessage::init(PreparedServiceText string) { +void ServicePreMessage::init( + PreparedServiceText string, + ClickHandlerPtr fullClickHandler) { text = Ui::Text::String( st::serviceTextStyle, string.text, kMarkupTextOptions, st::msgMinWidth); + handler = std::move(fullClickHandler); for (auto i = 0; i != int(string.links.size()); ++i) { text.setLink(i + 1, string.links[i]); } @@ -687,10 +690,16 @@ ClickHandlerPtr ServicePreMessage::textState( if (trect.contains(point)) { auto textRequest = request.forText(); textRequest.align = style::al_center; - return text.getState( + const auto link = text.getState( point - trect.topLeft(), trect.width(), textRequest).link; + if (link) { + return link; + } + } + if (handler && rect.contains(point)) { + return handler; } return {}; } @@ -1282,6 +1291,16 @@ void Element::validateText() { ? _textItem->customTextLinks() : contextDependentText.links; setTextWithLinks(markedText, customLinks); + + if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + if (!done->completed.empty() && !done->incompleted.empty()) { + setServicePreMessage( + item->composeTodoIncompleted(done), + done->lnk); + } else { + setServicePreMessage({}); + } + } } else { const auto unavailable = item->computeUnavailableReason(); if (!unavailable.isEmpty()) { @@ -1606,11 +1625,13 @@ void Element::setDisplayDate(bool displayDate) { } } -void Element::setServicePreMessage(PreparedServiceText text) { +void Element::setServicePreMessage( + PreparedServiceText text, + ClickHandlerPtr fullClickHandler) { if (!text.text.empty()) { AddComponents(ServicePreMessage::Bit()); const auto service = Get<ServicePreMessage>(); - service->init(std::move(text)); + service->init(std::move(text), std::move(fullClickHandler)); setPendingResize(); } else if (Has<ServicePreMessage>()) { RemoveComponents(ServicePreMessage::Bit()); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index ea4d9f0f19..a27dcae59d 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -309,7 +309,7 @@ private: // Any HistoryView::Element can have this Component for // displaying some text in layout of a service message above the message. struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> { - void init(PreparedServiceText string); + void init(PreparedServiceText string, ClickHandlerPtr fullClickHandler); int resizeToWidth(int newWidth, ElementChatMode mode); @@ -324,6 +324,7 @@ struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> { QRect g) const; Ui::Text::String text; + ClickHandlerPtr handler; int width = 0; int height = 0; @@ -456,7 +457,9 @@ public: // For blocks context this should be called only from recountDisplayDate(). void setDisplayDate(bool displayDate); - void setServicePreMessage(PreparedServiceText text); + void setServicePreMessage( + PreparedServiceText text, + ClickHandlerPtr fullClickHandler = nullptr); bool computeIsAttachToPrevious(not_null<Element*> previous); diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 02470dc925..c02bdd099c 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_abstract_structure.h" #include "data/data_chat.h" #include "data/data_channel.h" +#include "data/data_todo_list.h" #include "info/profile/info_profile_cover.h" #include "ui/chat/chat_style.h" #include "ui/effects/reaction_fly_animation.h" @@ -448,16 +449,14 @@ void Service::animateReaction(Ui::ReactionFlyAnimationArgs &&args) { } QSize Service::performCountCurrentSize(int newWidth) { - auto newHeight = displayedDateHeight(); - if (const auto bar = Get<UnreadBar>()) { - newHeight += bar->height(); - } - if (const auto monoforumBar = Get<MonoforumSenderBar>()) { - newHeight += monoforumBar->height(); - } + auto newHeight = marginTop(); data()->resolveDependent(); + if (const auto service = Get<ServicePreMessage>()) { + service->resizeToWidth(newWidth, delegate()->elementChatMode()); + } + if (isHidden()) { return { newWidth, newHeight }; } @@ -465,9 +464,7 @@ QSize Service::performCountCurrentSize(int newWidth) { const auto mediaDisplayed = media && media->isDisplayed(); auto contentWidth = newWidth; if (mediaDisplayed && media->hideServiceText()) { - newHeight += st::msgServiceMargin.top() - + media->resizeGetHeight(newWidth) - + st::msgServiceMargin.bottom(); + newHeight += media->resizeGetHeight(newWidth) + marginBottom(); } else if (!text().isEmpty()) { if (delegate()->elementChatMode() == ElementChatMode::Wide) { accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); @@ -481,12 +478,15 @@ QSize Service::performCountCurrentSize(int newWidth) { newHeight += (contentWidth >= maxWidth()) ? minHeight() : textHeightFor(nwidth); - newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom(); + newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom(); if (mediaDisplayed) { const auto mediaWidth = std::min(media->maxWidth(), nwidth); newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(mediaWidth); } + newHeight += marginBottom(); + } else { + newHeight -= st::msgServiceMargin.top(); } if (_reactions) { @@ -523,7 +523,7 @@ bool Service::isHidden() const { } int Service::marginTop() const { - auto result = st::msgServiceMargin.top(); + auto result = isHidden() ? 0 : st::msgServiceMargin.top(); result += displayedDateHeight(); if (const auto bar = Get<UnreadBar>()) { result += bar->height(); @@ -531,6 +531,9 @@ int Service::marginTop() const { if (const auto monoforumBar = Get<MonoforumSenderBar>()) { result += monoforumBar->height(); } + if (const auto service = Get<ServicePreMessage>()) { + result += service->height; + } return result; } @@ -566,6 +569,10 @@ void Service::draw(Painter &p, const PaintContext &context) const { } } + if (const auto service = Get<ServicePreMessage>()) { + service->paint(p, context, g, delegate()->elementChatMode()); + } + if (isHidden()) { return; } @@ -667,6 +674,13 @@ TextState Service::textState(QPoint point, StateRequest request) const { return result; } + if (const auto service = Get<ServicePreMessage>()) { + result.link = service->textState(point, request, g); + if (result.link) { + return result; + } + } + if (_reactions) { const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height(); const auto reactionsLeft = 0; @@ -724,6 +738,10 @@ TextState Service::textState(QPoint point, StateRequest request) const { result.link = custom->link; } else if (const auto payment = item->Get<HistoryServicePaymentRefund>()) { result.link = payment->link; + } else if (const auto done = item->Get<HistoryServiceTodoCompletions>()) { + result.link = done->lnk; + } else if (const auto append = item->Get<HistoryServiceTodoAppendTasks>()) { + result.link = append->lnk; } else if (media && data()->showSimilarChannels()) { result = media->textState(mediaPoint, request); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index 1d3554972a..8741c51f69 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -589,6 +589,10 @@ QImage Media::locationTakeImage() { return QImage(); } +std::vector<Media::TodoTaskInfo> Media::takeTasksInfo() { + return {}; +} + TextState Media::getStateGrouped( const QRect &geometry, RectParts sides, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 844f0465c4..adf66c7c3d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -209,6 +209,14 @@ public: not_null<DocumentData*> data, const Lottie::ColorReplacements *replacements); virtual QImage locationTakeImage(); + + struct TodoTaskInfo { + int id = 0; + PeerData *completedBy = nullptr; + TimeId completionDate = TimeId(); + }; + virtual std::vector<TodoTaskInfo> takeTasksInfo(); + virtual void checkAnimation() { } diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp new file mode 100644 index 0000000000..af6ad3c56f --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -0,0 +1,712 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/media/history_view_todo_list.h" + +#include "base/unixtime.h" +#include "core/ui_integration.h" // TextContext +#include "lang/lang_keys.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/view/history_view_message.h" +#include "history/view/history_view_cursor_state.h" +#include "calls/calls_instance.h" +#include "ui/chat/message_bubble.h" +#include "ui/chat/chat_style.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" +#include "ui/text/format_values.h" +#include "ui/effects/animations.h" +#include "ui/effects/radial_animation.h" +#include "ui/effects/ripple_animation.h" +#include "ui/toast/toast.h" +#include "ui/painter.h" +#include "data/data_media_types.h" +#include "data/data_poll.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "base/unixtime.h" +#include "base/timer.h" +#include "main/main_session.h" +#include "apiwrap.h" +#include "api/api_todo_lists.h" +#include "styles/style_chat.h" +#include "styles/style_widgets.h" +#include "styles/style_window.h" + +namespace HistoryView { +namespace { + +constexpr auto kShowRecentVotersCount = 3; +constexpr auto kRotateSegments = 8; +constexpr auto kRotateAmplitude = 3.; +constexpr auto kScaleSegments = 2; +constexpr auto kScaleAmplitude = 0.03; +constexpr auto kLargestRadialDuration = 30 * crl::time(1000); +constexpr auto kCriticalCloseDuration = 5 * crl::time(1000); + +} // namespace + +struct TodoList::Task { + Task(); + + void fillData( + not_null<TodoListData*> todolist, + const TodoListItem &original, + Ui::Text::MarkedContext context); + + Ui::Text::String text; + PeerData *completedBy = nullptr; + mutable Ui::PeerUserpicView userpic; + TimeId completionDate = 0; + int id = 0; + ClickHandlerPtr handler; + Ui::Animations::Simple selectedAnimation; + mutable std::unique_ptr<Ui::RippleAnimation> ripple; +}; + +TodoList::Task::Task() : text(st::msgMinWidth / 2) { +} + +void TodoList::Task::fillData( + not_null<TodoListData*> todolist, + const TodoListItem &original, + Ui::Text::MarkedContext context) { + id = original.id; + if (original.completedBy) { + completedBy = original.completedBy; + } + completionDate = original.completionDate; + if (!text.isEmpty() && text.toTextWithEntities() == original.text) { + return; + } + text.setMarkedText( + st::historyPollAnswerStyle, + original.text, + Ui::WebpageTextTitleOptions(), + context); +} + +TodoList::TodoList( + not_null<Element*> parent, + not_null<TodoListData*> todolist, + Element *replacing) +: Media(parent) +, _todolist(todolist) +, _title(st::msgMinWidth / 2) { + history()->owner().registerTodoListView(_todolist, _parent); + if (const auto media = replacing ? replacing->media() : nullptr) { + const auto info = media->takeTasksInfo(); + if (!info.empty()) { + setupPreviousState(info); + } + } +} + +void TodoList::setupPreviousState(const std::vector<TodoTaskInfo> &info) { + // If we restore state from the view we're replacing we'll be able to + // animate the changes properly. + updateTasks(true); + for (auto &task : _tasks) { + const auto i = ranges::find(info, task.id, &TodoTaskInfo::id); + if (i != end(info)) { + task.completedBy = i->completedBy; + task.completionDate = i->completionDate; + } + } +} + +QSize TodoList::countOptimalSize() { + updateTexts(); + + const auto paddings = st::msgPadding.left() + st::msgPadding.right(); + + auto maxWidth = st::msgFileMinWidth; + accumulate_max(maxWidth, paddings + _title.maxWidth()); + for (const auto &task : _tasks) { + accumulate_max( + maxWidth, + paddings + + st::historyPollAnswerPadding.left() + + task.text.maxWidth() + + st::historyPollAnswerPadding.right()); + } + + const auto tasksHeight = ranges::accumulate(ranges::views::all( + _tasks + ) | ranges::views::transform([](const Task &task) { + return st::historyPollAnswerPadding.top() + + task.text.minHeight() + + st::historyPollAnswerPadding.bottom(); + }), 0); + + const auto bottomButtonHeight = st::historyPollBottomButtonSkip; + auto minHeight = st::historyPollQuestionTop + + _title.minHeight() + + st::historyPollSubtitleSkip + + st::msgDateFont->height + + st::historyPollAnswersSkip + + tasksHeight + + st::historyPollTotalVotesSkip + + bottomButtonHeight + + st::msgDateFont->height + + st::msgPadding.bottom(); + if (!isBubbleTop()) { + minHeight -= st::msgFileTopMinus; + } + return { maxWidth, minHeight }; +} + +bool TodoList::canComplete() const { + return (_parent->data()->out() || _todolist->othersCanComplete()) + && _parent->data()->isRegular(); +} + +int TodoList::countTaskTop( + const Task &task, + int innerWidth) const { + auto tshift = st::historyPollQuestionTop; + if (!isBubbleTop()) { + tshift -= st::msgFileTopMinus; + } + tshift += _title.countHeight(innerWidth) + st::historyPollSubtitleSkip; + tshift += st::msgDateFont->height + st::historyPollAnswersSkip; + const auto i = ranges::find( + _tasks, + &task, + [](const Task &task) { return &task; }); + const auto countHeight = [&](const Task &task) { + return countTaskHeight(task, innerWidth); + }; + tshift += ranges::accumulate( + begin(_tasks), + i, + 0, + ranges::plus(), + countHeight); + return tshift; +} + +int TodoList::countTaskHeight( + const Task &task, + int innerWidth) const { + const auto answerWidth = innerWidth + - st::historyPollAnswerPadding.left() + - st::historyPollAnswerPadding.right(); + return st::historyPollAnswerPadding.top() + + task.text.countHeight(answerWidth) + + st::historyPollAnswerPadding.bottom(); +} + +QSize TodoList::countCurrentSize(int newWidth) { + accumulate_min(newWidth, maxWidth()); + const auto innerWidth = newWidth + - st::msgPadding.left() + - st::msgPadding.right(); + + const auto tasksHeight = ranges::accumulate(ranges::views::all( + _tasks + ) | ranges::views::transform([&](const Task &task) { + return countTaskHeight(task, innerWidth); + }), 0); + + const auto bottomButtonHeight = st::historyPollBottomButtonSkip; + auto newHeight = st::historyPollQuestionTop + + _title.countHeight(innerWidth) + + st::historyPollSubtitleSkip + + st::msgDateFont->height + + st::historyPollAnswersSkip + + tasksHeight + + st::historyPollTotalVotesSkip + + bottomButtonHeight + + st::msgDateFont->height + + st::msgPadding.bottom(); + if (!isBubbleTop()) { + newHeight -= st::msgFileTopMinus; + } + return { newWidth, newHeight }; +} + +void TodoList::updateTexts() { + if (_todoListVersion == _todolist->version) { + return; + } + const auto skipAnimations = _tasks.empty(); + _todoListVersion = _todolist->version; + + if (_title.toTextWithEntities() != _todolist->title) { + auto options = Ui::WebpageTextTitleOptions(); + options.maxw = options.maxh = 0; + _title.setMarkedText( + st::historyPollQuestionStyle, + _todolist->title, + options, + Core::TextContext({ + .session = &_todolist->session(), + .repaint = [=] { repaint(); }, + .customEmojiLoopLimit = 2, + })); + } + if (_flags != _todolist->flags() || _subtitle.isEmpty()) { + using Flag = PollData::Flag; + _flags = _todolist->flags(); + _subtitle.setText( + st::msgDateTextStyle, + (parent()->history()->peer->isUser() + ? tr::lng_todo_title(tr::now) + : tr::lng_todo_title_group(tr::now))); + } + updateTasks(skipAnimations); +} + +void TodoList::updateTasks(bool skipAnimations) { + const auto context = Core::TextContext({ + .session = &_todolist->session(), + .repaint = [=] { repaint(); }, + .customEmojiLoopLimit = 2, + }); + const auto changed = !ranges::equal( + _tasks, + _todolist->items, + ranges::equal_to(), + &Task::id, + &TodoListItem::id); + if (!changed) { + auto &&tasks = ranges::views::zip(_tasks, _todolist->items); + for (auto &&[task, original] : tasks) { + const auto wasDate = task.completionDate; + task.fillData(_todolist, original, context); + if (!skipAnimations && (!wasDate != !task.completionDate)) { + startToggleAnimation(task); + } + } + return; + } + _tasks = ranges::views::all( + _todolist->items + ) | ranges::views::transform([&](const TodoListItem &item) { + auto result = Task(); + result.id = item.id; + result.fillData(_todolist, item, context); + return result; + }) | ranges::to_vector; + + for (auto &task : _tasks) { + task.handler = createTaskClickHandler(task); + } +} + +ClickHandlerPtr TodoList::createTaskClickHandler( + const Task &task) { + const auto id = task.id; + return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] { + toggleCompletion(id); + })); +} + +void TodoList::startToggleAnimation(Task &task) { + const auto selected = (task.completionDate != 0); + task.selectedAnimation.start( + [=] { repaint(); }, + selected ? 0. : 1., + selected ? 1. : 0., + st::defaultCheck.duration); +} + +void TodoList::toggleCompletion(int id) { + const auto i = ranges::find( + _tasks, + id, + &Task::id); + if (i == end(_tasks)) { + return; + } + const auto selected = (i->completionDate != 0); + i->completionDate = selected ? TimeId() : base::unixtime::now(); + if (!selected) { + i->completedBy = _parent->history()->session().user(); + } + startToggleAnimation(*i); + repaint(); + + history()->session().api().todoLists().toggleCompletion( + _parent->data()->fullId(), + id, + !selected); +} + +void TodoList::updateCompletionStatus() { + const auto incompleted = int(ranges::count( + _todolist->items, + nullptr, + &TodoListItem::completedBy)); + const auto total = int(_todolist->items.size()); + if (_total == total + && _incompleted == incompleted + && !_completionStatusLabel.isEmpty()) { + return; + } + _total = total; + _incompleted = incompleted; + const auto totalText = QString::number(total); + const auto string = (incompleted == total) + ? tr::lng_todo_completed_none(tr::now, lt_total, totalText) + : tr::lng_todo_completed( + tr::now, + lt_count, + total - incompleted, + lt_total, + totalText); + _completionStatusLabel.setText(st::msgDateTextStyle, string); +} + +void TodoList::draw(Painter &p, const PaintContext &context) const { + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + auto paintw = width(); + + const auto stm = context.messageStyle(); + const auto padding = st::msgPadding; + auto tshift = st::historyPollQuestionTop; + if (!isBubbleTop()) { + tshift -= st::msgFileTopMinus; + } + paintw -= padding.left() + padding.right(); + + p.setPen(stm->historyTextFg); + _title.drawLeft(p, padding.left(), tshift, paintw, width(), style::al_left, 0, -1, context.selection); + tshift += _title.countHeight(paintw) + st::historyPollSubtitleSkip; + + p.setPen(stm->msgDateFg); + _subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width()); + tshift += st::msgDateFont->height + st::historyPollAnswersSkip; + + auto heavy = false; + auto created = false; + auto &&tasks = ranges::views::zip( + _tasks, + ranges::views::ints(0, int(_tasks.size()))); + for (const auto &[task, index] : tasks) { + const auto was = !task.userpic.null(); + const auto height = paintTask( + p, + task, + padding.left(), + tshift, + paintw, + width(), + context); + if (was) { + heavy = true; + } else if (!task.userpic.null()) { + created = true; + } + tshift += height; + } + if (!heavy && created) { + history()->owner().registerHeavyViewPart(_parent); + } + paintBottom(p, padding.left(), tshift, paintw, context); +} + +void TodoList::paintBottom( + Painter &p, + int left, + int top, + int paintw, + const PaintContext &context) const { + const auto stringtop = top + + st::msgPadding.bottom() + + st::historyPollBottomButtonTop; + const auto stm = context.messageStyle(); + + p.setPen(stm->msgDateFg); + _completionStatusLabel.draw(p, left, stringtop, paintw, style::al_top); +} + +void TodoList::radialAnimationCallback() const { + if (!anim::Disabled()) { + repaint(); + } +} + +int TodoList::paintTask( + Painter &p, + const Task &task, + int left, + int top, + int width, + int outerWidth, + const PaintContext &context) const { + const auto height = countTaskHeight(task, width); + const auto stm = context.messageStyle(); + const auto aleft = left + st::historyPollAnswerPadding.left(); + const auto awidth = width + - st::historyPollAnswerPadding.left() + - st::historyPollAnswerPadding.right(); + + if (task.ripple) { + p.setOpacity(st::historyPollRippleOpacity); + task.ripple->paint( + p, + left - st::msgPadding.left(), + top, + outerWidth, + &stm->msgWaveformInactive->c); + if (task.ripple->empty()) { + task.ripple.reset(); + } + p.setOpacity(1.); + } + + paintRadio(p, task, left, top, context); + + top += st::historyPollAnswerPadding.top(); + p.setPen(stm->historyTextFg); + task.text.drawLeft(p, aleft, top, awidth, outerWidth); + + return height; +} + +void TodoList::paintRadio( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const { + top += st::historyPollAnswerPadding.top(); + + const auto stm = context.messageStyle(); + + PainterHighQualityEnabler hq(p); + const auto &radio = st::historyPollRadio; + const auto over = ClickHandler::showAsActive(task.handler); + const auto ®ular = stm->msgDateFg; + + const auto checkmark = task.selectedAnimation.value( + task.completionDate ? 1. : 0.); + + const auto o = p.opacity(); + if (checkmark < 1.) { + p.setBrush(Qt::NoBrush); + p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity)); + } + + const auto rect = QRectF(left, top, radio.diameter, radio.diameter).marginsRemoved(QMarginsF(radio.thickness / 2., radio.thickness / 2., radio.thickness / 2., radio.thickness / 2.)); + if (checkmark > 0. && task.completedBy) { + const auto skip = st::lineWidth; + const auto userpic = QRect( + left + (radio.diameter / 2) + skip, + top + skip, + radio.diameter - 2 * skip, + radio.diameter - 2 * skip); + if (checkmark < 1.) { + p.save(); + p.setOpacity(checkmark); + p.translate(QRectF(userpic).center()); + const auto ratio = 0.4 + 0.6 * checkmark; + p.scale(ratio, ratio); + p.translate(-QRectF(userpic).center()); + } + task.completedBy->paintUserpic( + p, + task.userpic, + userpic.left(), + userpic.top(), + userpic.width()); + if (checkmark < 1.) { + p.restore(); + } + } + if (checkmark < 1.) { + auto pen = regular->p; + pen.setWidth(radio.thickness); + p.setPen(pen); + p.drawEllipse(rect); + } + + if (checkmark > 0.) { + const auto removeFull = (radio.diameter / 2 - radio.thickness); + const auto removeNow = removeFull * (1. - checkmark); + const auto color = stm->msgFileThumbLinkFg; + auto pen = color->p; + pen.setWidth(radio.thickness); + p.setPen(pen); + p.setBrush(color); + p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow })); + const auto &icon = stm->historyPollChosen; + icon.paint(p, left + (radio.diameter - icon.width()) / 2, top + (radio.diameter - icon.height()) / 2, width()); + + const auto stm = context.messageStyle(); + auto bgpen = stm->msgBg->p; + bgpen.setWidth(st::lineWidth); + const auto outline = QRect(left, top, radio.diameter, radio.diameter); + const auto paintContent = [&](QPainter &p) { + p.setPen(bgpen); + p.setBrush(Qt::NoBrush); + PainterHighQualityEnabler hq(p); + p.drawEllipse(outline); + }; + if (usesBubblePattern(context)) { + const auto add = st::lineWidth * 3; + const auto target = outline.marginsAdded( + { add, add, add, add }); + Ui::PaintPatternBubblePart( + p, + context.viewport, + context.bubblesPattern->pixmap, + target, + paintContent, + _userpicCircleCache); + } else { + paintContent(p); + } + } + + p.setOpacity(o); +} + +TextSelection TodoList::adjustSelection( + TextSelection selection, + TextSelectType type) const { + return _title.adjustSelection(selection, type); +} + +uint16 TodoList::fullSelectionLength() const { + return _title.length(); +} + +TextForMimeData TodoList::selectedText(TextSelection selection) const { + return _title.toTextForMimeData(selection); +} + +TextState TodoList::textState(QPoint point, StateRequest request) const { + auto result = TextState(_parent); + const auto can = canComplete(); + const auto padding = st::msgPadding; + auto paintw = width(); + auto tshift = st::historyPollQuestionTop; + if (!isBubbleTop()) { + tshift -= st::msgFileTopMinus; + } + paintw -= padding.left() + padding.right(); + + const auto questionH = _title.countHeight(paintw); + if (QRect(padding.left(), tshift, paintw, questionH).contains(point)) { + result = TextState(_parent, _title.getState( + point - QPoint(padding.left(), tshift), + paintw, + request.forText())); + return result; + } + tshift += questionH + st::historyPollSubtitleSkip; + tshift += st::msgDateFont->height + st::historyPollAnswersSkip; + for (const auto &task : _tasks) { + const auto height = countTaskHeight(task, paintw); + if (point.y() >= tshift && point.y() < tshift + height) { + if (can) { + _lastLinkPoint = point; + result.link = task.handler; + } else if (task.completionDate) { + result.customTooltip = true; + using Flag = Ui::Text::StateRequest::Flag; + if (request.flags & Flag::LookupCustomTooltip) { + result.customTooltipText = langDateTimeFull( + base::unixtime::parse(task.completionDate)); + } + } + return result; + } + tshift += height; + } + return result; +} + +void TodoList::clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) { + if (!handler) return; + + const auto i = ranges::find( + _tasks, + handler, + &Task::handler); + if (i != end(_tasks)) { + toggleRipple(*i, pressed); + } +} + +void TodoList::unloadHeavyPart() { + for (auto &task : _tasks) { + task.userpic = {}; + } +} + +bool TodoList::hasHeavyPart() const { + for (auto &task : _tasks) { + if (!task.userpic.null()) { + return true; + } + } + return false; +} + +std::vector<Media::TodoTaskInfo> TodoList::takeTasksInfo() { + if (_tasks.empty()) { + return {}; + } + return _tasks | ranges::views::transform([](const Task &task) { + return TodoTaskInfo{ + .id = task.id, + .completedBy = task.completedBy, + .completionDate = task.completionDate, + }; + }) | ranges::to_vector; +} + +void TodoList::toggleRipple(Task &task, bool pressed) { + if (pressed) { + const auto outerWidth = width(); + const auto innerWidth = outerWidth + - st::msgPadding.left() + - st::msgPadding.right(); + if (!task.ripple) { + auto mask = Ui::RippleAnimation::RectMask(QSize( + outerWidth, + countTaskHeight(task, innerWidth))); + task.ripple = std::make_unique<Ui::RippleAnimation>( + st::defaultRippleAnimation, + std::move(mask), + [=] { repaint(); }); + } + const auto top = countTaskTop(task, innerWidth); + task.ripple->add(_lastLinkPoint - QPoint(0, top)); + } else if (task.ripple) { + task.ripple->lastStop(); + } +} + +int TodoList::bottomButtonHeight() const { + const auto skip = st::historyPollChoiceRight.height() + - st::historyPollFillingBottom + - st::historyPollFillingHeight + - (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2; + return st::historyPollTotalVotesSkip + - skip + + st::historyPollBottomButtonSkip + + st::msgDateFont->height + + st::msgPadding.bottom(); +} + +TodoList::~TodoList() { + history()->owner().unregisterTodoListView(_todolist, _parent); + if (hasHeavyPart()) { + unloadHeavyPart(); + _parent->checkHeavyPart(); + } +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h new file mode 100644 index 0000000000..d0f25638b0 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.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 "history/view/media/history_view_media.h" +#include "ui/effects/animations.h" +#include "data/data_todo_list.h" +#include "base/weak_ptr.h" + +namespace Ui { +class RippleAnimation; +} // namespace Ui + +namespace HistoryView { + +class Message; + +class TodoList final : public Media { +public: + TodoList( + not_null<Element*> parent, + not_null<TodoListData*> todolist, + Element *replacing); + ~TodoList(); + + void draw(Painter &p, const PaintContext &context) const override; + TextState textState(QPoint point, StateRequest request) const override; + + bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override { + return true; + } + bool dragItemByHandler(const ClickHandlerPtr &p) const override { + return true; + } + + bool needsBubble() const override { + return true; + } + bool customInfoLayout() const override { + return false; + } + + [[nodiscard]] TextSelection adjustSelection( + TextSelection selection, + TextSelectType type) const override; + uint16 fullSelectionLength() const override; + TextForMimeData selectedText(TextSelection selection) const override; + + void clickHandlerPressedChanged( + const ClickHandlerPtr &handler, + bool pressed) override; + + void unloadHeavyPart() override; + bool hasHeavyPart() const override; + + std::vector<TodoTaskInfo> takeTasksInfo() override; + +private: + struct Task; + + QSize countOptimalSize() override; + QSize countCurrentSize(int newWidth) override; + + [[nodiscard]] bool canComplete() const; + + [[nodiscard]] int countTaskTop( + const Task &task, + int innerWidth) const; + [[nodiscard]] int countTaskHeight( + const Task &task, + int innerWidth) const; + [[nodiscard]] ClickHandlerPtr createTaskClickHandler( + const Task &task); + void updateTexts(); + void updateTasks(bool skipAnimations); + void startToggleAnimation(Task &task); + void updateCompletionStatus(); + void setupPreviousState(const std::vector<TodoTaskInfo> &info); + + int paintTask( + Painter &p, + const Task &task, + int left, + int top, + int width, + int outerWidth, + const PaintContext &context) const; + void paintRadio( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const; + void paintBottom( + Painter &p, + int left, + int top, + int paintw, + const PaintContext &context) const; + + void radialAnimationCallback() const; + + void toggleRipple(Task &task, bool pressed); + void toggleCompletion(int id); + + [[nodiscard]] int bottomButtonHeight() const; + + const not_null<TodoListData*> _todolist; + int _todoListVersion = 0; + int _total = 0; + int _incompleted = 0; + TodoListData::Flags _flags = TodoListData::Flags(); + + Ui::Text::String _title; + Ui::Text::String _subtitle; + + std::vector<Task> _tasks; + Ui::Text::String _completionStatusLabel; + + mutable QPoint _lastLinkPoint; + mutable QImage _userpicCircleCache; + mutable QImage _fillingIconCache; + +}; + +} // namespace HistoryView From 5666e84d92f2145eeb8760a75590c5f3b6753a2b Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 10 Jun 2025 18:16:48 +0400 Subject: [PATCH 175/310] Add ability to create todo lists. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 14 + Telegram/SourceFiles/api/api_todo_lists.cpp | 194 ++-- Telegram/SourceFiles/api/api_todo_lists.h | 10 +- .../SourceFiles/boxes/create_poll_box.cpp | 2 +- .../boxes/create_todo_list_box.cpp | 995 ++++++++++++++++++ .../SourceFiles/boxes/create_todo_list_box.h | 78 ++ Telegram/SourceFiles/data/data_peer.cpp | 4 + Telegram/SourceFiles/data/data_peer.h | 1 + .../view/history_view_top_bar_widget.cpp | 7 +- .../view/media/history_view_todo_list.cpp | 2 +- .../inline_bots/bot_attach_web_view.cpp | 20 + Telegram/SourceFiles/main/main_app_config.cpp | 16 +- Telegram/SourceFiles/main/main_app_config.h | 3 + Telegram/SourceFiles/ui/menu_icons.style | 1 + .../SourceFiles/window/window_peer_menu.cpp | 121 ++- .../SourceFiles/window/window_peer_menu.h | 6 + 17 files changed, 1365 insertions(+), 111 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/create_todo_list_box.cpp create mode 100644 Telegram/SourceFiles/boxes/create_todo_list_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index d23e37b829..e83797828e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -273,6 +273,8 @@ PRIVATE boxes/connection_box.h boxes/create_poll_box.cpp boxes/create_poll_box.h + boxes/create_todo_list_box.cpp + boxes/create_todo_list_box.h boxes/delete_messages_box.cpp boxes/delete_messages_box.h boxes/dictionaries_manager.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 205656b57e..80b008c240 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5860,6 +5860,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_todo_completed#one" = "{count} of {total} completed"; "lng_todo_completed#other" = "{count} of {total} completed"; "lng_todo_completed_none" = "None of {total} completed"; +"lng_todo_create" = "Create To-Do List"; +"lng_todo_create_title" = "New To-Do List"; +"lng_todo_create_title_placeholder" = "Title"; +"lng_todo_create_list" = "Tasks List"; +"lng_todo_create_list_add" = "Add a task..."; +"lng_todo_create_limit#one" = "You can add {count} more task."; +"lng_todo_create_limit#other" = "You can add {count} more tasks."; +"lng_todo_create_maximum" = "You have added the maximum number of tasks."; +"lng_todo_create_settings" = "Settings"; +"lng_todo_create_allow_add" = "Allow Others to Add Tasks"; +"lng_todo_create_allow_mark" = "Allow Others to Mark As Done"; +"lng_todo_create_button" = "Create"; +"lng_todo_choose_title" = "Please enter a title."; +"lng_todo_choose_tasks" = "Please enter at least one task."; "lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM."; "lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM."; diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp index dc5a7841cd..e72e8294ad 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.cpp +++ b/Telegram/SourceFiles/api/api_todo_lists.cpp @@ -10,15 +10,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL //#include "api/api_common.h" //#include "api/api_updates.h" #include "apiwrap.h" -//#include "base/random.h" -//#include "data/business/data_shortcut_messages.h" -//#include "data/data_changes.h" -//#include "data/data_histories.h" +#include "base/random.h" +#include "data/business/data_shortcut_messages.h" // ShortcutIdToMTP +#include "data/data_changes.h" +#include "data/data_histories.h" #include "data/data_todo_list.h" #include "data/data_session.h" #include "history/history.h" #include "history/history_item.h" -//#include "history/history_item_helpers.h" // ShouldSendSilent +#include "history/history_item_helpers.h" // ShouldSendSilent #include "main/main_session.h" namespace Api { @@ -37,98 +37,98 @@ TodoLists::TodoLists(not_null<ApiWrap*> api) , _api(&api->instance()) , _sendTimer([=] { sendAccumulatedToggles(false); }) { } -// -//void TodoLists::create( -// const PollData &data, -// SendAction action, -// Fn<void()> done, -// Fn<void()> fail) { -// _session->api().sendAction(action); -// -// const auto history = action.history; -// const auto peer = history->peer; -// const auto topicRootId = action.replyTo.messageId -// ? action.replyTo.topicRootId -// : 0; -// const auto monoforumPeerId = action.replyTo.monoforumPeerId; -// auto sendFlags = MTPmessages_SendMedia::Flags(0); -// if (action.replyTo) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; -// } -// const auto clearCloudDraft = action.clearDraft; -// if (clearCloudDraft) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; -// history->clearLocalDraft(topicRootId, monoforumPeerId); -// history->clearCloudDraft(topicRootId, monoforumPeerId); -// history->startSavingCloudDraft(topicRootId, monoforumPeerId); -// } -// const auto silentPost = ShouldSendSilent(peer, action.options); -// const auto starsPaid = std::min( -// peer->starsPerMessageChecked(), -// action.options.starsApproved); -// if (silentPost) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_silent; -// } -// if (action.options.scheduled) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; -// } -// if (action.options.shortcutId) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; -// } -// if (action.options.effectId) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_effect; -// } -// if (starsPaid) { -// action.options.starsApproved -= starsPaid; -// sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; -// } -// const auto sendAs = action.options.sendAs; -// if (sendAs) { -// sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; -// } -// auto &histories = history->owner().histories(); -// const auto randomId = base::RandomValue<uint64>(); -// histories.sendPreparedMessage( -// history, -// action.replyTo, -// randomId, -// Data::Histories::PrepareMessage<MTPmessages_SendMedia>( -// MTP_flags(sendFlags), -// peer->input, -// Data::Histories::ReplyToPlaceholder(), -// PollDataToInputMedia(&data), -// MTP_string(), -// MTP_long(randomId), -// MTPReplyMarkup(), -// MTPVector<MTPMessageEntity>(), -// MTP_int(action.options.scheduled), -// (sendAs ? sendAs->input : MTP_inputPeerEmpty()), -// Data::ShortcutIdToMTP(_session, action.options.shortcutId), -// MTP_long(action.options.effectId), -// MTP_long(starsPaid) -// ), [=](const MTPUpdates &result, const MTP::Response &response) { -// if (clearCloudDraft) { -// history->finishSavingCloudDraft( -// topicRootId, -// monoforumPeerId, -// UnixtimeFromMsgId(response.outerMsgId)); -// } -// _session->changes().historyUpdated( -// history, -// (action.options.scheduled -// ? Data::HistoryUpdate::Flag::ScheduledSent -// : Data::HistoryUpdate::Flag::MessageSent)); -// done(); -// }, [=](const MTP::Error &error, const MTP::Response &response) { -// if (clearCloudDraft) { -// history->finishSavingCloudDraft( -// topicRootId, -// monoforumPeerId, -// UnixtimeFromMsgId(response.outerMsgId)); -// } -// fail(); -// }); -//} + +void TodoLists::create( + const TodoListData &data, + SendAction action, + Fn<void()> done, + Fn<void()> fail) { + _session->api().sendAction(action); + + const auto history = action.history; + const auto peer = history->peer; + const auto topicRootId = action.replyTo.messageId + ? action.replyTo.topicRootId + : 0; + const auto monoforumPeerId = action.replyTo.monoforumPeerId; + auto sendFlags = MTPmessages_SendMedia::Flags(0); + if (action.replyTo) { + sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to; + } + const auto clearCloudDraft = action.clearDraft; + if (clearCloudDraft) { + sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft; + history->clearLocalDraft(topicRootId, monoforumPeerId); + history->clearCloudDraft(topicRootId, monoforumPeerId); + history->startSavingCloudDraft(topicRootId, monoforumPeerId); + } + const auto silentPost = ShouldSendSilent(peer, action.options); + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); + if (silentPost) { + sendFlags |= MTPmessages_SendMedia::Flag::f_silent; + } + if (action.options.scheduled) { + sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date; + } + if (action.options.shortcutId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut; + } + if (action.options.effectId) { + sendFlags |= MTPmessages_SendMedia::Flag::f_effect; + } + if (starsPaid) { + action.options.starsApproved -= starsPaid; + sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; + } + const auto sendAs = action.options.sendAs; + if (sendAs) { + sendFlags |= MTPmessages_SendMedia::Flag::f_send_as; + } + auto &histories = history->owner().histories(); + const auto randomId = base::RandomValue<uint64>(); + histories.sendPreparedMessage( + history, + action.replyTo, + randomId, + Data::Histories::PrepareMessage<MTPmessages_SendMedia>( + MTP_flags(sendFlags), + peer->input, + Data::Histories::ReplyToPlaceholder(), + TodoListDataToInputMedia(&data), + MTP_string(), + MTP_long(randomId), + MTPReplyMarkup(), + MTPVector<MTPMessageEntity>(), + MTP_int(action.options.scheduled), + (sendAs ? sendAs->input : MTP_inputPeerEmpty()), + Data::ShortcutIdToMTP(_session, action.options.shortcutId), + MTP_long(action.options.effectId), + MTP_long(starsPaid) + ), [=](const MTPUpdates &result, const MTP::Response &response) { + if (clearCloudDraft) { + history->finishSavingCloudDraft( + topicRootId, + monoforumPeerId, + UnixtimeFromMsgId(response.outerMsgId)); + } + _session->changes().historyUpdated( + history, + (action.options.scheduled + ? Data::HistoryUpdate::Flag::ScheduledSent + : Data::HistoryUpdate::Flag::MessageSent)); + done(); + }, [=](const MTP::Error &error, const MTP::Response &response) { + if (clearCloudDraft) { + history->finishSavingCloudDraft( + topicRootId, + monoforumPeerId, + UnixtimeFromMsgId(response.outerMsgId)); + } + fail(); + }); +} void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) { auto &entry = _toggles[itemId]; diff --git a/Telegram/SourceFiles/api/api_todo_lists.h b/Telegram/SourceFiles/api/api_todo_lists.h index 49891ea0bf..abb8c72d2a 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.h +++ b/Telegram/SourceFiles/api/api_todo_lists.h @@ -26,11 +26,11 @@ class TodoLists final { public: explicit TodoLists(not_null<ApiWrap*> api); - //void create( - // const PollData &data, - // SendAction action, - // Fn<void()> done, - // Fn<void()> fail); + void create( + const TodoListData &data, + SendAction action, + Fn<void()> done, + Fn<void()> fail); void toggleCompletion(FullMsgId itemId, int id, bool completed); private: diff --git a/Telegram/SourceFiles/boxes/create_poll_box.cpp b/Telegram/SourceFiles/boxes/create_poll_box.cpp index 926656d8b6..290da2d72e 100644 --- a/Telegram/SourceFiles/boxes/create_poll_box.cpp +++ b/Telegram/SourceFiles/boxes/create_poll_box.cpp @@ -114,7 +114,7 @@ private: void setPlaceholder() const; void removePlaceholder() const; - not_null<Ui::InputField*> field() const; + [[nodiscard]] not_null<Ui::InputField*> field() const; [[nodiscard]] PollAnswer toPollAnswer(int index) const; diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp b/Telegram/SourceFiles/boxes/create_todo_list_box.cpp new file mode 100644 index 0000000000..d251893d95 --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_todo_list_box.cpp @@ -0,0 +1,995 @@ +/* +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/create_todo_list_box.h" + +#include "base/call_delayed.h" +#include "base/event_filter.h" +#include "base/random.h" +#include "base/unique_qptr.h" +#include "chat_helpers/emoji_suggestions_widget.h" +#include "chat_helpers/message_field.h" +#include "chat_helpers/tabbed_panel.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_session.h" +#include "data/data_todo_list.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/view/history_view_schedule_box.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "menu/menu_send.h" +#include "ui/controls/emoji_button.h" +#include "ui/controls/emoji_button_factory.h" +#include "ui/rect.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/shadow.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/ui_utility.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" // defaultComposeFiles. +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace { + +constexpr auto kMaxOptionsCount = TodoListData::kMaxOptions; +constexpr auto kWarnTitleLimit = 12; +constexpr auto kWarnTaskLimit = 24; +constexpr auto kErrorLimit = 99; + +class Tasks { +public: + Tasks( + not_null<Ui::BoxContent*> box, + not_null<Ui::VerticalLayout*> container, + not_null<Window::SessionController*> controller, + ChatHelpers::TabbedPanel *emojiPanel); + + [[nodiscard]] bool hasTasks() const; + [[nodiscard]] bool isValid() const; + [[nodiscard]] std::vector<TodoListItem> toTodoListItems() const; + void focusFirst(); + + [[nodiscard]] rpl::producer<int> usedCount() const; + [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const; + [[nodiscard]] rpl::producer<> backspaceInFront() const; + [[nodiscard]] rpl::producer<> tabbed() const; + +private: + class Task { + public: + Task( + not_null<QWidget*> outer, + not_null<Ui::VerticalLayout*> container, + not_null<Main::Session*> session, + int position); + + Task(const Task &other) = delete; + Task &operator=(const Task &other) = delete; + + void toggleRemoveAlways(bool toggled); + + void show(anim::type animated); + void destroy(FnMut<void()> done); + + [[nodiscard]] bool hasShadow() const; + void createShadow(); + void destroyShadow(); + + [[nodiscard]] bool isEmpty() const; + [[nodiscard]] bool isGood() const; + [[nodiscard]] bool isTooLong() const; + [[nodiscard]] bool hasFocus() const; + void setFocus() const; + void clearValue(); + + void setPlaceholder() const; + void removePlaceholder() const; + + [[nodiscard]] not_null<Ui::InputField*> field() const; + + [[nodiscard]] TodoListItem toTodoListItem(int index) const; + + [[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const; + + private: + void createRemove(); + void createWarning(); + void updateFieldGeometry(); + + base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap; + not_null<Ui::RpWidget*> _content; + Ui::InputField *_field = nullptr; + base::unique_qptr<Ui::PlainShadow> _shadow; + base::unique_qptr<Ui::CrossButton> _remove; + rpl::variable<bool> *_removeAlways = nullptr; + int _limit = 0; + + }; + + [[nodiscard]] bool full() const; + [[nodiscard]] bool correctShadows() const; + void fixShadows(); + void removeEmptyTail(); + void addEmptyTask(); + void checkLastTask(); + void validateState(); + void fixAfterErase(); + void destroy(std::unique_ptr<Task> task); + void removeDestroyed(not_null<Task*> field); + int findField(not_null<Ui::InputField*> field) const; + + not_null<Ui::BoxContent*> _box; + not_null<Ui::VerticalLayout*> _container; + const not_null<Window::SessionController*> _controller; + ChatHelpers::TabbedPanel * const _emojiPanel; + int _position = 0; + int _tasksLimit = 0; + std::vector<std::unique_ptr<Task>> _list; + std::vector<std::unique_ptr<Task>> _destroyed; + rpl::variable<int> _usedCount = 0; + bool _hasTasks = false; + bool _isValid = false; + rpl::event_stream<not_null<QWidget*>> _scrollToWidget; + rpl::event_stream<> _backspaceInFront; + rpl::event_stream<> _tabbed; + rpl::lifetime _emojiPanelLifetime; + +}; + +void InitField( + not_null<QWidget*> container, + not_null<Ui::InputField*> field, + not_null<Main::Session*> session) { + field->setInstantReplaces(Ui::InstantReplaces::Default()); + field->setInstantReplacesEnabled( + Core::App().settings().replaceEmojiValue()); + auto options = Ui::Emoji::SuggestionsController::Options(); + options.suggestExactFirstWord = false; + Ui::Emoji::SuggestionsController::Init( + container, + field, + session, + options); +} + +not_null<Ui::FlatLabel*> CreateWarningLabel( + not_null<QWidget*> parent, + not_null<Ui::InputField*> field, + int valueLimit, + int warnLimit) { + const auto result = Ui::CreateChild<Ui::FlatLabel>( + parent.get(), + QString(), + st::createPollWarning); + result->setAttribute(Qt::WA_TransparentForMouseEvents); + field->changes( + ) | rpl::start_with_next([=] { + Ui::PostponeCall(crl::guard(field, [=] { + const auto length = field->getLastText().size(); + const auto value = valueLimit - length; + const auto shown = (value < warnLimit) + && (field->height() > st::createPollOptionField.heightMin); + if (value >= 0) { + result->setText(QString::number(value)); + } else { + constexpr auto kMinus = QChar(0x2212); + result->setMarkedText(Ui::Text::Colorized( + kMinus + QString::number(std::abs(value)))); + } + result->setVisible(shown); + })); + }, field->lifetime()); + return result; +} + +void FocusAtEnd(not_null<Ui::InputField*> field) { + field->setFocus(); + field->setCursorPosition(field->getLastText().size()); + field->ensureCursorVisible(); +} + +Tasks::Task::Task( + not_null<QWidget*> outer, + not_null<Ui::VerticalLayout*> container, + not_null<Main::Session*> session, + int position) +: _wrap(container->insert( + position, + object_ptr<Ui::SlideWrap<Ui::RpWidget>>( + container, + object_ptr<Ui::RpWidget>(container)))) +, _content(_wrap->entity()) +, _field( + Ui::CreateChild<Ui::InputField>( + _content.get(), + session->user()->isPremium() + ? st::createPollOptionFieldPremium + : st::createPollOptionField, + Ui::InputField::Mode::NoNewlines, + tr::lng_todo_create_list_add())) +, _limit(session->appConfig().todoListItemTextLimit()) { + InitField(outer, _field, session); + _field->setMaxLength(_limit + kErrorLimit); + _field->show(); + _field->customTab(true); + + _wrap->hide(anim::type::instant); + + _content->widthValue( + ) | rpl::start_with_next([=] { + updateFieldGeometry(); + }, _field->lifetime()); + + _field->heightValue( + ) | rpl::start_with_next([=](int height) { + _content->resize(_content->width(), height); + }, _field->lifetime()); + + createShadow(); + createRemove(); + createWarning(); + updateFieldGeometry(); +} + +bool Tasks::Task::hasShadow() const { + return (_shadow != nullptr); +} + +void Tasks::Task::createShadow() { + Expects(_content != nullptr); + + if (_shadow) { + return; + } + _shadow.reset(Ui::CreateChild<Ui::PlainShadow>(field().get())); + _shadow->show(); + field()->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto left = st::createPollFieldPadding.left(); + _shadow->setGeometry( + left, + size.height() - st::lineWidth, + size.width() - left, + st::lineWidth); + }, _shadow->lifetime()); +} + +void Tasks::Task::destroyShadow() { + _shadow = nullptr; +} + +void Tasks::Task::createRemove() { + using namespace rpl::mappers; + + const auto field = this->field(); + auto &lifetime = field->lifetime(); + + const auto remove = Ui::CreateChild<Ui::CrossButton>( + field.get(), + st::createPollOptionRemove); + remove->show(anim::type::instant); + + const auto toggle = lifetime.make_state<rpl::variable<bool>>(false); + _removeAlways = lifetime.make_state<rpl::variable<bool>>(false); + + field->changes( + ) | rpl::start_with_next([field, toggle] { + // Don't capture 'this'! Because Option is a value type. + *toggle = !field->getLastText().isEmpty(); + }, field->lifetime()); +#if 0 + rpl::combine( + toggle->value(), + _removeAlways->value(), + _1 || _2 + ) | rpl::start_with_next([=](bool shown) { + remove->toggle(shown, anim::type::normal); + }, remove->lifetime()); +#endif + + field->widthValue( + ) | rpl::start_with_next([=](int width) { + remove->moveToRight( + st::createPollOptionRemovePosition.x(), + st::createPollOptionRemovePosition.y(), + width); + }, remove->lifetime()); + + _remove.reset(remove); +} + +void Tasks::Task::createWarning() { + using namespace rpl::mappers; + + const auto field = this->field(); + const auto warning = CreateWarningLabel( + field, + field, + _limit, + kWarnTaskLimit); + rpl::combine( + field->sizeValue(), + warning->sizeValue() + ) | rpl::start_with_next([=](QSize size, QSize label) { + warning->moveToLeft( + (size.width() + - label.width() + - st::createPollWarningPosition.x()), + (size.height() + - label.height() + - st::createPollWarningPosition.y()), + size.width()); + }, warning->lifetime()); +} + +bool Tasks::Task::isEmpty() const { + return field()->getLastText().trimmed().isEmpty(); +} + +bool Tasks::Task::isGood() const { + return !field()->getLastText().trimmed().isEmpty() && !isTooLong(); +} + +bool Tasks::Task::isTooLong() const { + return (field()->getLastText().size() > _limit); +} + +bool Tasks::Task::hasFocus() const { + return field()->hasFocus(); +} + +void Tasks::Task::setFocus() const { + FocusAtEnd(field()); +} + +void Tasks::Task::clearValue() { + field()->setText(QString()); +} + +void Tasks::Task::setPlaceholder() const { + field()->setPlaceholder(tr::lng_todo_create_list_add()); +} + +void Tasks::Task::toggleRemoveAlways(bool toggled) { + *_removeAlways = toggled; +} + +void Tasks::Task::updateFieldGeometry() { + _field->resizeToWidth(_content->width()); + _field->moveToLeft(0, 0); +} + +not_null<Ui::InputField*> Tasks::Task::field() const { + return _field; +} + +void Tasks::Task::removePlaceholder() const { + field()->setPlaceholder(rpl::single(QString())); +} + +TodoListItem Tasks::Task::toTodoListItem(int index) const { + Expects(index >= 0 && index < kMaxOptionsCount); + + const auto text = field()->getTextWithTags(); + + auto result = TodoListItem{ + .text = TextWithEntities{ + .text = text.text, + .entities = TextUtilities::ConvertTextTagsToEntities(text.tags), + }, + .id = (index + 1) + }; + TextUtilities::Trim(result.text); + return result; +} + +rpl::producer<Qt::MouseButton> Tasks::Task::removeClicks() const { + return _remove->clicks(); +} + +Tasks::Tasks( + not_null<Ui::BoxContent*> box, + not_null<Ui::VerticalLayout*> container, + not_null<Window::SessionController*> controller, + ChatHelpers::TabbedPanel *emojiPanel) +: _box(box) +, _container(container) +, _controller(controller) +, _emojiPanel(emojiPanel) +, _position(_container->count()) +, _tasksLimit(controller->session().appConfig().todoListItemsLimit()) { + checkLastTask(); +} + +bool Tasks::full() const { + return (_list.size() >= _tasksLimit); +} + +bool Tasks::hasTasks() const { + return _hasTasks; +} + +bool Tasks::isValid() const { + return _isValid; +} + +rpl::producer<int> Tasks::usedCount() const { + return _usedCount.value(); +} + +rpl::producer<not_null<QWidget*>> Tasks::scrollToWidget() const { + return _scrollToWidget.events(); +} + +rpl::producer<> Tasks::backspaceInFront() const { + return _backspaceInFront.events(); +} + +rpl::producer<> Tasks::tabbed() const { + return _tabbed.events(); +} + +void Tasks::Task::show(anim::type animated) { + _wrap->show(animated); +} + +void Tasks::Task::destroy(FnMut<void()> done) { + if (anim::Disabled() || _wrap->isHidden()) { + Ui::PostponeCall(std::move(done)); + return; + } + _wrap->hide(anim::type::normal); + base::call_delayed( + st::slideWrapDuration * 2, + _content.get(), + std::move(done)); +} + +std::vector<TodoListItem> Tasks::toTodoListItems() const { + auto result = std::vector<TodoListItem>(); + result.reserve(_list.size()); + auto counter = int(0); + const auto makeTask = [&](const std::unique_ptr<Task> &task) { + return task->toTodoListItem(counter++); + }; + ranges::copy( + _list + | ranges::views::filter(&Task::isGood) + | ranges::views::transform(makeTask), + ranges::back_inserter(result)); + return result; +} + +void Tasks::focusFirst() { + Expects(!_list.empty()); + + _list.front()->setFocus(); +} + +bool Tasks::correctShadows() const { + // Last one should be without shadow. + const auto noShadow = ranges::find( + _list, + true, + ranges::not_fn(&Task::hasShadow)); + return (noShadow == end(_list) - 1); +} + +void Tasks::fixShadows() { + if (correctShadows()) { + return; + } + for (auto &option : _list) { + option->createShadow(); + } + _list.back()->destroyShadow(); +} + +void Tasks::removeEmptyTail() { + // Only one option at the end of options list can be empty. + // Remove all other trailing empty options. + // Only last empty and previous option have non-empty placeholders. + const auto focused = ranges::find_if( + _list, + &Task::hasFocus); + const auto end = _list.end(); + const auto reversed = ranges::views::reverse(_list); + const auto emptyItem = ranges::find_if( + reversed, + ranges::not_fn(&Task::isEmpty)).base(); + const auto focusLast = (focused > emptyItem) && (focused < end); + if (emptyItem == end) { + return; + } + if (focusLast) { + (*emptyItem)->setFocus(); + } + for (auto i = emptyItem + 1; i != end; ++i) { + destroy(std::move(*i)); + } + _list.erase(emptyItem + 1, end); + fixAfterErase(); +} + +void Tasks::destroy(std::unique_ptr<Task> task) { + const auto value = task.get(); + task->destroy([=] { removeDestroyed(value); }); + _destroyed.push_back(std::move(task)); +} + +void Tasks::fixAfterErase() { + Expects(!_list.empty()); + + const auto last = _list.end() - 1; + (*last)->setPlaceholder(); + (*last)->toggleRemoveAlways(false); + if (last != begin(_list)) { + (*(last - 1))->setPlaceholder(); + (*(last - 1))->toggleRemoveAlways(false); + } + fixShadows(); +} + +void Tasks::addEmptyTask() { + if (full()) { + return; + } else if (!_list.empty() && _list.back()->isEmpty()) { + return; + } + if (_list.size() > 1) { + (*(_list.end() - 2))->removePlaceholder(); + (*(_list.end() - 2))->toggleRemoveAlways(true); + } + _list.push_back(std::make_unique<Task>( + _box, + _container, + &_controller->session(), + _position + _list.size() + _destroyed.size())); + const auto field = _list.back()->field(); + if (const auto emojiPanel = _emojiPanel) { + const auto emojiToggle = Ui::AddEmojiToggleToField( + field, + _box, + _controller, + emojiPanel, + QPoint( + -st::createPollOptionFieldPremium.textMargins.right(), + st::createPollOptionEmojiPositionSkip)); + emojiToggle->shownValue() | rpl::start_with_next([=](bool shown) { + if (!shown) { + return; + } + _emojiPanelLifetime.destroy(); + emojiPanel->selector()->emojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { + if (field->hasFocus()) { + Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji); + } + }, _emojiPanelLifetime); + emojiPanel->selector()->customEmojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + if (field->hasFocus()) { + Data::InsertCustomEmoji(field, data.document); + } + }, _emojiPanelLifetime); + }, emojiToggle->lifetime()); + } + field->submits( + ) | rpl::start_with_next([=] { + const auto index = findField(field); + if (_list[index]->isGood() && index + 1 < _list.size()) { + _list[index + 1]->setFocus(); + } + }, field->lifetime()); + field->changes( + ) | rpl::start_with_next([=] { + Ui::PostponeCall(crl::guard(field, [=] { + validateState(); + })); + }, field->lifetime()); + field->focusedChanges( + ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] { + _scrollToWidget.fire_copy(field); + }, field->lifetime()); + field->tabbed( + ) | rpl::start_with_next([=] { + const auto index = findField(field); + if (index + 1 < _list.size()) { + _list[index + 1]->setFocus(); + } else { + _tabbed.fire({}); + } + }, field->lifetime()); + base::install_event_filter(field, [=](not_null<QEvent*> event) { + if (event->type() != QEvent::KeyPress + || !field->getLastText().isEmpty()) { + return base::EventFilterResult::Continue; + } + const auto key = static_cast<QKeyEvent*>(event.get())->key(); + if (key != Qt::Key_Backspace) { + return base::EventFilterResult::Continue; + } + + const auto index = findField(field); + if (index > 0) { + _list[index - 1]->setFocus(); + } else { + _backspaceInFront.fire({}); + } + return base::EventFilterResult::Cancel; + }); + + _list.back()->removeClicks( + ) | rpl::start_with_next([=] { + Ui::PostponeCall(crl::guard(field, [=] { + Expects(!_list.empty()); + + const auto item = begin(_list) + findField(field); + if (item == _list.end() - 1) { + (*item)->clearValue(); + return; + } + if ((*item)->hasFocus()) { + (*(item + 1))->setFocus(); + } + destroy(std::move(*item)); + _list.erase(item); + fixAfterErase(); + validateState(); + })); + }, field->lifetime()); + + _list.back()->show((_list.size() == 1) + ? anim::type::instant + : anim::type::normal); + fixShadows(); +} + +void Tasks::removeDestroyed(not_null<Task*> task) { + const auto i = ranges::find( + _destroyed, + task.get(), + &std::unique_ptr<Task>::get); + Assert(i != end(_destroyed)); + _destroyed.erase(i); +} + +void Tasks::validateState() { + checkLastTask(); + _hasTasks = (ranges::count_if(_list, &Task::isGood) > 0); + _isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong); + + const auto lastEmpty = !_list.empty() && _list.back()->isEmpty(); + _usedCount = _list.size() - (lastEmpty ? 1 : 0); +} + +int Tasks::findField(not_null<Ui::InputField*> field) const { + const auto result = ranges::find( + _list, + field, + &Task::field) - begin(_list); + + Ensures(result >= 0 && result < _list.size()); + return result; +} + +void Tasks::checkLastTask() { + removeEmptyTail(); + addEmptyTask(); +} + +} // namespace + +CreateTodoListBox::CreateTodoListBox( + QWidget*, + not_null<Window::SessionController*> controller, + rpl::producer<int> starsRequired, + Api::SendType sendType, + SendMenu::Details sendMenuDetails) +: _controller(controller) +, _sendType(sendType) +, _sendMenuDetails([result = sendMenuDetails] { return result; }) +, _starsRequired(std::move(starsRequired)) +, _titleLimit(controller->session().appConfig().todoListTitleLimit()) { +} + +auto CreateTodoListBox::submitRequests() const -> rpl::producer<Result> { + return _submitRequests.events(); +} + +void CreateTodoListBox::setInnerFocus() { + _setInnerFocus(); +} + +void CreateTodoListBox::submitFailed(const QString &error) { + showToast(error); +} + +not_null<Ui::InputField*> CreateTodoListBox::setupTitle( + not_null<Ui::VerticalLayout*> container) { + using namespace Settings; + + const auto session = &_controller->session(); + const auto isPremium = session->user()->isPremium(); + + const auto title = container->add( + object_ptr<Ui::InputField>( + container, + st::createPollField, + Ui::InputField::Mode::MultiLine, + tr::lng_todo_create_title_placeholder()), + st::createPollFieldPadding + + (isPremium + ? QMargins(0, 0, st::defaultComposeFiles.emoji.inner.width, 0) + : QMargins())); + InitField(getDelegate()->outerContainer(), title, session); + title->setMaxLength(_titleLimit + kErrorLimit); + title->setSubmitSettings(Ui::InputField::SubmitSettings::Both); + title->customTab(true); + + if (isPremium) { + using Selector = ChatHelpers::TabbedSelector; + const auto outer = getDelegate()->outerContainer(); + _emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>( + outer, + _controller, + object_ptr<Selector>( + nullptr, + _controller->uiShow(), + Window::GifPauseReason::Layer, + Selector::Mode::EmojiOnly)); + const auto emojiPanel = _emojiPanel.get(); + emojiPanel->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + emojiPanel->hide(); + emojiPanel->selector()->setCurrentPeer(session->user()); + + const auto emojiToggle = Ui::AddEmojiToggleToField( + title, + this, + _controller, + emojiPanel, + st::createPollOptionFieldPremiumEmojiPosition); + emojiPanel->selector()->emojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { + if (title->hasFocus()) { + Ui::InsertEmojiAtCursor(title->textCursor(), data.emoji); + } + }, emojiToggle->lifetime()); + emojiPanel->selector()->customEmojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + if (title->hasFocus()) { + Data::InsertCustomEmoji(title, data.document); + } + }, emojiToggle->lifetime()); + } + + const auto warning = CreateWarningLabel( + container, + title, + _titleLimit, + kWarnTitleLimit); + rpl::combine( + title->geometryValue(), + warning->sizeValue() + ) | rpl::start_with_next([=](QRect geometry, QSize label) { + warning->moveToLeft( + (container->width() + - label.width() + - st::createPollWarningPosition.x()), + (geometry.y() + - st::createPollFieldPadding.top() + - st::defaultSubsectionTitlePadding.bottom() + - st::defaultSubsectionTitle.style.font->height + + st::defaultSubsectionTitle.style.font->ascent + - st::createPollWarning.style.font->ascent), + geometry.width()); + }, warning->lifetime()); + + return title; +} + +object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { + using namespace Settings; + + const auto id = FullMsgId{ + PeerId(), + _controller->session().data().nextNonHistoryEntryId(), + }; + const auto error = lifetime().make_state<Errors>(Error::Title); + + auto result = object_ptr<Ui::VerticalLayout>(this); + const auto container = result.data(); + + const auto title = setupTitle(container); + Ui::AddDivider(container); + Ui::AddSkip(container); + container->add( + object_ptr<Ui::FlatLabel>( + container, + tr::lng_todo_create_list(), + st::defaultSubsectionTitle), + st::createPollFieldTitlePadding); + const auto tasks = lifetime().make_state<Tasks>( + this, + container, + _controller, + _emojiPanel ? _emojiPanel.get() : nullptr); + auto limit = tasks->usedCount() | rpl::after_next([=](int count) { + setCloseByEscape(!count); + setCloseByOutsideClick(!count); + }) | rpl::map([=](int count) { + const auto appConfig = &_controller->session().appConfig(); + const auto max = appConfig->todoListItemsLimit(); + return (count < max) + ? tr::lng_todo_create_limit(tr::now, lt_count, max - count) + : tr::lng_todo_create_maximum(tr::now); + }) | rpl::after_next([=] { + container->resizeToWidth(container->widthNoMargins()); + }); + container->add( + object_ptr<Ui::DividerLabel>( + container, + object_ptr<Ui::FlatLabel>( + container, + std::move(limit), + st::boxDividerLabel), + st::createPollLimitPadding)); + + title->tabbed( + ) | rpl::start_with_next([=] { + tasks->focusFirst(); + }, title->lifetime()); + + Ui::AddSkip(container); + Ui::AddSubsectionTitle(container, tr::lng_todo_create_settings()); + + const auto allowAdd = container->add( + object_ptr<Ui::Checkbox>( + container, + tr::lng_todo_create_allow_add(tr::now), + true, + st::defaultCheckbox), + st::createPollCheckboxMargin); + const auto allowMark = container->add( + object_ptr<Ui::Checkbox>( + container, + tr::lng_todo_create_allow_mark(tr::now), + true, + st::defaultCheckbox), + st::createPollCheckboxMargin); + + tasks->tabbed( + ) | rpl::start_with_next([=] { + title->setFocus(); + }, title->lifetime()); + + const auto isValidTitle = [=] { + const auto text = title->getLastText().trimmed(); + return !text.isEmpty() && (text.size() <= _titleLimit); + }; + title->submits( + ) | rpl::start_with_next([=] { + if (isValidTitle()) { + tasks->focusFirst(); + } + }, title->lifetime()); + + _setInnerFocus = [=] { + title->setFocusFast(); + }; + + const auto collectResult = [=] { + const auto textWithTags = title->getTextWithTags(); + using Flag = TodoListData::Flag; + auto result = TodoListData(&_controller->session().data(), id); + result.title.text = textWithTags.text; + result.title.entities = TextUtilities::ConvertTextTagsToEntities( + textWithTags.tags); + TextUtilities::Trim(result.title); + result.items = tasks->toTodoListItems(); + const auto allowAddTasks = allowAdd->checked(); + const auto allowMarkTasks = allowMark->checked(); + result.setFlags(Flag(0) + | (allowAddTasks ? Flag::OthersCanAppend : Flag(0)) + | (allowMarkTasks ? Flag::OthersCanComplete : Flag(0))); + return result; + }; + const auto collectError = [=] { + if (isValidTitle()) { + *error &= ~Error::Title; + } else { + *error |= Error::Title; + } + if (!tasks->hasTasks()) { + *error |= Error::Tasks; + } else if (!tasks->isValid()) { + *error |= Error::Other; + } else { + *error &= ~(Error::Tasks | Error::Other); + } + }; + const auto showError = [show = uiShow()]( + tr::phrase<> text) { + show->showToast(text(tr::now)); + }; + + const auto send = [=](Api::SendOptions sendOptions) { + collectError(); + if (*error & Error::Title) { + showError(tr::lng_todo_choose_title); + title->setFocus(); + } else if (*error & Error::Tasks) { + showError(tr::lng_todo_choose_tasks); + tasks->focusFirst(); + } else if (!*error) { + _submitRequests.fire({ collectResult(), sendOptions }); + } + }; + const auto sendAction = SendMenu::DefaultCallback( + _controller->uiShow(), + crl::guard(this, send)); + + tasks->scrollToWidget( + ) | rpl::start_with_next([=](not_null<QWidget*> widget) { + scrollToWidget(widget); + }, lifetime()); + + tasks->backspaceInFront( + ) | rpl::start_with_next([=] { + FocusAtEnd(title); + }, lifetime()); + + const auto isNormal = (_sendType == Api::SendType::Normal); + const auto schedule = [=] { + sendAction( + { .type = SendMenu::ActionType::Schedule }, + _sendMenuDetails()); + }; + const auto submit = addButton( + tr::lng_todo_create_button(), + [=] { isNormal ? send({}) : schedule(); }); + submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal + ? tr::lng_todo_create_button() + : tr::lng_schedule_button())); + const auto sendMenuDetails = [=] { + collectError(); + return (*error) ? SendMenu::Details() : _sendMenuDetails(); + }; + SendMenu::SetupMenuAndShortcuts( + submit.data(), + _controller->uiShow(), + sendMenuDetails, + sendAction); + addButton(tr::lng_cancel(), [=] { closeBox(); }); + + return result; +} + +void CreateTodoListBox::prepare() { + setTitle(tr::lng_todo_create_title()); + + const auto inner = setInnerWidget(setupContent()); + + setDimensionsToContent(st::boxWideWidth, inner); +} diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.h b/Telegram/SourceFiles/boxes/create_todo_list_box.h new file mode 100644 index 0000000000..3b89ca5e35 --- /dev/null +++ b/Telegram/SourceFiles/boxes/create_todo_list_box.h @@ -0,0 +1,78 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/layers/box_content.h" +#include "api/api_common.h" +#include "data/data_todo_list.h" +#include "base/flags.h" + +struct TodoListData; + +namespace ChatHelpers { +class TabbedPanel; +} // namespace ChatHelpers + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace SendMenu { +struct Details; +} // namespace SendMenu + +class CreateTodoListBox : public Ui::BoxContent { +public: + struct Result { + TodoListData todolist; + Api::SendOptions options; + }; + + CreateTodoListBox( + QWidget*, + not_null<Window::SessionController*> controller, + rpl::producer<int> starsRequired, + Api::SendType sendType, + SendMenu::Details sendMenuDetails); + + [[nodiscard]] rpl::producer<Result> submitRequests() const; + void submitFailed(const QString &error); + + void setInnerFocus() override; + +protected: + void prepare() override; + +private: + enum class Error { + Title = 0x01, + Tasks = 0x02, + Other = 0x04, + }; + friend constexpr inline bool is_flag_type(Error) { return true; } + using Errors = base::flags<Error>; + + [[nodiscard]] object_ptr<Ui::RpWidget> setupContent(); + [[nodiscard]] not_null<Ui::InputField*> setupTitle( + not_null<Ui::VerticalLayout*> container); + + const not_null<Window::SessionController*> _controller; + const Api::SendType _sendType = Api::SendType(); + const Fn<SendMenu::Details()> _sendMenuDetails; + rpl::variable<int> _starsRequired; + base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; + Fn<void()> _setInnerFocus; + Fn<rpl::producer<bool>()> _dataIsValidValue; + rpl::event_stream<Result> _submitRequests; + int _titleLimit = 0; + +}; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 46ec093b1e..2927add840 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -684,6 +684,10 @@ bool PeerData::canCreatePolls() const { return Data::CanSend(this, ChatRestriction::SendPolls); } +bool PeerData::canCreateTodoLists() const { + return Data::CanSend(this, ChatRestriction::SendPolls) || isUser(); +} + bool PeerData::canCreateTopics() const { if (const auto channel = asChannel()) { return channel->isForum() diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index 207b7361e2..66f9d91ef9 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -429,6 +429,7 @@ public: [[nodiscard]] bool canPinMessages() const; [[nodiscard]] bool canEditMessagesIndefinitely() const; [[nodiscard]] bool canCreatePolls() const; + [[nodiscard]] bool canCreateTodoLists() const; [[nodiscard]] bool canCreateTopics() const; [[nodiscard]] bool canManageTopics() const; [[nodiscard]] bool canManageGifts() const; diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp index a04c003cec..2ecbe5ce3a 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -1144,6 +1144,9 @@ void TopBarWidget::updateControlsVisibility() { const auto hasPollsMenu = (_activeChat.key.peer() && _activeChat.key.peer()->canCreatePolls()) || (topic && Data::CanSend(topic, ChatRestriction::SendPolls)); + const auto hasTodoListsMenu = (_activeChat.key.peer() + && _activeChat.key.peer()->canCreateTodoLists()) + || (topic && Data::CanSend(topic, ChatRestriction::SendPolls)); const auto hasTopicMenu = [&] { if (!topic || section != Section::Replies) { return false; @@ -1163,9 +1166,9 @@ void TopBarWidget::updateControlsVisibility() { && (section == Section::History ? true : (section == Section::Scheduled) - ? hasPollsMenu + ? (hasPollsMenu || hasTodoListsMenu) : (section == Section::Replies) - ? (hasPollsMenu || hasTopicMenu) + ? (hasPollsMenu || hasTodoListsMenu || hasTopicMenu) : (section == Section::ChatsList) ? (_activeChat.key.peer() && _activeChat.key.peer()->isForum()) : false); diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index af6ad3c56f..ae40429964 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -121,7 +121,7 @@ void TodoList::setupPreviousState(const std::vector<TodoTaskInfo> &info) { } QSize TodoList::countOptimalSize() { - updateTexts(); + updateTexts(); const auto paddings = st::msgPadding.left() + st::msgPadding.right(); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index aa99af3ee8..88d3a75fc9 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -2626,6 +2626,26 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu( { sendMenuType }); }, &st::menuIconCreatePoll); } + if (peer->canCreateTodoLists()) { + ++minimal; + raw->addAction(tr::lng_todo_create(tr::now), [=] { + const auto action = actionFactory(); + const auto source = action.options.scheduled + ? Api::SendType::Scheduled + : Api::SendType::Normal; + const auto sendMenuType = (action.replyTo.topicRootId + || action.history->peer->starsPerMessageChecked()) + ? SendMenu::Type::SilentOnly + : SendMenu::Type::Scheduled; + const auto replyTo = action.replyTo; + Window::PeerMenuCreateTodoList( + controller, + peer, + replyTo, + source, + { sendMenuType }); + }, &st::menuIconCreateTodoList); + } const auto session = &controller->session(); const auto locationType = ChatRestriction::SendOther; const auto config = ResolveMapsConfig(session); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 3e53a82bfb..8875884f24 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -145,9 +145,21 @@ int AppConfig::giftResaleReceiveThousandths() const { } int AppConfig::pollOptionsLimit() const { + return get<int>(u"poll_answers_max"_q, 12); +} + +int AppConfig::todoListItemsLimit() const { return get<int>( - u"poll_answers_max"_q, - _account->mtp().isTestMode() ? 12 : 10); + u"todo_items_max"_q, + _account->mtp().isTestMode() ? 10 : 30); +} + +int AppConfig::todoListTitleLimit() const { + return get<int>(u"todo_title_length_max"_q, 32); +} + +int AppConfig::todoListItemTextLimit() const { + return get<int>(u"todo_item_length_max"_q, 64); } void AppConfig::refresh(bool force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 8c460a3963..bf0053d79b 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -84,6 +84,9 @@ public: [[nodiscard]] int giftResaleReceiveThousandths() const; [[nodiscard]] int pollOptionsLimit() const; + [[nodiscard]] int todoListItemsLimit() const; + [[nodiscard]] int todoListTitleLimit() const; + [[nodiscard]] int todoListItemTextLimit() const; void refresh(bool force = false); diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index c54c168f81..1a50e96203 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -57,6 +57,7 @@ menuIconStats: icon {{ "menu/stats", menuIconColor }}; menuIconBoosts: icon {{ "menu/boosts", menuIconColor }}; menuIconEarn: icon {{ "menu/earn", menuIconColor }}; menuIconCreatePoll: icon {{ "menu/create_poll", menuIconColor }}; +menuIconCreateTodoList: icon {{ "menu/select", menuIconColor }}; menuIconQrCode: icon {{ "menu/qr_code", menuIconColor }}; menuIconExpand: icon {{ "menu/expand", menuIconColor }}; menuIconCollapse: icon {{ "menu/collapse", menuIconColor }}; diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 958cb3179b..61085178e6 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/moderate_messages_box.h" #include "boxes/choose_filter_box.h" #include "boxes/create_poll_box.h" +#include "boxes/create_todo_list_box.h" #include "boxes/pin_messages_box.h" #include "boxes/premium_limits_box.h" #include "boxes/report_messages_box.h" @@ -59,6 +60,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_blocked_peers.h" #include "api/api_chat_filters.h" #include "api/api_polls.h" +#include "api/api_todo_lists.h" #include "api/api_updates.h" #include "mtproto/mtproto_config.h" #include "history/history.h" @@ -290,6 +292,7 @@ private: void addManageTopic(); void addManageChat(); void addCreatePoll(); + void addCreateTodoList(); void addThemeEdit(); void addBlockUser(); void addViewDiscussion(); @@ -315,6 +318,8 @@ private: void addViewStatistics(); void addBoostChat(); + [[nodiscard]] bool skipCreateActions() const; + not_null<SessionController*> _controller; Dialogs::EntryState _request; Data::Thread *_thread = nullptr; @@ -1165,7 +1170,7 @@ void Filler::addViewStatistics() { } } -void Filler::addCreatePoll() { +bool Filler::skipCreateActions() const { const auto isJoinChannel = [&] { if (_request.section != Section::Replies) { if (const auto c = _peer->asChannel(); c && !c->amIn()) { @@ -1190,10 +1195,13 @@ void Filler::addCreatePoll() { const auto isBlocked = [&] { return _peer && _peer->isUser() && _peer->asUser()->isBlocked(); }(); - if (isBlocked || isJoinChannel || isBotStart) { + return isBlocked || isJoinChannel || isBotStart; +} + +void Filler::addCreatePoll() { + if (skipCreateActions()) { return; } - const auto can = _topic ? Data::CanSend(_topic, ChatRestriction::SendPolls) : _peer->canCreatePolls(); @@ -1229,6 +1237,42 @@ void Filler::addCreatePoll() { &st::menuIconCreatePoll); } +void Filler::addCreateTodoList() { + if (skipCreateActions()) { + return; + } + const auto can = _topic + ? Data::CanSend(_topic, ChatRestriction::SendPolls) + : _peer->canCreateTodoLists(); + if (!can) { + return; + } + const auto peer = _peer; + const auto controller = _controller; + const auto source = (_request.section == Section::Scheduled) + ? Api::SendType::Scheduled + : Api::SendType::Normal; + const auto sendMenuType = (_request.section == Section::Scheduled) + ? SendMenu::Type::Disabled + : (_request.section == Section::Replies + || _peer->starsPerMessageChecked()) + ? SendMenu::Type::SilentOnly + : SendMenu::Type::Scheduled; + const auto replyTo = _request.currentReplyTo; + auto callback = [=] { + PeerMenuCreateTodoList( + controller, + peer, + replyTo, + source, + { sendMenuType }); + }; + _addAction( + tr::lng_todo_create(tr::now), + std::move(callback), + &st::menuIconCreateTodoList); +} + void Filler::addThemeEdit() { if (_peer->isVerifyCodes() || _peer->isRepliesChat()) { return; @@ -1481,6 +1525,7 @@ void Filler::fillHistoryActions() { addSupportInfo(); addBoostChat(); addCreatePoll(); + addCreateTodoList(); addThemeEdit(); addViewDiscussion(); addDirectMessages(); @@ -1525,12 +1570,14 @@ void Filler::fillRepliesActions() { } addBoostChat(); addCreatePoll(); + addCreateTodoList(); addToggleTopicClosed(); addDeleteTopic(); } void Filler::fillScheduledActions() { addCreatePoll(); + addCreateTodoList(); } void Filler::fillArchiveActions() { @@ -1873,6 +1920,74 @@ void PeerMenuCreatePoll( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +void PeerMenuCreateTodoList( + not_null<Window::SessionController*> controller, + not_null<PeerData*> peer, + FullReplyTo replyTo, + Api::SendType sendType, + SendMenu::Details sendMenuDetails) { + auto starsRequired = peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::FullInfo + | Data::PeerUpdate::Flag::StarsPerMessage + ) | rpl::map([=] { + return peer->starsPerMessageChecked(); + }); + auto box = Box<CreateTodoListBox>( + controller, + std::move(starsRequired), + sendType, + sendMenuDetails); + struct State { + Fn<void(const CreateTodoListBox::Result &)> create; + SendPaymentHelper sendPayment; + bool lock = false; + }; + const auto weak = QPointer<CreateTodoListBox>(box); + const auto state = box->lifetime().make_state<State>(); + state->create = [=](const CreateTodoListBox::Result &result) { + const auto withPaymentApproved = crl::guard(weak, [=](int stars) { + if (const auto onstack = state->create) { + auto copy = result; + copy.options.starsApproved = stars; + onstack(copy); + } + }); + const auto checked = state->sendPayment.check( + controller, + peer, + 1, + result.options.starsApproved, + withPaymentApproved); + if (!checked || std::exchange(state->lock, true)) { + return; + } + auto action = Api::SendAction( + peer->owner().history(peer), + result.options); + action.replyTo = replyTo; + const auto local = action.history->localDraft( + replyTo.topicRootId, + replyTo.monoforumPeerId); + if (local) { + action.clearDraft = local->textWithTags.text.isEmpty(); + } else { + action.clearDraft = false; + } + const auto api = &peer->session().api(); + api->todoLists().create(result.todolist, action, crl::guard(weak, [=] { + state->create = nullptr; + weak->closeBox(); + }), crl::guard(weak, [=] { + state->lock = false; + weak->submitFailed(tr::lng_attach_failed(tr::now)); + })); + }; + box->submitRequests( + ) | rpl::start_with_next(state->create, box->lifetime()); + controller->show(std::move(box), Ui::LayerOption::CloseOther); +} + void PeerMenuBlockUserBox( not_null<Ui::GenericBox*> box, not_null<Window::Controller*> window, diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 15435b34ba..59beef33b7 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -111,6 +111,12 @@ void PeerMenuCreatePoll( PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +void PeerMenuCreateTodoList( + not_null<Window::SessionController*> controller, + not_null<PeerData*> peer, + FullReplyTo replyTo = FullReplyTo(), + Api::SendType sendType = Api::SendType::Normal, + SendMenu::Details sendMenuDetails = SendMenu::Details()); void PeerMenuDeleteTopicWithConfirmation( not_null<Window::SessionNavigation*> navigation, not_null<Data::ForumTopic*> topic); From e5de8e22b773538fd0252d14904cda876d3638b4 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 10 Jun 2025 18:25:35 +0400 Subject: [PATCH 176/310] Add fireworks on ending a task list. --- .../view/media/history_view_todo_list.cpp | 25 +++++++++++++++++++ .../view/media/history_view_todo_list.h | 8 ++++++ 2 files changed, 33 insertions(+) diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index ae40429964..e061a65bb4 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/effects/radial_animation.h" #include "ui/effects/ripple_animation.h" +#include "ui/effects/fireworks_animation.h" #include "ui/toast/toast.h" #include "ui/painter.h" #include "data/data_media_types.h" @@ -276,14 +277,19 @@ void TodoList::updateTasks(bool skipAnimations) { &Task::id, &TodoListItem::id); if (!changed) { + auto animated = false; auto &&tasks = ranges::views::zip(_tasks, _todolist->items); for (auto &&[task, original] : tasks) { const auto wasDate = task.completionDate; task.fillData(_todolist, original, context); if (!skipAnimations && (!wasDate != !task.completionDate)) { startToggleAnimation(task); + animated = true; } } + if (animated) { + maybeStartFireworks(); + } return; } _tasks = ranges::views::all( @@ -337,6 +343,15 @@ void TodoList::toggleCompletion(int id) { _parent->data()->fullId(), id, !selected); + + maybeStartFireworks(); +} + +void TodoList::maybeStartFireworks() { + if (!ranges::contains(_tasks, TimeId(), &Task::completionDate)) { + _fireworksAnimation = std::make_unique<Ui::FireworksAnimation>( + [=] { repaint(); }); + } } void TodoList::updateCompletionStatus() { @@ -625,6 +640,16 @@ TextState TodoList::textState(QPoint point, StateRequest request) const { return result; } +void TodoList::paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const { + if (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) { + return; + } + _fireworksAnimation = nullptr; +} + void TodoList::clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h index d0f25638b0..bc6e5ee08a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { class RippleAnimation; +class FireworksAnimation; } // namespace Ui namespace HistoryView { @@ -51,6 +52,11 @@ public: uint16 fullSelectionLength() const override; TextForMimeData selectedText(TextSelection selection) const override; + void paintBubbleFireworks( + Painter &p, + const QRect &bubble, + crl::time ms) const override; + void clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) override; @@ -80,6 +86,7 @@ private: void updateTasks(bool skipAnimations); void startToggleAnimation(Task &task); void updateCompletionStatus(); + void maybeStartFireworks(); void setupPreviousState(const std::vector<TodoTaskInfo> &info); int paintTask( @@ -122,6 +129,7 @@ private: std::vector<Task> _tasks; Ui::Text::String _completionStatusLabel; + mutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation; mutable QPoint _lastLinkPoint; mutable QImage _userpicCircleCache; mutable QImage _fillingIconCache; From bf217bf7aa9050db402dcb9098a4f4e07d2418b9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 10 Jun 2025 20:25:24 +0400 Subject: [PATCH 177/310] Check premium for todo lists actions. --- Telegram/Resources/langs/lang.strings | 10 +++ .../SourceFiles/boxes/premium_preview_box.cpp | 5 ++ .../SourceFiles/boxes/premium_preview_box.h | 1 + .../SourceFiles/data/data_media_types.cpp | 5 +- Telegram/SourceFiles/data/data_session.cpp | 13 ++++ Telegram/SourceFiles/data/data_session.h | 3 + .../view/media/history_view_todo_list.cpp | 62 +++++++++++++++++-- .../view/media/history_view_todo_list.h | 6 ++ .../inline_bots/bot_attach_web_view.cpp | 2 +- .../SourceFiles/settings/settings_premium.cpp | 11 ++++ .../SourceFiles/window/window_peer_menu.cpp | 39 ++++++++++++ .../SourceFiles/window/window_peer_menu.h | 6 ++ 12 files changed, 155 insertions(+), 8 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 80b008c240..c1b0d964ef 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2636,6 +2636,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_premium_summary_about_effects" = "Add over 500 animated effects to private messages."; "lng_premium_summary_subtitle_filter_tags" = "Tag Your Chats"; "lng_premium_summary_about_filter_tags" = "Display folder names for each chat in the chat list."; +"lng_premium_summary_subtitle_todo_lists" = "To-Do Lists"; +"lng_premium_summary_about_todo_lists" = "Create To-Do Lists, I guess.."; "lng_premium_summary_bottom_subtitle" = "About Telegram Premium"; "lng_premium_summary_bottom_about" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; "lng_premium_summary_button" = "Subscribe for {cost} per month"; @@ -5860,6 +5862,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_todo_completed#one" = "{count} of {total} completed"; "lng_todo_completed#other" = "{count} of {total} completed"; "lng_todo_completed_none" = "None of {total} completed"; +"lng_todo_menu_item" = "To-Do List"; "lng_todo_create" = "Create To-Do List"; "lng_todo_create_title" = "New To-Do List"; "lng_todo_create_title_placeholder" = "Title"; @@ -5875,6 +5878,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_todo_choose_title" = "Please enter a title."; "lng_todo_choose_tasks" = "Please enter at least one task."; +"lng_todo_add_title" = "Add Tasks"; +"lng_todo_create_premium" = "Only subscribers of {link} can create To-Do Lists."; +"lng_todo_add_premium" = "Only subscribers of {link} can add tasks."; +"lng_todo_mark_premium" = "Only subscribers of {link} can mark tasks as done."; +"lng_todo_premium_link" = "Telegram Premium"; +"lng_todo_mark_restricted" = "{user} has restricted others from marking tasks as done."; + "lng_outdated_title" = "PLEASE UPDATE YOUR OPERATING SYSTEM."; "lng_outdated_title_bits" = "PLEASE SWITCH TO A 64-BIT OPERATING SYSTEM."; "lng_outdated_soon" = "Otherwise, Telegram Desktop will stop updating on {date}."; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.cpp b/Telegram/SourceFiles/boxes/premium_preview_box.cpp index 53c6a6744e..d938945e10 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_preview_box.cpp @@ -133,6 +133,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) { return tr::lng_premium_summary_subtitle_business(); case PremiumFeature::Effects: return tr::lng_premium_summary_subtitle_effects(); + case PremiumFeature::TodoLists: + return tr::lng_premium_summary_subtitle_todo_lists(); case PremiumFeature::BusinessLocation: return tr::lng_business_subtitle_location(); @@ -198,6 +200,8 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) { return tr::lng_premium_summary_about_business(); case PremiumFeature::Effects: return tr::lng_premium_summary_about_effects(); + case PremiumFeature::TodoLists: + return tr::lng_premium_summary_about_todo_lists(); case PremiumFeature::BusinessLocation: return tr::lng_business_about_location(); @@ -538,6 +542,7 @@ struct VideoPreviewDocument { case PremiumFeature::LastSeen: return "last_seen"; case PremiumFeature::MessagePrivacy: return "message_privacy"; case PremiumFeature::Effects: return "effects"; + case PremiumFeature::TodoLists: return "todo_lists"; AssertIsDebug() case PremiumFeature::BusinessLocation: return "business_location"; case PremiumFeature::BusinessHours: return "business_hours"; diff --git a/Telegram/SourceFiles/boxes/premium_preview_box.h b/Telegram/SourceFiles/boxes/premium_preview_box.h index e631c97897..9ac7d04ac2 100644 --- a/Telegram/SourceFiles/boxes/premium_preview_box.h +++ b/Telegram/SourceFiles/boxes/premium_preview_box.h @@ -72,6 +72,7 @@ enum class PremiumFeature { Business, Effects, FilterTags, + TodoLists, // Business features. BusinessLocation, diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index 38f10e8885..f54f5e6add 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -2332,7 +2332,10 @@ MediaTodoList::~MediaTodoList() { } std::unique_ptr<Media> MediaTodoList::clone(not_null<HistoryItem*> parent) { - return std::make_unique<MediaTodoList>(parent, _todolist); + const auto id = parent->fullId(); + return std::make_unique<MediaTodoList>( + parent, + parent->history()->owner().duplicateTodoList(id, _todolist)); } TodoListData *MediaTodoList::todolist() const { diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index b173fe8b18..490db7d8df 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4142,6 +4142,19 @@ not_null<TodoListData*> Session::processTodoList( return result; } +not_null<TodoListData*> Session::duplicateTodoList( + TodoListId id, + not_null<TodoListData*> existing) { + const auto result = todoList(id); + result->title = existing->title; + result->items = existing->items; + for (auto &item : result->items) { + item.completedBy = nullptr; + item.completionDate = TimeId(); + } + return result; +} + void Session::checkPollsClosings() { const auto now = base::unixtime::now(); auto closest = 0; diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 62083465bf..825e497ff6 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -698,6 +698,9 @@ public: not_null<TodoListData*> processTodoList( TodoListId id, const MTPDmessageMediaToDo &data); + [[nodiscard]] not_null<TodoListData*> duplicateTodoList( + TodoListId id, + not_null<TodoListData*> existing); [[nodiscard]] not_null<CloudImage*> location( const LocationPoint &point); diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp index e061a65bb4..a41b10e1a9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.cpp @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_todo_list.h" #include "base/unixtime.h" +#include "core/application.h" +#include "core/click_handler_types.h" #include "core/ui_integration.h" // TextContext #include "lang/lang_keys.h" #include "history/history.h" @@ -35,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "apiwrap.h" #include "api/api_todo_lists.h" +#include "window/window_peer_menu.h" #include "styles/style_chat.h" #include "styles/style_widgets.h" #include "styles/style_window.h" @@ -324,6 +327,18 @@ void TodoList::startToggleAnimation(Task &task) { } void TodoList::toggleCompletion(int id) { + if (!canComplete()) { + _parent->delegate()->elementShowTooltip( + tr::lng_todo_mark_restricted( + tr::now, + lt_user, + Ui::Text::Bold(_parent->data()->from()->shortName()), + Ui::Text::RichLangValue), [] {}); + return; + } else if (!_parent->history()->session().premium()) { + Window::PeerMenuTodoWantsPremium(Window::TodoWantsPremium::Mark); + return; + } const auto i = ranges::find( _tasks, id, @@ -477,7 +492,11 @@ int TodoList::paintTask( p.setOpacity(1.); } - paintRadio(p, task, left, top, context); + if (canComplete()) { + paintRadio(p, task, left, top, context); + } else { + paintStatus(p, task, left, top, context); + } top += st::historyPollAnswerPadding.top(); p.setPen(stm->historyTextFg); @@ -584,6 +603,39 @@ void TodoList::paintRadio( p.setOpacity(o); } +void TodoList::paintStatus( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const { + top += st::historyPollAnswerPadding.top(); + + const auto stm = context.messageStyle(); + + const auto &radio = st::historyPollRadio; + const auto completed = (task.completionDate != 0); + + const auto rect = QRect(left, top, radio.diameter, radio.diameter); + if (completed) { + const auto &icon = stm->historyPollChosen; + icon.paint( + p, + left + (radio.diameter - icon.width()) / 2, + top + (radio.diameter - icon.height()) / 2, + width(), + stm->msgFileBg->c); + } else { + p.setPen(Qt::NoPen); + p.setBrush(stm->msgFileBg); + + PainterHighQualityEnabler hq(p); + p.drawEllipse(style::centerrect( + rect, + QRect(0, 0, st::mediaUnreadSize, st::mediaUnreadSize))); + } +} + TextSelection TodoList::adjustSelection( TextSelection selection, TextSelectType type) const { @@ -600,7 +652,6 @@ TextForMimeData TodoList::selectedText(TextSelection selection) const { TextState TodoList::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); - const auto can = canComplete(); const auto padding = st::msgPadding; auto paintw = width(); auto tshift = st::historyPollQuestionTop; @@ -622,10 +673,9 @@ TextState TodoList::textState(QPoint point, StateRequest request) const { for (const auto &task : _tasks) { const auto height = countTaskHeight(task, paintw); if (point.y() >= tshift && point.y() < tshift + height) { - if (can) { - _lastLinkPoint = point; - result.link = task.handler; - } else if (task.completionDate) { + _lastLinkPoint = point; + result.link = task.handler; + if (task.completionDate) { result.customTooltip = true; using Flag = Ui::Text::StateRequest::Flag; if (request.flags & Flag::LookupCustomTooltip) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h index bc6e5ee08a..37cb8ad2bd 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_todo_list.h +++ b/Telegram/SourceFiles/history/view/media/history_view_todo_list.h @@ -103,6 +103,12 @@ private: int left, int top, const PaintContext &context) const; + void paintStatus( + Painter &p, + const Task &task, + int left, + int top, + const PaintContext &context) const; void paintBottom( Painter &p, int left, diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 88d3a75fc9..d6ca306167 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -2628,7 +2628,7 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu( } if (peer->canCreateTodoLists()) { ++minimal; - raw->addAction(tr::lng_todo_create(tr::now), [=] { + raw->addAction(tr::lng_todo_menu_item(tr::now), [=] { const auto action = actionFactory(); const auto source = action.options.scheduled ? Api::SendType::Scheduled diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 79535dfe8f..1a44c95ac2 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -386,6 +386,15 @@ using Order = std::vector<QString>; PremiumFeature::Effects, }, }, + { + u"todo_lists"_q,AssertIsDebug() + Entry{ + &st::settingsPremiumIconTranslations, + tr::lng_premium_summary_subtitle_todo_lists(), + tr::lng_premium_summary_about_todo_lists(), + PremiumFeature::TodoLists, + }, + }, }; } @@ -1608,6 +1617,8 @@ std::vector<PremiumFeature> PremiumFeaturesOrder( return PremiumFeature::Wallpapers; } else if (s == u"effects"_q) { return PremiumFeature::Effects; + } else if (s == u"todo_lists"_q) {AssertIsDebug() + return PremiumFeature::TodoLists; } return PremiumFeature::kCount; }) | ranges::views::filter([](PremiumFeature type) { diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 61085178e6..f869102e19 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -98,6 +98,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "export/export_manager.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/premium_preview_box.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_boxes.h" @@ -1920,12 +1921,50 @@ void PeerMenuCreatePoll( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +void PeerMenuTodoWantsPremium(TodoWantsPremium type) { + const auto window = Core::App().activeWindow(); + if (!window) { + return; + } + const auto filter = [=](const auto &...) { + if (const auto controller = window->sessionController()) { + ShowPremiumPreviewBox(controller, PremiumFeature::TodoLists); + window->activate(); + } + return false; + }; + const auto link = Ui::Text::Link( + Ui::Text::Semibold(tr::lng_todo_premium_link(tr::now))); + const auto text = [&] { + switch (type) { + case TodoWantsPremium::Create: return tr::lng_todo_create_premium; + case TodoWantsPremium::Add: return tr::lng_todo_add_premium; + case TodoWantsPremium::Mark: return tr::lng_todo_mark_premium; + } + Unexpected("Type in PeerMenuTodoWantsPremium."); + }(); + constexpr auto kToastDuration = crl::time(4000); + window->uiShow()->showToast(Ui::Toast::Config{ + .text = text( + tr::now, + lt_link, + link, + Ui::Text::WithEntities), + .filter = filter, + .duration = kToastDuration, + }); +} + void PeerMenuCreateTodoList( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo, Api::SendType sendType, SendMenu::Details sendMenuDetails) { + if (!peer->session().premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Create); + return; + } auto starsRequired = peer->session().changes().peerFlagsValue( peer, Data::PeerUpdate::Flag::FullInfo diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 59beef33b7..18da8845c6 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -111,6 +111,12 @@ void PeerMenuCreatePoll( PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +enum class TodoWantsPremium { + Create, + Add, + Mark, +}; +void PeerMenuTodoWantsPremium(TodoWantsPremium type); void PeerMenuCreateTodoList( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, From 248fe1b53fdf3c571dfd82818ed0ad4d271cb145 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 12 Jun 2025 12:22:06 +0400 Subject: [PATCH 178/310] Add tasks to todo lists. --- Telegram/SourceFiles/api/api_todo_lists.cpp | 35 ++- Telegram/SourceFiles/api/api_todo_lists.h | 8 +- .../boxes/create_todo_list_box.cpp | 235 +++++++++++++++--- .../SourceFiles/boxes/create_todo_list_box.h | 29 +++ Telegram/SourceFiles/data/data_todo_list.cpp | 21 +- Telegram/SourceFiles/data/data_todo_list.h | 3 + .../history/history_inner_widget.cpp | 20 ++ .../history/view/history_view_element.cpp | 2 +- .../SourceFiles/window/window_peer_menu.cpp | 27 +- .../SourceFiles/window/window_peer_menu.h | 3 + 10 files changed, 325 insertions(+), 58 deletions(-) diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp index e72e8294ad..123b9d10f6 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.cpp +++ b/Telegram/SourceFiles/api/api_todo_lists.cpp @@ -42,7 +42,7 @@ void TodoLists::create( const TodoListData &data, SendAction action, Fn<void()> done, - Fn<void()> fail) { + Fn<void(QString)> fail) { _session->api().sendAction(action); const auto history = action.history; @@ -118,7 +118,9 @@ void TodoLists::create( (action.options.scheduled ? Data::HistoryUpdate::Flag::ScheduledSent : Data::HistoryUpdate::Flag::MessageSent)); - done(); + if (const auto onstack = done) { + onstack(); + } }, [=](const MTP::Error &error, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( @@ -126,10 +128,37 @@ void TodoLists::create( monoforumPeerId, UnixtimeFromMsgId(response.outerMsgId)); } - fail(); + if (const auto onstack = fail) { + onstack(error.type()); + } }); } +void TodoLists::add( + not_null<HistoryItem*> item, + const std::vector<TodoListItem> &items, + Fn<void()> done, + Fn<void(QString)> fail) { + if (items.empty()) { + return; + } + const auto session = _session; + _session->api().request(MTPmessages_AppendTodoList( + item->history()->peer->input, + MTP_int(item->id.bare), + TodoListItemsToMTP(&item->history()->session(), items) + )).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + if (const auto onstack = done) { + onstack(); + } + }).fail([=](const MTP::Error &error) { + if (const auto onstack = fail) { + onstack(error.type()); + } + }).send(); +} + void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) { auto &entry = _toggles[itemId]; if (completed) { diff --git a/Telegram/SourceFiles/api/api_todo_lists.h b/Telegram/SourceFiles/api/api_todo_lists.h index abb8c72d2a..7331a28407 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.h +++ b/Telegram/SourceFiles/api/api_todo_lists.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class ApiWrap; class HistoryItem; +struct TodoListItem; struct TodoListData; namespace Main { @@ -30,7 +31,12 @@ public: const TodoListData &data, SendAction action, Fn<void()> done, - Fn<void()> fail); + Fn<void(QString)> fail); + void add( + not_null<HistoryItem*> item, + const std::vector<TodoListItem> &items, + Fn<void()> done, + Fn<void(QString)> fail); void toggleCompletion(FullMsgId itemId, int id, bool completed); private: diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp b/Telegram/SourceFiles/boxes/create_todo_list_box.cpp index d251893d95..846c3aabea 100644 --- a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp +++ b/Telegram/SourceFiles/boxes/create_todo_list_box.cpp @@ -17,11 +17,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/tabbed_selector.h" #include "core/application.h" #include "core/core_settings.h" +#include "data/data_changes.h" +#include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_todo_list.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/view/history_view_schedule_box.h" +#include "history/history_item.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -60,7 +63,9 @@ public: not_null<Ui::BoxContent*> box, not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller, - ChatHelpers::TabbedPanel *emojiPanel); + ChatHelpers::TabbedPanel *emojiPanel, + std::vector<TodoListItem> existing = {}, + bool existingLocked = false); [[nodiscard]] bool hasTasks() const; [[nodiscard]] bool isValid() const; @@ -79,7 +84,10 @@ private: not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, - int position); + int id, + TextWithEntities text, + int position, + bool locked); Task(const Task &other) = delete; Task &operator=(const Task &other) = delete; @@ -93,6 +101,8 @@ private: void createShadow(); void destroyShadow(); + [[nodiscard]] int id() const; + [[nodiscard]] bool locked() const; [[nodiscard]] bool isEmpty() const; [[nodiscard]] bool isGood() const; [[nodiscard]] bool isTooLong() const; @@ -105,7 +115,7 @@ private: [[nodiscard]] not_null<Ui::InputField*> field() const; - [[nodiscard]] TodoListItem toTodoListItem(int index) const; + [[nodiscard]] TodoListItem toTodoListItem(int nextId) const; [[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const; @@ -114,6 +124,7 @@ private: void createWarning(); void updateFieldGeometry(); + int _id = 0; base::unique_qptr<Ui::SlideWrap<Ui::RpWidget>> _wrap; not_null<Ui::RpWidget*> _content; Ui::InputField *_field = nullptr; @@ -129,6 +140,11 @@ private: void fixShadows(); void removeEmptyTail(); void addEmptyTask(); + void addTask( + int id, + TextWithEntities text, + anim::type animated); + void initTaskField(not_null<Task*> task); void checkLastTask(); void validateState(); void fixAfterErase(); @@ -139,6 +155,8 @@ private: not_null<Ui::BoxContent*> _box; not_null<Ui::VerticalLayout*> _container; const not_null<Window::SessionController*> _controller; + const int _existingCount = 0; + const bool _existingLocked = false; ChatHelpers::TabbedPanel * const _emojiPanel; int _position = 0; int _tasksLimit = 0; @@ -210,8 +228,12 @@ Tasks::Task::Task( not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, - int position) -: _wrap(container->insert( + int id, + TextWithEntities text, + int position, + bool locked) +: _id(id) +, _wrap(container->insert( position, object_ptr<Ui::SlideWrap<Ui::RpWidget>>( container, @@ -228,8 +250,17 @@ Tasks::Task::Task( , _limit(session->appConfig().todoListItemTextLimit()) { InitField(outer, _field, session); _field->setMaxLength(_limit + kErrorLimit); + _field->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); + _field->finishAnimating(); _field->show(); - _field->customTab(true); + if (locked) { + _field->setDisabled(true); + } else { + _field->customTab(true); + } _wrap->hide(anim::type::instant); @@ -244,8 +275,10 @@ Tasks::Task::Task( }, _field->lifetime()); createShadow(); - createRemove(); - createWarning(); + if (!locked) { + createRemove(); + createWarning(); + } updateFieldGeometry(); } @@ -345,7 +378,9 @@ bool Tasks::Task::isEmpty() const { } bool Tasks::Task::isGood() const { - return !field()->getLastText().trimmed().isEmpty() && !isTooLong(); + return !locked() + && !field()->getLastText().trimmed().isEmpty() + && !isTooLong(); } bool Tasks::Task::isTooLong() const { @@ -357,7 +392,9 @@ bool Tasks::Task::hasFocus() const { } void Tasks::Task::setFocus() const { - FocusAtEnd(field()); + if (!locked()) { + FocusAtEnd(field()); + } } void Tasks::Task::clearValue() { @@ -369,7 +406,9 @@ void Tasks::Task::setPlaceholder() const { } void Tasks::Task::toggleRemoveAlways(bool toggled) { - *_removeAlways = toggled; + if (_removeAlways) { + *_removeAlways = toggled; + } } void Tasks::Task::updateFieldGeometry() { @@ -385,37 +424,49 @@ void Tasks::Task::removePlaceholder() const { field()->setPlaceholder(rpl::single(QString())); } -TodoListItem Tasks::Task::toTodoListItem(int index) const { - Expects(index >= 0 && index < kMaxOptionsCount); +int Tasks::Task::id() const { + return _id; +} +bool Tasks::Task::locked() const { + return !_remove; +} + +TodoListItem Tasks::Task::toTodoListItem(int nextId) const { const auto text = field()->getTextWithTags(); - auto result = TodoListItem{ .text = TextWithEntities{ .text = text.text, .entities = TextUtilities::ConvertTextTagsToEntities(text.tags), }, - .id = (index + 1) + .id = _id ? _id : nextId, }; TextUtilities::Trim(result.text); return result; } rpl::producer<Qt::MouseButton> Tasks::Task::removeClicks() const { - return _remove->clicks(); + return _remove ? _remove->clicks() : rpl::never<Qt::MouseButton>(); } Tasks::Tasks( not_null<Ui::BoxContent*> box, not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller, - ChatHelpers::TabbedPanel *emojiPanel) + ChatHelpers::TabbedPanel *emojiPanel, + std::vector<TodoListItem> existing, + bool existingLocked) : _box(box) , _container(container) , _controller(controller) +, _existingCount(existing.size()) +, _existingLocked(existingLocked) , _emojiPanel(emojiPanel) , _position(_container->count()) , _tasksLimit(controller->session().appConfig().todoListItemsLimit()) { + for (const auto &task : existing) { + addTask(task.id, task.text, anim::type::instant); + } checkLastTask(); } @@ -466,22 +517,21 @@ void Tasks::Task::destroy(FnMut<void()> done) { std::vector<TodoListItem> Tasks::toTodoListItems() const { auto result = std::vector<TodoListItem>(); result.reserve(_list.size()); - auto counter = int(0); - const auto makeTask = [&](const std::unique_ptr<Task> &task) { - return task->toTodoListItem(counter++); - }; - ranges::copy( - _list - | ranges::views::filter(&Task::isGood) - | ranges::views::transform(makeTask), - ranges::back_inserter(result)); + auto usedId = 0; + for (const auto &task : _list) { + if (task->isGood()) { + result.push_back(task->toTodoListItem(++usedId)); + } else if (const auto id = task->id()) { + usedId = std::max(usedId, id); + } + } return result; } void Tasks::focusFirst() { - Expects(!_list.empty()); - - _list.front()->setFocus(); + const auto locked = _existingLocked ? _existingCount : 0; + Assert(locked < _list.size()); + FocusAtEnd((_list.begin() + locked)->get()->field()); } bool Tasks::correctShadows() const { @@ -549,21 +599,45 @@ void Tasks::fixAfterErase() { } void Tasks::addEmptyTask() { - if (full()) { + if (!_list.empty() && _list.back()->isEmpty()) { return; - } else if (!_list.empty() && _list.back()->isEmpty()) { + } + const auto locked = _existingLocked ? _existingCount : 0; + addTask( + 0, // id + TextWithEntities(), + (locked < _list.size()) ? anim::type::normal : anim::type::instant); +} + +void Tasks::addTask( + int id, + TextWithEntities text, + anim::type animated) { + if (full()) { return; } if (_list.size() > 1) { (*(_list.end() - 2))->removePlaceholder(); (*(_list.end() - 2))->toggleRemoveAlways(true); } + const auto locked = id && _existingLocked; _list.push_back(std::make_unique<Task>( _box, _container, &_controller->session(), - _position + _list.size() + _destroyed.size())); - const auto field = _list.back()->field(); + id, + std::move(text), + _position + _list.size() + _destroyed.size(), + locked)); + if (!locked) { + initTaskField(_list.back().get()); + } + _list.back()->show(animated); + fixShadows(); +} + +void Tasks::initTaskField(not_null<Task*> task) { + const auto field = task->field(); if (const auto emojiPanel = _emojiPanel) { const auto emojiToggle = Ui::AddEmojiToggleToField( field, @@ -637,7 +711,7 @@ void Tasks::addEmptyTask() { return base::EventFilterResult::Cancel; }); - _list.back()->removeClicks( + task->removeClicks( ) | rpl::start_with_next([=] { Ui::PostponeCall(crl::guard(field, [=] { Expects(!_list.empty()); @@ -656,11 +730,6 @@ void Tasks::addEmptyTask() { validateState(); })); }, field->lifetime()); - - _list.back()->show((_list.size() == 1) - ? anim::type::instant - : anim::type::normal); - fixShadows(); } void Tasks::removeDestroyed(not_null<Task*> task) { @@ -678,7 +747,9 @@ void Tasks::validateState() { _isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong); const auto lastEmpty = !_list.empty() && _list.back()->isEmpty(); - _usedCount = _list.size() - (lastEmpty ? 1 : 0); + _usedCount = _list.size() + - (lastEmpty ? 1 : 0) + - (_existingLocked ? _existingCount : 0); } int Tasks::findField(not_null<Ui::InputField*> field) const { @@ -728,7 +799,7 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle( using namespace Settings; const auto session = &_controller->session(); - const auto isPremium = session->user()->isPremium(); + const auto isPremium = session->premium(); const auto title = container->add( object_ptr<Ui::InputField>( @@ -993,3 +1064,85 @@ void CreateTodoListBox::prepare() { setDimensionsToContent(st::boxWideWidth, inner); } + +AddTodoListTasksBox::AddTodoListTasksBox( + QWidget*, + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) +: _controller(controller) +, _item(item) { + _controller->session().changes().messageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + if (update.item == item) { + closeBox(); + } + }, lifetime()); +} + +void AddTodoListTasksBox::prepare() { + setTitle(tr::lng_todo_add_title()); + + const auto inner = setInnerWidget(setupContent()); + + setDimensionsToContent(st::boxWideWidth, inner); + + scrollToY(ScrollMax); +} + +object_ptr<Ui::RpWidget> AddTodoListTasksBox::setupContent() { + auto result = object_ptr<Ui::VerticalLayout>(this); + const auto container = result.data(); + + const auto tasks = lifetime().make_state<Tasks>( + this, + container, + _controller, + _emojiPanel ? _emojiPanel.get() : nullptr, + _item->media()->todolist()->items, + true); + auto limit = tasks->usedCount() | rpl::after_next([=](int count) { + setCloseByEscape(!count); + setCloseByOutsideClick(!count); + }) | rpl::map([=](int count) { + const auto appConfig = &_controller->session().appConfig(); + const auto max = appConfig->todoListItemsLimit(); + return (count < max) + ? tr::lng_todo_create_limit(tr::now, lt_count, max - count) + : tr::lng_todo_create_maximum(tr::now); + }) | rpl::after_next([=] { + container->resizeToWidth(container->widthNoMargins()); + }); + container->add( + object_ptr<Ui::DividerLabel>( + container, + object_ptr<Ui::FlatLabel>( + container, + std::move(limit), + st::boxDividerLabel), + st::createPollLimitPadding)); + + _setInnerFocus = [=] { + tasks->focusFirst(); + }; + + tasks->scrollToWidget( + ) | rpl::start_with_next([=](not_null<QWidget*> widget) { + scrollToWidget(widget); + }, lifetime()); + + const auto submit = addButton(tr::lng_settings_save(), [=] { + _submitRequests.fire({ tasks->toTodoListItems() }); + }); + addButton(tr::lng_cancel(), [=] { closeBox(); }); + + return result; +} + +auto AddTodoListTasksBox::submitRequests() const -> rpl::producer<Result> { + return _submitRequests.events(); +} + +void AddTodoListTasksBox::setInnerFocus() { + _setInnerFocus(); +} diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.h b/Telegram/SourceFiles/boxes/create_todo_list_box.h index 3b89ca5e35..3b9078150e 100644 --- a/Telegram/SourceFiles/boxes/create_todo_list_box.h +++ b/Telegram/SourceFiles/boxes/create_todo_list_box.h @@ -76,3 +76,32 @@ private: int _titleLimit = 0; }; + +class AddTodoListTasksBox : public Ui::BoxContent { +public: + struct Result { + std::vector<TodoListItem> items; + }; + + AddTodoListTasksBox( + QWidget*, + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item); + + [[nodiscard]] rpl::producer<Result> submitRequests() const; + + void setInnerFocus() override; + +protected: + void prepare() override; + +private: + [[nodiscard]] object_ptr<Ui::RpWidget> setupContent(); + + const not_null<Window::SessionController*> _controller; + const not_null<HistoryItem*> _item; + base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; + Fn<void()> _setInnerFocus; + rpl::event_stream<Result> _submitRequests; + +}; diff --git a/Telegram/SourceFiles/data/data_todo_list.cpp b/Telegram/SourceFiles/data/data_todo_list.cpp index 1bb1a86859..d50bcf6296 100644 --- a/Telegram/SourceFiles/data/data_todo_list.cpp +++ b/Telegram/SourceFiles/data/data_todo_list.cpp @@ -177,22 +177,23 @@ bool TodoListData::othersCanComplete() const { return (_flags & Flag::OthersCanComplete); } -MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) { +MTPVector<MTPTodoItem> TodoListItemsToMTP( + not_null<Main::Session*> session, + const std::vector<TodoListItem> &tasks) { const auto convert = [&](const TodoListItem &item) { return MTP_todoItem( MTP_int(item.id), MTP_textWithEntities( MTP_string(item.text.text), - Api::EntitiesToMTP( - &todolist->session(), - item.text.entities))); + Api::EntitiesToMTP(session, item.text.entities))); }; auto items = QVector<MTPTodoItem>(); - items.reserve(todolist->items.size()); - ranges::transform( - todolist->items, - ranges::back_inserter(items), - convert); + items.reserve(tasks.size()); + ranges::transform(tasks, ranges::back_inserter(items), convert); + return MTP_vector<MTPTodoItem>(items); +} + +MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) { using Flag = MTPDtodoList::Flag; const auto flags = Flag() | (todolist->othersCanAppend() @@ -208,7 +209,7 @@ MTPTodoList TodoListDataToMTP(not_null<const TodoListData*> todolist) { Api::EntitiesToMTP( &todolist->session(), todolist->title.entities)), - MTP_vector<MTPTodoItem>(items)); + TodoListItemsToMTP(&todolist->session(), todolist->items)); } MTPInputMedia TodoListDataToInputMedia( diff --git a/Telegram/SourceFiles/data/data_todo_list.h b/Telegram/SourceFiles/data/data_todo_list.h index 3ba84c6c78..a94d8c6760 100644 --- a/Telegram/SourceFiles/data/data_todo_list.h +++ b/Telegram/SourceFiles/data/data_todo_list.h @@ -70,6 +70,9 @@ private: }; +[[nodiscard]] MTPVector<MTPTodoItem> TodoListItemsToMTP( + not_null<Main::Session*> session, + const std::vector<TodoListItem> &tasks); [[nodiscard]] MTPTodoList TodoListDataToMTP( not_null<const TodoListData*> todolist); [[nodiscard]] MTPInputMedia TodoListDataToInputMedia( diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 3978655b7b..b00d999063 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -94,6 +94,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_click_handler.h" #include "data/data_histories.h" #include "data/data_changes.h" +#include "data/data_todo_list.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "styles/style_chat.h" #include "styles/style_menu_icons.h" @@ -2719,6 +2720,24 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } }; + const auto addTodoListAction = [&](HistoryItem *item) { + const auto media = item ? item->media() : nullptr; + const auto todolist = media ? media->todolist() : nullptr; + if (!todolist + || !item->isRegular() + || (!item->out() && !todolist->othersCanAppend())) { + return; + } + const auto itemId = item->fullId(); + _menu->addAction( + tr::lng_todo_add_title(tr::now), + crl::guard(this, [=] { + if (const auto item = session->data().message(itemId)) { + Window::PeerMenuAddTodoListTasks(_controller, item); + } + }), + &st::menuIconCreateTodoList); + }; const auto lnkPhoto = link ? reinterpret_cast<PhotoData*>( link->property(kPhotoLinkMediaProperty).toULongLong()) @@ -2889,6 +2908,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { addItemActions(item, item); } else { addReplyAction(partItemOrLeader); + addTodoListAction(partItemOrLeader); addItemActions(item, albumPartItem); if (item && !isUponSelected) { const auto media = (view ? view->media() : nullptr); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index cd5d3bc339..08a2373df5 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -620,7 +620,7 @@ int ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) { st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } auto contentWidth = width; - contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins + contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.right(); if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; } diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index f869102e19..bb9604b89d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -2017,9 +2017,9 @@ void PeerMenuCreateTodoList( api->todoLists().create(result.todolist, action, crl::guard(weak, [=] { state->create = nullptr; weak->closeBox(); - }), crl::guard(weak, [=] { + }), crl::guard(weak, [=](const QString &error) { state->lock = false; - weak->submitFailed(tr::lng_attach_failed(tr::now)); + weak->submitFailed(error); })); }; box->submitRequests( @@ -2027,6 +2027,29 @@ void PeerMenuCreateTodoList( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +void PeerMenuAddTodoListTasks( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto session = &item->history()->session(); + if (!session->premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Add); + return; + } + auto box = Box<AddTodoListTasksBox>(controller, item); + const auto raw = box.data(); + box->submitRequests( + ) | rpl::start_with_next([=](const AddTodoListTasksBox::Result &result) { + const auto show = raw->uiShow(); + raw->closeBox(); + session->api().todoLists().add( + item, + result.items, + [] {}, + [=](const QString &error) { show->showToast(error); }); + }, box->lifetime()); + controller->show(std::move(box), Ui::LayerOption::CloseOther); +} + void PeerMenuBlockUserBox( not_null<Ui::GenericBox*> box, not_null<Window::Controller*> window, diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 18da8845c6..4b5419ff8a 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -123,6 +123,9 @@ void PeerMenuCreateTodoList( FullReplyTo replyTo = FullReplyTo(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +void PeerMenuAddTodoListTasks( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item); void PeerMenuDeleteTopicWithConfirmation( not_null<Window::SessionNavigation*> navigation, not_null<Data::ForumTopic*> topic); From d83a80ec53684b91ca80c0359138346bedd0a644 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 12 Jun 2025 14:39:25 +0400 Subject: [PATCH 179/310] Improve adding tasks to todo lists. --- .../boxes/create_todo_list_box.cpp | 78 +++++++++++-------- .../history/history_inner_widget.cpp | 6 +- .../view/history_view_context_menu.cpp | 29 ++++++- .../SourceFiles/window/window_peer_menu.cpp | 16 ++++ .../SourceFiles/window/window_peer_menu.h | 1 + 5 files changed, 93 insertions(+), 37 deletions(-) diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp b/Telegram/SourceFiles/boxes/create_todo_list_box.cpp index 846c3aabea..ec20b1bfff 100644 --- a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp +++ b/Telegram/SourceFiles/boxes/create_todo_list_box.cpp @@ -72,7 +72,7 @@ public: [[nodiscard]] std::vector<TodoListItem> toTodoListItems() const; void focusFirst(); - [[nodiscard]] rpl::producer<int> usedCount() const; + [[nodiscard]] rpl::producer<int> addedCount() const; [[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const; [[nodiscard]] rpl::producer<> backspaceInFront() const; [[nodiscard]] rpl::producer<> tabbed() const; @@ -162,7 +162,7 @@ private: int _tasksLimit = 0; std::vector<std::unique_ptr<Task>> _list; std::vector<std::unique_ptr<Task>> _destroyed; - rpl::variable<int> _usedCount = 0; + rpl::variable<int> _addedCount = 0; bool _hasTasks = false; bool _isValid = false; rpl::event_stream<not_null<QWidget*>> _scrollToWidget; @@ -224,6 +224,26 @@ void FocusAtEnd(not_null<Ui::InputField*> field) { field->ensureCursorVisible(); } +[[nodiscard]] base::unique_qptr<ChatHelpers::TabbedPanel> MakeEmojiPanel( + not_null<QWidget*> outer, + not_null<Window::SessionController*> controller) { + auto result = base::make_unique_q<ChatHelpers::TabbedPanel>( + outer, + controller, + object_ptr<ChatHelpers::TabbedSelector>( + nullptr, + controller->uiShow(), + Window::GifPauseReason::Layer, + ChatHelpers::TabbedSelector::Mode::EmojiOnly)); + result->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + result ->hide(); + result->selector()->setCurrentPeer(controller->session().user()); + return result; +} + Tasks::Task::Task( not_null<QWidget*> outer, not_null<Ui::VerticalLayout*> container, @@ -482,8 +502,8 @@ bool Tasks::isValid() const { return _isValid; } -rpl::producer<int> Tasks::usedCount() const { - return _usedCount.value(); +rpl::producer<int> Tasks::addedCount() const { + return _addedCount.value(); } rpl::producer<not_null<QWidget*>> Tasks::scrollToWidget() const { @@ -747,7 +767,7 @@ void Tasks::validateState() { _isValid = _hasTasks && ranges::none_of(_list, &Task::isTooLong); const auto lastEmpty = !_list.empty() && _list.back()->isEmpty(); - _usedCount = _list.size() + _addedCount = _list.size() - (lastEmpty ? 1 : 0) - (_existingLocked ? _existingCount : 0); } @@ -817,37 +837,22 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle( title->customTab(true); if (isPremium) { - using Selector = ChatHelpers::TabbedSelector; - const auto outer = getDelegate()->outerContainer(); - _emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>( - outer, - _controller, - object_ptr<Selector>( - nullptr, - _controller->uiShow(), - Window::GifPauseReason::Layer, - Selector::Mode::EmojiOnly)); - const auto emojiPanel = _emojiPanel.get(); - emojiPanel->setDesiredHeightValues( - 1., - st::emojiPanMinHeight / 2, - st::emojiPanMinHeight); - emojiPanel->hide(); - emojiPanel->selector()->setCurrentPeer(session->user()); - + _emojiPanel = MakeEmojiPanel( + getDelegate()->outerContainer(), + _controller); const auto emojiToggle = Ui::AddEmojiToggleToField( title, this, _controller, - emojiPanel, + _emojiPanel.get(), st::createPollOptionFieldPremiumEmojiPosition); - emojiPanel->selector()->emojiChosen( + _emojiPanel->selector()->emojiChosen( ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { if (title->hasFocus()) { Ui::InsertEmojiAtCursor(title->textCursor(), data.emoji); } }, emojiToggle->lifetime()); - emojiPanel->selector()->customEmojiChosen( + _emojiPanel->selector()->customEmojiChosen( ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { if (title->hasFocus()) { Data::InsertCustomEmoji(title, data.document); @@ -906,7 +911,7 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { container, _controller, _emojiPanel ? _emojiPanel.get() : nullptr); - auto limit = tasks->usedCount() | rpl::after_next([=](int count) { + auto limit = tasks->addedCount() | rpl::after_next([=](int count) { setCloseByEscape(!count); setCloseByOutsideClick(!count); }) | rpl::map([=](int count) { @@ -1094,21 +1099,32 @@ object_ptr<Ui::RpWidget> AddTodoListTasksBox::setupContent() { auto result = object_ptr<Ui::VerticalLayout>(this); const auto container = result.data(); + if (_controller->session().premium()) { + _emojiPanel = MakeEmojiPanel( + getDelegate()->outerContainer(), + _controller); + } + + const auto media = _item->media(); + const auto todolist = media ? media->todolist() : nullptr; + Assert(todolist != nullptr); const auto tasks = lifetime().make_state<Tasks>( this, container, _controller, _emojiPanel ? _emojiPanel.get() : nullptr, - _item->media()->todolist()->items, + todolist->items, true); - auto limit = tasks->usedCount() | rpl::after_next([=](int count) { + const auto already = int(todolist->items.size()); + auto limit = tasks->addedCount() | rpl::after_next([=](int count) { setCloseByEscape(!count); setCloseByOutsideClick(!count); }) | rpl::map([=](int count) { const auto appConfig = &_controller->session().appConfig(); const auto max = appConfig->todoListItemsLimit(); - return (count < max) - ? tr::lng_todo_create_limit(tr::now, lt_count, max - count) + const auto total = already + count; + return (total < max) + ? tr::lng_todo_create_limit(tr::now, lt_count, max - total) : tr::lng_todo_create_maximum(tr::now); }) | rpl::after_next([=] { container->resizeToWidth(container->widthNoMargins()); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index b00d999063..3f8b82b933 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2721,11 +2721,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { }; const auto addTodoListAction = [&](HistoryItem *item) { - const auto media = item ? item->media() : nullptr; - const auto todolist = media ? media->todolist() : nullptr; - if (!todolist - || !item->isRegular() - || (!item->out() && !todolist->othersCanAppend())) { + if (!item || !Window::PeerMenuShowAddTodoListTasks(item)) { return; } const auto itemId = item->fullId(); diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index 58ea3e210e..a9b98a98d6 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -626,7 +626,9 @@ bool AddReplyToMessageAction( const auto peer = item ? item->history()->peer.get() : nullptr; if (!item || !item->isRegular() - || (context != Context::History && context != Context::Replies)) { + || (context != Context::History + && context != Context::Replies + && context != Context::Monoforum)) { return false; } const auto canSendReply = topic @@ -653,6 +655,30 @@ bool AddReplyToMessageAction( return true; } +bool AddTodoListAction( + not_null<Ui::PopupMenu*> menu, + const ContextMenuRequest &request, + not_null<ListWidget*> list) { + const auto context = list->elementContext(); + const auto item = request.item; + if (!item + || !Window::PeerMenuShowAddTodoListTasks(item) + || (context != Context::History + && context != Context::Replies + && context != Context::Monoforum + && context != Context::Pinned)) { + return false; + } + const auto itemId = item->fullId(); + const auto controller = list->controller(); + menu->addAction(tr::lng_todo_add_title(tr::now), [=] { + if (const auto item = controller->session().data().message(itemId)) { + Window::PeerMenuAddTodoListTasks(controller, item); + } + }, &st::menuIconCreateTodoList); + return true; +} + bool AddViewRepliesAction( not_null<Ui::PopupMenu*> menu, const ContextMenuRequest &request, @@ -1281,6 +1307,7 @@ base::unique_qptr<Ui::PopupMenu> FillContextMenu( st::popupMenuWithIcons); AddReplyToMessageAction(result, request, list); + AddTodoListAction(result, request, list); if (request.overSelection && !list->hasCopyRestrictionForSelected() diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index bb9604b89d..6457792767 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/delayed_activation.h" #include "ui/vertical_list.h" #include "ui/ui_utility.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "menu/menu_mute.h" @@ -2027,6 +2028,16 @@ void PeerMenuCreateTodoList( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item) { + const auto media = item ? item->media() : nullptr; + const auto todolist = media ? media->todolist() : nullptr; + const auto appConfig = &item->history()->session().appConfig(); + return item->isRegular() + && todolist + && (todolist->items.size() < appConfig->todoListItemsLimit()) + && (item->out() || todolist->othersCanAppend()); +} + void PeerMenuAddTodoListTasks( not_null<Window::SessionController*> controller, not_null<HistoryItem*> item) { @@ -2035,6 +2046,11 @@ void PeerMenuAddTodoListTasks( PeerMenuTodoWantsPremium(TodoWantsPremium::Add); return; } + const auto media = item->media(); + const auto todolist = media ? media->todolist() : nullptr; + if (!todolist) { + return; + } auto box = Box<AddTodoListTasksBox>(controller, item); const auto raw = box.data(); box->submitRequests( diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 4b5419ff8a..961ee18e86 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -123,6 +123,7 @@ void PeerMenuCreateTodoList( FullReplyTo replyTo = FullReplyTo(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +[[nodiscard]] bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item); void PeerMenuAddTodoListTasks( not_null<Window::SessionController*> controller, not_null<HistoryItem*> item); From 9290c90bdcdf0a93f46aa426aeca4fa4716f2fc0 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 12 Jun 2025 18:41:01 +0400 Subject: [PATCH 180/310] Allow fully editing todo lists. --- Telegram/CMakeLists.txt | 4 +- Telegram/SourceFiles/api/api_editing.cpp | 19 +++ Telegram/SourceFiles/api/api_editing.h | 7 ++ Telegram/SourceFiles/api/api_todo_lists.cpp | 20 +++- Telegram/SourceFiles/api/api_todo_lists.h | 7 ++ ...do_list_box.cpp => edit_todo_list_box.cpp} | 109 +++++++++++++----- ...e_todo_list_box.h => edit_todo_list_box.h} | 9 +- .../SourceFiles/data/data_media_types.cpp | 4 + Telegram/SourceFiles/data/data_media_types.h | 1 + Telegram/SourceFiles/data/data_peer.cpp | 3 +- .../SourceFiles/history/history_widget.cpp | 5 + .../history_view_compose_controls.cpp | 7 ++ .../view/history_view_chat_section.cpp | 2 + .../view/history_view_scheduled_section.cpp | 2 + .../business/settings_shortcut_messages.cpp | 3 + .../SourceFiles/window/window_peer_menu.cpp | 41 ++++++- .../SourceFiles/window/window_peer_menu.h | 3 + 17 files changed, 204 insertions(+), 42 deletions(-) rename Telegram/SourceFiles/boxes/{create_todo_list_box.cpp => edit_todo_list_box.cpp} (91%) rename Telegram/SourceFiles/boxes/{create_todo_list_box.h => edit_todo_list_box.h} (91%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e83797828e..b2f6cd3587 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -273,8 +273,6 @@ PRIVATE boxes/connection_box.h boxes/create_poll_box.cpp boxes/create_poll_box.h - boxes/create_todo_list_box.cpp - boxes/create_todo_list_box.h boxes/delete_messages_box.cpp boxes/delete_messages_box.h boxes/dictionaries_manager.cpp @@ -285,6 +283,8 @@ PRIVATE boxes/edit_caption_box.h boxes/edit_privacy_box.cpp boxes/edit_privacy_box.h + boxes/edit_todo_list_box.cpp + boxes/edit_todo_list_box.h boxes/gift_credits_box.cpp boxes/gift_credits_box.h boxes/gift_premium_box.cpp diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 4f5073a3b8..ed76f847f6 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/data_histories.h" #include "data/data_session.h" +#include "data/data_todo_list.h" #include "data/data_web_page.h" #include "history/view/controls/history_view_compose_media_edit_manager.h" #include "history/history.h" @@ -358,4 +359,22 @@ mtpRequestId EditTextMessage( std::nullopt); } +void EditTodoList( + not_null<HistoryItem*> item, + const TodoListData &data, + SendOptions options, + Fn<void(mtpRequestId requestId)> done, + Fn<void(const QString &error, mtpRequestId requestId)> fail) { + const auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) { + applyUpdates(); + done(id); + }; + EditMessage( + item, + options, + callback, + fail, + MTP_inputMediaTodo(TodoListDataToMTP(&data))); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_editing.h b/Telegram/SourceFiles/api/api_editing.h index 630e1cd8d5..ca3ff7c121 100644 --- a/Telegram/SourceFiles/api/api_editing.h +++ b/Telegram/SourceFiles/api/api_editing.h @@ -58,4 +58,11 @@ mtpRequestId EditTextMessage( Fn<void(const QString &error, mtpRequestId requestId)> fail, bool spoilered); +void EditTodoList( + not_null<HistoryItem*> item, + const TodoListData &data, + SendOptions options, + Fn<void(mtpRequestId requestId)> done, + Fn<void(const QString &error, mtpRequestId requestId)> fail); + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp index 123b9d10f6..ee0d64639a 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.cpp +++ b/Telegram/SourceFiles/api/api_todo_lists.cpp @@ -7,8 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "api/api_todo_lists.h" -//#include "api/api_common.h" -//#include "api/api_updates.h" +#include "api/api_editing.h" #include "apiwrap.h" #include "base/random.h" #include "data/business/data_shortcut_messages.h" // ShortcutIdToMTP @@ -134,6 +133,23 @@ void TodoLists::create( }); } +void TodoLists::edit( + not_null<HistoryItem*> item, + const TodoListData &data, + SendOptions options, + Fn<void()> done, + Fn<void(QString)> fail) { + EditTodoList(item, data, options, [=](mtpRequestId) { + if (const auto onstack = done) { + onstack(); + } + }, [=](const QString &error, mtpRequestId) { + if (const auto onstack = fail) { + onstack(error); + } + }); +} + void TodoLists::add( not_null<HistoryItem*> item, const std::vector<TodoListItem> &items, diff --git a/Telegram/SourceFiles/api/api_todo_lists.h b/Telegram/SourceFiles/api/api_todo_lists.h index 7331a28407..92d6a634b2 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.h +++ b/Telegram/SourceFiles/api/api_todo_lists.h @@ -22,6 +22,7 @@ class Session; namespace Api { struct SendAction; +struct SendOptions; class TodoLists final { public: @@ -32,6 +33,12 @@ public: SendAction action, Fn<void()> done, Fn<void(QString)> fail); + void edit( + not_null<HistoryItem*> item, + const TodoListData &data, + SendOptions options, + Fn<void()> done, + Fn<void(QString)> fail); void add( not_null<HistoryItem*> item, const std::vector<TodoListItem> &items, diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp similarity index 91% rename from Telegram/SourceFiles/boxes/create_todo_list_box.cpp rename to Telegram/SourceFiles/boxes/edit_todo_list_box.cpp index ec20b1bfff..65591aaf9a 100644 --- a/Telegram/SourceFiles/boxes/create_todo_list_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_todo_list_box.cpp @@ -5,7 +5,7 @@ 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/create_todo_list_box.h" +#include "boxes/edit_todo_list_box.h" #include "base/call_delayed.h" #include "base/event_filter.h" @@ -85,7 +85,6 @@ private: not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, int id, - TextWithEntities text, int position, bool locked); @@ -144,7 +143,7 @@ private: int id, TextWithEntities text, anim::type animated); - void initTaskField(not_null<Task*> task); + void initTaskField(not_null<Task*> task, TextWithEntities text); void checkLastTask(); void validateState(); void fixAfterErase(); @@ -249,7 +248,6 @@ Tasks::Task::Task( not_null<Ui::VerticalLayout*> container, not_null<Main::Session*> session, int id, - TextWithEntities text, int position, bool locked) : _id(id) @@ -270,11 +268,6 @@ Tasks::Task::Task( , _limit(session->appConfig().todoListItemTextLimit()) { InitField(outer, _field, session); _field->setMaxLength(_limit + kErrorLimit); - _field->setTextWithTags({ - text.text, - TextUtilities::ConvertEntitiesToTextTags(text.entities) - }); - _field->finishAnimating(); _field->show(); if (locked) { _field->setDisabled(true); @@ -487,7 +480,7 @@ Tasks::Tasks( for (const auto &task : existing) { addTask(task.id, task.text, anim::type::instant); } - checkLastTask(); + validateState(); } bool Tasks::full() const { @@ -539,10 +532,13 @@ std::vector<TodoListItem> Tasks::toTodoListItems() const { result.reserve(_list.size()); auto usedId = 0; for (const auto &task : _list) { + if (const auto id = task->id()) { + usedId = id; + } else if (task->isGood()) { + ++usedId; + } if (task->isGood()) { - result.push_back(task->toTodoListItem(++usedId)); - } else if (const auto id = task->id()) { - usedId = std::max(usedId, id); + result.push_back(task->toTodoListItem(usedId)); } } return result; @@ -646,17 +642,28 @@ void Tasks::addTask( _container, &_controller->session(), id, - std::move(text), _position + _list.size() + _destroyed.size(), locked)); + const auto field = _list.back()->field(); if (!locked) { - initTaskField(_list.back().get()); + initTaskField(_list.back().get(), std::move(text)); + } else { + InitMessageFieldHandlers( + _controller, + field, + Window::GifPauseReason::Layer, + [](not_null<DocumentData*>) { return true; }); + field->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); } + field->finishAnimating(); _list.back()->show(animated); fixShadows(); } -void Tasks::initTaskField(not_null<Task*> task) { +void Tasks::initTaskField(not_null<Task*> task, TextWithEntities text) { const auto field = task->field(); if (const auto emojiPanel = _emojiPanel) { const auto emojiToggle = Ui::AddEmojiToggleToField( @@ -686,6 +693,10 @@ void Tasks::initTaskField(not_null<Task*> task) { }, _emojiPanelLifetime); }, emojiToggle->lifetime()); } + field->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); field->submits( ) | rpl::start_with_next([=] { const auto index = findField(field); @@ -789,7 +800,7 @@ void Tasks::checkLastTask() { } // namespace -CreateTodoListBox::CreateTodoListBox( +EditTodoListBox::EditTodoListBox( QWidget*, not_null<Window::SessionController*> controller, rpl::producer<int> starsRequired, @@ -802,25 +813,41 @@ CreateTodoListBox::CreateTodoListBox( , _titleLimit(controller->session().appConfig().todoListTitleLimit()) { } -auto CreateTodoListBox::submitRequests() const -> rpl::producer<Result> { +EditTodoListBox::EditTodoListBox( + QWidget*, + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) +: _controller(controller) +, _sendMenuDetails([] { return SendMenu::Details(); }) +, _editingItem(item) +, _titleLimit(controller->session().appConfig().todoListTitleLimit()) { + _controller->session().changes().messageUpdates( + Data::MessageUpdate::Flag::Destroyed + ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { + if (update.item == item) { + closeBox(); + } + }, lifetime()); +} + +auto EditTodoListBox::submitRequests() const -> rpl::producer<Result> { return _submitRequests.events(); } -void CreateTodoListBox::setInnerFocus() { +void EditTodoListBox::setInnerFocus() { _setInnerFocus(); } -void CreateTodoListBox::submitFailed(const QString &error) { +void EditTodoListBox::submitFailed(const QString &error) { showToast(error); } -not_null<Ui::InputField*> CreateTodoListBox::setupTitle( +not_null<Ui::InputField*> EditTodoListBox::setupTitle( not_null<Ui::VerticalLayout*> container) { using namespace Settings; const auto session = &_controller->session(); const auto isPremium = session->premium(); - const auto title = container->add( object_ptr<Ui::InputField>( container, @@ -860,6 +887,15 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle( }, emojiToggle->lifetime()); } + const auto media = _editingItem ? _editingItem->media() : nullptr; + if (const auto todolist = media ? media->todolist() : nullptr) { + const auto &text = todolist->title; + title->setTextWithTags({ + text.text, + TextUtilities::ConvertEntitiesToTextTags(text.entities) + }); + } + const auto warning = CreateWarningLabel( container, title, @@ -885,7 +921,7 @@ not_null<Ui::InputField*> CreateTodoListBox::setupTitle( return title; } -object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { +object_ptr<Ui::RpWidget> EditTodoListBox::setupContent() { using namespace Settings; const auto id = FullMsgId{ @@ -906,11 +942,14 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { tr::lng_todo_create_list(), st::defaultSubsectionTitle), st::createPollFieldTitlePadding); + const auto media = _editingItem ? _editingItem->media() : nullptr; + const auto todolist = media ? media->todolist() : nullptr; const auto tasks = lifetime().make_state<Tasks>( this, container, _controller, - _emojiPanel ? _emojiPanel.get() : nullptr); + _emojiPanel ? _emojiPanel.get() : nullptr, + todolist ? todolist->items : std::vector<TodoListItem>()); auto limit = tasks->addedCount() | rpl::after_next([=](int count) { setCloseByEscape(!count); setCloseByOutsideClick(!count); @@ -944,14 +983,14 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { object_ptr<Ui::Checkbox>( container, tr::lng_todo_create_allow_add(tr::now), - true, + !todolist || todolist->othersCanAppend(), st::defaultCheckbox), st::createPollCheckboxMargin); const auto allowMark = container->add( object_ptr<Ui::Checkbox>( container, tr::lng_todo_create_allow_mark(tr::now), - true, + !todolist || todolist->othersCanComplete(), st::defaultCheckbox), st::createPollCheckboxMargin); @@ -1019,6 +1058,14 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { showError(tr::lng_todo_choose_tasks); tasks->focusFirst(); } else if (!*error) { + if (_editingItem) { + sendOptions = { + .scheduled = (_editingItem->isScheduled() + ? _editingItem->date() + : TimeId()), + .shortcutId = _editingItem->shortcutId(), + }; + } _submitRequests.fire({ collectResult(), sendOptions }); } }; @@ -1043,9 +1090,13 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { _sendMenuDetails()); }; const auto submit = addButton( - tr::lng_todo_create_button(), + (_editingItem + ? tr::lng_settings_save() + : tr::lng_todo_create_button()), [=] { isNormal ? send({}) : schedule(); }); - submit->setText(PaidSendButtonText(_starsRequired.value(), isNormal + submit->setText(PaidSendButtonText(_starsRequired.value(), _editingItem + ? tr::lng_settings_save() + : isNormal ? tr::lng_todo_create_button() : tr::lng_schedule_button())); const auto sendMenuDetails = [=] { @@ -1062,7 +1113,7 @@ object_ptr<Ui::RpWidget> CreateTodoListBox::setupContent() { return result; } -void CreateTodoListBox::prepare() { +void EditTodoListBox::prepare() { setTitle(tr::lng_todo_create_title()); const auto inner = setInnerWidget(setupContent()); diff --git a/Telegram/SourceFiles/boxes/create_todo_list_box.h b/Telegram/SourceFiles/boxes/edit_todo_list_box.h similarity index 91% rename from Telegram/SourceFiles/boxes/create_todo_list_box.h rename to Telegram/SourceFiles/boxes/edit_todo_list_box.h index 3b9078150e..45ec17f9d8 100644 --- a/Telegram/SourceFiles/boxes/create_todo_list_box.h +++ b/Telegram/SourceFiles/boxes/edit_todo_list_box.h @@ -30,19 +30,23 @@ namespace SendMenu { struct Details; } // namespace SendMenu -class CreateTodoListBox : public Ui::BoxContent { +class EditTodoListBox : public Ui::BoxContent { public: struct Result { TodoListData todolist; Api::SendOptions options; }; - CreateTodoListBox( + EditTodoListBox( QWidget*, not_null<Window::SessionController*> controller, rpl::producer<int> starsRequired, Api::SendType sendType, SendMenu::Details sendMenuDetails); + EditTodoListBox( + QWidget*, + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item); [[nodiscard]] rpl::producer<Result> submitRequests() const; void submitFailed(const QString &error); @@ -68,6 +72,7 @@ private: const not_null<Window::SessionController*> _controller; const Api::SendType _sendType = Api::SendType(); const Fn<SendMenu::Details()> _sendMenuDetails; + HistoryItem *_editingItem = nullptr; rpl::variable<int> _starsRequired; base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel; Fn<void()> _setInnerFocus; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index f54f5e6add..e4fc70bf56 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -2367,6 +2367,10 @@ TextForMimeData MediaTodoList::clipboardText() const { return TextForMimeData::Rich(std::move(result)); } +bool MediaTodoList::allowsEdit() const { + return parent()->out(); +} + bool MediaTodoList::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index fd4cc54d33..7f0e172306 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -625,6 +625,7 @@ public: TextWithEntities notificationText() const override; QString pinnedTextSubstring() const override; TextForMimeData clipboardText() const override; + bool allowsEdit() const override; bool updateInlineResultMedia(const MTPMessageMedia &media) override; bool updateSentMedia(const MTPMessageMedia &media) override; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 2927add840..b4f4588372 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -685,7 +685,8 @@ bool PeerData::canCreatePolls() const { } bool PeerData::canCreateTodoLists() const { - return Data::CanSend(this, ChatRestriction::SendPolls) || isUser(); + return session().premium() + && (Data::CanSend(this, ChatRestriction::SendPolls) || isUser()); } bool PeerData::canCreateTopics() const { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index eb170a4250..0eb51fbb53 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -8559,6 +8559,11 @@ void HistoryWidget::editMessage( } else if (_voiceRecordBar->isActive()) { controller()->showToast(tr::lng_edit_caption_voice(tr::now)); return; + } else if (const auto media = item->media()) { + if (const auto todolist = media->todolist()) { + Window::PeerMenuEditTodoList(controller(), item); + return; + } } else if (_composeSearch) { _composeSearch->hideAnimated(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index e0b3256f4d..52aecc6994 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -84,6 +84,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/spoiler_mess.h" #include "webrtc/webrtc_environment.h" #include "window/window_adaptive.h" +#include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "mainwindow.h" #include "styles/style_chat.h" @@ -2952,6 +2953,12 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) { if (_voiceRecordBar->isActive()) { _show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice())); return; + } else if (const auto media = item->media()) { + if (const auto todolist = media->todolist()) { + Assert(_regularWindow != nullptr); + Window::PeerMenuEditTodoList(_regularWindow, item); + return; + } } if (!isEditingMessage()) { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 7f40aac55f..03f871bbaf 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -352,6 +352,8 @@ ChatWidget::ChatWidget( _composeControls->editMessage( fullId, _inner->getSelectedTextRange(item)); + } else if (media->todolist()) { + Window::PeerMenuEditTodoList(controller, item); } } }, _inner->lifetime()); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 812893be3c..3842e2eb2e 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -231,6 +231,8 @@ ScheduledWidget::ScheduledWidget( _composeControls->editMessage( fullId, _inner->getSelectedTextRange(item)); + } else if (media->todolist()) { + Window::PeerMenuEditTodoList(controller, item); } } }, _inner->lifetime()); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 8963cff5b3..fa1fd40fd0 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" +#include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_chat_helpers.h" @@ -399,6 +400,8 @@ ShortcutMessages::ShortcutMessages( _composeControls->editMessage( fullId, _inner->getSelectedTextRange(item)); + } else if (media->todolist()) { + Window::PeerMenuEditTodoList(_controller, item); } } }, _inner->lifetime()); diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 6457792767..e806bf7c7d 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -29,7 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/moderate_messages_box.h" #include "boxes/choose_filter_box.h" #include "boxes/create_poll_box.h" -#include "boxes/create_todo_list_box.h" +#include "boxes/edit_todo_list_box.h" #include "boxes/pin_messages_box.h" #include "boxes/premium_limits_box.h" #include "boxes/report_messages_box.h" @@ -1244,7 +1244,8 @@ void Filler::addCreateTodoList() { return; } const auto can = _topic - ? Data::CanSend(_topic, ChatRestriction::SendPolls) + ? (_peer->session().premium() + && Data::CanSend(_topic, ChatRestriction::SendPolls)) : _peer->canCreateTodoLists(); if (!can) { return; @@ -1973,19 +1974,19 @@ void PeerMenuCreateTodoList( ) | rpl::map([=] { return peer->starsPerMessageChecked(); }); - auto box = Box<CreateTodoListBox>( + auto box = Box<EditTodoListBox>( controller, std::move(starsRequired), sendType, sendMenuDetails); struct State { - Fn<void(const CreateTodoListBox::Result &)> create; + Fn<void(const EditTodoListBox::Result &)> create; SendPaymentHelper sendPayment; bool lock = false; }; - const auto weak = QPointer<CreateTodoListBox>(box); + const auto weak = QPointer<EditTodoListBox>(box); const auto state = box->lifetime().make_state<State>(); - state->create = [=](const CreateTodoListBox::Result &result) { + state->create = [=](const EditTodoListBox::Result &result) { const auto withPaymentApproved = crl::guard(weak, [=](int stars) { if (const auto onstack = state->create) { auto copy = result; @@ -2028,6 +2029,34 @@ void PeerMenuCreateTodoList( controller->show(std::move(box), Ui::LayerOption::CloseOther); } +void PeerMenuEditTodoList( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto media = item->media(); + const auto todolist = media ? media->todolist() : nullptr; + if (!todolist) { + return; + } else if (!item->history()->session().premium()) { + PeerMenuTodoWantsPremium(TodoWantsPremium::Add); + return; + } + auto box = Box<EditTodoListBox>(controller, item); + const auto weak = QPointer<EditTodoListBox>(box); + box->submitRequests( + ) | rpl::start_with_next([=](const EditTodoListBox::Result &result) { + const auto api = &item->history()->session().api(); + api->todoLists().edit( + item, + result.todolist, + result.options, + crl::guard(weak, [=] { weak->closeBox(); }), + crl::guard(weak, [=](const QString &error) { + weak->submitFailed(error); + })); + }, box->lifetime()); + controller->show(std::move(box), Ui::LayerOption::CloseOther); +} + bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item) { const auto media = item ? item->media() : nullptr; const auto todolist = media ? media->todolist() : nullptr; diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 961ee18e86..1b33297498 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -123,6 +123,9 @@ void PeerMenuCreateTodoList( FullReplyTo replyTo = FullReplyTo(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); +void PeerMenuEditTodoList( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item); [[nodiscard]] bool PeerMenuShowAddTodoListTasks(not_null<HistoryItem*> item); void PeerMenuAddTodoListTasks( not_null<Window::SessionController*> controller, From b965aecc6c5fc36a1306ca153ecbb2d082252626 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 13 Jun 2025 13:40:46 +0400 Subject: [PATCH 181/310] Update API scheme to layer 206. --- Telegram/SourceFiles/api/api_common.cpp | 10 ++++++++++ Telegram/SourceFiles/api/api_common.h | 13 +++++++++++++ Telegram/SourceFiles/api/api_polls.cpp | 6 +++++- Telegram/SourceFiles/api/api_sending.cpp | 18 +++++++++++++++--- Telegram/SourceFiles/api/api_todo_lists.cpp | 6 +++++- Telegram/SourceFiles/api/api_updates.cpp | 6 ++++-- Telegram/SourceFiles/apiwrap.cpp | 18 ++++++++++++++---- .../data/business/data_shortcut_messages.cpp | 5 ++++- .../data/components/scheduled_messages.cpp | 8 ++++++-- Telegram/SourceFiles/data/data_session.cpp | 3 ++- .../export/data/export_data_types.cpp | 8 ++++++++ .../export/data/export_data_types.h | 11 ++++++++++- .../export/output/export_output_html.cpp | 18 ++++++++++++++++++ .../export/output/export_output_json.cpp | 12 ++++++++++++ .../admin_log/history_admin_log_item.cpp | 6 ++++-- Telegram/SourceFiles/history/history_item.cpp | 5 +++++ .../media/stories/media_stories_share.cpp | 6 +++++- Telegram/SourceFiles/mtproto/scheme/api.tl | 12 ++++++++---- .../settings/settings_privacy_controllers.cpp | 3 ++- .../SourceFiles/window/window_peer_menu.cpp | 3 ++- 20 files changed, 152 insertions(+), 25 deletions(-) diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index cfb1e72207..bd0e9302a4 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -14,6 +14,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Api { +MTPSuggestedPost SuggestToMTP(const std::optional<SuggestOptions> &suggest) { + using Flag = MTPDsuggestedPost::Flag; + return suggest + ? MTP_suggestedPost( + MTP_flags(suggest->date ? Flag::f_schedule_date : Flag()), + MTP_long(suggest->stars), + MTP_int(suggest->date)) + : MTPSuggestedPost(); +} + SendAction::SendAction( not_null<Data::Thread*> thread, SendOptions options) diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index c58f525c95..e648102d06 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -19,6 +19,18 @@ namespace Api { inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); +struct SuggestOptions { + int stars = 0; + TimeId date = 0; + + friend inline bool operator==( + const SuggestOptions &, + const SuggestOptions &) = default; +}; + +[[nodiscard]] MTPSuggestedPost SuggestToMTP( + const std::optional<SuggestOptions> &suggest); + struct SendOptions { uint64 price = 0; PeerData *sendAs = nullptr; @@ -31,6 +43,7 @@ struct SendOptions { bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; + std::optional<SuggestOptions> suggest; friend inline bool operator==( const SendOptions &, diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index d6ffaf551d..810d515875 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -75,6 +75,9 @@ void Polls::create( if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (action.options.suggest) { + sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; + } if (starsPaid) { action.options.starsApproved -= starsPaid; sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; @@ -102,7 +105,8 @@ void Polls::create( (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 814b0a9832..4d000104eb 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -109,6 +109,9 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) { if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (action.options.suggest) { + sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; + } if (action.options.invertCaption) { flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; @@ -136,7 +139,8 @@ void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) { (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(session, action.options.shortcutId), MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId); @@ -211,6 +215,9 @@ void SendExistingMedia( if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (action.options.suggest) { + sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; + } if (action.options.invertCaption) { flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; @@ -255,7 +262,8 @@ void SendExistingMedia( (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(session, action.options.shortcutId), MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { if (error.code() == 400 @@ -391,6 +399,9 @@ bool SendDice(MessageToSend &message) { if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (action.options.suggest) { + sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; + } if (action.options.invertCaption) { flags |= MessageFlag::InvertMedia; sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media; @@ -435,7 +446,8 @@ bool SendDice(MessageToSend &message) { (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(session, action.options.shortcutId), MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { }, [=](const MTP::Error &error, const MTP::Response &response) { api->sendMessageFail(error, peer, randomId, newId); diff --git a/Telegram/SourceFiles/api/api_todo_lists.cpp b/Telegram/SourceFiles/api/api_todo_lists.cpp index ee0d64639a..af45543d52 100644 --- a/Telegram/SourceFiles/api/api_todo_lists.cpp +++ b/Telegram/SourceFiles/api/api_todo_lists.cpp @@ -77,6 +77,9 @@ void TodoLists::create( if (action.options.effectId) { sendFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (action.options.suggest) { + sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; + } if (starsPaid) { action.options.starsApproved -= starsPaid; sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars; @@ -104,7 +107,8 @@ void TodoLists::create( (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (clearCloudDraft) { history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index aa3934e4d2..d2a19a7420 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -1228,7 +1228,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPlong(), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong()), // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost()), MessageFlags(), NewMessageType::Unread); } break; @@ -1267,7 +1268,8 @@ void Updates::applyUpdatesNoPtsCheck(const MTPUpdates &updates) { MTPlong(), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong()), // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost()), MessageFlags(), NewMessageType::Unread); } break; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index cbbbf0e65a..0e25f99903 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3963,6 +3963,10 @@ void ApiWrap::sendMessage(MessageToSend &&message) { sendFlags |= MTPmessages_SendMessage::Flag::f_effect; mediaFlags |= MTPmessages_SendMedia::Flag::f_effect; } + if (action.options.suggest) { + sendFlags |= MTPmessages_SendMessage::Flag::f_suggested_post; + mediaFlags |= MTPmessages_SendMedia::Flag::f_suggested_post; + } const auto starsPaid = std::min( peer->starsPerMessageChecked(), action.options.starsApproved); @@ -4030,7 +4034,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { (sendAs ? sendAs->input : MTP_inputPeerEmpty()), mtpShortcut, MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), done, fail); } else { histories.sendPreparedMessage( @@ -4049,7 +4054,8 @@ void ApiWrap::sendMessage(MessageToSend &&message) { (sendAs ? sendAs->input : MTP_inputPeerEmpty()), mtpShortcut, MTP_long(action.options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(action.options.suggest) ), done, fail); } isFirst = false; @@ -4355,6 +4361,7 @@ void ApiWrap::sendMediaWithRandomId( | (options.sendAs ? Flag::f_send_as : Flag(0)) | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) | (options.effectId ? Flag::f_effect : Flag(0)) + | (options.suggest ? Flag::f_suggested_post : Flag(0)) | (options.invertCaption ? Flag::f_invert_media : Flag(0)) | (starsPaid ? Flag::f_allow_paid_stars : Flag(0)); @@ -4383,7 +4390,8 @@ void ApiWrap::sendMediaWithRandomId( (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, options.shortcutId), MTP_long(options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4438,6 +4446,7 @@ void ApiWrap::sendMultiPaidMedia( | (options.sendAs ? Flag::f_send_as : Flag(0)) | (options.shortcutId ? Flag::f_quick_reply_shortcut : Flag(0)) | (options.effectId ? Flag::f_effect : Flag(0)) + | (options.suggest ? Flag::f_suggested_post : Flag(0)) | (options.invertCaption ? Flag::f_invert_media : Flag(0)) | (starsPaid ? Flag::f_allow_paid_stars : Flag(0)); @@ -4465,7 +4474,8 @@ void ApiWrap::sendMultiPaidMedia( (options.sendAs ? options.sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, options.shortcutId), MTP_long(options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (const auto album = _sendingAlbums.take(groupId)) { const auto copy = (*album)->items; diff --git a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp index a195921a4c..e3701b18b9 100644 --- a/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp +++ b/Telegram/SourceFiles/data/business/data_shortcut_messages.cpp @@ -93,7 +93,10 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_long(data.veffect().value_or_empty()), (data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()), MTP_int(data.vreport_delivery_until_date().value_or_empty()), - MTP_long(data.vpaid_message_stars().value_or_empty())); + MTP_long(data.vpaid_message_stars().value_or_empty()), + (data.vsuggested_post() + ? *data.vsuggested_post() + : MTPSuggestedPost())); }); } diff --git a/Telegram/SourceFiles/data/components/scheduled_messages.cpp b/Telegram/SourceFiles/data/components/scheduled_messages.cpp index 9e88a17f78..493a3cc36f 100644 --- a/Telegram/SourceFiles/data/components/scheduled_messages.cpp +++ b/Telegram/SourceFiles/data/components/scheduled_messages.cpp @@ -97,7 +97,10 @@ constexpr auto kRequestTimeLimit = 60 * crl::time(1000); MTP_long(data.veffect().value_or_empty()), // effect data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck(), MTP_int(data.vreport_delivery_until_date().value_or_empty()), - MTP_long(data.vpaid_message_stars().value_or_empty())); + MTP_long(data.vpaid_message_stars().value_or_empty()), + (data.vsuggested_post() + ? *data.vsuggested_post() + : MTPSuggestedPost())); }); } @@ -272,7 +275,8 @@ void ScheduledMessages::sendNowSimpleMessage( MTP_long(local->effectId()), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong()), // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost()), localFlags, NewMessageType::Unread); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 490db7d8df..66ab04bab2 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -4950,7 +4950,8 @@ void Session::insertCheckedServiceNotification( MTPlong(), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong()), // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost()), localFlags, NewMessageType::Unread); } diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 8d13561903..52d79e4079 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1757,6 +1757,14 @@ ServiceAction ParseServiceAction( | ranges::views::transform(ParseTodoListItem) | ranges::to_vector, }; + }, [&](const MTPDmessageActionSuggestedPostApproval &data) { + result.content = ActionSuggestedPostApproval{ + .rejectComment = data.vreject_comment().value_or_empty(), + .scheduleDate = data.vschedule_date().value_or_empty(), + .stars = int(data.vstars_amount().value_or_empty()), + .rejected = data.is_rejected(), + .balanceTooLow = data.is_balance_too_low(), + }; }, [&](const MTPDmessageActionConferenceCall &data) { auto content = ActionPhoneCall(); using State = ActionPhoneCall::State; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 6e7c873b28..38ba0d0cfe 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -698,6 +698,14 @@ struct ActionTodoAppendTasks { std::vector<TodoListItem> items; }; +struct ActionSuggestedPostApproval { + Utf8String rejectComment; + TimeId scheduleDate = 0; + int stars = 0; + bool rejected = false; + bool balanceTooLow = false; +}; + struct ServiceAction { std::variant< v::null_t, @@ -747,7 +755,8 @@ struct ServiceAction { ActionPaidMessagesRefunded, ActionPaidMessagesPrice, ActionTodoCompletions, - ActionTodoAppendTasks> content; + ActionTodoAppendTasks, + ActionSuggestedPostApproval> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index edb7d95adb..df2278f53c 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1446,6 +1446,24 @@ auto HtmlWriter::Wrap::pushMessage( + """); } return serviceFrom + " added tasks: " + tasks.join(", "); + }, [&](const ActionSuggestedPostApproval &data) { + return serviceFrom + + (data.rejected ? " rejected " : " approved ") + + "your suggested post" + + (data.stars + ? ", for " + QString::number(data.stars).toUtf8() + " stars" + : "") + + (data.scheduleDate + ? (", " + + FormatDateText(data.scheduleDate) + + " at " + + FormatTimeText(data.scheduleDate)) + : "") + + (data.rejectComment.isEmpty() + ? "." + : (", with comment: "" + + SerializeString(data.rejectComment) + + """)); }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index ec57f7f83a..7eb927f866 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -708,6 +708,18 @@ QByteArray SerializeMessage( return result; }) | ranges::to_vector; pushBare("items", SerializeArray(context, items)); + }, [&](const ActionSuggestedPostApproval &data) { + pushActor(); + pushAction("process_suggested_post"); + if (data.rejected) { + pushBare("rejected", "true"); + if (!data.rejectComment.isEmpty()) { + push("comment", data.rejectComment); + } + } else { + push("stars_amount", NumberToString(data.stars)); + push("scheduled_date", data.scheduleDate); + } }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index abe3d949c5..a93d1d1925 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -163,7 +163,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { | Flag::f_restriction_reason | Flag::f_ttl_period | Flag::f_factcheck - | Flag::f_report_delivery_until_date; + | Flag::f_report_delivery_until_date + | Flag::f_suggested_post; return MTP_message( MTP_flags(data.vflags().v & ~removeFlags), data.vid(), @@ -195,7 +196,8 @@ MTPMessage PrepareLogMessage(const MTPMessage &message, TimeId newDate) { MTP_long(data.veffect().value_or_empty()), MTPFactCheck(), MTPint(), // report_delivery_until_date - MTP_long(data.vpaid_message_stars().value_or_empty())); + MTP_long(data.vpaid_message_stars().value_or_empty()), + MTPSuggestedPost()); }); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 18dfcba072..2df23d25f6 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -5913,6 +5913,10 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return prepareTodoAppendTasksText(); }; + auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &) { + return PreparedServiceText{ { "process_suggested" } }; AssertIsDebug(); + }; + auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { Unexpected("PhoneCall type in setServiceMessageFromMtp."); }; @@ -5969,6 +5973,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareConferenceCall, prepareTodoCompletions, prepareTodoAppendTasks, + prepareSuggestedPostApproval, PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>, PrepareErrorText<MTPDmessageActionEmpty>)); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 80ec8e75ba..4773e7e068 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -135,6 +135,9 @@ namespace Media::Stories { if (options.effectId) { sendFlags |= SendFlag::f_effect; } + if (options.suggest) { + sendFlags |= SendFlag::f_suggested_post; + } if (options.invertCaption) { sendFlags |= SendFlag::f_invert_media; } @@ -170,7 +173,8 @@ namespace Media::Stories { MTP_inputPeerEmpty(), Data::ShortcutIdToMTP(session, options.shortcutId), MTP_long(options.effectId), - MTP_long(starsPaid) + MTP_long(starsPaid), + SuggestToMTP(options.suggest) ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index eacd073501..55fcec1aa4 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -117,7 +117,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message; +message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message; messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -192,6 +192,7 @@ messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction; messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction; messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction; +messageActionSuggestedPostApproval#af42ae29 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int stars_amount:flags.4?long = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?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; @@ -1993,6 +1994,8 @@ todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:fla todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; +suggestedPost#95ee6a6d flags:# accepted:flags.1?true rejected:flags.2?true stars_amount:long schedule_date:flags.0?int = SuggestedPost; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -2189,8 +2192,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#fbf2340a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; -messages.sendMedia#a550cd78 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; +messages.sendMessage#fe05dc9a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; +messages.sendMedia#ac55d9c1 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; messages.forwardMessages#38f0188c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; @@ -2409,6 +2412,7 @@ messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids: messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates; messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates; +messages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2726,4 +2730,4 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 205 +// LAYER 206 diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index c64c2c5ccc..5ce93e02bb 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -204,7 +204,8 @@ AdminLog::OwnedItem GenerateForwardedItem( MTPlong(), // effect MTPFactCheck(), MTPint(), // report_delivery_until_date - MTPlong() // paid_message_stars + MTPlong(), // paid_message_stars + MTPSuggestedPost() ).match([&](const MTPDmessage &data) { return history->makeMessage( history->nextNonHistoryEntryId(), diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index e806bf7c7d..3913b1b3c9 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -150,7 +150,8 @@ void ShareBotGame( MTPInputPeer(), // send_as MTPInputQuickReplyShortcut(), MTPlong(), - MTPlong() + MTPlong(), + MTPSuggestedPost() ), [=](const MTPUpdates &, const MTP::Response &) { }, [=](const MTP::Error &error, const MTP::Response &) { history->session().api().sendMessageFail(error, history->peer); From b2d7342b9e5652e8fddb15d91ed89db1cad04f6e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 16 Jun 2025 13:33:46 +0400 Subject: [PATCH 182/310] Attempt to fix monoforum muted setting. --- Telegram/SourceFiles/data/data_saved_sublist.cpp | 6 ++++++ Telegram/SourceFiles/history/history.cpp | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/data_saved_sublist.cpp b/Telegram/SourceFiles/data/data_saved_sublist.cpp index c8271b6824..488eab3a1a 100644 --- a/Telegram/SourceFiles/data/data_saved_sublist.cpp +++ b/Telegram/SourceFiles/data/data_saved_sublist.cpp @@ -878,6 +878,12 @@ Dialogs::BadgesState SavedSublist::chatListBadgesState() const { > _parent->owningHistory()->inboxReadTillId()); result.unreadMuted = muted(); } + if (_parent->owningHistory()->muted()) { + result.unreadMuted + = result.mentionMuted + = result.reactionMuted + = true; + } return result; } diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 367cde6421..5227ae17a7 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -2358,8 +2358,14 @@ Dialogs::UnreadState History::chatListUnreadState() const { if (const auto forum = peer->forum()) { return AdjustedForumUnreadState(forum->topicsList()->unreadState()); } else if (const auto monoforum = peer->monoforum()) { - return AdjustedForumUnreadState( - monoforum->chatsList()->unreadState()); + auto state = monoforum->chatsList()->unreadState(); + if (muted()) { + state.chatsMuted = state.chats; + state.marksMuted = state.marks; + state.messagesMuted = state.messages; + state.reactionsMuted = state.reactions; + } + return AdjustedForumUnreadState(state); } return computeUnreadState(); } From 0473374d51da0191c053cd1b30c8fae320ea4cd9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 16 Jun 2025 15:02:17 +0400 Subject: [PATCH 183/310] Allow admin sending to monoforum for free. --- Telegram/SourceFiles/data/data_peer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index b4f4588372..bfe6db59ea 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1678,7 +1678,9 @@ int PeerData::starsPerMessage() const { int PeerData::starsPerMessageChecked() const { if (const auto channel = asChannel()) { - if (channel->adminRights() || channel->amCreator()) { + if (channel->adminRights() + || channel->amCreator() + || amMonoforumAdmin()) { return 0; } } From 8dbc175c026d3883c00d466a4b5d3784d4e2afa5 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 16 Jun 2025 15:02:30 +0400 Subject: [PATCH 184/310] Update API scheme on layer 206. --- Telegram/Resources/langs/lang.strings | 3 + .../SourceFiles/api/api_chat_participants.cpp | 10 +-- Telegram/SourceFiles/apiwrap.cpp | 3 +- .../boxes/peers/choose_peer_box.cpp | 29 ++---- .../boxes/peers/edit_participant_box.cpp | 3 +- .../boxes/peers/edit_participants_box.cpp | 3 +- .../boxes/peers/edit_peer_info_box.cpp | 5 +- .../boxes/peers/edit_peer_permissions_box.cpp | 6 +- .../SourceFiles/core/local_url_handlers.cpp | 2 + Telegram/SourceFiles/data/data_channel.cpp | 26 ++---- .../data/data_chat_participant_status.cpp | 88 ++++++++++++++++++- .../data/data_chat_participant_status.h | 6 ++ .../admin_log/history_admin_log_item.cpp | 1 + Telegram/SourceFiles/mtproto/scheme/api.tl | 8 +- 14 files changed, 132 insertions(+), 61 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c1b0d964ef..d72646e86b 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -5207,6 +5207,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_rights_channel_edit_stories" = "Edit stories of others"; "lng_rights_channel_delete_stories" = "Delete stories of others"; "lng_rights_channel_manage_calls" = "Manage live streams"; +"lng_rights_channel_manage_direct" = "Manage direct messages"; "lng_rights_group_info" = "Change group info"; "lng_rights_group_ban" = "Ban users"; "lng_rights_group_invite_link" = "Invite users via link"; @@ -5530,6 +5531,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_admin_log_admin_create_topics" = "Create topics"; "lng_admin_log_admin_manage_calls" = "Manage video chats"; "lng_admin_log_admin_manage_calls_channel" = "Manage live streams"; +"lng_admin_log_admin_manage_direct" = "Manage direct messages"; "lng_admin_log_admin_add_admins" = "Add new admins"; "lng_admin_log_subscription_extend" = "{name} renewed subscription until {date}"; @@ -6197,6 +6199,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_request_channel_delete_messages" = "delete messages"; "lng_request_channel_add_subscribers" = "add subscribers"; "lng_request_channel_manage_livestreams" = "manage live streams"; +"lng_request_channel_manage_direct" = "manage direct messages"; "lng_request_channel_add_admins" = "add new admins"; "lng_request_channel_create" = "Create a New Channel for This"; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index af60bdfe3d..8093168d3c 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -655,10 +655,7 @@ void ChatParticipants::Restrict( channel->session().api().request(MTPchannels_EditBanned( channel->inputChannel, participant->input, - MTP_chatBannedRights( - MTP_flags(MTPDchatBannedRights::Flags::from_raw( - uint32(newRights.flags))), - MTP_int(newRights.until)) + RestrictionsToMTP(newRights) )).done([=](const MTPUpdates &result) { channel->session().api().applyUpdates(result); channel->applyEditBanned(participant, oldRights, newRights); @@ -763,10 +760,7 @@ void ChatParticipants::kick( const auto requestId = _api.request(MTPchannels_EditBanned( channel->inputChannel, participant->input, - MTP_chatBannedRights( - MTP_flags( - MTPDchatBannedRights::Flags::from_raw(uint32(rights.flags))), - MTP_int(rights.until)) + RestrictionsToMTP(rights) )).done([=](const MTPUpdates &result) { channel->session().api().applyUpdates(result); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 0e25f99903..004ae8c3b6 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2169,7 +2169,8 @@ void ApiWrap::saveDraftsToCloud() { Data::WebPageForMTP( cloudDraft->webpage, textWithTags.text.isEmpty()), - MTP_long(0) // effect + MTP_long(0), // effect + MTPSuggestedPost() // )).done([=](const MTPBool &result, const MTP::Response &response) { const auto requestId = response.requestId; history->finishSavingCloudDraft( diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index bc336675f8..7257a3c3f8 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -75,16 +75,12 @@ using RightsMap = std::vector<std::pair<ChatAdminRight, tr::phrase<>>>; using Flag = ChatAdminRight; return { { Flag::ChangeInfo, tr::lng_request_group_change_info }, - { - Flag::DeleteMessages, - tr::lng_request_group_delete_messages }, + { Flag::DeleteMessages, tr::lng_request_group_delete_messages }, { Flag::BanUsers, tr::lng_request_group_ban_users }, { Flag::InviteByLinkOrAdd, tr::lng_request_group_invite }, { Flag::PinMessages, tr::lng_request_group_pin_messages }, { Flag::ManageTopics, tr::lng_request_group_manage_topics }, - { - Flag::ManageCall, - tr::lng_request_group_manage_video_chats }, + { Flag::ManageCall, tr::lng_request_group_manage_video_chats }, { Flag::Anonymous, tr::lng_request_group_anonymous }, { Flag::AddAdmins, tr::lng_request_group_add_admins }, }; @@ -94,21 +90,12 @@ using RightsMap = std::vector<std::pair<ChatAdminRight, tr::phrase<>>>; using Flag = ChatAdminRight; return { { Flag::ChangeInfo, tr::lng_request_channel_change_info }, - { - Flag::PostMessages, - tr::lng_request_channel_post_messages }, - { - Flag::EditMessages, - tr::lng_request_channel_edit_messages }, - { - Flag::DeleteMessages, - tr::lng_request_channel_delete_messages }, - { - Flag::InviteByLinkOrAdd, - tr::lng_request_channel_add_subscribers }, - { - Flag::ManageCall, - tr::lng_request_channel_manage_livestreams }, + { Flag::PostMessages, tr::lng_request_channel_post_messages }, + { Flag::EditMessages, tr::lng_request_channel_edit_messages }, + { Flag::DeleteMessages, tr::lng_request_channel_delete_messages }, + { Flag::InviteByLinkOrAdd, tr::lng_request_channel_add_subscribers }, + { Flag::ManageCall, tr::lng_request_channel_manage_livestreams }, + { Flag::ManageDirect, tr::lng_request_channel_manage_direct }, { Flag::AddAdmins, tr::lng_request_channel_add_admins }, }; } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index e4166a82c0..50c3254a37 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -245,7 +245,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::EditStories | Flag::DeleteStories | Flag::InviteByLinkOrAdd - | Flag::ManageCall) }; + | Flag::ManageCall + | Flag::ManageDirect) }; } void EditAdminBox::prepare() { diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index ea60f38333..03fbc87c68 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -152,8 +152,7 @@ void SaveChannelAdmin( channel->session().api().request(MTPchannels_EditAdmin( channel->inputChannel, user->inputUser, - MTP_chatAdminRights(MTP_flags( - MTPDchatAdminRights::Flags::from_raw(uint32(newRights.flags)))), + AdminRightsToMTP(newRights), MTP_string(rank) )).done([=](const MTPUpdates &result) { channel->session().api().applyUpdates(result); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 19611e16e7..2cae756a63 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -183,10 +183,7 @@ void SaveDefaultRestrictions( const auto requestId = api->request( MTPmessages_EditChatDefaultBannedRights( peer->input, - MTP_chatBannedRights( - MTP_flags( - MTPDchatBannedRights::Flags::from_raw(uint32(rights))), - MTP_int(0))) + RestrictionsToMTP({ rights, 0 })) ).done([=](const MTPUpdates &result) { api->clearModifyRequest(key); api->applyUpdates(result); diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index d171501b95..0b762a6cf5 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -164,11 +164,15 @@ constexpr auto kDefaultChargeStars = 10; auto stories = std::vector<AdminRightLabel>{ { Flag::PostStories, tr::lng_rights_channel_post_stories(tr::now) }, { Flag::EditStories, tr::lng_rights_channel_edit_stories(tr::now) }, - { Flag::DeleteStories, tr::lng_rights_channel_delete_stories(tr::now) }, + { + Flag::DeleteStories, + tr::lng_rights_channel_delete_stories(tr::now), + }, }; auto second = std::vector<AdminRightLabel>{ { Flag::InviteByLinkOrAdd, tr::lng_rights_group_invite(tr::now) }, { Flag::ManageCall, tr::lng_rights_channel_manage_calls(tr::now) }, + { Flag::ManageDirect, tr::lng_rights_channel_manage_direct(tr::now) }, { Flag::AddAdmins, tr::lng_rights_add_admins(tr::now) }, }; return { diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 64e33b14ca..90ee230586 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -520,6 +520,8 @@ bool ShowWallPaper( result |= ChatAdminRight::AddAdmins; } else if (element == u"manage_video_chats"_q) { result |= ChatAdminRight::ManageCall; + } else if (element == u"manage_direct_messages"_q) { + result |= ChatAdminRight::ManageDirect; } else if (element == u"anonymous"_q) { result |= ChatAdminRight::Anonymous; } else if (element == u"manage_chat"_q) { diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 02a94552ec..2195f75654 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -642,45 +642,38 @@ void ChannelData::setAvailableMinId(MsgId availableMinId) { } bool ChannelData::canBanMembers() const { - return amCreator() - || (adminRights() & AdminRight::BanUsers); + return amCreator() || (adminRights() & AdminRight::BanUsers); } bool ChannelData::canPostMessages() const { - return amCreator() - || (adminRights() & AdminRight::PostMessages); + return amCreator() || (adminRights() & AdminRight::PostMessages); } bool ChannelData::canEditMessages() const { - return amCreator() - || (adminRights() & AdminRight::EditMessages); + return amCreator() || (adminRights() & AdminRight::EditMessages); } bool ChannelData::canDeleteMessages() const { - return amCreator() - || (adminRights() & AdminRight::DeleteMessages); + return amCreator() || (adminRights() & AdminRight::DeleteMessages); } bool ChannelData::canPostStories() const { - return amCreator() - || (adminRights() & AdminRight::PostStories); + return amCreator() || (adminRights() & AdminRight::PostStories); } bool ChannelData::canEditStories() const { if (isMonoforum()) { return false; } - return amCreator() - || (adminRights() & AdminRight::EditStories); + return amCreator() || (adminRights() & AdminRight::EditStories); } bool ChannelData::canDeleteStories() const { - return amCreator() - || (adminRights() & AdminRight::DeleteStories); + return amCreator() || (adminRights() & AdminRight::DeleteStories); } bool ChannelData::canAccessMonoforum() const { - return canPostMessages(); + return amCreator() || (adminRights() & AdminRight::ManageDirect); } bool ChannelData::canPostPaidMedia() const { @@ -704,8 +697,7 @@ bool ChannelData::canAddMembers() const { } bool ChannelData::canAddAdmins() const { - return amCreator() - || (adminRights() & AdminRight::AddAdmins); + return amCreator() || (adminRights() & AdminRight::AddAdmins); } bool ChannelData::allowsForwarding() const { diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index de38444757..f2ec641e04 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -30,14 +30,48 @@ namespace { [[nodiscard]] ChatAdminRights ChatAdminRightsFlags( const MTPChatAdminRights &rights) { return rights.match([](const MTPDchatAdminRights &data) { - return ChatAdminRights::from_raw(int32(data.vflags().v)); + using Flag = ChatAdminRight; + return (data.is_change_info() ? Flag::ChangeInfo : Flag()) + | (data.is_post_messages() ? Flag::PostMessages : Flag()) + | (data.is_edit_messages() ? Flag::EditMessages : Flag()) + | (data.is_delete_messages() ? Flag::DeleteMessages : Flag()) + | (data.is_ban_users() ? Flag::BanUsers : Flag()) + | (data.is_invite_users() ? Flag::InviteByLinkOrAdd : Flag()) + | (data.is_pin_messages() ? Flag::PinMessages : Flag()) + | (data.is_add_admins() ? Flag::AddAdmins : Flag()) + | (data.is_anonymous() ? Flag::Anonymous : Flag()) + | (data.is_manage_call() ? Flag::ManageCall : Flag()) + | (data.is_other() ? Flag::Other : Flag()) + | (data.is_manage_topics() ? Flag::ManageTopics : Flag()) + | (data.is_post_stories() ? Flag::PostStories : Flag()) + | (data.is_edit_stories() ? Flag::EditStories : Flag()) + | (data.is_delete_stories() ? Flag::DeleteStories : Flag()) + | (data.is_manage_direct() ? Flag::ManageDirect : Flag()); }); } [[nodiscard]] ChatRestrictions ChatBannedRightsFlags( const MTPChatBannedRights &rights) { return rights.match([](const MTPDchatBannedRights &data) { - return ChatRestrictions::from_raw(int32(data.vflags().v)); + using Flag = ChatRestriction; + return (data.is_view_messages() ? Flag::ViewMessages : Flag()) + | (data.is_send_stickers() ? Flag::SendStickers : Flag()) + | (data.is_send_gifs() ? Flag::SendGifs : Flag()) + | (data.is_send_games() ? Flag::SendGames : Flag()) + | (data.is_send_inline() ? Flag::SendInline : Flag()) + | (data.is_send_polls() ? Flag::SendPolls : Flag()) + | (data.is_send_photos() ? Flag::SendPhotos : Flag()) + | (data.is_send_videos() ? Flag::SendVideos : Flag()) + | (data.is_send_roundvideos() ? Flag::SendVideoMessages : Flag()) + | (data.is_send_audios() ? Flag::SendMusic : Flag()) + | (data.is_send_voices() ? Flag::SendVoiceMessages : Flag()) + | (data.is_send_docs() ? Flag::SendFiles : Flag()) + | (data.is_send_messages() ? Flag::SendOther : Flag()) + | (data.is_embed_links() ? Flag::EmbedLinks : Flag()) + | (data.is_change_info() ? Flag::ChangeInfo : Flag()) + | (data.is_invite_users() ? Flag::AddParticipants : Flag()) + | (data.is_pin_messages() ? Flag::PinMessages : Flag()) + | (data.is_manage_topics() ? Flag::CreateTopics : Flag()); }); } @@ -54,11 +88,61 @@ ChatAdminRightsInfo::ChatAdminRightsInfo(const MTPChatAdminRights &rights) : flags(ChatAdminRightsFlags(rights)) { } +MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info) { + using Flag = MTPDchatAdminRights::Flag; + using R = ChatAdminRight; + const auto flags = info.flags; + return MTP_chatAdminRights(MTP_flags(Flag() + | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) + | ((flags & R::PostMessages) ? Flag::f_post_messages : Flag()) + | ((flags & R::EditMessages) ? Flag::f_edit_messages : Flag()) + | ((flags & R::DeleteMessages) ? Flag::f_delete_messages : Flag()) + | ((flags & R::BanUsers) ? Flag::f_ban_users : Flag()) + | ((flags & R::InviteByLinkOrAdd) ? Flag::f_invite_users : Flag()) + | ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag()) + | ((flags & R::AddAdmins) ? Flag::f_add_admins : Flag()) + | ((flags & R::Anonymous) ? Flag::f_anonymous : Flag()) + | ((flags & R::ManageCall) ? Flag::f_manage_call : Flag()) + | ((flags & R::Other) ? Flag::f_other : Flag()) + | ((flags & R::ManageTopics) ? Flag::f_manage_topics : Flag()) + | ((flags & R::PostStories) ? Flag::f_post_stories : Flag()) + | ((flags & R::EditStories) ? Flag::f_edit_stories : Flag()) + | ((flags & R::DeleteStories) ? Flag::f_delete_stories : Flag()) + | ((flags & R::ManageDirect) ? Flag::f_manage_direct : Flag()))); +} + ChatRestrictionsInfo::ChatRestrictionsInfo(const MTPChatBannedRights &rights) : flags(ChatBannedRightsFlags(rights)) , until(ChatBannedRightsUntilDate(rights)) { } +MTPChatBannedRights RestrictionsToMTP(ChatRestrictionsInfo info) { + using Flag = MTPDchatBannedRights::Flag; + using R = ChatRestriction; + const auto flags = info.flags; + return MTP_chatBannedRights( + MTP_flags(Flag() + | ((flags & R::ViewMessages) ? Flag::f_view_messages : Flag()) + | ((flags & R::SendStickers) ? Flag::f_send_stickers : Flag()) + | ((flags & R::SendGifs) ? Flag::f_send_gifs : Flag()) + | ((flags & R::SendGames) ? Flag::f_send_games : Flag()) + | ((flags & R::SendInline) ? Flag::f_send_inline : Flag()) + | ((flags & R::SendPolls) ? Flag::f_send_polls : Flag()) + | ((flags & R::SendPhotos) ? Flag::f_send_photos : Flag()) + | ((flags & R::SendVideos) ? Flag::f_send_videos : Flag()) + | ((flags & R::SendVideoMessages) ? Flag::f_send_roundvideos : Flag()) + | ((flags & R::SendMusic) ? Flag::f_send_audios : Flag()) + | ((flags & R::SendVoiceMessages) ? Flag::f_send_voices : Flag()) + | ((flags & R::SendFiles) ? Flag::f_send_docs : Flag()) + | ((flags & R::SendOther) ? Flag::f_send_messages : Flag()) + | ((flags & R::EmbedLinks) ? Flag::f_embed_links : Flag()) + | ((flags & R::ChangeInfo) ? Flag::f_change_info : Flag()) + | ((flags & R::AddParticipants) ? Flag::f_invite_users : Flag()) + | ((flags & R::PinMessages) ? Flag::f_pin_messages : Flag()) + | ((flags & R::CreateTopics) ? Flag::f_manage_topics : Flag())), + MTP_int(info.until)); +} + namespace Data { std::vector<ChatRestrictions> ListOfRestrictions( diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.h b/Telegram/SourceFiles/data/data_chat_participant_status.h index 17a6ddfe7d..073096b09b 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.h +++ b/Telegram/SourceFiles/data/data_chat_participant_status.h @@ -36,6 +36,7 @@ enum class ChatAdminRight { PostStories = (1 << 14), EditStories = (1 << 15), DeleteStories = (1 << 16), + ManageDirect = (1 << 17), }; inline constexpr bool is_flag_type(ChatAdminRight) { return true; } using ChatAdminRights = base::flags<ChatAdminRight>; @@ -75,6 +76,8 @@ struct ChatAdminRightsInfo { ChatAdminRights flags; }; +[[nodiscard]] MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info); + struct ChatRestrictionsInfo { ChatRestrictionsInfo() = default; ChatRestrictionsInfo(ChatRestrictions flags, TimeId until) @@ -87,6 +90,9 @@ struct ChatRestrictionsInfo { TimeId until = 0; }; +[[nodiscard]] MTPChatBannedRights RestrictionsToMTP( + ChatRestrictionsInfo info); + namespace Data { class Thread; diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index a93d1d1925..0f656950b4 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -287,6 +287,7 @@ TextWithEntities GenerateAdminChangeText( { Flag::ManageTopics, tr::lng_admin_log_admin_manage_topics }, { Flag::PinMessages, tr::lng_admin_log_admin_pin_messages }, { Flag::ManageCall, tr::lng_admin_log_admin_manage_calls }, + { Flag::ManageDirect, tr::lng_admin_log_admin_manage_direct }, { Flag::AddAdmins, tr::lng_admin_log_admin_add_admins }, { Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous }, }; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 55fcec1aa4..1d89f321b4 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -117,7 +117,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message; +message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post:flags2.8?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message; messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -827,7 +827,7 @@ contacts.topPeers#70b772a8 categories:Vector<TopPeerCategoryPeers> chats:Vector< contacts.topPeersDisabled#b52c939d = contacts.TopPeers; draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; -draftMessage#2d65321f flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long = DraftMessage; +draftMessage#96eaa5eb flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers; @@ -1207,7 +1207,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 manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?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 other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct:flags.17?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 manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; @@ -2244,7 +2244,7 @@ messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_me messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs; -messages.saveDraft#d372c5ce flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long = Bool; +messages.saveDraft#54ae308e flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost = Bool; messages.getAllDrafts#6a3f8d65 = Updates; messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool; From 7e5a29a5ccc2d183f6ea2776960054da04b7135e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 13 Jun 2025 14:39:30 +0400 Subject: [PATCH 185/310] PoC suggesting posts to channels. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 13 + Telegram/SourceFiles/api/api_bot.cpp | 2 + Telegram/SourceFiles/api/api_common.cpp | 10 +- Telegram/SourceFiles/api/api_common.h | 14 +- Telegram/SourceFiles/api/api_sending.cpp | 3 + Telegram/SourceFiles/apiwrap.cpp | 14 +- .../chat_helpers/chat_helpers.style | 24 ++ Telegram/SourceFiles/data/data_drafts.cpp | 27 +- Telegram/SourceFiles/data/data_drafts.h | 4 + Telegram/SourceFiles/data/data_msg_id.h | 17 ++ Telegram/SourceFiles/data/data_peer.cpp | 3 + Telegram/SourceFiles/dialogs/dialogs_key.h | 1 + Telegram/SourceFiles/history/history.cpp | 15 ++ Telegram/SourceFiles/history/history.h | 1 + Telegram/SourceFiles/history/history_item.cpp | 15 ++ Telegram/SourceFiles/history/history_item.h | 1 + .../history/history_item_components.h | 8 + .../history/history_item_reply_markup.cpp | 30 ++- .../history/history_item_reply_markup.h | 17 ++ .../SourceFiles/history/history_widget.cpp | 152 +++++++++-- Telegram/SourceFiles/history/history_widget.h | 8 + .../history_view_compose_controls.cpp | 13 +- .../controls/history_view_draft_options.cpp | 1 + .../controls/history_view_forward_panel.cpp | 1 + .../view/history_view_chat_section.cpp | 1 + .../history/view/history_view_message.cpp | 15 ++ .../view/history_view_suggest_options.cpp | 237 ++++++++++++++++++ .../view/history_view_suggest_options.h | 53 ++++ .../inline_bots/bot_attach_web_view.cpp | 10 +- Telegram/SourceFiles/main/main_app_config.cpp | 4 + Telegram/SourceFiles/main/main_app_config.h | 2 + Telegram/SourceFiles/mainwidget.cpp | 1 + .../media/stories/media_stories_share.cpp | 2 +- .../business/settings_shortcut_messages.cpp | 1 + .../SourceFiles/storage/storage_account.cpp | 31 ++- .../SourceFiles/storage/storage_account.h | 1 + .../SourceFiles/support/support_helper.cpp | 1 + .../window/notifications_manager.cpp | 1 + .../SourceFiles/window/window_peer_menu.cpp | 8 + .../SourceFiles/window/window_peer_menu.h | 2 + .../window/window_session_controller.cpp | 4 + 42 files changed, 712 insertions(+), 58 deletions(-) create mode 100644 Telegram/SourceFiles/history/view/history_view_suggest_options.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_suggest_options.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index b2f6cd3587..cc060d2a60 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -898,6 +898,8 @@ PRIVATE history/view/history_view_sticker_toast.h history/view/history_view_subsection_tabs.cpp history/view/history_view_subsection_tabs.h + history/view/history_view_suggest_options.cpp + history/view/history_view_suggest_options.h history/view/history_view_text_helper.cpp history/view/history_view_text_helper.h history/view/history_view_transcribe_button.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d72646e86b..207aecbdae 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4415,6 +4415,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_preview_reply_to" = "Reply to {name}"; "lng_preview_reply_to_quote" = "Reply to quote from {name}"; +"lng_suggest_bar_title" = "Suggest a Post Below"; +"lng_suggest_bar_text" = "Click to offer a price for publishing."; +"lng_suggest_bar_priced" = "{amount} for publishing anytime."; +"lng_suggest_bar_priced_dated" = "{amount} {date}"; +"lng_suggest_bar_dated" = "Publish on {date}"; +"lng_suggest_options_title" = "Suggest a Message"; +"lng_suggest_options_price" = "Enter Price in Stars"; +"lng_suggest_options_price_about" = "Choose how many Stars you want to offer {channel} to publish this message."; +"lng_suggest_options_date" = "Time"; +"lng_suggest_options_date_any" = "Anytime"; +"lng_suggest_options_date_about" = "Select the date and time you want the message to be published."; +"lng_suggest_options_offer" = "Offer {amount}"; + "lng_reply_in_another_title" = "Reply in..."; "lng_reply_in_another_chat" = "Reply in Another Chat"; "lng_reply_in_author" = "Message author"; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 5342cbd2e0..87111914ef 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -399,10 +399,12 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { } } const auto replyTo = FullReplyTo(); + const auto suggest = SuggestPostOptions(); Window::PeerMenuCreatePoll( controller, item->history()->peer, replyTo, + suggest, chosen, disabled); } break; diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index bd0e9302a4..29617a3f16 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -14,13 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Api { -MTPSuggestedPost SuggestToMTP(const std::optional<SuggestOptions> &suggest) { +MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) { using Flag = MTPDsuggestedPost::Flag; - return suggest + return suggest.exists ? MTP_suggestedPost( - MTP_flags(suggest->date ? Flag::f_schedule_date : Flag()), - MTP_long(suggest->stars), - MTP_int(suggest->date)) + MTP_flags(suggest.date ? Flag::f_schedule_date : Flag()), + MTP_long(suggest.stars), + MTP_int(suggest.date)) : MTPSuggestedPost(); } diff --git a/Telegram/SourceFiles/api/api_common.h b/Telegram/SourceFiles/api/api_common.h index e648102d06..bbbff2b0cc 100644 --- a/Telegram/SourceFiles/api/api_common.h +++ b/Telegram/SourceFiles/api/api_common.h @@ -19,17 +19,7 @@ namespace Api { inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE); -struct SuggestOptions { - int stars = 0; - TimeId date = 0; - - friend inline bool operator==( - const SuggestOptions &, - const SuggestOptions &) = default; -}; - -[[nodiscard]] MTPSuggestedPost SuggestToMTP( - const std::optional<SuggestOptions> &suggest); +[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest); struct SendOptions { uint64 price = 0; @@ -43,7 +33,7 @@ struct SendOptions { bool invertCaption = false; bool hideViaBot = false; crl::time ttlSeconds = 0; - std::optional<SuggestOptions> suggest; + SuggestPostOptions suggest; friend inline bool operator==( const SendOptions &, diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 4d000104eb..1b2c0fc81f 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -239,6 +239,7 @@ void SendExistingMedia( .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, media, caption); const auto performRequest = [=](const auto &repeatRequest) -> void { @@ -426,6 +427,7 @@ bool SendDice(MessageToSend &message) { .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, TextWithEntities(), MTP_messageMediaDice( MTP_int(0), MTP_string(emoji))); @@ -652,6 +654,7 @@ void SendConfirmedFile( .postAuthor = NewMessagePostAuthor(action), .groupedId = groupId, .effectId = file->to.options.effectId, + .suggest = HistoryMessageSuggestInfo(file->to.options), }, caption, media); } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 004ae8c3b6..a1c6f380c9 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2170,7 +2170,7 @@ void ApiWrap::saveDraftsToCloud() { cloudDraft->webpage, textWithTags.text.isEmpty()), MTP_long(0), // effect - MTPSuggestedPost() // + Api::SuggestToMTP(cloudDraft->suggest) )).done([=](const MTPBool &result, const MTP::Response &response) { const auto requestId = response.requestId; history->finishSavingCloudDraft( @@ -3508,7 +3508,7 @@ void ApiWrap::forwardMessages( .shortcutId = action.options.shortcutId, .starsPaid = action.options.starsApproved, .postAuthor = NewMessagePostAuthor(action), - + .suggest = HistoryMessageSuggestInfo(action.options), // forwarded messages don't have effects //.effectId = action.options.effectId, }, item); @@ -3603,6 +3603,7 @@ void ApiWrap::sendSharedContact( .starsPaid = action.options.starsApproved, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, TextWithEntities(), MTP_messageMediaContact( MTP_string(phone), MTP_string(firstName), @@ -3986,6 +3987,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { .starsPaid = starsPaid, .postAuthor = NewMessagePostAuthor(action), .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), }, sending, media); const auto done = [=]( const MTPUpdates &result, @@ -4036,7 +4038,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { mtpShortcut, MTP_long(action.options.effectId), MTP_long(starsPaid), - SuggestToMTP(action.options.suggest) + Api::SuggestToMTP(action.options.suggest) ), done, fail); } else { histories.sendPreparedMessage( @@ -4056,7 +4058,7 @@ void ApiWrap::sendMessage(MessageToSend &&message) { mtpShortcut, MTP_long(action.options.effectId), MTP_long(starsPaid), - SuggestToMTP(action.options.suggest) + Api::SuggestToMTP(action.options.suggest) ), done, fail); } isFirst = false; @@ -4392,7 +4394,7 @@ void ApiWrap::sendMediaWithRandomId( Data::ShortcutIdToMTP(_session, options.shortcutId), MTP_long(options.effectId), MTP_long(starsPaid), - SuggestToMTP(options.suggest) + Api::SuggestToMTP(options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (done) done(true); if (updateRecentStickers) { @@ -4476,7 +4478,7 @@ void ApiWrap::sendMultiPaidMedia( Data::ShortcutIdToMTP(_session, options.shortcutId), MTP_long(options.effectId), MTP_long(starsPaid), - SuggestToMTP(options.suggest) + Api::SuggestToMTP(options.suggest) ), [=](const MTPUpdates &result, const MTP::Response &response) { if (const auto album = _sendingAlbums.take(groupId)) { const auto copy = (*album)->items; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 230c2ac31b..63bbaede49 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1144,6 +1144,30 @@ historyGiftToUser: IconButton(historyAttach) { icon: icon {{ "chat/input_gift", historyComposeIconFg }}; iconOver: icon {{ "chat/input_gift", historyComposeIconFgOver }}; } +historySuggestPostToggle: IconButton(historyDirectMessage) { + icon: icon{{ "menu/chat_discuss", historyComposeIconFg }}; + iconOver: icon{{ "menu/chat_discuss", historyComposeIconFgOver }}; +} +historySuggestIconPosition: point(12px, 12px); + +suggestOptionsPrice: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(2px, 20px, 2px, 0px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(2px, 0px, 2px, 0px); + placeholderScale: 0.; + placeholderFont: normalFont; + + border: 0px; + borderActive: 0px; + + heightMin: 32px; + + style: defaultTextStyle; +} historyAttachEmojiInner: IconButton(historyAttach) { icon: icon {{ "chat/input_smile_face", historyComposeIconFg }}; diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index ee8d348f9e..95fc3c4f9c 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -21,6 +21,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" namespace Data { +namespace { + +constexpr auto kMaxSuggestStars = 1'000'000'000; + +} // namespace WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) { const auto previewMedia = item->media(); @@ -45,6 +50,7 @@ WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) { Draft::Draft( const TextWithTags &textWithTags, FullReplyTo reply, + SuggestPostOptions suggest, const MessageCursor &cursor, WebPageDraft webpage, mtpRequestId saveRequestId) @@ -58,6 +64,7 @@ Draft::Draft( Draft::Draft( not_null<const Ui::InputField*> field, FullReplyTo reply, + SuggestPostOptions suggest, WebPageDraft webpage, mtpRequestId saveRequestId) : textWithTags(field->getTextWithTags()) @@ -106,9 +113,22 @@ void ApplyPeerCloudDraft( } }, [](const auto &) {}); } + auto suggest = SuggestPostOptions(); + if (!history->suggestDraftAllowed()) { + // Don't apply suggest options in unsupported chats. + } else if (const auto suggested = draft.vsuggested_post()) { + const auto &data = suggested->data(); + suggest.exists = 1; + suggest.date = data.vschedule_date().value_or_empty(); + suggest.stars = uint32(std::clamp( + data.vstars_amount().v, + uint64(), + uint64(kMaxSuggestStars))); + } auto cloudDraft = std::make_unique<Draft>( textWithTags, replyTo, + suggest, MessageCursor(Ui::kQFixedMax, Ui::kQFixedMax, Ui::kQFixedMax), std::move(webpage)); cloudDraft->date = date; @@ -150,18 +170,19 @@ void SetChatLinkDraft(not_null<PeerData*> peer, TextWithEntities draft) { const auto history = peer->owner().history(peer->id); const auto topicRootId = MsgId(); const auto monoforumPeerId = PeerId(); - history->setLocalDraft(std::make_unique<Data::Draft>( + history->setLocalDraft(std::make_unique<Draft>( textWithTags, FullReplyTo{ .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), cursor, - Data::WebPageDraft())); + WebPageDraft())); history->clearLocalEditDraft(topicRootId, monoforumPeerId); history->session().changes().entryUpdated( history, - Data::EntryUpdate::Flag::LocalDraftSet); + EntryUpdate::Flag::LocalDraftSet); } } // namespace Data diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index ab19e0cb2e..5af11ee198 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -52,18 +52,21 @@ struct Draft { Draft( const TextWithTags &textWithTags, FullReplyTo reply, + SuggestPostOptions suggest, const MessageCursor &cursor, WebPageDraft webpage, mtpRequestId saveRequestId = 0); Draft( not_null<const Ui::InputField*> field, FullReplyTo reply, + SuggestPostOptions suggest, WebPageDraft webpage, mtpRequestId saveRequestId = 0); TimeId date = 0; TextWithTags textWithTags; FullReplyTo reply; // reply.messageId.msg is editMsgId for edit draft. + SuggestPostOptions suggest; MessageCursor cursor; WebPageDraft webpage; mtpRequestId saveRequestId = 0; @@ -240,6 +243,7 @@ using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>; [[nodiscard]] inline bool DraftIsNull(const Draft *draft) { return !draft || (!draft->reply.messageId + && !draft->suggest.exists && DraftStringIsEmpty(draft->textWithTags.text)); } diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index 355aff7679..f093a6f2ef 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -190,6 +190,23 @@ struct FullReplyTo { friend inline bool operator==(FullReplyTo, FullReplyTo) = default; }; +struct SuggestPostOptions { + uint32 exists : 1 = 0; + uint32 stars : 31 = 0; + TimeId date = 0; + + explicit operator bool() const { + return exists != 0; + } + + friend inline auto operator<=>( + SuggestPostOptions, + SuggestPostOptions) = default; + friend inline bool operator==( + SuggestPostOptions, + SuggestPostOptions) = default; +}; + struct GlobalMsgId { FullMsgId itemId; uint64 sessionUniqueId = 0; diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index bfe6db59ea..14610cf5f9 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -685,6 +685,9 @@ bool PeerData::canCreatePolls() const { } bool PeerData::canCreateTodoLists() const { + if (isMonoforum()) { + return false; + } return session().premium() && (Data::CanSend(this, ChatRestriction::SendPolls) || isUser()); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_key.h b/Telegram/SourceFiles/dialogs/dialogs_key.h index de4f99f3ad..41c25beff6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_key.h +++ b/Telegram/SourceFiles/dialogs/dialogs_key.h @@ -121,6 +121,7 @@ struct EntryState { Section section = Section::History; FilterId filterId = 0; FullReplyTo currentReplyTo; + SuggestPostOptions currentSuggest; friend inline auto operator<=>( const EntryState&, diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 5227ae17a7..431bf17de1 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -239,6 +239,9 @@ void History::createLocalDraftFromCloud( draft->reply.topicRootId = topicRootId; draft->reply.monoforumPeerId = monoforumPeerId; + if (!suggestDraftAllowed()) { + draft->suggest = SuggestPostOptions(); + } auto existing = localDraft(topicRootId, monoforumPeerId); if (Data::DraftIsNull(existing) || !existing->date @@ -247,12 +250,14 @@ void History::createLocalDraftFromCloud( setLocalDraft(std::make_unique<Data::Draft>( draft->textWithTags, draft->reply, + draft->suggest, draft->cursor, draft->webpage)); existing = localDraft(topicRootId, monoforumPeerId); } else if (existing != draft) { existing->textWithTags = draft->textWithTags; existing->reply = draft->reply; + existing->suggest = draft->suggest; existing->cursor = draft->cursor; existing->webpage = draft->webpage; } @@ -325,6 +330,7 @@ Data::Draft *History::createCloudDraft( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); cloudDraft(topicRootId, monoforumPeerId)->date = TimeId(0); @@ -337,18 +343,23 @@ Data::Draft *History::createCloudDraft( setCloudDraft(std::make_unique<Data::Draft>( fromDraft->textWithTags, reply, + fromDraft->suggest, fromDraft->cursor, fromDraft->webpage)); existing = cloudDraft(topicRootId, monoforumPeerId); } else if (existing != fromDraft) { existing->textWithTags = fromDraft->textWithTags; existing->reply = fromDraft->reply; + existing->suggest = fromDraft->suggest; existing->cursor = fromDraft->cursor; existing->webpage = fromDraft->webpage; } existing->date = base::unixtime::now(); existing->reply.topicRootId = topicRootId; existing->reply.monoforumPeerId = monoforumPeerId; + if (!suggestDraftAllowed()) { + existing->suggest = SuggestPostOptions(); + } } if (const auto thread = threadFor(topicRootId, monoforumPeerId)) { @@ -3382,6 +3393,10 @@ bool History::amMonoforumAdmin() const { return (_flags & Flag::IsMonoforumAdmin); } +bool History::suggestDraftAllowed() const { + return peer->isMonoforum() || !peer->amMonoforumAdmin(); +} + not_null<History*> History::migrateToOrMe() const { if (const auto to = peer->migrateTo()) { return owner().history(to); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 86c14ccc37..a36d2000c6 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -79,6 +79,7 @@ public: void monoforumChanged(Data::SavedMessages *old); [[nodiscard]] bool amMonoforumAdmin() const; + [[nodiscard]] bool suggestDraftAllowed() const; [[nodiscard]] not_null<History*> migrateToOrMe() const; [[nodiscard]] History *migrateFrom() const; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 2df23d25f6..9ce8ed0b79 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -190,6 +190,7 @@ struct HistoryItem::CreateConfig { TimeId editDate = 0; HistoryMessageMarkupData markup; HistoryMessageRepliesData replies; + HistoryMessageSuggestInfo suggest; bool imported = false; // For messages created from existing messages (forwarded). @@ -3841,6 +3842,9 @@ void HistoryItem::createComponents(CreateConfig &&config) { mask |= HistoryMessageRestrictions::Bit(); } } + if (config.suggest.exists) { + mask |= HistoryMessageSuggestedPost::Bit(); + } UpdateComponents(mask); @@ -3935,6 +3939,13 @@ void HistoryItem::createComponents(CreateConfig &&config) { flagSensitiveContent(); } + if (const auto suggest = Get<HistoryMessageSuggestedPost>()) { + suggest->stars = config.suggest.stars; + suggest->date = config.suggest.date; + suggest->accepted = config.suggest.accepted; + suggest->rejected = config.suggest.rejected; + } + if (out() && isSending()) { if (const auto channel = _history->peer->asMegagroup()) { _boostsApplied = channel->mgInfo->boostsApplied; @@ -4137,6 +4148,9 @@ void HistoryItem::createComponentsHelper(HistoryItemCommonFields &&fields) { if (fields.flags & MessageFlag::HasViews) { config.viewsCount = 1; } + if (fields.suggest.exists) { + config.suggest = fields.suggest; + } createComponents(std::move(config)); } @@ -4261,6 +4275,7 @@ void HistoryItem::createComponents(const MTPDmessage &data) { config.postAuthor = qs(data.vpost_author().value_or_empty()); config.restrictions = Data::UnavailableReason::Extract( data.vrestriction_reason()); + config.suggest = HistoryMessageSuggestInfo(data.vsuggested_post()); createComponents(std::move(config)); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index d869944333..64e6880da6 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -82,6 +82,7 @@ struct HistoryItemCommonFields { uint64 groupedId = 0; EffectId effectId = 0; HistoryMessageMarkupData markup; + HistoryMessageSuggestInfo suggest; bool ignoreForwardFrom = false; bool ignoreForwardCaptions = false; }; diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 7050237255..7e6c627d27 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -614,6 +614,14 @@ struct HistoryMessageFactcheck bool requested = false; }; +struct HistoryMessageSuggestedPost +: RuntimeComponent<HistoryMessageSuggestedPost, HistoryItem> { + int stars = 0; + TimeId date = 0; + bool accepted = false; + bool rejected = false; +}; + struct HistoryMessageRestrictions : RuntimeComponent<HistoryMessageRestrictions, HistoryItem> { std::vector<Data::UnavailableReason> reasons; diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index 319a77238d..4a37ca15eb 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -312,7 +312,7 @@ HistoryMessageRepliesData::HistoryMessageRepliesData( if (!data) { return; } - const auto &fields = data->c_messageReplies(); + const auto &fields = data->data(); if (const auto list = fields.vrecent_repliers()) { recentRepliers.reserve(list->v.size()); for (const auto &id : list->v) { @@ -326,3 +326,31 @@ HistoryMessageRepliesData::HistoryMessageRepliesData( isNull = false; pts = fields.vreplies_pts().v; } + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + const MTPSuggestedPost *data) { + if (!data) { + return; + } + const auto &fields = data->data(); + stars = fields.vstars_amount().v; + date = fields.vschedule_date().value_or_empty(); + accepted = fields.is_accepted(); + rejected = fields.is_rejected(); + exists = true; +} + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + const Api::SendOptions &options) +: HistoryMessageSuggestInfo(options.suggest) { +} + +HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( + SuggestPostOptions options) { + if (!options.exists) { + return; + } + stars = options.stars; + date = options.date; + exists = true; +} diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 77895c3aa1..8a735903fb 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -10,6 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flags.h" #include "data/data_chat_participant_status.h" +namespace Api { +struct SendOptions; +} // namespace Api + namespace Data { class Session; } // namespace Data @@ -136,3 +140,16 @@ struct HistoryMessageRepliesData { bool isNull = true; int pts = 0; }; + +struct HistoryMessageSuggestInfo { + HistoryMessageSuggestInfo() = default; + explicit HistoryMessageSuggestInfo(const MTPSuggestedPost *data); + explicit HistoryMessageSuggestInfo(const Api::SendOptions &options); + explicit HistoryMessageSuggestInfo(SuggestPostOptions options); + + int stars = 0; + TimeId date = 0; + bool accepted = false; + bool rejected = false; + bool exists = false; +}; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 0eb51fbb53..4b968b884a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -118,6 +118,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_sticker_toast.h" #include "history/view/history_view_subsection_tabs.h" +#include "history/view/history_view_suggest_options.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" @@ -1039,6 +1040,7 @@ Dialogs::EntryState HistoryWidget::computeDialogsEntryState() const { .key = _history, .section = Dialogs::EntryState::Section::History, .currentReplyTo = replyTo(), + .currentSuggest = suggestOptions(), }; } @@ -1900,13 +1902,16 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), _preview->draft(), _saveEditMsgRequestId)); } else { - if (_replyTo || !_field->empty()) { + const auto suggest = suggestOptions(); + if (_replyTo || suggest.exists || !_field->empty()) { _history->setLocalDraft(std::make_unique<Data::Draft>( _field, _replyTo, + suggest, _preview->draft())); } else { _history->clearLocalDraft(topicRootId, monoforumPeerId); @@ -2270,6 +2275,8 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { if (_processingReplyTo) { _processingReplyItem = session().data().message( _processingReplyTo.messageId); + } else if (draft && draft->suggest) { + applySuggestOptions(draft->suggest); } processReply(); } @@ -2480,6 +2487,7 @@ void HistoryWidget::showHistory( setHistory(nullptr); _list = nullptr; _peer = nullptr; + _suggestOptions = nullptr; _sendPayment.clear(); _topicsRequested.clear(); _canSendMessages = false; @@ -2606,6 +2614,7 @@ void HistoryWidget::showHistory( } else if (_peer->isRepliesChat() || _peer->isVerifyCodes()) { updateNotifyControls(); } + refreshSuggestPostToggle(); refreshScheduledToggle(); refreshSendGiftToggle(); refreshSendAsToggle(); @@ -2917,6 +2926,7 @@ void HistoryWidget::registerDraftSource() { (editMsgId ? FullReplyTo{ FullMsgId(peerId, editMsgId) } : _replyTo), + (editMsgId ? SuggestPostOptions() : suggestOptions()), _field->getTextWithTags(), _preview->draft(), }; @@ -3154,6 +3164,41 @@ void HistoryWidget::refreshSendGiftToggle() { } } +void HistoryWidget::applySuggestOptions(SuggestPostOptions suggest) { + Expects(suggest.exists); + + using namespace HistoryView; + _suggestOptions = std::make_unique<SuggestOptions>( + controller(), + _peer, + suggest); + _suggestOptions->repaints() | rpl::start_with_next([=] { + updateField(); + }, _suggestOptions->lifetime()); +} + +void HistoryWidget::refreshSuggestPostToggle() { + const auto has = _peer + && _peer->isMonoforum() + && !_peer->amMonoforumAdmin(); + if (!_toggleSuggestPost && has) { + _toggleSuggestPost.create(this, st::historySuggestPostToggle); + _toggleSuggestPost->setVisible(!_suggestOptions); + _toggleSuggestPost->addClickHandler([=] { + applySuggestOptions({ .exists = 1 }); + cancelReply(); + _processingReplyTo = FullReplyTo(); + _processingReplyItem = nullptr; + updateControlsVisibility(); + updateControlsGeometry(); + }); + orderWidgets(); + } else if (_toggleSuggestPost && !has) { + _toggleSuggestPost.destroy(); + cancelSuggestPost(); + } +} + void HistoryWidget::setupSendAsToggle() { session().sendAsPeers().updated( ) | rpl::filter([=](not_null<PeerData*> peer) { @@ -3294,6 +3339,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->hide(); + } if (_giftToUser) { _giftToUser->hide(); } @@ -3408,6 +3456,14 @@ void HistoryWidget::updateControlsVisibility() { rightButtonsChanged = true; } } + if (_toggleSuggestPost) { + const auto was = _toggleSuggestPost->isVisible(); + const auto now = !_suggestOptions; + if (was != now) { + _toggleSuggestPost->setVisible(now); + rightButtonsChanged = true; + } + } if (_giftToUser) { const auto was = _giftToUser->isVisible(); const auto now = (!_editMsgId) && (!hideExtraButtons); @@ -3437,7 +3493,8 @@ void HistoryWidget::updateControlsVisibility() { || _replyTo || readyToForward() || _previewDrawPreview - || _kbReplyTo) { + || _kbReplyTo + || _suggestOptions) { if (_fieldBarCancel->isHidden()) { _fieldBarCancel->show(); updateControlsGeometry(); @@ -3466,6 +3523,9 @@ void HistoryWidget::updateControlsVisibility() { if (_scheduled) { _scheduled->hide(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->hide(); + } if (_giftToUser) { _giftToUser->hide(); } @@ -4503,6 +4563,7 @@ Api::SendAction HistoryWidget::prepareSendAction( Api::SendOptions options) const { auto result = Api::SendAction(_history, options); result.replyTo = replyTo(); + result.options.suggest = suggestOptions(); result.options.sendAs = _sendAs ? _history->session().sendAsPeers().resolveChosen( _history->peer).get() @@ -5040,7 +5101,11 @@ void HistoryWidget::updateOverStates(QPoint pos) { st::historyReplyHeight); const auto hasWebPage = !!_previewDrawPreview; const auto inDetails = detailsRect.contains(pos) - && (_editMsgId || replyTo() || isReadyToForward || hasWebPage); + && (_editMsgId + || replyTo() + || isReadyToForward + || hasWebPage + || _suggestOptions); const auto inPhotoEdit = inDetails && _photoEditMedia && QRect( @@ -5585,7 +5650,8 @@ void HistoryWidget::toggleKeyboard(bool manual) { if (!readyToForward() && !_previewDrawPreview && !_editMsgId - && !_replyTo) { + && !_replyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -5806,7 +5872,7 @@ void HistoryWidget::moveFieldControls() { } // (_botMenu.button) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel -// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send +// (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_giftToUser) (_silent|_cmdStart|_kbShow) (_toggleSuggestPost) (_kbHide|_tabbedSelectorToggle) _send // (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages) auto buttonsBottom = bottom - _attachToggle->height(); @@ -5848,6 +5914,10 @@ void HistoryWidget::moveFieldControls() { if (kbShowShown || _cmdStartShown || _silent) { right += _botCommandStart->width(); } + if (_toggleSuggestPost) { + _toggleSuggestPost->moveToRight(right, buttonsBottom); + right += _toggleSuggestPost->width(); + } if (_giftToUser) { _giftToUser->moveToRight(right, buttonsBottom); right += _giftToUser->width(); @@ -5912,6 +5982,9 @@ void HistoryWidget::updateFieldSize() { if (_silent && !_silent->isHidden()) { fieldWidth -= _silent->width(); } + if (_toggleSuggestPost && !_toggleSuggestPost->isHidden()) { + fieldWidth -= _toggleSuggestPost->width(); + } if (_giftToUser && !_giftToUser->isHidden()) { fieldWidth -= _giftToUser->width(); } @@ -6590,6 +6663,12 @@ FullReplyTo HistoryWidget::replyTo() const { : FullReplyTo(); } +SuggestPostOptions HistoryWidget::suggestOptions() const { + return (_history && _history->suggestDraftAllowed() && _suggestOptions) + ? _suggestOptions->values() + : SuggestPostOptions(); +} + bool HistoryWidget::hasSavedScroll() const { Expects(_history != nullptr); @@ -6797,7 +6876,8 @@ void HistoryWidget::updateHistoryGeometry( if (_editMsgId || replyTo() || readyToForward() - || _previewDrawPreview) { + || _previewDrawPreview + || _suggestOptions) { newScrollHeight -= st::historyReplyHeight; } if (_kbShown) { @@ -7143,7 +7223,8 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { _kbReplyTo = nullptr; if (!readyToForward() && !_previewDrawPreview - && !_replyTo) { + && !_replyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7162,7 +7243,8 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { if (!readyToForward() && !_previewDrawPreview && !_replyTo - && !_editMsgId) { + && !_editMsgId + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -7316,6 +7398,8 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { _kbReplyTo->history()->peer->id, Window::SectionShow::Way::Forward, _kbReplyTo->id); + } else if (_suggestOptions) { + _suggestOptions->edit(); } } @@ -7324,6 +7408,7 @@ void HistoryWidget::editDraftOptions() { const auto history = _history; const auto reply = _replyTo; + const auto suggest = suggestOptions(); const auto webpage = _preview->draft(); const auto forward = _forwardPanel->draft(); @@ -7348,7 +7433,7 @@ void HistoryWidget::editDraftOptions() { EditDraftOptions({ .show = controller()->uiShow(), .history = history, - .draft = Data::Draft(_field, reply, _preview->draft()), + .draft = Data::Draft(_field, reply, suggest, _preview->draft()), .usedLink = _preview->link(), .forward = _forwardPanel->draft(), .links = _preview->links(), @@ -8465,7 +8550,9 @@ void HistoryWidget::processReply() { if (!_peer || !_processingReplyTo) { return processCancel(); - } else if (!_processingReplyItem) { + } + cancelSuggestPost(); + if (!_processingReplyItem) { session().api().requestMessageData( session().data().peer(_processingReplyTo.messageId.peer), _processingReplyTo.messageId.msg, @@ -8524,16 +8611,19 @@ void HistoryWidget::setReplyFieldsFromProcessing() { if (_editMsgId) { if (const auto localDraft = _history->localDraft({}, {})) { localDraft->reply = id; + localDraft->suggest = SuggestPostOptions(); } else { _history->setLocalDraft(std::make_unique<Data::Draft>( TextWithTags(), id, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); } } else { _replyEditMsg = item; _replyTo = id; + cancelSuggestPost(); updateReplyEditText(_replyEditMsg); updateCanSendMessage(); updateBotKeyboard(); @@ -8573,10 +8663,12 @@ void HistoryWidget::editMessage( _send->clearState(); } if (!_editMsgId) { - if (_replyTo || !_field->empty()) { + const auto suggest = suggestOptions(); + if (_replyTo || suggest.exists || !_field->empty()) { _history->setLocalDraft(std::make_unique<Data::Draft>( _field, _replyTo, + suggest, _preview->draft())); } else { _history->clearLocalDraft(MsgId(), PeerId()); @@ -8593,6 +8685,7 @@ void HistoryWidget::editMessage( _history->setLocalEditDraft(std::make_unique<Data::Draft>( editData, FullReplyTo{ item->fullId() }, + SuggestPostOptions(), cursor, previewDraft)); applyDraft(); @@ -8679,7 +8772,8 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { mouseMoveEvent(0); if (!readyToForward() && !_previewDrawPreview - && !_kbReplyTo) { + && !_kbReplyTo + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -8754,7 +8848,8 @@ void HistoryWidget::cancelEdit() { mouseMoveEvent(nullptr); if (!readyToForward() && !_previewDrawPreview - && !replyTo()) { + && !replyTo() + && !_suggestOptions) { _fieldBarCancel->hide(); updateMouseTracking(); } @@ -8784,9 +8879,21 @@ void HistoryWidget::cancelFieldAreaState() { _history->setForwardDraft(MsgId(), PeerId(), {}); } else if (_kbReplyTo) { toggleKeyboard(); + } else if (_suggestOptions) { + cancelSuggestPost(); } } +bool HistoryWidget::cancelSuggestPost() { + if (!_suggestOptions) { + return false; + } + _suggestOptions = nullptr; + updateControlsVisibility(); + updateControlsGeometry(); + return true; +} + void HistoryWidget::fullInfoUpdated() { auto refresh = false; if (_list) { @@ -8891,6 +8998,7 @@ bool HistoryWidget::updateCanSendMessage() { if (!_canSendMessages) { cancelReply(); } + refreshSuggestPostToggle(); refreshScheduledToggle(); refreshSendGiftToggle(); refreshSilentToggle(); @@ -9190,13 +9298,12 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { auto backh = fieldHeight() + 2 * st::historySendPadding; auto hasForward = readyToForward(); auto drawMsgText = (_editMsgId || _replyTo) ? _replyEditMsg : _kbReplyTo; - if (_editMsgId || _replyTo || (!hasForward && _kbReplyTo)) { - backy -= st::historyReplyHeight; - backh += st::historyReplyHeight; - } else if (hasForward) { - backy -= st::historyReplyHeight; - backh += st::historyReplyHeight; - } else if (_previewDrawPreview) { + if (_editMsgId + || _replyTo + || hasForward + || _kbReplyTo + || _previewDrawPreview + || _suggestOptions) { backy -= st::historyReplyHeight; backh += st::historyReplyHeight; } @@ -9361,6 +9468,8 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { - _fieldBarCancel->width() - st::msgReplyPadding.right(); _forwardPanel->paint(p, x, backy, available, width()); + } else if (_suggestOptions) { + _suggestOptions->paintBar(p, 0, backy, width()); } } @@ -9462,7 +9571,8 @@ void HistoryWidget::paintEvent(QPaintEvent *e) { if (restrictionHidden || replyTo() || readyToForward() - || _kbShown) { + || _kbShown + || _suggestOptions) { if (!isSearching()) { drawField(p, clip); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 42f2cf7f8c..f18577abe2 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -109,6 +109,7 @@ class TranslateBar; class ComposeSearch; class SubsectionTabs; struct SelectedQuote; +class SuggestOptions; } // namespace HistoryView namespace HistoryView::Controls { @@ -214,9 +215,11 @@ public: not_null<PeerData*> peer); [[nodiscard]] FullReplyTo replyTo() const; + [[nodiscard]] SuggestPostOptions suggestOptions() const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; bool cancelReply(bool lastKeyboardUsed = false); + bool cancelSuggestPost(); void cancelEdit(); void updateForwarding(); @@ -676,6 +679,8 @@ private: void setupScheduledToggle(); void refreshScheduledToggle(); void refreshSendGiftToggle(); + void refreshSuggestPostToggle(); + void applySuggestOptions(SuggestPostOptions suggest); void setupSendAsToggle(); void refreshSendAsToggle(); void refreshAttachBotsMenu(); @@ -709,6 +714,8 @@ private: std::unique_ptr<Ui::SpoilerAnimation> _replySpoiler; mutable base::Timer _updateEditTimeLeftDisplay; + std::unique_ptr<HistoryView::SuggestOptions> _suggestOptions; + object_ptr<Ui::IconButton> _fieldBarCancel; std::unique_ptr<Ui::RpWidget> _topBars; @@ -821,6 +828,7 @@ private: object_ptr<Ui::IconButton> _botKeyboardShow; object_ptr<Ui::IconButton> _botKeyboardHide; object_ptr<Ui::IconButton> _botCommandStart; + object_ptr<Ui::IconButton> _toggleSuggestPost = { nullptr }; object_ptr<Ui::IconButton> _giftToUser = { nullptr }; object_ptr<Ui::SilentToggle> _silent = { nullptr }; object_ptr<Ui::IconButton> _scheduled = { nullptr }; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 52aecc6994..294ffd9ca5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1006,6 +1006,7 @@ void ComposeControls::setCurrentDialogsEntryState( unregisterDraftSources(); state.currentReplyTo.topicRootId = _topicRootId; state.currentReplyTo.monoforumPeerId = _monoforumPeerId; + state.currentSuggest = SuggestPostOptions(); _currentDialogsEntryState = state; updateForwarding(); registerDraftSource(); @@ -1299,7 +1300,11 @@ void ComposeControls::saveFieldToHistoryLocalDraft() { const auto key = draftKeyCurrent(); _history->setDraft( key, - std::make_unique<Data::Draft>(_field, id, _preview->draft())); + std::make_unique<Data::Draft>( + _field, + id, + SuggestPostOptions(), + _preview->draft())); } else { _history->clearDraft(draftKeyCurrent()); } @@ -1414,6 +1419,7 @@ void ComposeControls::init() { const auto topicRootId = _topicRootId; const auto monoforumPeerId = _monoforumPeerId; const auto reply = _header->replyingToMessage(); + const auto suggest = SuggestPostOptions(); const auto webpage = _preview->draft(); const auto done = [=]( @@ -1441,7 +1447,7 @@ void ComposeControls::init() { EditDraftOptions({ .show = _show, .history = history, - .draft = Data::Draft(_field, reply, _preview->draft()), + .draft = Data::Draft(_field, reply, suggest, _preview->draft()), .usedLink = _preview->link(), .forward = _header->forwardDraft(), .links = _preview->links(), @@ -1891,6 +1897,7 @@ void ComposeControls::registerDraftSource() { const auto draft = [=] { return Storage::MessageDraft{ _header->getDraftReply(), + SuggestPostOptions(), _field->getTextWithTags(), _preview->draft(), }; @@ -2980,6 +2987,7 @@ void ComposeControls::editMessage(not_null<HistoryItem*> item) { .topicRootId = key.topicRootId(), .monoforumPeerId = key.monoforumPeerId(), }, + SuggestPostOptions(), cursor, Data::WebPageDraft::FromItem(item))); applyDraft(); @@ -3076,6 +3084,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { std::make_unique<Data::Draft>( TextWithTags(), id, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft())); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 1277052948..f398211636 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -1386,6 +1386,7 @@ void ShowReplyToChatBox( history->setLocalDraft(std::make_unique<Data::Draft>( textWithTags, reply, + SuggestPostOptions(), cursor, Data::WebPageDraft())); history->clearLocalEditDraft(topicRootId, monoforumPeerId); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index ff314cdf9e..de53aeb673 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -353,6 +353,7 @@ void ClearDraftReplyTo( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }; + draft.suggest = SuggestPostOptions(); if (Data::DraftIsNull(&draft)) { history->clearLocalDraft(topicRootId, monoforumPeerId); } else { diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 03f871bbaf..a4d8b228ce 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -1830,6 +1830,7 @@ void ChatWidget::refreshTopBarActiveChat() { ? EntryState::Section::SavedSublist : EntryState::Section::Replies, .currentReplyTo = replyTo(), + .currentSuggest = SuggestPostOptions(), }; _topBar->setActiveChat(state, _sendAction.get()); _composeControls->setCurrentDialogsEntryState(state); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index eab953b1e2..fa9ed5edd2 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_message.h" +#include "base/unixtime.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "core/ui_integration.h" #include "history/view/history_view_cursor_state.h" @@ -454,6 +455,20 @@ Message::~Message() { void Message::initPaidInformation() { const auto item = data(); if (!item->history()->peer->isUser()) { + + + if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { + if (!suggest->stars && !suggest->date) { + setServicePreMessage({ { u"suggestion to publish for free anytime"_q } }); + } else if (!suggest->date) { + setServicePreMessage({ { u"suggestion to publish for %1 stars anytime"_q.arg(suggest->stars) }}); + } else if (!suggest->stars) { + setServicePreMessage({ { u"suggestion to publish for free %1"_q.arg(langDateTime(base::unixtime::parse(suggest->date))) }}); + } else { + setServicePreMessage({ { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } }); + } + } + return; } const auto media = this->media(); diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp new file mode 100644 index 0000000000..da6566ce0f --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -0,0 +1,237 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/history_view_suggest_options.h" + +#include "base/unixtime.h" +#include "data/data_channel.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/boxes/choose_date_time.h" +#include "ui/widgets/fields/number_input.h" +#include "ui/widgets/buttons.h" +#include "ui/vertical_list.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_settings.h" + +namespace HistoryView { +namespace { + +struct EditOptionsArgs { + int starsLimit = 0; + QString channelName; + SuggestPostOptions values; + Fn<void(SuggestPostOptions)> save; +}; + +void EditOptionsBox( + not_null<Ui::GenericBox*> box, + EditOptionsArgs &&args) { + struct State { + rpl::variable<TimeId> date; + }; + const auto state = box->lifetime().make_state<State>(); + state->date = args.values.date; + + box->setTitle(tr::lng_suggest_options_title()); + + const auto container = box->verticalLayout(); + + Ui::AddSkip(container); + Ui::AddSubsectionTitle(container, tr::lng_suggest_options_price()); + + const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>( + box, + st::editTagField.heightMin)); + auto owned = object_ptr<Ui::NumberInput>( + wrap, + st::editTagField, + tr::lng_paid_cost_placeholder(), + args.values.stars ? QString::number(args.values.stars) : QString(), + args.starsLimit); + const auto field = owned.data(); + wrap->widthValue() | rpl::start_with_next([=](int width) { + field->move(0, 0); + field->resize(width, field->height()); + wrap->resize(width, field->height()); + }, wrap->lifetime()); + field->paintRequest() | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(field); + st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); + }, field->lifetime()); + field->selectAll(); + box->setFocusCallback([=] { + field->setFocusFast(); + }); + + Ui::AddSkip(container); + Ui::AddSkip(container); + Ui::AddDividerText( + container, + tr::lng_suggest_options_price_about( + lt_channel, + rpl::single(args.channelName))); + Ui::AddSkip(container); + + const auto time = Settings::AddButtonWithLabel( + container, + tr::lng_suggest_options_date(), + state->date.value() | rpl::map([](TimeId date) { + return date + ? langDateTime(base::unixtime::parse(date)) + : tr::lng_suggest_options_date_any(tr::now); + }), + st::settingsButtonNoIcon); + + time->setClickedCallback([=] { + box->uiShow()->show(Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ + .title = tr::lng_suggest_options_date(), + .submit = tr::lng_settings_save(), + .done = [=](TimeId result) { state->date = result; }, + .min = [] { return base::unixtime::now() + 1; }, + .time = (state->date.current() + ? state->date.current() + : (base::unixtime::now() + 86400)), + })); + }); + + Ui::AddSkip(container); + Ui::AddDividerText(container, tr::lng_suggest_options_date_about()); + AssertIsDebug()//tr::lng_suggest_options_offer + const auto save = [=] { + const auto now = uint32(field->getLastText().toULongLong()); + if (now > args.starsLimit) { + field->showError(); + return; + } + const auto weak = Ui::MakeWeak(box); + args.save({ .stars = now, .date = state->date.current()}); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + + QObject::connect(field, &Ui::NumberInput::submitted, box, save); + + box->addButton(tr::lng_settings_save(), save); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +} // namespace + +SuggestOptions::SuggestOptions( + not_null<Window::SessionController*> controller, + not_null<PeerData*> peer, + SuggestPostOptions values) +: _controller(controller) +, _peer(peer) +, _values(values) { + updateTexts(); +} + +SuggestOptions::~SuggestOptions() = default; + +void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) { + st::historyDirectMessage.icon.paint( + p, + QPoint(x, y) + st::historySuggestIconPosition, + outerWidth); + + x += st::historyReplySkip; + auto available = outerWidth + - x + - st::historyReplyCancel.width + - st::msgReplyPadding.right(); + p.setPen(st::windowActiveTextFg); + _title.draw(p, { + .position = QPoint(x, y + st::msgReplyPadding.top()), + .availableWidth = available, + }); + p.setPen(st::windowSubTextFg); + _text.draw(p, { + .position = QPoint( + x, + y + st::msgReplyPadding.top() + st::msgServiceNameFont->height), + .availableWidth = available, + }); +} + +void SuggestOptions::edit() { + const auto apply = [=](SuggestPostOptions values) { + _values = values; + updateTexts(); + _repaints.fire({}); + }; + const auto broadcast = _peer->monoforumBroadcast(); + const auto &appConfig = _peer->session().appConfig(); + _controller->show(Box(EditOptionsBox, EditOptionsArgs{ + .starsLimit = appConfig.suggestedPostStarsMax(), + .channelName = (broadcast ? broadcast : _peer.get())->shortName(), + .values = _values, + .save = apply, + })); +} + +void SuggestOptions::updateTexts() { + _title.setText( + st::semiboldTextStyle, + tr::lng_suggest_bar_title(tr::now)); + _text.setMarkedText(st::defaultTextStyle, composeText()); +} + +TextWithEntities SuggestOptions::composeText() const { + if (!_values.stars && !_values.date) { + return tr::lng_suggest_bar_text(tr::now, Ui::Text::WithEntities); + } else if (!_values.date) { + return tr::lng_suggest_bar_priced( + tr::now, + lt_amount, + TextWithEntities{ QString::number(_values.stars) + " stars" }, + Ui::Text::WithEntities); + } else if (!_values.stars) { + return tr::lng_suggest_bar_dated( + tr::now, + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(_values.date)), + }, + Ui::Text::WithEntities); + } + return tr::lng_suggest_bar_priced_dated( + tr::now, + lt_amount, + TextWithEntities{ QString::number(_values.stars) + " stars," }, + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(_values.date)), + }, + Ui::Text::WithEntities); +} + +SuggestPostOptions SuggestOptions::values() const { + auto result = _values; + result.exists = 1; + return result; +} + +rpl::producer<> SuggestOptions::repaints() const { + return _repaints.events(); +} + +rpl::lifetime &SuggestOptions::lifetime() { + return _lifetime; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/history_view_suggest_options.h new file mode 100644 index 0000000000..26d11c1c70 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.h @@ -0,0 +1,53 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "api/api_common.h" + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView { + +class SuggestOptions final { +public: + SuggestOptions( + not_null<Window::SessionController*> controller, + not_null<PeerData*> peer, + SuggestPostOptions values); + ~SuggestOptions(); + + void paintBar(QPainter &p, int x, int y, int outerWidth); + void edit(); + + [[nodiscard]] SuggestPostOptions values() const; + + [[nodiscard]] rpl::producer<> repaints() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void updateTexts(); + + [[nodiscard]] TextWithEntities composeText() const; + + const not_null<Window::SessionController*> _controller; + const not_null<PeerData*> _peer; + + Ui::Text::String _title; + Ui::Text::String _text; + + SuggestPostOptions _values; + rpl::event_stream<> _repaints; + + rpl::lifetime _lifetime; + +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index d6ca306167..a1d7e29ac9 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -359,6 +359,7 @@ WebViewContext ResolveContext( if (const auto thread = state.key.thread()) { context.action = Api::SendAction(thread); context.action->replyTo = state.currentReplyTo; + context.action->options.suggest = state.currentSuggest; } else { context.action = Api::SendAction(bot->owner().history(bot)); } @@ -373,6 +374,7 @@ WebViewContext ResolveContext( .key = (topic ? Key{ topic } : Key{ history }), .section = (topic ? Section::Replies : Section::History), .currentReplyTo = context.action->replyTo, + .currentSuggest = context.action->options.suggest, }; } return context; @@ -2615,11 +2617,11 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu( ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); - const auto replyTo = action.replyTo; Window::PeerMenuCreatePoll( controller, peer, - replyTo, + action.replyTo, + action.options.suggest, flag, flag, source, @@ -2637,11 +2639,11 @@ std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu( || action.history->peer->starsPerMessageChecked()) ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; - const auto replyTo = action.replyTo; Window::PeerMenuCreateTodoList( controller, peer, - replyTo, + action.replyTo, + action.options.suggest, source, { sendMenuType }); }, &st::menuIconCreateTodoList); diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 8875884f24..1adcc5a856 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -162,6 +162,10 @@ int AppConfig::todoListItemTextLimit() const { return get<int>(u"todo_item_length_max"_q, 64); } +int AppConfig::suggestedPostStarsMax() const { + return get<int>(u"stars_suggested_post_amount_max"_q, 100'000); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index bf0053d79b..886e11280c 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -88,6 +88,8 @@ public: [[nodiscard]] int todoListTitleLimit() const; [[nodiscard]] int todoListItemTextLimit() const; + [[nodiscard]] int suggestedPostStarsMax() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index a26693f680..42f3494180 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -606,6 +606,7 @@ bool MainWidget::shareUrl( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), cursor, Data::WebPageDraft())); history->clearLocalEditDraft(topicRootId, monoforumPeerId); diff --git a/Telegram/SourceFiles/media/stories/media_stories_share.cpp b/Telegram/SourceFiles/media/stories/media_stories_share.cpp index 4773e7e068..7419a6715e 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_share.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_share.cpp @@ -174,7 +174,7 @@ namespace Media::Stories { Data::ShortcutIdToMTP(session, options.shortcutId), MTP_long(options.effectId), MTP_long(starsPaid), - SuggestToMTP(options.suggest) + Api::SuggestToMTP(options.suggest) ), [=]( const MTPUpdates &result, const MTP::Response &response) { diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index fa1fd40fd0..135f7df7e3 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -632,6 +632,7 @@ void ShortcutMessages::setupComposeControls() { .key = Dialogs::Key{ _history }, .section = Dialogs::EntryState::Section::ShortcutMessages, .currentReplyTo = replyTo(), + .currentSuggest = SuggestPostOptions(), }; _composeControls->setCurrentDialogsEntryState(state); diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 5d88879bfa..cbcb74e24e 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -67,6 +67,7 @@ constexpr auto kMultiDraftCursorsTagOld = quint64(0xFFFF'FFFF'FFFF'FF02ULL); constexpr auto kMultiDraftTag = quint64(0xFFFF'FFFF'FFFF'FF03ULL); constexpr auto kMultiDraftCursorsTag = quint64(0xFFFF'FFFF'FFFF'FF04ULL); constexpr auto kRichDraftsTag = quint64(0xFFFF'FFFF'FFFF'FF05ULL); +constexpr auto kDraftsTag2 = quint64(0xFFFF'FFFF'FFFF'FF06ULL); enum { // Local Storage Keys lskUserMap = 0x00, @@ -1187,6 +1188,7 @@ void EnumerateDrafts( callback( key, draft->reply, + draft->suggest, draft->textWithTags, draft->webpage, draft->cursor); @@ -1200,6 +1202,7 @@ void EnumerateDrafts( callback( key, draft.reply, + draft.suggest, draft.textWithTags, draft.webpage, cursor); @@ -1265,6 +1268,7 @@ void Account::writeDrafts(not_null<History*> history) { const auto sizeCallback = [&]( auto&&, // key const FullReplyTo &reply, + SuggestPostOptions suggest, const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor @@ -1272,6 +1276,7 @@ void Account::writeDrafts(not_null<History*> history) { + Serialize::stringSize(text.text) + TextUtilities::SerializeTagsSize(text.tags) + sizeof(qint64) + sizeof(qint64) // messageId + + sizeof(quint64) // suggest + Serialize::stringSize(webpage.url) + sizeof(qint32) // webpage.forceLargeMedia + sizeof(qint32) // webpage.forceSmallMedia @@ -1287,13 +1292,14 @@ void Account::writeDrafts(not_null<History*> history) { EncryptedDescriptor data(size); data.stream - << quint64(kRichDraftsTag) + << quint64(kDraftsTag2) << SerializePeerId(peerId) << quint32(count); const auto writeCallback = [&]( const Data::DraftKey &key, const FullReplyTo &reply, + SuggestPostOptions suggest, const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor @@ -1303,6 +1309,9 @@ void Account::writeDrafts(not_null<History*> history) { << TextUtilities::SerializeTags(text.tags) << qint64(reply.messageId.peer.value) << qint64(reply.messageId.msg.bare) + << quint64(quint64(quint32(suggest.date)) + | (quint64(suggest.stars) << 32) + | (quint64(suggest.exists) << 63)) << webpage.url << qint32(webpage.forceLargeMedia ? 1 : 0) << qint32(webpage.forceSmallMedia ? 1 : 0) @@ -1359,6 +1368,7 @@ void Account::writeDraftCursors(not_null<History*> history) { const auto writeCallback = [&]( const Data::DraftKey &key, auto&&, // reply + auto&&, // suggest auto&&, // text auto&&, // webpage const MessageCursor &cursor) { // cursor @@ -1519,12 +1529,14 @@ void Account::readDraftsWithCursors(not_null<History*> history) { } auto map = Data::HistoryDrafts(); const auto keysOld = (tag == kMultiDraftTagOld); - const auto rich = (tag == kRichDraftsTag); + const auto withSuggest = (tag == kDraftsTag2); + const auto rich = (tag == kRichDraftsTag) || withSuggest; for (auto i = 0; i != count; ++i) { TextWithTags text; QByteArray textTagsSerialized; qint64 keyValue = 0; qint64 messageIdPeer = 0, messageIdMsg = 0; + quint64 suggestSerialized = 0; qint32 keyValueOld = 0; QString webpageUrl; qint32 webpageForceLargeMedia = 0; @@ -1558,7 +1570,11 @@ void Account::readDraftsWithCursors(not_null<History*> history) { >> text.text >> textTagsSerialized >> messageIdPeer - >> messageIdMsg + >> messageIdMsg; + if (withSuggest) { + draft.stream >> suggestSerialized; + } + draft.stream >> webpageUrl >> webpageForceLargeMedia >> webpageForceSmallMedia @@ -1581,6 +1597,13 @@ void Account::readDraftsWithCursors(not_null<History*> history) { MsgId(messageIdMsg)), .topicRootId = key.topicRootId(), }, + SuggestPostOptions{ + .exists = uint32(suggestSerialized >> 63), + .stars = uint32( + (suggestSerialized & ~(1ULL << 63)) >> 32), + .date = TimeId( + uint32(suggestSerialized & 0xFFFF'FFFFULL)), + }, MessageCursor(), Data::WebPageDraft{ .url = webpageUrl, @@ -1654,6 +1677,7 @@ void Account::readDraftsWithCursorsLegacy( std::make_unique<Data::Draft>( msgData, FullReplyTo{ FullMsgId(peerId, MsgId(msgReplyTo)) }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft{ .removed = (msgPreviewCancelled == 1), @@ -1665,6 +1689,7 @@ void Account::readDraftsWithCursorsLegacy( std::make_unique<Data::Draft>( editData, FullReplyTo{ FullMsgId(peerId, editMsgId) }, + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft{ .removed = (editPreviewCancelled == 1), diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index c096039941..4003e10de5 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -53,6 +53,7 @@ enum class StartResult : uchar; struct MessageDraft { FullReplyTo reply; + SuggestPostOptions suggest; TextWithTags textWithTags; Data::WebPageDraft webpage; }; diff --git a/Telegram/SourceFiles/support/support_helper.cpp b/Telegram/SourceFiles/support/support_helper.cpp index 64e9ff8152..1119d19e2c 100644 --- a/Telegram/SourceFiles/support/support_helper.cpp +++ b/Telegram/SourceFiles/support/support_helper.cpp @@ -166,6 +166,7 @@ Data::Draft OccupiedDraft(const QString &normalizedName) { + ";n:" + normalizedName }, FullReplyTo(), + SuggestPostOptions(), MessageCursor(), Data::WebPageDraft() }; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 58dc24f52a..dddc7a54c7 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -1177,6 +1177,7 @@ void Manager::notificationActivated( .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, + SuggestPostOptions(), MessageCursor{ length, length, diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 3913b1b3c9..6c4b35abf4 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -1224,11 +1224,13 @@ void Filler::addCreatePoll() { : SendMenu::Type::Scheduled; const auto flag = PollData::Flags(); const auto replyTo = _request.currentReplyTo; + const auto suggest = _request.currentSuggest; auto callback = [=] { PeerMenuCreatePoll( controller, peer, replyTo, + suggest, flag, flag, source, @@ -1263,11 +1265,13 @@ void Filler::addCreateTodoList() { ? SendMenu::Type::SilentOnly : SendMenu::Type::Scheduled; const auto replyTo = _request.currentReplyTo; + const auto suggest = _request.currentSuggest; auto callback = [=] { PeerMenuCreateTodoList( controller, peer, replyTo, + suggest, source, { sendMenuType }); }; @@ -1852,6 +1856,7 @@ void PeerMenuCreatePoll( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo, + SuggestPostOptions suggest, PollData::Flags chosen, PollData::Flags disabled, Api::SendType sendType, @@ -1902,6 +1907,7 @@ void PeerMenuCreatePoll( peer->owner().history(peer), result.options); action.replyTo = replyTo; + action.options.suggest = suggest; const auto local = action.history->localDraft( replyTo.topicRootId, replyTo.monoforumPeerId); @@ -1962,6 +1968,7 @@ void PeerMenuCreateTodoList( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo, + SuggestPostOptions suggest, Api::SendType sendType, SendMenu::Details sendMenuDetails) { if (!peer->session().premium()) { @@ -2008,6 +2015,7 @@ void PeerMenuCreateTodoList( peer->owner().history(peer), result.options); action.replyTo = replyTo; + action.options.suggest = suggest; const auto local = action.history->localDraft( replyTo.topicRootId, replyTo.monoforumPeerId); diff --git a/Telegram/SourceFiles/window/window_peer_menu.h b/Telegram/SourceFiles/window/window_peer_menu.h index 1b33297498..f01801423f 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.h +++ b/Telegram/SourceFiles/window/window_peer_menu.h @@ -107,6 +107,7 @@ void PeerMenuCreatePoll( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo = FullReplyTo(), + SuggestPostOptions suggest = SuggestPostOptions(), PollData::Flags chosen = PollData::Flags(), PollData::Flags disabled = PollData::Flags(), Api::SendType sendType = Api::SendType::Normal, @@ -121,6 +122,7 @@ void PeerMenuCreateTodoList( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, FullReplyTo replyTo = FullReplyTo(), + SuggestPostOptions suggest = SuggestPostOptions(), Api::SendType sendType = Api::SendType::Normal, SendMenu::Details sendMenuDetails = SendMenu::Details()); void PeerMenuEditTodoList( diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 736c4c2743..9ef02540bb 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -2114,9 +2114,13 @@ bool SessionController::switchInlineQuery( && to.currentReplyTo.quote.empty()) { to.currentReplyTo.messageId.msg = MsgId(); } + if (!history->suggestDraftAllowed()) { + to.currentSuggest = SuggestPostOptions(); + } auto draft = std::make_unique<Data::Draft>( textWithTags, to.currentReplyTo, + to.currentSuggest, cursor, Data::WebPageDraft()); From f6d1fe6c04e013d2f20a171b07f8328b640c129f Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 17 Jun 2025 16:08:16 +0400 Subject: [PATCH 186/310] Update API scheme on layer 206. --- Telegram/SourceFiles/data/data_channel.cpp | 2 ++ .../SourceFiles/data/data_chat_participant_status.cpp | 8 ++++++-- Telegram/SourceFiles/data/data_session.cpp | 7 +++++-- Telegram/SourceFiles/mtproto/scheme/api.tl | 4 ++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 2195f75654..e9a3843c31 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -1246,6 +1246,8 @@ void ApplyChannelUpdate( } channel->setMessagesTTL(update.vttl_period().value_or_empty()); + channel->setStarsPerMessage( + update.vsend_paid_messages_stars().value_or_empty()); using Flag = ChannelDataFlag; const auto mask = Flag::CanSetUsername | Flag::CanViewParticipants diff --git a/Telegram/SourceFiles/data/data_chat_participant_status.cpp b/Telegram/SourceFiles/data/data_chat_participant_status.cpp index f2ec641e04..265e5203e5 100644 --- a/Telegram/SourceFiles/data/data_chat_participant_status.cpp +++ b/Telegram/SourceFiles/data/data_chat_participant_status.cpp @@ -46,7 +46,9 @@ namespace { | (data.is_post_stories() ? Flag::PostStories : Flag()) | (data.is_edit_stories() ? Flag::EditStories : Flag()) | (data.is_delete_stories() ? Flag::DeleteStories : Flag()) - | (data.is_manage_direct() ? Flag::ManageDirect : Flag()); + | (data.is_manage_direct_messages() + ? Flag::ManageDirect + : Flag()); }); } @@ -108,7 +110,9 @@ MTPChatAdminRights AdminRightsToMTP(ChatAdminRightsInfo info) { | ((flags & R::PostStories) ? Flag::f_post_stories : Flag()) | ((flags & R::EditStories) ? Flag::f_edit_stories : Flag()) | ((flags & R::DeleteStories) ? Flag::f_delete_stories : Flag()) - | ((flags & R::ManageDirect) ? Flag::f_manage_direct : Flag()))); + | ((flags & R::ManageDirect) + ? Flag::f_manage_direct_messages + : Flag()))); } ChatRestrictionsInfo::ChatRestrictionsInfo(const MTPChatBannedRights &rights) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 66ab04bab2..31d3b117da 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1047,8 +1047,11 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) { } channel->setPhoto(data.vphoto()); - channel->setStarsPerMessage( - data.vsend_paid_messages_stars().value_or_empty()); + const auto hasStarsPerMessage + = data.vsend_paid_messages_stars().has_value(); + if (!hasStarsPerMessage) { + channel->setStarsPerMessage(0); + } if (const auto monoforum = data.vlinked_monoforum_id()) { if (const auto linked = channelLoaded(monoforum->v)) { diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 1d89f321b4..1b9bc0a06f 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -104,7 +104,7 @@ channel#fe685355 flags:# creator:flags.0?true left:flags.2?true broadcast:flags. channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#52d6806b 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 flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long 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:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long 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?long 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 ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; +channelFull#e07429de 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 flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true id:long 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:flags.23?ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?long 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?long 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 ttl_period:flags.24?int pending_suggestions:flags.25?Vector<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -1207,7 +1207,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 manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct:flags.17?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 other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?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 manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; From e4a4be1f538027a0c104c7e8599e123007ad9ef9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 18 Jun 2025 11:02:25 +0400 Subject: [PATCH 187/310] Don't show visibility status in opened-by-link gifts. --- Telegram/SourceFiles/settings/settings_credits_graphics.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index d14773c7d3..0914fb1ad7 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -1157,7 +1157,8 @@ void GenericCreditsEntryBox( && giftChannel->canTransferGifts(); const auto starGiftCanManage = isStarGift && !creditsHistoryStarGift - && (e.in || giftToChannelCanManage); + && (e.in || giftToChannelCanManage) + && !e.fromGiftSlug; const auto starGiftCanTransfer = isStarGift && !creditsHistoryStarGift && (e.in || giftToChannelCanTransfer); From cb987c1baf24122d7960c799a77ad05977d1b3d8 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 18 Jun 2025 11:02:40 +0400 Subject: [PATCH 188/310] PoC suggested accept/decline. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/api/api_sending.cpp | 1 + Telegram/SourceFiles/api/api_suggest_post.cpp | 199 ++++++++++++++++++ Telegram/SourceFiles/api/api_suggest_post.h | 21 ++ Telegram/SourceFiles/apiwrap.cpp | 3 + .../SourceFiles/boxes/delete_messages_box.cpp | 41 ++++ .../SourceFiles/boxes/delete_messages_box.h | 2 + Telegram/SourceFiles/data/data_drafts.cpp | 2 + Telegram/SourceFiles/data/data_drafts.h | 1 + Telegram/SourceFiles/data/data_types.h | 2 + Telegram/SourceFiles/history/history_item.cpp | 36 +++- Telegram/SourceFiles/history/history_item.h | 1 + .../history/history_item_components.h | 1 + .../history/history_item_edition.cpp | 5 +- .../history/history_item_edition.h | 2 + .../history/history_item_helpers.cpp | 3 + .../SourceFiles/history/history_widget.cpp | 41 ++-- Telegram/SourceFiles/history/history_widget.h | 1 + .../history/view/history_view_message.cpp | 43 +++- .../view/history_view_suggest_options.cpp | 24 ++- .../view/history_view_suggest_options.h | 4 +- 21 files changed, 390 insertions(+), 45 deletions(-) create mode 100644 Telegram/SourceFiles/api/api_suggest_post.cpp create mode 100644 Telegram/SourceFiles/api/api_suggest_post.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cc060d2a60..6cf845c5fd 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -176,6 +176,8 @@ PRIVATE api/api_statistics_data_deserialize.h api/api_statistics_sender.cpp api/api_statistics_sender.h + api/api_suggest_post.cpp + api/api_suggest_post.h api/api_text_entities.cpp api/api_text_entities.h api/api_todo_lists.cpp diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 1b2c0fc81f..cbfa6a937d 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -638,6 +638,7 @@ void SendConfirmedFile( edition.useSameMarkup = true; edition.useSameReplies = true; edition.useSameReactions = true; + edition.useSameSuggest = true; edition.savePreviousMedia = true; itemToEdit->applyEdition(std::move(edition)); } else { diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp new file mode 100644 index 0000000000..6ed775bbee --- /dev/null +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -0,0 +1,199 @@ +/* +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 "api/api_suggest_post.h" + +#include "apiwrap.h" +#include "base/unixtime.h" +#include "core/click_handler_types.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/boxes/choose_date_time.h" +#include "window/window_session_controller.h" + +namespace Api { +namespace { + +void SendApproval( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item, + TimeId scheduleDate = 0) { + using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag; + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion + || suggestion->accepted + || suggestion->rejected + || suggestion->requestId) { + return; + } + + const auto id = item->fullId(); + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + const auto finish = [=] { + if (const auto item = session->data().message(id)) { + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (suggestion) { + suggestion->requestId = 0; + } + } + }; + suggestion->requestId = session->api().request( + MTPmessages_ToggleSuggestedPostApproval( + MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag()), + item->history()->peer->input, + MTP_int(item->id.bare), + MTP_int(scheduleDate), + MTPstring()) // reject_comment + ).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + finish(); + }).fail([=](const MTP::Error &error) { + if (const auto window = weak.get()) { + window->showToast(error.type()); + } + finish(); + }).send(); +} + +void SendDecline( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item, + const QString &comment) { + using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag; + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion + || suggestion->accepted + || suggestion->rejected + || suggestion->requestId) { + return; + } + + const auto id = item->fullId(); + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + const auto finish = [=] { + if (const auto item = session->data().message(id)) { + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (suggestion) { + suggestion->requestId = 0; + } + } + }; + suggestion->requestId = session->api().request( + MTPmessages_ToggleSuggestedPostApproval( + MTP_flags(Flag::f_reject + | (comment.isEmpty() ? Flag() : Flag::f_reject_comment)), + item->history()->peer->input, + MTP_int(item->id.bare), + MTPint(), // schedule_date + MTP_string(comment)) + ).done([=](const MTPUpdates &result) { + session->api().applyUpdates(result); + finish(); + }).fail([=](const MTP::Error &error) { + if (const auto window = weak.get()) { + window->showToast(error.type()); + } + finish(); + }).send(); +} + +void RequestApprovalDate( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto done = [=](TimeId result) { + SendApproval(controller, item, result); + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + auto dateBox = Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ + .title = tr::lng_suggest_options_date(), + .submit = tr::lng_settings_save(), + .done = done, + .min = [] { return base::unixtime::now() + 1; }, + .time = (base::unixtime::now() + 86400), + }); + *weak = dateBox.data(); + controller->uiShow()->show(std::move(dateBox)); +} + +} // namespace + +std::shared_ptr<ClickHandler> AcceptClickHandler( + not_null<HistoryItem*> item) { + const auto session = &item->history()->session(); + const auto id = item->fullId(); + return std::make_shared<LambdaClickHandler>([=](ClickContext context) { + const auto my = context.other.value<ClickHandlerContext>(); + const auto controller = my.sessionWindow.get(); + if (!controller || &controller->session() != session) { + return; + } + const auto item = session->data().message(id); + if (!item) { + return; + } + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } else if (!suggestion->date) { + RequestApprovalDate(controller, item); + } else { + SendApproval(controller, item); + } + }); +} + +std::shared_ptr<ClickHandler> DeclineClickHandler( + not_null<HistoryItem*> item) { + const auto session = &item->history()->session(); + const auto id = item->fullId(); + return std::make_shared<LambdaClickHandler>([=](ClickContext context) { + const auto my = context.other.value<ClickHandlerContext>(); + const auto controller = my.sessionWindow.get(); + if (!controller || &controller->session() != session) { + return; + } + const auto item = session->data().message(id); + if (!item) { + return; + } + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } else { + SendDecline(controller, item, "sorry, bro.."); + } + }); +} + +std::shared_ptr<ClickHandler> SuggestChangesClickHandler( + not_null<HistoryItem*> item) { + const auto session = &item->history()->session(); + const auto id = item->fullId(); + return std::make_shared<LambdaClickHandler>([=](ClickContext context) { + const auto my = context.other.value<ClickHandlerContext>(); + const auto window = my.sessionWindow.get(); + if (!window || &window->session() != session) { + return; + } + const auto item = session->data().message(id); + if (!item) { + return; + } + + }); +} + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_suggest_post.h b/Telegram/SourceFiles/api/api_suggest_post.h new file mode 100644 index 0000000000..0584ce7be7 --- /dev/null +++ b/Telegram/SourceFiles/api/api_suggest_post.h @@ -0,0 +1,21 @@ +/* +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 + +class ClickHandler; + +namespace Api { + +[[nodiscard]] std::shared_ptr<ClickHandler> AcceptClickHandler( + not_null<HistoryItem*> item); +[[nodiscard]] std::shared_ptr<ClickHandler> DeclineClickHandler( + not_null<HistoryItem*> item); +[[nodiscard]] std::shared_ptr<ClickHandler> SuggestChangesClickHandler( + not_null<HistoryItem*> item); + +} // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index a1c6f380c9..7d74812110 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2154,6 +2154,9 @@ void ApiWrap::saveDraftsToCloud() { if (!textWithTags.tags.isEmpty()) { flags |= MTPmessages_SaveDraft::Flag::f_entities; } + if (cloudDraft->suggest) { + flags |= MTPmessages_SaveDraft::Flag::f_suggested_post; + } auto entities = Api::EntitiesToMTP( _session, TextUtilities::ConvertTextTagsToEntities(textWithTags.tags), diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index d880ab5860..cee747e797 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_ttl_validator.h" +#include "ui/boxes/confirm_box.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" @@ -32,6 +33,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" +namespace { + +constexpr auto kPaidShowLive = 86400; + +} // namespace + DeleteMessagesBox::DeleteMessagesBox( QWidget*, not_null<HistoryItem*> item, @@ -492,7 +499,41 @@ void DeleteMessagesBox::keyPressEvent(QKeyEvent *e) { } } +bool DeleteMessagesBox::hasPaidSuggestedPosts() const { + const auto now = base::unixtime::now(); + for (const auto &id : _ids) { + if (const auto item = _session->data().message(id)) { + if (item->isPaidSuggestedPost()) { + const auto date = item->date(); + if (now < date || now - date <= kPaidShowLive) { + return true; + } + } + } + } + return false; +} + void DeleteMessagesBox::deleteAndClear() { + if (hasPaidSuggestedPosts() && !_confirmedDeletePaidSuggestedPosts) { + const auto weak = Ui::MakeWeak(this); + const auto callback = [=](Fn<void()> close) { + close(); + if (const auto strong = weak.data()) { + strong->_confirmedDeletePaidSuggestedPosts = true; + strong->deleteAndClear(); + } + }; + AssertIsDebug(); + uiShow()->show(Ui::MakeConfirmBox({ + .text = u"You won't receive Stars for this post if you delete it now. The post must remain visible for at least 24 hours after it was published."_q, + .confirmed = callback, + .confirmText = u"Delete Anyway"_q, + .confirmStyle = &st::attentionBoxButton, + .title = u"Stars will be lost"_q, + })); + return; + } if (_revoke && _revokeRemember && _revokeRemember->toggled() diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.h b/Telegram/SourceFiles/boxes/delete_messages_box.h index 9987fd8b32..ce5d430c8b 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.h +++ b/Telegram/SourceFiles/boxes/delete_messages_box.h @@ -58,6 +58,7 @@ private: [[nodiscard]] bool hasScheduledMessages() const; [[nodiscard]] std::optional<RevokeConfig> revokeText( not_null<PeerData*> peer) const; + [[nodiscard]] bool hasPaidSuggestedPosts() const; const not_null<Main::Session*> _session; @@ -82,6 +83,7 @@ private: object_ptr<Ui::LinkButton> _autoDeleteSettings = { nullptr }; int _fullHeight = 0; + bool _confirmedDeletePaidSuggestedPosts = false; Fn<void()> _deleteConfirmedCallback; diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index 95fc3c4f9c..5a61b486e8 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -56,6 +56,7 @@ Draft::Draft( mtpRequestId saveRequestId) : textWithTags(textWithTags) , reply(std::move(reply)) +, suggest(suggest) , cursor(cursor) , webpage(webpage) , saveRequestId(saveRequestId) { @@ -69,6 +70,7 @@ Draft::Draft( mtpRequestId saveRequestId) : textWithTags(field->getTextWithTags()) , reply(std::move(reply)) +, suggest(suggest) , cursor(field) , webpage(webpage) { } diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 5af11ee198..683188eadb 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -257,6 +257,7 @@ using HistoryDrafts = base::flat_map<DraftKey, std::unique_ptr<Draft>>; } return (a->textWithTags == b->textWithTags) && (a->reply == b->reply) + && (a->suggest == b->suggest) && (a->webpage == b->webpage); } diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index c8f373a4f6..46da299a9b 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -353,6 +353,8 @@ enum class MessageFlag : uint64 { ReactionsAllowed = (1ULL << 50), HideDisplayDate = (1ULL << 51), + + PaidSuggestedPost = (1ULL << 52), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags<MessageFlag>; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 9ce8ed0b79..bcef78a0e6 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1593,6 +1593,10 @@ bool HistoryItem::isEditingMedia() const { return Has<HistoryMessageSavedMediaData>(); } +bool HistoryItem::isPaidSuggestedPost() const { + return _flags & MessageFlag::PaidSuggestedPost; +} + void HistoryItem::clearSavedMedia() { RemoveComponents(HistoryMessageSavedMediaData::Bit()); } @@ -1887,6 +1891,21 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { } } + if (!edition.useSameSuggest) { + if (edition.suggest.exists) { + if (!Has<HistoryMessageSuggestedPost>()) { + AddComponents(HistoryMessageSuggestedPost::Bit()); + } + auto suggest = Get<HistoryMessageSuggestedPost>(); + suggest->stars = edition.suggest.stars; + suggest->date = edition.suggest.date; + suggest->accepted = edition.suggest.accepted; + suggest->rejected = edition.suggest.rejected; + } else { + RemoveComponents(HistoryMessageSuggestedPost::Bit()); + } + } + applyTTL(edition.ttl); setFactcheck(FromMTP(this, edition.mtpFactcheck)); @@ -2398,7 +2417,8 @@ bool HistoryItem::allowsSendNow() const { && isScheduled() && !isSending() && !hasFailed() - && !isEditingMedia(); + && !isEditingMedia() + && !isPaidSuggestedPost(); } bool HistoryItem::allowsReschedule() const { @@ -2425,7 +2445,8 @@ bool HistoryItem::allowsEdit(TimeId now) const { && !isTooOldForEdit(now) && (!_media || _media->allowsEdit()) && !isLegacyMessage() - && !isEditingMedia(); + && !isEditingMedia() + && !isPaidSuggestedPost(); } bool HistoryItem::allowsEditMedia() const { @@ -5928,8 +5949,15 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return prepareTodoAppendTasksText(); }; - auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &) { - return PreparedServiceText{ { "process_suggested" } }; AssertIsDebug(); + auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &data) { + if (data.is_balance_too_low()) { + return PreparedServiceText{ { u"balance too low :( need %1 stars"_q.arg(data.vstars_amount().value_or_empty()) } }; + } else if (data.is_rejected()) { + return PreparedServiceText{ { u"rejected :( comment: %1"_q.arg(qs(data.vreject_comment().value_or_empty())) } }; + } else if (const auto date = data.vschedule_date().value_or_empty()) { + return PreparedServiceText{ { u"approved!! for date: %1"_q.arg(langDateTime(base::unixtime::parse(date))) } }; + } + return PreparedServiceText{ { "approved!!" } }; AssertIsDebug(); }; auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 64e6880da6..71901f8e0c 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -312,6 +312,7 @@ public: [[nodiscard]] bool hasRealFromId() const; [[nodiscard]] bool isPostHidingAuthor() const; [[nodiscard]] bool isPostShowingAuthor() const; + [[nodiscard]] bool isPaidSuggestedPost() const; [[nodiscard]] bool isRegular() const; [[nodiscard]] bool isUploading() const; void sendFailed(); diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 7e6c627d27..f701aecfb0 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -618,6 +618,7 @@ struct HistoryMessageSuggestedPost : RuntimeComponent<HistoryMessageSuggestedPost, HistoryItem> { int stars = 0; TimeId date = 0; + mtpRequestId requestId = 0; bool accepted = false; bool rejected = false; }; diff --git a/Telegram/SourceFiles/history/history_item_edition.cpp b/Telegram/SourceFiles/history/history_item_edition.cpp index 1fb3482394..968a429c76 100644 --- a/Telegram/SourceFiles/history/history_item_edition.cpp +++ b/Telegram/SourceFiles/history/history_item_edition.cpp @@ -11,8 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" HistoryMessageEdition::HistoryMessageEdition( - not_null<Main::Session*> session, - const MTPDmessage &message) { + not_null<Main::Session*> session, + const MTPDmessage &message) +: suggest(HistoryMessageSuggestInfo(message.vsuggested_post())) { isEditHide = message.is_edit_hide(); isMediaUnread = message.is_media_unread(); editDate = message.vedit_date().value_or(-1); diff --git a/Telegram/SourceFiles/history/history_item_edition.h b/Telegram/SourceFiles/history/history_item_edition.h index c22ce713c8..8d0cd03aa2 100644 --- a/Telegram/SourceFiles/history/history_item_edition.h +++ b/Telegram/SourceFiles/history/history_item_edition.h @@ -30,11 +30,13 @@ struct HistoryMessageEdition { bool useSameReplies = false; bool useSameMarkup = false; bool useSameReactions = false; + bool useSameSuggest = false; bool savePreviousMedia = false; bool invertMedia = false; TextWithEntities textWithEntities; HistoryMessageMarkupData replyMarkup; HistoryMessageRepliesData replies; + HistoryMessageSuggestInfo suggest; const MTPMessageMedia *mtpMedia = nullptr; const MTPMessageReactions *mtpReactions = nullptr; const MTPFactCheck *mtpFactcheck = nullptr; diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 9b791852c9..3297854a55 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -762,6 +762,9 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag()) | ((flags & MTP::f_video_processing_pending) ? Flag::EstimatedDate + : Flag()) + | ((flags & MTP::f_paid_suggested_post) + ? Flag::PaidSuggestedPost : Flag()); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4b968b884a..29114bb086 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -3172,9 +3172,17 @@ void HistoryWidget::applySuggestOptions(SuggestPostOptions suggest) { controller(), _peer, suggest); - _suggestOptions->repaints() | rpl::start_with_next([=] { + _suggestOptions->updates() | rpl::start_with_next([=] { updateField(); + saveDraftWithTextNow(); }, _suggestOptions->lifetime()); + saveDraftWithTextNow(); +} + +void HistoryWidget::saveDraftWithTextNow() { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); } void HistoryWidget::refreshSuggestPostToggle() { @@ -4644,9 +4652,7 @@ void HistoryWidget::send(Api::SendOptions options) { if (_preview) { _preview->apply({ .removed = true }); } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); hideSelectorControlsAnimated(); @@ -7680,9 +7686,7 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { result.messageSendingFrom.localId); clearFieldText(); - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); auto &bots = cRefRecentInlineBots(); const auto index = bots.indexOf(result.bot); @@ -8296,9 +8300,7 @@ bool HistoryWidget::sendExistingDocument( if (_autocomplete && _autocomplete->stickersShown()) { clearFieldText(); - //_saveDraftText = true; - //_saveDraftStart = crl::now(); - //saveDraft(); + //saveDraftWithTextNow(); // won't be needed if SendInlineBotResult will clear the cloud draft saveCloudDraft(); @@ -8634,10 +8636,7 @@ void HistoryWidget::setReplyFieldsFromProcessing() { refreshTopBarActiveChat(); } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); - + saveDraftWithTextNow(); setInnerFocus(); } @@ -8702,10 +8701,7 @@ void HistoryWidget::editMessage( updateField(); SelectTextInFieldWithMargins(_field, selection); - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); - + saveDraftWithTextNow(); setInnerFocus(); } @@ -8794,9 +8790,7 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { } } if (wasReply) { - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } if (!_editMsgId && _keyboard->singleUse() @@ -8841,9 +8835,7 @@ void HistoryWidget::cancelEdit() { _saveEditMsgRequestId = 0; } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); mouseMoveEvent(nullptr); if (!readyToForward() @@ -8891,6 +8883,7 @@ bool HistoryWidget::cancelSuggestPost() { _suggestOptions = nullptr; updateControlsVisibility(); updateControlsGeometry(); + saveDraftWithTextNow(); return true; } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index f18577abe2..e225f5bf41 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -391,6 +391,7 @@ private: void saveDraft(bool delayed = false); void saveCloudDraft(); void saveDraftDelayed(); + void saveDraftWithTextNow(); void showMembersDropdown(); void windowIsVisibleChanged(); void saveFieldToHistoryLocalDraft(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index fa9ed5edd2..8ced796f8f 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_message.h" +#include "api/api_suggest_post.h" #include "base/unixtime.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "core/ui_integration.h" @@ -24,11 +25,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/share_box.h" #include "ui/effects/glare.h" #include "ui/effects/reaction_fly_animation.h" -#include "ui/rect.h" -#include "ui/round_rect.h" #include "ui/text/text_utilities.h" #include "ui/text/text_extended_data.h" #include "ui/power_saving.h" +#include "ui/rect.h" +#include "ui/round_rect.h" #include "data/components/factchecks.h" #include "data/components/sponsored_messages.h" #include "data/data_session.h" @@ -456,17 +457,45 @@ void Message::initPaidInformation() { const auto item = data(); if (!item->history()->peer->isUser()) { - if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { + auto text = PreparedServiceText(); if (!suggest->stars && !suggest->date) { - setServicePreMessage({ { u"suggestion to publish for free anytime"_q } }); + text = { { u"suggestion to publish for free anytime"_q } }; } else if (!suggest->date) { - setServicePreMessage({ { u"suggestion to publish for %1 stars anytime"_q.arg(suggest->stars) }}); + text = { { u"suggestion to publish for %1 stars anytime"_q.arg(suggest->stars) } }; } else if (!suggest->stars) { - setServicePreMessage({ { u"suggestion to publish for free %1"_q.arg(langDateTime(base::unixtime::parse(suggest->date))) }}); + text = { { u"suggestion to publish for free %1"_q.arg(langDateTime(base::unixtime::parse(suggest->date))) } }; } else { - setServicePreMessage({ { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } }); + text = { { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } }; } + const auto channelIsAuthor = item->from()->isChannel(); + const auto amMonoforumAdmin = item->history()->peer->amMonoforumAdmin(); + const auto broadcast = item->history()->peer->monoforumBroadcast(); + const auto canDecline = item->isRegular() + && !(suggest->accepted || suggest->rejected) + && (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin); + const auto canAccept = canDecline + && (channelIsAuthor + ? !amMonoforumAdmin + : (amMonoforumAdmin + && broadcast + && broadcast->canPostMessages())); + if (canDecline) { + text.links.push_back(Api::DeclineClickHandler(item)); + text.text.append(", ").append(Ui::Text::Link("[Decline]", text.links.size())); + if (canAccept) { + text.links.push_back(Api::AcceptClickHandler(item)); + text.text.append(", ").append(Ui::Text::Link("[Accept]", text.links.size())); + + text.links.push_back(Api::SuggestChangesClickHandler(item)); + text.text.append(", ").append(Ui::Text::Link("[SuggestChanges]", text.links.size())); + } + } else if (suggest->accepted) { + text.text.append(", accepted!"); + } else if (suggest->rejected) { + text.text.append(", rejected :("); + } + setServicePreMessage(std::move(text)); } return; diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp index da6566ce0f..8ba648a063 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -94,15 +94,27 @@ void EditOptionsBox( st::settingsButtonNoIcon); time->setClickedCallback([=] { - box->uiShow()->show(Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto parentWeak = Ui::MakeWeak(box); + const auto done = [=](TimeId result) { + if (parentWeak) { + state->date = result; + } + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + auto dateBox = Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ .title = tr::lng_suggest_options_date(), .submit = tr::lng_settings_save(), - .done = [=](TimeId result) { state->date = result; }, + .done = done, .min = [] { return base::unixtime::now() + 1; }, .time = (state->date.current() ? state->date.current() : (base::unixtime::now() + 86400)), - })); + }); + *weak = dateBox.data(); + box->uiShow()->show(std::move(dateBox)); }); Ui::AddSkip(container); @@ -172,7 +184,7 @@ void SuggestOptions::edit() { const auto apply = [=](SuggestPostOptions values) { _values = values; updateTexts(); - _repaints.fire({}); + _updates.fire({}); }; const auto broadcast = _peer->monoforumBroadcast(); const auto &appConfig = _peer->session().appConfig(); @@ -226,8 +238,8 @@ SuggestPostOptions SuggestOptions::values() const { return result; } -rpl::producer<> SuggestOptions::repaints() const { - return _repaints.events(); +rpl::producer<> SuggestOptions::updates() const { + return _updates.events(); } rpl::lifetime &SuggestOptions::lifetime() { diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/history_view_suggest_options.h index 26d11c1c70..8c5ef1ae41 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.h @@ -28,7 +28,7 @@ public: [[nodiscard]] SuggestPostOptions values() const; - [[nodiscard]] rpl::producer<> repaints() const; + [[nodiscard]] rpl::producer<> updates() const; [[nodiscard]] rpl::lifetime &lifetime(); @@ -44,7 +44,7 @@ private: Ui::Text::String _text; SuggestPostOptions _values; - rpl::event_stream<> _repaints; + rpl::event_stream<> _updates; rpl::lifetime _lifetime; From bf9492e083012670353c519ea5a6c9ca4e59422f Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 19 Jun 2025 20:53:15 +0400 Subject: [PATCH 189/310] Show approve/decline service messages. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 35 +++- Telegram/SourceFiles/api/api_bot.cpp | 22 ++ Telegram/SourceFiles/api/api_suggest_post.cpp | 63 ++++-- Telegram/SourceFiles/history/history_item.cpp | 91 +++++++-- Telegram/SourceFiles/history/history_item.h | 17 +- .../history/history_item_components.cpp | 89 ++++++++ .../history/history_item_components.h | 17 ++ .../history/history_item_reply_markup.h | 7 + .../history/view/history_view_element.cpp | 12 ++ .../history/view/history_view_message.cpp | 27 --- .../view/history_view_suggest_options.cpp | 4 +- .../view/media/history_view_media_generic.cpp | 30 ++- .../view/media/history_view_media_generic.h | 4 +- .../media/history_view_suggest_decision.cpp | 192 ++++++++++++++++++ .../media/history_view_suggest_decision.h | 25 +++ Telegram/SourceFiles/ui/chat/chat.style | 6 + 17 files changed, 576 insertions(+), 67 deletions(-) create mode 100644 Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp create mode 100644 Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 6cf845c5fd..3800886afa 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -818,6 +818,8 @@ PRIVATE history/view/media/history_view_sticker_player_abstract.h history/view/media/history_view_story_mention.cpp history/view/media/history_view_story_mention.h + history/view/media/history_view_suggest_decision.cpp + history/view/media/history_view_suggest_decision.h history/view/media/history_view_theme_document.cpp history/view/media/history_view_theme_document.h history/view/media/history_view_todo_list.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 207aecbdae..d36e7f77ed 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4421,12 +4421,45 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_bar_priced_dated" = "{amount} {date}"; "lng_suggest_bar_dated" = "Publish on {date}"; "lng_suggest_options_title" = "Suggest a Message"; +"lng_suggest_options_change" = "Suggest Changes"; "lng_suggest_options_price" = "Enter Price in Stars"; -"lng_suggest_options_price_about" = "Choose how many Stars you want to offer {channel} to publish this message."; +"lng_suggest_options_price_about" = "Choose how many Stars to pay to publish this message."; "lng_suggest_options_date" = "Time"; "lng_suggest_options_date_any" = "Anytime"; "lng_suggest_options_date_about" = "Select the date and time you want the message to be published."; "lng_suggest_options_offer" = "Offer {amount}"; +"lng_suggest_options_update" = "Update Terms"; + +"lng_suggest_action_decline" = "Decline"; +"lng_suggest_action_accept" = "Accept"; +"lng_suggest_action_change" = "Suggest Changes"; +"lng_suggest_action_your" = "You suggest to post this message."; +"lng_suggest_action_his" = "{from} suggests to post this message."; +"lng_suggest_action_price_label" = "Price"; +"lng_suggest_action_time_label" = "Time"; +"lng_suggest_action_agreement" = "Agreement reached!"; +"lng_suggest_action_agree_date" = "The post will be automatically published on {channel} {date}."; +"lng_suggest_action_your_charged" = "You have been charged {amount}."; +"lng_suggest_action_his_charged" = "{from} have been charged {amount}."; +"lng_suggest_action_agree_receive" = "{channel} will receive the Stars once the post has been live for 24 hours."; +"lng_suggest_action_agree_removed" = "If {channel} removes the post before it has been live for 24 hours, the Stars will be refunded."; +"lng_suggest_action_your_not_enough" = "**Transaction failed** because you didn't have enough Stars."; +"lng_suggest_action_his_not_enough" = "**Transaction failed** because the user didn't have enough Stars."; +"lng_suggest_action_declined" = "{from} rejected the message."; +"lng_suggest_action_declined_reason" = "{from} rejected the message with the comment."; +"lng_suggest_change_price" = "{from} suggests a new price for the message."; +"lng_suggest_change_time" = "{from} suggests a new time for the message."; +"lng_suggest_change_price_time" = "{from} suggests a new price and time for the message."; +"lng_suggest_change_content" = "{from} suggests changes for the message."; +"lng_suggest_change_price_label" = "New Price"; +"lng_suggest_change_time_label" = "New Time"; +"lng_suggest_change_text_label" = "Check the suggested message below"; +"lng_suggest_menu_edit_message" = "Edit Message"; +"lng_suggest_menu_edit_price" = "Edit Price"; +"lng_suggest_menu_edit_time" = "Edit Time"; +"lng_suggest_decline_title" = "Decline"; +"lng_suggest_decline_text" = "Do you want to decline publishing this post from {from}?"; +"lng_suggest_decline_reason" = "Add a reason (optional)"; "lng_reply_in_another_title" = "Reply in..."; "lng_reply_in_another_chat" = "Reply in Another Chat"; diff --git a/Telegram/SourceFiles/api/api_bot.cpp b/Telegram/SourceFiles/api/api_bot.cpp index 87111914ef..b86ce1a70c 100644 --- a/Telegram/SourceFiles/api/api_bot.cpp +++ b/Telegram/SourceFiles/api/api_bot.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_cloud_password.h" #include "api/api_send_progress.h" +#include "api/api_suggest_post.h" #include "boxes/share_box.h" #include "boxes/passcode_box.h" #include "boxes/url_auth_box.h" @@ -521,6 +522,27 @@ void ActivateBotCommand(ClickHandlerContext context, int row, int column) { controller->showToast(tr::lng_text_copied(tr::now)); } } break; + + case ButtonType::SuggestAccept: { + Api::AcceptClickHandler(item)->onClick(ClickContext{ + Qt::LeftButton, + QVariant::fromValue(context), + }); + } break; + + case ButtonType::SuggestDecline: { + Api::DeclineClickHandler(item)->onClick(ClickContext{ + Qt::LeftButton, + QVariant::fromValue(context), + }); + } break; + + case ButtonType::SuggestChange: { + Api::SuggestChangesClickHandler(item)->onClick(ClickContext{ + Qt::LeftButton, + QVariant::fromValue(context), + }); + } break; } } diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 6ed775bbee..0a8f4ffa36 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -17,7 +17,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/boxes/choose_date_time.h" +#include "ui/layers/generic_box.h" +#include "ui/boxes/confirm_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/fields/input_field.h" #include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_layers.h" namespace Api { namespace { @@ -128,6 +134,49 @@ void RequestApprovalDate( controller->uiShow()->show(std::move(dateBox)); } +void RequestDeclineComment( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto id = item->fullId(); + controller->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) { + const auto callback = std::make_shared<Fn<void()>>(); + Ui::ConfirmBox(box, { + .text = tr::lng_suggest_decline_text( + lt_from, + rpl::single(Ui::Text::Bold(item->from()->shortName())), + Ui::Text::WithEntities), + .confirmed = [=](Fn<void()> close) { (*callback)(); close(); }, + .confirmText = tr::lng_suggest_action_decline(), + .confirmStyle = &st::attentionBoxButton, + .title = tr::lng_suggest_decline_title(), + }); + const auto reason = box->addRow(object_ptr<Ui::InputField>( + box, + st::factcheckField, + Ui::InputField::Mode::NoNewlines, + tr::lng_suggest_decline_reason())); + box->setFocusCallback([=] { + reason->setFocusFast(); + }); + *callback = [=, weak = Ui::MakeWeak(box)] { + const auto item = controller->session().data().message(id); + if (!item) { + return; + } + SendDecline(controller, item, reason->getLastText().trimmed()); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + reason->submits( + ) | rpl::start_with_next([=](Qt::KeyboardModifiers modifiers) { + if (!(modifiers & Qt::ShiftModifier)) { + (*callback)(); + } + }, box->lifetime()); + })); +} + } // namespace std::shared_ptr<ClickHandler> AcceptClickHandler( @@ -157,24 +206,14 @@ std::shared_ptr<ClickHandler> AcceptClickHandler( std::shared_ptr<ClickHandler> DeclineClickHandler( not_null<HistoryItem*> item) { - const auto session = &item->history()->session(); const auto id = item->fullId(); return std::make_shared<LambdaClickHandler>([=](ClickContext context) { const auto my = context.other.value<ClickHandlerContext>(); const auto controller = my.sessionWindow.get(); - if (!controller || &controller->session() != session) { + if (!controller) { return; } - const auto item = session->data().message(id); - if (!item) { - return; - } - const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); - if (!suggestion) { - return; - } else { - SendDecline(controller, item, "sorry, bro.."); - } + RequestDeclineComment(controller, item); }); } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index bcef78a0e6..eb4e1fa1d3 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -828,6 +828,8 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { return done; } else if (const auto append = Get<HistoryServiceTodoAppendTasks>()) { return append; + } else if (const auto decision = Get<HistoryServiceSuggestDecision>()) { + return decision; } return nullptr; } @@ -1068,8 +1070,58 @@ bool HistoryItem::checkDiscussionLink(ChannelId id) const { return false; } -void HistoryItem::setReplyMarkup(HistoryMessageMarkupData &&markup) { +SuggestionActions HistoryItem::computeSuggestionActions() const { + return computeSuggestionActions(Get<HistoryMessageSuggestedPost>()); +} + +SuggestionActions HistoryItem::computeSuggestionActions( + const HistoryMessageSuggestedPost *suggest) const { + return suggest + ? computeSuggestionActions(suggest->accepted, suggest->rejected) + : SuggestionActions::None; +} + +SuggestionActions HistoryItem::computeSuggestionActions( + bool accepted, + bool rejected) const { + const auto channelIsAuthor = from()->isChannel(); + const auto amMonoforumAdmin = history()->peer->amMonoforumAdmin(); + const auto broadcast = history()->peer->monoforumBroadcast(); + const auto canDecline = isRegular() + && !(accepted || rejected) + && (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin); + const auto canAccept = canDecline + && (channelIsAuthor + ? !amMonoforumAdmin + : (amMonoforumAdmin + && broadcast + && broadcast->canPostMessages())); + return canAccept + ? SuggestionActions::AcceptAndDecline + : canDecline + ? SuggestionActions::Decline + : SuggestionActions::None; +} + +void HistoryItem::updateSuggestControls( + const HistoryMessageSuggestedPost *suggest) { + if (const auto markup = Get<HistoryMessageReplyMarkup>()) { + markup->updateSuggestControls(computeSuggestionActions(suggest)); + } +} + +void HistoryItem::setReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons) { const auto requestUpdate = [&] { + const auto actions = computeSuggestionActions(); + if (actions != SuggestionActions::None + && !Has<HistoryMessageReplyMarkup>()) { + AddComponents(HistoryMessageReplyMarkup::Bit()); + } + if (const auto markup = Get<HistoryMessageReplyMarkup>()) { + markup->updateSuggestControls(actions); + } history()->owner().requestItemResize(this); history()->session().changes().messageUpdated( this, @@ -1901,8 +1953,10 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { suggest->date = edition.suggest.date; suggest->accepted = edition.suggest.accepted; suggest->rejected = edition.suggest.rejected; + updateSuggestControls(suggest); } else { RemoveComponents(HistoryMessageSuggestedPost::Bit()); + updateSuggestControls(nullptr); } } @@ -1942,7 +1996,7 @@ void HistoryItem::applyEdition(const MTPDmessageService &message) { const auto wasSublist = savedSublist(); if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); - setReplyMarkup({}); + setReplyMarkup({}, true); removeFromSharedMediaIndex(); refreshMedia(nullptr); setTextValue({}); @@ -2144,8 +2198,10 @@ void HistoryItem::applyEditionToHistoryCleared() { ).c_messageService()); } -void HistoryItem::updateReplyMarkup(HistoryMessageMarkupData &&markup) { - setReplyMarkup(std::move(markup)); +void HistoryItem::updateReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons) { + setReplyMarkup(std::move(markup), ignoreSuggestButtons); } void HistoryItem::contributeToSlowmode(TimeId realDate) { @@ -3865,6 +3921,12 @@ void HistoryItem::createComponents(CreateConfig &&config) { } if (config.suggest.exists) { mask |= HistoryMessageSuggestedPost::Bit(); + if (computeSuggestionActions( + config.suggest.accepted, + config.suggest.rejected + ) != SuggestionActions::None) { + mask |= HistoryMessageReplyMarkup::Bit(); + } } UpdateComponents(mask); @@ -3965,6 +4027,7 @@ void HistoryItem::createComponents(CreateConfig &&config) { suggest->date = config.suggest.date; suggest->accepted = config.suggest.accepted; suggest->rejected = config.suggest.rejected; + updateSuggestControls(suggest); } if (out() && isSending()) { @@ -4601,6 +4664,15 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { ) | ranges::views::transform([&](const MTPTodoItem &item) { return TodoListItemFromMTP(session, item); }) | ranges::to_vector; + } else if (type == mtpc_messageActionSuggestedPostApproval) { + const auto &data = action.c_messageActionSuggestedPostApproval(); + UpdateComponents(HistoryServiceSuggestDecision::Bit()); + const auto decision = Get<HistoryServiceSuggestDecision>(); + decision->stars = data.vstars_amount().value_or_empty(); + decision->balanceTooLow = data.is_balance_too_low(); + decision->rejected = data.is_rejected(); + decision->rejectComment = qs(data.vreject_comment().value_or_empty()); + decision->date = data.vschedule_date().value_or_empty(); } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -4629,7 +4701,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { : PeerId(); const auto requiresMonoforumPeer = _history->peer->amMonoforumAdmin(); if (savedSublistPeer || requiresMonoforumPeer) { - UpdateComponents(HistoryMessageSaved::Bit()); + AddComponents(HistoryMessageSaved::Bit()); const auto saved = Get<HistoryMessageSaved>(); saved->sublistPeerId = savedSublistPeer ? savedSublistPeer @@ -5950,14 +6022,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { }; auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &data) { - if (data.is_balance_too_low()) { - return PreparedServiceText{ { u"balance too low :( need %1 stars"_q.arg(data.vstars_amount().value_or_empty()) } }; - } else if (data.is_rejected()) { - return PreparedServiceText{ { u"rejected :( comment: %1"_q.arg(qs(data.vreject_comment().value_or_empty())) } }; - } else if (const auto date = data.vschedule_date().value_or_empty()) { - return PreparedServiceText{ { u"approved!! for date: %1"_q.arg(langDateTime(base::unixtime::parse(date))) } }; - } - return PreparedServiceText{ { "approved!!" } }; AssertIsDebug(); + return PreparedServiceText{ { u"hello"_q } }; }; auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 71901f8e0c..a7551d89d8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -22,6 +22,7 @@ struct HistoryMessageMarkupData; struct HistoryMessageReplyMarkup; struct HistoryMessageTranslation; struct HistoryMessageForwarded; +struct HistoryMessageSuggestedPost; struct HistoryServiceDependentData; struct HistoryServiceTodoCompletions; enum class HistorySelfDestructType; @@ -29,6 +30,7 @@ struct PreparedServiceText; struct MessageFactcheck; class ReplyKeyboard; struct LanguageId; +enum class SuggestionActions : uchar; namespace base { template <typename Enum> @@ -353,7 +355,9 @@ public: void overrideMedia(std::unique_ptr<Data::Media> media); void applyEditionToHistoryCleared(); - void updateReplyMarkup(HistoryMessageMarkupData &&markup); + void updateReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons = false); void contributeToSlowmode(TimeId realDate = 0); void clearMediaAsExpired(); @@ -575,7 +579,16 @@ private: [[nodiscard]] bool checkDiscussionLink(ChannelId id) const; - void setReplyMarkup(HistoryMessageMarkupData &&markup); + void setReplyMarkup( + HistoryMessageMarkupData &&markup, + bool ignoreSuggestButtons = false); + [[nodiscard]] SuggestionActions computeSuggestionActions() const; + [[nodiscard]] SuggestionActions computeSuggestionActions( + const HistoryMessageSuggestedPost *suggest) const; + [[nodiscard]] SuggestionActions computeSuggestionActions( + bool accepted, + bool rejected) const; + void updateSuggestControls(const HistoryMessageSuggestedPost *suggest); void changeReplyToTopCounter( not_null<HistoryMessageReply*> reply, diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index f4381d5756..25503abc47 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -1214,6 +1214,95 @@ bool HistoryMessageReplyMarkup::hiddenBy(Data::Media *media) const { return false; } +void HistoryMessageReplyMarkup::updateSuggestControls( + SuggestionActions actions) { + if (actions == SuggestionActions::AcceptAndDecline) { + data.flags |= ReplyMarkupFlag::SuggestionAccept; + } else { + data.flags &= ~ReplyMarkupFlag::SuggestionAccept; + } + if (actions == SuggestionActions::None) { + data.flags &= ~ReplyMarkupFlag::SuggestionDecline; + } else { + data.flags |= ReplyMarkupFlag::Inline + | ReplyMarkupFlag::SuggestionDecline; + } + using Type = HistoryMessageMarkupButton::Type; + const auto has = [&](Type type) { + return !data.rows.empty() + && ranges::contains( + data.rows.back(), + type, + &HistoryMessageMarkupButton::type); + }; + if (actions == SuggestionActions::AcceptAndDecline) { + // ... rows ... + // [decline] | [accept] + // [suggestchanges] + if (has(Type::SuggestChange)) { + // Nothing changed. + } else { + if (has(Type::SuggestDecline)) { + data.rows.pop_back(); + } + data.rows.push_back({ + { + Type::SuggestDecline, + tr::lng_suggest_action_decline(tr::now), + }, + { + Type::SuggestAccept, + tr::lng_suggest_action_accept(tr::now), + }, + }); + data.rows.push_back({ { + Type::SuggestChange, + tr::lng_suggest_action_change(tr::now), + } }); + data.flags |= ReplyMarkupFlag::SuggestionAccept + | ReplyMarkupFlag::SuggestionDecline; + } + if (data.rows.size() > 2) { + data.flags |= ReplyMarkupFlag::SuggestionSeparator; + } else { + data.flags &= ~ReplyMarkupFlag::SuggestionSeparator; + } + } else { + while (!data.rows.empty()) { + if (has(Type::SuggestChange) || has(Type::SuggestAccept)) { + data.rows.pop_back(); + } else if (has(Type::SuggestDecline) + && actions == SuggestionActions::None) { + data.rows.pop_back(); + } else { + break; + } + } + data.flags &= ~ReplyMarkupFlag::SuggestionAccept; + if (actions == SuggestionActions::None) { + data.flags &= ReplyMarkupFlag::SuggestionDecline; + data.flags &= ~ReplyMarkupFlag::SuggestionSeparator; + } else { + if (!has(Type::SuggestDecline)) { + // ... rows ... + // [decline] + data.rows.push_back({ { + Type::SuggestDecline, + tr::lng_suggest_action_decline(tr::now), + } }); + data.flags |= ReplyMarkupFlag::SuggestionDecline; + } + if (data.rows.size() > 1) { + data.flags |= ReplyMarkupFlag::SuggestionSeparator; + } else { + data.flags &= ~ReplyMarkupFlag::SuggestionSeparator; + } + } + } + + inlineKeyboard = nullptr; +} + HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() = default; HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal( diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index f701aecfb0..92714a2e2d 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -58,6 +58,12 @@ struct BotKeyboardButton; extern const char kOptionFastButtonsMode[]; [[nodiscard]] bool FastButtonsMode(); +enum class SuggestionActions : uchar { + None, + Decline, + AcceptAndDecline, +}; + struct HistoryMessageVia : RuntimeComponent<HistoryMessageVia, HistoryItem> { void create(not_null<Data::Session*> owner, UserId userId); void resize(int32 availw) const; @@ -383,6 +389,7 @@ struct HistoryMessageReplyMarkup void createForwarded(const HistoryMessageReplyMarkup &original); void updateData(HistoryMessageMarkupData &&markup); + void updateSuggestControls(SuggestionActions actions); [[nodiscard]] bool hiddenBy(Data::Media *media) const; @@ -691,6 +698,16 @@ struct HistoryServiceTodoAppendTasks [[nodiscard]] TextWithEntities ComposeTodoTasksList( not_null<HistoryServiceTodoAppendTasks*> append); +struct HistoryServiceSuggestDecision +: RuntimeComponent<HistoryServiceSuggestDecision, HistoryItem> +, HistoryServiceDependentData { + int stars = 0; + TimeId date = 0; + QString rejectComment; + bool rejected = false; + bool balanceTooLow = false; +}; + struct HistoryServiceGameScore : RuntimeComponent<HistoryServiceGameScore, HistoryItem> , HistoryServiceDependentData { diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index 8a735903fb..be9084211e 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -37,6 +37,9 @@ enum class ReplyMarkupFlag : uint32 { IsNull = (1U << 7), OnlyBuyButton = (1U << 8), Persistent = (1U << 9), + SuggestionDecline = (1U << 10), + SuggestionAccept = (1U << 11), + SuggestionSeparator = (1U << 12), }; inline constexpr bool is_flag_type(ReplyMarkupFlag) { return true; } using ReplyMarkupFlags = base::flags<ReplyMarkupFlag>; @@ -85,6 +88,10 @@ struct HistoryMessageMarkupButton { WebView, SimpleWebView, CopyText, + + SuggestDecline, + SuggestAccept, + SuggestChange, }; HistoryMessageMarkupButton( diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 08a2373df5..251eb7d823 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -9,11 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_service_message.h" #include "history/view/history_view_message.h" +#include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_similar_channels.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" #include "history/view/media/history_view_custom_emoji.h" +#include "history/view/media/history_view_suggest_decision.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/reactions/history_view_reactions.h" #include "history/view/history_view_cursor_state.h" @@ -1072,6 +1074,16 @@ void Element::refreshMedia(Element *replacing) { this, std::make_unique<LargeEmoji>(this, emoji)); } + } else if (const auto decision = item->Get<HistoryServiceSuggestDecision>()) { + _media = std::make_unique<MediaGeneric>( + this, + GenerateSuggestDecisionMedia(this, decision), + MediaGenericDescriptor{ + .maxWidth = st::chatSuggestInfoWidth, + .serviceLink = decision->lnk, + .service = true, + .hideServiceText = true, + }); } else { _media = nullptr; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 8ced796f8f..8c097f9472 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -468,33 +468,6 @@ void Message::initPaidInformation() { } else { text = { { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } }; } - const auto channelIsAuthor = item->from()->isChannel(); - const auto amMonoforumAdmin = item->history()->peer->amMonoforumAdmin(); - const auto broadcast = item->history()->peer->monoforumBroadcast(); - const auto canDecline = item->isRegular() - && !(suggest->accepted || suggest->rejected) - && (channelIsAuthor ? !amMonoforumAdmin : amMonoforumAdmin); - const auto canAccept = canDecline - && (channelIsAuthor - ? !amMonoforumAdmin - : (amMonoforumAdmin - && broadcast - && broadcast->canPostMessages())); - if (canDecline) { - text.links.push_back(Api::DeclineClickHandler(item)); - text.text.append(", ").append(Ui::Text::Link("[Decline]", text.links.size())); - if (canAccept) { - text.links.push_back(Api::AcceptClickHandler(item)); - text.text.append(", ").append(Ui::Text::Link("[Accept]", text.links.size())); - - text.links.push_back(Api::SuggestChangesClickHandler(item)); - text.text.append(", ").append(Ui::Text::Link("[SuggestChanges]", text.links.size())); - } - } else if (suggest->accepted) { - text.text.append(", accepted!"); - } else if (suggest->rejected) { - text.text.append(", rejected :("); - } setServicePreMessage(std::move(text)); } diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp index 8ba648a063..1996ba2d42 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -78,9 +78,7 @@ void EditOptionsBox( Ui::AddSkip(container); Ui::AddDividerText( container, - tr::lng_suggest_options_price_about( - lt_channel, - rpl::single(args.channelName))); + tr::lng_suggest_options_price_about()); Ui::AddSkip(container); const auto time = Settings::AddButtonWithLabel( diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index bfac91b07d..7961f5c696 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -236,9 +236,11 @@ MediaGenericTextPart::MediaGenericTextPart( QMargins margins, const style::TextStyle &st, const base::flat_map<uint16, ClickHandlerPtr> &links, - const Ui::Text::MarkedContext &context) + const Ui::Text::MarkedContext &context, + style::align align) : _text(st::msgMinWidth) -, _margins(margins) { +, _margins(margins) +, _align(align) { _text.setMarkedText( st, text, @@ -254,12 +256,18 @@ void MediaGenericTextPart::draw( not_null<const MediaGeneric*> owner, const PaintContext &context, int outerWidth) const { + const auto use = (width() - _margins.left() - _margins.right()); setupPen(p, owner, context); _text.draw(p, { - .position = { (outerWidth - width()) / 2, _margins.top() }, + .position = { + ((_align == style::al_top) + ? ((outerWidth - use) / 2) + : _margins.left()), + _margins.top(), + }, .outerWidth = outerWidth, - .availableWidth = width(), - .align = style::al_top, + .availableWidth = use, + .align = _align, .palette = &(owner->service() ? context.st->serviceTextPalette() : context.messageStyle()->textPalette), @@ -284,11 +292,17 @@ TextState MediaGenericTextPart::textState( QPoint point, StateRequest request, int outerWidth) const { - point -= QPoint{ (outerWidth - width()) / 2, _margins.top() }; + const auto use = (width() - _margins.left() - _margins.right()); + point -= QPoint{ + ((_align == style::al_top) + ? ((outerWidth - use) / 2) + : _margins.left()), + _margins.top(), + }; auto result = TextState(); auto forText = request.forText(); - forText.align = style::al_top; - result.link = _text.getState(point, width(), forText).link; + forText.align = _align; + result.link = _text.getState(point, use, forText).link; return result; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index 0fd011914f..b5133743b3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -141,7 +141,8 @@ public: QMargins margins, const style::TextStyle &st = st::defaultTextStyle, const base::flat_map<uint16, ClickHandlerPtr> &links = {}, - const Ui::Text::MarkedContext &context = {}); + const Ui::Text::MarkedContext &context = {}, + style::align align = style::al_top); void draw( Painter &p, @@ -165,6 +166,7 @@ protected: private: Ui::Text::String _text; QMargins _margins; + style::align _align = {}; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp new file mode 100644 index 0000000000..d242eb0a89 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -0,0 +1,192 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "history/view/media/history_view_suggest_decision.h" + +#include "base/unixtime.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "history/view/media/history_view_media_generic.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "lang/lang_keys.h" +#include "ui/text/text_utilities.h" +#include "ui/text/format_values.h" +#include "styles/style_chat.h" +#include "styles/style_credits.h" + +namespace HistoryView { +namespace { + +enum EmojiType { + kAgreement, + kCalendar, + kMoney, + kHourglass, + kReload, + kDecline, + kDiscard, + kWarning, +}; + +[[nodiscard]] const char *Raw(EmojiType type) { + switch (type) { + case EmojiType::kAgreement: return "\xf0\x9f\xa4\x9d"; + case EmojiType::kCalendar: return "\xf0\x9f\x93\x86"; + case EmojiType::kMoney: return "\xf0\x9f\x92\xb0"; + case EmojiType::kHourglass: return "\xe2\x8c\x9b\xef\xb8\x8f"; + case EmojiType::kReload: return "\xf0\x9f\x94\x84"; + case EmojiType::kDecline: return "\xe2\x9d\x8c"; + case EmojiType::kDiscard: return "\xf0\x9f\x9a\xab"; + case EmojiType::kWarning: return "\xe2\x9a\xa0\xef\xb8\x8f"; + } + Unexpected("EmojiType in Raw."); +} + +[[nodiscard]] QString Emoji(EmojiType type) { + return QString::fromUtf8(Raw(type)); +} + +} // namespace + +auto GenerateSuggestDecisionMedia( + not_null<Element*> parent, + not_null<const HistoryServiceSuggestDecision*> decision) + -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)> { + return [=]( + not_null<MediaGeneric*> media, + Fn<void(std::unique_ptr<MediaGenericPart>)> push) { + const auto peer = parent->history()->peer; + const auto broadcast = peer->monoforumBroadcast(); + if (!broadcast) { + return; + } + + const auto sublistPeerId = parent->data()->sublistPeerId(); + const auto sublistPeer = peer->owner().peer(sublistPeerId); + + auto pushText = [&]( + TextWithEntities text, + QMargins margins = {}, + style::align align = style::al_left, + const base::flat_map<uint16, ClickHandlerPtr> &links = {}) { + push(std::make_unique<MediaGenericTextPart>( + std::move(text), + margins, + st::defaultTextStyle, + links, + Ui::Text::MarkedContext(), + align)); + }; + + if (decision->balanceTooLow) { + pushText( + TextWithEntities( + ).append(Emoji(kWarning)).append(' ').append( + (sublistPeer->isSelf() + ? tr::lng_suggest_action_your_not_enough + : tr::lng_suggest_action_his_not_enough)( + tr::now, + Ui::Text::RichLangValue)), + st::chatSuggestInfoFullMargin, + style::al_top); + } else if (decision->rejected) { + const auto withComment = !decision->rejectComment.isEmpty(); + pushText( + TextWithEntities( + ).append(Emoji(kDecline)).append(' ').append( + (withComment + ? tr::lng_suggest_action_declined_reason + : tr::lng_suggest_action_declined)( + tr::now, + lt_from, + Ui::Text::Bold(broadcast->name()), + Ui::Text::WithEntities)), + (withComment + ? st::chatSuggestInfoTitleMargin + : st::chatSuggestInfoFullMargin)); + if (withComment) { + pushText( + TextWithEntities().append('"').append( + decision->rejectComment + ).append('"'), + st::chatSuggestInfoLastMargin, + style::al_top); + } + } else { + const auto stars = decision->stars; + pushText( + TextWithEntities( + ).append(Emoji(kAgreement)).append(' ').append( + Ui::Text::Bold(tr::lng_suggest_action_agreement(tr::now)) + ), + st::chatSuggestInfoTitleMargin, + style::al_top); + pushText( + TextWithEntities( + ).append(Emoji(kCalendar)).append(' ').append( + tr::lng_suggest_action_agree_date( + tr::now, + lt_channel, + Ui::Text::Bold(broadcast->name()), + lt_date, + Ui::Text::Bold(Ui::FormatDateTime( + base::unixtime::parse(decision->date))), + Ui::Text::WithEntities)), + (stars + ? st::chatSuggestInfoMiddleMargin + : st::chatSuggestInfoLastMargin)); + if (stars) { + const auto amount = Ui::Text::Bold( + tr::lng_prize_credits_amount(tr::now, lt_count, stars)); + pushText( + TextWithEntities( + ).append(Emoji(kMoney)).append(' ').append( + (sublistPeer->isSelf() + ? tr::lng_suggest_action_your_charged( + tr::now, + lt_amount, + amount, + Ui::Text::WithEntities) + : tr::lng_suggest_action_his_charged( + tr::now, + lt_from, + Ui::Text::Bold(sublistPeer->shortName()), + lt_amount, + amount, + Ui::Text::WithEntities))), + st::chatSuggestInfoMiddleMargin); + + pushText( + TextWithEntities( + ).append(Emoji(kHourglass)).append(' ').append( + tr::lng_suggest_action_agree_receive( + tr::now, + lt_channel, + Ui::Text::Bold(broadcast->name()), + Ui::Text::WithEntities)), + st::chatSuggestInfoMiddleMargin); + + pushText( + TextWithEntities( + ).append(Emoji(kReload)).append(' ').append( + tr::lng_suggest_action_agree_removed( + tr::now, + lt_channel, + Ui::Text::Bold(broadcast->name()), + Ui::Text::WithEntities)), + st::chatSuggestInfoLastMargin); + } + } + }; +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h new file mode 100644 index 0000000000..6315d50b9f --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h @@ -0,0 +1,25 @@ +/* +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 + +struct HistoryServiceSuggestDecision; + +namespace HistoryView { + +class Element; +class MediaGeneric; +class MediaGenericPart; + +auto GenerateSuggestDecisionMedia( + not_null<Element*> parent, + not_null<const HistoryServiceSuggestDecision*> decision +) -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)>; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 3e3c2bbaf8..53791b79b2 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1052,6 +1052,12 @@ chatSimilarName: TextStyle(defaultTextStyle) { chatSimilarWidthMax: 424px; chatSimilarSkip: 12px; +chatSuggestInfoWidth: 272px; +chatSuggestInfoTitleMargin: margins(16px, 16px, 16px, 6px); +chatSuggestInfoMiddleMargin: margins(16px, 4px, 16px, 4px); +chatSuggestInfoLastMargin: margins(16px, 4px, 16px, 16px); +chatSuggestInfoFullMargin: margins(16px, 16px, 16px, 16px); + premiumRequiredWidth: 186px; premiumRequiredIcon: icon{{ "chat/large_lockedchat", msgServiceFg }}; premiumRequiredCircle: 60px; From ebce4d0f31d2cfaae32cf23e50a200ebecb8215e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 19 Jun 2025 23:19:22 +0400 Subject: [PATCH 190/310] Show suggested service info. --- Telegram/Resources/langs/lang.strings | 2 + .../history/view/history_view_element.cpp | 124 +++++++++++------- .../history/view/history_view_element.h | 9 +- .../history/view/history_view_message.cpp | 21 ++- .../view/media/history_view_media_generic.cpp | 9 ++ .../view/media/history_view_media_generic.h | 2 + .../media/history_view_suggest_decision.cpp | 97 +++++++++++++- .../media/history_view_suggest_decision.h | 8 ++ Telegram/SourceFiles/ui/chat/chat.style | 1 + 9 files changed, 209 insertions(+), 64 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d36e7f77ed..da2e1a761f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4436,7 +4436,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_action_your" = "You suggest to post this message."; "lng_suggest_action_his" = "{from} suggests to post this message."; "lng_suggest_action_price_label" = "Price"; +"lng_suggest_action_price_free" = "Free"; "lng_suggest_action_time_label" = "Time"; +"lng_suggest_action_time_any" = "Anytime"; "lng_suggest_action_agreement" = "Agreement reached!"; "lng_suggest_action_agree_date" = "The post will be automatically published on {channel} {date}."; "lng_suggest_action_your_charged" = "You have been charged {amount}."; diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 251eb7d823..89949f8636 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -602,7 +602,8 @@ void MonoforumSenderBar::Paint( void ServicePreMessage::init( PreparedServiceText string, - ClickHandlerPtr fullClickHandler) { + ClickHandlerPtr fullClickHandler, + std::unique_ptr<Media> media) { text = Ui::Text::String( st::serviceTextStyle, string.text, @@ -612,6 +613,7 @@ void ServicePreMessage::init( for (auto i = 0; i != int(string.links.size()); ++i) { text.setLink(i + 1, string.links[i]); } + this->media = std::move(media); } int ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) { @@ -621,27 +623,38 @@ int ServicePreMessage::resizeToWidth(int newWidth, ElementChatMode mode) { width, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } - auto contentWidth = width; - contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.right(); - if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { - contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + + if (media) { + media->initDimensions(); + media->resizeGetHeight(width); } - auto maxWidth = text.maxWidth() - + st::msgServicePadding.left() - + st::msgServicePadding.right(); - auto minHeight = text.minHeight(); + if (media && media->hideServiceText()) { + height = media->height() + st::msgServiceMargin.bottom(); + } else { + auto contentWidth = width; + contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.right(); + if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { + contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + } + + auto maxWidth = text.maxWidth() + + st::msgServicePadding.left() + + st::msgServicePadding.right(); + auto minHeight = text.minHeight(); + + auto nwidth = qMax(contentWidth + - st::msgServicePadding.left() + - st::msgServicePadding.right(), 0); + height = (contentWidth >= maxWidth) + ? minHeight + : text.countHeight(nwidth); + height += st::msgServicePadding.top() + + st::msgServicePadding.bottom() + + st::msgServiceMargin.top() + + st::msgServiceMargin.bottom(); + } - auto nwidth = qMax(contentWidth - - st::msgServicePadding.left() - - st::msgServicePadding.right(), 0); - height = (contentWidth >= maxWidth) - ? minHeight - : text.countHeight(nwidth); - height += st::msgServicePadding.top() - + st::msgServicePadding.bottom() - + st::msgServiceMargin.top() - + st::msgServiceMargin.bottom(); return height; } @@ -650,41 +663,53 @@ void ServicePreMessage::paint( const PaintContext &context, QRect g, ElementChatMode mode) const { - const auto top = g.top() - height - st::msgMargin.top(); - p.translate(0, top); + if (media && media->hideServiceText()) { + const auto left = (width - media->width()) / 2; + const auto top = g.top() - height - st::msgMargin.bottom(); + const auto position = QPoint(left, top); + p.translate(position); + media->draw(p, context.translated(-position).withSelection({})); + p.translate(-position); + } else { + const auto top = g.top() - height - st::msgMargin.top(); + p.translate(0, top); - const auto rect = QRect(0, 0, width, height) - - st::msgServiceMargin; - const auto trect = rect - st::msgServicePadding; + const auto rect = QRect(0, 0, width, height) + - st::msgServiceMargin; + const auto trect = rect - st::msgServicePadding; - ServiceMessagePainter::PaintComplexBubble( - p, - context.st, - rect.left(), - rect.width(), - text, - trect); + ServiceMessagePainter::PaintComplexBubble( + p, + context.st, + rect.left(), + rect.width(), + text, + trect); - p.setBrush(Qt::NoBrush); - p.setPen(context.st->msgServiceFg()); - p.setFont(st::msgServiceFont); - text.draw(p, { - .position = trect.topLeft(), - .availableWidth = trect.width(), - .align = style::al_top, - .palette = &context.st->serviceTextPalette(), - .now = context.now, - .fullWidthSelection = false, - //.selection = context.selection, - }); + p.setBrush(Qt::NoBrush); + p.setPen(context.st->msgServiceFg()); + p.setFont(st::msgServiceFont); + text.draw(p, { + .position = trect.topLeft(), + .availableWidth = trect.width(), + .align = style::al_top, + .palette = &context.st->serviceTextPalette(), + .now = context.now, + .fullWidthSelection = false, + //.selection = context.selection, + }); - p.translate(0, -top); + p.translate(0, -top); + } } ClickHandlerPtr ServicePreMessage::textState( QPoint point, const StateRequest &request, QRect g) const { + if (media && media->hideServiceText()) { + return {}; + } const auto top = g.top() - height - st::msgMargin.top(); const auto rect = QRect(0, top, width, height) - st::msgServiceMargin; @@ -1082,6 +1107,7 @@ void Element::refreshMedia(Element *replacing) { .maxWidth = st::chatSuggestInfoWidth, .serviceLink = decision->lnk, .service = true, + .fullAreaLink = true, .hideServiceText = true, }); } else { @@ -1639,11 +1665,15 @@ void Element::setDisplayDate(bool displayDate) { void Element::setServicePreMessage( PreparedServiceText text, - ClickHandlerPtr fullClickHandler) { - if (!text.text.empty()) { + ClickHandlerPtr fullClickHandler, + std::unique_ptr<Media> media) { + if (!text.text.empty() || media) { AddComponents(ServicePreMessage::Bit()); const auto service = Get<ServicePreMessage>(); - service->init(std::move(text), std::move(fullClickHandler)); + service->init( + std::move(text), + std::move(fullClickHandler), + std::move(media)); setPendingResize(); } else if (Has<ServicePreMessage>()) { RemoveComponents(ServicePreMessage::Bit()); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index a27dcae59d..1bf9a8e1a0 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -309,7 +309,10 @@ private: // Any HistoryView::Element can have this Component for // displaying some text in layout of a service message above the message. struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> { - void init(PreparedServiceText string, ClickHandlerPtr fullClickHandler); + void init( + PreparedServiceText string, + ClickHandlerPtr fullClickHandler, + std::unique_ptr<Media> media = nullptr); int resizeToWidth(int newWidth, ElementChatMode mode); @@ -323,6 +326,7 @@ struct ServicePreMessage : RuntimeComponent<ServicePreMessage, Element> { const StateRequest &request, QRect g) const; + std::unique_ptr<Media> media; Ui::Text::String text; ClickHandlerPtr handler; int width = 0; @@ -459,7 +463,8 @@ public: void setDisplayDate(bool displayDate); void setServicePreMessage( PreparedServiceText text, - ClickHandlerPtr fullClickHandler = nullptr); + ClickHandlerPtr fullClickHandler = nullptr, + std::unique_ptr<Media> media = nullptr); bool computeIsAttachToPrevious(not_null<Element*> previous); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 8c097f9472..74d02d17ce 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -14,7 +14,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_cursor_state.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" +#include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_web_page.h" +#include "history/view/media/history_view_suggest_decision.h" #include "history/view/reactions/history_view_reactions.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_group_call_bar.h" // UserpicInRow. @@ -458,17 +460,14 @@ void Message::initPaidInformation() { if (!item->history()->peer->isUser()) { if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { - auto text = PreparedServiceText(); - if (!suggest->stars && !suggest->date) { - text = { { u"suggestion to publish for free anytime"_q } }; - } else if (!suggest->date) { - text = { { u"suggestion to publish for %1 stars anytime"_q.arg(suggest->stars) } }; - } else if (!suggest->stars) { - text = { { u"suggestion to publish for free %1"_q.arg(langDateTime(base::unixtime::parse(suggest->date))) } }; - } else { - text = { { u"suggestion to publish for %1 stars %2"_q.arg(suggest->stars).arg(langDateTime(base::unixtime::parse(suggest->date))) } }; - } - setServicePreMessage(std::move(text)); + setServicePreMessage({}, {}, std::make_unique<MediaGeneric>( + this, + GenerateSuggestRequestMedia(this, suggest), + MediaGenericDescriptor{ + .maxWidth = st::chatSuggestWidth, + .service = true, + .hideServiceText = true, + })); } return; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index 7961f5c696..f0991f8f95 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -79,6 +79,7 @@ MediaGeneric::MediaGeneric( , _paintBg(std::move(descriptor.paintBg)) , _maxWidthCap(descriptor.maxWidth) , _service(descriptor.service) +, _fullAreaLink(descriptor.fullAreaLink) , _hideServiceText(descriptor.hideServiceText) { generate(this, [&](std::unique_ptr<Part> part) { _entries.push_back({ @@ -157,6 +158,14 @@ TextState MediaGeneric::textState( return result; } + if (_fullAreaLink && QRect(0, 0, width(), height()).contains(point)) { + const auto link = _parent->data()->Get<HistoryServiceCustomLink>(); + if (link) { + result.link = link->link; + return result; + } + } + for (const auto &entry : _entries) { const auto raw = entry.object.get(); const auto height = raw->height(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index b5133743b3..766d1b18c9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -59,6 +59,7 @@ struct MediaGenericDescriptor { not_null<const MediaGeneric*>)> paintBg; ClickHandlerPtr serviceLink; bool service = false; + bool fullAreaLink = false; bool hideServiceText = false; }; @@ -130,6 +131,7 @@ private: not_null<const MediaGeneric*>)> _paintBg; int _maxWidthCap = 0; bool _service : 1 = false; + bool _fullAreaLink : 1 = false; bool _hideServiceText : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp index d242eb0a89..b3237a4755 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -11,11 +11,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_session.h" #include "history/view/media/history_view_media_generic.h" +#include "history/view/media/history_view_unique_gift.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "lang/lang_keys.h" +#include "ui/chat/chat_style.h" #include "ui/text/text_utilities.h" #include "ui/text/format_values.h" #include "styles/style_chat.h" @@ -24,6 +26,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace HistoryView { namespace { +constexpr auto kFadedOpacity = 0.85; + enum EmojiType { kAgreement, kCalendar, @@ -114,12 +118,17 @@ auto GenerateSuggestDecisionMedia( ? st::chatSuggestInfoTitleMargin : st::chatSuggestInfoFullMargin)); if (withComment) { - pushText( + const auto fadedFg = [](const PaintContext &context) { + auto result = context.st->msgServiceFg()->c; + result.setAlphaF(result.alphaF() * kFadedOpacity); + return result; + }; + push(std::make_unique<TextPartColored>( TextWithEntities().append('"').append( decision->rejectComment ).append('"'), st::chatSuggestInfoLastMargin, - style::al_top); + fadedFg)); } } else { const auto stars = decision->stars; @@ -130,6 +139,7 @@ auto GenerateSuggestDecisionMedia( ), st::chatSuggestInfoTitleMargin, style::al_top); + const auto date = base::unixtime::parse(decision->date); pushText( TextWithEntities( ).append(Emoji(kCalendar)).append(' ').append( @@ -138,8 +148,16 @@ auto GenerateSuggestDecisionMedia( lt_channel, Ui::Text::Bold(broadcast->name()), lt_date, - Ui::Text::Bold(Ui::FormatDateTime( - base::unixtime::parse(decision->date))), + Ui::Text::Bold(tr::lng_mediaview_date_time( + tr::now, + lt_date, + QLocale().toString( + date.date(), + QLocale::ShortFormat), + lt_time, + QLocale().toString( + date.time(), + QLocale::ShortFormat))), Ui::Text::WithEntities)), (stars ? st::chatSuggestInfoMiddleMargin @@ -189,4 +207,75 @@ auto GenerateSuggestDecisionMedia( }; } +auto GenerateSuggestRequestMedia( + not_null<Element*> parent, + not_null<const HistoryMessageSuggestedPost*> suggest) + -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)> { + return [=]( + not_null<MediaGeneric*> media, + Fn<void(std::unique_ptr<MediaGenericPart>)> push) { + const auto normalFg = [](const PaintContext &context) { + return context.st->msgServiceFg()->c; + }; + const auto fadedFg = [](const PaintContext &context) { + auto result = context.st->msgServiceFg()->c; + result.setAlphaF(result.alphaF() * kFadedOpacity); + return result; + }; + const auto from = parent->data()->from(); + const auto peer = parent->history()->peer; + + auto pushText = [&]( + TextWithEntities text, + QMargins margins = {}, + style::align align = style::al_left, + const base::flat_map<uint16, ClickHandlerPtr> &links = {}) { + push(std::make_unique<MediaGenericTextPart>( + std::move(text), + margins, + st::defaultTextStyle, + links, + Ui::Text::MarkedContext(), + align)); + }; + + pushText( + (from->isSelf() + ? tr::lng_suggest_action_your( + tr::now, + Ui::Text::WithEntities) + : tr::lng_suggest_action_his( + tr::now, + lt_from, + Ui::Text::Bold(from->shortName()), + Ui::Text::WithEntities)), + st::chatSuggestInfoTitleMargin, + style::al_top); + + auto entries = std::vector<AttributeTable::Entry>(); + entries.push_back({ + tr::lng_suggest_action_price_label(tr::now), + Ui::Text::Bold(suggest->stars + ? tr::lng_prize_credits_amount( + tr::now, + lt_count, + suggest->stars) + : tr::lng_suggest_action_price_free(tr::now)), + }); + entries.push_back({ + tr::lng_suggest_action_time_label(tr::now), + Ui::Text::Bold(suggest->date + ? Ui::FormatDateTime(base::unixtime::parse(suggest->date)) + : tr::lng_suggest_action_time_any(tr::now)), + }); + push(std::make_unique<AttributeTable>( + std::move(entries), + st::chatSuggestInfoLastMargin, + fadedFg, + normalFg)); + }; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h index 6315d50b9f..56fc4f58f1 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +struct HistoryMessageSuggestedPost; struct HistoryServiceSuggestDecision; namespace HistoryView { @@ -22,4 +23,11 @@ auto GenerateSuggestDecisionMedia( not_null<MediaGeneric*>, Fn<void(std::unique_ptr<MediaGenericPart>)>)>; +auto GenerateSuggestRequestMedia( + not_null<Element*> parent, + not_null<const HistoryMessageSuggestedPost*> suggest +) -> Fn<void( + not_null<MediaGeneric*>, + Fn<void(std::unique_ptr<MediaGenericPart>)>)>; + } // namespace HistoryView diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 53791b79b2..34bb08479d 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1052,6 +1052,7 @@ chatSimilarName: TextStyle(defaultTextStyle) { chatSimilarWidthMax: 424px; chatSimilarSkip: 12px; +chatSuggestWidth: 216px; chatSuggestInfoWidth: 272px; chatSuggestInfoTitleMargin: margins(16px, 16px, 16px, 6px); chatSuggestInfoMiddleMargin: margins(16px, 4px, 16px, 4px); From ec28eea7f093bd41a9455d18c457ea7c2d2f067a Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 20 Jun 2025 10:17:59 +0400 Subject: [PATCH 191/310] Make keyboard fully shown with media. --- Telegram/SourceFiles/history/view/history_view_message.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 74d02d17ce..823d56dddb 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3869,6 +3869,9 @@ int Message::minWidthForMedia() const { accumulate_max(result, added + st::semiboldFont->width( tr::lng_replies_view_original(tr::now))); } + if (const auto keyboard = data()->inlineReplyKeyboard()) { + accumulate_max(result, keyboard->naturalWidth()); + } return result; } From e29dcf7489b7faf9623bf1bb5e3b8a9ac4d5f469 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 20 Jun 2025 13:13:17 +0400 Subject: [PATCH 192/310] Update API scheme on layer 206. Re-Suggest. --- Telegram/SourceFiles/api/api_suggest_post.cpp | 163 +++++++++++++++++- Telegram/SourceFiles/apiwrap.cpp | 12 +- Telegram/SourceFiles/boxes/share_box.cpp | 6 +- .../SourceFiles/history/history_widget.cpp | 19 +- Telegram/SourceFiles/history/history_widget.h | 1 + .../history/view/history_view_element.cpp | 4 +- .../view/history_view_suggest_options.cpp | 76 ++++---- .../view/history_view_suggest_options.h | 29 ++++ .../view/media/history_view_media_generic.cpp | 7 +- Telegram/SourceFiles/main/main_app_config.cpp | 8 + Telegram/SourceFiles/main/main_app_config.h | 2 + Telegram/SourceFiles/mtproto/scheme/api.tl | 2 +- 12 files changed, 278 insertions(+), 51 deletions(-) diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 0a8f4ffa36..8734cd8986 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -11,19 +11,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "core/click_handler_types.h" #include "data/data_session.h" +#include "history/view/history_view_suggest_options.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" +#include "history/history_item_helpers.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "mainwindow.h" #include "ui/boxes/choose_date_time.h" #include "ui/layers/generic_box.h" #include "ui/boxes/confirm_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" +#include "ui/widgets/popup_menu.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_layers.h" +#include "styles/style_menu_icons.h" namespace Api { namespace { @@ -116,19 +121,22 @@ void SendDecline( void RequestApprovalDate( not_null<Window::SessionController*> controller, not_null<HistoryItem*> item) { + const auto id = item->fullId(); const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); const auto done = [=](TimeId result) { - SendApproval(controller, item, result); + if (const auto item = controller->session().data().message(id)) { + SendApproval(controller, item, result); + } if (const auto strong = weak->data()) { strong->closeBox(); } }; - auto dateBox = Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ + using namespace HistoryView; + auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ + .session = &controller->session(), .title = tr::lng_suggest_options_date(), .submit = tr::lng_settings_save(), .done = done, - .min = [] { return base::unixtime::now() + 1; }, - .time = (base::unixtime::now() + 86400), }); *weak = dateBox.data(); controller->uiShow()->show(std::move(dateBox)); @@ -177,6 +185,137 @@ void RequestDeclineComment( })); } +struct SendSuggestState { + SendPaymentHelper sendPayment; +}; +void SendSuggest( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item, + std::shared_ptr<SendSuggestState> state, + Fn<void(SuggestPostOptions&)> modify, + Fn<void()> done = nullptr, + int starsApproved = 0) { + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } + const auto id = item->fullId(); + const auto withPaymentApproved = [=](int stars) { + if (const auto item = controller->session().data().message(id)) { + SendSuggest(controller, item, state, modify, done, stars); + } + }; + const auto checked = state->sendPayment.check( + controller->uiShow(), + item->history()->peer, + 1, + starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + const auto isForward = item->Get<HistoryMessageForwarded>(); + auto action = SendAction(item->history()); + action.options.suggest.exists = 1; + action.options.suggest.date = suggestion->date; + action.options.suggest.stars = suggestion->stars; + action.options.starsApproved = starsApproved; + action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId(); + action.replyTo.messageId = item->fullId(); + modify(action.options.suggest); + controller->session().api().forwardMessages({ + .items = { item }, + .options = (isForward + ? Data::ForwardOptions::PreserveInfo + : Data::ForwardOptions::NoSenderNames), + }, action); + if (const auto onstack = done) { + onstack(); + } +} + +void SuggestApprovalDate( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } + const auto id = item->fullId(); + const auto state = std::make_shared<SendSuggestState>(); + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto done = [=](TimeId result) { + const auto item = controller->session().data().message(id); + if (!item) { + return; + } + const auto close = [=] { + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + SendSuggest( + controller, + item, + state, + [=](SuggestPostOptions &options) { options.date = result; }, + close); + }; + using namespace HistoryView; + auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ + .session = &controller->session(), + .title = tr::lng_suggest_menu_edit_time(), + .submit = tr::lng_profile_suggest_button(), + .done = done, + .value = suggestion->date, + }); + *weak = dateBox.data(); + controller->uiShow()->show(std::move(dateBox)); +} + +void SuggestApprovalPrice( + not_null<Window::SessionController*> controller, + not_null<HistoryItem*> item) { + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } + const auto id = item->fullId(); + const auto state = std::make_shared<SendSuggestState>(); + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); + const auto done = [=](SuggestPostOptions result) { + const auto item = controller->session().data().message(id); + if (!item) { + return; + } + const auto close = [=] { + if (const auto strong = weak->data()) { + strong->closeBox(); + } + }; + SendSuggest( + controller, + item, + state, + [=](SuggestPostOptions &options) { options = result; }, + close); + }; + using namespace HistoryView; + auto dateBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ + .session = &controller->session(), + .done = done, + .value = { + .exists = true, + .stars = uint32(suggestion->stars), + .date = suggestion->date, + }, + }); + *weak = dateBox.data(); + controller->uiShow()->show(std::move(dateBox)); +} + } // namespace std::shared_ptr<ClickHandler> AcceptClickHandler( @@ -231,7 +370,23 @@ std::shared_ptr<ClickHandler> SuggestChangesClickHandler( if (!item) { return; } + const auto menu = Ui::CreateChild<Ui::PopupMenu>( + window->widget(), + st::popupMenuWithIcons); + menu->addAction(tr::lng_suggest_menu_edit_message(tr::now), [=] { + }, &st::menuIconEdit); + menu->addAction(tr::lng_suggest_menu_edit_price(tr::now), [=] { + if (const auto item = session->data().message(id)) { + SuggestApprovalPrice(window, item); + } + }, &st::menuIconTagSell); + menu->addAction(tr::lng_suggest_menu_edit_time(tr::now), [=] { + if (const auto item = session->data().message(id)) { + SuggestApprovalDate(window, item); + } + }, &st::menuIconSchedule); + menu->popup(QCursor::pos()); }); } diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 7d74812110..d4c72646c3 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3410,6 +3410,9 @@ void ApiWrap::forwardMessages( if (sendAs) { sendFlags |= SendFlag::f_send_as; } + if (action.options.suggest) { + sendFlags |= SendFlag::f_suggested_post; + } const auto kGeneralId = Data::ForumTopic::kGeneralId; const auto topicRootId = action.replyTo.topicRootId; const auto topMsgId = (topicRootId == kGeneralId) @@ -3422,7 +3425,7 @@ void ApiWrap::forwardMessages( const auto monoforumPeer = monoforumPeerId ? session().data().peer(monoforumPeerId).get() : nullptr; - if (monoforumPeer) { + if (monoforumPeer || (action.options.suggest && action.replyTo)) { sendFlags |= SendFlag::f_reply_to; } @@ -3454,14 +3457,17 @@ void ApiWrap::forwardMessages( MTP_vector<MTPlong>(randomIds), peer->input, MTP_int(topMsgId), - (monoforumPeer + (action.options.suggest + ? ReplyToForMTP(history, action.replyTo) + : monoforumPeer ? MTP_inputReplyToMonoForum(monoforumPeer->input) : MTPInputReplyTo()), MTP_int(action.options.scheduled), (sendAs ? sendAs->input : MTP_inputPeerEmpty()), Data::ShortcutIdToMTP(_session, action.options.shortcutId), MTPint(), // video_timestamp - MTP_long(starsPaid) + MTP_long(starsPaid), + Api::SuggestToMTP(action.options.suggest) )).done([=](const MTPUpdates &result) { if (!scheduled) { this->updates().checkForSentToScheduled(result); diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 385ea7ea4f..dac3fe5d97 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1769,7 +1769,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( ? Flag::f_quick_reply_shortcut : Flag(0)) | (starsPaid ? Flag::f_allow_paid_stars : Flag()) - | (sublistPeer ? Flag::f_reply_to : Flag()); + | (sublistPeer ? Flag::f_reply_to : Flag()) + | (options.suggest ? Flag::f_suggested_post : Flag()); threadHistory->sendRequestId = api.request( MTPmessages_ForwardMessages( MTP_flags(sendFlags), @@ -1785,7 +1786,8 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( MTP_inputPeerEmpty(), // send_as Data::ShortcutIdToMTP(session, options.shortcutId), MTP_int(videoTimestamp.value_or(0)), - MTP_long(starsPaid) + MTP_long(starsPaid), + Api::SuggestToMTP(options.suggest) )).done([=](const MTPUpdates &updates, mtpRequestId reqId) { threadHistory->session().api().applyUpdates(updates); state->requests.remove(reqId); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 29114bb086..4d251798a9 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -981,7 +981,7 @@ HistoryWidget::HistoryWidget( action.replyTo.messageId); if (action.replaceMediaOf) { } else if (action.options.scheduled) { - cancelReply(lastKeyboardUsed); + cancelReplyOrSuggest(lastKeyboardUsed); crl::on_main(this, [=, history = action.history] { controller->showSection( std::make_shared<HistoryView::ScheduledMemento>(history)); @@ -989,7 +989,7 @@ HistoryWidget::HistoryWidget( } else { fastShowAtEnd(action.history); if (!_justMarkingAsRead - && cancelReply(lastKeyboardUsed) + && cancelReplyOrSuggest(lastKeyboardUsed) && !action.clearDraft) { saveCloudDraft(); } @@ -8758,6 +8758,12 @@ bool HistoryWidget::lastForceReplyReplied() const { == FullMsgId(_peer->id, _history->lastKeyboardId)); } +bool HistoryWidget::cancelReplyOrSuggest(bool lastKeyboardUsed) { + const auto ok1 = cancelReply(lastKeyboardUsed); + const auto ok2 = cancelSuggestPost(); + return ok1 || ok2; +} + bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { bool wasReply = false; if (_replyTo) { @@ -8804,7 +8810,7 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) { } void HistoryWidget::cancelReplyAfterMediaSend(bool lastKeyboardUsed) { - if (cancelReply(lastKeyboardUsed)) { + if (cancelReplyOrSuggest(lastKeyboardUsed)) { saveCloudDraft(); } } @@ -8989,7 +8995,7 @@ bool HistoryWidget::updateCanSendMessage() { _canSendMessages = newCanSendMessages; _canSendTexts = newCanSendTexts; if (!_canSendMessages) { - cancelReply(); + cancelReplyOrSuggest(); } refreshSuggestPostToggle(); refreshScheduledToggle(); @@ -9064,8 +9070,9 @@ void HistoryWidget::escape() { } } else if (_autocomplete && !_autocomplete->isHidden()) { _autocomplete->hideAnimated(); - } else if (_replyTo && _field->getTextWithTags().text.isEmpty()) { - cancelReply(); + } else if ((_replyTo || _suggestOptions) + && _field->getTextWithTags().empty()) { + cancelReplyOrSuggest(); } else if (auto &voice = _voiceRecordBar; voice->isActive()) { voice->showDiscardBox(nullptr, anim::type::normal); } else { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index e225f5bf41..ac39ce3988 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -218,6 +218,7 @@ public: [[nodiscard]] SuggestPostOptions suggestOptions() const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; + bool cancelReplyOrSuggest(bool lastKeyboardUsed = false); bool cancelReply(bool lastKeyboardUsed = false); bool cancelSuggestPost(); void cancelEdit(); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 89949f8636..293a4064b9 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -668,7 +668,9 @@ void ServicePreMessage::paint( const auto top = g.top() - height - st::msgMargin.bottom(); const auto position = QPoint(left, top); p.translate(position); - media->draw(p, context.translated(-position).withSelection({})); + media->draw(p, context.selected() + ? context.translated(-position) + : context.translated(-position).withSelection({})); p.translate(-position); } else { const auto top = g.top() - height - st::msgMargin.top(); diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp index 1996ba2d42..0550266b2b 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -25,23 +25,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_settings.h" namespace HistoryView { -namespace { -struct EditOptionsArgs { - int starsLimit = 0; - QString channelName; - SuggestPostOptions values; - Fn<void(SuggestPostOptions)> save; -}; - -void EditOptionsBox( +void ChooseSuggestTimeBox( not_null<Ui::GenericBox*> box, - EditOptionsArgs &&args) { + SuggestTimeBoxArgs &&args) { + const auto now = base::unixtime::now(); + const auto min = args.session->appConfig().suggestedPostDelayMin() + 60; + const auto max = args.session->appConfig().suggestedPostDelayMax(); + const auto value = args.value + ? std::clamp(args.value, now + min, now + max) + : (now + 86400); + Ui::ChooseDateTimeBox(box, { + .title = std::move(args.title), + .submit = std::move(args.submit), + .done = std::move(args.done), + .min = [=] { return now + min; }, + .time = value, + .max = [=] { return now + max; }, + }); +} + +void ChooseSuggestPriceBox( + not_null<Ui::GenericBox*> box, + SuggestPriceBoxArgs &&args) { struct State { rpl::variable<TimeId> date; }; const auto state = box->lifetime().make_state<State>(); - state->date = args.values.date; + state->date = args.value.date; + + const auto limit = args.session->appConfig().suggestedPostStarsMax(); box->setTitle(tr::lng_suggest_options_title()); @@ -57,8 +70,8 @@ void EditOptionsBox( wrap, st::editTagField, tr::lng_paid_cost_placeholder(), - args.values.stars ? QString::number(args.values.stars) : QString(), - args.starsLimit); + args.value.stars ? QString::number(args.value.stars) : QString(), + limit); const auto field = owned.data(); wrap->widthValue() | rpl::start_with_next([=](int width) { field->move(0, 0); @@ -102,14 +115,12 @@ void EditOptionsBox( strong->closeBox(); } }; - auto dateBox = Box(Ui::ChooseDateTimeBox, Ui::ChooseDateTimeBoxArgs{ + auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ + .session = args.session, .title = tr::lng_suggest_options_date(), .submit = tr::lng_settings_save(), .done = done, - .min = [] { return base::unixtime::now() + 1; }, - .time = (state->date.current() - ? state->date.current() - : (base::unixtime::now() + 86400)), + .value = state->date.current(), }); *weak = dateBox.data(); box->uiShow()->show(std::move(dateBox)); @@ -120,15 +131,15 @@ void EditOptionsBox( AssertIsDebug()//tr::lng_suggest_options_offer const auto save = [=] { const auto now = uint32(field->getLastText().toULongLong()); - if (now > args.starsLimit) { + if (now > limit) { field->showError(); return; } - const auto weak = Ui::MakeWeak(box); - args.save({ .stars = now, .date = state->date.current()}); - if (const auto strong = weak.data()) { - strong->closeBox(); - } + args.done({ + .exists = true, + .stars = now, + .date = state->date.current(), + }); }; QObject::connect(field, &Ui::NumberInput::submitted, box, save); @@ -139,8 +150,6 @@ void EditOptionsBox( }); } -} // namespace - SuggestOptions::SuggestOptions( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, @@ -179,18 +188,19 @@ void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) { } void SuggestOptions::edit() { + const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); const auto apply = [=](SuggestPostOptions values) { _values = values; updateTexts(); _updates.fire({}); + if (const auto strong = weak->data()) { + strong->closeBox(); + } }; - const auto broadcast = _peer->monoforumBroadcast(); - const auto &appConfig = _peer->session().appConfig(); - _controller->show(Box(EditOptionsBox, EditOptionsArgs{ - .starsLimit = appConfig.suggestedPostStarsMax(), - .channelName = (broadcast ? broadcast : _peer.get())->shortName(), - .values = _values, - .save = apply, + *weak = _controller->show(Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ + .session = &_peer->session(), + .done = apply, + .value = _values, })); } diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/history_view_suggest_options.h index 8c5ef1ae41..c326696d6b 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.h @@ -9,12 +9,41 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Main { +class Session; +} // namespace Main + namespace Window { class SessionController; } // namespace Window namespace HistoryView { +struct SuggestTimeBoxArgs { + not_null<Main::Session*> session; + rpl::producer<QString> title; + rpl::producer<QString> submit; + Fn<void(TimeId)> done; + TimeId value = 0; +}; +void ChooseSuggestTimeBox( + not_null<Ui::GenericBox*> box, + SuggestTimeBoxArgs &&args); + +struct SuggestPriceBoxArgs { + not_null<Main::Session*> session; + bool updating = false; + Fn<void(SuggestPostOptions)> done; + SuggestPostOptions value; +}; +void ChooseSuggestPriceBox( + not_null<Ui::GenericBox*> box, + SuggestPriceBoxArgs &&args); + class SuggestOptions final { public: SuggestOptions( diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index f0991f8f95..cacaf4f59c 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -134,7 +134,12 @@ void MediaGeneric::draw(Painter &p, const PaintContext &context) const { const auto radius = st::msgServiceGiftBoxRadius; p.setPen(Qt::NoPen); p.setBrush(context.st->msgServiceBg()); - p.drawRoundedRect(QRect(0, 0, width(), height()), radius, radius); + const auto rect = QRect(0, 0, width(), height()); + p.drawRoundedRect(rect, radius, radius); + //if (context.selected()) { + // p.setBrush(context.st->serviceTextPalette().selectBg); + // p.drawRoundedRect(rect, radius, radius); + //} } auto translated = 0; diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index 1adcc5a856..3b9c79ea26 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -166,6 +166,14 @@ int AppConfig::suggestedPostStarsMax() const { return get<int>(u"stars_suggested_post_amount_max"_q, 100'000); } +int AppConfig::suggestedPostDelayMin() const { + return get<int>(u"stars_suggested_post_future_min"_q, 300); +} + +int AppConfig::suggestedPostDelayMax() const { + return get<int>(u"appConfig.stars_suggested_post_future_max"_q, 2678400); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 886e11280c..0d9a4f6fbc 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -89,6 +89,8 @@ public: [[nodiscard]] int todoListItemTextLimit() const; [[nodiscard]] int suggestedPostStarsMax() const; + [[nodiscard]] int suggestedPostDelayMin() const; + [[nodiscard]] int suggestedPostDelayMax() const; void refresh(bool force = false); diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 1b9bc0a06f..22fd24059c 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -2194,7 +2194,7 @@ messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; messages.sendMessage#fe05dc9a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; messages.sendMedia#ac55d9c1 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; -messages.forwardMessages#38f0188c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates; +messages.forwardMessages#978928ca flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#fc78af9b peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult; From 498116c3f6c38fec94bf1a8f4b5383faad6621f2 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 20 Jun 2025 14:31:31 +0400 Subject: [PATCH 193/310] Show correctly change suggestions. --- .../history/view/history_view_about_view.cpp | 3 +- .../history/view/history_view_element.cpp | 3 +- .../history/view/history_view_message.cpp | 54 +++++-- .../history/view/history_view_message.h | 6 + .../view/media/history_view_media_generic.cpp | 13 +- .../view/media/history_view_media_generic.h | 5 +- .../media/history_view_suggest_decision.cpp | 135 ++++++++++++++---- Telegram/SourceFiles/ui/chat/chat.style | 4 +- 8 files changed, 167 insertions(+), 56 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index c4028d362a..4a0229b25e 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -619,6 +619,8 @@ void AboutView::make(Data::ChatIntro data, bool preview) { const auto sendIntroSticker = [=](not_null<DocumentData*> sticker) { _sendIntroSticker.fire_copy(sticker); }; + owned->data()->setCustomServiceLink( + std::make_shared<LambdaClickHandler>(handler)); owned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>( owned.get(), GenerateChatIntro( @@ -629,7 +631,6 @@ void AboutView::make(Data::ChatIntro data, bool preview) { sendIntroSticker), HistoryView::MediaGenericDescriptor{ .maxWidth = st::chatIntroWidth, - .serviceLink = std::make_shared<LambdaClickHandler>(handler), .service = true, .hideServiceText = preview || text.isEmpty(), })); diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 293a4064b9..0b09c36dca 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -1107,9 +1107,8 @@ void Element::refreshMedia(Element *replacing) { GenerateSuggestDecisionMedia(this, decision), MediaGenericDescriptor{ .maxWidth = st::chatSuggestInfoWidth, - .serviceLink = decision->lnk, + .fullAreaLink = decision->lnk, .service = true, - .fullAreaLink = true, .hideServiceText = true, }); } else { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 823d56dddb..a6f901bf9e 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -421,7 +421,9 @@ Message::Message( , _bottomInfo( &data->history()->owner().reactions(), BottomInfoDataFromMessage(this)) { - if (const auto media = data->media()) { + if (data->Get<HistoryMessageSuggestedPost>()) { + _hideReply = 1; + } else if (const auto media = data->media()) { if (media->giveawayResults()) { _hideReply = 1; } @@ -455,21 +457,33 @@ Message::~Message() { } } +void Message::refreshSuggestedInfo( + not_null<HistoryItem*> item, + not_null<const HistoryMessageSuggestedPost*> suggest, + const HistoryMessageReply *replyData) { + const auto link = (replyData && replyData->resolvedMessage) + ? JumpToMessageClickHandler( + replyData->resolvedMessage.get(), + item->fullId()) + : ClickHandlerPtr(); + setServicePreMessage({}, link, std::make_unique<MediaGeneric>( + this, + GenerateSuggestRequestMedia(this, suggest), + MediaGenericDescriptor{ + .maxWidth = st::chatSuggestWidth, + .fullAreaLink = link, + .service = true, + .hideServiceText = true, + })); +} + void Message::initPaidInformation() { const auto item = data(); - if (!item->history()->peer->isUser()) { - + if (item->history()->peer->isMonoforum()) { if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { - setServicePreMessage({}, {}, std::make_unique<MediaGeneric>( - this, - GenerateSuggestRequestMedia(this, suggest), - MediaGenericDescriptor{ - .maxWidth = st::chatSuggestWidth, - .service = true, - .hideServiceText = true, - })); + const auto replyData = item->Get<HistoryMessageReply>(); + refreshSuggestedInfo(item, suggest, replyData); } - return; } const auto media = this->media(); @@ -828,6 +842,22 @@ QSize Message::performCountOptimalSize() { RemoveComponents(Reply::Bit()); } + if (item->history()->peer->isMonoforum()) { + if (const auto suggest = item->Get<HistoryMessageSuggestedPost>()) { + if (const auto service = Get<ServicePreMessage>()) { + // Ok, we didn't have the message, but now we have. + // That means this is not a plain post suggestion, + // but a suggestion of changes to previous suggestion. + if (service->media + && !service->handler + && replyData + && replyData->resolvedMessage) { + refreshSuggestedInfo(item, suggest, replyData); + } + } + } + } + const auto factcheck = item->Get<HistoryMessageFactcheck>(); if (factcheck && !factcheck->data.text.empty()) { AddComponents(Factcheck::Bit()); diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index efade0c7fa..e19d950e1e 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -15,6 +15,8 @@ class HistoryItem; struct HistoryMessageEdited; struct HistoryMessageForwarded; struct HistoryMessageReplyMarkup; +struct HistoryMessageSuggestedPost; +struct HistoryMessageReply; namespace Data { struct ReactionId; @@ -175,6 +177,10 @@ private: bool updateBottomInfo(); void initPaidInformation(); + void refreshSuggestedInfo( + not_null<HistoryItem*> item, + not_null<const HistoryMessageSuggestedPost*> suggest, + const HistoryMessageReply *reply); void initLogEntryOriginal(); void initPsa(); void fromNameUpdated(int width) const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index cacaf4f59c..1e2bc404d5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -77,19 +77,15 @@ MediaGeneric::MediaGeneric( MediaGenericDescriptor &&descriptor) : Media(parent) , _paintBg(std::move(descriptor.paintBg)) +, _fullAreaLink(descriptor.fullAreaLink) , _maxWidthCap(descriptor.maxWidth) , _service(descriptor.service) -, _fullAreaLink(descriptor.fullAreaLink) , _hideServiceText(descriptor.hideServiceText) { generate(this, [&](std::unique_ptr<Part> part) { _entries.push_back({ .object = std::move(part), }); }); - if (descriptor.serviceLink) { - parent->data()->setCustomServiceLink( - std::move(descriptor.serviceLink)); - } } MediaGeneric::~MediaGeneric() { @@ -164,11 +160,8 @@ TextState MediaGeneric::textState( } if (_fullAreaLink && QRect(0, 0, width(), height()).contains(point)) { - const auto link = _parent->data()->Get<HistoryServiceCustomLink>(); - if (link) { - result.link = link->link; - return result; - } + result.link = _fullAreaLink; + return result; } for (const auto &entry : _entries) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index 766d1b18c9..e7b3b6b186 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -57,9 +57,8 @@ struct MediaGenericDescriptor { Painter&, const PaintContext&, not_null<const MediaGeneric*>)> paintBg; - ClickHandlerPtr serviceLink; + ClickHandlerPtr fullAreaLink; bool service = false; - bool fullAreaLink = false; bool hideServiceText = false; }; @@ -129,9 +128,9 @@ private: Painter&, const PaintContext&, not_null<const MediaGeneric*>)> _paintBg; + ClickHandlerPtr _fullAreaLink; int _maxWidthCap = 0; bool _service : 1 = false; - bool _fullAreaLink : 1 = false; bool _hideServiceText : 1 = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp index b3237a4755..7623f8eab9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -57,6 +57,53 @@ enum EmojiType { return QString::fromUtf8(Raw(type)); } +struct Changes { + bool date = false; + bool price = false; + bool message = true; +}; +[[nodiscard]] std::optional<Changes> ResolveChanges( + not_null<HistoryItem*> changed, + HistoryItem *original) { + const auto wasSuggest = original + ? original->Get<HistoryMessageSuggestedPost>() + : nullptr; + const auto nowSuggest = changed->Get<HistoryMessageSuggestedPost>(); + if (!wasSuggest || !nowSuggest) { + return {}; + } + auto result = Changes(); + if (wasSuggest->date != nowSuggest->date) { + result.date = true; + } + if (wasSuggest->stars != nowSuggest->stars) { + result.price = true; + } + const auto wasText = original->originalText(); + const auto nowText = changed->originalText(); + const auto mediaSame = [&] { + const auto wasMedia = original->media(); + const auto nowMedia = changed->media(); + if (!wasMedia && !nowMedia) { + return true; + } else if (!wasMedia + || !nowMedia + || !wasMedia->allowsEditCaption() + || !nowMedia->allowsEditCaption()) { + return false; + } + // We treat as "same" only same photo or same file. + return (wasMedia->photo() == nowMedia->photo()) + && (wasMedia->document() == nowMedia->document()); + }; + if (!result.price && !result.date) { + result.message = true; + } else if (wasText == nowText && mediaSame()) { + result.message = false; + } + return result; +} + } // namespace auto GenerateSuggestDecisionMedia( @@ -224,8 +271,14 @@ auto GenerateSuggestRequestMedia( result.setAlphaF(result.alphaF() * kFadedOpacity); return result; }; - const auto from = parent->data()->from(); - const auto peer = parent->history()->peer; + const auto item = parent->data(); + const auto replyData = item->Get<HistoryMessageReply>(); + const auto original = replyData + ? replyData->resolvedMessage.get() + : nullptr; + const auto changes = ResolveChanges(item, original); + const auto from = item->from(); + const auto peer = item->history()->peer; auto pushText = [&]( TextWithEntities text, @@ -242,39 +295,67 @@ auto GenerateSuggestRequestMedia( }; pushText( - (from->isSelf() + ((!changes && from->isSelf()) ? tr::lng_suggest_action_your( tr::now, Ui::Text::WithEntities) - : tr::lng_suggest_action_his( - tr::now, - lt_from, - Ui::Text::Bold(from->shortName()), - Ui::Text::WithEntities)), + : (!changes + ? tr::lng_suggest_action_his + : changes->message + ? tr::lng_suggest_change_content + : (changes->date && changes->price) + ? tr::lng_suggest_change_price_time + : changes->price + ? tr::lng_suggest_change_price + : tr::lng_suggest_change_time)( + tr::now, + lt_from, + Ui::Text::Bold(from->shortName()), + Ui::Text::WithEntities)), st::chatSuggestInfoTitleMargin, style::al_top); auto entries = std::vector<AttributeTable::Entry>(); - entries.push_back({ - tr::lng_suggest_action_price_label(tr::now), - Ui::Text::Bold(suggest->stars - ? tr::lng_prize_credits_amount( + if (!changes || changes->price) { + entries.push_back({ + (changes + ? tr::lng_suggest_change_price_label + : tr::lng_suggest_action_price_label)(tr::now), + Ui::Text::Bold(suggest->stars + ? tr::lng_prize_credits_amount( + tr::now, + lt_count, + suggest->stars) + : tr::lng_suggest_action_price_free(tr::now)), + }); + } + if (!changes || changes->date) { + entries.push_back({ + (changes + ? tr::lng_suggest_change_time_label + : tr::lng_suggest_action_time_label)(tr::now), + Ui::Text::Bold(suggest->date + ? Ui::FormatDateTime(base::unixtime::parse(suggest->date)) + : tr::lng_suggest_action_time_any(tr::now)), + }); + } + if (!entries.empty()) { + push(std::make_unique<AttributeTable>( + std::move(entries), + ((changes && changes->message) + ? st::chatSuggestTableMiddleMargin + : st::chatSuggestTableLastMargin), + fadedFg, + normalFg)); + } + if (changes && changes->message) { + push(std::make_unique<TextPartColored>( + tr::lng_suggest_change_text_label( tr::now, - lt_count, - suggest->stars) - : tr::lng_suggest_action_price_free(tr::now)), - }); - entries.push_back({ - tr::lng_suggest_action_time_label(tr::now), - Ui::Text::Bold(suggest->date - ? Ui::FormatDateTime(base::unixtime::parse(suggest->date)) - : tr::lng_suggest_action_time_any(tr::now)), - }); - push(std::make_unique<AttributeTable>( - std::move(entries), - st::chatSuggestInfoLastMargin, - fadedFg, - normalFg)); + Ui::Text::WithEntities), + st::chatSuggestInfoLastMargin, + fadedFg)); + } }; } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 34bb08479d..65d11c42c0 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1052,11 +1052,13 @@ chatSimilarName: TextStyle(defaultTextStyle) { chatSimilarWidthMax: 424px; chatSimilarSkip: 12px; -chatSuggestWidth: 216px; +chatSuggestWidth: 236px; chatSuggestInfoWidth: 272px; chatSuggestInfoTitleMargin: margins(16px, 16px, 16px, 6px); chatSuggestInfoMiddleMargin: margins(16px, 4px, 16px, 4px); chatSuggestInfoLastMargin: margins(16px, 4px, 16px, 16px); +chatSuggestTableMiddleMargin: margins(8px, 4px, 8px, 4px); +chatSuggestTableLastMargin: margins(8px, 4px, 8px, 16px); chatSuggestInfoFullMargin: margins(16px, 16px, 16px, 16px); premiumRequiredWidth: 186px; From dc19f2e76c51b00a95c17c707b28e13e729400c4 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 20 Jun 2025 21:31:54 +0400 Subject: [PATCH 194/310] Start suggesting changes to messages by editing. --- Telegram/SourceFiles/api/api_editing.cpp | 288 ++++++++++++++++-- Telegram/SourceFiles/api/api_suggest_post.cpp | 54 +++- .../history/history_inner_widget.cpp | 3 + Telegram/SourceFiles/history/history_item.h | 13 +- .../SourceFiles/history/history_widget.cpp | 144 ++++++--- Telegram/SourceFiles/history/history_widget.h | 7 +- .../view/history_view_chat_section.cpp | 9 +- .../view/history_view_suggest_options.cpp | 40 ++- .../view/history_view_suggest_options.h | 18 +- .../media/history_view_suggest_decision.cpp | 58 ++-- 10 files changed, 496 insertions(+), 138 deletions(-) diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index ed76f847f6..6f1c322718 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "api/api_media.h" #include "api/api_text_entities.h" +#include "base/random.h" #include "ui/boxes/confirm_box.h" #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" @@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "history/view/controls/history_view_compose_media_edit_manager.h" #include "history/history.h" +#include "history/history_item_components.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "mtproto/mtproto_response.h" @@ -46,6 +48,230 @@ template <typename T> constexpr auto ErrorWithoutId = is_callable_plain_v<T, QString>; +template <typename DoneCallback, typename FailCallback> +mtpRequestId SuggestMessage( + not_null<HistoryItem*> item, + const TextWithEntities &textWithEntities, + Data::WebPageDraft webpage, + SendOptions options, + DoneCallback &&done, + FailCallback &&fail) { + Expects(options.suggest.exists); + Expects(!options.scheduled); + + const auto session = &item->history()->session(); + const auto api = &session->api(); + + const auto text = textWithEntities.text; + const auto sentEntities = EntitiesToMTP( + session, + textWithEntities.entities, + ConvertOption::SkipLocal); + + const auto emptyFlag = MTPmessages_SendMessage::Flag(0); + auto replyTo = FullReplyTo{ + .messageId = item->fullId(), + .monoforumPeerId = (item->history()->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId()), + }; + const auto flags = emptyFlag + | MTPmessages_SendMessage::Flag::f_reply_to + | MTPmessages_SendMessage::Flag::f_suggested_post + | (webpage.removed + ? MTPmessages_SendMessage::Flag::f_no_webpage + : emptyFlag) + | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) + || options.invertCaption) + ? MTPmessages_SendMessage::Flag::f_invert_media + : emptyFlag) + | (!sentEntities.v.isEmpty() + ? MTPmessages_SendMessage::Flag::f_entities + : emptyFlag) + | (options.starsApproved + ? MTPmessages_SendMessage::Flag::f_allow_paid_stars + : emptyFlag); + const auto randomId = base::RandomValue<uint64>(); + return api->request(MTPmessages_SendMessage( + MTP_flags(flags), + item->history()->peer->input, + ReplyToForMTP(item->history(), replyTo), + MTP_string(text), + MTP_long(randomId), + MTPReplyMarkup(), + sentEntities, + MTPint(), // schedule_date + MTPInputPeer(), // send_as + MTPInputQuickReplyShortcut(), // quick_reply_shortcut + MTPlong(), // effect + MTP_long(options.starsApproved), + Api::SuggestToMTP(options.suggest) + )).done([=]( + const MTPUpdates &result, + [[maybe_unused]] mtpRequestId requestId) { + const auto apply = [=] { api->applyUpdates(result); }; + + if constexpr (WithId<DoneCallback>) { + done(apply, requestId); + } else if constexpr (WithoutId<DoneCallback>) { + done(apply); + } else if constexpr (WithoutCallback<DoneCallback>) { + done(); + apply(); + } else { + t_bad_callback(done); + } + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + if constexpr (ErrorWithId<FailCallback>) { + fail(error.type(), requestId); + } else if constexpr (ErrorWithoutId<FailCallback>) { + fail(error.type()); + } else if constexpr (WithoutCallback<FailCallback>) { + fail(); + } else { + t_bad_callback(fail); + } + }).send(); +} + +template <typename DoneCallback, typename FailCallback> +mtpRequestId SuggestMedia( + not_null<HistoryItem*> item, + const TextWithEntities &textWithEntities, + Data::WebPageDraft webpage, + SendOptions options, + DoneCallback &&done, + FailCallback &&fail, + std::optional<MTPInputMedia> inputMedia) { + Expects(options.suggest.exists); + Expects(!options.scheduled); + + const auto session = &item->history()->session(); + const auto api = &session->api(); + + const auto text = textWithEntities.text; + const auto sentEntities = EntitiesToMTP( + session, + textWithEntities.entities, + ConvertOption::SkipLocal); + + const auto updateRecentStickers = inputMedia + ? Api::HasAttachedStickers(*inputMedia) + : false; + + const auto emptyFlag = MTPmessages_SendMedia::Flag(0); + auto replyTo = FullReplyTo{ + .messageId = item->fullId(), + .monoforumPeerId = (item->history()->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId()), + }; + const auto flags = emptyFlag + | MTPmessages_SendMedia::Flag::f_reply_to + | MTPmessages_SendMedia::Flag::f_suggested_post + | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) + || options.invertCaption) + ? MTPmessages_SendMedia::Flag::f_invert_media + : emptyFlag) + | (!sentEntities.v.isEmpty() + ? MTPmessages_SendMedia::Flag::f_entities + : emptyFlag) + | (options.starsApproved + ? MTPmessages_SendMedia::Flag::f_allow_paid_stars + : emptyFlag); + const auto randomId = base::RandomValue<uint64>(); + return api->request(MTPmessages_SendMedia( + MTP_flags(flags), + item->history()->peer->input, + ReplyToForMTP(item->history(), replyTo), + inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())), + MTP_string(text), + MTP_long(randomId), + MTPReplyMarkup(), + sentEntities, + MTPint(), // schedule_date + MTPInputPeer(), // send_as + MTPInputQuickReplyShortcut(), // quick_reply_shortcut + MTPlong(), // effect + MTP_long(options.starsApproved), + Api::SuggestToMTP(options.suggest) + )).done([=]( + const MTPUpdates &result, + [[maybe_unused]] mtpRequestId requestId) { + const auto apply = [=] { api->applyUpdates(result); }; + + if constexpr (WithId<DoneCallback>) { + done(apply, requestId); + } else if constexpr (WithoutId<DoneCallback>) { + done(apply); + } else if constexpr (WithoutCallback<DoneCallback>) { + done(); + apply(); + } else { + t_bad_callback(done); + } + + if (updateRecentStickers) { + api->requestSpecialStickersForce(false, false, true); + } + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + if constexpr (ErrorWithId<FailCallback>) { + fail(error.type(), requestId); + } else if constexpr (ErrorWithoutId<FailCallback>) { + fail(error.type()); + } else if constexpr (WithoutCallback<FailCallback>) { + fail(); + } else { + t_bad_callback(fail); + } + }).send(); +} + +template <typename DoneCallback, typename FailCallback> +mtpRequestId SuggestMessageOrMedia( + not_null<HistoryItem*> item, + const TextWithEntities &textWithEntities, + Data::WebPageDraft webpage, + SendOptions options, + DoneCallback &&done, + FailCallback &&fail, + std::optional<MTPInputMedia> inputMedia) { + const auto wasMedia = item->media(); + if (!inputMedia && wasMedia && wasMedia->allowsEditCaption()) { + if (const auto photo = wasMedia->photo()) { + inputMedia = MTP_inputMediaPhoto( + MTP_flags(0), + photo->mtpInput(), + MTPint()); // ttl_seconds + } else if (const auto document = wasMedia->document()) { + inputMedia = MTP_inputMediaDocument( + MTP_flags(0), + document->mtpInput(), + MTPInputPhoto(), // video_cover + MTPint(), // video_timestamp + MTPint(), // ttl_seconds + MTPstring()); // query + } + } + if (inputMedia || (!webpage.removed && !webpage.url.isEmpty())) { + return SuggestMedia( + item, + textWithEntities, + webpage, + options, + std::move(done), + std::move(fail), + inputMedia); + } + return SuggestMessage( + item, + textWithEntities, + webpage, + options, + std::move(done), + std::move(fail)); +} + template <typename DoneCallback, typename FailCallback> mtpRequestId EditMessage( not_null<HistoryItem*> item, @@ -55,6 +281,18 @@ mtpRequestId EditMessage( DoneCallback &&done, FailCallback &&fail, std::optional<MTPInputMedia> inputMedia = std::nullopt) { + if (item->computeSuggestionActions() + == SuggestionActions::AcceptAndDecline) { + return SuggestMessageOrMedia( + item, + textWithEntities, + webpage, + options, + std::move(done), + std::move(fail), + inputMedia); + } + const auto session = &item->history()->session(); const auto api = &session->api(); @@ -71,31 +309,31 @@ mtpRequestId EditMessage( const auto emptyFlag = MTPmessages_EditMessage::Flag(0); const auto flags = emptyFlag - | ((!text.isEmpty() || media) - ? MTPmessages_EditMessage::Flag::f_message - : emptyFlag) - | ((media && inputMedia.has_value()) - ? MTPmessages_EditMessage::Flag::f_media - : emptyFlag) - | (webpage.removed - ? MTPmessages_EditMessage::Flag::f_no_webpage - : emptyFlag) - | ((!webpage.removed && !webpage.url.isEmpty()) - ? MTPmessages_EditMessage::Flag::f_media - : emptyFlag) - | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) - || options.invertCaption) - ? MTPmessages_EditMessage::Flag::f_invert_media - : emptyFlag) - | (!sentEntities.v.isEmpty() - ? MTPmessages_EditMessage::Flag::f_entities - : emptyFlag) - | (options.scheduled - ? MTPmessages_EditMessage::Flag::f_schedule_date - : emptyFlag) - | (item->isBusinessShortcut() - ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id - : emptyFlag); + | ((!text.isEmpty() || media) + ? MTPmessages_EditMessage::Flag::f_message + : emptyFlag) + | ((media && inputMedia.has_value()) + ? MTPmessages_EditMessage::Flag::f_media + : emptyFlag) + | (webpage.removed + ? MTPmessages_EditMessage::Flag::f_no_webpage + : emptyFlag) + | ((!webpage.removed && !webpage.url.isEmpty()) + ? MTPmessages_EditMessage::Flag::f_media + : emptyFlag) + | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) + || options.invertCaption) + ? MTPmessages_EditMessage::Flag::f_invert_media + : emptyFlag) + | (!sentEntities.v.isEmpty() + ? MTPmessages_EditMessage::Flag::f_entities + : emptyFlag) + | (options.scheduled + ? MTPmessages_EditMessage::Flag::f_schedule_date + : emptyFlag) + | (item->isBusinessShortcut() + ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id + : emptyFlag); const auto id = item->isScheduled() ? session->scheduledMessages().lookupId(item) diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 8734cd8986..a6a1f09379 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -9,8 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" +#include "chat_helpers/message_field.h" #include "core/click_handler_types.h" +#include "data/data_changes.h" #include "data/data_session.h" +#include "data/data_saved_sublist.h" #include "history/view/history_view_suggest_options.h" #include "history/history.h" #include "history/history_item.h" @@ -134,9 +137,8 @@ void RequestApprovalDate( using namespace HistoryView; auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ .session = &controller->session(), - .title = tr::lng_suggest_options_date(), - .submit = tr::lng_settings_save(), .done = done, + .mode = SuggestMode::New, }); *weak = dateBox.data(); controller->uiShow()->show(std::move(dateBox)); @@ -266,10 +268,9 @@ void SuggestApprovalDate( using namespace HistoryView; auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ .session = &controller->session(), - .title = tr::lng_suggest_menu_edit_time(), - .submit = tr::lng_profile_suggest_button(), .done = done, .value = suggestion->date, + .mode = SuggestMode::Change, }); *weak = dateBox.data(); controller->uiShow()->show(std::move(dateBox)); @@ -311,6 +312,7 @@ void SuggestApprovalPrice( .stars = uint32(suggestion->stars), .date = suggestion->date, }, + .mode = SuggestMode::Change, }); *weak = dateBox.data(); controller->uiShow()->show(std::move(dateBox)); @@ -373,9 +375,47 @@ std::shared_ptr<ClickHandler> SuggestChangesClickHandler( const auto menu = Ui::CreateChild<Ui::PopupMenu>( window->widget(), st::popupMenuWithIcons); - menu->addAction(tr::lng_suggest_menu_edit_message(tr::now), [=] { - - }, &st::menuIconEdit); + if (HistoryView::CanEditSuggestedMessage(item)) { + menu->addAction(tr::lng_suggest_menu_edit_message(tr::now), [=] { + const auto item = session->data().message(id); + if (!item) { + return; + } + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } + const auto history = item->history(); + const auto editData = PrepareEditText(item); + const auto cursor = MessageCursor{ + int(editData.text.size()), + int(editData.text.size()), + Ui::kQFixedMax + }; + const auto monoforumPeerId = history->amMonoforumAdmin() + ? item->sublistPeerId() + : PeerId(); + const auto previewDraft = Data::WebPageDraft::FromItem(item); + history->setLocalEditDraft(std::make_unique<Data::Draft>( + editData, + FullReplyTo{ + .messageId = FullMsgId(history->peer->id, item->id), + .monoforumPeerId = monoforumPeerId, + }, + SuggestPostOptions{ + .exists = 1, + .stars = uint32(suggestion->stars), + .date = suggestion->date, + }, + cursor, + previewDraft)); + history->session().changes().entryUpdated( + (monoforumPeerId + ? item->savedSublist() + : (Data::Thread*)history.get()), + Data::EntryUpdate::Flag::LocalDraftSet); + }, &st::menuIconEdit); + } menu->addAction(tr::lng_suggest_menu_edit_price(tr::now), [=] { if (const auto item = session->data().message(id)) { SuggestApprovalPrice(window, item); diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 3f8b82b933..0fe7e05bdf 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2410,6 +2410,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { highlightId); }, &st::menuIconViewReplies); } + _menu->addAction(u"Add Offer"_q, [=] { + + }, &st::menuIconDiscussion); const auto t = base::unixtime::now(); const auto editItem = (albumPartItem && albumPartItem->allowsEdit(t)) ? albumPartItem diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index a7551d89d8..62bb3b6e94 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -544,6 +544,13 @@ public: [[nodiscard]] bool canUpdateDate() const; void customEmojiRepaint(); + [[nodiscard]] SuggestionActions computeSuggestionActions() const; + [[nodiscard]] SuggestionActions computeSuggestionActions( + const HistoryMessageSuggestedPost *suggest) const; + [[nodiscard]] SuggestionActions computeSuggestionActions( + bool accepted, + bool rejected) const; + [[nodiscard]] bool needsUpdateForVideoQualities(const MTPMessage &data); [[nodiscard]] TimeId ttlDestroyAt() const { @@ -582,12 +589,6 @@ private: void setReplyMarkup( HistoryMessageMarkupData &&markup, bool ignoreSuggestButtons = false); - [[nodiscard]] SuggestionActions computeSuggestionActions() const; - [[nodiscard]] SuggestionActions computeSuggestionActions( - const HistoryMessageSuggestedPost *suggest) const; - [[nodiscard]] SuggestionActions computeSuggestionActions( - bool accepted, - bool rejected) const; void updateSuggestControls(const HistoryMessageSuggestedPost *suggest); void changeReplyToTopCounter( diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4d251798a9..17eabdada5 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2269,6 +2269,12 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { if (!_replyEditMsg) { requestMessageData(_editMsgId); } + if (editDraft && editDraft->suggest) { + using namespace HistoryView; + applySuggestOptions(editDraft->suggest, SuggestMode::Change); + } else { + cancelSuggestPost(); + } } else { const auto draft = _history->localDraft(MsgId(), PeerId()); _processingReplyTo = draft ? draft->reply : FullReplyTo(); @@ -2276,7 +2282,8 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { _processingReplyItem = session().data().message( _processingReplyTo.messageId); } else if (draft && draft->suggest) { - applySuggestOptions(draft->suggest); + using namespace HistoryView; + applySuggestOptions(draft->suggest, SuggestMode::New); } processReply(); } @@ -2352,7 +2359,9 @@ void HistoryWidget::showHistory( if (_peer->id == peerId) { updateForwarding(); - if (showAtMsgId == ShowAtUnreadMsgId + if (params.reapplyLocalDraft) { + return; + } else if (showAtMsgId == ShowAtUnreadMsgId && insideJumpToEndInsteadOfToUnread()) { DEBUG_LOG(("JumpToEnd(%1, %2, %3): " "Jumping to end instead of unread." @@ -3164,14 +3173,17 @@ void HistoryWidget::refreshSendGiftToggle() { } } -void HistoryWidget::applySuggestOptions(SuggestPostOptions suggest) { +void HistoryWidget::applySuggestOptions( + SuggestPostOptions suggest, + HistoryView::SuggestMode mode) { Expects(suggest.exists); using namespace HistoryView; _suggestOptions = std::make_unique<SuggestOptions>( controller(), _peer, - suggest); + suggest, + mode); _suggestOptions->updates() | rpl::start_with_next([=] { updateField(); saveDraftWithTextNow(); @@ -3193,7 +3205,8 @@ void HistoryWidget::refreshSuggestPostToggle() { _toggleSuggestPost.create(this, st::historySuggestPostToggle); _toggleSuggestPost->setVisible(!_suggestOptions); _toggleSuggestPost->addClickHandler([=] { - applySuggestOptions({ .exists = 1 }); + using namespace HistoryView; + applySuggestOptions({ .exists = 1 }, SuggestMode::New); cancelReply(); _processingReplyTo = FullReplyTo(); _processingReplyItem = nullptr; @@ -4419,7 +4432,7 @@ TextWithEntities HistoryWidget::prepareTextForEditMsg() const { return left; } -void HistoryWidget::saveEditMsg() { +void HistoryWidget::saveEditMessage(Api::SendOptions options) { Expects(_history != nullptr); if (_saveEditMsgRequestId) { @@ -4442,9 +4455,11 @@ void HistoryWidget::saveEditMsg() { || webPageDraft.url.isEmpty() || !webPageDraft.manual) && !hasMediaWithCaption) { - const auto suggestModerateActions = false; - controller()->show( - Box<DeleteMessagesBox>(item, suggestModerateActions)); + if (item->computeSuggestionActions() == SuggestionActions::None) { + const auto suggestModerateActions = false; + controller()->show( + Box<DeleteMessagesBox>(item, suggestModerateActions)); + } return; } else { const auto maxCaptionSize = !hasMediaWithCaption @@ -4506,11 +4521,27 @@ void HistoryWidget::saveEditMsg() { })(); }; + options.invertCaption = _mediaEditManager.invertCaption(); + options.suggest = suggestOptions(); + + const auto withPaymentApproved = [=](int approved) { + auto copy = options; + copy.starsApproved = approved; + saveEditMessage(copy); + }; + const auto checked = checkSendPayment( + 1 + int(_forwardPanel->items().size()), + options.starsApproved, + withPaymentApproved); + if (!checked) { + return; + } + _saveEditMsgRequestId = Api::EditTextMessage( item, sending, webPageDraft, - { .invertCaption = _mediaEditManager.invertCaption() }, + options, done, fail, _mediaEditManager.spoilered()); @@ -4611,7 +4642,7 @@ void HistoryWidget::send(Api::SendOptions options) { if (!_history) { return; } else if (_editMsgId) { - saveEditMsg(); + saveEditMessage({}); return; } else if (!options.scheduled && showSlowmodeError()) { return; @@ -7386,10 +7417,14 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { } else if (_previewDrawPreview) { editDraftOptions(); } else if (_editMsgId) { - controller()->showPeerHistory( - _peer, - Window::SectionShow::Way::Forward, - _editMsgId); + if (_suggestOptions) { + _suggestOptions->edit(); + } else { + controller()->showPeerHistory( + _peer, + Window::SectionShow::Way::Forward, + _editMsgId); + } } else if (_replyTo && ((e->modifiers() & Qt::ControlModifier) || (e->button() != Qt::LeftButton))) { @@ -8834,6 +8869,7 @@ void HistoryWidget::cancelEdit() { _replyEditMsg = nullptr; setEditMsgId(0); _history->clearLocalEditDraft(MsgId(), PeerId()); + cancelSuggestPost(); applyDraft(); if (_saveEditMsgRequestId) { @@ -9367,14 +9403,18 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { const auto paused = p.inactive(); const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler); auto replyLeft = st::historyReplySkip; - (_editMsgId - ? st::historyEditIcon - : (_replyTo && !_replyTo.quote.empty()) - ? st::historyQuoteIcon - : st::historyReplyIcon).paint( - p, - st::historyReplyIconPosition + QPoint(0, backy), - width()); + if (_suggestOptions) { + _suggestOptions->paintIcon(p, 0, backy, width()); + } else { + (_editMsgId + ? st::historyEditIcon + : (_replyTo && !_replyTo.quote.empty()) + ? st::historyQuoteIcon + : st::historyReplyIcon).paint( + p, + st::historyReplyIconPosition + QPoint(0, backy), + width()); + } if (drawMsgText) { if (hasPreview) { if (preview) { @@ -9412,37 +9452,41 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { } replyLeft += st::historyReplyPreview + st::msgReplyBarSkip; } - p.setPen(st::historyReplyNameFg); - if (_editMsgId) { - paintEditHeader(p, rect, replyLeft, backy); + if (_suggestOptions) { + _suggestOptions->paintLines(p, replyLeft, backy, width()); } else { - _replyToName.drawElided( - p, - replyLeft, - backy + st::msgReplyPadding.top(), - width() + p.setPen(st::historyReplyNameFg); + if (_editMsgId) { + paintEditHeader(p, rect, replyLeft, backy); + } else { + _replyToName.drawElided( + p, + replyLeft, + backy + st::msgReplyPadding.top(), + width() + - replyLeft + - _fieldBarCancel->width() + - st::msgReplyPadding.right()); + } + p.setPen(st::historyComposeAreaFg); + _replyEditMsgText.draw(p, { + .position = QPoint( + replyLeft, + st::msgReplyPadding.top() + + st::msgServiceNameFont->height + + backy), + .availableWidth = width() - replyLeft - _fieldBarCancel->width() - - st::msgReplyPadding.right()); + - st::msgReplyPadding.right(), + .palette = &st::historyComposeAreaPalette, + .spoiler = Ui::Text::DefaultSpoilerCache(), + .now = now, + .pausedEmoji = paused || On(PowerSaving::kEmojiChat), + .pausedSpoiler = pausedSpoiler, + .elisionLines = 1, + }); } - p.setPen(st::historyComposeAreaFg); - _replyEditMsgText.draw(p, { - .position = QPoint( - replyLeft, - st::msgReplyPadding.top() - + st::msgServiceNameFont->height - + backy), - .availableWidth = width() - - replyLeft - - _fieldBarCancel->width() - - st::msgReplyPadding.right(), - .palette = &st::historyComposeAreaPalette, - .spoiler = Ui::Text::DefaultSpoilerCache(), - .now = now, - .pausedEmoji = paused || On(PowerSaving::kEmojiChat), - .pausedSpoiler = pausedSpoiler, - .elisionLines = 1, - }); } else { p.setFont(st::msgDateFont); p.setPen(st::historyComposeAreaFgService); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index ac39ce3988..ec81821ee6 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -110,6 +110,7 @@ class ComposeSearch; class SubsectionTabs; struct SelectedQuote; class SuggestOptions; +enum class SuggestMode; } // namespace HistoryView namespace HistoryView::Controls { @@ -589,7 +590,7 @@ private: void createUnreadBarAndResize(); [[nodiscard]] TextWithEntities prepareTextForEditMsg() const; - void saveEditMsg(); + void saveEditMessage(Api::SendOptions options = {}); void setupPreview(); void editDraftOptions(); @@ -682,7 +683,9 @@ private: void refreshScheduledToggle(); void refreshSendGiftToggle(); void refreshSuggestPostToggle(); - void applySuggestOptions(SuggestPostOptions suggest); + void applySuggestOptions( + SuggestPostOptions suggest, + HistoryView::SuggestMode mode); void setupSendAsToggle(); void refreshSendAsToggle(); void refreshAttachBotsMenu(); diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index a4d8b228ce..15840ea3a6 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2381,13 +2381,14 @@ bool ChatWidget::showInternal( const Window::SectionShow ¶ms) { if (auto logMemento = dynamic_cast<ChatMemento*>(memento.get())) { if (logMemento->id() == _id) { - restoreState(logMemento); - if (!logMemento->highlightId()) { - showAtPosition(Data::UnreadMessagePosition); - } if (params.reapplyLocalDraft) { _composeControls->applyDraft( ComposeControls::FieldHistoryAction::NewEntry); + } else { + restoreState(logMemento); + if (!logMemento->highlightId()) { + showAtPosition(Data::UnreadMessagePosition); + } } return true; } diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp index 0550266b2b..f8b3b89f81 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "data/data_channel.h" +#include "data/data_media_types.h" +#include "history/history_item.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -36,8 +38,12 @@ void ChooseSuggestTimeBox( ? std::clamp(args.value, now + min, now + max) : (now + 86400); Ui::ChooseDateTimeBox(box, { - .title = std::move(args.title), - .submit = std::move(args.submit), + .title = ((args.mode == SuggestMode::New) + ? tr::lng_suggest_options_date() + : tr::lng_suggest_menu_edit_time()), + .submit = ((args.mode == SuggestMode::New) + ? tr::lng_settings_save() + : tr::lng_suggest_options_update()), .done = std::move(args.done), .min = [=] { return now + min; }, .time = value, @@ -56,7 +62,9 @@ void ChooseSuggestPriceBox( const auto limit = args.session->appConfig().suggestedPostStarsMax(); - box->setTitle(tr::lng_suggest_options_title()); + box->setTitle((args.mode == SuggestMode::New) + ? tr::lng_suggest_options_title() + : tr::lng_suggest_options_change()); const auto container = box->verticalLayout(); @@ -117,10 +125,9 @@ void ChooseSuggestPriceBox( }; auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ .session = args.session, - .title = tr::lng_suggest_options_date(), - .submit = tr::lng_settings_save(), .done = done, .value = state->date.current(), + .mode = args.mode, }); *weak = dateBox.data(); box->uiShow()->show(std::move(dateBox)); @@ -150,25 +157,38 @@ void ChooseSuggestPriceBox( }); } +bool CanEditSuggestedMessage(not_null<HistoryItem*> item) { + const auto media = item->media(); + return !media || media->allowsEditCaption(); +} + SuggestOptions::SuggestOptions( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, - SuggestPostOptions values) + SuggestPostOptions values, + SuggestMode mode) : _controller(controller) , _peer(peer) +, _mode(mode) , _values(values) { updateTexts(); } SuggestOptions::~SuggestOptions() = default; -void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) { +void SuggestOptions::paintIcon(QPainter &p, int x, int y, int outerWidth) { st::historyDirectMessage.icon.paint( p, QPoint(x, y) + st::historySuggestIconPosition, outerWidth); +} - x += st::historyReplySkip; +void SuggestOptions::paintBar(QPainter &p, int x, int y, int outerWidth) { + paintIcon(p, x, y, outerWidth); + paintLines(p, x + st::historyReplySkip, y, outerWidth); +} + +void SuggestOptions::paintLines(QPainter &p, int x, int y, int outerWidth) { auto available = outerWidth - x - st::historyReplyCancel.width @@ -207,7 +227,9 @@ void SuggestOptions::edit() { void SuggestOptions::updateTexts() { _title.setText( st::semiboldTextStyle, - tr::lng_suggest_bar_title(tr::now)); + ((_mode == SuggestMode::New) + ? tr::lng_suggest_bar_title(tr::now) + : tr::lng_suggest_options_change(tr::now))); _text.setMarkedText(st::defaultTextStyle, composeText()); } diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/history_view_suggest_options.h index c326696d6b..fbf15d12b7 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.h @@ -23,12 +23,16 @@ class SessionController; namespace HistoryView { +enum class SuggestMode { + New, + Change, +}; + struct SuggestTimeBoxArgs { not_null<Main::Session*> session; - rpl::producer<QString> title; - rpl::producer<QString> submit; Fn<void(TimeId)> done; TimeId value = 0; + SuggestMode mode = SuggestMode::New; }; void ChooseSuggestTimeBox( not_null<Ui::GenericBox*> box, @@ -39,22 +43,29 @@ struct SuggestPriceBoxArgs { bool updating = false; Fn<void(SuggestPostOptions)> done; SuggestPostOptions value; + SuggestMode mode = SuggestMode::New; }; void ChooseSuggestPriceBox( not_null<Ui::GenericBox*> box, SuggestPriceBoxArgs &&args); +[[nodiscard]] bool CanEditSuggestedMessage(not_null<HistoryItem*> item); + class SuggestOptions final { public: SuggestOptions( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, - SuggestPostOptions values); + SuggestPostOptions values, + SuggestMode mode); ~SuggestOptions(); void paintBar(QPainter &p, int x, int y, int outerWidth); void edit(); + void paintIcon(QPainter &p, int x, int y, int outerWidth); + void paintLines(QPainter &p, int x, int y, int outerWidth); + [[nodiscard]] SuggestPostOptions values() const; [[nodiscard]] rpl::producer<> updates() const; @@ -68,6 +79,7 @@ private: const not_null<Window::SessionController*> _controller; const not_null<PeerData*> _peer; + const SuggestMode _mode = SuggestMode::New; Ui::Text::String _title; Ui::Text::String _text; diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp index 7623f8eab9..9a81e9afa9 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -316,38 +316,32 @@ auto GenerateSuggestRequestMedia( style::al_top); auto entries = std::vector<AttributeTable::Entry>(); - if (!changes || changes->price) { - entries.push_back({ - (changes - ? tr::lng_suggest_change_price_label - : tr::lng_suggest_action_price_label)(tr::now), - Ui::Text::Bold(suggest->stars - ? tr::lng_prize_credits_amount( - tr::now, - lt_count, - suggest->stars) - : tr::lng_suggest_action_price_free(tr::now)), - }); - } - if (!changes || changes->date) { - entries.push_back({ - (changes - ? tr::lng_suggest_change_time_label - : tr::lng_suggest_action_time_label)(tr::now), - Ui::Text::Bold(suggest->date - ? Ui::FormatDateTime(base::unixtime::parse(suggest->date)) - : tr::lng_suggest_action_time_any(tr::now)), - }); - } - if (!entries.empty()) { - push(std::make_unique<AttributeTable>( - std::move(entries), - ((changes && changes->message) - ? st::chatSuggestTableMiddleMargin - : st::chatSuggestTableLastMargin), - fadedFg, - normalFg)); - } + entries.push_back({ + ((changes && changes->price) + ? tr::lng_suggest_change_price_label + : tr::lng_suggest_action_price_label)(tr::now), + Ui::Text::Bold(suggest->stars + ? tr::lng_prize_credits_amount( + tr::now, + lt_count, + suggest->stars) + : tr::lng_suggest_action_price_free(tr::now)), + }); + entries.push_back({ + ((changes && changes->date) + ? tr::lng_suggest_change_time_label + : tr::lng_suggest_action_time_label)(tr::now), + Ui::Text::Bold(suggest->date + ? Ui::FormatDateTime(base::unixtime::parse(suggest->date)) + : tr::lng_suggest_action_time_any(tr::now)), + }); + push(std::make_unique<AttributeTable>( + std::move(entries), + ((changes && changes->message) + ? st::chatSuggestTableMiddleMargin + : st::chatSuggestTableLastMargin), + fadedFg, + normalFg)); if (changes && changes->message) { push(std::make_unique<TextPartColored>( tr::lng_suggest_change_text_label( From 0fa50f19519a1082caf41308fe7d0f11a1828866 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Mon, 23 Jun 2025 18:10:07 +0400 Subject: [PATCH 195/310] Update API scheme on layer 206. --- Telegram/CMakeLists.txt | 2 +- Telegram/SourceFiles/api/api_common.cpp | 2 +- Telegram/SourceFiles/api/api_credits.cpp | 32 ++-- Telegram/SourceFiles/api/api_suggest_post.cpp | 16 +- Telegram/SourceFiles/api/api_updates.cpp | 5 + .../SourceFiles/boxes/gift_credits_box.cpp | 2 +- .../SourceFiles/boxes/gift_premium_box.cpp | 8 +- .../boxes/peers/edit_peer_info_box.cpp | 8 +- Telegram/SourceFiles/boxes/star_gift_box.cpp | 14 +- Telegram/SourceFiles/core/credits_amount.h | 155 ++++++++++++++++++ Telegram/SourceFiles/core/stars_amount.h | 108 ------------ .../SourceFiles/data/components/credits.cpp | 59 ++++--- .../SourceFiles/data/components/credits.h | 26 ++- Telegram/SourceFiles/data/data_credits.h | 8 +- Telegram/SourceFiles/data/data_credits_earn.h | 8 +- Telegram/SourceFiles/data/data_drafts.cpp | 13 +- .../SourceFiles/data/data_media_types.cpp | 2 +- Telegram/SourceFiles/data/data_media_types.h | 5 +- .../data/data_message_reactions.cpp | 8 +- Telegram/SourceFiles/data/data_msg_id.h | 11 +- Telegram/SourceFiles/data/data_user.cpp | 5 +- Telegram/SourceFiles/data/data_user.h | 4 +- .../export/data/export_data_types.cpp | 16 +- .../export/data/export_data_types.h | 9 +- .../export/output/export_output_html.cpp | 14 +- .../export/output/export_output_json.cpp | 15 +- Telegram/SourceFiles/history/history_item.cpp | 49 +++++- .../history/history_item_components.h | 4 +- .../history/history_item_reply_markup.cpp | 4 +- .../history/history_item_reply_markup.h | 2 +- .../view/history_view_suggest_options.cpp | 36 +++- .../view/media/history_view_premium_gift.cpp | 4 + .../view/media/history_view_premium_gift.h | 1 + .../media/history_view_suggest_decision.cpp | 26 +-- .../info/bot/earn/info_bot_earn_list.cpp | 14 +- .../bot/starref/info_bot_starref_common.cpp | 2 +- .../channel_statistics/earn/earn_format.cpp | 4 +- .../channel_statistics/earn/earn_format.h | 2 +- .../earn/info_channel_earn_list.cpp | 18 +- .../info/profile/info_profile_actions.cpp | 13 +- .../info_statistics_list_controllers.cpp | 2 +- Telegram/SourceFiles/lang/lang_tag.cpp | 10 +- Telegram/SourceFiles/lang/lang_tag.h | 9 +- Telegram/SourceFiles/mtproto/scheme/api.tl | 10 +- .../SourceFiles/payments/payments_form.cpp | 2 +- Telegram/SourceFiles/payments/payments_form.h | 2 +- .../payments/ui/payments_reaction_box.cpp | 2 +- .../payments/ui/payments_reaction_box.h | 2 +- .../SourceFiles/settings/settings_credits.cpp | 4 +- .../settings/settings_credits_graphics.cpp | 56 +++---- .../settings/settings_credits_graphics.h | 6 +- .../SourceFiles/settings/settings_main.cpp | 4 +- Telegram/SourceFiles/stdafx.h | 2 +- .../SourceFiles/storage/storage_account.cpp | 49 ++++-- .../ui/effects/credits_graphics.cpp | 6 +- .../SourceFiles/ui/effects/credits_graphics.h | 2 +- Telegram/SourceFiles/ui/ui_pch.h | 2 +- 57 files changed, 556 insertions(+), 348 deletions(-) create mode 100644 Telegram/SourceFiles/core/credits_amount.h delete mode 100644 Telegram/SourceFiles/core/stars_amount.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 3800886afa..d36f0213fa 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -470,6 +470,7 @@ PRIVATE core/crash_report_window.h core/crash_reports.cpp core/crash_reports.h + core/credits_amount.h core/deadlock_detector.h core/file_utilities.cpp core/file_utilities.h @@ -483,7 +484,6 @@ PRIVATE core/sandbox.h core/shortcuts.cpp core/shortcuts.h - core/stars_amount.h core/ui_integration.cpp core/ui_integration.h core/update_checker.cpp diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index 29617a3f16..65ec607c04 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -19,7 +19,7 @@ MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) { return suggest.exists ? MTP_suggestedPost( MTP_flags(suggest.date ? Flag::f_schedule_date : Flag()), - MTP_long(suggest.stars), + StarsAmountToTL(suggest.price()), MTP_int(suggest.date)) : MTPSuggestedPost(); } diff --git a/Telegram/SourceFiles/api/api_credits.cpp b/Telegram/SourceFiles/api/api_credits.cpp index ec49947ebf..cda8180343 100644 --- a/Telegram/SourceFiles/api/api_credits.cpp +++ b/Telegram/SourceFiles/api/api_credits.cpp @@ -80,16 +80,15 @@ constexpr auto kTransactionsLimit = 100; }, [](const auto &) { return (const MTPDstarGift*)nullptr; }) : nullptr; const auto reaction = tl.data().is_reaction(); - const auto amount = Data::FromTL(tl.data().vstars()); - const auto starrefAmount = tl.data().vstarref_amount() - ? Data::FromTL(*tl.data().vstarref_amount()) - : StarsAmount(); + const auto amount = CreditsAmountFromTL(tl.data().vstars()); + const auto starrefAmount = CreditsAmountFromTL( + tl.data().vstarref_amount()); const auto starrefCommission = tl.data().vstarref_commission_permille().value_or_empty(); const auto starrefBarePeerId = tl.data().vstarref_peer() ? peerFromMTP(*tl.data().vstarref_peer()).value : 0; - const auto incoming = (amount >= StarsAmount()); + const auto incoming = (amount >= CreditsAmount()); const auto paidMessagesCount = tl.data().vpaid_messages().value_or_empty(); const auto premiumMonthsForStars @@ -108,7 +107,7 @@ constexpr auto kTransactionsLimit = 100; .date = base::unixtime::parse(tl.data().vdate().v), .photoId = photo ? photo->id : 0, .extended = std::move(extended), - .credits = Data::FromTL(tl.data().vstars()), + .credits = CreditsAmountFromTL(tl.data().vstars()), .bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()), .barePeerId = saveActorId ? peer->id.value : barePeerId, .bareGiveawayMsgId = uint64( @@ -116,7 +115,7 @@ constexpr auto kTransactionsLimit = 100; .bareGiftStickerId = giftStickerId, .bareActorId = saveActorId ? barePeerId : uint64(0), .uniqueGift = parsedGift ? parsedGift->unique : nullptr, - .starrefAmount = paidMessagesCount ? StarsAmount() : starrefAmount, + .starrefAmount = paidMessagesCount ? CreditsAmount() : starrefAmount, .starrefCommission = paidMessagesCount ? 0 : starrefCommission, .starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId, .peerType = tl.data().vpeer().match([](const HistoryPeerTL &) { @@ -147,7 +146,7 @@ constexpr auto kTransactionsLimit = 100; .paidMessagesCount = paidMessagesCount, .paidMessagesAmount = (paidMessagesCount ? starrefAmount - : StarsAmount()), + : CreditsAmount()), .paidMessagesCommission = paidMessagesCount ? starrefCommission : 0, .starsConverted = int(nonUniqueGift ? nonUniqueGift->vconvert_stars().v @@ -216,7 +215,7 @@ constexpr auto kTransactionsLimit = 100; return Data::CreditsStatusSlice{ .list = std::move(entries), .subscriptions = std::move(subscriptions), - .balance = Data::FromTL(status.data().vbalance()), + .balance = CreditsAmountFromTL(status.data().vbalance()), .subscriptionsMissingBalance = status.data().vsubscriptions_missing_balance().value_or_empty(), .allLoaded = !status.data().vnext_offset().has_value() @@ -300,11 +299,14 @@ void CreditsStatus::request( using TLResult = MTPpayments_StarsStatus; _requestId = _api.request(MTPpayments_GetStarsStatus( + MTP_flags(0), _peer->isSelf() ? MTP_inputPeerSelf() : _peer->input )).done([=](const TLResult &result) { _requestId = 0; const auto &balance = result.data().vbalance(); - _peer->session().credits().apply(_peer->id, Data::FromTL(balance)); + _peer->session().credits().apply( + _peer->id, + CreditsAmountFromTL(balance)); if (const auto onstack = done) { onstack(StatusFromTL(result, _peer)); } @@ -420,13 +422,15 @@ rpl::producer<rpl::no_value, QString> CreditsEarnStatistics::request() { )).done([=](const MTPpayments_StarsRevenueStats &result) { const auto &data = result.data(); const auto &status = data.vstatus().data(); - using Data::FromTL; _data = Data::CreditsEarnStatistics{ .revenueGraph = StatisticalGraphFromTL( data.vrevenue_graph()), - .currentBalance = FromTL(status.vcurrent_balance()), - .availableBalance = FromTL(status.vavailable_balance()), - .overallRevenue = FromTL(status.voverall_revenue()), + .currentBalance = CreditsAmountFromTL( + status.vcurrent_balance()), + .availableBalance = CreditsAmountFromTL( + status.vavailable_balance()), + .overallRevenue = CreditsAmountFromTL( + status.voverall_revenue()), .usdRate = data.vusd_rate().v, .isWithdrawalEnabled = status.is_withdrawal_enabled(), .nextWithdrawalAt = status.vnext_withdrawal_at() diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index a6a1f09379..b6a04474d3 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -220,7 +220,9 @@ void SendSuggest( auto action = SendAction(item->history()); action.options.suggest.exists = 1; action.options.suggest.date = suggestion->date; - action.options.suggest.stars = suggestion->stars; + action.options.suggest.priceWhole = suggestion->price.whole(); + action.options.suggest.priceNano = suggestion->price.nano(); + action.options.suggest.ton = suggestion->price.ton() ? 1 : 0; action.options.starsApproved = starsApproved; action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin() ? item->sublistPeerId() @@ -308,8 +310,10 @@ void SuggestApprovalPrice( .session = &controller->session(), .done = done, .value = { - .exists = true, - .stars = uint32(suggestion->stars), + .exists = uint32(1), + .priceWhole = uint32(suggestion->price.whole()), + .priceNano = uint32(suggestion->price.nano()), + .ton = uint32(suggestion->price.ton() ? 1 : 0), .date = suggestion->date, }, .mode = SuggestMode::Change, @@ -403,8 +407,10 @@ std::shared_ptr<ClickHandler> SuggestChangesClickHandler( .monoforumPeerId = monoforumPeerId, }, SuggestPostOptions{ - .exists = 1, - .stars = uint32(suggestion->stars), + .exists = uint32(1), + .priceWhole = uint32(suggestion->price.whole()), + .priceNano = uint32(suggestion->price.nano()), + .ton = uint32(suggestion->price.ton() ? 1 : 0), .date = suggestion->date, }, cursor, diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index d2a19a7420..7b0c538850 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2756,6 +2756,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->credits().apply(data); } break; + case mtpc_updateStarsTonBalance: { + const auto &data = update.c_updateStarsBalance(); + _session->credits().apply(data); + } break; + case mtpc_updatePaidReactionPrivacy: { const auto &data = update.c_updatePaidReactionPrivacy(); _session->api().globalPrivacy().updatePaidReactionShownPeer( diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index 761b387e53..8df343cec4 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -99,7 +99,7 @@ void GiftCreditsBox( Main::MakeSessionShow(box->uiShow(), &peer->session()), box->verticalLayout(), peer, - StarsAmount(), + CreditsAmount(), [=] { gifted(); box->uiShow()->hideLayer(); }, tr::lng_credits_summary_options_subtitle(), {}); diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index cb5dee6e26..0b898a86ee 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -411,7 +411,7 @@ void AddTableRow( table->st().defaultValue.style.font->height); const auto label = Ui::CreateChild<Ui::FlatLabel>( raw, - Lang::FormatStarsAmountDecimal(entry.credits), + Lang::FormatCreditsAmountDecimal(entry.credits), table->st().defaultValue, st::defaultPopupMenu); @@ -1630,8 +1630,8 @@ void AddCreditsHistoryEntryTable( const auto full = int(base::SafeRound(entry.credits.value() / (1. - (entry.starrefCommission / 1000.)))); auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored); - const auto starsText = Lang::FormatStarsAmountDecimal( - StarsAmount{ full }); + const auto starsText = Lang::FormatCreditsAmountDecimal( + CreditsAmount{ full }); AddTableRow( table, tr::lng_credits_box_history_entry_gift_full_price(), @@ -1787,7 +1787,7 @@ void AddCreditsHistoryEntryTable( auto value = Ui::Text::IconEmoji(&st::starIconEmojiColored); const auto full = (entry.in ? 1 : -1) * (entry.credits + entry.paidMessagesAmount); - const auto starsText = Lang::FormatStarsAmountDecimal(full); + const auto starsText = Lang::FormatCreditsAmountDecimal(full); AddTableRow( table, tr::lng_credits_paid_messages_full(), diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 2cae756a63..16c8cae875 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -1095,8 +1095,8 @@ void Controller::fillDirectMessagesButton() { : rpl::single(Ui::Text::IconEmoji( &st::starIconEmojiColored ).append(' ').append( - Lang::FormatStarsAmountDecimal( - StarsAmount{ starsPerMessage }))); + Lang::FormatCreditsAmountDecimal( + CreditsAmount{ starsPerMessage }))); }) | rpl::flatten_latest(); AddButtonWithText( _controls.buttonsLayout, @@ -1907,7 +1907,7 @@ void Controller::fillBotCreditsButton() { auto &lifetime = _controls.buttonsLayout->lifetime(); const auto state = lifetime.make_state<State>(); if (const auto balance = _peer->session().credits().balance(_peer->id)) { - state->balance = Lang::FormatStarsAmountDecimal(balance); + state->balance = Lang::FormatCreditsAmountDecimal(balance); } const auto wrap = _controls.buttonsLayout->add( @@ -1932,7 +1932,7 @@ void Controller::fillBotCreditsButton() { if (data.balance) { wrap->toggle(true, anim::type::normal); } - state->balance = Lang::FormatStarsAmountDecimal(data.balance); + state->balance = Lang::FormatCreditsAmountDecimal(data.balance); }); } { diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index d021cba69f..1a6827043b 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -2007,7 +2007,7 @@ void SoldOutBox( Data::CreditsHistoryEntry{ .firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate), .lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate), - .credits = StarsAmount(gift.info.stars), + .credits = CreditsAmount(gift.info.stars), .bareGiftStickerId = gift.info.document->id, .peerType = Data::CreditsHistoryEntry::PeerType::Peer, .limitedCount = gift.info.limitedCount, @@ -2039,8 +2039,8 @@ void AddUpgradeButton( tr::lng_gift_send_unique( lt_price, rpl::single(star.append(' ' - + Lang::FormatStarsAmountDecimal( - StarsAmount{ cost }))), + + Lang::FormatCreditsAmountDecimal( + CreditsAmount{ cost }))), Text::WithEntities), st::boxLabel, st::defaultPopupMenu, @@ -2355,9 +2355,9 @@ void SendGiftBox( tr::lng_gift_send_stars_balance( lt_amount, peer->session().credits().balanceValue( - ) | rpl::map([=](StarsAmount amount) { + ) | rpl::map([=](CreditsAmount amount) { return base::duplicate(star).append( - Lang::FormatStarsAmountDecimal(amount)); + Lang::FormatCreditsAmountDecimal(amount)); }), lt_link, tr::lng_gift_send_stars_balance_link( @@ -4614,8 +4614,8 @@ void UpgradeBox( ? tr::lng_gift_upgrade_button( lt_price, rpl::single(star.append( - ' ' + Lang::FormatStarsAmountDecimal( - StarsAmount{ cost }))), + ' ' + Lang::FormatCreditsAmountDecimal( + CreditsAmount{ cost }))), Ui::Text::WithEntities) : tr::lng_gift_upgrade_confirm(Ui::Text::WithEntities)), &controller->session(), diff --git a/Telegram/SourceFiles/core/credits_amount.h b/Telegram/SourceFiles/core/credits_amount.h new file mode 100644 index 0000000000..1704e28161 --- /dev/null +++ b/Telegram/SourceFiles/core/credits_amount.h @@ -0,0 +1,155 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/basic_types.h" + +class MTPstarsAmount; + +namespace tl { +template <typename bare> +class boxed; +} // namespace tl + +using MTPStarsAmount = tl::boxed<MTPstarsAmount>; + +inline constexpr auto kOneStarInNano = int64(1'000'000'000); + +enum class CreditsType { + Stars, + Ton, +}; + +class CreditsAmount { +public: + CreditsAmount() = default; + explicit CreditsAmount( + int64 whole, + CreditsType type = CreditsType::Stars) + : _ton((type == CreditsType::Ton) ? 1 : 0) + , _whole(whole) { + } + CreditsAmount( + int64 whole, + int64 nano, + CreditsType type = CreditsType::Stars) + : _ton((type == CreditsType::Ton) ? 1 : 0) + , _whole(whole) + , _nano(nano) { + normalize(); + } + + [[nodiscard]] int64 whole() const { + return _whole; + } + + [[nodiscard]] int64 nano() const { + return _nano; + } + + [[nodiscard]] double value() const { + return double(_whole) + double(_nano) / kOneStarInNano; + } + + [[nodiscard]] bool ton() const { + return (_ton == 1); + } + [[nodiscard]] bool stars() const { + return (_ton == 0); + } + [[nodiscard]] CreditsType type() const { + return !_ton ? CreditsType::Stars : CreditsType::Ton; + } + + [[nodiscard]] bool empty() const { + return !_whole && !_nano; + } + + [[nodiscard]] inline bool operator!() const { + return empty(); + } + [[nodiscard]] inline explicit operator bool() const { + return !empty(); + } + + inline CreditsAmount &operator+=(CreditsAmount other) { + _whole += other._whole; + _nano += other._nano; + normalize(); + return *this; + } + inline CreditsAmount &operator-=(CreditsAmount other) { + _whole -= other._whole; + _nano -= other._nano; + normalize(); + return *this; + } + inline CreditsAmount &operator*=(int64 multiplier) { + _whole *= multiplier; + _nano *= multiplier; + normalize(); + return *this; + } + inline CreditsAmount operator-() const { + auto result = *this; + result *= -1; + return result; + } + + friend inline auto operator<=>(CreditsAmount, CreditsAmount) = default; + friend inline bool operator==(CreditsAmount, CreditsAmount) = default; + + [[nodiscard]] CreditsAmount abs() const { + return (_whole < 0) ? CreditsAmount(-_whole, -_nano) : *this; + } + +private: + void normalize() { + if (_nano < 0) { + const auto shifts = (-_nano + kOneStarInNano - 1) + / kOneStarInNano; + _nano += shifts * kOneStarInNano; + _whole -= shifts; + } else if (_nano >= kOneStarInNano) { + const auto shifts = _nano / kOneStarInNano; + _nano -= shifts * kOneStarInNano; + _whole += shifts; + } + } + + int64 _ton : 2 = 0; + int64 _whole : 62 = 0; + int64 _nano = 0; + +}; + +[[nodiscard]] inline CreditsAmount operator+( + CreditsAmount a, + CreditsAmount b) { + return a += b; +} + +[[nodiscard]] inline CreditsAmount operator-( + CreditsAmount a, + CreditsAmount b) { + return a -= b; +} + +[[nodiscard]] inline CreditsAmount operator*(CreditsAmount a, int64 b) { + return a *= b; +} + +[[nodiscard]] inline CreditsAmount operator*(int64 a, CreditsAmount b) { + return b *= a; +} + +[[nodiscard]] CreditsAmount CreditsAmountFromTL( + const MTPStarsAmount &amount); +[[nodiscard]] CreditsAmount CreditsAmountFromTL( + const MTPStarsAmount *amount); +[[nodiscard]] MTPStarsAmount StarsAmountToTL(CreditsAmount amount); diff --git a/Telegram/SourceFiles/core/stars_amount.h b/Telegram/SourceFiles/core/stars_amount.h deleted file mode 100644 index ca0e6fbe2d..0000000000 --- a/Telegram/SourceFiles/core/stars_amount.h +++ /dev/null @@ -1,108 +0,0 @@ -/* -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/basic_types.h" - -inline constexpr auto kOneStarInNano = int64(1'000'000'000); - -class StarsAmount { -public: - StarsAmount() = default; - explicit StarsAmount(int64 whole) : _whole(whole) {} - StarsAmount(int64 whole, int64 nano) : _whole(whole), _nano(nano) { - normalize(); - } - - [[nodiscard]] int64 whole() const { - return _whole; - } - - [[nodiscard]] int64 nano() const { - return _nano; - } - - [[nodiscard]] double value() const { - return double(_whole) + double(_nano) / kOneStarInNano; - } - - [[nodiscard]] bool empty() const { - return !_whole && !_nano; - } - - [[nodiscard]] inline bool operator!() const { - return empty(); - } - [[nodiscard]] inline explicit operator bool() const { - return !empty(); - } - - inline StarsAmount &operator+=(StarsAmount other) { - _whole += other._whole; - _nano += other._nano; - normalize(); - return *this; - } - inline StarsAmount &operator-=(StarsAmount other) { - _whole -= other._whole; - _nano -= other._nano; - normalize(); - return *this; - } - inline StarsAmount &operator*=(int64 multiplier) { - _whole *= multiplier; - _nano *= multiplier; - normalize(); - return *this; - } - inline StarsAmount operator-() const { - auto result = *this; - result *= -1; - return result; - } - - friend inline auto operator<=>(StarsAmount, StarsAmount) = default; - friend inline bool operator==(StarsAmount, StarsAmount) = default; - - [[nodiscard]] StarsAmount abs() const { - return (_whole < 0) ? StarsAmount(-_whole, -_nano) : *this; - } - -private: - int64 _whole = 0; - int64 _nano = 0; - - void normalize() { - if (_nano < 0) { - const auto shifts = (-_nano + kOneStarInNano - 1) - / kOneStarInNano; - _nano += shifts * kOneStarInNano; - _whole -= shifts; - } else if (_nano >= kOneStarInNano) { - const auto shifts = _nano / kOneStarInNano; - _nano -= shifts * kOneStarInNano; - _whole += shifts; - } - } -}; - -[[nodiscard]] inline StarsAmount operator+(StarsAmount a, StarsAmount b) { - return a += b; -} - -[[nodiscard]] inline StarsAmount operator-(StarsAmount a, StarsAmount b) { - return a -= b; -} - -[[nodiscard]] inline StarsAmount operator*(StarsAmount a, int64 b) { - return a *= b; -} - -[[nodiscard]] inline StarsAmount operator*(int64 a, StarsAmount b) { - return b *= a; -} diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 119847e0fc..5f93ba303c 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -19,11 +19,6 @@ constexpr auto kReloadThreshold = 60 * crl::time(1000); } // namespace -StarsAmount FromTL(const MTPStarsAmount &value) { - const auto &data = value.data(); - return StarsAmount(data.vamount().v, data.vnanos().v); -} - Credits::Credits(not_null<Main::Session*> session) : _session(session) , _reload([=] { load(true); }) { @@ -32,7 +27,7 @@ Credits::Credits(not_null<Main::Session*> session) Credits::~Credits() = default; void Credits::apply(const MTPDupdateStarsBalance &data) { - apply(FromTL(data.vbalance())); + apply(CreditsAmountFromTL(data.vbalance())); } rpl::producer<float64> Credits::rateValue( @@ -80,13 +75,13 @@ rpl::producer<bool> Credits::loadedValue() const { ) | rpl::then(_loadedChanges.events() | rpl::map_to(true)); } -StarsAmount Credits::balance() const { +CreditsAmount Credits::balance() const { return _nonLockedBalance.current(); } -StarsAmount Credits::balance(PeerId peerId) const { +CreditsAmount Credits::balance(PeerId peerId) const { const auto it = _cachedPeerBalances.find(peerId); - return (it != _cachedPeerBalances.end()) ? it->second : StarsAmount(); + return (it != _cachedPeerBalances.end()) ? it->second : CreditsAmount(); } uint64 Credits::balanceCurrency(PeerId peerId) const { @@ -94,19 +89,19 @@ uint64 Credits::balanceCurrency(PeerId peerId) const { return (it != _cachedPeerCurrencyBalances.end()) ? it->second : 0; } -rpl::producer<StarsAmount> Credits::balanceValue() const { +rpl::producer<CreditsAmount> Credits::balanceValue() const { return _nonLockedBalance.value(); } void Credits::updateNonLockedValue() { _nonLockedBalance = (_balance >= _locked) ? (_balance - _locked) - : StarsAmount(); + : CreditsAmount(); } -void Credits::lock(StarsAmount count) { +void Credits::lock(CreditsAmount count) { Expects(loaded()); - Expects(count >= StarsAmount(0)); + Expects(count >= CreditsAmount(0)); Expects(_locked + count <= _balance); _locked += count; @@ -114,8 +109,8 @@ void Credits::lock(StarsAmount count) { updateNonLockedValue(); } -void Credits::unlock(StarsAmount count) { - Expects(count >= StarsAmount(0)); +void Credits::unlock(CreditsAmount count) { + Expects(count >= CreditsAmount(0)); Expects(_locked >= count); _locked -= count; @@ -123,12 +118,12 @@ void Credits::unlock(StarsAmount count) { updateNonLockedValue(); } -void Credits::withdrawLocked(StarsAmount count) { - Expects(count >= StarsAmount(0)); +void Credits::withdrawLocked(CreditsAmount count) { + Expects(count >= CreditsAmount(0)); Expects(_locked >= count); _locked -= count; - apply(_balance >= count ? (_balance - count) : StarsAmount(0)); + apply(_balance >= count ? (_balance - count) : CreditsAmount(0)); invalidate(); } @@ -136,7 +131,7 @@ void Credits::invalidate() { _reload.call(); } -void Credits::apply(StarsAmount balance) { +void Credits::apply(CreditsAmount balance) { _balance = balance; updateNonLockedValue(); @@ -146,7 +141,7 @@ void Credits::apply(StarsAmount balance) { } } -void Credits::apply(PeerId peerId, StarsAmount balance) { +void Credits::apply(PeerId peerId, CreditsAmount balance) { _cachedPeerBalances[peerId] = balance; _refreshedByPeerId.fire_copy(peerId); } @@ -166,3 +161,27 @@ bool Credits::statsEnabled() const { } } // namespace Data + +CreditsAmount CreditsAmountFromTL(const MTPStarsAmount &amount) { + return amount.match([&](const MTPDstarsAmount &data) { + return CreditsAmount( + data.vamount().v, + data.vnanos().v, + CreditsType::Stars); + }, [&](const MTPDstarsTonAmount &data) { + return CreditsAmount( + data.vamount().v / uint64(1'000'000'000), + data.vamount().v % uint64(1'000'000'000), + CreditsType::Ton); + }); +} + +CreditsAmount CreditsAmountFromTL(const MTPStarsAmount *amount) { + return amount ? CreditsAmountFromTL(*amount) : CreditsAmount(); +} + +MTPStarsAmount StarsAmountToTL(CreditsAmount amount) { + return amount.ton() ? MTP_starsTonAmount( + MTP_long(amount.whole() * uint64(1'000'000'000) + amount.nano()) + ) : MTP_starsAmount(MTP_long(amount.whole()), MTP_int(amount.nano())); +} diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h index dac6df8981..000df652e3 100644 --- a/Telegram/SourceFiles/data/components/credits.h +++ b/Telegram/SourceFiles/data/components/credits.h @@ -13,23 +13,21 @@ class Session; namespace Data { -[[nodiscard]] StarsAmount FromTL(const MTPStarsAmount &value); - class Credits final { public: explicit Credits(not_null<Main::Session*> session); ~Credits(); void load(bool force = false); - void apply(StarsAmount balance); - void apply(PeerId peerId, StarsAmount balance); + void apply(CreditsAmount balance); + void apply(PeerId peerId, CreditsAmount balance); [[nodiscard]] bool loaded() const; [[nodiscard]] rpl::producer<bool> loadedValue() const; - [[nodiscard]] StarsAmount balance() const; - [[nodiscard]] StarsAmount balance(PeerId peerId) const; - [[nodiscard]] rpl::producer<StarsAmount> balanceValue() const; + [[nodiscard]] CreditsAmount balance() const; + [[nodiscard]] CreditsAmount balance(PeerId peerId) const; + [[nodiscard]] rpl::producer<CreditsAmount> balanceValue() const; [[nodiscard]] rpl::producer<float64> rateValue( not_null<PeerData*> ownedBotOrChannel); @@ -40,9 +38,9 @@ public: void applyCurrency(PeerId peerId, uint64 balance); [[nodiscard]] uint64 balanceCurrency(PeerId peerId) const; - void lock(StarsAmount count); - void unlock(StarsAmount count); - void withdrawLocked(StarsAmount count); + void lock(CreditsAmount count); + void unlock(CreditsAmount count); + void withdrawLocked(CreditsAmount count); void invalidate(); void apply(const MTPDupdateStarsBalance &data); @@ -54,12 +52,12 @@ private: std::unique_ptr<rpl::lifetime> _loader; - base::flat_map<PeerId, StarsAmount> _cachedPeerBalances; + base::flat_map<PeerId, CreditsAmount> _cachedPeerBalances; base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances; - StarsAmount _balance; - StarsAmount _locked; - rpl::variable<StarsAmount> _nonLockedBalance; + CreditsAmount _balance; + CreditsAmount _locked; + rpl::variable<CreditsAmount> _nonLockedBalance; rpl::event_stream<> _loadedChanges; crl::time _lastLoaded = 0; float64 _rate = 0.; diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index 0432097980..3a3c7e77f3 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -59,7 +59,7 @@ struct CreditsHistoryEntry final { QDateTime lastSaleDate; PhotoId photoId = 0; std::vector<CreditsHistoryMedia> extended; - StarsAmount credits; + CreditsAmount credits; uint64 bareMsgId = 0; uint64 barePeerId = 0; uint64 bareGiveawayMsgId = 0; @@ -72,7 +72,7 @@ struct CreditsHistoryEntry final { uint64 stargiftId = 0; std::shared_ptr<UniqueGift> uniqueGift; Fn<std::vector<CreditsHistoryEntry>()> pinnedSavedGifts; - StarsAmount starrefAmount; + CreditsAmount starrefAmount; int starrefCommission = 0; uint64 starrefRecipientId = 0; PeerType peerType; @@ -80,7 +80,7 @@ struct CreditsHistoryEntry final { QDateTime successDate; QString successLink; int paidMessagesCount = 0; - StarsAmount paidMessagesAmount; + CreditsAmount paidMessagesAmount; int paidMessagesCommission = 0; int limitedCount = 0; int limitedLeft = 0; @@ -115,7 +115,7 @@ struct CreditsStatusSlice final { using OffsetToken = QString; std::vector<CreditsHistoryEntry> list; std::vector<SubscriptionEntry> subscriptions; - StarsAmount balance; + CreditsAmount balance; uint64 subscriptionsMissingBalance = 0; bool allLoaded = false; OffsetToken token; diff --git a/Telegram/SourceFiles/data/data_credits_earn.h b/Telegram/SourceFiles/data/data_credits_earn.h index e26e2bebc0..af1d840c72 100644 --- a/Telegram/SourceFiles/data/data_credits_earn.h +++ b/Telegram/SourceFiles/data/data_credits_earn.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "data/data_statistics_chart.h" #include <QtCore/QDateTime> @@ -22,9 +22,9 @@ struct CreditsEarnStatistics final { && overallRevenue; } Data::StatisticalGraph revenueGraph; - StarsAmount currentBalance; - StarsAmount availableBalance; - StarsAmount overallRevenue; + CreditsAmount currentBalance; + CreditsAmount availableBalance; + CreditsAmount overallRevenue; float64 usdRate = 0.; bool isWithdrawalEnabled = false; QDateTime nextWithdrawalAt; diff --git a/Telegram/SourceFiles/data/data_drafts.cpp b/Telegram/SourceFiles/data/data_drafts.cpp index 5a61b486e8..9aae0e8bb0 100644 --- a/Telegram/SourceFiles/data/data_drafts.cpp +++ b/Telegram/SourceFiles/data/data_drafts.cpp @@ -21,11 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" namespace Data { -namespace { - -constexpr auto kMaxSuggestStars = 1'000'000'000; - -} // namespace WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) { const auto previewMedia = item->media(); @@ -122,10 +117,10 @@ void ApplyPeerCloudDraft( const auto &data = suggested->data(); suggest.exists = 1; suggest.date = data.vschedule_date().value_or_empty(); - suggest.stars = uint32(std::clamp( - data.vstars_amount().v, - uint64(), - uint64(kMaxSuggestStars))); + const auto price = CreditsAmountFromTL(data.vprice()); + suggest.priceWhole = price.whole(); + suggest.priceNano = price.nano(); + suggest.ton = price.ton() ? 1 : 0; } auto cloudDraft = std::make_unique<Draft>( textWithTags, diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index e4fc70bf56..3fb17b5c44 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -2514,7 +2514,7 @@ MediaGiftBox::MediaGiftBox( not_null<HistoryItem*> parent, not_null<PeerData*> from, GiftType type, - int count) + int64 count) : MediaGiftBox(parent, from, GiftCode{ .count = count, .type = type }) { } diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 7f0e172306..dab3e57b2e 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -136,6 +136,7 @@ struct GiveawayResults { enum class GiftType : uchar { Premium, // count - months Credits, // count - credits + Ton, // count - nano tons StarGift, // count - stars }; @@ -155,7 +156,7 @@ struct GiftCode { int starsUpgradedBySender = 0; int limitedCount = 0; int limitedLeft = 0; - int count = 0; + int64 count = 0; GiftType type = GiftType::Premium; bool viaGiveaway : 1 = false; bool transferred : 1 = false; @@ -678,7 +679,7 @@ public: not_null<HistoryItem*> parent, not_null<PeerData*> from, GiftType type, - int count); + int64 count); MediaGiftBox( not_null<HistoryItem*> parent, not_null<PeerData*> from, diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index fec98a7920..a909a2c4cd 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -2245,7 +2245,7 @@ void MessageReactions::scheduleSendPaid( _paid->scheduledPrivacySet = true; } if (count > 0) { - _item->history()->session().credits().lock(StarsAmount(count)); + _item->history()->session().credits().lock(CreditsAmount(count)); } _item->history()->owner().reactions().schedulePaid(_item); } @@ -2259,7 +2259,7 @@ void MessageReactions::cancelScheduledPaid() { if (_paid->scheduledFlag) { if (const auto amount = int(_paid->scheduled)) { _item->history()->session().credits().unlock( - StarsAmount(amount)); + CreditsAmount(amount)); } _paid->scheduled = 0; _paid->scheduledFlag = 0; @@ -2322,9 +2322,9 @@ void MessageReactions::finishPaidSending( if (const auto amount = send.count) { const auto credits = &_item->history()->session().credits(); if (success) { - credits->withdrawLocked(StarsAmount(amount)); + credits->withdrawLocked(CreditsAmount(amount)); } else { - credits->unlock(StarsAmount(amount)); + credits->unlock(CreditsAmount(amount)); } } } diff --git a/Telegram/SourceFiles/data/data_msg_id.h b/Telegram/SourceFiles/data/data_msg_id.h index f093a6f2ef..bee8324194 100644 --- a/Telegram/SourceFiles/data/data_msg_id.h +++ b/Telegram/SourceFiles/data/data_msg_id.h @@ -192,9 +192,18 @@ struct FullReplyTo { struct SuggestPostOptions { uint32 exists : 1 = 0; - uint32 stars : 31 = 0; + uint32 priceWhole : 31 = 0; + uint32 priceNano : 31 = 0; + uint32 ton : 1 = 0; TimeId date = 0; + [[nodiscard]] CreditsAmount price() const { + return CreditsAmount( + priceWhole, + priceNano, + ton ? CreditsType::Ton : CreditsType::Stars); + } + explicit operator bool() const { return exists != 0; } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index f7abd8d9f0..7dfd473d52 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -868,9 +868,8 @@ StarRefProgram ParseStarRefProgram(const MTPStarRefProgram *program) { const auto &data = program->data(); result.commission = data.vcommission_permille().v; result.durationMonths = data.vduration_months().value_or_empty(); - result.revenuePerUser = data.vdaily_revenue_per_user() - ? Data::FromTL(*data.vdaily_revenue_per_user()) - : StarsAmount(); + result.revenuePerUser = CreditsAmountFromTL( + data.vdaily_revenue_per_user()); result.endDate = data.vend_date().value_or_empty(); return result; } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 5e57eaef90..a47340132f 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "data/components/credits.h" #include "data/data_birthday.h" #include "data/data_peer.h" @@ -28,7 +28,7 @@ using DisallowedGiftTypes = base::flags<DisallowedGiftType>; } // namespace Api struct StarRefProgram { - StarsAmount revenuePerUser; + CreditsAmount revenuePerUser; TimeId endDate = 0; ushort commission = 0; uint8 durationMonths = 0; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 52d79e4079..8b7cf589c1 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1681,11 +1681,21 @@ ServiceAction ParseServiceAction( content.transactionId = data.vcharge().data().vid().v; result.content = content; }, [&](const MTPDmessageActionGiftStars &data) { - auto content = ActionGiftStars(); + auto content = ActionGiftCredits(); content.cost = Ui::FillAmountAndCurrency( data.vamount().v, qs(data.vcurrency())).toUtf8(); - content.credits = data.vstars().v; + content.amount = CreditsAmount(data.vstars().v, CreditsType::Stars); + result.content = content; + }, [&](const MTPDmessageActionGiftTon &data) { + auto content = ActionGiftCredits(); + content.cost = Ui::FillAmountAndCurrency( + data.vamount().v, + qs(data.vcurrency())).toUtf8(); + content.amount = CreditsAmount( + data.vamount().v / uint64(1'000'000'000), + data.vamount().v % uint64(1'000'000'000), + CreditsType::Ton); result.content = content; }, [&](const MTPDmessageActionPrizeStars &data) { result.content = ActionPrizeStars{ @@ -1761,7 +1771,7 @@ ServiceAction ParseServiceAction( result.content = ActionSuggestedPostApproval{ .rejectComment = data.vreject_comment().value_or_empty(), .scheduleDate = data.vschedule_date().value_or_empty(), - .stars = int(data.vstars_amount().value_or_empty()), + .price = CreditsAmountFromTL(data.vprice()), .rejected = data.is_rejected(), .balanceTooLow = data.is_balance_too_low(), }; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 38ba0d0cfe..805e2d7682 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "scheme.h" #include "base/optional.h" #include "base/variant.h" +#include "core/credits_amount.h" #include "data/data_peer_id.h" #include <QtCore/QSize> @@ -658,9 +659,9 @@ struct ActionPaymentRefunded { Utf8String transactionId; }; -struct ActionGiftStars { +struct ActionGiftCredits { Utf8String cost; - int credits = 0; + CreditsAmount amount; }; struct ActionPrizeStars { @@ -701,7 +702,7 @@ struct ActionTodoAppendTasks { struct ActionSuggestedPostApproval { Utf8String rejectComment; TimeId scheduleDate = 0; - int stars = 0; + CreditsAmount price; bool rejected = false; bool balanceTooLow = false; }; @@ -749,7 +750,7 @@ struct ServiceAction { ActionGiveawayResults, ActionBoostApply, ActionPaymentRefunded, - ActionGiftStars, + ActionGiftCredits, ActionPrizeStars, ActionStarGift, ActionPaidMessagesRefunded, diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index df2278f53c..5ada50c0bb 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1353,16 +1353,16 @@ auto HtmlWriter::Wrap::pushMessage( + " refunded back " + amount; return result; - }, [&](const ActionGiftStars &data) { - if (!data.credits || data.cost.isEmpty()) { + }, [&](const ActionGiftCredits &data) { + if (!data.amount || data.cost.isEmpty()) { return serviceFrom + " sent you a gift."; } return serviceFrom + " sent you a gift for " + data.cost + ": " - + QString::number(data.credits).toUtf8() - + " Telegram Stars."; + + QString::number(data.amount.value()).toUtf8() + + (data.amount.ton() ? " TON." : " Telegram Stars."); }, [&](const ActionPrizeStars &data) { return "You won a prize in a giveaway organized by " + peers.wrapPeerName(data.peerId) @@ -1450,8 +1450,10 @@ auto HtmlWriter::Wrap::pushMessage( return serviceFrom + (data.rejected ? " rejected " : " approved ") + "your suggested post" - + (data.stars - ? ", for " + QString::number(data.stars).toUtf8() + " stars" + + (data.price + ? (", for " + + QString::number(data.price.value()).toUtf8() + + (data.price.ton() ? " TON" : " stars")) : "") + (data.scheduleDate ? (", " diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index 7eb927f866..b1317a9103 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -644,14 +644,17 @@ QByteArray SerializeMessage( pushBare("peer_name", wrapPeerName(data.peerId)); push("peer_id", data.peerId); push("charge_id", data.transactionId); - }, [&](const ActionGiftStars &data) { + }, [&](const ActionGiftCredits &data) { pushActor(); - pushAction("send_stars_gift"); + pushAction(data.amount.ton() + ? "send_ton_gift" + : "send_stars_gift"); if (!data.cost.isEmpty()) { push("cost", data.cost); } - if (data.credits) { - push("stars", data.credits); + if (data.amount) { + push("amount_whole", data.amount.whole()); + push("amount_nano", data.amount.nano()); } }, [&](const ActionPrizeStars &data) { pushActor(); @@ -717,7 +720,9 @@ QByteArray SerializeMessage( push("comment", data.rejectComment); } } else { - push("stars_amount", NumberToString(data.stars)); + push("price_amount_whole", NumberToString(data.price.whole())); + push("price_amount_nano", NumberToString(data.price.nano())); + push("price_currency", data.price.ton() ? "TON" : "Stars"); push("scheduled_date", data.scheduleDate); } }, [](v::null_t) {}); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index eb4e1fa1d3..4eff615c20 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1949,7 +1949,7 @@ void HistoryItem::applyEdition(HistoryMessageEdition &&edition) { AddComponents(HistoryMessageSuggestedPost::Bit()); } auto suggest = Get<HistoryMessageSuggestedPost>(); - suggest->stars = edition.suggest.stars; + suggest->price = edition.suggest.price; suggest->date = edition.suggest.date; suggest->accepted = edition.suggest.accepted; suggest->rejected = edition.suggest.rejected; @@ -4023,7 +4023,7 @@ void HistoryItem::createComponents(CreateConfig &&config) { } if (const auto suggest = Get<HistoryMessageSuggestedPost>()) { - suggest->stars = config.suggest.stars; + suggest->price = config.suggest.price; suggest->date = config.suggest.date; suggest->accepted = config.suggest.accepted; suggest->rejected = config.suggest.rejected; @@ -4668,7 +4668,7 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { const auto &data = action.c_messageActionSuggestedPostApproval(); UpdateComponents(HistoryServiceSuggestDecision::Bit()); const auto decision = Get<HistoryServiceSuggestDecision>(); - decision->stars = data.vstars_amount().value_or_empty(); + decision->price = CreditsAmountFromTL(data.vprice()); decision->balanceTooLow = data.is_balance_too_low(); decision->rejected = data.is_rejected(); decision->rejectComment = qs(data.vreject_comment().value_or_empty()); @@ -5735,6 +5735,42 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { return result; }; + auto prepareGiftTon = [&]( + const MTPDmessageActionGiftTon &action) { + auto result = PreparedServiceText(); + const auto isSelf = (_from->id == _from->session().userPeerId()); + const auto peer = isSelf ? _history->peer : _from; + const auto amount = action.vamount().v; + const auto currency = qs(action.vcurrency()); + const auto cost = AmountAndStarCurrency( + &_history->session(), + amount, + currency); + const auto anonymous = _from->isServiceUser(); + if (anonymous) { + result.text = tr::lng_action_gift_received_anonymous( + tr::now, + lt_cost, + cost, + Ui::Text::WithEntities); + } else { + result.links.push_back(peer->createOpenLink()); + result.text = isSelf + ? tr::lng_action_gift_sent(tr::now, + lt_cost, + cost, + Ui::Text::WithEntities) + : tr::lng_action_gift_received( + tr::now, + lt_user, + Ui::Text::Link(peer->shortName(), 1), // Link 1. + lt_cost, + cost, + Ui::Text::WithEntities); + } + return result; + }; + auto prepareGiftPrize = [&]( const MTPDmessageActionPrizeStars &action) { auto result = PreparedServiceText(); @@ -6073,6 +6109,7 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareBoostApply, preparePaymentRefunded, prepareGiftStars, + prepareGiftTon, prepareGiftPrize, prepareStarGift, prepareStarGiftUnique, @@ -6194,6 +6231,12 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { _from, Data::GiftType::Credits, data.vstars().v); + }, [&](const MTPDmessageActionGiftTon &data) { + _media = std::make_unique<Data::MediaGiftBox>( + this, + _from, + Data::GiftType::Ton, + data.vamount().v); }, [&](const MTPDmessageActionPrizeStars &data) { _media = std::make_unique<Data::MediaGiftBox>( this, diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 92714a2e2d..f934726c64 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -623,7 +623,7 @@ struct HistoryMessageFactcheck struct HistoryMessageSuggestedPost : RuntimeComponent<HistoryMessageSuggestedPost, HistoryItem> { - int stars = 0; + CreditsAmount price; TimeId date = 0; mtpRequestId requestId = 0; bool accepted = false; @@ -701,7 +701,7 @@ struct HistoryServiceTodoAppendTasks struct HistoryServiceSuggestDecision : RuntimeComponent<HistoryServiceSuggestDecision, HistoryItem> , HistoryServiceDependentData { - int stars = 0; + CreditsAmount price; TimeId date = 0; QString rejectComment; bool rejected = false; diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.cpp b/Telegram/SourceFiles/history/history_item_reply_markup.cpp index 4a37ca15eb..8ca15fe623 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.cpp +++ b/Telegram/SourceFiles/history/history_item_reply_markup.cpp @@ -333,7 +333,7 @@ HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( return; } const auto &fields = data->data(); - stars = fields.vstars_amount().v; + price = CreditsAmountFromTL(fields.vprice()); date = fields.vschedule_date().value_or_empty(); accepted = fields.is_accepted(); rejected = fields.is_rejected(); @@ -350,7 +350,7 @@ HistoryMessageSuggestInfo::HistoryMessageSuggestInfo( if (!options.exists) { return; } - stars = options.stars; + price = options.price(); date = options.date; exists = true; } diff --git a/Telegram/SourceFiles/history/history_item_reply_markup.h b/Telegram/SourceFiles/history/history_item_reply_markup.h index be9084211e..03740b1ea6 100644 --- a/Telegram/SourceFiles/history/history_item_reply_markup.h +++ b/Telegram/SourceFiles/history/history_item_reply_markup.h @@ -154,7 +154,7 @@ struct HistoryMessageSuggestInfo { explicit HistoryMessageSuggestInfo(const Api::SendOptions &options); explicit HistoryMessageSuggestInfo(SuggestPostOptions options); - int stars = 0; + CreditsAmount price; TimeId date = 0; bool accepted = false; bool rejected = false; diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp index f8b3b89f81..b5c8f84097 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -78,7 +78,9 @@ void ChooseSuggestPriceBox( wrap, st::editTagField, tr::lng_paid_cost_placeholder(), - args.value.stars ? QString::number(args.value.stars) : QString(), + (args.value.price() + ? QString::number(args.value.price().value()) + : QString()), limit); const auto field = owned.data(); wrap->widthValue() | rpl::start_with_next([=](int width) { @@ -137,14 +139,18 @@ void ChooseSuggestPriceBox( Ui::AddDividerText(container, tr::lng_suggest_options_date_about()); AssertIsDebug()//tr::lng_suggest_options_offer const auto save = [=] { - const auto now = uint32(field->getLastText().toULongLong()); + const auto now = field->getLastText().toDouble(); if (now > limit) { field->showError(); return; } + const auto value = CreditsAmount( + int(std::floor(now)), + int(base::SafeRound((now - std::floor(now)) * 1'000'000'000.))); args.done({ .exists = true, - .stars = now, + .priceWhole = uint32(value.whole()), + .priceNano = uint32(value.nano()), .date = state->date.current(), }); }; @@ -234,15 +240,21 @@ void SuggestOptions::updateTexts() { } TextWithEntities SuggestOptions::composeText() const { - if (!_values.stars && !_values.date) { + if (!_values.price() && !_values.date) { return tr::lng_suggest_bar_text(tr::now, Ui::Text::WithEntities); + } else if (!_values.date && _values.price().ton()) { + return tr::lng_suggest_bar_priced(AssertIsDebug() + tr::now, + lt_amount, + TextWithEntities{ Lang::FormatCreditsAmountDecimal(_values.price()) + " TON" }, + Ui::Text::WithEntities); } else if (!_values.date) { return tr::lng_suggest_bar_priced( tr::now, lt_amount, - TextWithEntities{ QString::number(_values.stars) + " stars" }, + TextWithEntities{ Lang::FormatCreditsAmountDecimal(_values.price()) + " stars" }, Ui::Text::WithEntities); - } else if (!_values.stars) { + } else if (!_values.price()) { return tr::lng_suggest_bar_dated( tr::now, lt_date, @@ -250,11 +262,21 @@ TextWithEntities SuggestOptions::composeText() const { langDateTime(base::unixtime::parse(_values.date)), }, Ui::Text::WithEntities); + } else if (_values.price().ton()) { + return tr::lng_suggest_bar_priced_dated( + tr::now, + lt_amount, + TextWithEntities{ Lang::FormatCreditsAmountDecimal(_values.price()) + " TON," }, + lt_date, + TextWithEntities{ + langDateTime(base::unixtime::parse(_values.date)), + }, + Ui::Text::WithEntities); } return tr::lng_suggest_bar_priced_dated( tr::now, lt_amount, - TextWithEntities{ QString::number(_values.stars) + " stars," }, + TextWithEntities{ Lang::FormatCreditsAmountDecimal(_values.price()) + " stars," }, lt_date, TextWithEntities{ langDateTime(base::unixtime::parse(_values.date)), diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 39679cb448..698e46bd19 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -376,6 +376,10 @@ bool PremiumGift::gift() const { return _data.slug.isEmpty() || !_data.channel; } +bool PremiumGift::tonGift() const { + return (_data.type == Data::GiftType::Ton); +} + bool PremiumGift::starGift() const { return (_data.type == Data::GiftType::StarGift); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index 86a30c094a..5b2c2d4bca 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -52,6 +52,7 @@ public: private: [[nodiscard]] bool incomingGift() const; [[nodiscard]] bool outgoingGift() const; + [[nodiscard]] bool tonGift() const; [[nodiscard]] bool starGift() const; [[nodiscard]] bool starGiftUpgrade() const; [[nodiscard]] bool gift() const; diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp index 9a81e9afa9..163278761e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -76,7 +76,7 @@ struct Changes { if (wasSuggest->date != nowSuggest->date) { result.date = true; } - if (wasSuggest->stars != nowSuggest->stars) { + if (wasSuggest->price != nowSuggest->price) { result.price = true; } const auto wasText = original->originalText(); @@ -178,7 +178,7 @@ auto GenerateSuggestDecisionMedia( fadedFg)); } } else { - const auto stars = decision->stars; + const auto price = decision->price; pushText( TextWithEntities( ).append(Emoji(kAgreement)).append(' ').append( @@ -206,12 +206,16 @@ auto GenerateSuggestDecisionMedia( date.time(), QLocale::ShortFormat))), Ui::Text::WithEntities)), - (stars + (price ? st::chatSuggestInfoMiddleMargin : st::chatSuggestInfoLastMargin)); - if (stars) { - const auto amount = Ui::Text::Bold( - tr::lng_prize_credits_amount(tr::now, lt_count, stars)); + if (price) { + const auto amount = Ui::Text::Bold(price.ton() + ? (Lang::FormatCreditsAmountDecimal(price) + u" TON"_q) + : tr::lng_prize_credits_amount( + tr::now, + lt_count_decimal, + price.value())); pushText( TextWithEntities( ).append(Emoji(kMoney)).append(' ').append( @@ -320,12 +324,14 @@ auto GenerateSuggestRequestMedia( ((changes && changes->price) ? tr::lng_suggest_change_price_label : tr::lng_suggest_action_price_label)(tr::now), - Ui::Text::Bold(suggest->stars - ? tr::lng_prize_credits_amount( + Ui::Text::Bold(!suggest->price + ? tr::lng_suggest_action_price_free(tr::now) + : suggest->price.ton() AssertIsDebug() + ? (Lang::FormatCreditsAmountDecimal(suggest->price) + u" TON"_q) + : tr::lng_prize_credits_amount( tr::now, lt_count, - suggest->stars) - : tr::lng_suggest_action_price_free(tr::now)), + suggest->price.value())), }); entries.push_back({ ((changes && changes->date) diff --git a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp index 300a067033..377cebf260 100644 --- a/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp +++ b/Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp @@ -135,8 +135,8 @@ void InnerWidget::fill() { return _state.overallRevenue; }) ); - auto valueToString = [](StarsAmount v) { - return Lang::FormatStarsAmountDecimal(v); + auto valueToString = [](CreditsAmount v) { + return Lang::FormatCreditsAmountDecimal(v); }; if (data.revenueGraph.chart) { @@ -161,7 +161,7 @@ void InnerWidget::fill() { Ui::AddSkip(container, st::channelEarnOverviewTitleSkip); const auto addOverview = [&]( - rpl::producer<StarsAmount> value, + rpl::producer<CreditsAmount> value, const tr::phrase<> &text) { const auto line = container->add( Ui::CreateSkipWidget(container, 0), @@ -177,8 +177,10 @@ void InnerWidget::fill() { line, std::move( value - ) | rpl::map([=](StarsAmount v) { - return v ? ToUsd(v, multiplier, kMinorLength) : QString(); + ) | rpl::map([=](CreditsAmount v) { + return v + ? ToUsd(v, multiplier, kMinorLength) + : QString(); }), st::channelEarnOverviewSubMinorLabel); rpl::combine( @@ -254,7 +256,7 @@ void InnerWidget::fill() { (peer()->isSelf() ? rpl::duplicate(overallBalanceValue) | rpl::type_erased() : rpl::duplicate(availableBalanceValue) - ) | rpl::map([=](StarsAmount v) { + ) | rpl::map([=](CreditsAmount v) { return v ? ToUsd(v, multiplier, kMinorLength) : QString(); })); container->resizeToWidth(container->width()); diff --git a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp index 26130b96f0..89dcda70a3 100644 --- a/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp +++ b/Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp @@ -603,7 +603,7 @@ object_ptr<Ui::BoxContent> JoinStarRefBox( const auto layout = box->verticalLayout(); const auto session = &initialRecipient->session(); auto text = Ui::Text::Colorized(Ui::CreditsEmoji(session)); - text.append(Lang::FormatStarsAmountRounded(average)); + text.append(Lang::FormatCreditsAmountRounded(average)); layout->add( object_ptr<Ui::FlatLabel>( box, diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp index 70e54d9bb7..88c7744f73 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp @@ -50,11 +50,11 @@ QString ToUsd( Data::EarnInt value, float64 rate, int afterFloat) { - return ToUsd(StarsAmount(value), rate, afterFloat); + return ToUsd(CreditsAmount(value), rate, afterFloat); } QString ToUsd( - StarsAmount value, + CreditsAmount value, float64 rate, int afterFloat) { constexpr auto kApproximately = QChar(0x2248); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h index 2f0b14848f..6750d05540 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.h @@ -18,7 +18,7 @@ namespace Info::ChannelEarn { float64 rate, int afterFloat); [[nodiscard]] QString ToUsd( - StarsAmount value, + CreditsAmount value, float64 rate, int afterFloat); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp index 3d28ab3025..bda1ca5495 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_channel_earn_list.cpp @@ -258,9 +258,9 @@ void InnerWidget::load() { } const auto &data = d.vstatus().data(); auto &e = _state.creditsEarn; - e.currentBalance = Data::FromTL(data.vcurrent_balance()); - e.availableBalance = Data::FromTL(data.vavailable_balance()); - e.overallRevenue = Data::FromTL(data.voverall_revenue()); + e.currentBalance = CreditsAmountFromTL(data.vcurrent_balance()); + e.availableBalance = CreditsAmountFromTL(data.vavailable_balance()); + e.overallRevenue = CreditsAmountFromTL(data.voverall_revenue()); e.isWithdrawalEnabled = data.is_withdrawal_enabled(); e.nextWithdrawalAt = data.vnext_withdrawal_at() ? base::unixtime::parse( @@ -395,7 +395,7 @@ void InnerWidget::fill() { //constexpr auto kApproximately = QChar(0x2248); const auto multiplier = data.usdRate; - const auto creditsToUsdMap = [=](StarsAmount c) { + const auto creditsToUsdMap = [=](CreditsAmount c) { const auto creditsMultiplier = _state.creditsEarn.usdRate * Data::kEarnMultiplier; return c ? ToUsd(c, creditsMultiplier, 0) : QString(); @@ -707,7 +707,7 @@ void InnerWidget::fill() { const auto addOverview = [&]( rpl::producer<EarnInt> currencyValue, - rpl::producer<StarsAmount> creditsValue, + rpl::producer<CreditsAmount> creditsValue, const tr::phrase<> &text, bool showCurrency, bool showCredits) { @@ -741,8 +741,10 @@ void InnerWidget::fill() { const auto creditsLabel = Ui::CreateChild<Ui::FlatLabel>( line, - rpl::duplicate(creditsValue) | rpl::map([](StarsAmount value) { - return Lang::FormatStarsAmountDecimal(value); + rpl::duplicate( + creditsValue + ) | rpl::map([](CreditsAmount value) { + return Lang::FormatCreditsAmountDecimal(value); }), st::channelEarnOverviewMajorLabel); const auto icon = Ui::CreateSingleStarWidget( @@ -761,7 +763,7 @@ void InnerWidget::fill() { int available, const QSize &size, const QSize &creditsSize, - StarsAmount credits) { + CreditsAmount credits) { const auto skip = st::channelEarnOverviewSubMinorLabelPos.x(); line->resize(line->width(), size.height()); minorLabel->moveToLeft( diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 31a9468dfb..ba4ac5c49e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -940,19 +940,20 @@ rpl::producer<uint64> AddCurrencyAction( return state->balance.value(); } -rpl::producer<StarsAmount> AddCreditsAction( +rpl::producer<CreditsAmount> AddCreditsAction( not_null<UserData*> user, not_null<Ui::VerticalLayout*> wrap, not_null<Controller*> controller) { struct State final { - rpl::variable<StarsAmount> balance; + rpl::variable<CreditsAmount> balance; }; const auto state = wrap->lifetime().make_state<State>(); const auto parentController = controller->parentController(); const auto wrapButton = AddActionButton( wrap, tr::lng_manage_peer_bot_balance_credits(), - state->balance.value() | rpl::map(rpl::mappers::_1 > StarsAmount(0)), + state->balance.value( + ) | rpl::map(rpl::mappers::_1 > CreditsAmount(0)), [=] { parentController->showSection(Info::BotEarn::Make(user)); }, nullptr); { @@ -992,7 +993,7 @@ rpl::producer<StarsAmount> AddCreditsAction( ) | rpl::start_with_next([=, &st]( int width, const QString &button, - StarsAmount balance) { + CreditsAmount balance) { const auto available = width - rect::m::sum::h(st.padding) - st.style.font->width(button) @@ -1000,7 +1001,7 @@ rpl::producer<StarsAmount> AddCreditsAction( name->setMarkedText( base::duplicate(icon) .append(QChar(' ')) - .append(Lang::FormatStarsAmountDecimal(balance)), + .append(Lang::FormatCreditsAmountDecimal(balance)), Core::TextContext({ .session = &user->session(), .repaint = [=] { name->update(); }, @@ -2404,7 +2405,7 @@ void ActionsFiller::addBalanceActions(not_null<UserData*> user) { std::move(currencyBalance), std::move(creditsBalance) ) | rpl::map((rpl::mappers::_1 > 0) - || (rpl::mappers::_2 > StarsAmount(0)))); + || (rpl::mappers::_2 > CreditsAmount(0)))); } void ActionsFiller::addInviteToGroupAction(not_null<UserData*> user) { diff --git a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp index 33a131d110..59a66d8732 100644 --- a/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp +++ b/Telegram/SourceFiles/info/statistics/info_statistics_list_controllers.cpp @@ -927,7 +927,7 @@ void CreditsRow::init() { st::semiboldTextStyle, TextWithEntities() .append(_entry.in ? QChar('+') : kMinus) - .append(Lang::FormatStarsAmountDecimal(_entry.credits.abs())) + .append(Lang::FormatCreditsAmountDecimal(_entry.credits.abs())) .append(QChar(' ')) .append(manager.creditsEmoji()), kMarkupTextOptions, diff --git a/Telegram/SourceFiles/lang/lang_tag.cpp b/Telegram/SourceFiles/lang/lang_tag.cpp index d11cb1039f..eb6f938bf8 100644 --- a/Telegram/SourceFiles/lang/lang_tag.cpp +++ b/Telegram/SourceFiles/lang/lang_tag.cpp @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "lang/lang_tag.h" -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "lang/lang_keys.h" #include "ui/text/text.h" #include "base/qt/qt_common_adapters.h" @@ -952,18 +952,18 @@ QString FormatExactCountDecimal(float64 number) { return QLocale().toString(number, 'f', QLocale::FloatingPointShortest); } -ShortenedCount FormatStarsAmountToShort(StarsAmount amount) { +ShortenedCount FormatCreditsAmountToShort(CreditsAmount amount) { const auto attempt = FormatCountToShort(amount.whole()); return attempt.shortened ? attempt : ShortenedCount{ - .string = FormatStarsAmountDecimal(amount), + .string = FormatCreditsAmountDecimal(amount), }; } -QString FormatStarsAmountDecimal(StarsAmount amount) { +QString FormatCreditsAmountDecimal(CreditsAmount amount) { return FormatExactCountDecimal(amount.value()); } -QString FormatStarsAmountRounded(StarsAmount amount) { +QString FormatCreditsAmountRounded(CreditsAmount amount) { const auto value = amount.value(); return FormatExactCountDecimal(base::SafeRound(value * 100.) / 100.); } diff --git a/Telegram/SourceFiles/lang/lang_tag.h b/Telegram/SourceFiles/lang/lang_tag.h index 1012d27714..40f3ef9972 100644 --- a/Telegram/SourceFiles/lang/lang_tag.h +++ b/Telegram/SourceFiles/lang/lang_tag.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -class StarsAmount; +class CreditsAmount; enum lngtag_count : int; @@ -29,9 +29,10 @@ struct ShortenedCount { [[nodiscard]] ShortenedCount FormatCountToShort(int64 number); [[nodiscard]] QString FormatCountDecimal(int64 number); [[nodiscard]] QString FormatExactCountDecimal(float64 number); -[[nodiscard]] ShortenedCount FormatStarsAmountToShort(StarsAmount amount); -[[nodiscard]] QString FormatStarsAmountDecimal(StarsAmount amount); -[[nodiscard]] QString FormatStarsAmountRounded(StarsAmount amount); +[[nodiscard]] ShortenedCount FormatCreditsAmountToShort( + CreditsAmount amount); +[[nodiscard]] QString FormatCreditsAmountDecimal(CreditsAmount amount); +[[nodiscard]] QString FormatCreditsAmountRounded(CreditsAmount amount); struct PluralResult { int keyShift = 0; diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 22fd24059c..54f9cd1816 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -192,7 +192,8 @@ messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction; messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction; messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction; -messageActionSuggestedPostApproval#af42ae29 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int stars_amount:flags.4?long = MessageAction; +messageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction; +messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?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; @@ -1925,6 +1926,7 @@ payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector<Connected payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector<StarRefProgram> users:Vector<User> next_offset:flags.0?string = payments.SuggestedStarRefBots; starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount; +starsTonAmount#74aee3e0 amount:long = StarsAmount; messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers; messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector<Document> = messages.FoundStickers; @@ -1994,7 +1996,7 @@ todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:fla todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion; -suggestedPost#95ee6a6d flags:# accepted:flags.1?true rejected:flags.2?true stars_amount:long schedule_date:flags.0?int = SuggestedPost; +suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost; ---functions--- @@ -2571,8 +2573,8 @@ payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; payments.getStarsTopupOptions#c00ec7d3 = Vector<StarsTopupOption>; -payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; -payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; +payments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus; +payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer = payments.StarsRevenueStats; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index a17b2dcfd4..20bebbef6e 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -631,7 +631,7 @@ void Form::processReceipt(const MTPDpayments_paymentReceiptStars &data) { ImageLocation()) : nullptr, .peerId = peerFromUser(data.vbot_id().v), - .credits = StarsAmount(data.vtotal_amount().v), + .credits = CreditsAmount(data.vtotal_amount().v), .date = data.vdate().v, }; _updates.fire(CreditsReceiptReady{ .data = receiptData }); diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 2e199e3598..598f9a23d8 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -211,7 +211,7 @@ struct CreditsReceiptData { QString description; PhotoData *photo = nullptr; PeerId peerId = PeerId(0); - StarsAmount credits; + CreditsAmount credits; TimeId date = 0; }; diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index 1786d250ce..e227c5933b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -33,7 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings { [[nodiscard]] not_null<Ui::RpWidget*> AddBalanceWidget( not_null<Ui::RpWidget*> parent, - rpl::producer<StarsAmount> balanceValue, + rpl::producer<CreditsAmount> balanceValue, bool rightAlign, rpl::producer<float64> opacityValue = nullptr); } // namespace Settings diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 1468d53baf..bada338249 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -41,7 +41,7 @@ struct PaidReactionBoxArgs { QString channel; Fn<rpl::producer<TextWithContext>(rpl::producer<int> amount)> submit; - rpl::producer<StarsAmount> balanceValue; + rpl::producer<CreditsAmount> balanceValue; Fn<void(int, uint64)> send; }; diff --git a/Telegram/SourceFiles/settings/settings_credits.cpp b/Telegram/SourceFiles/settings/settings_credits.cpp index 9fd0ed483f..d71be42094 100644 --- a/Telegram/SourceFiles/settings/settings_credits.cpp +++ b/Telegram/SourceFiles/settings/settings_credits.cpp @@ -405,7 +405,7 @@ void Credits::setupContent() { const auto balanceAmount = Ui::CreateChild<Ui::FlatLabel>( balanceLine, _controller->session().credits().balanceValue( - ) | rpl::map(Lang::FormatStarsAmountDecimal), + ) | rpl::map(Lang::FormatCreditsAmountDecimal), st::creditsSettingsBigBalance); balanceAmount->sizeValue() | rpl::start_with_next([=] { balanceLine->resize( @@ -718,7 +718,7 @@ Fn<void()> BuyStarsHandler::handler( const auto options = _api ? _api->options() : Data::CreditTopupOptions(); - const auto amount = StarsAmount(); + const auto amount = CreditsAmount(); const auto weak = Ui::MakeWeak(box); FillCreditOptions(show, inner, self, amount, [=] { if (const auto strong = weak.data()) { diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 0914fb1ad7..9acd7b6bb9 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -141,13 +141,13 @@ class Balance final public: using Ui::RpWidget::RpWidget; - void setBalance(StarsAmount balance) { + void setBalance(CreditsAmount balance) { _balance = balance; - _tooltip = Lang::FormatStarsAmountDecimal(balance); + _tooltip = Lang::FormatCreditsAmountDecimal(balance); } void enterEventHook(QEnterEvent *e) override { - if (_balance >= StarsAmount(10'000)) { + if (_balance >= CreditsAmount(10'000)) { Ui::Tooltip::Show(1000, this); } } @@ -170,7 +170,7 @@ public: private: QString _tooltip; - StarsAmount _balance; + CreditsAmount _balance; }; @@ -506,7 +506,7 @@ void FillCreditOptions( std::shared_ptr<Main::SessionShow> show, not_null<Ui::VerticalLayout*> container, not_null<PeerData*> peer, - StarsAmount minimumCredits, + CreditsAmount minimumCredits, Fn<void()> paid, rpl::producer<QString> subtitle, std::vector<Data::CreditTopupOption> preloadedTopupOptions) { @@ -552,12 +552,12 @@ void FillCreditOptions( - int(singleStarWidth * 1.5); const auto buttonHeight = st.height + rect::m::sum::v(st.padding); const auto minCredits = (!options.empty() - && (minimumCredits > StarsAmount(options.back().credits))) - ? StarsAmount() + && (minimumCredits > CreditsAmount(options.back().credits))) + ? CreditsAmount() : minimumCredits; for (auto i = 0; i < options.size(); i++) { const auto &option = options[i]; - if (StarsAmount(option.credits) < minCredits) { + if (CreditsAmount(option.credits) < minCredits) { continue; } const auto button = [&] { @@ -684,7 +684,7 @@ void FillCreditOptions( not_null<Ui::RpWidget*> AddBalanceWidget( not_null<Ui::RpWidget*> parent, - rpl::producer<StarsAmount> balanceValue, + rpl::producer<CreditsAmount> balanceValue, bool rightAlign, rpl::producer<float64> opacityValue) { struct State final { @@ -720,10 +720,10 @@ not_null<Ui::RpWidget*> AddBalanceWidget( }; std::move( balanceValue - ) | rpl::start_with_next([=](StarsAmount value) { + ) | rpl::start_with_next([=](CreditsAmount value) { state->count.setText( st::semiboldTextStyle, - Lang::FormatStarsAmountToShort(value).string); + Lang::FormatCreditsAmountToShort(value).string); balance->setBalance(value); resize(); }, balance->lifetime()); @@ -1468,7 +1468,7 @@ void GenericCreditsEntryBox( : (e.gift && !creditsHistoryStarGift) ? QString() : QString(kMinus)) - .append(Lang::FormatStarsAmountDecimal(e.credits.abs())) + .append(Lang::FormatCreditsAmountDecimal(e.credits.abs())) .append(QChar(' ')) .append(owner->customEmojiManager().creditsEmoji()); text->setMarkedText( @@ -2069,7 +2069,7 @@ void GiftedCreditsBox( ? tr::lng_credits_box_history_entry_gift_name : tr::lng_credits_box_history_entry_gift_sent)(tr::now), .date = base::unixtime::parse(date), - .credits = StarsAmount(count), + .credits = CreditsAmount(count), .bareMsgId = uint64(), .barePeerId = (anonymous ? uint64() : peer->id.value), .peerType = (anonymous ? PeerType::Fragment : PeerType::Peer), @@ -2092,7 +2092,7 @@ void CreditsPrizeBox( .title = QString(), .description = TextWithEntities(), .date = base::unixtime::parse(date), - .credits = StarsAmount(data.count), + .credits = CreditsAmount(data.count), .barePeerId = data.channel ? data.channel->id.value : 0, @@ -2115,7 +2115,7 @@ void GlobalStarGiftBox( box, show, Data::CreditsHistoryEntry{ - .credits = StarsAmount(data.stars), + .credits = CreditsAmount(data.stars), .bareGiftStickerId = data.document->id, .bareGiftOwnerId = ownerId, .bareGiftResaleRecipientId = ((resaleRecipientId != selfId) @@ -2142,7 +2142,7 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( return { .description = data.message, .date = base::unixtime::parse(data.date), - .credits = StarsAmount(data.info.stars), + .credits = CreditsAmount(data.info.stars), .bareMsgId = uint64(data.manageId.userMessageId().bare), .barePeerId = data.fromId.value, .bareGiftStickerId = data.info.document->id, @@ -2221,7 +2221,7 @@ void StarGiftViewBox( .id = data.slug, .description = data.message, .date = base::unixtime::parse(item->date()), - .credits = StarsAmount(data.count), + .credits = CreditsAmount(data.count), .bareMsgId = uint64(item->id.bare), .barePeerId = fromId.value, .bareGiftStickerId = data.document ? data.document->id : 0, @@ -2272,7 +2272,7 @@ void ShowRefundInfoBox( auto info = Data::CreditsHistoryEntry(); info.id = refund->transactionId; info.date = base::unixtime::parse(item->date()); - info.credits = StarsAmount(refund->amount); + info.credits = CreditsAmount(refund->amount); info.barePeerId = refund->peer->id.value; info.peerType = Data::CreditsHistoryEntry::PeerType::Peer; info.refunded = true; @@ -2370,7 +2370,7 @@ void SmallBalanceBox( Fn<void()> paid) { Expects(show->session().credits().loaded()); - auto credits = StarsAmount(wholeCredits); + auto credits = CreditsAmount(wholeCredits); box->setWidth(st::boxWideWidth); box->addButton(tr::lng_close(), [=] { box->closeBox(); }); @@ -2399,8 +2399,8 @@ void SmallBalanceBox( }); auto needed = show->session().credits().balanceValue( - ) | rpl::map([=](StarsAmount balance) { - return (balance < credits) ? (credits - balance) : StarsAmount(); + ) | rpl::map([=](CreditsAmount balance) { + return (balance < credits) ? (credits - balance) : CreditsAmount(); }); const auto content = [&]() -> Ui::Premium::TopBarAbstract* { return box->setPinnedToTopContent(object_ptr<Ui::Premium::TopBar>( @@ -2412,8 +2412,8 @@ void SmallBalanceBox( rpl::duplicate( needed ) | rpl::filter( - rpl::mappers::_1 > StarsAmount(0) - ) | rpl::map([](StarsAmount amount) { + rpl::mappers::_1 > CreditsAmount(0) + ) | rpl::map([](CreditsAmount amount) { return amount.value(); })), .about = (v::is<SmallBalanceSubscription>(source) @@ -2507,7 +2507,7 @@ void AddWithdrawalWidget( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, rpl::producer<QString> secondButtonUrl, - rpl::producer<StarsAmount> availableBalanceValue, + rpl::producer<CreditsAmount> availableBalanceValue, rpl::producer<QDateTime> dateValue, bool withdrawalEnabled, rpl::producer<QString> usdValue) { @@ -2522,8 +2522,8 @@ void AddWithdrawalWidget( labels, rpl::duplicate( availableBalanceValue - ) | rpl::map([](StarsAmount v) { - return Lang::FormatStarsAmountDecimal(v); + ) | rpl::map([](CreditsAmount v) { + return Lang::FormatCreditsAmountDecimal(v); }), st::channelEarnBalanceMajorLabel); const auto icon = Ui::CreateSingleStarWidget( @@ -2622,7 +2622,7 @@ void AddWithdrawalWidget( st::settingsPremiumIconStar, { 0, -st::moderateBoxExpandInnerSkip, 0, 0 }, true)); - using Balance = rpl::variable<StarsAmount>; + using Balance = rpl::variable<CreditsAmount>; const auto currentBalance = input->lifetime().make_state<Balance>( rpl::duplicate(availableBalanceValue)); const auto process = [=] { @@ -2834,7 +2834,7 @@ void MaybeRequestBalanceIncrease( state->lifetime.destroy(); const auto balance = session->credits().balance(); - if (StarsAmount(credits) <= balance) { + if (CreditsAmount(credits) <= balance) { if (const auto onstack = done) { onstack(SmallBalanceResult::Already); } diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index a1bf02a4bd..ceea526d12 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -72,14 +72,14 @@ void FillCreditOptions( std::shared_ptr<Main::SessionShow> show, not_null<Ui::VerticalLayout*> container, not_null<PeerData*> peer, - StarsAmount minCredits, + CreditsAmount minCredits, Fn<void()> paid, rpl::producer<QString> subtitle, std::vector<Data::CreditTopupOption> preloadedTopupOptions); [[nodiscard]] not_null<Ui::RpWidget*> AddBalanceWidget( not_null<Ui::RpWidget*> parent, - rpl::producer<StarsAmount> balanceValue, + rpl::producer<CreditsAmount> balanceValue, bool rightAlign, rpl::producer<float64> opacityValue = nullptr); @@ -88,7 +88,7 @@ void AddWithdrawalWidget( not_null<Window::SessionController*> controller, not_null<PeerData*> peer, rpl::producer<QString> secondButtonUrl, - rpl::producer<StarsAmount> availableBalanceValue, + rpl::producer<CreditsAmount> availableBalanceValue, rpl::producer<QDateTime> dateValue, bool withdrawalEnabled, rpl::producer<QString> usdValue); diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 1d65911e3f..289eba499f 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -738,9 +738,9 @@ void SetupPremium( container, tr::lng_settings_credits(), controller->session().credits().balanceValue( - ) | rpl::map([=](StarsAmount c) { + ) | rpl::map([=](CreditsAmount c) { return c - ? Lang::FormatStarsAmountToShort(c).string + ? Lang::FormatCreditsAmountToShort(c).string : QString(); }), st::settingsButton), diff --git a/Telegram/SourceFiles/stdafx.h b/Telegram/SourceFiles/stdafx.h index 977126555b..6647362881 100644 --- a/Telegram/SourceFiles/stdafx.h +++ b/Telegram/SourceFiles/stdafx.h @@ -134,7 +134,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/palette.h" #include "styles/style_basic.h" -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "core/utils.h" #include "logs.h" #include "config.h" diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index cbcb74e24e..e39e6ba018 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -136,6 +136,33 @@ auto EmptyMessageDraftSources() return cWorkingDir() + u"tdata/tdld/"_q; } +[[nodiscard]] std::pair<quint64, quint64> SerializeSuggest( + SuggestPostOptions options) { + return { + ((quint64(options.exists) << 63) + | (quint64(quint32(options.date)))), + ((quint64(options.ton) << 63) + | (quint64(options.priceWhole) << 32) + | (quint64(options.priceNano))), + }; +} + +[[nodiscard]] SuggestPostOptions DeserializeSuggest( + std::pair<quint64, quint64> suggest) { + const auto exists = (suggest.first >> 63) ? 1 : 0; + const auto date = TimeId(uint32(suggest.first & 0xFFFF'FFFFULL)); + const auto ton = (suggest.second >> 63) ? 1 : 0; + const auto priceWhole = uint32((suggest.second >> 32) & 0x7FFF'FFFFULL); + const auto priceNano = uint32(suggest.second & 0xFFFF'FFFFULL); + return { + .exists = uint32(exists), + .priceWhole = priceWhole, + .priceNano = priceNano, + .ton = uint32(ton), + .date = date, + }; +} + } // namespace Account::Account(not_null<Main::Account*> owner, const QString &dataName) @@ -1276,7 +1303,7 @@ void Account::writeDrafts(not_null<History*> history) { + Serialize::stringSize(text.text) + TextUtilities::SerializeTagsSize(text.tags) + sizeof(qint64) + sizeof(qint64) // messageId - + sizeof(quint64) // suggest + + (sizeof(quint64) * 2) // suggest + Serialize::stringSize(webpage.url) + sizeof(qint32) // webpage.forceLargeMedia + sizeof(qint32) // webpage.forceSmallMedia @@ -1303,15 +1330,15 @@ void Account::writeDrafts(not_null<History*> history) { const TextWithTags &text, const Data::WebPageDraft &webpage, auto&&) { // cursor + const auto serialized = SerializeSuggest(suggest); data.stream << key.serialize() << text.text << TextUtilities::SerializeTags(text.tags) << qint64(reply.messageId.peer.value) << qint64(reply.messageId.msg.bare) - << quint64(quint64(quint32(suggest.date)) - | (quint64(suggest.stars) << 32) - | (quint64(suggest.exists) << 63)) + << serialized.first + << serialized.second << webpage.url << qint32(webpage.forceLargeMedia ? 1 : 0) << qint32(webpage.forceSmallMedia ? 1 : 0) @@ -1536,7 +1563,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) { QByteArray textTagsSerialized; qint64 keyValue = 0; qint64 messageIdPeer = 0, messageIdMsg = 0; - quint64 suggestSerialized = 0; + std::pair<quint64, quint64> suggestSerialized; qint32 keyValueOld = 0; QString webpageUrl; qint32 webpageForceLargeMedia = 0; @@ -1572,7 +1599,9 @@ void Account::readDraftsWithCursors(not_null<History*> history) { >> messageIdPeer >> messageIdMsg; if (withSuggest) { - draft.stream >> suggestSerialized; + draft.stream + >> suggestSerialized.first + >> suggestSerialized.second; } draft.stream >> webpageUrl @@ -1597,13 +1626,7 @@ void Account::readDraftsWithCursors(not_null<History*> history) { MsgId(messageIdMsg)), .topicRootId = key.topicRootId(), }, - SuggestPostOptions{ - .exists = uint32(suggestSerialized >> 63), - .stars = uint32( - (suggestSerialized & ~(1ULL << 63)) >> 32), - .date = TimeId( - uint32(suggestSerialized & 0xFFFF'FFFFULL)), - }, + DeserializeSuggest(suggestSerialized), MessageCursor(), Data::WebPageDraft{ .url = webpageUrl, diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp index d7822ee730..a3dcc9f066 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.cpp @@ -164,11 +164,11 @@ not_null<RpWidget*> CreateSingleStarWidget( not_null<MaskedInputField*> AddInputFieldForCredits( not_null<VerticalLayout*> container, - rpl::producer<StarsAmount> value) { + rpl::producer<CreditsAmount> value) { const auto &st = st::botEarnInputField; const auto inputContainer = container->add( CreateSkipWidget(container, st.heightMin)); - const auto currentValue = rpl::variable<StarsAmount>( + const auto currentValue = rpl::variable<CreditsAmount>( rpl::duplicate(value)); const auto input = CreateChild<NumberInput>( inputContainer, @@ -178,7 +178,7 @@ not_null<MaskedInputField*> AddInputFieldForCredits( currentValue.current().whole()); rpl::duplicate( value - ) | rpl::start_with_next([=](StarsAmount v) { + ) | rpl::start_with_next([=](CreditsAmount v) { input->changeLimit(v.whole()); input->setText(QString::number(v.whole())); }, input->lifetime()); diff --git a/Telegram/SourceFiles/ui/effects/credits_graphics.h b/Telegram/SourceFiles/ui/effects/credits_graphics.h index 022c7cac80..5df03f4af2 100644 --- a/Telegram/SourceFiles/ui/effects/credits_graphics.h +++ b/Telegram/SourceFiles/ui/effects/credits_graphics.h @@ -44,7 +44,7 @@ using PaintRoundImageCallback = Fn<void( [[nodiscard]] not_null<Ui::MaskedInputField*> AddInputFieldForCredits( not_null<Ui::VerticalLayout*> container, - rpl::producer<StarsAmount> value); + rpl::producer<CreditsAmount> value); PaintRoundImageCallback GenerateCreditsPaintUserpicCallback( const Data::CreditsHistoryEntry &entry); diff --git a/Telegram/SourceFiles/ui/ui_pch.h b/Telegram/SourceFiles/ui/ui_pch.h index 10bd37c13e..eb9a86a713 100644 --- a/Telegram/SourceFiles/ui/ui_pch.h +++ b/Telegram/SourceFiles/ui/ui_pch.h @@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flat_map.h" #include "base/flat_set.h" -#include "core/stars_amount.h" +#include "core/credits_amount.h" #include "ui/arc_angles.h" #include "ui/text/text.h" From 6272b79f70c6b5e660f06a65c1d52b39167f83d9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 24 Jun 2025 13:39:24 +0400 Subject: [PATCH 196/310] Allow suggesting with TON. --- Telegram/Resources/langs/lang.strings | 8 +- Telegram/SourceFiles/api/api_common.cpp | 3 +- Telegram/SourceFiles/api/api_updates.cpp | 5 - .../SourceFiles/data/components/credits.cpp | 14 +- .../SourceFiles/data/components/credits.h | 1 + .../view/history_view_suggest_options.cpp | 290 +++++++++++++++--- .../channel_statistics/earn/earn_icons.cpp | 12 +- .../info/channel_statistics/earn/earn_icons.h | 1 + Telegram/SourceFiles/ui/chat/chat.style | 18 ++ .../SourceFiles/ui/controls/ton_common.cpp | 240 +++++++++++++++ Telegram/SourceFiles/ui/controls/ton_common.h | 46 +++ Telegram/cmake/td_ui.cmake | 2 + 12 files changed, 584 insertions(+), 56 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/ton_common.cpp create mode 100644 Telegram/SourceFiles/ui/controls/ton_common.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index da2e1a761f..cc0603dd6e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4422,8 +4422,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_bar_dated" = "Publish on {date}"; "lng_suggest_options_title" = "Suggest a Message"; "lng_suggest_options_change" = "Suggest Changes"; -"lng_suggest_options_price" = "Enter Price in Stars"; -"lng_suggest_options_price_about" = "Choose how many Stars to pay to publish this message."; +"lng_suggest_options_stars_offer" = "Offer Stars"; +"lng_suggest_options_stars_price" = "Enter Price in Stars"; +"lng_suggest_options_stars_price_about" = "Choose how many Stars to pay to publish this message."; +"lng_suggest_options_ton_offer" = "Offer TON"; +"lng_suggest_options_ton_price" = "Enter Price in TON"; +"lng_suggest_options_ton_price_about" = "Choose how many TON to pay to publish this message."; "lng_suggest_options_date" = "Time"; "lng_suggest_options_date_any" = "Anytime"; "lng_suggest_options_date_about" = "Select the date and time you want the message to be published."; diff --git a/Telegram/SourceFiles/api/api_common.cpp b/Telegram/SourceFiles/api/api_common.cpp index 65ec607c04..55017ade84 100644 --- a/Telegram/SourceFiles/api/api_common.cpp +++ b/Telegram/SourceFiles/api/api_common.cpp @@ -18,7 +18,8 @@ MTPSuggestedPost SuggestToMTP(SuggestPostOptions suggest) { using Flag = MTPDsuggestedPost::Flag; return suggest.exists ? MTP_suggestedPost( - MTP_flags(suggest.date ? Flag::f_schedule_date : Flag()), + MTP_flags((suggest.date ? Flag::f_schedule_date : Flag()) + | (suggest.price().empty() ? Flag() : Flag::f_price)), StarsAmountToTL(suggest.price()), MTP_int(suggest.date)) : MTPSuggestedPost(); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 7b0c538850..d2a19a7420 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2756,11 +2756,6 @@ void Updates::feedUpdate(const MTPUpdate &update) { _session->credits().apply(data); } break; - case mtpc_updateStarsTonBalance: { - const auto &data = update.c_updateStarsBalance(); - _session->credits().apply(data); - } break; - case mtpc_updatePaidReactionPrivacy: { const auto &data = update.c_updatePaidReactionPrivacy(); _session->api().globalPrivacy().updatePaidReactionShownPeer( diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 5f93ba303c..5323305331 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -132,12 +132,16 @@ void Credits::invalidate() { } void Credits::apply(CreditsAmount balance) { - _balance = balance; - updateNonLockedValue(); + if (balance.ton()) { + _balanceTon = balance; + } else { + _balance = balance; + updateNonLockedValue(); - const auto was = std::exchange(_lastLoaded, crl::now()); - if (!was) { - _loadedChanges.fire({}); + const auto was = std::exchange(_lastLoaded, crl::now()); + if (!was) { + _loadedChanges.fire({}); + } } } diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h index 000df652e3..a26715f473 100644 --- a/Telegram/SourceFiles/data/components/credits.h +++ b/Telegram/SourceFiles/data/components/credits.h @@ -56,6 +56,7 @@ private: base::flat_map<PeerId, uint64> _cachedPeerCurrencyBalances; CreditsAmount _balance; + CreditsAmount _balanceTon; CreditsAmount _locked; rpl::variable<CreditsAmount> _nonLockedBalance; rpl::event_stream<> _loadedChanges; diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp index b5c8f84097..be36d9e24d 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp @@ -8,9 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_suggest_options.h" #include "base/unixtime.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_media_types.h" +#include "data/data_session.h" #include "history/history_item.h" +#include "info/channel_statistics/earn/earn_icons.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -18,12 +22,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/boxes/choose_date_time.h" +#include "ui/controls/ton_common.h" #include "ui/widgets/fields/number_input.h" +#include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/painter.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_channel_earn.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace HistoryView { @@ -37,6 +48,7 @@ void ChooseSuggestTimeBox( const auto value = args.value ? std::clamp(args.value, now + min, now + max) : (now + 86400); + const auto done = args.done; Ui::ChooseDateTimeBox(box, { .title = ((args.mode == SuggestMode::New) ? tr::lng_suggest_options_date() @@ -44,21 +56,34 @@ void ChooseSuggestTimeBox( .submit = ((args.mode == SuggestMode::New) ? tr::lng_settings_save() : tr::lng_suggest_options_update()), - .done = std::move(args.done), + .done = done, .min = [=] { return now + min; }, .time = value, .max = [=] { return now + max; }, }); + + box->addLeftButton(tr::lng_suggest_options_date_any(), [=] { + done(TimeId()); + }); } void ChooseSuggestPriceBox( not_null<Ui::GenericBox*> box, SuggestPriceBoxArgs &&args) { + struct Button { + QRect geometry; + Ui::Text::String text; + bool active = false; + }; struct State { + std::vector<Button> buttons; rpl::variable<TimeId> date; + rpl::variable<bool> ton; + bool inButton = false; }; const auto state = box->lifetime().make_state<State>(); state->date = args.value.date; + state->ton = (args.value.ton != 0); const auto limit = args.session->appConfig().suggestedPostStarsMax(); @@ -67,41 +92,214 @@ void ChooseSuggestPriceBox( : tr::lng_suggest_options_change()); const auto container = box->verticalLayout(); - - Ui::AddSkip(container); - Ui::AddSubsectionTitle(container, tr::lng_suggest_options_price()); - - const auto wrap = box->addRow(object_ptr<Ui::FixedHeightWidget>( - box, - st::editTagField.heightMin)); - auto owned = object_ptr<Ui::NumberInput>( - wrap, - st::editTagField, - tr::lng_paid_cost_placeholder(), - (args.value.price() - ? QString::number(args.value.price().value()) - : QString()), - limit); - const auto field = owned.data(); - wrap->widthValue() | rpl::start_with_next([=](int width) { - field->move(0, 0); - field->resize(width, field->height()); - wrap->resize(width, field->height()); - }, wrap->lifetime()); - field->paintRequest() | rpl::start_with_next([=](QRect clip) { - auto p = QPainter(field); - st::paidStarIcon.paint(p, 0, st::paidStarIconTop, field->width()); - }, field->lifetime()); - field->selectAll(); - box->setFocusCallback([=] { - field->setFocusFast(); + state->buttons.push_back({ + .text = Ui::Text::String( + st::semiboldTextStyle, + tr::lng_suggest_options_stars_offer(tr::now)), + .active = !state->ton.current(), + }); + state->buttons.push_back({ + .text = Ui::Text::String( + st::semiboldTextStyle, + tr::lng_suggest_options_ton_offer(tr::now)), + .active = state->ton.current(), }); + auto x = 0; + auto y = st::giftBoxTabsMargin.top(); + const auto padding = st::giftBoxTabPadding; + for (auto &button : state->buttons) { + const auto width = button.text.maxWidth(); + const auto height = st::semiboldTextStyle.font->height; + const auto r = QRect(0, 0, width, height).marginsAdded(padding); + button.geometry = QRect(QPoint(x, y), r.size()); + x += r.width() + st::giftBoxTabSkip; + } + const auto buttons = box->addRow(object_ptr<Ui::RpWidget>(box)); + const auto height = y + + state->buttons.back().geometry.height() + + st::giftBoxTabsMargin.bottom(); + buttons->resize(buttons->width(), height); + + buttons->setMouseTracking(true); + buttons->events() | rpl::start_with_next([=](not_null<QEvent*> e) { + const auto type = e->type(); + switch (type) { + case QEvent::MouseMove: { + const auto in = [&] { + const auto me = static_cast<QMouseEvent*>(e.get()); + const auto position = me->pos(); + for (const auto &button : state->buttons) { + if (button.geometry.contains(position)) { + return true; + } + } + return false; + }(); + if (state->inButton != in) { + state->inButton = in; + buttons->setCursor(in + ? style::cur_pointer + : style::cur_default); + } + } break; + case QEvent::MouseButtonPress: { + const auto me = static_cast<QMouseEvent*>(e.get()); + if (me->button() != Qt::LeftButton) { + break; + } + const auto position = me->pos(); + for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) { + if (state->buttons[i].geometry.contains(position)) { + state->ton = (i != 0); + state->buttons[i].active = true; + state->buttons[1 - i].active = false; + buttons->update(); + break; + } + } + } break; + } + }, buttons->lifetime()); + + buttons->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(buttons); + auto hq = PainterHighQualityEnabler(p); + const auto padding = st::giftBoxTabPadding; + for (const auto &button : state->buttons) { + const auto geometry = button.geometry; + if (button.active) { + p.setBrush(st::giftBoxTabBgActive); + p.setPen(Qt::NoPen); + const auto radius = geometry.height() / 2.; + p.drawRoundedRect(geometry, radius, radius); + p.setPen(st::giftBoxTabFgActive); + } else { + p.setPen(st::giftBoxTabFg); + } + button.text.draw(p, { + .position = geometry.marginsRemoved(padding).topLeft(), + .availableWidth = button.text.maxWidth(), + }); + } + }, buttons->lifetime()); + Ui::AddSkip(container); - Ui::AddSkip(container); + + const auto added = st::boxRowPadding - st::defaultSubsectionTitlePadding; + const auto manager = &args.session->data().customEmojiManager(); + const auto makeIcon = [&]( + not_null<QWidget*> parent, + TextWithEntities text) { + return Ui::CreateChild<Ui::FlatLabel>( + parent, + rpl::single(text), + st::defaultFlatLabel, + st::defaultPopupMenu, + Core::TextContext({ .session = args.session })); + }; + + const auto starsWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto starsInner = starsWrap->entity(); + + Ui::AddSubsectionTitle( + starsInner, + tr::lng_suggest_options_stars_price(), + QMargins(added.left(), 0, added.right(), 0)); + + const auto starsFieldWrap = starsInner->add( + object_ptr<Ui::FixedHeightWidget>( + box, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedStarsField = object_ptr<Ui::NumberInput>( + starsFieldWrap, + st::editTagField, + rpl::single(u"0"_q), + ((args.value.exists && args.value.priceWhole && !args.value.ton) + ? QString::number(args.value.priceWhole) + : QString()), + limit); + const auto starsField = ownedStarsField.data(); + const auto starsIcon = makeIcon(starsField, manager->creditsEmoji()); + + starsFieldWrap->widthValue() | rpl::start_with_next([=](int width) { + starsIcon->move(st::starsFieldIconPosition); + starsField->move(0, 0); + starsField->resize(width, starsField->height()); + starsFieldWrap->resize(width, starsField->height()); + }, starsFieldWrap->lifetime()); + + Ui::AddSkip(starsInner); + Ui::AddSkip(starsInner); Ui::AddDividerText( - container, - tr::lng_suggest_options_price_about()); + starsInner, + tr::lng_suggest_options_stars_price_about()); + + const auto tonWrap = container->add( + object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>( + container, + object_ptr<Ui::VerticalLayout>(container))); + const auto tonInner = tonWrap->entity(); + + Ui::AddSubsectionTitle( + tonInner, + tr::lng_suggest_options_ton_price(), + QMargins(added.left(), 0, added.right(), 0)); + + const auto tonFieldWrap = tonInner->add( + object_ptr<Ui::FixedHeightWidget>( + box, + st::editTagField.heightMin), + st::boxRowPadding); + auto ownedTonField = object_ptr<Ui::InputField>::fromRaw( + Ui::CreateTonAmountInput( + tonFieldWrap, + rpl::single('0' + Ui::TonAmountSeparator() + '0'), + ((args.value.price() && args.value.ton) + ? (int64(args.value.priceWhole) * Ui::kNanosInOne + + int64(args.value.priceNano)) + : 0))); + const auto tonField = ownedTonField.data(); + const auto tonIcon = makeIcon(tonField, Ui::Text::SingleCustomEmoji( + manager->registerInternalEmoji( + Ui::Earn::IconCurrencyColored( + st::tonFieldIconSize, + st::windowActiveTextFg->c), + st::channelEarnCurrencyCommonMargins, + false))); + + tonFieldWrap->widthValue() | rpl::start_with_next([=](int width) { + tonIcon->move(st::tonFieldIconPosition); + tonField->move(0, 0); + tonField->resize(width, tonField->height()); + tonFieldWrap->resize(width, tonField->height()); + }, tonFieldWrap->lifetime()); + + Ui::AddSkip(tonInner); + Ui::AddSkip(tonInner); + Ui::AddDividerText( + tonInner, + tr::lng_suggest_options_ton_price_about()); + + tonWrap->toggleOn(state->ton.value(), anim::type::instant); + starsWrap->toggleOn( + state->ton.value() | rpl::map(!rpl::mappers::_1), + anim::type::instant); + + box->setFocusCallback([=] { + if (state->ton.current()) { + tonField->selectAll(); + tonField->setFocusFast(); + } else { + starsField->selectAll(); + starsField->setFocusFast(); + } + }); + Ui::AddSkip(container); const auto time = Settings::AddButtonWithLabel( @@ -139,23 +337,37 @@ void ChooseSuggestPriceBox( Ui::AddDividerText(container, tr::lng_suggest_options_date_about()); AssertIsDebug()//tr::lng_suggest_options_offer const auto save = [=] { - const auto now = field->getLastText().toDouble(); - if (now > limit) { - field->showError(); - return; + auto nanos = int64(); + if (state->ton.current()) { + const auto now = Ui::ParseTonAmountString( + tonField->getLastText()); + if (!now || (*now < 0) || (*now > limit * Ui::kNanosInOne)) { + tonField->showError(); + return; + } + nanos = *now; + } else { + const auto now = starsField->getLastText().toLongLong(); + if (now < 0 || now > limit) { + starsField->showError(); + return; + } + nanos = now * Ui::kNanosInOne; } const auto value = CreditsAmount( - int(std::floor(now)), - int(base::SafeRound((now - std::floor(now)) * 1'000'000'000.))); + nanos / Ui::kNanosInOne, + nanos % Ui::kNanosInOne); args.done({ .exists = true, .priceWhole = uint32(value.whole()), .priceNano = uint32(value.nano()), + .ton = uint32(state->ton.current() ? 1 : 0), .date = state->date.current(), }); }; - QObject::connect(field, &Ui::NumberInput::submitted, box, save); + QObject::connect(starsField, &Ui::NumberInput::submitted, box, save); + tonField->submits() | rpl::start_with_next(save, tonField->lifetime()); box->addButton(tr::lng_settings_save(), save); box->addButton(tr::lng_cancel(), [=] { diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp index f6ca314bcc..fa5b2444da 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp @@ -47,10 +47,8 @@ namespace { } // namespace -QImage IconCurrencyColored( - const style::font &font, - const QColor &c) { - const auto s = Size(font->ascent); +QImage IconCurrencyColored(int size, const QColor &c) { + const auto s = Size(size); auto svg = QSvgRenderer(CurrencySvg(c)); auto image = QImage( s * style::DevicePixelRatio(), @@ -64,6 +62,12 @@ QImage IconCurrencyColored( return image; } +QImage IconCurrencyColored( + const style::font &font, + const QColor &c) { + return IconCurrencyColored(font->ascent, c); +} + QByteArray CurrencySvgColored(const QColor &c) { return CurrencySvg(c); } diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h index eb82e11fd5..0e5a419ec7 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.h @@ -13,6 +13,7 @@ class CustomEmoji; namespace Ui::Earn { +[[nodiscard]] QImage IconCurrencyColored(int size, const QColor &c); [[nodiscard]] QImage IconCurrencyColored( const style::font &font, const QColor &c); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 65d11c42c0..1f762126ba 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1352,3 +1352,21 @@ chatTabsOutlineHorizontal: ChatTabsOutline { chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) { } + +tonInput: InputField(defaultInputField) { + textBg: transparent; + textMargins: margins(0px, 7px, 0px, 7px); + + placeholderFg: placeholderFg; + placeholderFgActive: placeholderFgActive; + placeholderFgError: placeholderFgActive; + placeholderMargins: margins(0px, 0px, 0px, 0px); + placeholderScale: 0.; + placeholderFont: boxTextFont; + + heightMin: 34px; + heightMax: 100px; +} +starsFieldIconPosition: point(0px, 10px); +tonFieldIconSize: 16px; +tonFieldIconPosition: point(2px, 9px); diff --git a/Telegram/SourceFiles/ui/controls/ton_common.cpp b/Telegram/SourceFiles/ui/controls/ton_common.cpp new file mode 100644 index 0000000000..ce36200bad --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/ton_common.cpp @@ -0,0 +1,240 @@ +/* +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 "ui/controls/ton_common.h" + +#include "base/qthelp_url.h" +#include "ui/widgets/fields/input_field.h" +#include "ui/ui_utility.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" +//#include "styles/style_wallet.h" + +#include <QtCore/QLocale> + +namespace Ui { +namespace { + +constexpr auto kOneTon = kNanosInOne; +constexpr auto kNanoDigits = 9; + +struct FixedAmount { + QString text; + int position = 0; +}; + +std::optional<int64> ParseAmountTons(const QString &trimmed) { + auto ok = false; + const auto grams = int64(trimmed.toLongLong(&ok)); + return (ok + && (grams <= std::numeric_limits<int64>::max() / kOneTon) + && (grams >= std::numeric_limits<int64>::min() / kOneTon)) + ? std::make_optional(grams * kOneTon) + : std::nullopt; +} + +std::optional<int64> ParseAmountNano(QString trimmed) { + while (trimmed.size() < kNanoDigits) { + trimmed.append('0'); + } + auto zeros = 0; + for (const auto ch : trimmed) { + if (ch == '0') { + ++zeros; + } else { + break; + } + } + if (zeros == trimmed.size()) { + return 0; + } else if (trimmed.size() > kNanoDigits) { + return std::nullopt; + } + auto ok = false; + const auto value = trimmed.mid(zeros).toLongLong(&ok); + return (ok && value > 0 && value < kOneTon) + ? std::make_optional(value) + : std::nullopt; +} + +[[nodiscard]] FixedAmount FixTonAmountInput( + const QString &was, + const QString &text, + int position) { + constexpr auto kMaxDigitsCount = 9; + const auto separator = FormatTonAmount(1).separator; + + auto result = FixedAmount{ text, position }; + if (text.isEmpty()) { + return result; + } else if (text.startsWith('.') + || text.startsWith(',') + || text.startsWith(separator)) { + result.text.prepend('0'); + ++result.position; + } + auto separatorFound = false; + auto digitsCount = 0; + for (auto i = 0; i != result.text.size();) { + const auto ch = result.text[i]; + const auto atSeparator = result.text.midRef(i).startsWith(separator); + if (ch >= '0' && ch <= '9' && digitsCount < kMaxDigitsCount) { + ++i; + ++digitsCount; + continue; + } else if (!separatorFound + && (atSeparator || ch == '.' || ch == ',')) { + separatorFound = true; + if (!atSeparator) { + result.text.replace(i, 1, separator); + } + digitsCount = 0; + i += separator.size(); + continue; + } + result.text.remove(i, 1); + if (result.position > i) { + --result.position; + } + } + if (result.text == "0" && result.position > 0) { + if (was.startsWith('0')) { + result.text = QString(); + result.position = 0; + } else { + result.text += separator; + result.position += separator.size(); + } + } + return result; +} + +} // namespace + +FormattedTonAmount FormatTonAmount(int64 amount, TonFormatFlags flags) { + auto result = FormattedTonAmount(); + const auto grams = amount / kOneTon; + const auto preciseNanos = std::abs(amount) % kOneTon; + auto roundedNanos = preciseNanos; + if (flags & TonFormatFlag::Rounded) { + if (std::abs(grams) >= 1'000'000 && (roundedNanos % 1'000'000)) { + roundedNanos -= (roundedNanos % 1'000'000); + } else if (std::abs(grams) >= 1'000 && (roundedNanos % 1'000)) { + roundedNanos -= (roundedNanos % 1'000); + } + } + const auto precise = (roundedNanos == preciseNanos); + auto nanos = preciseNanos; + auto zeros = 0; + while (zeros < kNanoDigits && nanos % 10 == 0) { + nanos /= 10; + ++zeros; + } + const auto system = QLocale::system(); + const auto locale = (flags & TonFormatFlag::Simple) + ? QLocale::c() + : system; + const auto separator = system.decimalPoint(); + + result.wholeString = locale.toString(grams); + if ((flags & TonFormatFlag::Signed) && amount > 0) { + result.wholeString = locale.positiveSign() + result.wholeString; + } else if (amount < 0 && grams == 0) { + result.wholeString = locale.negativeSign() + result.wholeString; + } + result.full = result.wholeString; + if (zeros < kNanoDigits) { + result.separator = separator; + result.nanoString = QString("%1" + ).arg(nanos, kNanoDigits - zeros, 10, QChar('0')); + if (!precise) { + const auto nanoLength = (std::abs(grams) >= 1'000'000) + ? 3 + : (std::abs(grams) >= 1'000) + ? 6 + : 9; + result.nanoString = result.nanoString.mid(0, nanoLength); + } + result.full += separator + result.nanoString; + } + return result; +} + +std::optional<int64> ParseTonAmountString(const QString &amount) { + const auto trimmed = amount.trimmed(); + const auto separator = QString(QLocale::system().decimalPoint()); + const auto index1 = trimmed.indexOf('.'); + const auto index2 = trimmed.indexOf(','); + const auto index3 = (separator == "." || separator == ",") + ? -1 + : trimmed.indexOf(separator); + const auto found = (index1 >= 0 ? 1 : 0) + + (index2 >= 0 ? 1 : 0) + + (index3 >= 0 ? 1 : 0); + if (found > 1) { + return std::nullopt; + } + const auto index = (index1 >= 0) + ? index1 + : (index2 >= 0) + ? index2 + : index3; + const auto used = (index1 >= 0) + ? "." + : (index2 >= 0) + ? "," + : separator; + const auto grams = ParseAmountTons(trimmed.mid(0, index)); + const auto nano = ParseAmountNano(trimmed.mid(index + used.size())); + if (index < 0 || index == trimmed.size() - used.size()) { + return grams; + } else if (index == 0) { + return nano; + } else if (!nano || !grams) { + return std::nullopt; + } + return *grams + (*grams < 0 ? (-*nano) : (*nano)); +} + +QString TonAmountSeparator() { + return FormatTonAmount(1).separator; +} + +not_null<Ui::InputField*> CreateTonAmountInput( + not_null<QWidget*> parent, + rpl::producer<QString> placeholder, + int64 amount) { + const auto result = Ui::CreateChild<Ui::InputField>( + parent.get(), + st::editTagField, + Ui::InputField::Mode::SingleLine, + std::move(placeholder), + (amount > 0 + ? FormatTonAmount(amount, TonFormatFlag::Simple).full + : QString())); + const auto lastAmountValue = std::make_shared<QString>(); + result->changes() | rpl::start_with_next([=] { + Ui::PostponeCall(result, [=] { + const auto position = result->textCursor().position(); + const auto now = result->getLastText(); + const auto fixed = FixTonAmountInput( + *lastAmountValue, + now, + position); + *lastAmountValue = fixed.text; + if (fixed.text == now) { + return; + } + result->setText(fixed.text); + result->setFocusFast(); + result->setCursorPosition(fixed.position); + }); + }, result->lifetime()); + return result; +} + +} // namespace Wallet diff --git a/Telegram/SourceFiles/ui/controls/ton_common.h b/Telegram/SourceFiles/ui/controls/ton_common.h new file mode 100644 index 0000000000..189a793d8b --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/ton_common.h @@ -0,0 +1,46 @@ +/* +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/flags.h" + +namespace Ui { + +class InputField; + +inline constexpr auto kNanosInOne = 1'000'000'000LL; + +struct FormattedTonAmount { + QString wholeString; + QString separator; + QString nanoString; + QString full; +}; + +enum class TonFormatFlag { + Signed = 0x01, + Rounded = 0x02, + Simple = 0x04, +}; +constexpr bool is_flag_type(TonFormatFlag) { return true; }; +using TonFormatFlags = base::flags<TonFormatFlag>; + +[[nodiscard]] FormattedTonAmount FormatTonAmount( + int64 amount, + TonFormatFlags flags = TonFormatFlags()); +[[nodiscard]] std::optional<int64> ParseTonAmountString( + const QString &amount); + +[[nodiscard]] QString TonAmountSeparator(); + +[[nodiscard]] not_null<Ui::InputField*> CreateTonAmountInput( + not_null<QWidget*> parent, + rpl::producer<QString> placeholder, + int64 amount = 0); + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 7bdd372fd6..813c5bf02d 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -395,6 +395,8 @@ PRIVATE ui/controls/swipe_handler_data.h ui/controls/tabbed_search.cpp ui/controls/tabbed_search.h + ui/controls/ton_common.cpp + ui/controls/ton_common.h ui/controls/who_reacted_context_action.cpp ui/controls/who_reacted_context_action.h ui/controls/window_outdated_bar.cpp From 881aed50eafa41db62c59acaab685ee859ba3fd9 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 24 Jun 2025 15:00:05 +0400 Subject: [PATCH 197/310] Support _suggestOptions for changes in ComposeControls. --- Telegram/CMakeLists.txt | 4 +- Telegram/SourceFiles/api/api_suggest_post.cpp | 2 +- Telegram/SourceFiles/history/history.cpp | 2 +- .../SourceFiles/history/history_widget.cpp | 19 +-- Telegram/SourceFiles/history/history_widget.h | 3 +- .../history_view_compose_controls.cpp | 115 ++++++++++++++---- .../controls/history_view_compose_controls.h | 1 + .../history_view_suggest_options.cpp | 10 +- .../history_view_suggest_options.h | 8 +- 9 files changed, 118 insertions(+), 46 deletions(-) rename Telegram/SourceFiles/history/view/{ => controls}/history_view_suggest_options.cpp (98%) rename Telegram/SourceFiles/history/view/{ => controls}/history_view_suggest_options.h (92%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index d36f0213fa..a27e737f1e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -757,6 +757,8 @@ PRIVATE history/view/controls/history_view_draft_options.h history/view/controls/history_view_forward_panel.cpp history/view/controls/history_view_forward_panel.h + history/view/controls/history_view_suggest_options.cpp + history/view/controls/history_view_suggest_options.h history/view/controls/history_view_ttl_button.cpp history/view/controls/history_view_ttl_button.h history/view/controls/history_view_voice_record_bar.cpp @@ -902,8 +904,6 @@ PRIVATE history/view/history_view_sticker_toast.h history/view/history_view_subsection_tabs.cpp history/view/history_view_subsection_tabs.h - history/view/history_view_suggest_options.cpp - history/view/history_view_suggest_options.h history/view/history_view_text_helper.cpp history/view/history_view_text_helper.h history/view/history_view_transcribe_button.cpp diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index b6a04474d3..1c5299e647 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_session.h" #include "data/data_saved_sublist.h" -#include "history/view/history_view_suggest_options.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 431bf17de1..04da9df94b 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -3394,7 +3394,7 @@ bool History::amMonoforumAdmin() const { } bool History::suggestDraftAllowed() const { - return peer->isMonoforum() || !peer->amMonoforumAdmin(); + return peer->isMonoforum() && !peer->amMonoforumAdmin(); } not_null<History*> History::migrateToOrMe() const { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 17eabdada5..93c1f62a2d 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -97,8 +97,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_search.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" -#include "history/view/controls/history_view_voice_record_bar.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/controls/history_view_ttl_button.h" +#include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_webpage_processor.h" #include "history/view/reactions/history_view_reactions_button.h" #include "history/view/history_view_cursor_state.h" @@ -118,7 +119,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_requests_bar.h" #include "history/view/history_view_sticker_toast.h" #include "history/view/history_view_subsection_tabs.h" -#include "history/view/history_view_suggest_options.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" #include "profile/profile_block_group_members.h" @@ -1902,7 +1902,7 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() { .topicRootId = topicRootId, .monoforumPeerId = monoforumPeerId, }, - SuggestPostOptions(), + suggestOptions(true), _preview->draft(), _saveEditMsgRequestId)); } else { @@ -2935,7 +2935,7 @@ void HistoryWidget::registerDraftSource() { (editMsgId ? FullReplyTo{ FullMsgId(peerId, editMsgId) } : _replyTo), - (editMsgId ? SuggestPostOptions() : suggestOptions()), + suggestOptions(editMsgId != 0), _field->getTextWithTags(), _preview->draft(), }; @@ -3180,7 +3180,7 @@ void HistoryWidget::applySuggestOptions( using namespace HistoryView; _suggestOptions = std::make_unique<SuggestOptions>( - controller(), + controller()->uiShow(), _peer, suggest, mode); @@ -4522,7 +4522,7 @@ void HistoryWidget::saveEditMessage(Api::SendOptions options) { }; options.invertCaption = _mediaEditManager.invertCaption(); - options.suggest = suggestOptions(); + options.suggest = suggestOptions(true); const auto withPaymentApproved = [=](int approved) { auto copy = options; @@ -6700,8 +6700,11 @@ FullReplyTo HistoryWidget::replyTo() const { : FullReplyTo(); } -SuggestPostOptions HistoryWidget::suggestOptions() const { - return (_history && _history->suggestDraftAllowed() && _suggestOptions) +SuggestPostOptions HistoryWidget::suggestOptions( + bool skipNoAdminCheck) const { + const auto checked = skipNoAdminCheck + || (_history && _history->suggestDraftAllowed()); + return (checked && _suggestOptions) ? _suggestOptions->values() : SuggestPostOptions(); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index ec81821ee6..8c6a61f8b3 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -216,7 +216,8 @@ public: not_null<PeerData*> peer); [[nodiscard]] FullReplyTo replyTo() const; - [[nodiscard]] SuggestPostOptions suggestOptions() const; + [[nodiscard]] SuggestPostOptions suggestOptions( + bool skipNoAdminCheck = false) const; bool lastForceReplyReplied(const FullMsgId &replyTo) const; bool lastForceReplyReplied() const; bool cancelReplyOrSuggest(bool lastKeyboardUsed = false); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 294ffd9ca5..a64e31d825 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -54,8 +54,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_media_edit_manager.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" -#include "history/view/controls/history_view_voice_record_bar.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/controls/history_view_ttl_button.h" +#include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_webpage_processor.h" #include "history/view/history_view_reply.h" #include "history/view/history_view_webpage_preview.h" @@ -134,7 +135,10 @@ public: void updateTopicRootId(MsgId topicRootId); void init(); - void editMessage(FullMsgId id, bool photoEditAllowed = false); + void editMessage( + FullMsgId id, + SuggestPostOptions suggest, + bool photoEditAllowed = false); void replyToMessage(FullReplyTo id); void updateForwarding( Data::Thread *thread, @@ -159,6 +163,7 @@ public: [[nodiscard]] SendMenu::Details saveMenuDetails(bool hasSendText) const; [[nodiscard]] FullReplyTo getDraftReply() const; + [[nodiscard]] SuggestPostOptions suggestOptions() const; [[nodiscard]] rpl::producer<> editCancelled() const { return _editCancelled.events(); } @@ -171,6 +176,9 @@ public: [[nodiscard]] rpl::producer<> previewCancelled() const { return _previewCancelled.events(); } + [[nodiscard]] rpl::producer<> saveDraftRequests() const { + return _saveDraftRequests.events(); + } [[nodiscard]] rpl::producer<bool> visibleChanged(); @@ -188,6 +196,9 @@ private: bool hasPreview() const; + void applySuggestOptions(SuggestPostOptions suggest, SuggestMode mode); + void cancelSuggestPost(); + struct Preview { Controls::WebpageParsed parsed; Ui::Text::String title; @@ -206,11 +217,13 @@ private: rpl::event_stream<> _replyCancelled; rpl::event_stream<> _forwardCancelled; rpl::event_stream<> _previewCancelled; + rpl::event_stream<> _saveDraftRequests; rpl::lifetime _previewLifetime; rpl::variable<FullMsgId> _editMsgId; rpl::variable<FullReplyTo> _replyTo; std::unique_ptr<ForwardPanel> _forwardPanel; + std::unique_ptr<SuggestOptions> _suggestOptions; rpl::producer<> _toForwardUpdated; HistoryItem *_shownMessage = nullptr; @@ -282,7 +295,9 @@ void FieldHeader::init() { p.fillRect(rect(), st::historyComposeAreaBg); const auto position = st::historyReplyIconPosition; - if (_preview.parsed) { + if (_suggestOptions) { + _suggestOptions->paintIcon(p, 0, 0, width()); + } else if (_preview.parsed) { st::historyLinkIcon.paint(p, position, width()); } else if (isEditingMessage()) { st::historyEditIcon.paint(p, position, width()); @@ -647,7 +662,11 @@ void FieldHeader::paintEditOrReplyToMessage(Painter &p) { st::historyEditMedia.paintInCenter(p, to); p.setOpacity(1.); } + } + if (_suggestOptions) { + _suggestOptions->paintLines(p, textLeft, 0, width()); + return; } p.setPen(st::historyReplyNameFg); @@ -734,6 +753,12 @@ FullReplyTo FieldHeader::getDraftReply() const { : _replyTo.current(); } +SuggestPostOptions FieldHeader::suggestOptions() const { + return _suggestOptions + ? _suggestOptions->values() + : SuggestPostOptions(); +} + void FieldHeader::updateControlsGeometry(QSize size) { _cancel->moveToRight(0, 0); _clickableRect = QRect( @@ -748,7 +773,10 @@ void FieldHeader::updateControlsGeometry(QSize size) { st::historyReplyPreview); } -void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { +void FieldHeader::editMessage( + FullMsgId id, + SuggestPostOptions suggest, + bool photoEditAllowed) { _photoEditAllowed = photoEditAllowed; _editMsgId = id; if (!id) { @@ -760,9 +788,38 @@ void FieldHeader::editMessage(FullMsgId id, bool photoEditAllowed) { _inPhotoEdit = false; _inPhotoEditOver.stop(); } + if (id && suggest) { + applySuggestOptions(suggest, SuggestMode::Change); + } else { + cancelSuggestPost(); + } update(); } +void FieldHeader::applySuggestOptions( + SuggestPostOptions suggest, + SuggestMode mode) { + Expects(suggest.exists); + + using namespace HistoryView; + _suggestOptions = std::make_unique<SuggestOptions>( + _show, + _history->peer, + suggest, + mode); + _suggestOptions->updates() | rpl::start_with_next([=] { + update(); + _saveDraftRequests.fire({}); + }, _suggestOptions->lifetime()); +} + +void FieldHeader::cancelSuggestPost() { + if (!_suggestOptions) { + return; + } + _suggestOptions = nullptr; +} + void FieldHeader::replyToMessage(FullReplyTo id) { id.monoforumPeerId = 0; _replyTo = id; @@ -802,6 +859,7 @@ MessageToEdit FieldHeader::queryToEdit() { .scheduled = item->isScheduled() ? item->date() : 0, .shortcutId = item->shortcutId(), .invertCaption = _mediaEditManager.invertCaption(), + .suggest = suggestOptions(), }, .spoilered = _mediaEditManager.spoilered(), }; @@ -1467,9 +1525,12 @@ void ComposeControls::init() { if (_preview) { _preview->apply({ .removed = true }); } - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); + }, _wrap->lifetime()); + + _header->saveDraftRequests( + ) | rpl::start_with_next([=] { + saveDraftWithTextNow(); }, _wrap->lifetime()); _header->editCancelled( @@ -1704,9 +1765,7 @@ void ComposeControls::initFieldAutocomplete() { if (!_showSlowmodeError || !_showSlowmodeError()) { setText({}); } - //_saveDraftText = true; - //_saveDraftStart = crl::now(); - //saveDraft(); + //saveDraftWithTextNow(); // Won't be needed if SendInlineBotResult clears the cloud draft. //saveCloudDraft(); _fileChosen.fire(std::move(data)); @@ -1845,6 +1904,12 @@ Data::DraftKey ComposeControls::draftKeyCurrent() const { return draftKey(isEditingMessage() ? DraftType::Edit : DraftType::Normal); } +void ComposeControls::saveDraftWithTextNow() { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); +} + void ComposeControls::saveDraft(bool delayed) { if (delayed) { const auto now = crl::now(); @@ -1897,7 +1962,7 @@ void ComposeControls::registerDraftSource() { const auto draft = [=] { return Storage::MessageDraft{ _header->getDraftReply(), - SuggestPostOptions(), + _header->suggestOptions(), _field->getTextWithTags(), _preview->draft(), }; @@ -1948,6 +2013,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { const auto editingId = (draft && draft == editDraft) ? draft->reply.messageId : FullMsgId(); + const auto editingSuggest = (draft && draft == editDraft) + ? draft->suggest + : SuggestPostOptions(); InvokeQueued(_autocomplete.get(), [=] { if (_autocomplete) { @@ -1968,7 +2036,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { if (hadFocus) { _field->setFocus(); } - _header->editMessage({}); + _header->editMessage({}, {}); _header->replyToMessage({}); if (_preview) { _preview->apply({ .removed = true }); @@ -2015,7 +2083,10 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { Data::PhotoSize::Large, item->fullId()); } - _header->editMessage(editingId, _photoEditMedia != nullptr); + _header->editMessage( + editingId, + editingSuggest, + _photoEditMedia != nullptr); if (_preview) { _preview->apply( Data::WebPageDraft::FromItem(item), @@ -2026,7 +2097,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; - _header->editMessage(editingId, false); + _header->editMessage(editingId, SuggestPostOptions(), false); return false; }; if (!resolve()) { @@ -2048,7 +2119,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { _canReplaceMedia = _canAddMedia = false; _photoEditMedia = nullptr; _header->replyToMessage(draft->reply); - _header->editMessage({}); + _header->editMessage({}, {}); if (_preview) { _preview->setDisabled(false); } @@ -3036,10 +3107,7 @@ void ComposeControls::cancelEditMessage() { _history->clearDraft(draftKey(DraftType::Edit)); applyDraft(); - - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } void ComposeControls::maybeCancelEditMessage() { @@ -3091,10 +3159,7 @@ void ComposeControls::replyToMessage(FullReplyTo id) { } else { _header->replyToMessage(id); } - - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } void ComposeControls::cancelReplyMessage() { @@ -3112,9 +3177,7 @@ void ComposeControls::cancelReplyMessage() { } } if (wasReply) { - _saveDraftText = true; - _saveDraftStart = crl::now(); - saveDraft(); + saveDraftWithTextNow(); } } } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index cd391e0078..4efc974160 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -331,6 +331,7 @@ private: [[nodiscard]] Data::DraftKey draftKeyCurrent() const; void saveDraft(bool delayed = false); void saveDraftDelayed(); + void saveDraftWithTextNow(); void saveCloudDraft(); void writeDrafts(); diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp similarity index 98% rename from Telegram/SourceFiles/history/view/history_view_suggest_options.cpp rename to Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index be36d9e24d..b8fe4fe0ab 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -5,9 +5,10 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "history/view/history_view_suggest_options.h" +#include "history/view/controls/history_view_suggest_options.h" #include "base/unixtime.h" +#include "chat_helpers/compose/compose_show.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" @@ -29,7 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "ui/vertical_list.h" -#include "window/window_session_controller.h" #include "styles/style_channel_earn.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" @@ -381,11 +381,11 @@ bool CanEditSuggestedMessage(not_null<HistoryItem*> item) { } SuggestOptions::SuggestOptions( - not_null<Window::SessionController*> controller, + std::shared_ptr<ChatHelpers::Show> show, not_null<PeerData*> peer, SuggestPostOptions values, SuggestMode mode) -: _controller(controller) +: _show(std::move(show)) , _peer(peer) , _mode(mode) , _values(values) { @@ -435,7 +435,7 @@ void SuggestOptions::edit() { strong->closeBox(); } }; - *weak = _controller->show(Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ + *weak = _show->show(Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ .session = &_peer->session(), .done = apply, .value = _values, diff --git a/Telegram/SourceFiles/history/view/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h similarity index 92% rename from Telegram/SourceFiles/history/view/history_view_suggest_options.h rename to Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h index fbf15d12b7..b4b49fd671 100644 --- a/Telegram/SourceFiles/history/view/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Ui { class GenericBox; } // namespace Ui @@ -54,7 +58,7 @@ void ChooseSuggestPriceBox( class SuggestOptions final { public: SuggestOptions( - not_null<Window::SessionController*> controller, + std::shared_ptr<ChatHelpers::Show> show, not_null<PeerData*> peer, SuggestPostOptions values, SuggestMode mode); @@ -77,7 +81,7 @@ private: [[nodiscard]] TextWithEntities composeText() const; - const not_null<Window::SessionController*> _controller; + const std::shared_ptr<ChatHelpers::Show> _show; const not_null<PeerData*> _peer; const SuggestMode _mode = SuggestMode::New; From 1ecd7aa7cff251aa1ab0901e651958c9c8be3bff Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 26 Jun 2025 10:42:01 +0400 Subject: [PATCH 198/310] Implement suggestion of changes. --- Telegram/Resources/animations/diamond.tgs | Bin 0 -> 47932 bytes Telegram/Resources/langs/lang.strings | 14 ++- .../Resources/qrc/telegram/animations.qrc | 1 + Telegram/SourceFiles/api/api_editing.cpp | 80 +++++------------- Telegram/SourceFiles/api/api_suggest_post.cpp | 6 +- Telegram/SourceFiles/apiwrap.cpp | 14 ++- .../SourceFiles/boxes/edit_caption_box.cpp | 9 ++ Telegram/SourceFiles/boxes/edit_caption_box.h | 5 ++ Telegram/SourceFiles/history/history_item.cpp | 2 +- .../SourceFiles/history/history_widget.cpp | 9 +- .../history_view_compose_controls.cpp | 15 +++- .../controls/history_view_suggest_options.cpp | 8 +- .../controls/history_view_suggest_options.h | 3 +- .../view/media/history_view_premium_gift.cpp | 34 +++++++- .../media/history_view_suggest_decision.cpp | 16 +++- 15 files changed, 134 insertions(+), 82 deletions(-) create mode 100644 Telegram/Resources/animations/diamond.tgs diff --git a/Telegram/Resources/animations/diamond.tgs b/Telegram/Resources/animations/diamond.tgs new file mode 100644 index 0000000000000000000000000000000000000000..63d1896d9a7f14fde1bad6bf002fae9cd0f0b104 GIT binary patch literal 47932 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index cc0603dd6e..59438de715 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4423,9 +4423,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_options_title" = "Suggest a Message"; "lng_suggest_options_change" = "Suggest Changes"; "lng_suggest_options_stars_offer" = "Offer Stars"; +"lng_suggest_options_stars_request" = "Request Stars"; "lng_suggest_options_stars_price" = "Enter Price in Stars"; "lng_suggest_options_stars_price_about" = "Choose how many Stars to pay to publish this message."; "lng_suggest_options_ton_offer" = "Offer TON"; +"lng_suggest_options_ton_request" = "Request TON"; "lng_suggest_options_ton_price" = "Enter Price in TON"; "lng_suggest_options_ton_price_about" = "Choose how many TON to pay to publish this message."; "lng_suggest_options_date" = "Time"; @@ -4447,10 +4449,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_action_agree_date" = "The post will be automatically published on {channel} {date}."; "lng_suggest_action_your_charged" = "You have been charged {amount}."; "lng_suggest_action_his_charged" = "{from} have been charged {amount}."; -"lng_suggest_action_agree_receive" = "{channel} will receive the Stars once the post has been live for 24 hours."; -"lng_suggest_action_agree_removed" = "If {channel} removes the post before it has been live for 24 hours, the Stars will be refunded."; -"lng_suggest_action_your_not_enough" = "**Transaction failed** because you didn't have enough Stars."; -"lng_suggest_action_his_not_enough" = "**Transaction failed** because the user didn't have enough Stars."; +"lng_suggest_action_agree_receive_stars" = "{channel} will receive the Stars once the post has been live for 24 hours."; +"lng_suggest_action_agree_receive_ton" = "{channel} will receive TON once the post has been live for 24 hours."; +"lng_suggest_action_agree_removed_stars" = "If {channel} removes the post before it has been live for 24 hours, the Stars will be refunded."; +"lng_suggest_action_agree_removed_ton" = "If {channel} removes the post before it has been live for 24 hours, TON will be refunded."; +"lng_suggest_action_your_not_enough_stars" = "**Transaction failed** because you didn't have enough Stars."; +"lng_suggest_action_your_not_enough_ton" = "**Transaction failed** because you didn't have enough TON."; +"lng_suggest_action_his_not_enough_stars" = "**Transaction failed** because the user didn't have enough Stars."; +"lng_suggest_action_his_not_enough_ton" = "**Transaction failed** because the user didn't have enough TON."; "lng_suggest_action_declined" = "{from} rejected the message."; "lng_suggest_action_declined_reason" = "{from} rejected the message with the comment."; "lng_suggest_change_price" = "{from} suggests a new price for the message."; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index 98917d9924..9beaa522d3 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -26,6 +26,7 @@ <file alias="hours.tgs">../../animations/hours.tgs</file> <file alias="phone.tgs">../../animations/phone.tgs</file> <file alias="chat_link.tgs">../../animations/chat_link.tgs</file> + <file alias="diamond.tgs">../../animations/diamond.tgs</file> <file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file> <file alias="collectible_phone.tgs">../../animations/collectible_phone.tgs</file> <file alias="search.tgs">../../animations/search.tgs</file> diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 6f1c322718..e3ec52e172 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/scheduled_messages.h" #include "data/data_file_origin.h" #include "data/data_histories.h" +#include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_todo_list.h" #include "data/data_web_page.h" @@ -62,76 +63,39 @@ mtpRequestId SuggestMessage( const auto session = &item->history()->session(); const auto api = &session->api(); - const auto text = textWithEntities.text; - const auto sentEntities = EntitiesToMTP( - session, - textWithEntities.entities, - ConvertOption::SkipLocal); - - const auto emptyFlag = MTPmessages_SendMessage::Flag(0); - auto replyTo = FullReplyTo{ + const auto thread = item->history()->amMonoforumAdmin() + ? item->savedSublist() + : (Data::Thread*)item->history(); + auto action = SendAction(thread, options); + action.replyTo = FullReplyTo{ .messageId = item->fullId(), .monoforumPeerId = (item->history()->amMonoforumAdmin() ? item->sublistPeerId() : PeerId()), }; - const auto flags = emptyFlag - | MTPmessages_SendMessage::Flag::f_reply_to - | MTPmessages_SendMessage::Flag::f_suggested_post - | (webpage.removed - ? MTPmessages_SendMessage::Flag::f_no_webpage - : emptyFlag) - | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) - || options.invertCaption) - ? MTPmessages_SendMessage::Flag::f_invert_media - : emptyFlag) - | (!sentEntities.v.isEmpty() - ? MTPmessages_SendMessage::Flag::f_entities - : emptyFlag) - | (options.starsApproved - ? MTPmessages_SendMessage::Flag::f_allow_paid_stars - : emptyFlag); - const auto randomId = base::RandomValue<uint64>(); - return api->request(MTPmessages_SendMessage( - MTP_flags(flags), - item->history()->peer->input, - ReplyToForMTP(item->history(), replyTo), - MTP_string(text), - MTP_long(randomId), - MTPReplyMarkup(), - sentEntities, - MTPint(), // schedule_date - MTPInputPeer(), // send_as - MTPInputQuickReplyShortcut(), // quick_reply_shortcut - MTPlong(), // effect - MTP_long(options.starsApproved), - Api::SuggestToMTP(options.suggest) - )).done([=]( - const MTPUpdates &result, - [[maybe_unused]] mtpRequestId requestId) { - const auto apply = [=] { api->applyUpdates(result); }; - if constexpr (WithId<DoneCallback>) { - done(apply, requestId); - } else if constexpr (WithoutId<DoneCallback>) { - done(apply); - } else if constexpr (WithoutCallback<DoneCallback>) { - done(); - apply(); - } else { - t_bad_callback(done); - } - }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + auto message = MessageToSend(std::move(action)); + message.textWithTags = TextWithTags{ + textWithEntities.text, + TextUtilities::ConvertEntitiesToTextTags(textWithEntities.entities) + }; + message.webPage = webpage; + api->sendMessage(std::move(message)); + + const auto requestId = -1; + crl::on_main(session, [=] { + const auto type = u"MESSAGE_NOT_MODIFIED"_q; if constexpr (ErrorWithId<FailCallback>) { - fail(error.type(), requestId); + fail(type, requestId); } else if constexpr (ErrorWithoutId<FailCallback>) { - fail(error.type()); + fail(type); } else if constexpr (WithoutCallback<FailCallback>) { fail(); } else { t_bad_callback(fail); } - }).send(); + }); + return requestId; } template <typename DoneCallback, typename FailCallback> @@ -253,7 +217,7 @@ mtpRequestId SuggestMessageOrMedia( MTPstring()); // query } } - if (inputMedia || (!webpage.removed && !webpage.url.isEmpty())) { + if (inputMedia) { return SuggestMedia( item, textWithEntities, diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index 1c5299e647..f3bba3c9ae 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -268,11 +268,12 @@ void SuggestApprovalDate( close); }; using namespace HistoryView; + const auto admin = item->history()->amMonoforumAdmin(); auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ .session = &controller->session(), .done = done, .value = suggestion->date, - .mode = SuggestMode::Change, + .mode = (admin ? SuggestMode::ChangeAdmin : SuggestMode::ChangeUser), }); *weak = dateBox.data(); controller->uiShow()->show(std::move(dateBox)); @@ -306,6 +307,7 @@ void SuggestApprovalPrice( close); }; using namespace HistoryView; + const auto admin = item->history()->amMonoforumAdmin(); auto dateBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ .session = &controller->session(), .done = done, @@ -316,7 +318,7 @@ void SuggestApprovalPrice( .ton = uint32(suggestion->price.ton() ? 1 : 0), .date = suggestion->date, }, - .mode = SuggestMode::Change, + .mode = (admin ? SuggestMode::ChangeAdmin : SuggestMode::ChangeUser), }); *weak = dateBox.data(); controller->uiShow()->show(std::move(dateBox)); diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index d4c72646c3..415a17acba 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -3661,7 +3661,19 @@ void ApiWrap::editMedia( if (list.files.empty()) return; auto &file = list.files.front(); - const auto to = FileLoadTaskOptions(action); + auto to = FileLoadTaskOptions(action); + const auto existing = to.replaceMediaOf + ? session().data().message(action.history->peer, to.replaceMediaOf) + : nullptr; + if (existing && existing->computeSuggestionActions() + == SuggestionActions::AcceptAndDecline) { + to.replyTo.messageId = { + action.history->peer->id, + to.replaceMediaOf + }; + to.replyTo.monoforumPeerId = existing->sublistPeerId(); + to.replaceMediaOf = MsgId(); + } _fileLoader->addTask(std::make_unique<FileLoadTask>( &session(), file.path, diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 25501bd295..cb27997d66 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -232,12 +232,14 @@ EditCaptionBox::EditCaptionBox( not_null<Window::SessionController*> controller, not_null<HistoryItem*> item, TextWithTags &&text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Ui::PreparedList &&list, Fn<void()> saved) : _controller(controller) , _historyItem(item) +, _suggest(suggest) , _isAllowedEditMedia(item->allowsEditMedia()) , _albumType(ComputeAlbumType(item)) , _controls(base::make_unique_q<Ui::VerticalLayout>(this)) @@ -271,6 +273,7 @@ void EditCaptionBox::StartMediaReplace( not_null<Window::SessionController*> controller, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn<void()> saved) { @@ -284,6 +287,7 @@ void EditCaptionBox::StartMediaReplace( controller, item, std::move(text), + suggest, spoilered, invertCaption, std::move(list), @@ -300,6 +304,7 @@ void EditCaptionBox::StartMediaReplace( FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn<void()> saved) { @@ -335,6 +340,7 @@ void EditCaptionBox::StartMediaReplace( controller, item, std::move(text), + suggest, spoilered, invertCaption, std::move(list), @@ -347,6 +353,7 @@ void EditCaptionBox::StartPhotoEdit( std::shared_ptr<Data::PhotoMedia> media, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn<void()> saved) { @@ -365,6 +372,7 @@ void EditCaptionBox::StartPhotoEdit( controller, item, std::move(text), + suggest, spoilered, invertCaption, std::move(list), @@ -1001,6 +1009,7 @@ void EditCaptionBox::save() { }; auto options = Api::SendOptions(); + options.suggest = _suggest; options.scheduled = item->isScheduled() ? item->date() : 0; options.shortcutId = item->shortcutId(); options.invertCaption = _mediaEditManager.invertCaption(); diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index 7453506161..48d858f971 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -39,6 +39,7 @@ public: not_null<Window::SessionController*> controller, not_null<HistoryItem*> item, TextWithTags &&text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Ui::PreparedList &&list, @@ -49,6 +50,7 @@ public: not_null<Window::SessionController*> controller, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn<void()> saved); @@ -57,6 +59,7 @@ public: FullMsgId itemId, Ui::PreparedList &&list, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn<void()> saved); @@ -65,6 +68,7 @@ public: std::shared_ptr<Data::PhotoMedia> media, FullMsgId itemId, TextWithTags text, + SuggestPostOptions suggest, bool spoilered, bool invertCaption, Fn<void()> saved); @@ -111,6 +115,7 @@ private: const not_null<Window::SessionController*> _controller; const not_null<HistoryItem*> _historyItem; + const SuggestPostOptions _suggest; const bool _isAllowedEditMedia; const Ui::AlbumType _albumType; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 4eff615c20..311f5c3d4c 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -6236,7 +6236,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) { this, _from, Data::GiftType::Ton, - data.vamount().v); + data.vcrypto_amount().v); }, [&](const MTPDmessageActionPrizeStars &data) { _media = std::make_unique<Data::MediaGiftBox>( this, diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 93c1f62a2d..1347f00640 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2271,7 +2271,11 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { } if (editDraft && editDraft->suggest) { using namespace HistoryView; - applySuggestOptions(editDraft->suggest, SuggestMode::Change); + applySuggestOptions( + editDraft->suggest, + (_history->amMonoforumAdmin() + ? SuggestMode::ChangeAdmin + : SuggestMode::ChangeUser)); } else { cancelSuggestPost(); } @@ -3030,6 +3034,7 @@ bool HistoryWidget::updateReplaceMediaButton() { controller(), { _history->peer->id, _editMsgId }, _field->getTextWithTags(), + suggestOptions(), _mediaEditManager.spoilered(), _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); @@ -6276,6 +6281,7 @@ bool HistoryWidget::confirmSendingFiles( { _history->peer->id, _editMsgId }, std::move(list), _field->getTextWithTags(), + suggestOptions(), _mediaEditManager.spoilered(), _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); @@ -7412,6 +7418,7 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { _photoEditMedia, { _history->peer->id, _editMsgId }, _field->getTextWithTags(), + suggestOptions(), _mediaEditManager.spoilered(), _mediaEditManager.invertCaption(), crl::guard(_list, [=] { cancelEdit(); })); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index a64e31d825..ce1ceee294 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -420,9 +420,13 @@ void FieldHeader::init() { if (_preview.parsed) { _editOptionsRequests.fire({}); } else if (isEditingMessage()) { - _jumpToItemRequests.fire(FullReplyTo{ - .messageId = _editMsgId.current() - }); + if (_suggestOptions) { + _suggestOptions->edit(); + } else { + _jumpToItemRequests.fire(FullReplyTo{ + .messageId = _editMsgId.current() + }); + } } else if (reply && (e->modifiers() & Qt::ControlModifier)) { _jumpToItemRequests.fire_copy(reply); } else if (reply || readyToForward()) { @@ -789,7 +793,7 @@ void FieldHeader::editMessage( _inPhotoEditOver.stop(); } if (id && suggest) { - applySuggestOptions(suggest, SuggestMode::Change); + applySuggestOptions(suggest, SuggestMode::ChangeAdmin); } else { cancelSuggestPost(); } @@ -1227,6 +1231,7 @@ bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { _editingId, std::move(list), _field->getTextWithTags(), + _header->suggestOptions(), queryToEdit.spoilered, queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); @@ -1466,6 +1471,7 @@ void ComposeControls::init() { _photoEditMedia, _editingId, _field->getTextWithTags(), + _header->suggestOptions(), queryToEdit.spoilered, queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); @@ -3093,6 +3099,7 @@ bool ComposeControls::updateReplaceMediaButton() { _regularWindow, _editingId, _field->getTextWithTags(), + _header->suggestOptions(), queryToEdit.spoilered, queryToEdit.options.invertCaption, crl::guard(_wrap.get(), [=] { cancelEditMessage(); })); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index b8fe4fe0ab..076c1f7717 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -95,13 +95,17 @@ void ChooseSuggestPriceBox( state->buttons.push_back({ .text = Ui::Text::String( st::semiboldTextStyle, - tr::lng_suggest_options_stars_offer(tr::now)), + (args.mode == SuggestMode::ChangeAdmin + ? tr::lng_suggest_options_stars_request(tr::now) + : tr::lng_suggest_options_stars_offer(tr::now))), .active = !state->ton.current(), }); state->buttons.push_back({ .text = Ui::Text::String( st::semiboldTextStyle, - tr::lng_suggest_options_ton_offer(tr::now)), + (args.mode == SuggestMode::ChangeAdmin + ? tr::lng_suggest_options_ton_request(tr::now) + : tr::lng_suggest_options_ton_offer(tr::now))), .active = state->ton.current(), }); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h index b4b49fd671..55063b1f97 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h @@ -29,7 +29,8 @@ namespace HistoryView { enum class SuggestMode { New, - Change, + ChangeUser, + ChangeAdmin, }; struct SuggestTimeBoxArgs { diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 698e46bd19..8db3f9d8c4 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/gift_premium_box.h" // ResolveGiftCode #include "chat_helpers/stickers_gift_box_pack.h" +#include "chat_helpers/stickers_lottie.h" #include "core/click_handler_types.h" // ClickHandlerContext #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" @@ -47,7 +48,7 @@ PremiumGift::PremiumGift( PremiumGift::~PremiumGift() = default; int PremiumGift::top() { - return starGift() + return (starGift() || tonGift()) ? st::msgServiceStarGiftStickerTop : st::msgServiceGiftBoxStickerTop; } @@ -57,7 +58,7 @@ int PremiumGift::width() { } QSize PremiumGift::size() { - return starGift() + return (starGift() || tonGift()) ? QSize( st::msgServiceStarGiftStickerSize, st::msgServiceStarGiftStickerSize) @@ -68,7 +69,10 @@ QSize PremiumGift::size() { TextWithEntities PremiumGift::title() { using namespace Ui::Text; - if (starGift()) { + if (tonGift()) { + AssertIsDebug(); + return { QString::number(_data.count / 1'000'000'000LL) + u" TON"_q }; + } else if (starGift()) { const auto peer = _parent->history()->peer; return peer->isSelf() ? tr::lng_action_gift_self_subtitle(tr::now, WithEntities) @@ -114,7 +118,10 @@ TextWithEntities PremiumGift::title() { } TextWithEntities PremiumGift::subtitle() { - if (starGift()) { + if (tonGift()) { + AssertIsDebug(); + return { u"Use TON to suggest posts to channels."_q }; + } else if (starGift()) { const auto toChannel = _data.channel && _parent->history()->peer->isServiceUser(); return !_data.message.empty() @@ -242,6 +249,14 @@ bool PremiumGift::buttonMinistars() { } ClickHandlerPtr PremiumGift::createViewLink() { + if (tonGift()) { + return std::make_shared<LambdaClickHandler>([=](ClickContext context) { + const auto my = context.other.value<ClickHandlerContext>(); + if (const auto window = my.sessionWindow.get()) { + window->showSettings(Settings::CreditsId()); + } + }); + } if (auto link = OpenStarGiftLink(_parent->data())) { return link; } @@ -401,6 +416,17 @@ int PremiumGift::credits() const { void PremiumGift::ensureStickerCreated() const { if (_sticker) { return; + } else if (tonGift()) { + const auto document = ChatHelpers::GenerateLocalTgsSticker( + &_parent->history()->session(), + "diamond"); + const auto sticker = document->sticker(); + Assert(sticker != nullptr); + _sticker.emplace(_parent, document, false, _parent); + _sticker->setPlayingOnce(true); + _sticker->initSize(st::msgServiceStarGiftStickerSize); + _parent->repaint(); + return; } else if (const auto document = _data.document) { const auto sticker = document->sticker(); Assert(sticker != nullptr); diff --git a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp index 163278761e..14238743c8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_suggest_decision.cpp @@ -143,8 +143,12 @@ auto GenerateSuggestDecisionMedia( TextWithEntities( ).append(Emoji(kWarning)).append(' ').append( (sublistPeer->isSelf() - ? tr::lng_suggest_action_your_not_enough - : tr::lng_suggest_action_his_not_enough)( + ? (decision->price.ton() + ? tr::lng_suggest_action_your_not_enough_ton + : tr::lng_suggest_action_your_not_enough_stars) + : (decision->price.ton() + ? tr::lng_suggest_action_his_not_enough_ton + : tr::lng_suggest_action_his_not_enough_stars))( tr::now, Ui::Text::RichLangValue)), st::chatSuggestInfoFullMargin, @@ -237,7 +241,9 @@ auto GenerateSuggestDecisionMedia( pushText( TextWithEntities( ).append(Emoji(kHourglass)).append(' ').append( - tr::lng_suggest_action_agree_receive( + (price.ton() + ? tr::lng_suggest_action_agree_receive_ton + : tr::lng_suggest_action_agree_receive_stars)( tr::now, lt_channel, Ui::Text::Bold(broadcast->name()), @@ -247,7 +253,9 @@ auto GenerateSuggestDecisionMedia( pushText( TextWithEntities( ).append(Emoji(kReload)).append(' ').append( - tr::lng_suggest_action_agree_removed( + (price.ton() + ? tr::lng_suggest_action_agree_removed_ton + : tr::lng_suggest_action_agree_removed_stars)( tr::now, lt_channel, Ui::Text::Bold(broadcast->name()), From b929e2a7b2b0d36bc23f64c682000a200394fac6 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 26 Jun 2025 13:55:20 +0400 Subject: [PATCH 199/310] Update API scheme on layer 160, show correct warnings. --- Telegram/Resources/langs/lang.strings | 5 +++ .../SourceFiles/boxes/delete_messages_box.cpp | 33 +++++++++++----- .../SourceFiles/boxes/delete_messages_box.h | 4 +- Telegram/SourceFiles/data/data_types.h | 3 +- .../export/data/export_data_types.cpp | 8 ++++ .../export/data/export_data_types.h | 12 +++++- .../export/output/export_output_html.cpp | 9 +++++ .../export/output/export_output_json.cpp | 10 +++++ Telegram/SourceFiles/history/history_item.cpp | 38 ++++++++++++++++--- Telegram/SourceFiles/history/history_item.h | 8 +++- .../history/history_item_components.h | 13 +++++++ .../history/history_item_helpers.cpp | 6 ++- Telegram/SourceFiles/mtproto/scheme/api.tl | 4 +- 13 files changed, 132 insertions(+), 21 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 59438de715..3ff6069205 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4472,6 +4472,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_decline_title" = "Decline"; "lng_suggest_decline_text" = "Do you want to decline publishing this post from {from}?"; "lng_suggest_decline_reason" = "Add a reason (optional)"; +"lng_suggest_warn_title_stars" = "Stars will be lost"; +"lng_suggest_warn_title_ton" = "TON will be lost"; +"lng_suggest_warn_text_stars" = "You won't receive **Stars** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published."; +"lng_suggest_warn_text_ton" = "You won't receive **TON** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published."; +"lng_suggest_warn_delete_anyway" = "Delete Anyway"; "lng_reply_in_another_title" = "Reply in..."; "lng_reply_in_another_chat" = "Reply in Another Chat"; diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.cpp b/Telegram/SourceFiles/boxes/delete_messages_box.cpp index cee747e797..885ce1d2b6 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/delete_messages_box.cpp @@ -499,23 +499,32 @@ void DeleteMessagesBox::keyPressEvent(QKeyEvent *e) { } } -bool DeleteMessagesBox::hasPaidSuggestedPosts() const { +PaidPostType DeleteMessagesBox::paidPostType() const { + auto result = PaidPostType::None; const auto now = base::unixtime::now(); for (const auto &id : _ids) { if (const auto item = _session->data().message(id)) { - if (item->isPaidSuggestedPost()) { + const auto type = item->paidType(); + if (type != PaidPostType::None) { const auto date = item->date(); if (now < date || now - date <= kPaidShowLive) { - return true; + if (type == PaidPostType::Ton) { + return type; + } else if (type == PaidPostType::Stars) { + result = type; + } } } } } - return false; + return result; } void DeleteMessagesBox::deleteAndClear() { - if (hasPaidSuggestedPosts() && !_confirmedDeletePaidSuggestedPosts) { + const auto warnPaidType = _confirmedDeletePaidSuggestedPosts + ? PaidPostType::None + : paidPostType(); + if (warnPaidType != PaidPostType::None) { const auto weak = Ui::MakeWeak(this); const auto callback = [=](Fn<void()> close) { close(); @@ -524,13 +533,19 @@ void DeleteMessagesBox::deleteAndClear() { strong->deleteAndClear(); } }; - AssertIsDebug(); + const auto ton = (warnPaidType == PaidPostType::Ton); uiShow()->show(Ui::MakeConfirmBox({ - .text = u"You won't receive Stars for this post if you delete it now. The post must remain visible for at least 24 hours after it was published."_q, + .text = (ton + ? tr::lng_suggest_warn_text_ton + : tr::lng_suggest_warn_text_stars)( + tr::now, + Ui::Text::RichLangValue), .confirmed = callback, - .confirmText = u"Delete Anyway"_q, + .confirmText = tr::lng_suggest_warn_delete_anyway(tr::now), .confirmStyle = &st::attentionBoxButton, - .title = u"Stars will be lost"_q, + .title = (ton + ? tr::lng_suggest_warn_title_ton + : tr::lng_suggest_warn_title_stars)(tr::now), })); return; } diff --git a/Telegram/SourceFiles/boxes/delete_messages_box.h b/Telegram/SourceFiles/boxes/delete_messages_box.h index ce5d430c8b..3762212b0d 100644 --- a/Telegram/SourceFiles/boxes/delete_messages_box.h +++ b/Telegram/SourceFiles/boxes/delete_messages_box.h @@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/layers/box_content.h" +enum class PaidPostType : uchar; + namespace Main { class Session; } // namespace Main @@ -58,7 +60,7 @@ private: [[nodiscard]] bool hasScheduledMessages() const; [[nodiscard]] std::optional<RevokeConfig> revokeText( not_null<PeerData*> peer) const; - [[nodiscard]] bool hasPaidSuggestedPosts() const; + [[nodiscard]] PaidPostType paidPostType() const; const not_null<Main::Session*> _session; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 46da299a9b..d0dce0f1ea 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -354,7 +354,8 @@ enum class MessageFlag : uint64 { HideDisplayDate = (1ULL << 51), - PaidSuggestedPost = (1ULL << 52), + StarsPaidSuggested = (1ULL << 52), + TonPaidSuggested = (1ULL << 53), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags<MessageFlag>; diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 8b7cf589c1..e4ef4a0a6f 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1775,6 +1775,14 @@ ServiceAction ParseServiceAction( .rejected = data.is_rejected(), .balanceTooLow = data.is_balance_too_low(), }; + }, [&](const MTPDmessageActionSuggestedPostSuccess &data) { + result.content = ActionSuggestedPostSuccess{ + .price = CreditsAmountFromTL(data.vprice()), + }; + }, [&](const MTPDmessageActionSuggestedPostRefund &data) { + result.content = ActionSuggestedPostRefund{ + .payerInitiated = data.is_payer_initiated(), + }; }, [&](const MTPDmessageActionConferenceCall &data) { auto content = ActionPhoneCall(); using State = ActionPhoneCall::State; diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index 805e2d7682..2c650871a3 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -707,6 +707,14 @@ struct ActionSuggestedPostApproval { bool balanceTooLow = false; }; +struct ActionSuggestedPostSuccess { + CreditsAmount price; +}; + +struct ActionSuggestedPostRefund { + bool payerInitiated = false; +}; + struct ServiceAction { std::variant< v::null_t, @@ -757,7 +765,9 @@ struct ServiceAction { ActionPaidMessagesPrice, ActionTodoCompletions, ActionTodoAppendTasks, - ActionSuggestedPostApproval> content; + ActionSuggestedPostApproval, + ActionSuggestedPostSuccess, + ActionSuggestedPostRefund> content; }; ServiceAction ParseServiceAction( diff --git a/Telegram/SourceFiles/export/output/export_output_html.cpp b/Telegram/SourceFiles/export/output/export_output_html.cpp index 5ada50c0bb..11b0e4913b 100644 --- a/Telegram/SourceFiles/export/output/export_output_html.cpp +++ b/Telegram/SourceFiles/export/output/export_output_html.cpp @@ -1466,6 +1466,15 @@ auto HtmlWriter::Wrap::pushMessage( : (", with comment: "" + SerializeString(data.rejectComment) + """)); + }, [&](const ActionSuggestedPostSuccess &data) { + return "The paid post was shown for 24 hours and " + + QString::number(data.price.value()).toUtf8() + + (data.price.ton() ? " TON" : " stars") + + " were transferred to the channel."; + }, [&](const ActionSuggestedPostRefund &data) { + return QByteArray() + (data.payerInitiated + ? "The user refunded the payment, post was deleted." + : "The admin deleted the post early, the payment was refunded."); }, [](v::null_t) { return QByteArray(); }); if (!serviceText.isEmpty()) { diff --git a/Telegram/SourceFiles/export/output/export_output_json.cpp b/Telegram/SourceFiles/export/output/export_output_json.cpp index b1317a9103..b591bfb3dd 100644 --- a/Telegram/SourceFiles/export/output/export_output_json.cpp +++ b/Telegram/SourceFiles/export/output/export_output_json.cpp @@ -725,6 +725,16 @@ QByteArray SerializeMessage( push("price_currency", data.price.ton() ? "TON" : "Stars"); push("scheduled_date", data.scheduleDate); } + }, [&](const ActionSuggestedPostSuccess &data) { + pushActor(); + pushAction("suggested_post_success"); + push("price_amount_whole", NumberToString(data.price.whole())); + push("price_amount_nano", NumberToString(data.price.nano())); + push("price_currency", data.price.ton() ? "TON" : "Stars"); + }, [&](const ActionSuggestedPostRefund &data) { + pushActor(); + pushAction("suggested_post_refund"); + push("user_initiated", data.payerInitiated); }, [](v::null_t) {}); if (v::is_null(message.action.content)) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 311f5c3d4c..d3800408c7 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -830,6 +830,8 @@ HistoryServiceDependentData *HistoryItem::GetServiceDependentData() { return append; } else if (const auto decision = Get<HistoryServiceSuggestDecision>()) { return decision; + } else if (const auto finish = Get<HistoryServiceSuggestFinish>()) { + return finish; } return nullptr; } @@ -1645,8 +1647,12 @@ bool HistoryItem::isEditingMedia() const { return Has<HistoryMessageSavedMediaData>(); } -bool HistoryItem::isPaidSuggestedPost() const { - return _flags & MessageFlag::PaidSuggestedPost; +PaidPostType HistoryItem::paidType() const { + return (_flags & MessageFlag::StarsPaidSuggested) + ? PaidPostType::Stars + : (_flags & MessageFlag::TonPaidSuggested) + ? PaidPostType::Ton + : PaidPostType::None; } void HistoryItem::clearSavedMedia() { @@ -2474,7 +2480,7 @@ bool HistoryItem::allowsSendNow() const { && !isSending() && !hasFailed() && !isEditingMedia() - && !isPaidSuggestedPost(); + && (paidType() == PaidPostType::None); } bool HistoryItem::allowsReschedule() const { @@ -2502,7 +2508,7 @@ bool HistoryItem::allowsEdit(TimeId now) const { && (!_media || _media->allowsEdit()) && !isLegacyMessage() && !isEditingMedia() - && !isPaidSuggestedPost(); + && (paidType() == PaidPostType::None); } bool HistoryItem::allowsEditMedia() const { @@ -4673,6 +4679,18 @@ void HistoryItem::createServiceFromMtp(const MTPDmessageService &message) { decision->rejected = data.is_rejected(); decision->rejectComment = qs(data.vreject_comment().value_or_empty()); decision->date = data.vschedule_date().value_or_empty(); + } else if (type == mtpc_messageActionSuggestedPostSuccess) { + const auto &data = action.c_messageActionSuggestedPostSuccess(); + UpdateComponents(HistoryServiceSuggestFinish::Bit()); + const auto finish = Get<HistoryServiceSuggestFinish>(); + finish->successPrice = CreditsAmountFromTL(data.vprice()); + } else if (type == mtpc_messageActionSuggestedPostRefund) { + const auto &data = action.c_messageActionSuggestedPostRefund(); + UpdateComponents(HistoryServiceSuggestFinish::Bit()); + const auto finish = Get<HistoryServiceSuggestFinish>(); + finish->refundType = data.is_payer_initiated() + ? SuggestRefundType::User + : SuggestRefundType::Admin; } if (const auto replyTo = message.vreply_to()) { replyTo->match([&](const MTPDmessageReplyHeader &data) { @@ -6058,7 +6076,15 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { }; auto prepareSuggestedPostApproval = [&](const MTPDmessageActionSuggestedPostApproval &data) { - return PreparedServiceText{ { u"hello"_q } }; + return PreparedServiceText{ { tr::lng_suggest_action_agreement(tr::now) } }; + }; + + auto prepareSuggestedPostSuccess = [&](const MTPDmessageActionSuggestedPostSuccess &data) { + return PreparedServiceText{ { u"hello"_q } }; AssertIsDebug(); + }; + + auto prepareSuggestedPostRefund = [&](const MTPDmessageActionSuggestedPostRefund &data) { + return PreparedServiceText{ { u"hello"_q } }; AssertIsDebug(); }; auto prepareConferenceCall = [&](const MTPDmessageActionConferenceCall &) -> PreparedServiceText { @@ -6119,6 +6145,8 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) { prepareTodoCompletions, prepareTodoAppendTasks, prepareSuggestedPostApproval, + prepareSuggestedPostSuccess, + prepareSuggestedPostRefund, PrepareEmptyText<MTPDmessageActionRequestedPeerSentMe>, PrepareErrorText<MTPDmessageActionEmpty>)); diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 62bb3b6e94..7126716fd5 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -95,6 +95,12 @@ enum class HistoryReactionSource : char { Existing, }; +enum class PaidPostType : uchar { + None, + Stars, + Ton, +}; + class HistoryItem final : public RuntimeComposer<HistoryItem> { public: [[nodiscard]] static std::unique_ptr<Data::Media> CreateMedia( @@ -314,7 +320,6 @@ public: [[nodiscard]] bool hasRealFromId() const; [[nodiscard]] bool isPostHidingAuthor() const; [[nodiscard]] bool isPostShowingAuthor() const; - [[nodiscard]] bool isPaidSuggestedPost() const; [[nodiscard]] bool isRegular() const; [[nodiscard]] bool isUploading() const; void sendFailed(); @@ -325,6 +330,7 @@ public: [[nodiscard]] bool hasUnpaidContent() const; [[nodiscard]] bool inHighlightProcess() const; void highlightProcessDone(); + [[nodiscard]] PaidPostType paidType() const; void setCommentsInboxReadTill(MsgId readTillId); void setCommentsMaxId(MsgId maxId); diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index f934726c64..4ca58bde84 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -708,6 +708,19 @@ struct HistoryServiceSuggestDecision bool balanceTooLow = false; }; +enum class SuggestRefundType { + None, + User, + Admin, +}; + +struct HistoryServiceSuggestFinish +: RuntimeComponent<HistoryServiceSuggestFinish, HistoryItem> +, HistoryServiceDependentData { + CreditsAmount successPrice; + SuggestRefundType refundType = SuggestRefundType::None; +}; + struct HistoryServiceGameScore : RuntimeComponent<HistoryServiceGameScore, HistoryItem> , HistoryServiceDependentData { diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 3297854a55..81f52890cb 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -763,8 +763,10 @@ MessageFlags FlagsFromMTP( | ((flags & MTP::f_video_processing_pending) ? Flag::EstimatedDate : Flag()) - | ((flags & MTP::f_paid_suggested_post) - ? Flag::PaidSuggestedPost + | ((flags & MTP::f_paid_suggested_post_ton) + ? Flag::TonPaidSuggested + : (flags & MTP::f_paid_suggested_post_stars) + ? Flag::StarsPaidSuggested : Flag()); } diff --git a/Telegram/SourceFiles/mtproto/scheme/api.tl b/Telegram/SourceFiles/mtproto/scheme/api.tl index 54f9cd1816..2a8ccb6b4b 100644 --- a/Telegram/SourceFiles/mtproto/scheme/api.tl +++ b/Telegram/SourceFiles/mtproto/scheme/api.tl @@ -117,7 +117,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post:flags2.8?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message; +message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message; messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -193,6 +193,8 @@ messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1? messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction; messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction; messageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction; +messageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction; +messageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction; messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; From 4c1b962486253499c95ef182482e37cb7b2391ee Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 26 Jun 2025 16:30:16 +0400 Subject: [PATCH 200/310] Support adding an offer to existing message. --- Telegram/Resources/langs/lang.strings | 2 + Telegram/SourceFiles/api/api_suggest_post.cpp | 149 ++++++++++-------- Telegram/SourceFiles/api/api_suggest_post.h | 8 + .../history/history_inner_widget.cpp | 15 +- Telegram/SourceFiles/history/history_item.cpp | 19 ++- Telegram/SourceFiles/history/history_item.h | 2 + .../SourceFiles/history/history_widget.cpp | 4 + .../controls/history_view_suggest_options.cpp | 15 ++ .../controls/history_view_suggest_options.h | 2 + 9 files changed, 144 insertions(+), 72 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3ff6069205..10eaf2cf9f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4232,6 +4232,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_edit_msg" = "Edit"; "lng_context_add_factcheck" = "Add Fact Check"; "lng_context_edit_factcheck" = "Edit Fact Check"; +"lng_context_add_offer" = "Add Offer"; "lng_context_forward_msg" = "Forward"; "lng_context_send_now_msg" = "Send Now"; "lng_context_reschedule" = "Reschedule"; @@ -5363,6 +5364,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_restricted_send_polls_all" = "Sorry, sending polls is not allowed in this group."; "lng_restricted_send_public_polls" = "Sorry, polls with visible votes can't be forwarded to channels."; +"lng_restricted_send_todo_lists" = "Sorry, To-Do lists can't be forwarded to channels."; "lng_restricted_send_paid_media" = "Sorry, paid media can't be sent to this channel."; "lng_restricted_send_voice_messages" = "{user} doesn't accept voice messages."; diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index f3bba3c9ae..b28699d618 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -37,7 +37,7 @@ namespace Api { namespace { void SendApproval( - not_null<Window::SessionController*> controller, + std::shared_ptr<Main::SessionShow> show, not_null<HistoryItem*> item, TimeId scheduleDate = 0) { using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag; @@ -50,8 +50,7 @@ void SendApproval( } const auto id = item->fullId(); - const auto weak = base::make_weak(controller); - const auto session = &controller->session(); + const auto session = &show->session(); const auto finish = [=] { if (const auto item = session->data().message(id)) { const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); @@ -71,15 +70,13 @@ void SendApproval( session->api().applyUpdates(result); finish(); }).fail([=](const MTP::Error &error) { - if (const auto window = weak.get()) { - window->showToast(error.type()); - } + show->showToast(error.type()); finish(); }).send(); } void SendDecline( - not_null<Window::SessionController*> controller, + std::shared_ptr<Main::SessionShow> show, not_null<HistoryItem*> item, const QString &comment) { using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag; @@ -92,8 +89,7 @@ void SendDecline( } const auto id = item->fullId(); - const auto weak = base::make_weak(controller); - const auto session = &controller->session(); + const auto session = &show->session(); const auto finish = [=] { if (const auto item = session->data().message(id)) { const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); @@ -114,21 +110,19 @@ void SendDecline( session->api().applyUpdates(result); finish(); }).fail([=](const MTP::Error &error) { - if (const auto window = weak.get()) { - window->showToast(error.type()); - } + show->showToast(error.type()); finish(); }).send(); } void RequestApprovalDate( - not_null<Window::SessionController*> controller, + std::shared_ptr<Main::SessionShow> show, not_null<HistoryItem*> item) { const auto id = item->fullId(); const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); const auto done = [=](TimeId result) { - if (const auto item = controller->session().data().message(id)) { - SendApproval(controller, item, result); + if (const auto item = show->session().data().message(id)) { + SendApproval(show, item, result); } if (const auto strong = weak->data()) { strong->closeBox(); @@ -136,19 +130,19 @@ void RequestApprovalDate( }; using namespace HistoryView; auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ - .session = &controller->session(), + .session = &show->session(), .done = done, .mode = SuggestMode::New, }); *weak = dateBox.data(); - controller->uiShow()->show(std::move(dateBox)); + show->show(std::move(dateBox)); } void RequestDeclineComment( - not_null<Window::SessionController*> controller, + std::shared_ptr<Main::SessionShow> show, not_null<HistoryItem*> item) { const auto id = item->fullId(); - controller->uiShow()->show(Box([=](not_null<Ui::GenericBox*> box) { + show->show(Box([=](not_null<Ui::GenericBox*> box) { const auto callback = std::make_shared<Fn<void()>>(); Ui::ConfirmBox(box, { .text = tr::lng_suggest_decline_text( @@ -169,11 +163,11 @@ void RequestDeclineComment( reason->setFocusFast(); }); *callback = [=, weak = Ui::MakeWeak(box)] { - const auto item = controller->session().data().message(id); + const auto item = show->session().data().message(id); if (!item) { return; } - SendDecline(controller, item, reason->getLastText().trimmed()); + SendDecline(show, item, reason->getLastText().trimmed()); if (const auto strong = weak.data()) { strong->closeBox(); } @@ -191,24 +185,21 @@ struct SendSuggestState { SendPaymentHelper sendPayment; }; void SendSuggest( - not_null<Window::SessionController*> controller, + std::shared_ptr<Main::SessionShow> show, not_null<HistoryItem*> item, std::shared_ptr<SendSuggestState> state, Fn<void(SuggestPostOptions&)> modify, Fn<void()> done = nullptr, int starsApproved = 0) { const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); - if (!suggestion) { - return; - } const auto id = item->fullId(); const auto withPaymentApproved = [=](int stars) { - if (const auto item = controller->session().data().message(id)) { - SendSuggest(controller, item, state, modify, done, stars); + if (const auto item = show->session().data().message(id)) { + SendSuggest(show, item, state, modify, done, stars); } }; const auto checked = state->sendPayment.check( - controller->uiShow(), + show, item->history()->peer, 1, starsApproved, @@ -218,18 +209,23 @@ void SendSuggest( } const auto isForward = item->Get<HistoryMessageForwarded>(); auto action = SendAction(item->history()); + action.options.suggest.exists = 1; - action.options.suggest.date = suggestion->date; - action.options.suggest.priceWhole = suggestion->price.whole(); - action.options.suggest.priceNano = suggestion->price.nano(); - action.options.suggest.ton = suggestion->price.ton() ? 1 : 0; + if (suggestion) { + action.options.suggest.date = suggestion->date; + action.options.suggest.priceWhole = suggestion->price.whole(); + action.options.suggest.priceNano = suggestion->price.nano(); + action.options.suggest.ton = suggestion->price.ton() ? 1 : 0; + } + modify(action.options.suggest); + action.options.starsApproved = starsApproved; action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin() ? item->sublistPeerId() : PeerId(); action.replyTo.messageId = item->fullId(); - modify(action.options.suggest); - controller->session().api().forwardMessages({ + show->session().api().sendAction(action); + show->session().api().forwardMessages({ .items = { item }, .options = (isForward ? Data::ForwardOptions::PreserveInfo @@ -241,7 +237,7 @@ void SendSuggest( } void SuggestApprovalDate( - not_null<Window::SessionController*> controller, + std::shared_ptr<Main::SessionShow> show, not_null<HistoryItem*> item) { const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); if (!suggestion) { @@ -251,7 +247,7 @@ void SuggestApprovalDate( const auto state = std::make_shared<SendSuggestState>(); const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); const auto done = [=](TimeId result) { - const auto item = controller->session().data().message(id); + const auto item = show->session().data().message(id); if (!item) { return; } @@ -261,7 +257,7 @@ void SuggestApprovalDate( } }; SendSuggest( - controller, + show, item, state, [=](SuggestPostOptions &options) { options.date = result; }, @@ -270,27 +266,25 @@ void SuggestApprovalDate( using namespace HistoryView; const auto admin = item->history()->amMonoforumAdmin(); auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{ - .session = &controller->session(), + .session = &show->session(), .done = done, .value = suggestion->date, .mode = (admin ? SuggestMode::ChangeAdmin : SuggestMode::ChangeUser), }); *weak = dateBox.data(); - controller->uiShow()->show(std::move(dateBox)); + show->show(std::move(dateBox)); } -void SuggestApprovalPrice( - not_null<Window::SessionController*> controller, - not_null<HistoryItem*> item) { - const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); - if (!suggestion) { - return; - } +void SuggestOfferForMessage( + std::shared_ptr<Main::SessionShow> show, + not_null<HistoryItem*> item, + SuggestPostOptions values, + HistoryView::SuggestMode mode) { const auto id = item->fullId(); const auto state = std::make_shared<SendSuggestState>(); const auto weak = std::make_shared<QPointer<Ui::BoxContent>>(); const auto done = [=](SuggestPostOptions result) { - const auto item = controller->session().data().message(id); + const auto item = show->session().data().message(id); if (!item) { return; } @@ -300,28 +294,39 @@ void SuggestApprovalPrice( } }; SendSuggest( - controller, + show, item, state, [=](SuggestPostOptions &options) { options = result; }, close); }; using namespace HistoryView; - const auto admin = item->history()->amMonoforumAdmin(); - auto dateBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ - .session = &controller->session(), + auto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ + .session = &show->session(), .done = done, - .value = { - .exists = uint32(1), - .priceWhole = uint32(suggestion->price.whole()), - .priceNano = uint32(suggestion->price.nano()), - .ton = uint32(suggestion->price.ton() ? 1 : 0), - .date = suggestion->date, - }, - .mode = (admin ? SuggestMode::ChangeAdmin : SuggestMode::ChangeUser), + .value = values, + .mode = mode, }); - *weak = dateBox.data(); - controller->uiShow()->show(std::move(dateBox)); + *weak = priceBox.data(); + show->show(std::move(priceBox)); +} + +void SuggestApprovalPrice( + std::shared_ptr<Main::SessionShow> show, + not_null<HistoryItem*> item) { + const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); + if (!suggestion) { + return; + } + const auto admin = item->history()->amMonoforumAdmin(); + using namespace HistoryView; + SuggestOfferForMessage(show, item, { + .exists = uint32(1), + .priceWhole = uint32(suggestion->price.whole()), + .priceNano = uint32(suggestion->price.nano()), + .ton = uint32(suggestion->price.ton() ? 1 : 0), + .date = suggestion->date, + }, admin ? SuggestMode::ChangeAdmin : SuggestMode::ChangeUser); } } // namespace @@ -340,13 +345,14 @@ std::shared_ptr<ClickHandler> AcceptClickHandler( if (!item) { return; } + const auto show = controller->uiShow(); const auto suggestion = item->Get<HistoryMessageSuggestedPost>(); if (!suggestion) { return; } else if (!suggestion->date) { - RequestApprovalDate(controller, item); + RequestApprovalDate(show, item); } else { - SendApproval(controller, item); + SendApproval(show, item); } }); } @@ -360,7 +366,7 @@ std::shared_ptr<ClickHandler> DeclineClickHandler( if (!controller) { return; } - RequestDeclineComment(controller, item); + RequestDeclineComment(controller->uiShow(), item); }); } @@ -426,16 +432,27 @@ std::shared_ptr<ClickHandler> SuggestChangesClickHandler( } menu->addAction(tr::lng_suggest_menu_edit_price(tr::now), [=] { if (const auto item = session->data().message(id)) { - SuggestApprovalPrice(window, item); + SuggestApprovalPrice(window->uiShow(), item); } }, &st::menuIconTagSell); menu->addAction(tr::lng_suggest_menu_edit_time(tr::now), [=] { if (const auto item = session->data().message(id)) { - SuggestApprovalDate(window, item); + SuggestApprovalDate(window->uiShow(), item); } }, &st::menuIconSchedule); menu->popup(QCursor::pos()); }); } +void AddOfferToMessage( + std::shared_ptr<Main::SessionShow> show, + FullMsgId itemId) { + const auto session = &show->session(); + const auto item = session->data().message(itemId); + if (!item || !HistoryView::CanAddOfferToMessage(item)) { + return; + } + SuggestOfferForMessage(show, item, {}, HistoryView::SuggestMode::New); +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_suggest_post.h b/Telegram/SourceFiles/api/api_suggest_post.h index 0584ce7be7..582f0adca0 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.h +++ b/Telegram/SourceFiles/api/api_suggest_post.h @@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class ClickHandler; +namespace Main { +class SessionShow; +} // namespace Main + namespace Api { [[nodiscard]] std::shared_ptr<ClickHandler> AcceptClickHandler( @@ -18,4 +22,8 @@ namespace Api { [[nodiscard]] std::shared_ptr<ClickHandler> SuggestChangesClickHandler( not_null<HistoryItem*> item); +void AddOfferToMessage( + std::shared_ptr<Main::SessionShow> show, + FullMsgId itemId); + } // namespace Api diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 0fe7e05bdf..c5e7a82f6c 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_helpers.h" #include "history/view/controls/history_view_forward_panel.h" #include "history/view/controls/history_view_draft_options.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" #include "history/view/reactions/history_view_reactions.h" @@ -76,6 +77,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "apiwrap.h" #include "api/api_attached_stickers.h" +#include "api/api_suggest_post.h" #include "api/api_toggling_media.h" #include "api/api_who_reacted.h" #include "api/api_views.h" @@ -2410,9 +2412,6 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { highlightId); }, &st::menuIconViewReplies); } - _menu->addAction(u"Add Offer"_q, [=] { - - }, &st::menuIconDiscussion); const auto t = base::unixtime::now(); const auto editItem = (albumPartItem && albumPartItem->allowsEdit(t)) ? albumPartItem @@ -2812,6 +2811,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { forwardItem(itemId); }, &st::menuIconForward); } + if (HistoryView::CanAddOfferToMessage(item)) { + _menu->addAction(tr::lng_context_add_offer(tr::now), [=] { + Api::AddOfferToMessage(_controller->uiShow(), itemId); + }, &st::menuIconTagSell); + } if (item->canDelete()) { const auto callback = [=] { deleteItem(itemId); }; if (item->isUploading()) { @@ -3062,6 +3066,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { forwardAsGroup(itemId); }, &st::menuIconForward); } + if (HistoryView::CanAddOfferToMessage(item)) { + _menu->addAction(tr::lng_context_add_offer(tr::now), [=] { + Api::AddOfferToMessage(_controller->uiShow(), itemId); + }, &st::menuIconTagSell); + } if (canDelete) { const auto callback = [=] { deleteAsGroup(itemId); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index d3800408c7..8394682656 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2695,11 +2695,26 @@ Data::SendError HistoryItem::errorTextForForward( } else if (requiresInline && !Data::CanSend(to, kInline)) { const auto forInline = Data::RestrictionError(peer, kInline); return forInline ? forInline : tr::lng_forward_cant(tr::now); - } else if (_media + } else if (const auto specific = errorTextForForwardIgnoreRights(to)) { + return specific; + } else if (!Data::CanSend(to, requiredRight, false)) { + return tr::lng_forward_cant(tr::now); + } + return {}; +} + +Data::SendError HistoryItem::errorTextForForwardIgnoreRights( + not_null<Data::Thread*> to) const { + const auto peer = to->peer(); + if (_media && _media->poll() && _media->poll()->publicVotes() && peer->isBroadcast()) { return tr::lng_restricted_send_public_polls(tr::now); + } else if (_media + && _media->todolist() + && (peer->isBroadcast() || peer->isMonoforum())) { + return tr::lng_restricted_send_todo_lists(tr::now); } else if (_media && _media->invoice() && _media->invoice()->isPaidMedia @@ -2707,8 +2722,6 @@ Data::SendError HistoryItem::errorTextForForward( && peer->isFullLoaded() && !peer->asBroadcast()->canPostPaidMedia()) { return tr::lng_restricted_send_paid_media(tr::now); - } else if (!Data::CanSend(to, requiredRight, false)) { - return tr::lng_forward_cant(tr::now); } return {}; } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 7126716fd5..978624e85e 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -437,6 +437,8 @@ public: [[nodiscard]] bool requiresSendInlineRight() const; [[nodiscard]] Data::SendError errorTextForForward( not_null<Data::Thread*> to) const; + [[nodiscard]] Data::SendError errorTextForForwardIgnoreRights( + not_null<Data::Thread*> to) const; [[nodiscard]] const HistoryMessageTranslation *translation() const; [[nodiscard]] bool translationShowRequiresCheck(LanguageId to) const; bool translationShowRequiresRequest(LanguageId to); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 1347f00640..2dc081cde4 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2200,6 +2200,10 @@ void HistoryWidget::fastShowAtEnd(not_null<History*> history) { _pinnedClickedId = FullMsgId(); _minPinnedId = std::nullopt; if (_history->isReadyFor(_showAtMsgId)) { + _history->forgetScrollState(); + if (_migrated) { + _migrated->forgetScrollState(); + } historyLoaded(); } else { firstLoadMessages(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index 076c1f7717..805d5b5bb8 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -14,7 +14,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_channel.h" #include "data/data_media_types.h" #include "data/data_session.h" +#include "history/history.h" #include "history/history_item.h" +#include "history/history_item_components.h" #include "info/channel_statistics/earn/earn_icons.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -384,6 +386,19 @@ bool CanEditSuggestedMessage(not_null<HistoryItem*> item) { return !media || media->allowsEditCaption(); } +bool CanAddOfferToMessage(not_null<HistoryItem*> item) { + const auto history = item->history(); + const auto broadcast = history->peer->monoforumBroadcast(); + return broadcast + && !history->amMonoforumAdmin() + && !item->Get<HistoryMessageSuggestedPost>() + && !item->groupId() + && item->isRegular() + && !item->isService() + && !item->errorTextForForwardIgnoreRights( + history->owner().history(broadcast)).has_value(); +} + SuggestOptions::SuggestOptions( std::shared_ptr<ChatHelpers::Show> show, not_null<PeerData*> peer, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h index 55063b1f97..b16e3e6c0a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.h @@ -56,6 +56,8 @@ void ChooseSuggestPriceBox( [[nodiscard]] bool CanEditSuggestedMessage(not_null<HistoryItem*> item); +[[nodiscard]] bool CanAddOfferToMessage(not_null<HistoryItem*> item); + class SuggestOptions final { public: SuggestOptions( From c83bae3bb5f898c1317eeca1c6a152d859d042de Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Thu, 26 Jun 2025 16:56:37 +0400 Subject: [PATCH 201/310] Use correct icon in post suggesting. --- Telegram/Resources/icons/chat/input_paid.svg | 8 ++++++++ Telegram/SourceFiles/chat_helpers/chat_helpers.style | 9 +++++---- .../view/controls/history_view_suggest_options.cpp | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 Telegram/Resources/icons/chat/input_paid.svg diff --git a/Telegram/Resources/icons/chat/input_paid.svg b/Telegram/Resources/icons/chat/input_paid.svg new file mode 100644 index 0000000000..1179751c9a --- /dev/null +++ b/Telegram/Resources/icons/chat/input_paid.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>Icon / Input / input_paid + + + + + \ No newline at end of file diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 63bbaede49..a2158bbe20 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1144,11 +1144,12 @@ historyGiftToUser: IconButton(historyAttach) { icon: icon {{ "chat/input_gift", historyComposeIconFg }}; iconOver: icon {{ "chat/input_gift", historyComposeIconFgOver }}; } -historySuggestPostToggle: IconButton(historyDirectMessage) { - icon: icon{{ "menu/chat_discuss", historyComposeIconFg }}; - iconOver: icon{{ "menu/chat_discuss", historyComposeIconFgOver }}; +historySuggestPostToggle: IconButton(historyAttach) { + icon: icon{{ "chat/input_paid", historyComposeIconFg }}; + iconOver: icon{{ "chat/input_paid", historyComposeIconFgOver }}; } -historySuggestIconPosition: point(12px, 12px); +historySuggestIconPosition: point(4px, 4px); +historySuggestIconActive: icon{{ "chat/input_paid", windowActiveTextFg }}; suggestOptionsPrice: InputField(defaultInputField) { textBg: transparent; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index 805d5b5bb8..abad2795e5 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -414,7 +414,7 @@ SuggestOptions::SuggestOptions( SuggestOptions::~SuggestOptions() = default; void SuggestOptions::paintIcon(QPainter &p, int x, int y, int outerWidth) { - st::historyDirectMessage.icon.paint( + st::historySuggestIconActive.paint( p, QPoint(x, y) + st::historySuggestIconPosition, outerWidth); From 6f305c8974a08ba856d3edcd30f0f3f048cc56de Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 26 Jun 2025 17:06:14 +0400 Subject: [PATCH 202/310] Improve layout in suggested price box. --- .../history/view/controls/history_view_suggest_options.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index abad2795e5..36d34afca6 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -121,7 +121,10 @@ void ChooseSuggestPriceBox( button.geometry = QRect(QPoint(x, y), r.size()); x += r.width() + st::giftBoxTabSkip; } - const auto buttons = box->addRow(object_ptr(box)); + const auto buttons = box->addRow( + object_ptr(box), + (st::boxRowPadding + - QMargins(padding.left() / 2, 0, padding.right() / 2, 0))); const auto height = y + state->buttons.back().geometry.height() + st::giftBoxTabsMargin.bottom(); From 4840a9094b5eaf45264d6760d844c2a47ecd46f3 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 26 Jun 2025 22:30:55 +0400 Subject: [PATCH 203/310] Check amounts of stars/TON. --- Telegram/Resources/langs/lang.strings | 5 + Telegram/SourceFiles/api/api_suggest_post.cpp | 24 ++-- Telegram/SourceFiles/core/credits_amount.h | 6 +- .../SourceFiles/data/components/credits.cpp | 56 +++++++- .../SourceFiles/data/components/credits.h | 19 ++- Telegram/SourceFiles/data/data_channel.cpp | 8 +- Telegram/SourceFiles/data/data_channel.h | 8 ++ Telegram/SourceFiles/data/data_session.cpp | 23 +-- Telegram/SourceFiles/data/data_user.cpp | 2 + .../history/history_item_helpers.cpp | 130 +++++++++++++---- .../history/history_item_helpers.h | 17 ++- .../SourceFiles/history/history_widget.cpp | 51 +++---- Telegram/SourceFiles/history/history_widget.h | 4 +- .../controls/history_view_suggest_options.cpp | 134 ++++++++++++++++-- .../controls/history_view_suggest_options.h | 7 +- .../view/history_view_chat_section.cpp | 18 +-- .../history/view/history_view_chat_section.h | 2 +- .../inline_bots/bot_attach_web_view.cpp | 4 +- .../media/stories/media_stories_reply.cpp | 39 ++--- .../media/stories/media_stories_reply.h | 2 +- .../settings/settings_credits_graphics.cpp | 9 ++ .../settings/settings_credits_graphics.h | 6 +- Telegram/SourceFiles/ui/chat/chat.style | 8 ++ .../SourceFiles/window/window_peer_menu.cpp | 42 +++--- 24 files changed, 474 insertions(+), 150 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 10eaf2cf9f..078d50f4ab 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2911,6 +2911,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts."; "lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}."; "lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages."; +"lng_credits_small_balance_for_suggest" = "Buy **Stars** to suggest post to {channel}."; "lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram."; "lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars."; "lng_credits_enough" = "You have enough stars at the moment. {link}"; @@ -4478,6 +4479,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_suggest_warn_text_stars" = "You won't receive **Stars** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published."; "lng_suggest_warn_text_ton" = "You won't receive **TON** for the post if you delete it now. The post must remain visible for at least **24 hours** after it was published."; "lng_suggest_warn_delete_anyway" = "Delete Anyway"; +"lng_suggest_low_ton_title" = "{amount} TON Needed"; +"lng_suggest_low_ton_text" = "Buy **TON** to suggest message to {channel} and others."; +"lng_suggest_low_ton_fragment" = "Buy on Fragment"; +"lng_suggest_low_ton_fragment_url" = "https://fragment.com/ads/topup"; "lng_reply_in_another_title" = "Reply in..."; "lng_reply_in_another_chat" = "Reply in Another Chat"; diff --git a/Telegram/SourceFiles/api/api_suggest_post.cpp b/Telegram/SourceFiles/api/api_suggest_post.cpp index b28699d618..a4d6dbf366 100644 --- a/Telegram/SourceFiles/api/api_suggest_post.cpp +++ b/Telegram/SourceFiles/api/api_suggest_post.cpp @@ -198,18 +198,8 @@ void SendSuggest( SendSuggest(show, item, state, modify, done, stars); } }; - const auto checked = state->sendPayment.check( - show, - item->history()->peer, - 1, - starsApproved, - withPaymentApproved); - if (!checked) { - return; - } const auto isForward = item->Get(); auto action = SendAction(item->history()); - action.options.suggest.exists = 1; if (suggestion) { action.options.suggest.date = suggestion->date; @@ -218,12 +208,22 @@ void SendSuggest( action.options.suggest.ton = suggestion->price.ton() ? 1 : 0; } modify(action.options.suggest); - action.options.starsApproved = starsApproved; action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin() ? item->sublistPeerId() : PeerId(); action.replyTo.messageId = item->fullId(); + + const auto checked = state->sendPayment.check( + show, + item->history()->peer, + action.options, + 1, + withPaymentApproved); + if (!checked) { + return; + } + show->session().api().sendAction(action); show->session().api().forwardMessages({ .items = { item }, @@ -302,7 +302,7 @@ void SuggestOfferForMessage( }; using namespace HistoryView; auto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{ - .session = &show->session(), + .peer = item->history()->peer, .done = done, .value = values, .mode = mode, diff --git a/Telegram/SourceFiles/core/credits_amount.h b/Telegram/SourceFiles/core/credits_amount.h index 1704e28161..12b26239c4 100644 --- a/Telegram/SourceFiles/core/credits_amount.h +++ b/Telegram/SourceFiles/core/credits_amount.h @@ -101,8 +101,10 @@ public: return result; } - friend inline auto operator<=>(CreditsAmount, CreditsAmount) = default; - friend inline bool operator==(CreditsAmount, CreditsAmount) = default; + friend inline constexpr auto operator<=>(CreditsAmount, CreditsAmount) + = default; + friend inline constexpr bool operator==(CreditsAmount, CreditsAmount) + = default; [[nodiscard]] CreditsAmount abs() const { return (_whole < 0) ? CreditsAmount(-_whole, -_nano) : *this; diff --git a/Telegram/SourceFiles/data/components/credits.cpp b/Telegram/SourceFiles/data/components/credits.cpp index 5323305331..bbd27a2ba2 100644 --- a/Telegram/SourceFiles/data/components/credits.cpp +++ b/Telegram/SourceFiles/data/components/credits.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/credits.h" +#include "apiwrap.h" #include "api/api_credits.h" #include "data/data_user.h" #include "main/main_app_config.h" @@ -93,6 +94,54 @@ rpl::producer Credits::balanceValue() const { return _nonLockedBalance.value(); } +void Credits::tonLoad(bool force) { + if (_tonRequestId + || (!force + && _tonLastLoaded + && _tonLastLoaded + kReloadThreshold > crl::now())) { + return; + } + _tonRequestId = _session->api().request(MTPpayments_GetStarsStatus( + MTP_flags(MTPpayments_GetStarsStatus::Flag::f_ton), + MTP_inputPeerSelf() + )).done([=](const MTPpayments_StarsStatus &result) { + _tonRequestId = 0; + const auto amount = CreditsAmountFromTL(result.data().vbalance()); + if (amount.ton()) { + apply(amount); + } else if (amount.empty()) { + apply(CreditsAmount(0, CreditsType::Ton)); + } else { + LOG(("API Error: Got weird balance.")); + } + }).fail([=](const MTP::Error &error) { + _tonRequestId = 0; + LOG(("API Error: Couldn't get TON balance, error: %1" + ).arg(error.type())); + }).send(); +} + +bool Credits::tonLoaded() const { + return _tonLastLoaded != 0; +} + +rpl::producer Credits::tonLoadedValue() const { + if (tonLoaded()) { + return rpl::single(true); + } + return rpl::single( + false + ) | rpl::then(_tonLoadedChanges.events() | rpl::map_to(true)); +} + +CreditsAmount Credits::tonBalance() const { + return _tonBalance.current(); +} + +rpl::producer Credits::tonBalanceValue() const { + return _tonBalance.value(); +} + void Credits::updateNonLockedValue() { _nonLockedBalance = (_balance >= _locked) ? (_balance - _locked) @@ -133,7 +182,12 @@ void Credits::invalidate() { void Credits::apply(CreditsAmount balance) { if (balance.ton()) { - _balanceTon = balance; + _tonBalance = balance; + + const auto was = std::exchange(_tonLastLoaded, crl::now()); + if (!was) { + _tonLoadedChanges.fire({}); + } } else { _balance = balance; updateNonLockedValue(); diff --git a/Telegram/SourceFiles/data/components/credits.h b/Telegram/SourceFiles/data/components/credits.h index a26715f473..053b3f02a4 100644 --- a/Telegram/SourceFiles/data/components/credits.h +++ b/Telegram/SourceFiles/data/components/credits.h @@ -19,12 +19,8 @@ public: ~Credits(); void load(bool force = false); - void apply(CreditsAmount balance); - void apply(PeerId peerId, CreditsAmount balance); - [[nodiscard]] bool loaded() const; [[nodiscard]] rpl::producer loadedValue() const; - [[nodiscard]] CreditsAmount balance() const; [[nodiscard]] CreditsAmount balance(PeerId peerId) const; [[nodiscard]] rpl::producer balanceValue() const; @@ -33,6 +29,15 @@ public: [[nodiscard]] rpl::producer<> refreshedByPeerId(PeerId peerId); + void tonLoad(bool force = false); + [[nodiscard]] bool tonLoaded() const; + [[nodiscard]] rpl::producer tonLoadedValue() const; + [[nodiscard]] CreditsAmount tonBalance() const; + [[nodiscard]] rpl::producer tonBalanceValue() const; + + void apply(CreditsAmount balance); + void apply(PeerId peerId, CreditsAmount balance); + [[nodiscard]] bool statsEnabled() const; void applyCurrency(PeerId peerId, uint64 balance); @@ -56,13 +61,17 @@ private: base::flat_map _cachedPeerCurrencyBalances; CreditsAmount _balance; - CreditsAmount _balanceTon; CreditsAmount _locked; rpl::variable _nonLockedBalance; rpl::event_stream<> _loadedChanges; crl::time _lastLoaded = 0; float64 _rate = 0.; + rpl::variable _tonBalance; + rpl::event_stream<> _tonLoadedChanges; + crl::time _tonLastLoaded = false; + mtpRequestId _tonRequestId = 0; + bool _statsEnabled = false; rpl::event_stream _refreshedByPeerId; diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index e9a3843c31..2f02a41d3f 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -1262,7 +1262,9 @@ void ApplyChannelUpdate( | Flag::PaidMediaAllowed | Flag::CanViewCreditsRevenue | Flag::StargiftsAvailable - | Flag::PaidMessagesAvailable; + | Flag::PaidMessagesAvailable + | Flag::HasStarsPerMessage + | Flag::StarsPerMessageKnown; channel->setFlags((channel->flags() & ~mask) | (update.is_can_set_username() ? Flag::CanSetUsername : Flag()) | (update.is_can_view_participants() @@ -1289,7 +1291,9 @@ void ApplyChannelUpdate( : Flag()) | (update.is_paid_messages_available() ? Flag::PaidMessagesAvailable - : Flag())); + : Flag()) + | (channel->starsPerMessage() ? Flag::HasStarsPerMessage : Flag()) + | Flag::StarsPerMessageKnown); channel->setUserpicPhoto(update.vchat_photo()); if (const auto migratedFrom = update.vmigrated_from_chat_id()) { channel->addFlags(Flag::Megagroup); diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 214ac56b86..11f74847f7 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -83,6 +83,8 @@ enum class ChannelDataFlag : uint64 { MonoforumAdmin = (1ULL << 40), MonoforumDisabled = (1ULL << 41), ForumTabs = (1ULL << 42), + HasStarsPerMessage = (1ULL << 43), + StarsPerMessageKnown = (1ULL << 44), }; inline constexpr bool is_flag_type(ChannelDataFlag) { return true; }; using ChannelDataFlags = base::flags; @@ -280,6 +282,12 @@ public: [[nodiscard]] bool paidMessagesAvailable() const { return flags() & Flag::PaidMessagesAvailable; } + [[nodiscard]] bool hasStarsPerMessage() const { + return flags() & Flag::HasStarsPerMessage; + } + [[nodiscard]] bool starsPerMessageKnown() const { + return flags() & Flag::StarsPerMessageKnown; + } [[nodiscard]] bool useSubsectionTabs() const; [[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights( diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 31d3b117da..1ec256053a 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -992,7 +992,14 @@ not_null Session::processChat(const MTPChat &data) { ? Flag::StoriesHidden : Flag()) | Flag::AutoTranslation - | Flag::Monoforum; + | Flag::Monoforum + | Flag::HasStarsPerMessage + | Flag::StarsPerMessageKnown; + const auto hasStarsPerMessage + = data.vsend_paid_messages_stars().has_value(); + if (!hasStarsPerMessage) { + channel->setStarsPerMessage(0); + } const auto storiesState = minimal ? std::optional() : data.is_stories_unavailable() @@ -1034,7 +1041,13 @@ not_null Session::processChat(const MTPChat &data) { ? Flag::StoriesHidden : Flag()) | (data.is_autotranslation() ? Flag::AutoTranslation : Flag()) - | (data.is_monoforum() ? Flag::Monoforum : Flag()); + | (data.is_monoforum() ? Flag::Monoforum : Flag()) + | (hasStarsPerMessage + ? (Flag::HasStarsPerMessage + | (channel->starsPerMessageKnown() + ? Flag::StarsPerMessageKnown + : Flag())) + : Flag::StarsPerMessageKnown); channel->setFlags((channel->flags() & ~flagsMask) | flagsSet); channel->setBotVerifyDetailsIcon( data.vbot_verification_icon().value_or_empty()); @@ -1047,12 +1060,6 @@ not_null Session::processChat(const MTPChat &data) { } channel->setPhoto(data.vphoto()); - const auto hasStarsPerMessage - = data.vsend_paid_messages_stars().has_value(); - if (!hasStarsPerMessage) { - channel->setStarsPerMessage(0); - } - if (const auto monoforum = data.vlinked_monoforum_id()) { if (const auto linked = channelLoaded(monoforum->v)) { channel->setMonoforumLink(linked); diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 7dfd473d52..c0cfae99b5 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -719,6 +719,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { | Flag::CanPinMessages | Flag::VoiceMessagesForbidden | Flag::ReadDatesPrivate + | Flag::HasStarsPerMessage | Flag::MessageMoneyRestrictionsKnown | Flag::RequiresPremiumToWrite; user->setFlags((user->flags() & ~mask) @@ -732,6 +733,7 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { ? Flag::VoiceMessagesForbidden : Flag()) | (update.is_read_dates_private() ? Flag::ReadDatesPrivate : Flag()) + | (user->starsPerMessage() ? Flag::HasStarsPerMessage : Flag()) | Flag::MessageMoneyRestrictionsKnown | (update.is_contact_require_premium() ? Flag::RequiresPremiumToWrite diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 81f52890cb..06eb877ff6 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_user.h" +#include "history/view/controls/history_view_suggest_options.h" #include "history/history.h" #include "history/history_item_components.h" #include "main/main_account.h" @@ -189,20 +190,27 @@ Data::SendErrorWithThread GetErrorForSending( std::optional ComputePaymentDetails( not_null peer, int messagesCount) { - if (const auto user = peer->asUser()) { - if (user->hasStarsPerMessage() - && !user->messageMoneyRestrictionsKnown()) { - user->updateFull(); - return {}; - } - } else if (const auto channel = peer->asChannel()) { - if (!channel->isFullLoaded()) { - channel->updateFull(); - return {}; - } + const auto user = peer->asUser(); + const auto channel = user ? nullptr : peer->asChannel(); + const auto has = (user && user->hasStarsPerMessage()) + || (channel && channel->hasStarsPerMessage()); + if (!has) { + return SendPaymentDetails(); } - if (!peer->session().credits().loaded()) { + + const auto known1 = peer->session().credits().loaded(); + if (!known1) { peer->session().credits().load(); + } + + const auto known2 = user + ? user->messageMoneyRestrictionsKnown() + : channel->starsPerMessageKnown(); + if (!known2) { + peer->updateFull(); + } + + if (!known1 || !known2) { return {}; } else if (const auto perMessage = peer->starsPerMessageChecked()) { return SendPaymentDetails{ @@ -213,6 +221,21 @@ std::optional ComputePaymentDetails( return SendPaymentDetails(); } +bool SuggestPaymentDataReady( + not_null peer, + SuggestPostOptions suggest) { + if (!suggest.exists || !suggest.price()) { + return true; + } else if (suggest.ton && !peer->session().credits().tonLoaded()) { + peer->session().credits().tonLoad(); + return false; + } else if (!suggest.ton && !peer->session().credits().loaded()) { + peer->session().credits().load(); + return false; + } + return true; +} + object_ptr MakeSendErrorBox( const Data::SendErrorWithThread &error, bool withTitle) { @@ -250,13 +273,15 @@ void ShowSendPaidConfirm( not_null peer, SendPaymentDetails details, Fn confirmed, - PaidConfirmStyles styles) { + PaidConfirmStyles styles, + int suggestStarsPrice) { return ShowSendPaidConfirm( navigation->uiShow(), peer, details, confirmed, - styles); + styles, + suggestStarsPrice); } void ShowSendPaidConfirm( @@ -264,13 +289,15 @@ void ShowSendPaidConfirm( not_null peer, SendPaymentDetails details, Fn confirmed, - PaidConfirmStyles styles) { + PaidConfirmStyles styles, + int suggestStarsPrice) { ShowSendPaidConfirm( std::move(show), std::vector>{ peer }, details, confirmed, - styles); + styles, + suggestStarsPrice); } void ShowSendPaidConfirm( @@ -278,7 +305,8 @@ void ShowSendPaidConfirm( const std::vector> &peers, SendPaymentDetails details, Fn confirmed, - PaidConfirmStyles styles) { + PaidConfirmStyles styles, + int suggestStarsPrice) { Expects(!peers.empty()); const auto singlePeer = (peers.size() > 1) @@ -286,7 +314,7 @@ void ShowSendPaidConfirm( : peers.front().get(); const auto singlePeerId = singlePeer ? singlePeer->id : PeerId(); const auto check = [=] { - const auto required = details.stars; + const auto required = details.stars + suggestStarsPrice; if (!required) { return; } @@ -296,10 +324,13 @@ void ShowSendPaidConfirm( confirmed(); } }; - Settings::MaybeRequestBalanceIncrease( + using namespace Settings; + MaybeRequestBalanceIncrease( show, required, - Settings::SmallBalanceForMessage{ .recipientId = singlePeerId }, + (suggestStarsPrice + ? SmallBalanceSource(SmallBalanceForSuggest{ singlePeerId }) + : SmallBalanceForMessage{ singlePeerId }), done); }; auto usersOnly = true; @@ -388,15 +419,15 @@ void ShowSendPaidConfirm( bool SendPaymentHelper::check( not_null navigation, not_null peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn resend, PaidConfirmStyles styles) { return check( navigation->uiShow(), peer, + options, messagesCount, - starsApproved, std::move(resend), styles); } @@ -404,17 +435,27 @@ bool SendPaymentHelper::check( bool SendPaymentHelper::check( std::shared_ptr show, not_null peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn resend, PaidConfirmStyles styles) { clear(); + const auto suggest = options.suggest; + const auto starsApproved = options.starsApproved; + const auto suggestPriceStars = suggest.ton + ? 0 + : int(base::SafeRound(suggest.price().value())); + const auto suggestPriceTon = suggest.ton + ? suggest.price() + : CreditsAmount(); const auto details = ComputePaymentDetails(peer, messagesCount); - if (!details) { + const auto suggestDetails = SuggestPaymentDataReady(peer, suggest); + if (!details || !suggestDetails) { _resend = [=] { resend(starsApproved); }; - if (!peer->session().credits().loaded()) { + if ((!details || !suggest.ton) + && !peer->session().credits().loaded()) { peer->session().credits().loadedValue( ) | rpl::filter( rpl::mappers::_1 @@ -425,6 +466,18 @@ bool SendPaymentHelper::check( }, _lifetime); } + if ((!suggestDetails && suggest.ton) + && !peer->session().credits().tonLoaded()) { + peer->session().credits().tonLoadedValue( + ) | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + if (const auto callback = base::take(_resend)) { + callback(); + } + }, _lifetime); + } + peer->session().changes().peerUpdates( peer, Data::PeerUpdate::Flag::FullInfo @@ -438,7 +491,32 @@ bool SendPaymentHelper::check( } else if (const auto stars = details->stars; stars > starsApproved) { ShowSendPaidConfirm(show, peer, *details, [=] { resend(stars); - }, styles); + }, styles, suggestPriceStars); + return false; + } else if (suggestPriceStars + && (CreditsAmount(details->stars + suggestPriceStars) + > peer->session().credits().balance())) { + const auto peerId = peer->id; + const auto forMessages = details->stars; + const auto required = forMessages + suggestPriceStars; + const auto done = [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Success + || result == Settings::SmallBalanceResult::Already) { + resend(forMessages); + } + }; + using namespace Settings; + MaybeRequestBalanceIncrease( + show, + required, + SmallBalanceForSuggest{ peerId }, + done); + return false; + } + if (suggestPriceTon + && suggestPriceTon > peer->session().credits().tonBalance()) { + show->show( + Box(HistoryView::InsufficientTonBox, peer, suggestPriceTon)); return false; } return true; diff --git a/Telegram/SourceFiles/history/history_item_helpers.h b/Telegram/SourceFiles/history/history_item_helpers.h index 11270500f6..d1c472dd58 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.h +++ b/Telegram/SourceFiles/history/history_item_helpers.h @@ -149,6 +149,10 @@ struct SendPaymentDetails { not_null peer, int messagesCount); +[[nodiscard]] bool SuggestPaymentDataReady( + not_null peer, + SuggestPostOptions suggest); + struct PaidConfirmStyles { const style::FlatLabel *label = nullptr; const style::Checkbox *checkbox = nullptr; @@ -158,34 +162,37 @@ void ShowSendPaidConfirm( not_null peer, SendPaymentDetails details, Fn confirmed, - PaidConfirmStyles styles = {}); + PaidConfirmStyles styles = {}, + int suggestStarsPrice = 0); void ShowSendPaidConfirm( std::shared_ptr show, not_null peer, SendPaymentDetails details, Fn confirmed, - PaidConfirmStyles styles = {}); + PaidConfirmStyles styles = {}, + int suggestStarsPrice = 0); void ShowSendPaidConfirm( std::shared_ptr show, const std::vector> &peers, SendPaymentDetails details, Fn confirmed, - PaidConfirmStyles styles = {}); + PaidConfirmStyles styles = {}, + int suggestStarsPrice = 0); class SendPaymentHelper final { public: [[nodiscard]] bool check( not_null navigation, not_null peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn resend, PaidConfirmStyles styles = {}); [[nodiscard]] bool check( std::shared_ptr show, not_null peer, + Api::SendOptions options, int messagesCount, - int starsApproved, Fn resend, PaidConfirmStyles styles = {}); diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 2dc081cde4..7b6c602cf0 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4540,7 +4540,7 @@ void HistoryWidget::saveEditMessage(Api::SendOptions options) { }; const auto checked = checkSendPayment( 1 + int(_forwardPanel->items().size()), - options.starsApproved, + options, withPaymentApproved); if (!checked) { return; @@ -4629,15 +4629,15 @@ void HistoryWidget::sendVoice(const VoiceToSend &data) { copy.options.starsApproved = approved; sendVoice(copy); }; + auto action = prepareSendAction(data.options); const auto checked = checkSendPayment( 1 + int(_forwardPanel->items().size()), - data.options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - auto action = prepareSendAction(data.options); session().api().sendVoiceMessage( data.bytes, data.waveform, @@ -4678,7 +4678,7 @@ void HistoryWidget::send(Api::SendOptions options) { message.textWithTags, ignoreSlowmodeCountdown, withPaymentApproved, - options.starsApproved)) { + message.action.options)) { return; } @@ -5011,14 +5011,14 @@ FullMsgId HistoryWidget::cornerButtonsCurrentId() { bool HistoryWidget::checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn withPaymentApproved) { return _peer && _sendPayment.check( controller(), _peer, + options, messagesCount, - starsApproved, std::move(withPaymentApproved)); } @@ -5209,9 +5209,11 @@ void HistoryWidget::sendBotCommand( copy.starsApproved = approved; sendBotCommand(request, copy); }; + + const auto action = prepareSendAction(options); const auto checked = checkSendPayment( 1, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; @@ -5226,7 +5228,7 @@ void HistoryWidget::sendBotCommand( ? request.command : Bot::WrapCommandInChat(_peer, request.command, request.context); - auto message = Api::MessageToSend(prepareSendAction(options)); + auto message = Api::MessageToSend(action); message.textWithTags = { toSend, TextWithTags::Tags() }; message.action.replyTo = request.replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) @@ -6233,7 +6235,7 @@ bool HistoryWidget::showSendMessageError( const TextWithTags &textWithTags, bool ignoreSlowmodeCountdown, Fn withPaymentApproved, - int starsApproved) { + Api::SendOptions options) { if (!_canSendMessages) { return false; } @@ -6254,7 +6256,7 @@ bool HistoryWidget::showSendMessageError( return withPaymentApproved && !checkSendPayment( request.messagesCount, - starsApproved, + options, withPaymentApproved); } @@ -6369,6 +6371,11 @@ void HistoryWidget::sendingFilesConfirmed( void HistoryWidget::sendingFilesConfirmed( std::shared_ptr bundle, Api::SendOptions options) { + const auto compress = bundle->way.sendImagesAsPhotos(); + const auto type = compress ? SendMediaType::Photo : SendMediaType::File; + auto action = prepareSendAction(options); + action.clearDraft = false; + const auto withPaymentApproved = [=](int approved) { auto copy = options; copy.starsApproved = approved; @@ -6376,16 +6383,12 @@ void HistoryWidget::sendingFilesConfirmed( }; const auto checked = checkSendPayment( bundle->totalCount, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; } - const auto compress = bundle->way.sendImagesAsPhotos(); - const auto type = compress ? SendMediaType::Photo : SendMediaType::File; - auto action = prepareSendAction(options); - action.clearDraft = false; if (bundle->sendComment) { auto message = Api::MessageToSend(action); message.textWithTags = base::take(bundle->caption); @@ -7715,9 +7718,13 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { copy.options.starsApproved = approved; sendInlineResult(copy); }; + + auto action = prepareSendAction(result.options); + action.generateLocal = true; + const auto checked = checkSendPayment( 1, - result.options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return; @@ -7725,9 +7732,6 @@ void HistoryWidget::sendInlineResult(InlineBots::ResultSelected result) { controller()->sendingAnimation().appendSending( result.messageSendingFrom); - - auto action = prepareSendAction(result.options); - action.generateLocal = true; session().api().sendInlineResult( result.bot, result.result.get(), @@ -8336,7 +8340,7 @@ bool HistoryWidget::sendExistingDocument( }; const auto checked = checkSendPayment( 1, - messageToSend.action.options.starsApproved, + messageToSend.action.options, withPaymentApproved); if (!checked) { return false; @@ -8375,6 +8379,7 @@ bool HistoryWidget::sendExistingPhoto( } else if (showSlowmodeError()) { return false; } + const auto action = prepareSendAction(options); const auto withPaymentApproved = [=](int approved) { auto copy = options; @@ -8383,15 +8388,13 @@ bool HistoryWidget::sendExistingPhoto( }; const auto checked = checkSendPayment( 1, - options.starsApproved, + action.options, withPaymentApproved); if (!checked) { return false; } - Api::SendExistingPhoto( - Api::MessageToSend(prepareSendAction(options)), - photo); + Api::SendExistingPhoto(Api::MessageToSend(action), photo); hideSelectorControlsAnimated(); diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 8c6a61f8b3..7166ea8768 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -371,7 +371,7 @@ private: [[nodiscard]] bool checkSendPayment( int messagesCount, - int starsApproved, + Api::SendOptions options, Fn withPaymentApproved); void checkSuggestToGigagroup(); @@ -489,7 +489,7 @@ private: const TextWithTags &textWithTags, bool ignoreSlowmodeCountdown, Fn withPaymentApproved = nullptr, - int starsApproved = 0); + Api::SendOptions options = {}); void sendingFilesConfirmed( Ui::PreparedList &&list, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp index 36d34afca6..9b3015dbec 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_suggest_options.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "chat_helpers/compose/compose_show.h" #include "core/ui_integration.h" +#include "data/components/credits.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_channel.h" #include "data/data_media_types.h" @@ -19,9 +20,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "info/channel_statistics/earn/earn_icons.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "settings/settings_common.h" +#include "settings/settings_credits_graphics.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/boxes/choose_date_time.h" @@ -30,8 +33,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" +#include "ui/basic_click_handlers.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/vertical_list.h" +#include "styles/style_boxes.h" #include "styles/style_channel_earn.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" @@ -81,13 +87,19 @@ void ChooseSuggestPriceBox( std::vector