From 1d2df636521227bb19193cd323631a18ba9378b3 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 20 May 2025 12:33:03 +0000 Subject: [PATCH 001/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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 zcmeI5X>T0YlBWNP!OyIk+!ufB2Kx2@#`U1>`-uHcAK&u*SFc{#7hk@A z|M~;X-~D;>Ggof@`S#b}ze{-F&C5StzY90~*`I&o3F!Fn$IX|;?Tt14>)U^Q_=(4V zfBEk9Zy)UXdr0G{U-LDA(l~8*SHrc0YqY6> z>-x->?~>bHvM;md#qC|aPxyMmU&*as+xf*8)}WrB-O}^3MtGhnySvNteNBINbh~YH z+TP{+s;q8lxXM%eT=V|ycGvE!q!>KV?#rTlK|BAv`SQcNf4%2ch zE9SY`C%IOPKTzz8DmyZ(4NT&mJ)GvBbzjqti*@@rEmv7q2~(-2EXyzY)*Y}czcpf6 ze$n@9-tLQniDN2@wA-*%9WFHe+%`;hyKnMt$A$+Aru^94F3s$;x6SI@I-#RY>ldF) zZ;>&Xr0|&WXcuTKU7n~9er-X>%x>9?`BiVkXN;Mu`*mQfBrx&eL}W^IwCZB`*t#vfC? z#iVhs3#5tGHu}n-(Nve-@FK?U&41gk?@?e_`=)Gs^WojwUtX`!_`{orqA}<*Lqh)J z=QnRw$oL?|WvcZ^*Fn`8^;K$wuChi3BXGyC-15Kf@ZP~44b9RBzDEI?JobM4CVkV! z@7s4Te|!JK+jqa-e0X~9<6k_MqT75}T^sA4x znC0x{eOpO69E~7GZIj6^Omf{jb#E+9@RY!rF7faLE{gwd=H?s&Q;5|jV=$3+Fd3Jk zz+O)F>3vH}0>ha6Mlqh=+Q0hxm>*wSK7T%f(Y2xvzardRe9t@)_&RV!!2Ges?+*G8 zK8oN4>F$`MANVB75&PfRNZsk2YV;sOHa609K7%MqP(@AqjiMw?n$SuVDQVW(PI0v4 z-Pp#_Qe0wiQ=CW3ql-*AdBvqz=GIDFkK}cfofFrJs|m9?+R~#eSsUH;RuC7HotRqt#B5v&BNcUyga@D;6GtRgc-BD0!5cNo%3t?S1S#?`i7-*U7 z;+qdL(owO)2O=rmhoQc7>RVcjeEi?WeDGN8ZO7_xGQcob$=0r<3HrRZZRte$4zUH;h4Ic25XP?Fj{)P&}K_Y z$~fy{NlBk71|BvfoH1uDTeh-C<#x8<8Jn|(wzInUmU>r+Vs6CfB8cikGs{h_GtULt z;_d~;JZsx2RckXh=bov%0R|0e#k!Z?2aq=$V?WRy7!6Sm`74d1S=hO5+93<_wZi5p z@x$c_fzR^5fp}f6%fw49c60MVtSBSCiiZS;0Kw6*CY<1E3@=DWucfCQLHxxX`s8YT z#gM@uT0*{Rt>b;P-e6Xxc>6kf?qh<+4)1%ZFX;*gVwv9(8Eh5dzbM1?#eTv&b_{r# zTQ{4$8%l?qM`c=SHwdad{-kuSa*L#fw(e|e5B*=IX&_=&ACZlbv z{2r$V(I25O>?eIN-YPefHka?1-~{0r$!`1EJsE^^=!owk)Efmwy1>X`l7eUBa<2o1 z(TVdkxosc(ePRybeR@}Zcpu}>m7rJ*yFL__pAs=cSf+ue>=n}o6_O}aob7W$4Yzk0 zgV@5wYB-i634lDb=8JtIzd7u;OoS0&g@>>)0m{6FA7!cCuUHUxg78~MqXvhw^eS3H zd7~+D1BIJ-SS_=F(Iio3(hx)mXdd>cp&W8JNJnzU>vsj5wF+n!-Xb`U`eGMKNW#af zZ-fUFf}zv+!b?SBARs5)VnZo9V4pP&&`phSnKL*>Wljg#CWqkdm|<;113_NWG8VZ( zZl?2?6R5J8677cONcIg*dO{#|VsnF1icP|3)s>Ay$^gKHuDK8)RTG8z1)WHL(wCDAquuiWc>fTfsN{;lb$MTAVUd-2T;lM@Vrs{BqZAJ$9NVw1y4_g9ni&Yd zUXVoRD-8^D+u=Np?nnB^k^!ic8^q(J?Z_Y}tnGbPhdrhJ&l)lm2|%#|v;f0wTlt>- zC~km}1@Uc&D{bA74QgB1kaQcq0vTc63MQ&xL)NTqTdZ&oh=y4ewjUWj_M_rcTKP;X z?TG)?&5`|h?>zSEh)J|NV?SsktS!BCwjnP&m2?Rv3Tt8eupu6LDmAjkT61F-TC*%W z;lnI;{$Ml7L>8<*RBMA?qWaiOid8Yj;1c2gQdFuV6X{z2Sm_#V$=>F*ME5CendTC9ML@Ch%CGQ6`$2ASh=%w#55xJXp`8$e1dC`gHon%mL#e%7av5cY&T| zb7j`veN0x^niLhwh(%WAAU|ped%ZtQCrwDBfhXZmQ=ewCGKVS3s6Y#lrOqSB_B<0+ zZE9c1QR8xyFM|Erd&hTX36tkx=s?LhVQ(V9CKma0Qr#%JPU@4N)FsK;Pv@S$J_bA2 z!$jSbVu`Tc(CGt&X=0e9Xl13;XmmKYOiG?dq0IuMwZm?Kc`Z#hVkx)i)GRG6XV!Ot z>XRq>gvdag;ag#8A2ul}uJjX3AJg|{mI=G4E!mer_fi&tRH5*8oC2VxrCX_e)LGL! zdAHFP6iHZ0r@#nbX>ZEWSNdHln}@wiQ@u4c(P*_N@z0^~0 z`V`4@4G#sIYR*PA-ZFqOmU#P4OcT=)%@jM|>lFH4km$`H?r~9K?`J1>9Igoz^Lovep=^B2Tes>6ubG!bdVq5G%dU=TU*b zS`j%UCS}tmg(5QS3}6%ZyRd^;zH4NKaMcy-i}Q)iiw|lU>x(G}B&cI-zfL^ud) zH`Z$Q9o$j)(7*==a9+mJquU$blHkR_ST$!EFV#oc?0g=-;F@@D1`G>12N6arj75q6a!1nBN*0vBg~WT@ghQ0%G6At)mD^ zCBVFEaTH~!e;&1uqbbj@cnB!Rqu&Y~!5-g_>mB~cWVLr#NHC41Q1^U6rWIawaziiB zyBeMIGQC4?6bg81#AkE2sdp_BXr*uK?I1h4@zcu{Y0DCfY=?Din7M_P&_NsT+lH3r zGlz3|WrVh{fDFyCXC`ssT*EKx(&8cEiWx5kn+&H}pbVLr%M?t@$MOymmfN>m)5SGi0{0Bn{bZ zN7gdGwHM6|;RGJM0W>N-QRPSOPY^}mPL0M5He*X9IKvS|49<{LA0hLRQbAcmYapvF z)*Rtx#lWhTP^cW_zDt%ZoFBn@XeZo#ca!d7M4<}aq#B2Trz$cbaDcA$vvQg`Jtn+TJfJnXcrle$)N z`*la1Q=Z`T9fh14@2FvIf~m)ny)-DQa7Ty+8wMm#rC6$3tGXnv;%f0HYsCfq?~L ze!OA6+B9qhb_}R8Jo70_03yQ599RipP3~GSD@CZ)pfGo}V95r5OhNq@LKIz~y+zmHbt+p@1vx|?gQI0iF3n5y~mK6wGYMyN!eiKwyaUSJ7=w5eAzZ75Y>h_Gl zJnlcYP@?sCjE9|!gD0@`CtoG!X0U z_u~M#3N#TZu6e0}Aj-+y32S6Eomf0Y4JrZTp@E5%Lcqx2cM3TJQKQjCoBGjsrmG_L zEcRzV5FQcj2V#Bw!Ztql{gHZclzup#>jnXMXwlGTKM*rP`n)&D1nZ38C$ky8;2BH+ zQxV;uF-?IFxBV{il$&;MAWhZ4!V{)cthPOnr8O{A;f4*}4>B3n4ooNbmULahlW0vU zf>bJlk4SWKCTU|NOBJ5NviJ;_{bnO$p*{tpHHYkg4YsQeMDfjXfb$vu=l~BVHQ@@I z9MK`Tv%LwghSGxyH#gBM2oXI=UA9Jar58vsbT}`MX~SnWrZ3JrhHVTtT~D9Z>o}H# zlag!C+mOXNJuSj5Gj-@)!j2vImt+_&xtC^pe$B$_-Koq7(+B%CmWi(h9}T_{1(xOe zxn%93K1UU(bIDrV5eXrm>*BBRIvQuc1~|$6MrmJZgGfXPj6h>Xs>p`kEWKnhFMauq zjW5xT^m;VYIepZ!8@{l*07Vz17_xGqiP0VUpRBp_t9U@H{^EN919{H&H5ePh~prsWA}dCOh(ovZjtSPYc0K2DXU9 zEbz32Q+Sk}LL!_})Rbt|-Nl2~9nur-s~(#kpp}tQ8|yB!+8VgF%{3L077ycrwoOqL zlH<&Uw>%)&0yw)saU1pHXB3kqQw1LzZCL$`@f(IiQjxMqEf7nYRQ6y-`de+u^pxc< z!$ef2oSPtJS)R)6Iac06Oheq_xc6}EFFQJadhzGJJbJbGx?es9X6HJ7Hy|)ixs0Ff zhUx8<>jLQ(h#Ozm0qF*0B66Soj&ZBF=O8M=L1{3H(;+V#l?Eifrq42dR8Fu60@u>r zJJ(fJXW^SQViLkkmoolHz2A3p1-TAQ7*U z2Gk{!faNAN&XI=XR`VGhRfm!w%6S7#j1&-QeG{+RXC_8Z@vf1^Y~wOS^BvqEi_Q)| zlWJy{Z_J|f{U#rst+8{`kK8NQL^aZBQ&E)kDo9)_F1M#0V)@S9obaeVXVG1qy`(#t zj;XUWbbg8jIU@|C&6Gl+ja49LHNhWN_2!atc z4TyE1PnQ`%=YT5&e)=iyrQOq|&?V^#g`a+sdr7@Q;S-o;*94v}h4=89Cos$R?Bxfb zaHugo(#%OKJ1150L|Y@Igj_l?MciC^#yFX&yZU6aQIO7?P6ELNJ=M)7=;5` zU=1ukV*|0;zK;V{0}_ziM1fkBfzes^WFO#>DI0U=a2*oKuQg|n3L7_8_?t)xO^iG< zTSG&WSje$i`x_KB4HL&=I$Zj5Hhq1HgmaL#iu>lEVgXqxfY@N!+O{T`&bclXZWJ#W8aDF2pMst+`Wu>W=o^ z(xX5J9y`kHozjH+n-12FnPr>c$5-sIrpgR97Ihz^h?tfB9L$Jm>AAC<|> zVuxS#e&wxJEJo@i!te;#;T@+0*3^r7VDPZzh%3ds;9Logrnodn*&&_hzh@hKc*pS+ zB(l;UK!Bh(VUqLX{%psvD-Ns>F}eVKTZ0d8e2F%t!-r?n`YiCnBW&wn*hAbNh2=tc zWnXPa-q&M?c`JXr5M99aD)z;?@Sok2zIF`P>j>g?!19Fw#FG~dSJtS{pD>E)N=xVd z6$11CAMU`ejvhAHOAjA*q$^j4_~D}u1&Ha&3z#zVluh8nI#*KRrhnzd<_2-PtT*pp3=h0}IArx}MxNY6KimmVsp2p!aGw02&h zYV?4hYANwl^XCLe9?ys*_G6+tB04CAdcXFSG90tUK`ngT;Vxq$IkTcWKqDnaPU>I_ zx``Gp=rnuw>tpIXyAH_IiT2!g%+!x|T_F>H+I2uCLigDVncg_=v=9bFr{p0qIT#d) zNWD7Fx8XCVBT3vLDuK}Ovpf43scR=F>D*xCoeNzOZfGJLE-cBcbW1}$namXc7+A0{ zO%k~}pOzNGVSzypAvA@Y2JmFX@(}VmunTP`oZM$J7xx;GDm!8dzLz6{)7&elj!Jk< zA;h2ACfMv`>q5o{Z%Dk);|fv6z;GwE{9gxyc_ZSwS90Q7an0Z)aIe{JY~GRc&a2s+ zOvltG+q?g!Ep%@>)ZSh@!>4!toz#oj`+Ytx4N+dIpUP&^KK$HaDw9LgC=M`&5Z{MV zsZ0{+$nlgiWel0d44DB-dgxQrCIH=K2r>37FlfoZP~*kK8HM@eCsWyE1eRu+6&%uS zIn4~o!$F3HWsgcAakitZ#^C7y+7-9lT39t;Ew$uw;4h82FC!Nzs4e<$y#HVnn zY2cM^6F%{zKGKZ`zz{zn0zIbF={A}=S?o4~VtO_NHpkrv5<<1=p`%GI&_1$Tj$lb_ z2$^L2jDAC?FLn%_NiC81Bqq5RNKcnS7j{b&K9OmjTBf6=_%!BuVk$mC3MaPPh{C5b z&&T%jL(w>lWiIW32UuUH7#$Ht!4*q9s}IhQoLIpL6v8kH`_t=}uO?t()n{BPYE&k7r`sIeWUvs0`8;4QD>%jX+=2KXq<6AqsI9oN-jA4M3}d<$}KNOEuxmo^~6q zHC#C}3-a?ZF|dTS7;irO3KYQc`U@~01vZ(-In+l?(}Ry@W5MlQd5=2O(GOQtqqZ>5 z$!&I#6h(>4j3`Rjw4s(-6e(%O+Fd-Gbj$ptXPhTGpJk4&xI~X|UoQ7(5Y(19g9Z5t zSfE9f;59CBqf1o!0@qFutpM}r_NebfDyOcrvc6sxS|QtEK#TMl zcRw-Snw(3WsOp>p3_#4bcis_qW}-8XNMf9Q#1Yf;k4S2I4iZU@vyfPFoQcG?<7{N! zt@ZtKgpb6M;Em@RG_-kUEdkSLcWjyvss0q3($yJb+~By^&1CIhm?GkhhcO z{oFF;G&xq!*E3ZCz~23N^BY?0E0k6vVje|-5ne~zEa zlK#23{@mLiet7>{g?+}4@+9M6ul(l2ySKl*{vzd>gE82v(^k1-M2!tTz5I*CAo?Di zZy?P1h>Q6sZOw&zl&0V^K8g~T@KKcX0zQhAUcATA;?g~i7MJaDw76uCqvf&&!mLN@ zQglJdaG@U6qh6-Rwc-*zsz*RNAH+ad=Nzs$c%t*hFuwDSxmvZZ#>^+unMWis&OYLZ z>G?+_H9ZH3B*$4uEIH0ZBFS+s5=)Nrk=S3{=Rwag}j%lbwOzbEk}*+DoSbC1q!Nj z)Gv<^dK(lf!%k|HsH`HWE8pxI@7tDTMNJOg-A>UbWEz9jY;Vbb^AdKz7zo(xKy7ls zt0?1(l}f@Uq%RopkVO4HBm!w{;TS`V%L2G&DtKjo25La9Fqh|;VMI?T8>@ri`9-E2 z*%w8flx+ekMm0~8rpjta25ZCdOIm8?dp*-yyWdzEr5K0#y%SlD#d}g{;b12+w-x&cYK>uPLDyYP-zw<65738 z?PQXRw|n|U0Z>kAw~hjVhggeEv!-et&kJX@)8LNvOR^3YV#jJ&UK!Z2R`n|ImRa%b z9VI9M%t9W)@7od-{dwRoY*JoI4;1&^kEnLv=r^ zQfK#VnyoTS7PUOO+0yFOY=Mf1UWBO*idG=;o^ByS)va23M8625x&_ZEvq^8XSN7oJ)dT_a^#i5PVm1Zm^CL(*9lT9Jy9Be>a8Wm0EL}SnSf8L zILM5;8fz09@@lM|WULAFHFQDASQ|q|GYpAikWnf8DONOk7ySKcSuRvv!v~@>-FijJ zEDBSvGOHdjG)wT&l<5D6rdIVHP@5_asuS=Ocf~OwbpqF0W_lY5l;bJkkiXCgNbEj~ zPiGSk&gBe>$0PEDaa=JDDjZK4=meei;D#F-N2cN(rt%Kw8Q*8h*$}(7J3bIsnRFEu zQud<|7jw5xGRjuee9Kry;X$5S>X4b1oZrfcgepBOooGZNj=YS#3}|ExYNnCMIzfVV zNQ|b7QI;dVt|h_tD5H4FBrj{-uW$pG!jo~uae!wwY zS2`b*3!ke(UI=z|za#&e7I74$?m@RGz=k6i(BX&*x-W7g{K0_>QIt4rA&Qb7wGc&0 zk6DPL#UTrEv^ZcPt`!#yvmPP(P%KVM#1Wv=Oyf9O9IFu5iaWxidej3IX0)u2%N#{? zO^dhy^Kfj&nLNnZ#~dlL1%lP{k4S2I4iZU@vyfPFoQcG?<7{N!t@Zsf2fFoqB$gcK zB(d!{FNy0`&rBA`Ss$Jw>|~*x^~Jo9Ou9VwcgX=T@>0*=8L3Z6bhT7*1GQ=ijg z0R$9(Psn9^ID`N&n+!k+{sbznP|0iqMgZfUxHuZrF2|FB>j`fR}1MW-7bG_7K-R+^S?;q9<54P77wM<^7y--tz&t$_mY)6|R#4o471 z`Yu2Oa#Hl=Q22GtCvt+hylm;6*eAkRDQ9ruC`?j0Yr@x-I>j{-%IOr4tJaFymUtI& zMga)O>}YL1=jc{=_HyufFV-DBCv?hr*;y0yNje2@ydTa1zgqBK;abC^AOu|5XcBfM z3r8!Byr#qQ!+nMTWRS~HJXj7@5PrZh40tkJfb+THu}Y3ZZBwg)c*)6Hb{wA8E7gB0 zRE_GKUIAL^+@TNAsU=6_X`T8+Q+3)BT=Y;r>3*HEu@>E$r}gU7EWy(z%T?78A3UvF zpJ)-DGFglM)6;tOX(sEGPA$se(>nF2Q?>4>ePTL;6v!(&;EE1-^ZxZmw8>3}(^3z5 zWT|}(G@uxoYE%V1iVYBU7Pz#IKciZv+;dZ72FSft-pp#{o!c{dD9WZmJN!lF4ep~m zi>ijwk9CdJQ3E$-7-;_vHC@N~*ijk+ISq9`fXFFWU}y^HHTwhvSReU%Z)ia4Ih$)! z+@#_q7px|gm%v;>nC4*Fu{0LuL<7M)-9!w-c_*zqcn@wo5h>h}pJ41c;4yvQCZXSX zc4=(N>XHrLx9?v5_Wp;r?|!|}8nsJ*d-wKVzgrIc_c$SV(>xXaL=$&uIVd-YbxAn` zuVZWO6tW1r zu0@TJ9H^BWH*qxKW>bCa3j`{a$m7+9<9bI4bUCV)m%w^#V7``6P^iX3ww)GJ98D-+ z`DJ?7C;^q^tHlMrsdsRnwF-0V?JPt-5{|R?*x80rT4)J}pmL4l$2;TPljRZG!U9Ob zJ1~%IJsFkL$dm+Xx12JT(^RV5P zmWEt4lBo2+SL0|JG1ge3hO9M`tRZiWC2PoCW7`_?*LfG$S;u@^j2LVzSwj{Z+t!fD zMt1SQWfzEAAFKuLIOMYnZLM#^g|>#Ac2isPN$h#;e4y4zT4-j-Y-33pvfGZVWqxZf z62~3e*Rq>8MCQe2Ppeo+<%*fZM;dmYynmm=hp=TuNH#>sD6MdTMD_*qJ-KaEX5$c5 zwvU#NKl0)bC?+MHavobfRH{l z4naKF`6JlZC2_i4#cj?IebMiTk-bMLA0*iz*pO)Fx>6;Z^WYu}(RC~t28948Xk*X zdcnX4R#DEX3#)M#K53PXu6LK|n0de#Zz4sOn&La5m`ja)Ek7wWitBJPc3P4mgU#4_ zF{<#0Z3K#9vHpuxm8-f3Xo-bwCwDRgN8%}`?`y;-+n2mg-WC4p2I6qtAK`{~A34^4 zZE(Aj$MXh#2=KxHa+`KHv*O4G@@2Ye8n6)6sN>4hRkHBN>?3yuGcnS69 z&41gkU*GTxIng=VZ9hKj)VZ(sc}$<~%j zc)(I-VjuS$NJVPRsjkf>#6HW0IA17gW6qni0i=jlO5Is>)Rj;p;i zRDr>}DJK>*OD(M#Hr1fr8)9^<+hRf;yi3ajVujvcG79dH-%>UQ?4a`u=;OfTj=Ky> zT;bAaaL%&h6OrA0B%fEG`EoDHx6=k!H*efH!u>R8>$4`#Cyw%j3awEA%#Pgzhgz~^ zktpLp>-KS5$$EYw14jV%>CP$`?Kt8ne3|L=Qq*VDfT;jif(*f<9`;hic1YcGJy~*K zdRa$s6^`yYY*zrkszZb(ZpWMt*@=aEb+#C5O#$DE3U@#Um4hEz7!zp_ut(gA@&g@F zjtup`WTC4l=k(OO!1fGBw*CS6^iRw|GeHTT`IjOw!IqD0J z2U;XFJnxP^il-lormk=L`J0?`EJP>Po0)GD=N${ptOrcy%{cei)Xcn7p358AOwU2K zM1kg=>RCuEInG34+i^CsrCaks#sv}7yI69Zlf<^;yd<()&P*c7>DfsfIXyp#CC77l zJ936B#hMmV8Akv%1`P52oER^}W|rPb*w8f7zyIT_ufF*AumAq-e|`NG@rjWqVouQ{ zB#{A3PRT&ZT54ol>~ID=gvIfnIB6f=nfV_=!4j&v!>z*47hO?{TVU7y*o5N|*b6+< z|LmTWU))~8&^lKoO2U83Q+KhK9eA%^b$A_g;9oQg3<^X=pkx?c-G=O^wW%j*7*G<* zY3N4c@8xu1MjO}5V$qPbqbCS#re(oI99%NAvPnd(!Kbz~;>=;DBW!6T^$MXlhtoEu zPpO}85=W2cCC0Tv)X5ebUbo6p1kRo+;#sq%#IU>=HgsxBIxCp%=4EHy$W`Y%v}}!i zusH(zV1>(r7&bE>ln&xMdvxjvp>Xk?Qqg(sdl58=DU%OUan6VGel73b`n0+udd_n} z{xjP5m+~yJRBN5ig2%cH2}xtuk{7;NUj68$_q(bIUz_ua z>~0`XypxjnHI?ye(iyDWBp4NP7x1grApm9GcAZ>+gIbi*F}}qSo#d3?mVB;5=Tx$r zL8jfNnT-a4Fp6k$7eBY10s#_;!9cF@US3a&;NLk=L2i0RM}vV+5QHgiHp*}=`PnuN z#fqsDjE1fQ$hr#24Ms?qbzJjPFfncbepQw?ka6Tv5gdsX<4-8 zG*Y9_dmwG}e9E?wj!eQ$2=!caVXQd+pX)|h-@jsSx=0-=)W3?NGRj%e&uR;w>!w~1(8M?nE=9ER?2^IH>xoe&i zns%boiLZH14==#GXw<|f-Ccd^c}{FK1rzVzllcC}d(tY^u5QV3Jn@?2bg1bYg#5_1 zC86+|<1|#rA3^PY9=mp0B5oi>IaKTPC~`uKVlj+7K;hu;Qt56*qde$Wg+Qm`Q(eq* z5>X7B;f`2Lqg*;=;9L?wpjZsC5&zlsVQ^&zBv1&F)ki-@LDfqJ@q8#qO(dh~!Cyz{ zz`SY1UIe&~>03XLVNiWI2Y3t*@bDVFYUy`%K%8XwmL7` zJ$Kemx(7S#kJ%lK^bjy~X1so+T`XpXUXx6L420y Date: Mon, 19 May 2025 16:35:51 +0400 Subject: [PATCH 050/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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 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%0AEQ zA!$YoeEd&<&Pm%H>h}+;zoU)$lA(m_#VuC}PlV}6LhetuyB%N1LeptyU{RypMHB8Y z)i>V5i<4Uid}4|_2R&A80TZEp{})nIp?&RoE54uKD`3r*U#<pU>u+qlo>wr6@x3oK z(YjJ=^E<IRdmr;TpA088FIU&PI`rP-Z;MFzFJG{2?!%@|>+tT;znvJwToy99b&+5R z8pRyRJ-}~2?qXXz-bz$^MZVyq`CK6@C>J{s=f>6LUWBO5xS@&k_PKcn)~;P4j~E0K z4bZ2}|7FogQu&hegMQEabO4=;Cb+>0^h5O@G>S$KEOK&S26KlUho<pF(&BQ=h7qTk zI)er?dCpT4KNi-|`2(vVKbFEDbu3{4Ld<W7JH}%ub|H1eet5%*+QDpKWJVq)`R%h# z<4aRtTkfBum7DsUs)VRIn$lKw`~#>ZkCK>4xP-bZ3_Ft0tY&I&H+SJE*?5X6`ud&! ze;aoJyc*y?&!O0zwHn^U>waHmd`Gj`Gl=)EKsZ{OWd@+$Us%KK-A8iNOL54Oa)R=l zZY$0baM>i3N@tiq5z5)|TCph;H2L>R8kv&LvC1&G+cpwIVg8%L^OF9C{_Gn;j3~L| zp-S*-laZQV21r6hVokL|GrBdRLLc0tmzy#ZB0vfT{jJBZ@n^o?%Y&n?o{;4bY4czj zn-|&!%X|v&XWIHlrEId(SIx`L>3?PN?3c~`+eno-`;d?eH9+fS?KlrMsKD`DCtV*z z-}(Je9P?Q0_WJUNEMdzzA-_~UGRd9Pbg{6s{e3UX9V9uT==+0XHwN3pf<q(n23nbM z7*Q!2<0z)rGC_At>EOAaJ!#6Az&XSikkij@PlHyFM{<Z1z~9GC<mp0plg5r_<3up@ z?a&{iFf%#WRs8V#!;0Vssm*_uj|0{QJggca&8#3NW^!$xMjm@2Q2+C{=f{3nSkC7| zPPf_oBHW-KmxYb>=VR5$Z7zJUFD-W`t0OTEN664M5+mKd?SR8JhJ_hNs`>><2hM{t z1(M^g<p)kfj>9j@F-`^BjBM^%<3Jo1`ArG)pAz;-VmLygxW6eEB|?E`6S<@^D?V~} zY?1<B>5tH7@gpSh(WuGM?HZS%Rvj8N$0Hom2ZS|o7DAl}x?rUl%5W^~==<#Ufn)z) z91K7hiC%#lmDL;>>!FxXrXrT9CBw~<y~cK{Olgq{l!`FPk)e$MvsPl*TTt{3xKf$& zVSlMkrPGDO3i46j6z5fQK=shG)PyLm(y2M{LK@^;aUcj*5+S{ojS%rYg`<HF$B3F4 zspBAh_zS!@N8af7JIq{#91fU<(#ve3{}`H0_aEsuG2lwL38B-ma(RQGCRxYN>L&gN z|D<DVoSJZ_+FU1j21Cj#Rk_z_+SyoGS<|q1KJJb)s=M2KKhCm*g_rq`*U^ZZd3uJ9 zB`|D3Zhb@hN;TKY3^nimyU24L<n}`1={m2_!;Soy4(lzF(8Da?NE<K8VU5|vPXApM z()_xtR78oGDD{{UTZ>7+UJF3k!b%XDy{%z`Chh)W@Z2px_tNmdejfa%d(_$!-M%6{ z8?Z37a3?ip?&w~2+#3uJ>UYV5+2=;q8RHsUK}W*B0d|)<YCKRjH2^ZV8F`WE6ribx z@Uj;QlGQ?R>?3xPqK=q22nno>3!}^&ZLLNZtk1=g3uCN)A;txYAJgJuaZnd2#dC6* ziE^3g=DXRX;?50Tb>T7o)kJX#VFDOZj)&1@VY*6VWyqDTAR5%&b=JZ2mzIoRp-~Yv zZgTwERW;uXDa-H%;d^AY5Ha3=HR7^9Nzma>GXHU+kQnA!DmRbbM!5hz{F`CkYAje0 zE`|aqjW&?qq4G5YQm>5=o6AGOMMtU>_OW=_b@2Pq<U&ivdr4PYCJvoaZi!w4N1_ZO zA|aQ7=;$^=woJLkcV92*Miy3avqI?wi@p;2Eb4`F=r}@@Gb+?AxhU>7GPNIP2Y#7G zUuh+&Gc+^WUPoUkV$FyU>Z4qnPpy}H7-0whHwY;lu;s9WBj0L!cstQp^<a{m+AcGy z-ya{t4lUT;EK<7-%yExN0COqO5L09DoCU$L|29>>oR`uULYY-#0zPqq((7V$YFCFb zcJ*RaVwn@I(D){;K`&@(yJIIpnRH`tGcxt0{rFka#Q(r6lDZLH!ekAOdT?5{OMzSs ztmmv{BFYqg_qKKE`+Vcq^|^D1UyMj)7M%eE2Rd)jkai%-l%258`C-)^B_h$$Qe_!X z3B#^8;OG#^C>>BdE$5g1=@L~-IlE#MEK%UriYHD`J)(f;8gF4N|BeumDa|eM=2#s^ z`^UgYyTZLJuOI~uKDr$EtS9vmPU`Y*UaL!KkfzTM<2>G-xQVPaLtQRsg0uNi+a}p& z*bJ>3x^?ePv?Q@;@3$dsQ`xn0)+3vG_)CQH1+;<JA+gfR7;91;Hi;zZmDPn$J>=?D 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 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 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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 zcmb7HX*|?X_n$#!4TbDvsL5_bwkVllFlZ)1Wn|y?7|S3GV_&lGB1>e+8k4M1*+*Hk zQ?d`)LeH<~#sB~Md2#N!%Q>IBeb4uv8)=}Y$wbdh4+4RhkXjmOKqveYS}LHl6MnV@ zD6l75Qyo+}z`F`GG^~x0HYgNG6sXgJz!CN!ntvvMa03DYQRjm})PM&6>&vJ5ztMzz z>i@6*GgMQfCjyg7A~o(lz=1b%eEU#6JUtuA@S_6nen+fm*j@y*{u|nY*{ti^%bns> zoTKB27Wq88zqW?xH-kT|MeRBy?<!-gPCeADzPxdC>mV{~x^?rt?LeR6k6i!3_nv1> ziarCfgDSiDfxZmiTw9yB*5owA9Y{Xw5Um}}O)741Z1DfuG-r6e-rx~&eOGyl^dcc) z`z)(mJ4G}_?4hJlg>iexIR<voE~zoKzbCtUt7KbONtex;_x{VDQ_3G!9S4%h<iBTs zwzi2JzLmo-t6v+I%iE7EeQ>ShmUFNgba_&JMaqm<*;`yhWO*n{3<7~rq<g|?6g^i? zeip{B^ksPNuJr#UY^eKw`t*s!QUf-%a@d)V5I8$rjQ6sBiowu+w70j%6zkr2)Zk?G zYrPu{*k`4u<Mw_Iv$!><>`|7#d@X-?%XxA%8q|E<bH0P%+w#N(z2Gy~6?yr#-<}&Y zRaJ7bvd^Ye>!Hx=2PuY$Wo2a&rghu&=}*&YZ3lB1>uaglat@{%U8@YsjmGDRD^xT7 zTkla>^6(Nrb?ual3`Jkc(&Kj?aM{=MNzQG-U}k<5+DM(`>y)R!379pxMXI-Q-jH`1 zXIVC7jgCmM>PwfoPkgg0WLRZ(+CgBjP#{sfjws7VyFYyR3xCd!mtjgY<N1Bz0x*w} z+rSaa__27SlkOKH-UptVo<HY0EX@<yu0NTE4P2FwgChO^o}Hd&;#N5F)=V7>qr05^ zYYV;*!7L!}GOh4dK^b#=xU=}a6vO~i3p^R$-Kcof=pu9U9frQHD#-lkm`C2}@Z`v| zu({hoX%TVqePB<|#bX}c@@&nh!U6uc>i62%)K2)+LyVpw2QgZrP%lU6HJ4>5|02k! z5uzD~D^@}xkqbW3D|r-&JI*r(oFQkw$FMS0m&o)B@aW_x*aDrtL`YAn#3PGuriCL` zhf$UgRkyid?Q3K9e-5@>kAHPDgLe{M>7;o-p_{OJ#JM92Ajxz1CH(sp86};*7uT7e zzX6sUtGDxZUMMQ>8^i=dRsHhjbmBugo>D8hg{T;!V%j6)iy4$dKXxgz$KkLUyXrvz zodf-*I{401aTE0G2RCkYQ?yNDvbpdllrK`1bWW`+!GaTZ0w0>gjY>mYW?P>Ws1tz8 zBKZ9b$n;G+oUqMP#5ys$i(`uWO7a4KnG$r_J_fW}_QX{p7l%Wdy*GYkE?uLy?B_^y z{BO@>y<<;LAKyGGYIUd}RM`X9oa{4;AFJo2x=g@RK3&fZACa4XLakjSt<;Ib3JJb( z`*y5No)lucq3fu_KMA<=uv$L5>1TEINs;t(77p#G4e`Ieo`ecWbSR$}E{ct#(H(Tv z{TAw`--irLt#m8)Ltrtl6|Dw+xzU?UMh4X4y*AoPOo<+=W4+S^EHe|YR<HGRa;|(& zlDnPI5fBi5kq=n4sY<lA6EzV|FNQAC0@a_1-2uGFCHjjV6zeV`VUVonRtUI#ylSAo z4S_V}s`z?rOv<U9_6P)#{103j47W?5--&ECd!M1~ENWc{dTOxvv5C7mHUo%_8digE z8lU_VJ1X3h#TE8@X;|AUg6K-6QWLpkZD@o)pRnJE5#fP}-;8AjbL4a1Wv!5g0zkhV zaOhSjj#w2U3?Fe@lULhmX=k<)=tDcF-+8W$RSEQIC%k082GZO}%qMW&wlGB;5=NGE zi>mz(Hp?P;XGanJv=7ERhH0~yRS$y=D-imjWH@~bZpXsye2zs}8-T1B{>TeE>us1? z8&n|)3`|tBiumv5=hi1`5&GSih5L?X1OL3aKNIZrrW$ld{LFfoGnzLR7R`>R9LP~7 z-{Q6&%#=irXUN*VP5R{zrV3esOJWFj@AW|Ewt|Gw;{rh^?q?^yYGtjgf`Vo*KFxTC zkXkjv%sw5~YS4!c1AK_m8KDuZcAk=l@Y0qhxtm)+xnNdA*2_W7M(G0dYqxYW0NSui z{(yd^o%AQB=G7bfrtHX${f(&}ZWG9TuV!X&#g_POnEJiIUOg6ffaGEi4@XdJA6q(e z!4@3Hs~=-|&Iv-hVZS%OKWfwdb$_bCdC}ohnu9e!%lhu|5k2VKA5|aDL|?IkS@lw@ zbEgHd89$5I{E_KwwfsmVB}qhwe)Q>xNYaZJ_A7&VQs%6K@k3nzJ)NViHrA0740bM{ zVeDr|*pNVoeH-u^4p-;-W8h1g6obR(3-IlmOOOj};SCBS*MtvndPTmd%$vv<kn&m- zT}YnxWNC<OYf1+d(`fPNl^&Iy&naTF9nhc5-~*03;~#>8SW1E}7d312PQZgZF=zc( z4t&<bWZf4uWEf$o(dHMkU9M!?i4f+6WD+^dGSd5C_+u{_B4>bMPlRfI;NMdp8Cyb0 zmu>e~$s$A*cdq)*G=uIx4k=t;@bZqv7)0J!WmY>8l$2V@d)rh1@Tf(;0y%Q-zN3Lc zK=5_h_TJoa8>Vv?Jn_5zw=A4@Mm=`b^}-p7PrlMB9Lc$8#yFX%udyzsNMTi#8wvnK zG8}%1yHxvj4rDeUwP2YlC&Ry4rr?5j{&eO-2ay$GrILyu622hX-}ztMmmMCAg31qw z$*WJ~4a}q&OqLBaKU+JQ=50ONY}s1ZS}7ng(wH{7NKgw@a%BBZiqB6<{2{?jn(T=e zXuN^lv%Svvz(ZM(WU^l_>o(WcXyCmce?6|Lj#}uV50cs?@{$OzVwPd?Vtr{B)j9gE zeU;;hT3d-n4as*e$PQILX;~+6wUWSMpRA&c^4nR>v0bIhZQG_ZZkwY&rbqKD@^j76 z=MU~PG@{yxsxPA%Dm{AH^wb(mveo=&Tk-ce=5-f<5IGg;9_g<zsa;9~)wF%dOuWS@ zb6DD3@|BHCRx|myrp0|Re2Jwqg6*o4_-Bf3qi?4fzXQF&%M~JR76tyuTV6GhF65vv zIU%8qv%TFk{k+I=bc2Vi)422n>sWEIRD3m0=q38D-4R|swm;kRo!V5;%a(WD4W_kf zETHM>>2}<Le8Cqr2N!I-jBJc}&m}J`VW-FS@b)=x8dPqjdb+Dt5@N%~rBj3LjzX-U z6SsA%9mA+IYdxEsy5#UbhYD2j$LW4Z^^PtClku9hm!N*=4)|eU??N`R==Hag+ghpF z*)a^sD<5q7xzpl>zV!AQ*V!d5cUnZ!3EDg`W1=MMq)9$Q$RHk9<mkXlskWXO<*xsh z#cj{=DSL<2_sb+eJThbmC!?a}39bIBrauO9UW`3g5m|_|tPLgH*J-!=n67OmC}8d9 z*E|=4%M`yFfW`l2U-OopErT%^+d^vsjwp!`O<1`hp8-u<dkI0pQF{DBnuQucvINL8 zz#>t9GX<7e6N09j+-HOS;_tjXl;UG8F+pyYcQV|6=V2Y^OTjLGNwb}z(KBB7d~99Z zV3)?gqo_wc-QGG|S9i1h*sZ1%tY0H?sd!-J6uhc;nxp7>tyiI2f+i1uqbMFnv`nt^ zj)00o>71Xwn}4iw%tD6N=1)7&ts5aVKdZC+>F*^{yB~S`+3z0Dot$kn&c;MBYyF6E zXf>8^fp}(g+__83G|U`hK;^Ksj#U-YHez3n8o?A$V+!0~xz3PGzB=TKqj0dH<&pVX zpAdVwjmu&9JpYbh8uX=~o#49A^GqJ%k}4{qG7Fy<7677}?8nfi?5DvlO$qba(%<t` z{Y?t(O7h5@tFtF>&sLu{UTINv_!9H)6%e-<Nq@Ml6uy`k^uy-z(1u!rjbJslGL)CI zy3TbraE9dTB41|yhQ~Q;(;RiXFG>|OYluc)+Vy(%+(mjw?^WqQT)<DU!A_Neo7X@U zce<!MG@_zcWy$;7N}gZc=Q{^YVnXn`UrqcZG^@<sqtR$%`J{}D)d$M7VtF-|J$_7< zCCkp=Dl#HC#-yw2=VslxYqfR@)w)z9)Owg5d0^I(wYVE2#AdYW-O479YN{-C4ihR} zCeD+`&$}21LzAZU4)qq@QPpvbGK!lMwGr??KAY1u`GLo7j7YO!GMr1Mea;z)LOmPG zjb;K@M9OYgoiAj|&gO_RCgEzL&2g`xDst&c6ICe^Hzvk(>hH1$xKWmnLZ}w6RST~( zHlAqM6lR%Z-cH4}h<6(36vkdv((PjkP<ML=q)Bn%A*WZAiLQwk0^I^|C$Og$uiH4_ zPF`_iC<>cD$u2-c(UO1`UcSl_ds}k2PWb&bPkwNA4UlSBv-6#n=w?Aw9*T!mP`V%Q zVK6ibqsf(=p$r@__3%jB0y)coyU|{?#$R{ZPWQ<A<7AYHJ=Op80r90dW7c-f$ax)L 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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/340] 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 29d1f1f14aa7e9004258ff4341e91e0eb536e9a5 Mon Sep 17 00:00:00 2001 From: AlexeyZavar <sltkval1@gmail.com> Date: Fri, 6 Jun 2025 11:00:03 +0300 Subject: [PATCH 159/340] fix: copy sticker owner ID if not found --- Telegram/SourceFiles/boxes/sticker_set_box.cpp | 5 +++-- .../SourceFiles/info/profile/info_profile_actions.cpp | 9 +++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index bfca445777..02db10e63e 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -791,7 +791,7 @@ void StickerSetBox::updateButtons() { searchById( innerId, session, - [session, weak](const QString &username, UserData *user) + [session, weak, innerId](const QString &username, UserData *user) { if (!weak) { return; @@ -803,7 +803,8 @@ void StickerSetBox::updateButtons() { } if (!user) { - strongInner->showToast(tr::ayu_UserNotFoundMessage(tr::now)); + QGuiApplication::clipboard()->setText(QString::number(innerId)); + strongInner->showToast(tr::ayu_IDCopiedToast(tr::now)); return; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 720389f8e1..843f7bff4b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1524,8 +1524,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() { const auto idText = IDString(user); if (!idText.isEmpty()) { QGuiApplication::clipboard()->setText(idText); - const auto msg = tr::ayu_IDCopiedToast(tr::now); - controller->showToast(msg); + controller->showToast(tr::ayu_IDCopiedToast(tr::now)); } return false; }); @@ -1660,8 +1659,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() { const auto idText = IDString(peer); if (!idText.isEmpty()) { QGuiApplication::clipboard()->setText(idText); - const auto msg = tr::ayu_IDCopiedToast(tr::now); - controller->showToast(msg); + controller->showToast(tr::ayu_IDCopiedToast(tr::now)); } return false; }); @@ -1685,8 +1683,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() { const auto idText = IDString(peer->forumTopicFor(topicRootId)->topicRootId()); if (!idText.isEmpty()) { QGuiApplication::clipboard()->setText(idText); - const auto msg = tr::ayu_IDCopiedToast(tr::now); - controller->showToast(msg); + controller->showToast(tr::ayu_IDCopiedToast(tr::now)); } return false; }); From dc33accae77cc6d872ceced996d75d361c76ed74 Mon Sep 17 00:00:00 2001 From: AlexeyZavar <sltkval1@gmail.com> Date: Fri, 6 Jun 2025 11:38:15 +0300 Subject: [PATCH 160/340] chore: update sqlite3 --- .../SourceFiles/ayu/libs/sqlite/sqlite3.c | 4317 +++++++++++------ .../SourceFiles/ayu/libs/sqlite/sqlite3.h | 146 +- 2 files changed, 2919 insertions(+), 1544 deletions(-) diff --git a/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.c b/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.c index 37b534afb2..58dc0e9208 100644 --- a/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.c +++ b/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.c @@ -1,6 +1,6 @@ /****************************************************************************** ** This file is an amalgamation of many separate C source files from SQLite -** version 3.49.1. By combining all the individual C code files into this +** version 3.50.0. By combining all the individual C code files into this ** single large file, the entire code can be compiled as a single translation ** unit. This allows many compilers to do optimizations that would not be ** possible if the files were compiled separately. Performance improvements @@ -18,7 +18,7 @@ ** separate file. This file contains only code for the core SQLite library. ** ** The content in this amalgamation comes from Fossil check-in -** 873d4e274b4988d260ba8354a9718324a1c2 with changes in files: +** dfc790f998f450d9c35e3ba1c8c89c17466c with changes in files: ** ** */ @@ -452,7 +452,7 @@ extern "C" { ** ** Since [version 3.6.18] ([dateof:3.6.18]), ** SQLite source code has been stored in the -** <a href="http://www.fossil-scm.org/">Fossil configuration management +** <a href="http://fossil-scm.org/">Fossil configuration management ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to ** a string which identifies a particular check-in of SQLite ** within its configuration management system. ^The SQLITE_SOURCE_ID @@ -465,9 +465,9 @@ extern "C" { ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ -#define SQLITE_VERSION "3.49.1" -#define SQLITE_VERSION_NUMBER 3049001 -#define SQLITE_SOURCE_ID "2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70" +#define SQLITE_VERSION "3.50.0" +#define SQLITE_VERSION_NUMBER 3050000 +#define SQLITE_SOURCE_ID "2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742" /* ** CAPI3REF: Run-Time Library Version Numbers @@ -1482,6 +1482,12 @@ struct sqlite3_io_methods { ** the value that M is to be set to. Before returning, the 32-bit signed ** integer is overwritten with the previous value of M. ** +** <li>[[SQLITE_FCNTL_BLOCK_ON_CONNECT]] +** The [SQLITE_FCNTL_BLOCK_ON_CONNECT] opcode is used to configure the +** VFS to block when taking a SHARED lock to connect to a wal mode database. +** This is used to implement the functionality associated with +** SQLITE_SETLK_BLOCK_ON_CONNECT. +** ** <li>[[SQLITE_FCNTL_DATA_VERSION]] ** The [SQLITE_FCNTL_DATA_VERSION] opcode is used to detect changes to ** a database file. The argument is a pointer to a 32-bit unsigned integer. @@ -1578,6 +1584,7 @@ struct sqlite3_io_methods { #define SQLITE_FCNTL_CKSM_FILE 41 #define SQLITE_FCNTL_RESET_CACHE 42 #define SQLITE_FCNTL_NULL_IO 43 +#define SQLITE_FCNTL_BLOCK_ON_CONNECT 44 /* deprecated names */ #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE @@ -2308,13 +2315,16 @@ struct sqlite3_mem_methods { ** ** [[SQLITE_CONFIG_LOOKASIDE]] <dt>SQLITE_CONFIG_LOOKASIDE</dt> ** <dd> ^(The SQLITE_CONFIG_LOOKASIDE option takes two arguments that determine -** the default size of lookaside memory on each [database connection]. +** the default size of [lookaside memory] on each [database connection]. ** The first argument is the -** size of each lookaside buffer slot and the second is the number of -** slots allocated to each database connection.)^ ^(SQLITE_CONFIG_LOOKASIDE -** sets the <i>default</i> lookaside size. The [SQLITE_DBCONFIG_LOOKASIDE] -** option to [sqlite3_db_config()] can be used to change the lookaside -** configuration on individual connections.)^ </dd> +** size of each lookaside buffer slot ("sz") and the second is the number of +** slots allocated to each database connection ("cnt").)^ +** ^(SQLITE_CONFIG_LOOKASIDE sets the <i>default</i> lookaside size. +** The [SQLITE_DBCONFIG_LOOKASIDE] option to [sqlite3_db_config()] can +** be used to change the lookaside configuration on individual connections.)^ +** The [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to change the +** default lookaside configuration at compile-time. +** </dd> ** ** [[SQLITE_CONFIG_PCACHE2]] <dt>SQLITE_CONFIG_PCACHE2</dt> ** <dd> ^(The SQLITE_CONFIG_PCACHE2 option takes a single argument which is @@ -2551,31 +2561,50 @@ struct sqlite3_mem_methods { ** [[SQLITE_DBCONFIG_LOOKASIDE]] ** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt> ** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the -** configuration of the lookaside memory allocator within a database +** configuration of the [lookaside memory allocator] within a database ** connection. ** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i> ** in the [DBCONFIG arguments|usual format]. ** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two, ** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE ** should have a total of five parameters. -** ^The first argument (the third parameter to [sqlite3_db_config()] is a +** <ol> +** <li><p>The first argument ("buf") is a ** pointer to a memory buffer to use for lookaside memory. -** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb -** may be NULL in which case SQLite will allocate the -** lookaside buffer itself using [sqlite3_malloc()]. ^The second argument is the -** size of each lookaside buffer slot. ^The third argument is the number of -** slots. The size of the buffer in the first argument must be greater than -** or equal to the product of the second and third arguments. The buffer -** must be aligned to an 8-byte boundary. ^If the second argument to -** SQLITE_DBCONFIG_LOOKASIDE is not a multiple of 8, it is internally -** rounded down to the next smaller multiple of 8. ^(The lookaside memory +** The first argument may be NULL in which case SQLite will allocate the +** lookaside buffer itself using [sqlite3_malloc()]. +** <li><P>The second argument ("sz") is the +** size of each lookaside buffer slot. Lookaside is disabled if "sz" +** is less than 8. The "sz" argument should be a multiple of 8 less than +** 65536. If "sz" does not meet this constraint, it is reduced in size until +** it does. +** <li><p>The third argument ("cnt") is the number of slots. Lookaside is disabled +** if "cnt"is less than 1. The "cnt" value will be reduced, if necessary, so +** that the product of "sz" and "cnt" does not exceed 2,147,418,112. The "cnt" +** parameter is usually chosen so that the product of "sz" and "cnt" is less +** than 1,000,000. +** </ol> +** <p>If the "buf" argument is not NULL, then it must +** point to a memory buffer with a size that is greater than +** or equal to the product of "sz" and "cnt". +** The buffer must be aligned to an 8-byte boundary. +** The lookaside memory ** configuration for a database connection can only be changed when that ** connection is not currently using lookaside memory, or in other words -** when the "current value" returned by -** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero. +** when the value returned by [SQLITE_DBSTATUS_LOOKASIDE_USED] is zero. ** Any attempt to change the lookaside memory configuration when lookaside ** memory is in use leaves the configuration unchanged and returns -** [SQLITE_BUSY].)^</dd> +** [SQLITE_BUSY]. +** If the "buf" argument is NULL and an attempt +** to allocate memory based on "sz" and "cnt" fails, then +** lookaside is silently disabled. +** <p> +** The [SQLITE_CONFIG_LOOKASIDE] configuration option can be used to set the +** default lookaside configuration at initialization. The +** [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to set the default lookaside +** configuration at compile-time. Typical values for lookaside are 1200 for +** "sz" and 40 to 100 for "cnt". +** </dd> ** ** [[SQLITE_DBCONFIG_ENABLE_FKEY]] ** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt> @@ -3312,6 +3341,44 @@ SQLITE_API int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*); */ SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms); +/* +** CAPI3REF: Set the Setlk Timeout +** METHOD: sqlite3 +** +** This routine is only useful in SQLITE_ENABLE_SETLK_TIMEOUT builds. If +** the VFS supports blocking locks, it sets the timeout in ms used by +** eligible locks taken on wal mode databases by the specified database +** handle. In non-SQLITE_ENABLE_SETLK_TIMEOUT builds, or if the VFS does +** not support blocking locks, this function is a no-op. +** +** Passing 0 to this function disables blocking locks altogether. Passing +** -1 to this function requests that the VFS blocks for a long time - +** indefinitely if possible. The results of passing any other negative value +** are undefined. +** +** Internally, each SQLite database handle store two timeout values - the +** busy-timeout (used for rollback mode databases, or if the VFS does not +** support blocking locks) and the setlk-timeout (used for blocking locks +** on wal-mode databases). The sqlite3_busy_timeout() method sets both +** values, this function sets only the setlk-timeout value. Therefore, +** to configure separate busy-timeout and setlk-timeout values for a single +** database handle, call sqlite3_busy_timeout() followed by this function. +** +** Whenever the number of connections to a wal mode database falls from +** 1 to 0, the last connection takes an exclusive lock on the database, +** then checkpoints and deletes the wal file. While it is doing this, any +** new connection that tries to read from the database fails with an +** SQLITE_BUSY error. Or, if the SQLITE_SETLK_BLOCK_ON_CONNECT flag is +** passed to this API, the new connection blocks until the exclusive lock +** has been released. +*/ +SQLITE_API int sqlite3_setlk_timeout(sqlite3*, int ms, int flags); + +/* +** CAPI3REF: Flags for sqlite3_setlk_timeout() +*/ +#define SQLITE_SETLK_BLOCK_ON_CONNECT 0x01 + /* ** CAPI3REF: Convenience Routines For Running Queries ** METHOD: sqlite3 @@ -5427,7 +5494,7 @@ SQLITE_API const void *sqlite3_column_decltype16(sqlite3_stmt*,int); ** other than [SQLITE_ROW] before any subsequent invocation of ** sqlite3_step(). Failure to reset the prepared statement using ** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from -** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1], +** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1]), ** sqlite3_step() began ** calling [sqlite3_reset()] automatically in this circumstance rather ** than returning [SQLITE_MISUSE]. This is not considered a compatibility @@ -7323,6 +7390,8 @@ SQLITE_API int sqlite3_autovacuum_pages( ** ** ^The second argument is a pointer to the function to invoke when a ** row is updated, inserted or deleted in a rowid table. +** ^The update hook is disabled by invoking sqlite3_update_hook() +** with a NULL pointer as the second parameter. ** ^The first argument to the callback is a copy of the third argument ** to sqlite3_update_hook(). ** ^The second callback argument is one of [SQLITE_INSERT], [SQLITE_DELETE], @@ -11805,9 +11874,10 @@ SQLITE_API void sqlite3session_table_filter( ** is inserted while a session object is enabled, then later deleted while ** the same session object is disabled, no INSERT record will appear in the ** changeset, even though the delete took place while the session was disabled. -** Or, if one field of a row is updated while a session is disabled, and -** another field of the same row is updated while the session is enabled, the -** resulting changeset will contain an UPDATE change that updates both fields. +** Or, if one field of a row is updated while a session is enabled, and +** then another field of the same row is updated while the session is disabled, +** the resulting changeset will contain an UPDATE change that updates both +** fields. */ SQLITE_API int sqlite3session_changeset( sqlite3_session *pSession, /* Session object */ @@ -11879,8 +11949,9 @@ SQLITE_API sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession ** database zFrom the contents of the two compatible tables would be ** identical. ** -** It an error if database zFrom does not exist or does not contain the -** required compatible table. +** Unless the call to this function is a no-op as described above, it is an +** error if database zFrom does not exist or does not contain the required +** compatible table. ** ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg @@ -12015,7 +12086,7 @@ SQLITE_API int sqlite3changeset_start_v2( ** The following flags may passed via the 4th parameter to ** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]: ** -** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd> +** <dt>SQLITE_CHANGESETSTART_INVERT <dd> ** Invert the changeset while iterating through it. This is equivalent to ** inverting a changeset using sqlite3changeset_invert() before applying it. ** It is an error to specify this flag with a patchset. @@ -12330,19 +12401,6 @@ SQLITE_API int sqlite3changeset_concat( void **ppOut /* OUT: Buffer containing output changeset */ ); - -/* -** CAPI3REF: Upgrade the Schema of a Changeset/Patchset -*/ -SQLITE_API int sqlite3changeset_upgrade( - sqlite3 *db, - const char *zDb, - int nIn, const void *pIn, /* Input changeset */ - int *pnOut, void **ppOut /* OUT: Inverse of input */ -); - - - /* ** CAPI3REF: Changegroup Handle ** @@ -14090,14 +14148,22 @@ struct fts5_api { ** * Terms in the GROUP BY or ORDER BY clauses of a SELECT statement. ** * Terms in the VALUES clause of an INSERT statement ** -** The hard upper limit here is 32676. Most database people will +** The hard upper limit here is 32767. Most database people will ** tell you that in a well-normalized database, you usually should ** not have more than a dozen or so columns in any table. And if ** that is the case, there is no point in having more than a few ** dozen values in any of the other situations described above. +** +** An index can only have SQLITE_MAX_COLUMN columns from the user +** point of view, but the underlying b-tree that implements the index +** might have up to twice as many columns in a WITHOUT ROWID table, +** since must also store the primary key at the end. Hence the +** column count for Index is u16 instead of i16. */ -#ifndef SQLITE_MAX_COLUMN +#if !defined(SQLITE_MAX_COLUMN) # define SQLITE_MAX_COLUMN 2000 +#elif SQLITE_MAX_COLUMN>32767 +# error SQLITE_MAX_COLUMN may not exceed 32767 #endif /* @@ -14749,6 +14815,7 @@ struct HashElem { HashElem *next, *prev; /* Next and previous elements in the table */ void *data; /* Data associated with this element */ const char *pKey; /* Key associated with this element */ + unsigned int h; /* hash for pKey */ }; /* @@ -15109,7 +15176,17 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash*); ** ourselves. */ #ifndef offsetof -#define offsetof(STRUCTURE,FIELD) ((int)((char*)&((STRUCTURE*)0)->FIELD)) +#define offsetof(STRUCTURE,FIELD) ((size_t)((char*)&((STRUCTURE*)0)->FIELD)) +#endif + +/* +** Work around C99 "flex-array" syntax for pre-C99 compilers, so as +** to avoid complaints from -fsanitize=strict-bounds. +*/ +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define FLEXARRAY +#else +# define FLEXARRAY 1 #endif /* @@ -15187,6 +15264,11 @@ typedef INT16_TYPE i16; /* 2-byte signed integer */ typedef UINT8_TYPE u8; /* 1-byte unsigned integer */ typedef INT8_TYPE i8; /* 1-byte signed integer */ +/* A bitfield type for use inside of structures. Always follow with :N where +** N is the number of bits. +*/ +typedef unsigned bft; /* Bit Field Type */ + /* ** SQLITE_MAX_U32 is a u64 constant that is the maximum u64 value ** that can be stored in a u32 without loss of data. The value @@ -15355,6 +15437,14 @@ typedef INT16_TYPE LogEst; #define LARGEST_UINT64 (0xffffffff|(((u64)0xffffffff)<<32)) #define SMALLEST_INT64 (((i64)-1) - LARGEST_INT64) +/* +** Macro SMXV(n) return the maximum value that can be held in variable n, +** assuming n is a signed integer type. UMXV(n) is similar for unsigned +** integer types. +*/ +#define SMXV(n) ((((i64)1)<<(sizeof(n)-1))-1) +#define UMXV(n) ((((i64)1)<<(sizeof(n)))-1) + /* ** Round up a number to the next larger multiple of 8. This is used ** to force 8-byte alignment on 64-bit architectures. @@ -17331,8 +17421,8 @@ SQLITE_PRIVATE int sqlite3NotPureFunc(sqlite3_context*); SQLITE_PRIVATE int sqlite3VdbeBytecodeVtabInit(sqlite3*); #endif -/* Use SQLITE_ENABLE_COMMENTS to enable generation of extra comments on -** each VDBE opcode. +/* Use SQLITE_ENABLE_EXPLAIN_COMMENTS to enable generation of extra +** comments on each VDBE opcode. ** ** Use the SQLITE_ENABLE_MODULE_COMMENTS macro to see some extra no-op ** comments in VDBE programs that show key decision points in the code @@ -18055,6 +18145,10 @@ struct sqlite3 { Savepoint *pSavepoint; /* List of active savepoints */ int nAnalysisLimit; /* Number of index rows to ANALYZE */ int busyTimeout; /* Busy handler timeout, in msec */ +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + int setlkTimeout; /* Blocking lock timeout, in msec. -1 -> inf. */ + int setlkFlags; /* Flags passed to setlk_timeout() */ +#endif int nSavepoint; /* Number of non-transaction savepoints */ int nStatement; /* Number of nested statement-transactions */ i64 nDeferredCons; /* Net deferred constraints this transaction. */ @@ -18733,6 +18827,7 @@ struct Table { } u; Trigger *pTrigger; /* List of triggers on this object */ Schema *pSchema; /* Schema that contains this table */ + u8 aHx[16]; /* Column aHt[K%sizeof(aHt)] might have hash K */ }; /* @@ -18866,9 +18961,13 @@ struct FKey { struct sColMap { /* Mapping of columns in pFrom to columns in zTo */ int iFrom; /* Index of column in pFrom */ char *zCol; /* Name of column in zTo. If NULL use PRIMARY KEY */ - } aCol[1]; /* One entry for each of nCol columns */ + } aCol[FLEXARRAY]; /* One entry for each of nCol columns */ }; +/* The size (in bytes) of an FKey object holding N columns. The answer +** does NOT include space to hold the zTo name. */ +#define SZ_FKEY(N) (offsetof(FKey,aCol)+(N)*sizeof(struct sColMap)) + /* ** SQLite supports many different ways to resolve a constraint ** error. ROLLBACK processing means that a constraint violation @@ -18930,9 +19029,12 @@ struct KeyInfo { u16 nAllField; /* Total columns, including key plus others */ sqlite3 *db; /* The database connection */ u8 *aSortFlags; /* Sort order for each column. */ - CollSeq *aColl[1]; /* Collating sequence for each term of the key */ + CollSeq *aColl[FLEXARRAY]; /* Collating sequence for each term of the key */ }; +/* The size (in bytes) of a KeyInfo object with up to N fields */ +#define SZ_KEYINFO(N) (offsetof(KeyInfo,aColl) + (N)*sizeof(CollSeq*)) + /* ** Allowed bit values for entries in the KeyInfo.aSortFlags[] array. */ @@ -19052,7 +19154,7 @@ struct Index { Pgno tnum; /* DB Page containing root of this index */ LogEst szIdxRow; /* Estimated average row size in bytes */ u16 nKeyCol; /* Number of columns forming the key */ - u16 nColumn; /* Number of columns stored in the index */ + u16 nColumn; /* Nr columns in btree. Can be 2*Table.nCol */ u8 onError; /* OE_Abort, OE_Ignore, OE_Replace, or OE_None */ unsigned idxType:2; /* 0:Normal 1:UNIQUE, 2:PRIMARY KEY, 3:IPK */ unsigned bUnordered:1; /* Use this index for == or IN queries only */ @@ -19061,9 +19163,9 @@ struct Index { unsigned isCovering:1; /* True if this is a covering index */ unsigned noSkipScan:1; /* Do not try to use skip-scan if true */ unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */ - unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */ unsigned bNoQuery:1; /* Do not use this index to optimize queries */ unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */ + unsigned bIdxRowid:1; /* One or more of the index keys is the ROWID */ unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */ unsigned bHasExpr:1; /* Index contains an expression, either a literal ** expression, or a reference to a VIRTUAL column */ @@ -19390,10 +19492,10 @@ struct Expr { /* Macros can be used to test, set, or clear bits in the ** Expr.flags field. */ -#define ExprHasProperty(E,P) (((E)->flags&(P))!=0) -#define ExprHasAllProperty(E,P) (((E)->flags&(P))==(P)) -#define ExprSetProperty(E,P) (E)->flags|=(P) -#define ExprClearProperty(E,P) (E)->flags&=~(P) +#define ExprHasProperty(E,P) (((E)->flags&(u32)(P))!=0) +#define ExprHasAllProperty(E,P) (((E)->flags&(u32)(P))==(u32)(P)) +#define ExprSetProperty(E,P) (E)->flags|=(u32)(P) +#define ExprClearProperty(E,P) (E)->flags&=~(u32)(P) #define ExprAlwaysTrue(E) (((E)->flags&(EP_OuterON|EP_IsTrue))==EP_IsTrue) #define ExprAlwaysFalse(E) (((E)->flags&(EP_OuterON|EP_IsFalse))==EP_IsFalse) #define ExprIsFullSize(E) (((E)->flags&(EP_Reduced|EP_TokenOnly))==0) @@ -19505,9 +19607,14 @@ struct ExprList { int iConstExprReg; /* Register in which Expr value is cached. Used only ** by Parse.pConstExpr */ } u; - } a[1]; /* One slot for each expression in the list */ + } a[FLEXARRAY]; /* One slot for each expression in the list */ }; +/* The size (in bytes) of an ExprList object that is big enough to hold +** as many as N expressions. */ +#define SZ_EXPRLIST(N) \ + (offsetof(ExprList,a) + (N)*sizeof(struct ExprList_item)) + /* ** Allowed values for Expr.a.eEName */ @@ -19535,9 +19642,12 @@ struct IdList { int nId; /* Number of identifiers on the list */ struct IdList_item { char *zName; /* Name of the identifier */ - } a[1]; + } a[FLEXARRAY]; }; +/* The size (in bytes) of an IdList object that can hold up to N IDs. */ +#define SZ_IDLIST(N) (offsetof(IdList,a)+(N)*sizeof(struct IdList_item)) + /* ** Allowed values for IdList.eType, which determines which value of the a.u4 ** is valid. @@ -19657,11 +19767,19 @@ struct OnOrUsing { ** */ struct SrcList { - int nSrc; /* Number of tables or subqueries in the FROM clause */ - u32 nAlloc; /* Number of entries allocated in a[] below */ - SrcItem a[1]; /* One entry for each identifier on the list */ + int nSrc; /* Number of tables or subqueries in the FROM clause */ + u32 nAlloc; /* Number of entries allocated in a[] below */ + SrcItem a[FLEXARRAY]; /* One entry for each identifier on the list */ }; +/* Size (in bytes) of a SrcList object that can hold as many as N +** SrcItem objects. */ +#define SZ_SRCLIST(N) (offsetof(SrcList,a)+(N)*sizeof(SrcItem)) + +/* Size (in bytes( of a SrcList object that holds 1 SrcItem. This is a +** special case of SZ_SRCITEM(1) that comes up often. */ +#define SZ_SRCLIST_1 (offsetof(SrcList,a)+sizeof(SrcItem)) + /* ** Permitted values of the SrcList.a.jointype field */ @@ -20130,25 +20248,32 @@ struct Parse { char *zErrMsg; /* An error message */ Vdbe *pVdbe; /* An engine for executing database bytecode */ int rc; /* Return code from execution */ - u8 colNamesSet; /* TRUE after OP_ColumnName has been issued to pVdbe */ - u8 checkSchema; /* Causes schema cookie check after an error */ + LogEst nQueryLoop; /* Est number of iterations of a query (10*log2(N)) */ u8 nested; /* Number of nested calls to the parser/code generator */ u8 nTempReg; /* Number of temporary registers in aTempReg[] */ u8 isMultiWrite; /* True if statement may modify/insert multiple rows */ u8 mayAbort; /* True if statement may throw an ABORT exception */ u8 hasCompound; /* Need to invoke convertCompoundSelectToSubquery() */ - u8 okConstFactor; /* OK to factor out constants */ u8 disableLookaside; /* Number of times lookaside has been disabled */ u8 prepFlags; /* SQLITE_PREPARE_* flags */ u8 withinRJSubrtn; /* Nesting level for RIGHT JOIN body subroutines */ - u8 bHasWith; /* True if statement contains WITH */ u8 mSubrtnSig; /* mini Bloom filter on available SubrtnSig.selId */ + u8 eTriggerOp; /* TK_UPDATE, TK_INSERT or TK_DELETE */ + u8 bReturning; /* Coding a RETURNING trigger */ + u8 eOrconf; /* Default ON CONFLICT policy for trigger steps */ + u8 disableTriggers; /* True to disable triggers */ #if defined(SQLITE_DEBUG) || defined(SQLITE_COVERAGE_TEST) u8 earlyCleanup; /* OOM inside sqlite3ParserAddCleanup() */ #endif #ifdef SQLITE_DEBUG u8 ifNotExists; /* Might be true if IF NOT EXISTS. Assert()s only */ + u8 isCreate; /* CREATE TABLE, INDEX, or VIEW (but not TRIGGER) + ** and ALTER TABLE ADD COLUMN. */ #endif + bft colNamesSet :1; /* TRUE after OP_ColumnName has been issued to pVdbe */ + bft bHasWith :1; /* True if statement contains WITH */ + bft okConstFactor :1; /* OK to factor out constants */ + bft checkSchema :1; /* Causes schema cookie check after an error */ int nRangeReg; /* Size of the temporary register block */ int iRangeReg; /* First register in temporary register block */ int nErr; /* Number of errors seen */ @@ -20163,12 +20288,9 @@ struct Parse { ExprList *pConstExpr;/* Constant expressions */ IndexedExpr *pIdxEpr;/* List of expressions used by active indexes */ IndexedExpr *pIdxPartExpr; /* Exprs constrained by index WHERE clauses */ - Token constraintName;/* Name of the constraint currently being parsed */ yDbMask writeMask; /* Start a write transaction on these databases */ yDbMask cookieMask; /* Bitmask of schema verified databases */ - int regRowid; /* Register holding rowid of CREATE TABLE entry */ - int regRoot; /* Register holding root page number for new objects */ - int nMaxArg; /* Max args passed to user function by sub-program */ + int nMaxArg; /* Max args to xUpdate and xFilter vtab methods */ int nSelect; /* Number of SELECT stmts. Counter for Select.selId */ #ifndef SQLITE_OMIT_PROGRESS_CALLBACK u32 nProgressSteps; /* xProgress steps taken during sqlite3_prepare() */ @@ -20182,17 +20304,6 @@ struct Parse { Table *pTriggerTab; /* Table triggers are being coded for */ TriggerPrg *pTriggerPrg; /* Linked list of coded triggers */ ParseCleanup *pCleanup; /* List of cleanup operations to run after parse */ - union { - int addrCrTab; /* Address of OP_CreateBtree on CREATE TABLE */ - Returning *pReturning; /* The RETURNING clause */ - } u1; - u32 oldmask; /* Mask of old.* columns referenced */ - u32 newmask; /* Mask of new.* columns referenced */ - LogEst nQueryLoop; /* Est number of iterations of a query (10*log2(N)) */ - u8 eTriggerOp; /* TK_UPDATE, TK_INSERT or TK_DELETE */ - u8 bReturning; /* Coding a RETURNING trigger */ - u8 eOrconf; /* Default ON CONFLICT policy for trigger steps */ - u8 disableTriggers; /* True to disable triggers */ /************************************************************************** ** Fields above must be initialized to zero. The fields that follow, @@ -20204,6 +20315,19 @@ struct Parse { int aTempReg[8]; /* Holding area for temporary registers */ Parse *pOuterParse; /* Outer Parse object when nested */ Token sNameToken; /* Token with unqualified schema object name */ + u32 oldmask; /* Mask of old.* columns referenced */ + u32 newmask; /* Mask of new.* columns referenced */ + union { + struct { /* These fields available when isCreate is true */ + int addrCrTab; /* Address of OP_CreateBtree on CREATE TABLE */ + int regRowid; /* Register holding rowid of CREATE TABLE entry */ + int regRoot; /* Register holding root page for new objects */ + Token constraintName; /* Name of the constraint currently being parsed */ + } cr; + struct { /* These fields available to all other statements */ + Returning *pReturning; /* The RETURNING clause */ + } d; + } u1; /************************************************************************ ** Above is constant between recursions. Below is reset before and after @@ -20719,9 +20843,13 @@ struct With { int nCte; /* Number of CTEs in the WITH clause */ int bView; /* Belongs to the outermost Select of a view */ With *pOuter; /* Containing WITH clause, or NULL */ - Cte a[1]; /* For each CTE in the WITH clause.... */ + Cte a[FLEXARRAY]; /* For each CTE in the WITH clause.... */ }; +/* The size (in bytes) of a With object that can hold as many +** as N different CTEs. */ +#define SZ_WITH(N) (offsetof(With,a) + (N)*sizeof(Cte)) + /* ** The Cte object is not guaranteed to persist for the entire duration ** of code generation. (The query flattener or other parser tree @@ -20750,9 +20878,13 @@ struct DbClientData { DbClientData *pNext; /* Next in a linked list */ void *pData; /* The data */ void (*xDestructor)(void*); /* Destructor. Might be NULL */ - char zName[1]; /* Name of this client data. MUST BE LAST */ + char zName[FLEXARRAY]; /* Name of this client data. MUST BE LAST */ }; +/* The size (in bytes) of a DbClientData object that can has a name +** that is N bytes long, including the zero-terminator. */ +#define SZ_DBCLIENTDATA(N) (offsetof(DbClientData,zName)+(N)) + #ifdef SQLITE_DEBUG /* ** An instance of the TreeView object is used for printing the content of @@ -21195,7 +21327,7 @@ SQLITE_PRIVATE void sqlite3SubqueryColumnTypes(Parse*,Table*,Select*,char); SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse*,Select*,char); SQLITE_PRIVATE void sqlite3OpenSchemaTable(Parse *, int); SQLITE_PRIVATE Index *sqlite3PrimaryKeyIndex(Table*); -SQLITE_PRIVATE i16 sqlite3TableColumnToIndex(Index*, i16); +SQLITE_PRIVATE int sqlite3TableColumnToIndex(Index*, int); #ifdef SQLITE_OMIT_GENERATED_COLUMNS # define sqlite3TableColumnToStorage(T,X) (X) /* No-op pass-through */ # define sqlite3StorageColumnToTable(T,X) (X) /* No-op pass-through */ @@ -21293,7 +21425,7 @@ SQLITE_PRIVATE void sqlite3SrcListAssignCursors(Parse*, SrcList*); SQLITE_PRIVATE void sqlite3IdListDelete(sqlite3*, IdList*); SQLITE_PRIVATE void sqlite3ClearOnOrUsing(sqlite3*, OnOrUsing*); SQLITE_PRIVATE void sqlite3SrcListDelete(sqlite3*, SrcList*); -SQLITE_PRIVATE Index *sqlite3AllocateIndexObject(sqlite3*,i16,int,char**); +SQLITE_PRIVATE Index *sqlite3AllocateIndexObject(sqlite3*,int,int,char**); SQLITE_PRIVATE void sqlite3CreateIndex(Parse*,Token*,Token*,SrcList*,ExprList*,int,Token*, Expr*, int, int, u8); SQLITE_PRIVATE void sqlite3DropIndex(Parse*, SrcList*, int); @@ -21429,7 +21561,8 @@ SQLITE_PRIVATE Select *sqlite3SelectDup(sqlite3*,const Select*,int); SQLITE_PRIVATE FuncDef *sqlite3FunctionSearch(int,const char*); SQLITE_PRIVATE void sqlite3InsertBuiltinFuncs(FuncDef*,int); SQLITE_PRIVATE FuncDef *sqlite3FindFunction(sqlite3*,const char*,int,u8,u8); -SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum*,sqlite3_value*); +SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum*,sqlite3_value*,int); +SQLITE_PRIVATE int sqlite3AppendOneUtf8Character(char*, u32); SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(void); SQLITE_PRIVATE void sqlite3RegisterDateTimeFunctions(void); SQLITE_PRIVATE void sqlite3RegisterJsonFunctions(void); @@ -22294,6 +22427,9 @@ static const char * const sqlite3azCompileOpt[] = { #ifdef SQLITE_BUG_COMPATIBLE_20160819 "BUG_COMPATIBLE_20160819", #endif +#ifdef SQLITE_BUG_COMPATIBLE_20250510 + "BUG_COMPATIBLE_20250510", +#endif #ifdef SQLITE_CASE_SENSITIVE_LIKE "CASE_SENSITIVE_LIKE", #endif @@ -22530,6 +22666,9 @@ static const char * const sqlite3azCompileOpt[] = { #ifdef SQLITE_ENABLE_SESSION "ENABLE_SESSION", #endif +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + "ENABLE_SETLK_TIMEOUT", +#endif #ifdef SQLITE_ENABLE_SNAPSHOT "ENABLE_SNAPSHOT", #endif @@ -22584,6 +22723,9 @@ static const char * const sqlite3azCompileOpt[] = { #ifdef SQLITE_EXTRA_INIT "EXTRA_INIT=" CTIMEOPT_VAL(SQLITE_EXTRA_INIT), #endif +#ifdef SQLITE_EXTRA_INIT_MUTEXED + "EXTRA_INIT_MUTEXED=" CTIMEOPT_VAL(SQLITE_EXTRA_INIT_MUTEXED), +#endif #ifdef SQLITE_EXTRA_SHUTDOWN "EXTRA_SHUTDOWN=" CTIMEOPT_VAL(SQLITE_EXTRA_SHUTDOWN), #endif @@ -23568,12 +23710,19 @@ struct VdbeCursor { #endif VdbeTxtBlbCache *pCache; /* Cache of large TEXT or BLOB values */ - /* 2*nField extra array elements allocated for aType[], beyond the one - ** static element declared in the structure. nField total array slots for - ** aType[] and nField+1 array slots for aOffset[] */ - u32 aType[1]; /* Type values record decode. MUST BE LAST */ + /* Space is allocated for aType to hold at least 2*nField+1 entries: + ** nField slots for aType[] and nField+1 array slots for aOffset[] */ + u32 aType[FLEXARRAY]; /* Type values record decode. MUST BE LAST */ }; +/* +** The size (in bytes) of a VdbeCursor object that has an nField value of N +** or less. The value of SZ_VDBECURSOR(n) is guaranteed to be a multiple +** of 8. +*/ +#define SZ_VDBECURSOR(N) \ + (ROUND8(offsetof(VdbeCursor,aType)) + ((N)+1)*sizeof(u64)) + /* Return true if P is a null-only cursor */ #define IsNullCursor(P) \ @@ -23830,13 +23979,16 @@ struct sqlite3_context { u8 enc; /* Encoding to use for results */ u8 skipFlag; /* Skip accumulator loading if true */ u16 argc; /* Number of arguments */ - sqlite3_value *argv[1]; /* Argument set */ + sqlite3_value *argv[FLEXARRAY]; /* Argument set */ }; -/* A bitfield type for use inside of structures. Always follow with :N where -** N is the number of bits. +/* +** The size (in bytes) of an sqlite3_context object that holds N +** argv[] arguments. */ -typedef unsigned bft; /* Bit Field Type */ +#define SZ_CONTEXT(N) \ + (offsetof(sqlite3_context,argv)+(N)*sizeof(sqlite3_value*)) + /* The ScanStatus object holds a single value for the ** sqlite3_stmt_scanstatus() interface. @@ -23897,7 +24049,7 @@ struct Vdbe { i64 nStmtDefCons; /* Number of def. constraints when stmt started */ i64 nStmtDefImmCons; /* Number of def. imm constraints when stmt started */ Mem *aMem; /* The memory locations */ - Mem **apArg; /* Arguments to currently executing user function */ + Mem **apArg; /* Arguments xUpdate and xFilter vtab methods */ VdbeCursor **apCsr; /* One element of this array for each open cursor */ Mem *aVar; /* Values for the OP_Variable opcode. */ @@ -23917,6 +24069,7 @@ struct Vdbe { #ifdef SQLITE_DEBUG int rcApp; /* errcode set by sqlite3_result_error_code() */ u32 nWrite; /* Number of write operations that have occurred */ + int napArg; /* Size of the apArg[] array */ #endif u16 nResColumn; /* Number of columns in one row of the result set */ u16 nResAlloc; /* Column slots allocated to aColName[] */ @@ -23969,7 +24122,7 @@ struct PreUpdate { VdbeCursor *pCsr; /* Cursor to read old values from */ int op; /* One of SQLITE_INSERT, UPDATE, DELETE */ u8 *aRecord; /* old.* database record */ - KeyInfo keyinfo; + KeyInfo *pKeyinfo; /* Key information */ UnpackedRecord *pUnpacked; /* Unpacked version of aRecord[] */ UnpackedRecord *pNewUnpacked; /* Unpacked version of new.* record */ int iNewReg; /* Register for new.* values */ @@ -23981,6 +24134,7 @@ struct PreUpdate { Table *pTab; /* Schema object being updated */ Index *pPk; /* PK index if pTab is WITHOUT ROWID */ sqlite3_value **apDflt; /* Array of default values, if required */ + u8 keyinfoSpace[SZ_KEYINFO(0)]; /* Space to hold pKeyinfo[0] content */ }; /* @@ -24347,8 +24501,9 @@ SQLITE_PRIVATE int sqlite3LookasideUsed(sqlite3 *db, int *pHighwater){ nInit += countLookasideSlots(db->lookaside.pSmallInit); nFree += countLookasideSlots(db->lookaside.pSmallFree); #endif /* SQLITE_OMIT_TWOSIZE_LOOKASIDE */ - if( pHighwater ) *pHighwater = db->lookaside.nSlot - nInit; - return db->lookaside.nSlot - (nInit+nFree); + assert( db->lookaside.nSlot >= nInit+nFree ); + if( pHighwater ) *pHighwater = (int)(db->lookaside.nSlot - nInit); + return (int)(db->lookaside.nSlot - (nInit+nFree)); } /* @@ -24401,7 +24556,7 @@ SQLITE_API int sqlite3_db_status( assert( (op-SQLITE_DBSTATUS_LOOKASIDE_HIT)>=0 ); assert( (op-SQLITE_DBSTATUS_LOOKASIDE_HIT)<3 ); *pCurrent = 0; - *pHighwater = db->lookaside.anStat[op - SQLITE_DBSTATUS_LOOKASIDE_HIT]; + *pHighwater = (int)db->lookaside.anStat[op-SQLITE_DBSTATUS_LOOKASIDE_HIT]; if( resetFlag ){ db->lookaside.anStat[op - SQLITE_DBSTATUS_LOOKASIDE_HIT] = 0; } @@ -25913,7 +26068,7 @@ static int daysAfterMonday(DateTime *pDate){ ** In other words, return the day of the week according ** to this code: ** -** 0=Sunday, 1=Monday, 2=Tues, ..., 6=Saturday +** 0=Sunday, 1=Monday, 2=Tuesday, ..., 6=Saturday */ static int daysAfterSunday(DateTime *pDate){ assert( pDate->validJD ); @@ -30122,6 +30277,8 @@ SQLITE_PRIVATE sqlite3_mutex_methods const *sqlite3DefaultMutex(void){ #ifdef __CYGWIN__ # include <sys/cygwin.h> +# include <sys/stat.h> /* amalgamator: dontcache */ +# include <unistd.h> /* amalgamator: dontcache */ # include <errno.h> /* amalgamator: dontcache */ #endif @@ -31516,17 +31673,17 @@ SQLITE_PRIVATE int sqlite3ApiExit(sqlite3* db, int rc){ #define etPERCENT 7 /* Percent symbol. %% */ #define etCHARX 8 /* Characters. %c */ /* The rest are extensions, not normally found in printf() */ -#define etSQLESCAPE 9 /* Strings with '\'' doubled. %q */ -#define etSQLESCAPE2 10 /* Strings with '\'' doubled and enclosed in '', - NULL pointers replaced by SQL NULL. %Q */ -#define etTOKEN 11 /* a pointer to a Token structure */ -#define etSRCITEM 12 /* a pointer to a SrcItem */ -#define etPOINTER 13 /* The %p conversion */ -#define etSQLESCAPE3 14 /* %w -> Strings with '\"' doubled */ -#define etORDINAL 15 /* %r -> 1st, 2nd, 3rd, 4th, etc. English only */ -#define etDECIMAL 16 /* %d or %u, but not %x, %o */ +#define etESCAPE_q 9 /* Strings with '\'' doubled. %q */ +#define etESCAPE_Q 10 /* Strings with '\'' doubled and enclosed in '', + NULL pointers replaced by SQL NULL. %Q */ +#define etTOKEN 11 /* a pointer to a Token structure */ +#define etSRCITEM 12 /* a pointer to a SrcItem */ +#define etPOINTER 13 /* The %p conversion */ +#define etESCAPE_w 14 /* %w -> Strings with '\"' doubled */ +#define etORDINAL 15 /* %r -> 1st, 2nd, 3rd, 4th, etc. English only */ +#define etDECIMAL 16 /* %d or %u, but not %x, %o */ -#define etINVALID 17 /* Any unrecognized conversion type */ +#define etINVALID 17 /* Any unrecognized conversion type */ /* @@ -31565,9 +31722,9 @@ static const et_info fmtinfo[] = { { 's', 0, 4, etSTRING, 0, 0 }, { 'g', 0, 1, etGENERIC, 30, 0 }, { 'z', 0, 4, etDYNSTRING, 0, 0 }, - { 'q', 0, 4, etSQLESCAPE, 0, 0 }, - { 'Q', 0, 4, etSQLESCAPE2, 0, 0 }, - { 'w', 0, 4, etSQLESCAPE3, 0, 0 }, + { 'q', 0, 4, etESCAPE_q, 0, 0 }, + { 'Q', 0, 4, etESCAPE_Q, 0, 0 }, + { 'w', 0, 4, etESCAPE_w, 0, 0 }, { 'c', 0, 0, etCHARX, 0, 0 }, { 'o', 8, 0, etRADIX, 0, 2 }, { 'u', 10, 0, etDECIMAL, 0, 0 }, @@ -32164,25 +32321,7 @@ SQLITE_API void sqlite3_str_vappendf( } }else{ unsigned int ch = va_arg(ap,unsigned int); - if( ch<0x00080 ){ - buf[0] = ch & 0xff; - length = 1; - }else if( ch<0x00800 ){ - buf[0] = 0xc0 + (u8)((ch>>6)&0x1f); - buf[1] = 0x80 + (u8)(ch & 0x3f); - length = 2; - }else if( ch<0x10000 ){ - buf[0] = 0xe0 + (u8)((ch>>12)&0x0f); - buf[1] = 0x80 + (u8)((ch>>6) & 0x3f); - buf[2] = 0x80 + (u8)(ch & 0x3f); - length = 3; - }else{ - buf[0] = 0xf0 + (u8)((ch>>18) & 0x07); - buf[1] = 0x80 + (u8)((ch>>12) & 0x3f); - buf[2] = 0x80 + (u8)((ch>>6) & 0x3f); - buf[3] = 0x80 + (u8)(ch & 0x3f); - length = 4; - } + length = sqlite3AppendOneUtf8Character(buf, ch); } if( precision>1 ){ i64 nPrior = 1; @@ -32262,22 +32401,31 @@ SQLITE_API void sqlite3_str_vappendf( while( ii>=0 ) if( (bufpt[ii--] & 0xc0)==0x80 ) width++; } break; - case etSQLESCAPE: /* %q: Escape ' characters */ - case etSQLESCAPE2: /* %Q: Escape ' and enclose in '...' */ - case etSQLESCAPE3: { /* %w: Escape " characters */ + case etESCAPE_q: /* %q: Escape ' characters */ + case etESCAPE_Q: /* %Q: Escape ' and enclose in '...' */ + case etESCAPE_w: { /* %w: Escape " characters */ i64 i, j, k, n; - int needQuote, isnull; + int needQuote = 0; char ch; - char q = ((xtype==etSQLESCAPE3)?'"':'\''); /* Quote character */ char *escarg; + char q; if( bArgList ){ escarg = getTextArg(pArgList); }else{ escarg = va_arg(ap,char*); } - isnull = escarg==0; - if( isnull ) escarg = (xtype==etSQLESCAPE2 ? "NULL" : "(NULL)"); + if( escarg==0 ){ + escarg = (xtype==etESCAPE_Q ? "NULL" : "(NULL)"); + }else if( xtype==etESCAPE_Q ){ + needQuote = 1; + } + if( xtype==etESCAPE_w ){ + q = '"'; + flag_alternateform = 0; + }else{ + q = '\''; + } /* For %q, %Q, and %w, the precision is the number of bytes (or ** characters if the ! flags is present) to use from the input. ** Because of the extra quoting characters inserted, the number @@ -32290,7 +32438,30 @@ SQLITE_API void sqlite3_str_vappendf( while( (escarg[i+1]&0xc0)==0x80 ){ i++; } } } - needQuote = !isnull && xtype==etSQLESCAPE2; + if( flag_alternateform ){ + /* For %#q, do unistr()-style backslash escapes for + ** all control characters, and for backslash itself. + ** For %#Q, do the same but only if there is at least + ** one control character. */ + u32 nBack = 0; + u32 nCtrl = 0; + for(k=0; k<i; k++){ + if( escarg[k]=='\\' ){ + nBack++; + }else if( ((u8*)escarg)[k]<=0x1f ){ + nCtrl++; + } + } + if( nCtrl || xtype==etESCAPE_q ){ + n += nBack + 5*nCtrl; + if( xtype==etESCAPE_Q ){ + n += 10; + needQuote = 2; + } + }else{ + flag_alternateform = 0; + } + } n += i + 3; if( n>etBUFSIZE ){ bufpt = zExtra = printfTempBuf(pAccum, n); @@ -32299,13 +32470,41 @@ SQLITE_API void sqlite3_str_vappendf( bufpt = buf; } j = 0; - if( needQuote ) bufpt[j++] = q; - k = i; - for(i=0; i<k; i++){ - bufpt[j++] = ch = escarg[i]; - if( ch==q ) bufpt[j++] = ch; + if( needQuote ){ + if( needQuote==2 ){ + memcpy(&bufpt[j], "unistr('", 8); + j += 8; + }else{ + bufpt[j++] = '\''; + } + } + k = i; + if( flag_alternateform ){ + for(i=0; i<k; i++){ + bufpt[j++] = ch = escarg[i]; + if( ch==q ){ + bufpt[j++] = ch; + }else if( ch=='\\' ){ + bufpt[j++] = '\\'; + }else if( ((unsigned char)ch)<=0x1f ){ + bufpt[j-1] = '\\'; + bufpt[j++] = 'u'; + bufpt[j++] = '0'; + bufpt[j++] = '0'; + bufpt[j++] = ch>=0x10 ? '1' : '0'; + bufpt[j++] = "0123456789abcdef"[ch&0xf]; + } + } + }else{ + for(i=0; i<k; i++){ + bufpt[j++] = ch = escarg[i]; + if( ch==q ) bufpt[j++] = ch; + } + } + if( needQuote ){ + bufpt[j++] = '\''; + if( needQuote==2 ) bufpt[j++] = ')'; } - if( needQuote ) bufpt[j++] = q; bufpt[j] = 0; length = j; goto adjust_width_for_utf8; @@ -32548,7 +32747,7 @@ SQLITE_API void sqlite3_str_appendall(sqlite3_str *p, const char *z){ static SQLITE_NOINLINE char *strAccumFinishRealloc(StrAccum *p){ char *zText; assert( p->mxAlloc>0 && !isMalloced(p) ); - zText = sqlite3DbMallocRaw(p->db, p->nChar+1 ); + zText = sqlite3DbMallocRaw(p->db, 1+(u64)p->nChar ); if( zText ){ memcpy(zText, p->zText, p->nChar+1); p->printfFlags |= SQLITE_PRINTF_MALLOCED; @@ -32793,6 +32992,15 @@ SQLITE_API char *sqlite3_snprintf(int n, char *zBuf, const char *zFormat, ...){ return zBuf; } +/* Maximum size of an sqlite3_log() message. */ +#if defined(SQLITE_MAX_LOG_MESSAGE) + /* Leave the definition as supplied */ +#elif SQLITE_PRINT_BUF_SIZE*10>10000 +# define SQLITE_MAX_LOG_MESSAGE 10000 +#else +# define SQLITE_MAX_LOG_MESSAGE (SQLITE_PRINT_BUF_SIZE*10) +#endif + /* ** This is the routine that actually formats the sqlite3_log() message. ** We house it in a separate routine from sqlite3_log() to avoid using @@ -32809,7 +33017,7 @@ SQLITE_API char *sqlite3_snprintf(int n, char *zBuf, const char *zFormat, ...){ */ static void renderLogMsg(int iErrCode, const char *zFormat, va_list ap){ StrAccum acc; /* String accumulator */ - char zMsg[SQLITE_PRINT_BUF_SIZE*3]; /* Complete log message */ + char zMsg[SQLITE_MAX_LOG_MESSAGE]; /* Complete log message */ sqlite3StrAccumInit(&acc, 0, zMsg, sizeof(zMsg), 0); sqlite3_str_vappendf(&acc, zFormat, ap); @@ -34804,6 +35012,35 @@ static const unsigned char sqlite3Utf8Trans1[] = { } \ } +/* +** Write a single UTF8 character whose value is v into the +** buffer starting at zOut. zOut must be sized to hold at +** least four bytes. Return the number of bytes needed +** to encode the new character. +*/ +SQLITE_PRIVATE int sqlite3AppendOneUtf8Character(char *zOut, u32 v){ + if( v<0x00080 ){ + zOut[0] = (u8)(v & 0xff); + return 1; + } + if( v<0x00800 ){ + zOut[0] = 0xc0 + (u8)((v>>6) & 0x1f); + zOut[1] = 0x80 + (u8)(v & 0x3f); + return 2; + } + if( v<0x10000 ){ + zOut[0] = 0xe0 + (u8)((v>>12) & 0x0f); + zOut[1] = 0x80 + (u8)((v>>6) & 0x3f); + zOut[2] = 0x80 + (u8)(v & 0x3f); + return 3; + } + zOut[0] = 0xf0 + (u8)((v>>18) & 0x07); + zOut[1] = 0x80 + (u8)((v>>12) & 0x3f); + zOut[2] = 0x80 + (u8)((v>>6) & 0x3f); + zOut[3] = 0x80 + (u8)(v & 0x3f); + return 4; +} + /* ** Translate a single UTF-8 character. Return the unicode value. ** @@ -35225,7 +35462,7 @@ SQLITE_PRIVATE int sqlite3Utf16ByteLen(const void *zIn, int nByte, int nChar){ int n = 0; if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++; - while( n<nChar && ALWAYS(z<=zEnd) ){ + while( n<nChar && z<=zEnd ){ c = z[0]; z += 2; if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2; @@ -36400,7 +36637,11 @@ SQLITE_PRIVATE void sqlite3FpDecode(FpDecode *p, double r, int iRound, int mxRou } p->z = &p->zBuf[i+1]; assert( i+p->n < sizeof(p->zBuf) ); - while( ALWAYS(p->n>0) && p->z[p->n-1]=='0' ){ p->n--; } + assert( p->n>0 ); + while( p->z[p->n-1]=='0' ){ + p->n--; + assert( p->n>0 ); + } } /* @@ -36905,7 +37146,7 @@ SQLITE_PRIVATE int sqlite3MulInt64(i64 *pA, i64 iB){ } /* -** Compute the absolute value of a 32-bit signed integer, of possible. Or +** Compute the absolute value of a 32-bit signed integer, if possible. Or ** if the integer has a value of -2147483648, return +2147483647 */ SQLITE_PRIVATE int sqlite3AbsInt32(int x){ @@ -37186,12 +37427,19 @@ SQLITE_PRIVATE void sqlite3HashClear(Hash *pH){ */ static unsigned int strHash(const char *z){ unsigned int h = 0; - unsigned char c; - while( (c = (unsigned char)*z++)!=0 ){ /*OPTIMIZATION-IF-TRUE*/ + while( z[0] ){ /*OPTIMIZATION-IF-TRUE*/ /* Knuth multiplicative hashing. (Sorting & Searching, p. 510). ** 0x9e3779b1 is 2654435761 which is the closest prime number to - ** (2**32)*golden_ratio, where golden_ratio = (sqrt(5) - 1)/2. */ - h += sqlite3UpperToLower[c]; + ** (2**32)*golden_ratio, where golden_ratio = (sqrt(5) - 1)/2. + ** + ** Only bits 0xdf for ASCII and bits 0xbf for EBCDIC each octet are + ** hashed since the omitted bits determine the upper/lower case difference. + */ +#ifdef SQLITE_EBCDIC + h += 0xbf & (unsigned char)*(z++); +#else + h += 0xdf & (unsigned char)*(z++); +#endif h *= 0x9e3779b1; } return h; @@ -37264,9 +37512,8 @@ static int rehash(Hash *pH, unsigned int new_size){ pH->htsize = new_size = sqlite3MallocSize(new_ht)/sizeof(struct _ht); memset(new_ht, 0, new_size*sizeof(struct _ht)); for(elem=pH->first, pH->first=0; elem; elem = next_elem){ - unsigned int h = strHash(elem->pKey) % new_size; next_elem = elem->next; - insertElement(pH, &new_ht[h], elem); + insertElement(pH, &new_ht[elem->h % new_size], elem); } return 1; } @@ -37284,23 +37531,22 @@ static HashElem *findElementWithHash( HashElem *elem; /* Used to loop thru the element list */ unsigned int count; /* Number of elements left to test */ unsigned int h; /* The computed hash */ - static HashElem nullElement = { 0, 0, 0, 0 }; + static HashElem nullElement = { 0, 0, 0, 0, 0 }; + h = strHash(pKey); if( pH->ht ){ /*OPTIMIZATION-IF-TRUE*/ struct _ht *pEntry; - h = strHash(pKey) % pH->htsize; - pEntry = &pH->ht[h]; + pEntry = &pH->ht[h % pH->htsize]; elem = pEntry->chain; count = pEntry->count; }else{ - h = 0; elem = pH->first; count = pH->count; } if( pHash ) *pHash = h; while( count ){ assert( elem!=0 ); - if( sqlite3StrICmp(elem->pKey,pKey)==0 ){ + if( h==elem->h && sqlite3StrICmp(elem->pKey,pKey)==0 ){ return elem; } elem = elem->next; @@ -37312,10 +37558,9 @@ static HashElem *findElementWithHash( /* Remove a single entry from the hash table given a pointer to that ** element and a hash on the element's key. */ -static void removeElementGivenHash( +static void removeElement( Hash *pH, /* The pH containing "elem" */ - HashElem* elem, /* The element to be removed from the pH */ - unsigned int h /* Hash value for the element */ + HashElem *elem /* The element to be removed from the pH */ ){ struct _ht *pEntry; if( elem->prev ){ @@ -37327,7 +37572,7 @@ static void removeElementGivenHash( elem->next->prev = elem->prev; } if( pH->ht ){ - pEntry = &pH->ht[h]; + pEntry = &pH->ht[elem->h % pH->htsize]; if( pEntry->chain==elem ){ pEntry->chain = elem->next; } @@ -37378,7 +37623,7 @@ SQLITE_PRIVATE void *sqlite3HashInsert(Hash *pH, const char *pKey, void *data){ if( elem->data ){ void *old_data = elem->data; if( data==0 ){ - removeElementGivenHash(pH,elem,h); + removeElement(pH,elem); }else{ elem->data = data; elem->pKey = pKey; @@ -37389,15 +37634,13 @@ SQLITE_PRIVATE void *sqlite3HashInsert(Hash *pH, const char *pKey, void *data){ new_elem = (HashElem*)sqlite3Malloc( sizeof(HashElem) ); if( new_elem==0 ) return data; new_elem->pKey = pKey; + new_elem->h = h; new_elem->data = data; pH->count++; - if( pH->count>=10 && pH->count > 2*pH->htsize ){ - if( rehash(pH, pH->count*2) ){ - assert( pH->htsize>0 ); - h = strHash(pKey) % pH->htsize; - } + if( pH->count>=5 && pH->count > 2*pH->htsize ){ + rehash(pH, pH->count*3); } - insertElement(pH, pH->ht ? &pH->ht[h] : 0, new_elem); + insertElement(pH, pH->ht ? &pH->ht[new_elem->h % pH->htsize] : 0, new_elem); return 0; } @@ -38880,6 +39123,7 @@ struct unixFile { #endif #ifdef SQLITE_ENABLE_SETLK_TIMEOUT unsigned iBusyTimeout; /* Wait this many millisec on locks */ + int bBlockOnConnect; /* True to block for SHARED locks */ #endif #if OS_VXWORKS struct vxworksFileId *pId; /* Unique file ID */ @@ -40273,6 +40517,13 @@ static int unixFileLock(unixFile *pFile, struct flock *pLock){ rc = 0; } }else{ +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + if( pFile->bBlockOnConnect && pLock->l_type==F_RDLCK + && pLock->l_start==SHARED_FIRST && pLock->l_len==SHARED_SIZE + ){ + rc = osFcntl(pFile->h, F_SETLKW, pLock); + }else +#endif rc = osSetPosixAdvisoryLock(pFile->h, pLock, pFile); } return rc; @@ -42634,8 +42885,9 @@ static int unixFileControl(sqlite3_file *id, int op, void *pArg){ #ifdef SQLITE_ENABLE_SETLK_TIMEOUT case SQLITE_FCNTL_LOCK_TIMEOUT: { int iOld = pFile->iBusyTimeout; + int iNew = *(int*)pArg; #if SQLITE_ENABLE_SETLK_TIMEOUT==1 - pFile->iBusyTimeout = *(int*)pArg; + pFile->iBusyTimeout = iNew<0 ? 0x7FFFFFFF : (unsigned)iNew; #elif SQLITE_ENABLE_SETLK_TIMEOUT==2 pFile->iBusyTimeout = !!(*(int*)pArg); #else @@ -42644,7 +42896,12 @@ static int unixFileControl(sqlite3_file *id, int op, void *pArg){ *(int*)pArg = iOld; return SQLITE_OK; } -#endif + case SQLITE_FCNTL_BLOCK_ON_CONNECT: { + int iNew = *(int*)pArg; + pFile->bBlockOnConnect = iNew; + return SQLITE_OK; + } +#endif /* SQLITE_ENABLE_SETLK_TIMEOUT */ #if SQLITE_MAX_MMAP_SIZE>0 case SQLITE_FCNTL_MMAP_SIZE: { i64 newLimit = *(i64*)pArg; @@ -43627,7 +43884,7 @@ static int unixShmLock( ** ** It is not permitted to block on the RECOVER lock. */ -#ifdef SQLITE_ENABLE_SETLK_TIMEOUT +#if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG) { u16 lockMask = (p->exclMask|p->sharedMask); assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || ( @@ -45436,7 +45693,7 @@ static int unixSleep(sqlite3_vfs *NotUsed, int microseconds){ /* Almost all modern unix systems support nanosleep(). But if you are ** compiling for one of the rare exceptions, you can use - ** -DHAVE_NANOSLEEP=0 (perhaps in conjuction with -DHAVE_USLEEP if + ** -DHAVE_NANOSLEEP=0 (perhaps in conjunction with -DHAVE_USLEEP if ** usleep() is available) in order to bypass the use of nanosleep() */ nanosleep(&sp, NULL); @@ -47157,8 +47414,18 @@ struct winFile { sqlite3_int64 mmapSize; /* Size of mapped region */ sqlite3_int64 mmapSizeMax; /* Configured FCNTL_MMAP_SIZE value */ #endif +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + DWORD iBusyTimeout; /* Wait this many millisec on locks */ + int bBlockOnConnect; +#endif }; +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT +# define winFileBusyTimeout(pDbFd) pDbFd->iBusyTimeout +#else +# define winFileBusyTimeout(pDbFd) 0 +#endif + /* ** The winVfsAppData structure is used for the pAppData member for all of the ** Win32 VFS variants. @@ -47477,7 +47744,7 @@ static struct win_syscall { { "FileTimeToLocalFileTime", (SYSCALL)0, 0 }, #endif -#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(CONST FILETIME*, \ +#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(const FILETIME*, \ LPFILETIME))aSyscall[11].pCurrent) #if SQLITE_OS_WINCE @@ -47486,7 +47753,7 @@ static struct win_syscall { { "FileTimeToSystemTime", (SYSCALL)0, 0 }, #endif -#define osFileTimeToSystemTime ((BOOL(WINAPI*)(CONST FILETIME*, \ +#define osFileTimeToSystemTime ((BOOL(WINAPI*)(const FILETIME*, \ LPSYSTEMTIME))aSyscall[12].pCurrent) { "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 }, @@ -47592,6 +47859,12 @@ static struct win_syscall { #define osGetFullPathNameW ((DWORD(WINAPI*)(LPCWSTR,DWORD,LPWSTR, \ LPWSTR*))aSyscall[25].pCurrent) +/* +** For GetLastError(), MSDN says: +** +** Minimum supported client: Windows XP [desktop apps | UWP apps] +** Minimum supported server: Windows Server 2003 [desktop apps | UWP apps] +*/ { "GetLastError", (SYSCALL)GetLastError, 0 }, #define osGetLastError ((DWORD(WINAPI*)(VOID))aSyscall[26].pCurrent) @@ -47760,7 +48033,7 @@ static struct win_syscall { { "LockFile", (SYSCALL)0, 0 }, #endif -#ifndef osLockFile +#if !defined(osLockFile) && defined(SQLITE_WIN32_HAS_ANSI) #define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ DWORD))aSyscall[47].pCurrent) #endif @@ -47824,7 +48097,7 @@ static struct win_syscall { { "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 }, -#define osSystemTimeToFileTime ((BOOL(WINAPI*)(CONST SYSTEMTIME*, \ +#define osSystemTimeToFileTime ((BOOL(WINAPI*)(const SYSTEMTIME*, \ LPFILETIME))aSyscall[56].pCurrent) #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT @@ -47833,7 +48106,7 @@ static struct win_syscall { { "UnlockFile", (SYSCALL)0, 0 }, #endif -#ifndef osUnlockFile +#if !defined(osUnlockFile) && defined(SQLITE_WIN32_HAS_ANSI) #define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ DWORD))aSyscall[57].pCurrent) #endif @@ -47874,11 +48147,13 @@ static struct win_syscall { #define osCreateEventExW ((HANDLE(WINAPI*)(LPSECURITY_ATTRIBUTES,LPCWSTR, \ DWORD,DWORD))aSyscall[62].pCurrent) -#if !SQLITE_OS_WINRT +/* +** For WaitForSingleObject(), MSDN says: +** +** Minimum supported client: Windows XP [desktop apps | UWP apps] +** Minimum supported server: Windows Server 2003 [desktop apps | UWP apps] +*/ { "WaitForSingleObject", (SYSCALL)WaitForSingleObject, 0 }, -#else - { "WaitForSingleObject", (SYSCALL)0, 0 }, -#endif #define osWaitForSingleObject ((DWORD(WINAPI*)(HANDLE, \ DWORD))aSyscall[63].pCurrent) @@ -48025,6 +48300,97 @@ static struct win_syscall { #define osFlushViewOfFile \ ((BOOL(WINAPI*)(LPCVOID,SIZE_T))aSyscall[79].pCurrent) +/* +** If SQLITE_ENABLE_SETLK_TIMEOUT is defined, we require CreateEvent() +** to implement blocking locks with timeouts. MSDN says: +** +** Minimum supported client: Windows XP [desktop apps | UWP apps] +** Minimum supported server: Windows Server 2003 [desktop apps | UWP apps] +*/ +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + { "CreateEvent", (SYSCALL)CreateEvent, 0 }, +#else + { "CreateEvent", (SYSCALL)0, 0 }, +#endif + +#define osCreateEvent ( \ + (HANDLE(WINAPI*) (LPSECURITY_ATTRIBUTES,BOOL,BOOL,LPCSTR)) \ + aSyscall[80].pCurrent \ +) + +/* +** If SQLITE_ENABLE_SETLK_TIMEOUT is defined, we require CancelIo() +** for the case where a timeout expires and a lock request must be +** cancelled. +** +** Minimum supported client: Windows XP [desktop apps | UWP apps] +** Minimum supported server: Windows Server 2003 [desktop apps | UWP apps] +*/ +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + { "CancelIo", (SYSCALL)CancelIo, 0 }, +#else + { "CancelIo", (SYSCALL)0, 0 }, +#endif + +#define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent) + +#if defined(SQLITE_WIN32_HAS_WIDE) && defined(_WIN32) + { "GetModuleHandleW", (SYSCALL)GetModuleHandleW, 0 }, +#else + { "GetModuleHandleW", (SYSCALL)0, 0 }, +#endif + +#define osGetModuleHandleW ((HMODULE(WINAPI*)(LPCWSTR))aSyscall[82].pCurrent) + +#ifndef _WIN32 + { "getenv", (SYSCALL)getenv, 0 }, +#else + { "getenv", (SYSCALL)0, 0 }, +#endif + +#define osGetenv ((const char *(*)(const char *))aSyscall[83].pCurrent) + +#ifndef _WIN32 + { "getcwd", (SYSCALL)getcwd, 0 }, +#else + { "getcwd", (SYSCALL)0, 0 }, +#endif + +#define osGetcwd ((char*(*)(char*,size_t))aSyscall[84].pCurrent) + +#ifndef _WIN32 + { "readlink", (SYSCALL)readlink, 0 }, +#else + { "readlink", (SYSCALL)0, 0 }, +#endif + +#define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[85].pCurrent) + +#ifndef _WIN32 + { "lstat", (SYSCALL)lstat, 0 }, +#else + { "lstat", (SYSCALL)0, 0 }, +#endif + +#define osLstat ((int(*)(const char*,struct stat*))aSyscall[86].pCurrent) + +#ifndef _WIN32 + { "__errno", (SYSCALL)__errno, 0 }, +#else + { "__errno", (SYSCALL)0, 0 }, +#endif + +#define osErrno (*((int*(*)(void))aSyscall[87].pCurrent)()) + +#ifndef _WIN32 + { "cygwin_conv_path", (SYSCALL)cygwin_conv_path, 0 }, +#else + { "cygwin_conv_path", (SYSCALL)0, 0 }, +#endif + +#define osCygwin_conv_path ((size_t(*)(unsigned int, \ + const void *, void *, size_t))aSyscall[88].pCurrent) + }; /* End of the overrideable system calls */ /* @@ -48198,6 +48564,7 @@ SQLITE_API int sqlite3_win32_reset_heap(){ } #endif /* SQLITE_WIN32_MALLOC */ +#ifdef _WIN32 /* ** This function outputs the specified (ANSI) string to the Win32 debugger ** (if available). @@ -48240,6 +48607,7 @@ SQLITE_API void sqlite3_win32_write_debug(const char *zBuf, int nBuf){ } #endif } +#endif /* _WIN32 */ /* ** The following routine suspends the current thread for at least ms @@ -48323,7 +48691,9 @@ SQLITE_API int sqlite3_win32_is_nt(void){ } return osInterlockedCompareExchange(&sqlite3_os_type, 2, 2)==2; #elif SQLITE_TEST - return osInterlockedCompareExchange(&sqlite3_os_type, 2, 2)==2; + return osInterlockedCompareExchange(&sqlite3_os_type, 2, 2)==2 + || osInterlockedCompareExchange(&sqlite3_os_type, 0, 0)==0 + ; #else /* ** NOTE: All sub-platforms where the GetVersionEx[AW] functions are @@ -48538,6 +48908,7 @@ SQLITE_PRIVATE void sqlite3MemSetDefault(void){ } #endif /* SQLITE_WIN32_MALLOC */ +#ifdef _WIN32 /* ** Convert a UTF-8 string to Microsoft Unicode. ** @@ -48563,6 +48934,7 @@ static LPWSTR winUtf8ToUnicode(const char *zText){ } return zWideText; } +#endif /* _WIN32 */ /* ** Convert a Microsoft Unicode string to UTF-8. @@ -48597,28 +48969,29 @@ static char *winUnicodeToUtf8(LPCWSTR zWideText){ ** Space to hold the returned string is obtained from sqlite3_malloc(). */ static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){ - int nByte; + int nWideChar; LPWSTR zMbcsText; int codepage = useAnsi ? CP_ACP : CP_OEMCP; - nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, - 0)*sizeof(WCHAR); - if( nByte==0 ){ + nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, + 0); + if( nWideChar==0 ){ return 0; } - zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) ); + zMbcsText = sqlite3MallocZero( nWideChar*sizeof(WCHAR) ); if( zMbcsText==0 ){ return 0; } - nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, - nByte); - if( nByte==0 ){ + nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, + nWideChar); + if( nWideChar==0 ){ sqlite3_free(zMbcsText); zMbcsText = 0; } return zMbcsText; } +#ifdef _WIN32 /* ** Convert a Microsoft Unicode string to a multi-byte character string, ** using the ANSI or OEM code page. @@ -48646,6 +49019,7 @@ static char *winUnicodeToMbcs(LPCWSTR zWideText, int useAnsi){ } return zText; } +#endif /* _WIN32 */ /* ** Convert a multi-byte character string to UTF-8. @@ -48665,6 +49039,7 @@ static char *winMbcsToUtf8(const char *zText, int useAnsi){ return zTextUtf8; } +#ifdef _WIN32 /* ** Convert a UTF-8 string to a multi-byte character string. ** @@ -48714,6 +49089,7 @@ SQLITE_API char *sqlite3_win32_unicode_to_utf8(LPCWSTR zWideText){ #endif return winUnicodeToUtf8(zWideText); } +#endif /* _WIN32 */ /* ** This is a public wrapper for the winMbcsToUtf8() function. @@ -48731,6 +49107,7 @@ SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){ return winMbcsToUtf8(zText, osAreFileApisANSI()); } +#ifdef _WIN32 /* ** This is a public wrapper for the winMbcsToUtf8() function. */ @@ -48855,6 +49232,7 @@ SQLITE_API int sqlite3_win32_set_directory( ){ return sqlite3_win32_set_directory16(type, zValue); } +#endif /* _WIN32 */ /* ** The return value of winGetLastErrorMsg @@ -49403,13 +49781,94 @@ static BOOL winLockFile( ovlp.Offset = offsetLow; ovlp.OffsetHigh = offsetHigh; return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp); +#ifdef SQLITE_WIN32_HAS_ANSI }else{ return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow, numBytesHigh); +#endif } #endif } +/* +** Lock a region of nByte bytes starting at offset offset of file hFile. +** Take an EXCLUSIVE lock if parameter bExclusive is true, or a SHARED lock +** otherwise. If nMs is greater than zero and the lock cannot be obtained +** immediately, block for that many ms before giving up. +** +** This function returns SQLITE_OK if the lock is obtained successfully. If +** some other process holds the lock, SQLITE_BUSY is returned if nMs==0, or +** SQLITE_BUSY_TIMEOUT otherwise. Or, if an error occurs, SQLITE_IOERR. +*/ +static int winHandleLockTimeout( + HANDLE hFile, + DWORD offset, + DWORD nByte, + int bExcl, + DWORD nMs +){ + DWORD flags = LOCKFILE_FAIL_IMMEDIATELY | (bExcl?LOCKFILE_EXCLUSIVE_LOCK:0); + int rc = SQLITE_OK; + BOOL ret; + + if( !osIsNT() ){ + ret = winLockFile(&hFile, flags, offset, 0, nByte, 0); + }else{ + OVERLAPPED ovlp; + memset(&ovlp, 0, sizeof(OVERLAPPED)); + ovlp.Offset = offset; + +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + if( nMs!=0 ){ + flags &= ~LOCKFILE_FAIL_IMMEDIATELY; + } + ovlp.hEvent = osCreateEvent(NULL, TRUE, FALSE, NULL); + if( ovlp.hEvent==NULL ){ + return SQLITE_IOERR_LOCK; + } +#endif + + ret = osLockFileEx(hFile, flags, 0, nByte, 0, &ovlp); + +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + /* If SQLITE_ENABLE_SETLK_TIMEOUT is defined, then the file-handle was + ** opened with FILE_FLAG_OVERHEAD specified. In this case, the call to + ** LockFileEx() may fail because the request is still pending. This can + ** happen even if LOCKFILE_FAIL_IMMEDIATELY was specified. + ** + ** If nMs is 0, then LOCKFILE_FAIL_IMMEDIATELY was set in the flags + ** passed to LockFileEx(). In this case, if the operation is pending, + ** block indefinitely until it is finished. + ** + ** Otherwise, wait for up to nMs ms for the operation to finish. nMs + ** may be set to INFINITE. + */ + if( !ret && GetLastError()==ERROR_IO_PENDING ){ + DWORD nDelay = (nMs==0 ? INFINITE : nMs); + DWORD res = osWaitForSingleObject(ovlp.hEvent, nDelay); + if( res==WAIT_OBJECT_0 ){ + ret = TRUE; + }else if( res==WAIT_TIMEOUT ){ + rc = SQLITE_BUSY_TIMEOUT; + }else{ + /* Some other error has occurred */ + rc = SQLITE_IOERR_LOCK; + } + + /* If it is still pending, cancel the LockFileEx() call. */ + osCancelIo(hFile); + } + + osCloseHandle(ovlp.hEvent); +#endif + } + + if( rc==SQLITE_OK && !ret ){ + rc = SQLITE_BUSY; + } + return rc; +} + /* ** Unlock a file region. */ @@ -49434,13 +49893,23 @@ static BOOL winUnlockFile( ovlp.Offset = offsetLow; ovlp.OffsetHigh = offsetHigh; return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp); +#ifdef SQLITE_WIN32_HAS_ANSI }else{ return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow, numBytesHigh); +#endif } #endif } +/* +** Remove an nByte lock starting at offset iOff from HANDLE h. +*/ +static int winHandleUnlock(HANDLE h, int iOff, int nByte){ + BOOL ret = winUnlockFile(&h, iOff, 0, nByte, 0); + return (ret ? SQLITE_OK : SQLITE_IOERR_UNLOCK); +} + /***************************************************************************** ** The next group of routines implement the I/O methods specified ** by the sqlite3_io_methods object. @@ -49454,66 +49923,70 @@ static BOOL winUnlockFile( #endif /* -** Move the current position of the file handle passed as the first -** argument to offset iOffset within the file. If successful, return 0. -** Otherwise, set pFile->lastErrno and return non-zero. +** Seek the file handle h to offset nByte of the file. +** +** If successful, return SQLITE_OK. Or, if an error occurs, return an SQLite +** error code. */ -static int winSeekFile(winFile *pFile, sqlite3_int64 iOffset){ +static int winHandleSeek(HANDLE h, sqlite3_int64 iOffset){ + int rc = SQLITE_OK; /* Return value */ + #if !SQLITE_OS_WINRT LONG upperBits; /* Most sig. 32 bits of new offset */ LONG lowerBits; /* Least sig. 32 bits of new offset */ DWORD dwRet; /* Value returned by SetFilePointer() */ - DWORD lastErrno; /* Value returned by GetLastError() */ - - OSTRACE(("SEEK file=%p, offset=%lld\n", pFile->h, iOffset)); upperBits = (LONG)((iOffset>>32) & 0x7fffffff); lowerBits = (LONG)(iOffset & 0xffffffff); + dwRet = osSetFilePointer(h, lowerBits, &upperBits, FILE_BEGIN); + /* API oddity: If successful, SetFilePointer() returns a dword ** containing the lower 32-bits of the new file-offset. Or, if it fails, ** it returns INVALID_SET_FILE_POINTER. However according to MSDN, ** INVALID_SET_FILE_POINTER may also be a valid new offset. So to determine ** whether an error has actually occurred, it is also necessary to call - ** GetLastError(). - */ - dwRet = osSetFilePointer(pFile->h, lowerBits, &upperBits, FILE_BEGIN); - - if( (dwRet==INVALID_SET_FILE_POINTER - && ((lastErrno = osGetLastError())!=NO_ERROR)) ){ - pFile->lastErrno = lastErrno; - winLogError(SQLITE_IOERR_SEEK, pFile->lastErrno, - "winSeekFile", pFile->zPath); - OSTRACE(("SEEK file=%p, rc=SQLITE_IOERR_SEEK\n", pFile->h)); - return 1; + ** GetLastError(). */ + if( dwRet==INVALID_SET_FILE_POINTER ){ + DWORD lastErrno = osGetLastError(); + if( lastErrno!=NO_ERROR ){ + rc = SQLITE_IOERR_SEEK; + } } - - OSTRACE(("SEEK file=%p, rc=SQLITE_OK\n", pFile->h)); - return 0; #else - /* - ** Same as above, except that this implementation works for WinRT. - */ - + /* This implementation works for WinRT. */ LARGE_INTEGER x; /* The new offset */ BOOL bRet; /* Value returned by SetFilePointerEx() */ x.QuadPart = iOffset; - bRet = osSetFilePointerEx(pFile->h, x, 0, FILE_BEGIN); + bRet = osSetFilePointerEx(h, x, 0, FILE_BEGIN); if(!bRet){ - pFile->lastErrno = osGetLastError(); - winLogError(SQLITE_IOERR_SEEK, pFile->lastErrno, - "winSeekFile", pFile->zPath); - OSTRACE(("SEEK file=%p, rc=SQLITE_IOERR_SEEK\n", pFile->h)); - return 1; + rc = SQLITE_IOERR_SEEK; } - - OSTRACE(("SEEK file=%p, rc=SQLITE_OK\n", pFile->h)); - return 0; #endif + + OSTRACE(("SEEK file=%p, offset=%lld rc=%s\n", h, iOffset, sqlite3ErrName(rc))); + return rc; } +/* +** Move the current position of the file handle passed as the first +** argument to offset iOffset within the file. If successful, return 0. +** Otherwise, set pFile->lastErrno and return non-zero. +*/ +static int winSeekFile(winFile *pFile, sqlite3_int64 iOffset){ + int rc; + + rc = winHandleSeek(pFile->h, iOffset); + if( rc!=SQLITE_OK ){ + pFile->lastErrno = osGetLastError(); + winLogError(rc, pFile->lastErrno, "winSeekFile", pFile->zPath); + } + return rc; +} + + #if SQLITE_MAX_MMAP_SIZE>0 /* Forward references to VFS helper methods used for memory mapped files */ static int winMapfile(winFile*, sqlite3_int64); @@ -49773,6 +50246,60 @@ static int winWrite( return SQLITE_OK; } +/* +** Truncate the file opened by handle h to nByte bytes in size. +*/ +static int winHandleTruncate(HANDLE h, sqlite3_int64 nByte){ + int rc = SQLITE_OK; /* Return code */ + rc = winHandleSeek(h, nByte); + if( rc==SQLITE_OK ){ + if( 0==osSetEndOfFile(h) ){ + rc = SQLITE_IOERR_TRUNCATE; + } + } + return rc; +} + +/* +** Determine the size in bytes of the file opened by the handle passed as +** the first argument. +*/ +static int winHandleSize(HANDLE h, sqlite3_int64 *pnByte){ + int rc = SQLITE_OK; + +#if SQLITE_OS_WINRT + FILE_STANDARD_INFO info; + BOOL b; + b = osGetFileInformationByHandleEx(h, FileStandardInfo, &info, sizeof(info)); + if( b ){ + *pnByte = info.EndOfFile.QuadPart; + }else{ + rc = SQLITE_IOERR_FSTAT; + } +#else + DWORD upperBits = 0; + DWORD lowerBits = 0; + + assert( pnByte ); + lowerBits = osGetFileSize(h, &upperBits); + *pnByte = (((sqlite3_int64)upperBits)<<32) + lowerBits; + if( lowerBits==INVALID_FILE_SIZE && osGetLastError()!=NO_ERROR ){ + rc = SQLITE_IOERR_FSTAT; + } +#endif + + return rc; +} + +/* +** Close the handle passed as the only argument. +*/ +static void winHandleClose(HANDLE h){ + if( h!=INVALID_HANDLE_VALUE ){ + osCloseHandle(h); + } +} + /* ** Truncate an open file to a specified size */ @@ -50028,8 +50555,9 @@ static int winFileSize(sqlite3_file *id, sqlite3_int64 *pSize){ ** Different API routines are called depending on whether or not this ** is Win9x or WinNT. */ -static int winGetReadLock(winFile *pFile){ +static int winGetReadLock(winFile *pFile, int bBlock){ int res; + DWORD mask = ~(bBlock ? LOCKFILE_FAIL_IMMEDIATELY : 0); OSTRACE(("READ-LOCK file=%p, lock=%d\n", pFile->h, pFile->locktype)); if( osIsNT() ){ #if SQLITE_OS_WINCE @@ -50039,7 +50567,7 @@ static int winGetReadLock(winFile *pFile){ */ res = winceLockFile(&pFile->h, SHARED_FIRST, 0, 1, 0); #else - res = winLockFile(&pFile->h, SQLITE_LOCKFILEEX_FLAGS, SHARED_FIRST, 0, + res = winLockFile(&pFile->h, SQLITE_LOCKFILEEX_FLAGS&mask, SHARED_FIRST, 0, SHARED_SIZE, 0); #endif } @@ -50048,7 +50576,7 @@ static int winGetReadLock(winFile *pFile){ int lk; sqlite3_randomness(sizeof(lk), &lk); pFile->sharedLockByte = (short)((lk & 0x7fffffff)%(SHARED_SIZE - 1)); - res = winLockFile(&pFile->h, SQLITE_LOCKFILE_FLAGS, + res = winLockFile(&pFile->h, SQLITE_LOCKFILE_FLAGS&mask, SHARED_FIRST+pFile->sharedLockByte, 0, 1, 0); } #endif @@ -50143,46 +50671,62 @@ static int winLock(sqlite3_file *id, int locktype){ assert( locktype!=PENDING_LOCK ); assert( locktype!=RESERVED_LOCK || pFile->locktype==SHARED_LOCK ); - /* Lock the PENDING_LOCK byte if we need to acquire a PENDING lock or + /* Lock the PENDING_LOCK byte if we need to acquire an EXCLUSIVE lock or ** a SHARED lock. If we are acquiring a SHARED lock, the acquisition of ** the PENDING_LOCK byte is temporary. */ newLocktype = pFile->locktype; - if( pFile->locktype==NO_LOCK - || (locktype==EXCLUSIVE_LOCK && pFile->locktype<=RESERVED_LOCK) + if( locktype==SHARED_LOCK + || (locktype==EXCLUSIVE_LOCK && pFile->locktype==RESERVED_LOCK) ){ int cnt = 3; - while( cnt-->0 && (res = winLockFile(&pFile->h, SQLITE_LOCKFILE_FLAGS, - PENDING_BYTE, 0, 1, 0))==0 ){ + + /* Flags for the LockFileEx() call. This should be an exclusive lock if + ** this call is to obtain EXCLUSIVE, or a shared lock if this call is to + ** obtain SHARED. */ + int flags = LOCKFILE_FAIL_IMMEDIATELY; + if( locktype==EXCLUSIVE_LOCK ){ + flags |= LOCKFILE_EXCLUSIVE_LOCK; + } + while( cnt>0 ){ /* Try 3 times to get the pending lock. This is needed to work ** around problems caused by indexing and/or anti-virus software on ** Windows systems. + ** ** If you are using this code as a model for alternative VFSes, do not - ** copy this retry logic. It is a hack intended for Windows only. - */ + ** copy this retry logic. It is a hack intended for Windows only. */ + res = winLockFile(&pFile->h, flags, PENDING_BYTE, 0, 1, 0); + if( res ) break; + lastErrno = osGetLastError(); OSTRACE(("LOCK-PENDING-FAIL file=%p, count=%d, result=%d\n", - pFile->h, cnt, res)); + pFile->h, cnt, res + )); + if( lastErrno==ERROR_INVALID_HANDLE ){ pFile->lastErrno = lastErrno; rc = SQLITE_IOERR_LOCK; OSTRACE(("LOCK-FAIL file=%p, count=%d, rc=%s\n", - pFile->h, cnt, sqlite3ErrName(rc))); + pFile->h, cnt, sqlite3ErrName(rc) + )); return rc; } - if( cnt ) sqlite3_win32_sleep(1); + + cnt--; + if( cnt>0 ) sqlite3_win32_sleep(1); } gotPendingLock = res; - if( !res ){ - lastErrno = osGetLastError(); - } } /* Acquire a shared lock */ if( locktype==SHARED_LOCK && res ){ assert( pFile->locktype==NO_LOCK ); - res = winGetReadLock(pFile); +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + res = winGetReadLock(pFile, pFile->bBlockOnConnect); +#else + res = winGetReadLock(pFile, 0); +#endif if( res ){ newLocktype = SHARED_LOCK; }else{ @@ -50220,7 +50764,7 @@ static int winLock(sqlite3_file *id, int locktype){ newLocktype = EXCLUSIVE_LOCK; }else{ lastErrno = osGetLastError(); - winGetReadLock(pFile); + winGetReadLock(pFile, 0); } } @@ -50300,7 +50844,7 @@ static int winUnlock(sqlite3_file *id, int locktype){ type = pFile->locktype; if( type>=EXCLUSIVE_LOCK ){ winUnlockFile(&pFile->h, SHARED_FIRST, 0, SHARED_SIZE, 0); - if( locktype==SHARED_LOCK && !winGetReadLock(pFile) ){ + if( locktype==SHARED_LOCK && !winGetReadLock(pFile, 0) ){ /* This should never happen. We should always be able to ** reacquire the read lock */ rc = winLogError(SQLITE_IOERR_UNLOCK, osGetLastError(), @@ -50510,6 +51054,28 @@ static int winFileControl(sqlite3_file *id, int op, void *pArg){ return rc; } #endif + +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + case SQLITE_FCNTL_LOCK_TIMEOUT: { + int iOld = pFile->iBusyTimeout; + int iNew = *(int*)pArg; +#if SQLITE_ENABLE_SETLK_TIMEOUT==1 + pFile->iBusyTimeout = (iNew < 0) ? INFINITE : (DWORD)iNew; +#elif SQLITE_ENABLE_SETLK_TIMEOUT==2 + pFile->iBusyTimeout = (DWORD)(!!iNew); +#else +# error "SQLITE_ENABLE_SETLK_TIMEOUT must be set to 1 or 2" +#endif + *(int*)pArg = iOld; + return SQLITE_OK; + } + case SQLITE_FCNTL_BLOCK_ON_CONNECT: { + int iNew = *(int*)pArg; + pFile->bBlockOnConnect = iNew; + return SQLITE_OK; + } +#endif /* SQLITE_ENABLE_SETLK_TIMEOUT */ + } OSTRACE(("FCNTL file=%p, rc=SQLITE_NOTFOUND\n", pFile->h)); return SQLITE_NOTFOUND; @@ -50590,23 +51156,27 @@ static int winShmMutexHeld(void) { ** ** The following fields are read-only after the object is created: ** -** fid ** zFilename ** ** Either winShmNode.mutex must be held or winShmNode.nRef==0 and ** winShmMutexHeld() is true when reading or writing any other field ** in this structure. ** +** File-handle hSharedShm is used to (a) take the DMS lock, (b) truncate +** the *-shm file if the DMS-locking protocol demands it, and (c) map +** regions of the *-shm file into memory using MapViewOfFile() or +** similar. Other locks are taken by individual clients using the +** winShm.hShm handles. */ struct winShmNode { sqlite3_mutex *mutex; /* Mutex to access this object */ char *zFilename; /* Name of the file */ - winFile hFile; /* File handle from winOpen */ + HANDLE hSharedShm; /* File handle open on zFilename */ + int isUnlocked; /* DMS lock has not yet been obtained */ + int isReadonly; /* True if read-only */ int szRegion; /* Size of shared-memory regions */ int nRegion; /* Size of array apRegion */ - u8 isReadonly; /* True if read-only */ - u8 isUnlocked; /* True if no DMS lock held */ struct ShmRegion { HANDLE hMap; /* File handle from CreateFileMapping */ @@ -50615,7 +51185,6 @@ struct winShmNode { DWORD lastErrno; /* The Windows errno from the last I/O error */ int nRef; /* Number of winShm objects pointing to this */ - winShm *pFirst; /* All winShm objects pointing to this */ winShmNode *pNext; /* Next in list of all winShmNode objects */ #if defined(SQLITE_DEBUG) || defined(SQLITE_HAVE_OS_TRACE) u8 nextShmId; /* Next available winShm.id value */ @@ -50631,23 +51200,15 @@ static winShmNode *winShmNodeList = 0; /* ** Structure used internally by this VFS to record the state of an -** open shared memory connection. -** -** The following fields are initialized when this object is created and -** are read-only thereafter: -** -** winShm.pShmNode -** winShm.id -** -** All other fields are read/write. The winShm.pShmNode->mutex must be held -** while accessing any read/write fields. +** open shared memory connection. There is one such structure for each +** winFile open on a wal mode database. */ struct winShm { winShmNode *pShmNode; /* The underlying winShmNode object */ - winShm *pNext; /* Next winShm with the same winShmNode */ - u8 hasMutex; /* True if holding the winShmNode mutex */ u16 sharedMask; /* Mask of shared locks held */ u16 exclMask; /* Mask of exclusive locks held */ + HANDLE hShm; /* File-handle on *-shm file. For locking. */ + int bReadonly; /* True if hShm is opened read-only */ #if defined(SQLITE_DEBUG) || defined(SQLITE_HAVE_OS_TRACE) u8 id; /* Id of this connection with its winShmNode */ #endif @@ -50659,50 +51220,6 @@ struct winShm { #define WIN_SHM_BASE ((22+SQLITE_SHM_NLOCK)*4) /* first lock byte */ #define WIN_SHM_DMS (WIN_SHM_BASE+SQLITE_SHM_NLOCK) /* deadman switch */ -/* -** Apply advisory locks for all n bytes beginning at ofst. -*/ -#define WINSHM_UNLCK 1 -#define WINSHM_RDLCK 2 -#define WINSHM_WRLCK 3 -static int winShmSystemLock( - winShmNode *pFile, /* Apply locks to this open shared-memory segment */ - int lockType, /* WINSHM_UNLCK, WINSHM_RDLCK, or WINSHM_WRLCK */ - int ofst, /* Offset to first byte to be locked/unlocked */ - int nByte /* Number of bytes to lock or unlock */ -){ - int rc = 0; /* Result code form Lock/UnlockFileEx() */ - - /* Access to the winShmNode object is serialized by the caller */ - assert( pFile->nRef==0 || sqlite3_mutex_held(pFile->mutex) ); - - OSTRACE(("SHM-LOCK file=%p, lock=%d, offset=%d, size=%d\n", - pFile->hFile.h, lockType, ofst, nByte)); - - /* Release/Acquire the system-level lock */ - if( lockType==WINSHM_UNLCK ){ - rc = winUnlockFile(&pFile->hFile.h, ofst, 0, nByte, 0); - }else{ - /* Initialize the locking parameters */ - DWORD dwFlags = LOCKFILE_FAIL_IMMEDIATELY; - if( lockType == WINSHM_WRLCK ) dwFlags |= LOCKFILE_EXCLUSIVE_LOCK; - rc = winLockFile(&pFile->hFile.h, dwFlags, ofst, 0, nByte, 0); - } - - if( rc!= 0 ){ - rc = SQLITE_OK; - }else{ - pFile->lastErrno = osGetLastError(); - rc = SQLITE_BUSY; - } - - OSTRACE(("SHM-LOCK file=%p, func=%s, errno=%lu, rc=%s\n", - pFile->hFile.h, (lockType == WINSHM_UNLCK) ? "winUnlockFile" : - "winLockFile", pFile->lastErrno, sqlite3ErrName(rc))); - - return rc; -} - /* Forward references to VFS methods */ static int winOpen(sqlite3_vfs*,const char*,sqlite3_file*,int,int*); static int winDelete(sqlite3_vfs *,const char*,int); @@ -50734,11 +51251,7 @@ static void winShmPurge(sqlite3_vfs *pVfs, int deleteFlag){ osGetCurrentProcessId(), i, bRc ? "ok" : "failed")); UNUSED_VARIABLE_VALUE(bRc); } - if( p->hFile.h!=NULL && p->hFile.h!=INVALID_HANDLE_VALUE ){ - SimulateIOErrorBenign(1); - winClose((sqlite3_file *)&p->hFile); - SimulateIOErrorBenign(0); - } + winHandleClose(p->hSharedShm); if( deleteFlag ){ SimulateIOErrorBenign(1); sqlite3BeginBenignMalloc(); @@ -50756,42 +51269,239 @@ static void winShmPurge(sqlite3_vfs *pVfs, int deleteFlag){ } /* -** The DMS lock has not yet been taken on shm file pShmNode. Attempt to -** take it now. Return SQLITE_OK if successful, or an SQLite error -** code otherwise. -** -** If the DMS cannot be locked because this is a readonly_shm=1 -** connection and no other process already holds a lock, return -** SQLITE_READONLY_CANTINIT and set pShmNode->isUnlocked=1. +** The DMS lock has not yet been taken on the shm file associated with +** pShmNode. Take the lock. Truncate the *-shm file if required. +** Return SQLITE_OK if successful, or an SQLite error code otherwise. */ -static int winLockSharedMemory(winShmNode *pShmNode){ - int rc = winShmSystemLock(pShmNode, WINSHM_WRLCK, WIN_SHM_DMS, 1); +static int winLockSharedMemory(winShmNode *pShmNode, DWORD nMs){ + HANDLE h = pShmNode->hSharedShm; + int rc = SQLITE_OK; + + assert( sqlite3_mutex_held(pShmNode->mutex) ); + rc = winHandleLockTimeout(h, WIN_SHM_DMS, 1, 1, 0); + if( rc==SQLITE_OK ){ + /* We have an EXCLUSIVE lock on the DMS byte. This means that this + ** is the first process to open the file. Truncate it to zero bytes + ** in this case. */ + if( pShmNode->isReadonly ){ + rc = SQLITE_READONLY_CANTINIT; + }else{ + rc = winHandleTruncate(h, 0); + } + + /* Release the EXCLUSIVE lock acquired above. */ + winUnlockFile(&h, WIN_SHM_DMS, 0, 1, 0); + }else if( (rc & 0xFF)==SQLITE_BUSY ){ + rc = SQLITE_OK; + } if( rc==SQLITE_OK ){ - if( pShmNode->isReadonly ){ - pShmNode->isUnlocked = 1; - winShmSystemLock(pShmNode, WINSHM_UNLCK, WIN_SHM_DMS, 1); - return SQLITE_READONLY_CANTINIT; - }else if( winTruncate((sqlite3_file*)&pShmNode->hFile, 0) ){ - winShmSystemLock(pShmNode, WINSHM_UNLCK, WIN_SHM_DMS, 1); - return winLogError(SQLITE_IOERR_SHMOPEN, osGetLastError(), - "winLockSharedMemory", pShmNode->zFilename); + /* Take a SHARED lock on the DMS byte. */ + rc = winHandleLockTimeout(h, WIN_SHM_DMS, 1, 0, nMs); + if( rc==SQLITE_OK ){ + pShmNode->isUnlocked = 0; } } - if( rc==SQLITE_OK ){ - winShmSystemLock(pShmNode, WINSHM_UNLCK, WIN_SHM_DMS, 1); - } + return rc; +} - return winShmSystemLock(pShmNode, WINSHM_RDLCK, WIN_SHM_DMS, 1); + +/* +** Convert a UTF-8 filename into whatever form the underlying +** operating system wants filenames in. Space to hold the result +** is obtained from malloc and must be freed by the calling +** function +** +** On Cygwin, 3 possible input forms are accepted: +** - If the filename starts with "<drive>:/" or "<drive>:\", +** it is converted to UTF-16 as-is. +** - If the filename contains '/', it is assumed to be a +** Cygwin absolute path, it is converted to a win32 +** absolute path in UTF-16. +** - Otherwise it must be a filename only, the win32 filename +** is returned in UTF-16. +** Note: If the function cygwin_conv_path() fails, only +** UTF-8 -> UTF-16 conversion will be done. This can only +** happen when the file path >32k, in which case winUtf8ToUnicode() +** will fail too. +*/ +static void *winConvertFromUtf8Filename(const char *zFilename){ + void *zConverted = 0; + if( osIsNT() ){ +#ifdef __CYGWIN__ + int nChar; + LPWSTR zWideFilename; + + if( osCygwin_conv_path && !(winIsDriveLetterAndColon(zFilename) + && winIsDirSep(zFilename[2])) ){ + i64 nByte; + int convertflag = CCP_POSIX_TO_WIN_W; + if( !strchr(zFilename, '/') ) convertflag |= CCP_RELATIVE; + nByte = (i64)osCygwin_conv_path(convertflag, + zFilename, 0, 0); + if( nByte>0 ){ + zConverted = sqlite3MallocZero(12+(u64)nByte); + if ( zConverted==0 ){ + return zConverted; + } + zWideFilename = zConverted; + /* Filenames should be prefixed, except when converted + * full path already starts with "\\?\". */ + if( osCygwin_conv_path(convertflag, zFilename, + zWideFilename+4, nByte)==0 ){ + if( (convertflag&CCP_RELATIVE) ){ + memmove(zWideFilename, zWideFilename+4, nByte); + }else if( memcmp(zWideFilename+4, L"\\\\", 4) ){ + memcpy(zWideFilename, L"\\\\?\\", 8); + }else if( zWideFilename[6]!='?' ){ + memmove(zWideFilename+6, zWideFilename+4, nByte); + memcpy(zWideFilename, L"\\\\?\\UNC", 14); + }else{ + memmove(zWideFilename, zWideFilename+4, nByte); + } + return zConverted; + } + sqlite3_free(zConverted); + } + } + nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0); + if( nChar==0 ){ + return 0; + } + zWideFilename = sqlite3MallocZero( nChar*sizeof(WCHAR)+12 ); + if( zWideFilename==0 ){ + return 0; + } + nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, + zWideFilename, nChar); + if( nChar==0 ){ + sqlite3_free(zWideFilename); + zWideFilename = 0; + }else if( nChar>MAX_PATH + && winIsDriveLetterAndColon(zFilename) + && winIsDirSep(zFilename[2]) ){ + memmove(zWideFilename+4, zWideFilename, nChar*sizeof(WCHAR)); + zWideFilename[2] = '\\'; + memcpy(zWideFilename, L"\\\\?\\", 8); + }else if( nChar>MAX_PATH + && winIsDirSep(zFilename[0]) && winIsDirSep(zFilename[1]) + && zFilename[2] != '?' ){ + memmove(zWideFilename+6, zWideFilename, nChar*sizeof(WCHAR)); + memcpy(zWideFilename, L"\\\\?\\UNC", 14); + } + zConverted = zWideFilename; +#else + zConverted = winUtf8ToUnicode(zFilename); +#endif /* __CYGWIN__ */ + } +#if defined(SQLITE_WIN32_HAS_ANSI) && defined(_WIN32) + else{ + zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI()); + } +#endif + /* caller will handle out of memory */ + return zConverted; } /* -** Open the shared-memory area associated with database file pDbFd. +** This function is used to open a handle on a *-shm file. ** -** When opening a new shared-memory file, if no other instances of that -** file are currently open, in this process or in other processes, then -** the file must be truncated to zero length or have its header cleared. +** If SQLITE_ENABLE_SETLK_TIMEOUT is defined at build time, then the file +** is opened with FILE_FLAG_OVERLAPPED specified. If not, it is not. +*/ +static int winHandleOpen( + const char *zUtf8, /* File to open */ + int *pbReadonly, /* IN/OUT: True for readonly handle */ + HANDLE *ph /* OUT: New HANDLE for file */ +){ + int rc = SQLITE_OK; + void *zConverted = 0; + int bReadonly = *pbReadonly; + HANDLE h = INVALID_HANDLE_VALUE; + +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + const DWORD flag_overlapped = FILE_FLAG_OVERLAPPED; +#else + const DWORD flag_overlapped = 0; +#endif + + /* Convert the filename to the system encoding. */ + zConverted = winConvertFromUtf8Filename(zUtf8); + if( zConverted==0 ){ + OSTRACE(("OPEN name=%s, rc=SQLITE_IOERR_NOMEM", zUtf8)); + rc = SQLITE_IOERR_NOMEM_BKPT; + goto winopenfile_out; + } + + /* Ensure the file we are trying to open is not actually a directory. */ + if( winIsDir(zConverted) ){ + OSTRACE(("OPEN name=%s, rc=SQLITE_CANTOPEN_ISDIR", zUtf8)); + rc = SQLITE_CANTOPEN_ISDIR; + goto winopenfile_out; + } + + /* TODO: platforms. + ** TODO: retry-on-ioerr. + */ + if( osIsNT() ){ +#if SQLITE_OS_WINRT + CREATEFILE2_EXTENDED_PARAMETERS extendedParameters; + memset(&extendedParameters, 0, sizeof(extendedParameters)); + extendedParameters.dwSize = sizeof(extendedParameters); + extendedParameters.dwFileAttributes = FILE_ATTRIBUTE_NORMAL; + extendedParameters.dwFileFlags = flag_overlapped; + extendedParameters.dwSecurityQosFlags = SECURITY_ANONYMOUS; + h = osCreateFile2((LPCWSTR)zConverted, + (GENERIC_READ | (bReadonly ? 0 : GENERIC_WRITE)),/* dwDesiredAccess */ + FILE_SHARE_READ | FILE_SHARE_WRITE, /* dwShareMode */ + OPEN_ALWAYS, /* dwCreationDisposition */ + &extendedParameters + ); +#else + h = osCreateFileW((LPCWSTR)zConverted, /* lpFileName */ + (GENERIC_READ | (bReadonly ? 0 : GENERIC_WRITE)), /* dwDesiredAccess */ + FILE_SHARE_READ | FILE_SHARE_WRITE, /* dwShareMode */ + NULL, /* lpSecurityAttributes */ + OPEN_ALWAYS, /* dwCreationDisposition */ + FILE_ATTRIBUTE_NORMAL|flag_overlapped, + NULL + ); +#endif + }else{ + /* Due to pre-processor directives earlier in this file, + ** SQLITE_WIN32_HAS_ANSI is always defined if osIsNT() is false. */ +#ifdef SQLITE_WIN32_HAS_ANSI + h = osCreateFileA((LPCSTR)zConverted, + (GENERIC_READ | (bReadonly ? 0 : GENERIC_WRITE)), /* dwDesiredAccess */ + FILE_SHARE_READ | FILE_SHARE_WRITE, /* dwShareMode */ + NULL, /* lpSecurityAttributes */ + OPEN_ALWAYS, /* dwCreationDisposition */ + FILE_ATTRIBUTE_NORMAL|flag_overlapped, + NULL + ); +#endif + } + + if( h==INVALID_HANDLE_VALUE ){ + if( bReadonly==0 ){ + bReadonly = 1; + rc = winHandleOpen(zUtf8, &bReadonly, &h); + }else{ + rc = SQLITE_CANTOPEN_BKPT; + } + } + + winopenfile_out: + sqlite3_free(zConverted); + *pbReadonly = bReadonly; + *ph = h; + return rc; +} + + +/* +** Open the shared-memory area associated with database file pDbFd. */ static int winOpenSharedMemory(winFile *pDbFd){ struct winShm *p; /* The connection to be opened */ @@ -50803,98 +51513,83 @@ static int winOpenSharedMemory(winFile *pDbFd){ assert( pDbFd->pShm==0 ); /* Not previously opened */ /* Allocate space for the new sqlite3_shm object. Also speculatively - ** allocate space for a new winShmNode and filename. - */ + ** allocate space for a new winShmNode and filename. */ p = sqlite3MallocZero( sizeof(*p) ); if( p==0 ) return SQLITE_IOERR_NOMEM_BKPT; nName = sqlite3Strlen30(pDbFd->zPath); - pNew = sqlite3MallocZero( sizeof(*pShmNode) + nName + 17 ); + pNew = sqlite3MallocZero( sizeof(*pShmNode) + (i64)nName + 17 ); if( pNew==0 ){ sqlite3_free(p); return SQLITE_IOERR_NOMEM_BKPT; } pNew->zFilename = (char*)&pNew[1]; + pNew->hSharedShm = INVALID_HANDLE_VALUE; + pNew->isUnlocked = 1; sqlite3_snprintf(nName+15, pNew->zFilename, "%s-shm", pDbFd->zPath); sqlite3FileSuffix3(pDbFd->zPath, pNew->zFilename); + /* Open a file-handle on the *-shm file for this connection. This file-handle + ** is only used for locking. The mapping of the *-shm file is created using + ** the shared file handle in winShmNode.hSharedShm. */ + p->bReadonly = sqlite3_uri_boolean(pDbFd->zPath, "readonly_shm", 0); + rc = winHandleOpen(pNew->zFilename, &p->bReadonly, &p->hShm); + /* Look to see if there is an existing winShmNode that can be used. - ** If no matching winShmNode currently exists, create a new one. - */ + ** If no matching winShmNode currently exists, then create a new one. */ winShmEnterMutex(); for(pShmNode = winShmNodeList; pShmNode; pShmNode=pShmNode->pNext){ /* TBD need to come up with better match here. Perhaps - ** use FILE_ID_BOTH_DIR_INFO Structure. - */ + ** use FILE_ID_BOTH_DIR_INFO Structure. */ if( sqlite3StrICmp(pShmNode->zFilename, pNew->zFilename)==0 ) break; } - if( pShmNode ){ - sqlite3_free(pNew); - }else{ - int inFlags = SQLITE_OPEN_WAL; - int outFlags = 0; - + if( pShmNode==0 ){ pShmNode = pNew; - pNew = 0; - ((winFile*)(&pShmNode->hFile))->h = INVALID_HANDLE_VALUE; - pShmNode->pNext = winShmNodeList; - winShmNodeList = pShmNode; + /* Allocate a mutex for this winShmNode object, if one is required. */ if( sqlite3GlobalConfig.bCoreMutex ){ pShmNode->mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - if( pShmNode->mutex==0 ){ - rc = SQLITE_IOERR_NOMEM_BKPT; - goto shm_open_err; + if( pShmNode->mutex==0 ) rc = SQLITE_IOERR_NOMEM_BKPT; + } + + /* Open a file-handle to use for mappings, and for the DMS lock. */ + if( rc==SQLITE_OK ){ + HANDLE h = INVALID_HANDLE_VALUE; + pShmNode->isReadonly = p->bReadonly; + rc = winHandleOpen(pNew->zFilename, &pShmNode->isReadonly, &h); + pShmNode->hSharedShm = h; + } + + /* If successful, link the new winShmNode into the global list. If an + ** error occurred, free the object. */ + if( rc==SQLITE_OK ){ + pShmNode->pNext = winShmNodeList; + winShmNodeList = pShmNode; + pNew = 0; + }else{ + sqlite3_mutex_free(pShmNode->mutex); + if( pShmNode->hSharedShm!=INVALID_HANDLE_VALUE ){ + osCloseHandle(pShmNode->hSharedShm); } } - - if( 0==sqlite3_uri_boolean(pDbFd->zPath, "readonly_shm", 0) ){ - inFlags |= SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; - }else{ - inFlags |= SQLITE_OPEN_READONLY; - } - rc = winOpen(pDbFd->pVfs, pShmNode->zFilename, - (sqlite3_file*)&pShmNode->hFile, - inFlags, &outFlags); - if( rc!=SQLITE_OK ){ - rc = winLogError(rc, osGetLastError(), "winOpenShm", - pShmNode->zFilename); - goto shm_open_err; - } - if( outFlags==SQLITE_OPEN_READONLY ) pShmNode->isReadonly = 1; - - rc = winLockSharedMemory(pShmNode); - if( rc!=SQLITE_OK && rc!=SQLITE_READONLY_CANTINIT ) goto shm_open_err; } - /* Make the new connection a child of the winShmNode */ - p->pShmNode = pShmNode; + /* If no error has occurred, link the winShm object to the winShmNode and + ** the winShm to pDbFd. */ + if( rc==SQLITE_OK ){ + p->pShmNode = pShmNode; + pShmNode->nRef++; #if defined(SQLITE_DEBUG) || defined(SQLITE_HAVE_OS_TRACE) - p->id = pShmNode->nextShmId++; + p->id = pShmNode->nextShmId++; #endif - pShmNode->nRef++; - pDbFd->pShm = p; + pDbFd->pShm = p; + }else if( p ){ + winHandleClose(p->hShm); + sqlite3_free(p); + } + + assert( rc!=SQLITE_OK || pShmNode->isUnlocked==0 || pShmNode->nRegion==0 ); winShmLeaveMutex(); - - /* The reference count on pShmNode has already been incremented under - ** the cover of the winShmEnterMutex() mutex and the pointer from the - ** new (struct winShm) object to the pShmNode has been set. All that is - ** left to do is to link the new object into the linked list starting - ** at pShmNode->pFirst. This must be done while holding the pShmNode->mutex - ** mutex. - */ - sqlite3_mutex_enter(pShmNode->mutex); - p->pNext = pShmNode->pFirst; - pShmNode->pFirst = p; - sqlite3_mutex_leave(pShmNode->mutex); - return rc; - - /* Jump here on any error */ -shm_open_err: - winShmSystemLock(pShmNode, WINSHM_UNLCK, WIN_SHM_DMS, 1); - winShmPurge(pDbFd->pVfs, 0); /* This call frees pShmNode if required */ - sqlite3_free(p); sqlite3_free(pNew); - winShmLeaveMutex(); return rc; } @@ -50909,27 +51604,19 @@ static int winShmUnmap( winFile *pDbFd; /* Database holding shared-memory */ winShm *p; /* The connection to be closed */ winShmNode *pShmNode; /* The underlying shared-memory file */ - winShm **pp; /* For looping over sibling connections */ pDbFd = (winFile*)fd; p = pDbFd->pShm; if( p==0 ) return SQLITE_OK; + if( p->hShm!=INVALID_HANDLE_VALUE ){ + osCloseHandle(p->hShm); + } + pShmNode = p->pShmNode; - - /* Remove connection p from the set of connections associated - ** with pShmNode */ - sqlite3_mutex_enter(pShmNode->mutex); - for(pp=&pShmNode->pFirst; (*pp)!=p; pp = &(*pp)->pNext){} - *pp = p->pNext; - - /* Free the connection p */ - sqlite3_free(p); - pDbFd->pShm = 0; - sqlite3_mutex_leave(pShmNode->mutex); + winShmEnterMutex(); /* If pShmNode->nRef has reached 0, then close the underlying - ** shared-memory file, too */ - winShmEnterMutex(); + ** shared-memory file, too. */ assert( pShmNode->nRef>0 ); pShmNode->nRef--; if( pShmNode->nRef==0 ){ @@ -50937,6 +51624,9 @@ static int winShmUnmap( } winShmLeaveMutex(); + /* Free the connection p */ + sqlite3_free(p); + pDbFd->pShm = 0; return SQLITE_OK; } @@ -50951,10 +51641,9 @@ static int winShmLock( ){ winFile *pDbFd = (winFile*)fd; /* Connection holding shared memory */ winShm *p = pDbFd->pShm; /* The shared memory being locked */ - winShm *pX; /* For looping over all siblings */ winShmNode *pShmNode; int rc = SQLITE_OK; /* Result code */ - u16 mask; /* Mask of locks to take or release */ + u16 mask = (u16)((1U<<(ofst+n)) - (1U<<ofst)); /* Mask of locks to [un]take */ if( p==0 ) return SQLITE_IOERR_SHMLOCK; pShmNode = p->pShmNode; @@ -50968,85 +51657,82 @@ static int winShmLock( || flags==(SQLITE_SHM_UNLOCK | SQLITE_SHM_EXCLUSIVE) ); assert( n==1 || (flags & SQLITE_SHM_EXCLUSIVE)!=0 ); - mask = (u16)((1U<<(ofst+n)) - (1U<<ofst)); - assert( n>1 || mask==(1<<ofst) ); - sqlite3_mutex_enter(pShmNode->mutex); - if( flags & SQLITE_SHM_UNLOCK ){ - u16 allMask = 0; /* Mask of locks held by siblings */ + /* Check that, if this to be a blocking lock, no locks that occur later + ** in the following list than the lock being obtained are already held: + ** + ** 1. Checkpointer lock (ofst==1). + ** 2. Write lock (ofst==0). + ** 3. Read locks (ofst>=3 && ofst<SQLITE_SHM_NLOCK). + ** + ** In other words, if this is a blocking lock, none of the locks that + ** occur later in the above list than the lock being obtained may be + ** held. + ** + ** It is not permitted to block on the RECOVER lock. + */ +#if defined(SQLITE_ENABLE_SETLK_TIMEOUT) && defined(SQLITE_DEBUG) + { + u16 lockMask = (p->exclMask|p->sharedMask); + assert( (flags & SQLITE_SHM_UNLOCK) || pDbFd->iBusyTimeout==0 || ( + (ofst!=2) /* not RECOVER */ + && (ofst!=1 || lockMask==0 || lockMask==2) + && (ofst!=0 || lockMask<3) + && (ofst<3 || lockMask<(1<<ofst)) + )); + } +#endif - /* See if any siblings hold this same lock */ - for(pX=pShmNode->pFirst; pX; pX=pX->pNext){ - if( pX==p ) continue; - assert( (pX->exclMask & (p->exclMask|p->sharedMask))==0 ); - allMask |= pX->sharedMask; - } + /* Check if there is any work to do. There are three cases: + ** + ** a) An unlock operation where there are locks to unlock, + ** b) An shared lock where the requested lock is not already held + ** c) An exclusive lock where the requested lock is not already held + ** + ** The SQLite core never requests an exclusive lock that it already holds. + ** This is assert()ed immediately below. */ + assert( flags!=(SQLITE_SHM_EXCLUSIVE|SQLITE_SHM_LOCK) + || 0==(p->exclMask & mask) + ); + if( ((flags & SQLITE_SHM_UNLOCK) && ((p->exclMask|p->sharedMask) & mask)) + || (flags==(SQLITE_SHM_SHARED|SQLITE_SHM_LOCK) && 0==(p->sharedMask & mask)) + || (flags==(SQLITE_SHM_EXCLUSIVE|SQLITE_SHM_LOCK)) + ){ - /* Unlock the system-level locks */ - if( (mask & allMask)==0 ){ - rc = winShmSystemLock(pShmNode, WINSHM_UNLCK, ofst+WIN_SHM_BASE, n); - }else{ - rc = SQLITE_OK; - } + if( flags & SQLITE_SHM_UNLOCK ){ + /* Case (a) - unlock. */ - /* Undo the local locks */ - if( rc==SQLITE_OK ){ - p->exclMask &= ~mask; - p->sharedMask &= ~mask; - } - }else if( flags & SQLITE_SHM_SHARED ){ - u16 allShared = 0; /* Union of locks held by connections other than "p" */ + assert( (p->exclMask & p->sharedMask)==0 ); + assert( !(flags & SQLITE_SHM_EXCLUSIVE) || (p->exclMask & mask)==mask ); + assert( !(flags & SQLITE_SHM_SHARED) || (p->sharedMask & mask)==mask ); - /* Find out which shared locks are already held by sibling connections. - ** If any sibling already holds an exclusive lock, go ahead and return - ** SQLITE_BUSY. - */ - for(pX=pShmNode->pFirst; pX; pX=pX->pNext){ - if( (pX->exclMask & mask)!=0 ){ - rc = SQLITE_BUSY; - break; - } - allShared |= pX->sharedMask; - } + rc = winHandleUnlock(p->hShm, ofst+WIN_SHM_BASE, n); - /* Get shared locks at the system level, if necessary */ - if( rc==SQLITE_OK ){ - if( (allShared & mask)==0 ){ - rc = winShmSystemLock(pShmNode, WINSHM_RDLCK, ofst+WIN_SHM_BASE, n); - }else{ - rc = SQLITE_OK; - } - } - - /* Get the local shared locks */ - if( rc==SQLITE_OK ){ - p->sharedMask |= mask; - } - }else{ - /* Make sure no sibling connections hold locks that will block this - ** lock. If any do, return SQLITE_BUSY right away. - */ - for(pX=pShmNode->pFirst; pX; pX=pX->pNext){ - if( (pX->exclMask & mask)!=0 || (pX->sharedMask & mask)!=0 ){ - rc = SQLITE_BUSY; - break; - } - } - - /* Get the exclusive locks at the system level. Then if successful - ** also mark the local connection as being locked. - */ - if( rc==SQLITE_OK ){ - rc = winShmSystemLock(pShmNode, WINSHM_WRLCK, ofst+WIN_SHM_BASE, n); + /* If successful, also clear the bits in sharedMask/exclMask */ if( rc==SQLITE_OK ){ - assert( (p->sharedMask & mask)==0 ); - p->exclMask |= mask; + p->exclMask = (p->exclMask & ~mask); + p->sharedMask = (p->sharedMask & ~mask); + } + }else{ + int bExcl = ((flags & SQLITE_SHM_EXCLUSIVE) ? 1 : 0); + DWORD nMs = winFileBusyTimeout(pDbFd); + rc = winHandleLockTimeout(p->hShm, ofst+WIN_SHM_BASE, n, bExcl, nMs); + if( rc==SQLITE_OK ){ + if( bExcl ){ + p->exclMask = (p->exclMask | mask); + }else{ + p->sharedMask = (p->sharedMask | mask); + } } } } - sqlite3_mutex_leave(pShmNode->mutex); - OSTRACE(("SHM-LOCK pid=%lu, id=%d, sharedMask=%03x, exclMask=%03x, rc=%s\n", - osGetCurrentProcessId(), p->id, p->sharedMask, p->exclMask, - sqlite3ErrName(rc))); + + OSTRACE(( + "SHM-LOCK(%d,%d,%d) pid=%lu, id=%d, sharedMask=%03x, exclMask=%03x," + " rc=%s\n", + ofst, n, flags, + osGetCurrentProcessId(), p->id, p->sharedMask, p->exclMask, + sqlite3ErrName(rc)) + ); return rc; } @@ -51108,13 +51794,15 @@ static int winShmMap( sqlite3_mutex_enter(pShmNode->mutex); if( pShmNode->isUnlocked ){ - rc = winLockSharedMemory(pShmNode); + /* Take the DMS lock. */ + assert( pShmNode->nRegion==0 ); + rc = winLockSharedMemory(pShmNode, winFileBusyTimeout(pDbFd)); if( rc!=SQLITE_OK ) goto shmpage_out; - pShmNode->isUnlocked = 0; } - assert( szRegion==pShmNode->szRegion || pShmNode->nRegion==0 ); + assert( szRegion==pShmNode->szRegion || pShmNode->nRegion==0 ); if( pShmNode->nRegion<=iRegion ){ + HANDLE hShared = pShmNode->hSharedShm; struct ShmRegion *apNew; /* New aRegion[] array */ int nByte = (iRegion+1)*szRegion; /* Minimum required file size */ sqlite3_int64 sz; /* Current size of wal-index file */ @@ -51125,10 +51813,9 @@ static int winShmMap( ** Check to see if it has been allocated (i.e. if the wal-index file is ** large enough to contain the requested region). */ - rc = winFileSize((sqlite3_file *)&pShmNode->hFile, &sz); + rc = winHandleSize(hShared, &sz); if( rc!=SQLITE_OK ){ - rc = winLogError(SQLITE_IOERR_SHMSIZE, osGetLastError(), - "winShmMap1", pDbFd->zPath); + rc = winLogError(rc, osGetLastError(), "winShmMap1", pDbFd->zPath); goto shmpage_out; } @@ -51137,19 +51824,17 @@ static int winShmMap( ** zero, exit early. *pp will be set to NULL and SQLITE_OK returned. ** ** Alternatively, if isWrite is non-zero, use ftruncate() to allocate - ** the requested memory region. - */ + ** the requested memory region. */ if( !isWrite ) goto shmpage_out; - rc = winTruncate((sqlite3_file *)&pShmNode->hFile, nByte); + rc = winHandleTruncate(hShared, nByte); if( rc!=SQLITE_OK ){ - rc = winLogError(SQLITE_IOERR_SHMSIZE, osGetLastError(), - "winShmMap2", pDbFd->zPath); + rc = winLogError(rc, osGetLastError(), "winShmMap2", pDbFd->zPath); goto shmpage_out; } } /* Map the requested memory region into this processes address space. */ - apNew = (struct ShmRegion *)sqlite3_realloc64( + apNew = (struct ShmRegion*)sqlite3_realloc64( pShmNode->aRegion, (iRegion+1)*sizeof(apNew[0]) ); if( !apNew ){ @@ -51168,18 +51853,13 @@ static int winShmMap( void *pMap = 0; /* Mapped memory region */ #if SQLITE_OS_WINRT - hMap = osCreateFileMappingFromApp(pShmNode->hFile.h, - NULL, protect, nByte, NULL - ); + hMap = osCreateFileMappingFromApp(hShared, NULL, protect, nByte, NULL); #elif defined(SQLITE_WIN32_HAS_WIDE) - hMap = osCreateFileMappingW(pShmNode->hFile.h, - NULL, protect, 0, nByte, NULL - ); + hMap = osCreateFileMappingW(hShared, NULL, protect, 0, nByte, NULL); #elif defined(SQLITE_WIN32_HAS_ANSI) && SQLITE_WIN32_CREATEFILEMAPPINGA - hMap = osCreateFileMappingA(pShmNode->hFile.h, - NULL, protect, 0, nByte, NULL - ); + hMap = osCreateFileMappingA(hShared, NULL, protect, 0, nByte, NULL); #endif + OSTRACE(("SHM-MAP-CREATE pid=%lu, region=%d, size=%d, rc=%s\n", osGetCurrentProcessId(), pShmNode->nRegion, nByte, hMap ? "ok" : "failed")); @@ -51222,7 +51902,9 @@ shmpage_out: }else{ *pp = 0; } - if( pShmNode->isReadonly && rc==SQLITE_OK ) rc = SQLITE_READONLY; + if( pShmNode->isReadonly && rc==SQLITE_OK ){ + rc = SQLITE_READONLY; + } sqlite3_mutex_leave(pShmNode->mutex); return rc; } @@ -51542,47 +52224,6 @@ static winVfsAppData winNolockAppData = { ** sqlite3_vfs object. */ -#if defined(__CYGWIN__) -/* -** Convert a filename from whatever the underlying operating system -** supports for filenames into UTF-8. Space to hold the result is -** obtained from malloc and must be freed by the calling function. -*/ -static char *winConvertToUtf8Filename(const void *zFilename){ - char *zConverted = 0; - if( osIsNT() ){ - zConverted = winUnicodeToUtf8(zFilename); - } -#ifdef SQLITE_WIN32_HAS_ANSI - else{ - zConverted = winMbcsToUtf8(zFilename, osAreFileApisANSI()); - } -#endif - /* caller will handle out of memory */ - return zConverted; -} -#endif - -/* -** Convert a UTF-8 filename into whatever form the underlying -** operating system wants filenames in. Space to hold the result -** is obtained from malloc and must be freed by the calling -** function. -*/ -static void *winConvertFromUtf8Filename(const char *zFilename){ - void *zConverted = 0; - if( osIsNT() ){ - zConverted = winUtf8ToUnicode(zFilename); - } -#ifdef SQLITE_WIN32_HAS_ANSI - else{ - zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI()); - } -#endif - /* caller will handle out of memory */ - return zConverted; -} - /* ** This function returns non-zero if the specified UTF-8 string buffer ** ends with a directory separator character or one was successfully @@ -51595,7 +52236,14 @@ static int winMakeEndInDirSep(int nBuf, char *zBuf){ if( winIsDirSep(zBuf[nLen-1]) ){ return 1; }else if( nLen+1<nBuf ){ - zBuf[nLen] = winGetDirSep(); + if( !osGetenv ){ + zBuf[nLen] = winGetDirSep(); + }else if( winIsDriveLetterAndColon(zBuf) && winIsDirSep(zBuf[2]) ){ + zBuf[nLen] = '\\'; + zBuf[2]='\\'; + }else{ + zBuf[nLen] = '/'; + } zBuf[nLen+1] = '\0'; return 1; } @@ -51622,14 +52270,14 @@ static int winTempDirDefined(void){ ** The pointer returned in pzBuf must be freed via sqlite3_free(). */ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ - static char zChars[] = + static const char zChars[] = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789"; size_t i, j; DWORD pid; int nPre = sqlite3Strlen30(SQLITE_TEMP_FILE_PREFIX); - int nMax, nBuf, nDir, nLen; + i64 nMax, nBuf, nDir, nLen; char *zBuf; /* It's odd to simulate an io-error here, but really this is just @@ -51641,7 +52289,8 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ /* Allocate a temporary buffer to store the fully qualified file ** name for the temporary file. If this fails, we cannot continue. */ - nMax = pVfs->mxPathname; nBuf = nMax + 2; + nMax = pVfs->mxPathname; + nBuf = 2 + (i64)nMax; zBuf = sqlite3MallocZero( nBuf ); if( !zBuf ){ OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); @@ -51672,7 +52321,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ } #if defined(__CYGWIN__) - else{ + else if( osGetenv!=NULL ){ static const char *azDirs[] = { 0, /* getenv("SQLITE_TMPDIR") */ 0, /* getenv("TMPDIR") */ @@ -51688,11 +52337,11 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ unsigned int i; const char *zDir = 0; - if( !azDirs[0] ) azDirs[0] = getenv("SQLITE_TMPDIR"); - if( !azDirs[1] ) azDirs[1] = getenv("TMPDIR"); - if( !azDirs[2] ) azDirs[2] = getenv("TMP"); - if( !azDirs[3] ) azDirs[3] = getenv("TEMP"); - if( !azDirs[4] ) azDirs[4] = getenv("USERPROFILE"); + if( !azDirs[0] ) azDirs[0] = osGetenv("SQLITE_TMPDIR"); + if( !azDirs[1] ) azDirs[1] = osGetenv("TMPDIR"); + if( !azDirs[2] ) azDirs[2] = osGetenv("TMP"); + if( !azDirs[3] ) azDirs[3] = osGetenv("TEMP"); + if( !azDirs[4] ) azDirs[4] = osGetenv("USERPROFILE"); for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){ void *zConverted; if( zDir==0 ) continue; @@ -51701,7 +52350,7 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ ** it must be converted to a native Win32 path via the Cygwin API ** prior to using it. */ - if( winIsDriveLetterAndColon(zDir) ){ + { zConverted = winConvertFromUtf8Filename(zDir); if( !zConverted ){ sqlite3_free(zBuf); @@ -51714,44 +52363,12 @@ static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ break; } sqlite3_free(zConverted); - }else{ - zConverted = sqlite3MallocZero( nMax+1 ); - if( !zConverted ){ - sqlite3_free(zBuf); - OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM_BKPT; - } - if( cygwin_conv_path( - osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir, - zConverted, nMax+1)<0 ){ - sqlite3_free(zConverted); - sqlite3_free(zBuf); - OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n")); - return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno, - "winGetTempname2", zDir); - } - if( winIsDir(zConverted) ){ - /* At this point, we know the candidate directory exists and should - ** be used. However, we may need to convert the string containing - ** its name into UTF-8 (i.e. if it is UTF-16 right now). - */ - char *zUtf8 = winConvertToUtf8Filename(zConverted); - if( !zUtf8 ){ - sqlite3_free(zConverted); - sqlite3_free(zBuf); - OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); - return SQLITE_IOERR_NOMEM_BKPT; - } - sqlite3_snprintf(nMax, zBuf, "%s", zUtf8); - sqlite3_free(zUtf8); - sqlite3_free(zConverted); - break; - } - sqlite3_free(zConverted); } } } -#elif !SQLITE_OS_WINRT && !defined(__CYGWIN__) +#endif + +#if !SQLITE_OS_WINRT && defined(_WIN32) else if( osIsNT() ){ char *zMulti; LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) ); @@ -51875,7 +52492,7 @@ static int winIsDir(const void *zConverted){ return 0; /* Invalid name? */ } attr = sAttrData.dwFileAttributes; -#if SQLITE_OS_WINCE==0 +#if SQLITE_OS_WINCE==0 && defined(SQLITE_WIN32_HAS_ANSI) }else{ attr = osGetFileAttributesA((char*)zConverted); #endif @@ -51891,6 +52508,12 @@ static int winAccess( int *pResOut /* OUT: Result */ ); +/* +** The Windows version of xAccess() accepts an extra bit in the flags +** parameter that prevents an anti-virus retry loop. +*/ +#define NORETRY 0x4000 + /* ** Open a file. */ @@ -51915,6 +52538,7 @@ static int winOpen( void *zConverted; /* Filename in OS encoding */ const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */ int cnt = 0; + int isRO = 0; /* file is known to be accessible readonly */ /* If argument zPath is a NULL pointer, this function is required to open ** a temporary file. Use this buffer to store the file name in. @@ -52079,9 +52703,9 @@ static int winOpen( &extendedParameters); if( h!=INVALID_HANDLE_VALUE ) break; if( isReadWrite ){ - int rc2, isRO = 0; + int rc2; sqlite3BeginBenignMalloc(); - rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); + rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); sqlite3EndBenignMalloc(); if( rc2==SQLITE_OK && isRO ) break; } @@ -52096,9 +52720,9 @@ static int winOpen( NULL); if( h!=INVALID_HANDLE_VALUE ) break; if( isReadWrite ){ - int rc2, isRO = 0; + int rc2; sqlite3BeginBenignMalloc(); - rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); + rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); sqlite3EndBenignMalloc(); if( rc2==SQLITE_OK && isRO ) break; } @@ -52116,9 +52740,9 @@ static int winOpen( NULL); if( h!=INVALID_HANDLE_VALUE ) break; if( isReadWrite ){ - int rc2, isRO = 0; + int rc2; sqlite3BeginBenignMalloc(); - rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); + rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); sqlite3EndBenignMalloc(); if( rc2==SQLITE_OK && isRO ) break; } @@ -52133,7 +52757,7 @@ static int winOpen( if( h==INVALID_HANDLE_VALUE ){ sqlite3_free(zConverted); sqlite3_free(zTmpname); - if( isReadWrite && !isExclusive ){ + if( isReadWrite && isRO && !isExclusive ){ return winOpen(pVfs, zName, id, ((flags|SQLITE_OPEN_READONLY) & ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)), @@ -52335,8 +52959,14 @@ static int winAccess( int rc = 0; DWORD lastErrno = 0; void *zConverted; + int noRetry = 0; /* Do not use winRetryIoerr() */ UNUSED_PARAMETER(pVfs); + if( (flags & NORETRY)!=0 ){ + noRetry = 1; + flags &= ~NORETRY; + } + SimulateIOError( return SQLITE_IOERR_ACCESS; ); OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n", zFilename, flags, pResOut)); @@ -52359,7 +52989,10 @@ static int winAccess( memset(&sAttrData, 0, sizeof(sAttrData)); while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted, GetFileExInfoStandard, - &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){} + &sAttrData)) + && !noRetry + && winRetryIoerr(&cnt, &lastErrno) + ){ /* Loop until true */} if( rc ){ /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file ** as if it does not exist. @@ -52427,6 +53060,7 @@ static BOOL winIsDriveLetterAndColon( return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' ); } +#ifdef _WIN32 /* ** Returns non-zero if the specified path name should be used verbatim. If ** non-zero is returned from this function, the calling function must simply @@ -52463,6 +53097,70 @@ static BOOL winIsVerbatimPathname( */ return FALSE; } +#endif /* _WIN32 */ + +#ifdef __CYGWIN__ +/* +** Simplify a filename into its canonical form +** by making the following changes: +** +** * convert any '/' to '\' (win32) or reverse (Cygwin) +** * removing any trailing and duplicate / (except for UNC paths) +** * convert /./ into just / +** +** Changes are made in-place. Return the new name length. +** +** The original filename is in z[0..]. If the path is shortened, +** no-longer used bytes will be written by '\0'. +*/ +static void winSimplifyName(char *z){ + int i, j; + for(i=j=0; z[i]; ++i){ + if( winIsDirSep(z[i]) ){ +#if !defined(SQLITE_TEST) + /* Some test-cases assume that "./foo" and "foo" are different */ + if( z[i+1]=='.' && winIsDirSep(z[i+2]) ){ + ++i; + continue; + } +#endif + if( !z[i+1] || (winIsDirSep(z[i+1]) && (i!=0)) ){ + continue; + } + z[j++] = osGetenv?'/':'\\'; + }else{ + z[j++] = z[i]; + } + } + while(j<i) z[j++] = '\0'; +} + +#define SQLITE_MAX_SYMLINKS 100 + +static int mkFullPathname( + const char *zPath, /* Input path */ + char *zOut, /* Output buffer */ + int nOut /* Allocated size of buffer zOut */ +){ + int nPath = sqlite3Strlen30(zPath); + int iOff = 0; + if( zPath[0]!='/' ){ + if( osGetcwd(zOut, nOut-2)==0 ){ + return winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "getcwd", zPath); + } + iOff = sqlite3Strlen30(zOut); + zOut[iOff++] = '/'; + } + if( (iOff+nPath+1)>nOut ){ + /* SQLite assumes that xFullPathname() nul-terminates the output buffer + ** even if it returns an error. */ + zOut[iOff] = '\0'; + return SQLITE_CANTOPEN_BKPT; + } + sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath); + return SQLITE_OK; +} +#endif /* __CYGWIN__ */ /* ** Turn a relative pathname into a full pathname. Write the full @@ -52475,8 +53173,8 @@ static int winFullPathnameNoMutex( int nFull, /* Size of output buffer in bytes */ char *zFull /* Output buffer */ ){ -#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__) - DWORD nByte; +#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT + int nByte; void *zConverted; char *zOut; #endif @@ -52489,64 +53187,82 @@ static int winFullPathnameNoMutex( zRelative++; } -#if defined(__CYGWIN__) SimulateIOError( return SQLITE_ERROR ); - UNUSED_PARAMETER(nFull); - assert( nFull>=pVfs->mxPathname ); - if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){ - /* - ** NOTE: We are dealing with a relative path name and the data - ** directory has been set. Therefore, use it as the basis - ** for converting the relative path name to an absolute - ** one by prepending the data directory and a slash. - */ - char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); - if( !zOut ){ - return SQLITE_IOERR_NOMEM_BKPT; - } - if( cygwin_conv_path( - (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) | - CCP_RELATIVE, zRelative, zOut, pVfs->mxPathname+1)<0 ){ - sqlite3_free(zOut); - return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, - "winFullPathname1", zRelative); - }else{ - char *zUtf8 = winConvertToUtf8Filename(zOut); - if( !zUtf8 ){ - sqlite3_free(zOut); - return SQLITE_IOERR_NOMEM_BKPT; - } - sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", - sqlite3_data_directory, winGetDirSep(), zUtf8); - sqlite3_free(zUtf8); - sqlite3_free(zOut); - } - }else{ - char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); - if( !zOut ){ - return SQLITE_IOERR_NOMEM_BKPT; - } - if( cygwin_conv_path( - (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A), - zRelative, zOut, pVfs->mxPathname+1)<0 ){ - sqlite3_free(zOut); - return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, - "winFullPathname2", zRelative); - }else{ - char *zUtf8 = winConvertToUtf8Filename(zOut); - if( !zUtf8 ){ - sqlite3_free(zOut); - return SQLITE_IOERR_NOMEM_BKPT; - } - sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8); - sqlite3_free(zUtf8); - sqlite3_free(zOut); + +#ifdef __CYGWIN__ + if( osGetcwd ){ + zFull[nFull-1] = '\0'; + if( !winIsDriveLetterAndColon(zRelative) || !winIsDirSep(zRelative[2]) ){ + int rc = SQLITE_OK; + int nLink = 1; /* Number of symbolic links followed so far */ + const char *zIn = zRelative; /* Input path for each iteration of loop */ + char *zDel = 0; + struct stat buf; + + UNUSED_PARAMETER(pVfs); + + do { + /* Call lstat() on path zIn. Set bLink to true if the path is a symbolic + ** link, or false otherwise. */ + int bLink = 0; + if( osLstat && osReadlink ) { + if( osLstat(zIn, &buf)!=0 ){ + int myErrno = osErrno; + if( myErrno!=ENOENT ){ + rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)myErrno, "lstat", zIn); + } + }else{ + bLink = ((buf.st_mode & 0170000) == 0120000); + } + + if( bLink ){ + if( zDel==0 ){ + zDel = sqlite3MallocZero(nFull); + if( zDel==0 ) rc = SQLITE_NOMEM; + }else if( ++nLink>SQLITE_MAX_SYMLINKS ){ + rc = SQLITE_CANTOPEN_BKPT; + } + + if( rc==SQLITE_OK ){ + nByte = osReadlink(zIn, zDel, nFull-1); + if( nByte ==(DWORD)-1 ){ + rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "readlink", zIn); + }else{ + if( zDel[0]!='/' ){ + int n; + for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--); + if( nByte+n+1>nFull ){ + rc = SQLITE_CANTOPEN_BKPT; + }else{ + memmove(&zDel[n], zDel, nByte+1); + memcpy(zDel, zIn, n); + nByte += n; + } + } + zDel[nByte] = '\0'; + } + } + + zIn = zDel; + } + } + + assert( rc!=SQLITE_OK || zIn!=zFull || zIn[0]=='/' ); + if( rc==SQLITE_OK && zIn!=zFull ){ + rc = mkFullPathname(zIn, zFull, nFull); + } + if( bLink==0 ) break; + zIn = zFull; + }while( rc==SQLITE_OK ); + + sqlite3_free(zDel); + winSimplifyName(zFull); + return rc; } } - return SQLITE_OK; -#endif +#endif /* __CYGWIN__ */ -#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && !defined(__CYGWIN__) +#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && defined(_WIN32) SimulateIOError( return SQLITE_ERROR ); /* WinCE has no concept of a relative pathname, or so I am told. */ /* WinRT has no way to convert a relative path to an absolute one. */ @@ -52565,7 +53281,8 @@ static int winFullPathnameNoMutex( return SQLITE_OK; #endif -#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__) +#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT +#if defined(_WIN32) /* It's odd to simulate an io-error here, but really this is just ** using the io-error infrastructure to test that SQLite handles this ** function failing. This function could fail if, for example, the @@ -52583,6 +53300,7 @@ static int winFullPathnameNoMutex( sqlite3_data_directory, winGetDirSep(), zRelative); return SQLITE_OK; } +#endif zConverted = winConvertFromUtf8Filename(zRelative); if( zConverted==0 ){ return SQLITE_IOERR_NOMEM_BKPT; @@ -52621,13 +53339,12 @@ static int winFullPathnameNoMutex( return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), "winFullPathname3", zRelative); } - nByte += 3; - zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) ); + zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) + 3*sizeof(zTemp[0]) ); if( zTemp==0 ){ sqlite3_free(zConverted); return SQLITE_IOERR_NOMEM_BKPT; } - nByte = osGetFullPathNameA((char*)zConverted, nByte, zTemp, 0); + nByte = osGetFullPathNameA((char*)zConverted, nByte+3, zTemp, 0); if( nByte==0 ){ sqlite3_free(zConverted); sqlite3_free(zTemp); @@ -52640,7 +53357,26 @@ static int winFullPathnameNoMutex( } #endif if( zOut ){ +#ifdef __CYGWIN__ + if( memcmp(zOut, "\\\\?\\", 4) ){ + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); + }else if( memcmp(zOut+4, "UNC\\", 4) ){ + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+4); + }else{ + char *p = zOut+6; + *p = '\\'; + if( osGetcwd ){ + /* On Cygwin, UNC paths use forward slashes */ + while( *p ){ + if( *p=='\\' ) *p = '/'; + ++p; + } + } + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+6); + } +#else sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); +#endif /* __CYGWIN__ */ sqlite3_free(zOut); return SQLITE_OK; }else{ @@ -52670,25 +53406,8 @@ static int winFullPathname( */ static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){ HANDLE h; -#if defined(__CYGWIN__) - int nFull = pVfs->mxPathname+1; - char *zFull = sqlite3MallocZero( nFull ); - void *zConverted = 0; - if( zFull==0 ){ - OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0)); - return 0; - } - if( winFullPathname(pVfs, zFilename, nFull, zFull)!=SQLITE_OK ){ - sqlite3_free(zFull); - OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0)); - return 0; - } - zConverted = winConvertFromUtf8Filename(zFull); - sqlite3_free(zFull); -#else void *zConverted = winConvertFromUtf8Filename(zFilename); UNUSED_PARAMETER(pVfs); -#endif if( zConverted==0 ){ OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0)); return 0; @@ -53037,7 +53756,7 @@ SQLITE_API int sqlite3_os_init(void){ /* Double-check that the aSyscall[] array has been constructed ** correctly. See ticket [bb3a86e890c8e96ab] */ - assert( ArraySize(aSyscall)==80 ); + assert( ArraySize(aSyscall)==89 ); /* get memory map allocation granularity */ memset(&winSysInfo, 0, sizeof(SYSTEM_INFO)); @@ -53656,13 +54375,13 @@ static int memdbOpen( } if( p==0 ){ MemStore **apNew; - p = sqlite3Malloc( sizeof(*p) + szName + 3 ); + p = sqlite3Malloc( sizeof(*p) + (i64)szName + 3 ); if( p==0 ){ sqlite3_mutex_leave(pVfsMutex); return SQLITE_NOMEM; } apNew = sqlite3Realloc(memdb_g.apMemStore, - sizeof(apNew[0])*(memdb_g.nMemStore+1) ); + sizeof(apNew[0])*(1+(i64)memdb_g.nMemStore) ); if( apNew==0 ){ sqlite3_free(p); sqlite3_mutex_leave(pVfsMutex); @@ -54095,7 +54814,7 @@ SQLITE_PRIVATE int sqlite3MemdbInit(void){ ** no fewer collisions than the no-op *1. */ #define BITVEC_HASH(X) (((X)*1)%BITVEC_NINT) -#define BITVEC_NPTR (BITVEC_USIZE/sizeof(Bitvec *)) +#define BITVEC_NPTR ((u32)(BITVEC_USIZE/sizeof(Bitvec *))) /* @@ -54278,7 +54997,7 @@ SQLITE_PRIVATE void sqlite3BitvecClear(Bitvec *p, u32 i, void *pBuf){ } } if( p->iSize<=BITVEC_NBIT ){ - p->u.aBitmap[i/BITVEC_SZELEM] &= ~(1 << (i&(BITVEC_SZELEM-1))); + p->u.aBitmap[i/BITVEC_SZELEM] &= ~(BITVEC_TELEM)(1<<(i&(BITVEC_SZELEM-1))); }else{ unsigned int j; u32 *aiValues = pBuf; @@ -54329,7 +55048,7 @@ SQLITE_PRIVATE u32 sqlite3BitvecSize(Bitvec *p){ ** individual bits within V. */ #define SETBIT(V,I) V[I>>3] |= (1<<(I&7)) -#define CLEARBIT(V,I) V[I>>3] &= ~(1<<(I&7)) +#define CLEARBIT(V,I) V[I>>3] &= ~(BITVEC_TELEM)(1<<(I&7)) #define TESTBIT(V,I) (V[I>>3]&(1<<(I&7)))!=0 /* @@ -54372,7 +55091,7 @@ SQLITE_PRIVATE int sqlite3BitvecBuiltinTest(int sz, int *aOp){ /* Allocate the Bitvec to be tested and a linear array of ** bits to act as the reference */ pBitvec = sqlite3BitvecCreate( sz ); - pV = sqlite3MallocZero( (sz+7)/8 + 1 ); + pV = sqlite3MallocZero( (7+(i64)sz)/8 + 1 ); pTmpSpace = sqlite3_malloc64(BITVEC_SZ); if( pBitvec==0 || pV==0 || pTmpSpace==0 ) goto bitvec_end; @@ -55613,10 +56332,6 @@ static SQLITE_WSD struct PCacheGlobal { sqlite3_mutex *mutex; /* Mutex for accessing the following: */ PgFreeslot *pFree; /* Free page blocks */ int nFreeSlot; /* Number of unused pcache slots */ - /* The following value requires a mutex to change. We skip the mutex on - ** reading because (1) most platforms read a 32-bit integer atomically and - ** (2) even if an incorrect value is read, no great harm is done since this - ** is really just an optimization. */ int bUnderPressure; /* True if low on PAGECACHE memory */ } pcache1_g; @@ -55664,7 +56379,7 @@ SQLITE_PRIVATE void sqlite3PCacheBufferSetup(void *pBuf, int sz, int n){ pcache1.nReserve = n>90 ? 10 : (n/10 + 1); pcache1.pStart = pBuf; pcache1.pFree = 0; - pcache1.bUnderPressure = 0; + AtomicStore(&pcache1.bUnderPressure,0); while( n-- ){ p = (PgFreeslot*)pBuf; p->pNext = pcache1.pFree; @@ -55732,7 +56447,7 @@ static void *pcache1Alloc(int nByte){ if( p ){ pcache1.pFree = pcache1.pFree->pNext; pcache1.nFreeSlot--; - pcache1.bUnderPressure = pcache1.nFreeSlot<pcache1.nReserve; + AtomicStore(&pcache1.bUnderPressure,pcache1.nFreeSlot<pcache1.nReserve); assert( pcache1.nFreeSlot>=0 ); sqlite3StatusHighwater(SQLITE_STATUS_PAGECACHE_SIZE, nByte); sqlite3StatusUp(SQLITE_STATUS_PAGECACHE_USED, 1); @@ -55771,7 +56486,7 @@ static void pcache1Free(void *p){ pSlot->pNext = pcache1.pFree; pcache1.pFree = pSlot; pcache1.nFreeSlot++; - pcache1.bUnderPressure = pcache1.nFreeSlot<pcache1.nReserve; + AtomicStore(&pcache1.bUnderPressure,pcache1.nFreeSlot<pcache1.nReserve); assert( pcache1.nFreeSlot<=pcache1.nSlot ); sqlite3_mutex_leave(pcache1.mutex); }else{ @@ -55902,7 +56617,7 @@ SQLITE_PRIVATE void sqlite3PageFree(void *p){ */ static int pcache1UnderMemoryPressure(PCache1 *pCache){ if( pcache1.nSlot && (pCache->szPage+pCache->szExtra)<=pcache1.szSlot ){ - return pcache1.bUnderPressure; + return AtomicLoad(&pcache1.bUnderPressure); }else{ return sqlite3HeapNearlyFull(); } @@ -55919,12 +56634,12 @@ static int pcache1UnderMemoryPressure(PCache1 *pCache){ */ static void pcache1ResizeHash(PCache1 *p){ PgHdr1 **apNew; - unsigned int nNew; - unsigned int i; + u64 nNew; + u32 i; assert( sqlite3_mutex_held(p->pGroup->mutex) ); - nNew = p->nHash*2; + nNew = 2*(u64)p->nHash; if( nNew<256 ){ nNew = 256; } @@ -56147,7 +56862,7 @@ static void pcache1Destroy(sqlite3_pcache *p); static sqlite3_pcache *pcache1Create(int szPage, int szExtra, int bPurgeable){ PCache1 *pCache; /* The newly created page cache */ PGroup *pGroup; /* The group the new page cache will belong to */ - int sz; /* Bytes of memory required to allocate the new cache */ + i64 sz; /* Bytes of memory required to allocate the new cache */ assert( (szPage & (szPage-1))==0 && szPage>=512 && szPage<=65536 ); assert( szExtra < 300 ); @@ -58626,7 +59341,7 @@ static void checkPage(PgHdr *pPg){ ** If an error occurs while reading from the journal file, an SQLite ** error code is returned. */ -static int readSuperJournal(sqlite3_file *pJrnl, char *zSuper, u32 nSuper){ +static int readSuperJournal(sqlite3_file *pJrnl, char *zSuper, u64 nSuper){ int rc; /* Return code */ u32 len; /* Length in bytes of super-journal name */ i64 szJ; /* Total size in bytes of journal file pJrnl */ @@ -59181,6 +59896,15 @@ static void pager_unlock(Pager *pPager){ if( pagerUseWal(pPager) ){ assert( !isOpen(pPager->jfd) ); + if( pPager->eState==PAGER_ERROR ){ + /* If an IO error occurs in wal.c while attempting to wrap the wal file, + ** then the Wal object may be holding a write-lock but no read-lock. + ** This call ensures that the write-lock is dropped as well. We cannot + ** have sqlite3WalEndReadTransaction() drop the write-lock, as it once + ** did, because this would break "BEGIN EXCLUSIVE" handling for + ** SQLITE_ENABLE_SETLK_TIMEOUT builds. */ + sqlite3WalEndWriteTransaction(pPager->pWal); + } sqlite3WalEndReadTransaction(pPager->pWal); pPager->eState = PAGER_OPEN; }else if( !pPager->exclusiveMode ){ @@ -59862,12 +60586,12 @@ static int pager_delsuper(Pager *pPager, const char *zSuper){ char *zJournal; /* Pointer to one journal within MJ file */ char *zSuperPtr; /* Space to hold super-journal filename */ char *zFree = 0; /* Free this buffer */ - int nSuperPtr; /* Amount of space allocated to zSuperPtr[] */ + i64 nSuperPtr; /* Amount of space allocated to zSuperPtr[] */ /* Allocate space for both the pJournal and pSuper file descriptors. ** If successful, open the super-journal file for reading. */ - pSuper = (sqlite3_file *)sqlite3MallocZero(pVfs->szOsFile * 2); + pSuper = (sqlite3_file *)sqlite3MallocZero(2 * (i64)pVfs->szOsFile); if( !pSuper ){ rc = SQLITE_NOMEM_BKPT; pJournal = 0; @@ -59885,11 +60609,14 @@ static int pager_delsuper(Pager *pPager, const char *zSuper){ */ rc = sqlite3OsFileSize(pSuper, &nSuperJournal); if( rc!=SQLITE_OK ) goto delsuper_out; - nSuperPtr = pVfs->mxPathname+1; + nSuperPtr = 1 + (i64)pVfs->mxPathname; + assert( nSuperJournal>=0 && nSuperPtr>0 ); zFree = sqlite3Malloc(4 + nSuperJournal + nSuperPtr + 2); if( !zFree ){ rc = SQLITE_NOMEM_BKPT; goto delsuper_out; + }else{ + assert( nSuperJournal<=0x7fffffff ); } zFree[0] = zFree[1] = zFree[2] = zFree[3] = 0; zSuperJournal = &zFree[4]; @@ -60150,7 +60877,7 @@ static int pager_playback(Pager *pPager, int isHot){ ** for pageSize. */ zSuper = pPager->pTmpSpace; - rc = readSuperJournal(pPager->jfd, zSuper, pPager->pVfs->mxPathname+1); + rc = readSuperJournal(pPager->jfd, zSuper, 1+(i64)pPager->pVfs->mxPathname); if( rc==SQLITE_OK && zSuper[0] ){ rc = sqlite3OsAccess(pVfs, zSuper, SQLITE_ACCESS_EXISTS, &res); } @@ -60289,7 +61016,7 @@ end_playback: ** which case it requires 4 0x00 bytes in memory immediately before ** the filename. */ zSuper = &pPager->pTmpSpace[4]; - rc = readSuperJournal(pPager->jfd, zSuper, pPager->pVfs->mxPathname+1); + rc = readSuperJournal(pPager->jfd, zSuper, 1+(i64)pPager->pVfs->mxPathname); testcase( rc!=SQLITE_OK ); } if( rc==SQLITE_OK @@ -62060,6 +62787,7 @@ SQLITE_PRIVATE int sqlite3PagerOpen( const char *zUri = 0; /* URI args to copy */ int nUriByte = 1; /* Number of bytes of URI args at *zUri */ + /* Figure out how much space is required for each journal file-handle ** (there are two of them, the main journal and the sub-journal). */ journalFileSize = ROUND8(sqlite3JournalSize(pVfs)); @@ -62085,8 +62813,8 @@ SQLITE_PRIVATE int sqlite3PagerOpen( */ if( zFilename && zFilename[0] ){ const char *z; - nPathname = pVfs->mxPathname+1; - zPathname = sqlite3DbMallocRaw(0, nPathname*2); + nPathname = pVfs->mxPathname + 1; + zPathname = sqlite3DbMallocRaw(0, 2*(i64)nPathname); if( zPathname==0 ){ return SQLITE_NOMEM_BKPT; } @@ -62173,14 +62901,14 @@ SQLITE_PRIVATE int sqlite3PagerOpen( ROUND8(sizeof(*pPager)) + /* Pager structure */ ROUND8(pcacheSize) + /* PCache object */ ROUND8(pVfs->szOsFile) + /* The main db file */ - journalFileSize * 2 + /* The two journal files */ + (u64)journalFileSize * 2 + /* The two journal files */ SQLITE_PTRSIZE + /* Space to hold a pointer */ 4 + /* Database prefix */ - nPathname + 1 + /* database filename */ - nUriByte + /* query parameters */ - nPathname + 8 + 1 + /* Journal filename */ + (u64)nPathname + 1 + /* database filename */ + (u64)nUriByte + /* query parameters */ + (u64)nPathname + 8 + 1 + /* Journal filename */ #ifndef SQLITE_OMIT_WAL - nPathname + 4 + 1 + /* WAL filename */ + (u64)nPathname + 4 + 1 + /* WAL filename */ #endif 3 /* Terminator */ ); @@ -65635,6 +66363,11 @@ struct WalCkptInfo { /* ** An open write-ahead log file is represented by an instance of the ** following object. +** +** writeLock: +** This is usually set to 1 whenever the WRITER lock is held. However, +** if it is set to 2, then the WRITER lock is held but must be released +** by walHandleException() if a SEH exception is thrown. */ struct Wal { sqlite3_vfs *pVfs; /* The VFS used to create pDbFd */ @@ -65725,9 +66458,13 @@ struct WalIterator { u32 *aPgno; /* Array of page numbers. */ int nEntry; /* Nr. of entries in aPgno[] and aIndex[] */ int iZero; /* Frame number associated with aPgno[0] */ - } aSegment[1]; /* One for every 32KB page in the wal-index */ + } aSegment[FLEXARRAY]; /* One for every 32KB page in the wal-index */ }; +/* Size (in bytes) of a WalIterator object suitable for N or fewer segments */ +#define SZ_WALITERATOR(N) \ + (offsetof(WalIterator,aSegment)*(N)*sizeof(struct WalSegment)) + /* ** Define the parameters of the hash tables in the wal-index file. There ** is a hash-table following every HASHTABLE_NPAGE page numbers in the @@ -65886,7 +66623,7 @@ static SQLITE_NOINLINE int walIndexPageRealloc( /* Enlarge the pWal->apWiData[] array if required */ if( pWal->nWiData<=iPage ){ - sqlite3_int64 nByte = sizeof(u32*)*(iPage+1); + sqlite3_int64 nByte = sizeof(u32*)*(1+(i64)iPage); volatile u32 **apNew; apNew = (volatile u32 **)sqlite3Realloc((void *)pWal->apWiData, nByte); if( !apNew ){ @@ -65995,10 +66732,8 @@ static void walChecksumBytes( s1 = s2 = 0; } - assert( nByte>=8 ); - assert( (nByte&0x00000007)==0 ); - assert( nByte<=65536 ); - assert( nByte%4==0 ); + /* nByte is a multiple of 8 between 8 and 65536 */ + assert( nByte>=8 && (nByte&7)==0 && nByte<=65536 ); if( !nativeCksum ){ do { @@ -67088,8 +67823,7 @@ static int walIteratorInit(Wal *pWal, u32 nBackfill, WalIterator **pp){ /* Allocate space for the WalIterator object. */ nSegment = walFramePage(iLast) + 1; - nByte = sizeof(WalIterator) - + (nSegment-1)*sizeof(struct WalSegment) + nByte = SZ_WALITERATOR(nSegment) + iLast*sizeof(ht_slot); p = (WalIterator *)sqlite3_malloc64(nByte + sizeof(ht_slot) * (iLast>HASHTABLE_NPAGE?HASHTABLE_NPAGE:iLast) @@ -67160,7 +67894,7 @@ static int walEnableBlockingMs(Wal *pWal, int nMs){ static int walEnableBlocking(Wal *pWal){ int res = 0; if( pWal->db ){ - int tmout = pWal->db->busyTimeout; + int tmout = pWal->db->setlkTimeout; if( tmout ){ res = walEnableBlockingMs(pWal, tmout); } @@ -67546,7 +68280,9 @@ static int walHandleException(Wal *pWal){ static const int S = 1; static const int E = (1<<SQLITE_SHM_NLOCK); int ii; - u32 mUnlock = pWal->lockMask & ~( + u32 mUnlock; + if( pWal->writeLock==2 ) pWal->writeLock = 0; + mUnlock = pWal->lockMask & ~( (pWal->readLock<0 ? 0 : (S << WAL_READ_LOCK(pWal->readLock))) | (pWal->writeLock ? (E << WAL_WRITE_LOCK) : 0) | (pWal->ckptLock ? (E << WAL_CKPT_LOCK) : 0) @@ -67818,7 +68554,12 @@ static int walIndexReadHdr(Wal *pWal, int *pChanged){ if( bWriteLock || SQLITE_OK==(rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1)) ){ - pWal->writeLock = 1; + /* If the write-lock was just obtained, set writeLock to 2 instead of + ** the usual 1. This causes walIndexPage() to behave as if the + ** write-lock were held (so that it allocates new pages as required), + ** and walHandleException() to unlock the write-lock if a SEH exception + ** is thrown. */ + if( !bWriteLock ) pWal->writeLock = 2; if( SQLITE_OK==(rc = walIndexPage(pWal, 0, &page0)) ){ badHdr = walIndexTryHdr(pWal, pChanged); if( badHdr ){ @@ -68603,8 +69344,11 @@ SQLITE_PRIVATE int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged){ ** read-lock. */ SQLITE_PRIVATE void sqlite3WalEndReadTransaction(Wal *pWal){ - sqlite3WalEndWriteTransaction(pWal); +#ifndef SQLITE_ENABLE_SETLK_TIMEOUT + assert( pWal->writeLock==0 || pWal->readLock<0 ); +#endif if( pWal->readLock>=0 ){ + sqlite3WalEndWriteTransaction(pWal); walUnlockShared(pWal, WAL_READ_LOCK(pWal->readLock)); pWal->readLock = -1; } @@ -68797,7 +69541,7 @@ SQLITE_PRIVATE int sqlite3WalBeginWriteTransaction(Wal *pWal){ ** read-transaction was even opened, making this call a no-op. ** Return early. */ if( pWal->writeLock ){ - assert( !memcmp(&pWal->hdr,(void *)walIndexHdr(pWal),sizeof(WalIndexHdr)) ); + assert( !memcmp(&pWal->hdr,(void*)pWal->apWiData[0],sizeof(WalIndexHdr)) ); return SQLITE_OK; } #endif @@ -70246,6 +70990,12 @@ struct CellInfo { */ #define BTCURSOR_MAX_DEPTH 20 +/* +** Maximum amount of storage local to a database page, regardless of +** page size. +*/ +#define BT_MAX_LOCAL 65501 /* 65536 - 35 */ + /* ** A cursor is a pointer to a particular entry within a particular ** b-tree within a database file. @@ -70654,7 +71404,7 @@ SQLITE_PRIVATE int sqlite3BtreeHoldsMutex(Btree *p){ */ static void SQLITE_NOINLINE btreeEnterAll(sqlite3 *db){ int i; - int skipOk = 1; + u8 skipOk = 1; Btree *p; assert( sqlite3_mutex_held(db->mutex) ); for(i=0; i<db->nDb; i++){ @@ -71510,7 +72260,7 @@ static int saveCursorKey(BtCursor *pCur){ ** below. */ void *pKey; pCur->nKey = sqlite3BtreePayloadSize(pCur); - pKey = sqlite3Malloc( pCur->nKey + 9 + 8 ); + pKey = sqlite3Malloc( ((i64)pCur->nKey) + 9 + 8 ); if( pKey ){ rc = sqlite3BtreePayload(pCur, 0, (int)pCur->nKey, pKey); if( rc==SQLITE_OK ){ @@ -71800,7 +72550,7 @@ SQLITE_PRIVATE void sqlite3BtreeCursorHint(BtCursor *pCur, int eHintType, ...){ */ SQLITE_PRIVATE void sqlite3BtreeCursorHintFlags(BtCursor *pCur, unsigned x){ assert( x==BTREE_SEEK_EQ || x==BTREE_BULKLOAD || x==0 ); - pCur->hints = x; + pCur->hints = (u8)x; } @@ -71994,14 +72744,15 @@ static SQLITE_NOINLINE void btreeParseCellAdjustSizeForOverflow( static int btreePayloadToLocal(MemPage *pPage, i64 nPayload){ int maxLocal; /* Maximum amount of payload held locally */ maxLocal = pPage->maxLocal; + assert( nPayload>=0 ); if( nPayload<=maxLocal ){ - return nPayload; + return (int)nPayload; }else{ int minLocal; /* Minimum amount of payload held locally */ int surplus; /* Overflow payload available for local storage */ minLocal = pPage->minLocal; - surplus = minLocal + (nPayload - minLocal)%(pPage->pBt->usableSize-4); - return ( surplus <= maxLocal ) ? surplus : minLocal; + surplus = (int)(minLocal +(nPayload - minLocal)%(pPage->pBt->usableSize-4)); + return (surplus <= maxLocal) ? surplus : minLocal; } } @@ -72111,11 +72862,13 @@ static void btreeParseCellPtr( pInfo->pPayload = pIter; testcase( nPayload==pPage->maxLocal ); testcase( nPayload==(u32)pPage->maxLocal+1 ); + assert( nPayload>=0 ); + assert( pPage->maxLocal <= BT_MAX_LOCAL ); if( nPayload<=pPage->maxLocal ){ /* This is the (easy) common case where the entire payload fits ** on the local page. No overflow is required. */ - pInfo->nSize = nPayload + (u16)(pIter - pCell); + pInfo->nSize = (u16)nPayload + (u16)(pIter - pCell); if( pInfo->nSize<4 ) pInfo->nSize = 4; pInfo->nLocal = (u16)nPayload; }else{ @@ -72148,11 +72901,13 @@ static void btreeParseCellPtrIndex( pInfo->pPayload = pIter; testcase( nPayload==pPage->maxLocal ); testcase( nPayload==(u32)pPage->maxLocal+1 ); + assert( nPayload>=0 ); + assert( pPage->maxLocal <= BT_MAX_LOCAL ); if( nPayload<=pPage->maxLocal ){ /* This is the (easy) common case where the entire payload fits ** on the local page. No overflow is required. */ - pInfo->nSize = nPayload + (u16)(pIter - pCell); + pInfo->nSize = (u16)nPayload + (u16)(pIter - pCell); if( pInfo->nSize<4 ) pInfo->nSize = 4; pInfo->nLocal = (u16)nPayload; }else{ @@ -72691,14 +73446,14 @@ static SQLITE_INLINE int allocateSpace(MemPage *pPage, int nByte, int *pIdx){ ** at the end of the page. So do additional corruption checks inside this ** routine and return SQLITE_CORRUPT if any problems are found. */ -static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){ - u16 iPtr; /* Address of ptr to next freeblock */ - u16 iFreeBlk; /* Address of the next freeblock */ +static int freeSpace(MemPage *pPage, int iStart, int iSize){ + int iPtr; /* Address of ptr to next freeblock */ + int iFreeBlk; /* Address of the next freeblock */ u8 hdr; /* Page header size. 0 or 100 */ - u8 nFrag = 0; /* Reduction in fragmentation */ - u16 iOrigSize = iSize; /* Original value of iSize */ - u16 x; /* Offset to cell content area */ - u32 iEnd = iStart + iSize; /* First byte past the iStart buffer */ + int nFrag = 0; /* Reduction in fragmentation */ + int iOrigSize = iSize; /* Original value of iSize */ + int x; /* Offset to cell content area */ + int iEnd = iStart + iSize; /* First byte past the iStart buffer */ unsigned char *data = pPage->aData; /* Page content */ u8 *pTmp; /* Temporary ptr into data[] */ @@ -72725,7 +73480,7 @@ static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){ } iPtr = iFreeBlk; } - if( iFreeBlk>pPage->pBt->usableSize-4 ){ /* TH3: corrupt081.100 */ + if( iFreeBlk>(int)pPage->pBt->usableSize-4 ){ /* TH3: corrupt081.100 */ return SQLITE_CORRUPT_PAGE(pPage); } assert( iFreeBlk>iPtr || iFreeBlk==0 || CORRUPT_DB ); @@ -72740,7 +73495,7 @@ static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){ nFrag = iFreeBlk - iEnd; if( iEnd>iFreeBlk ) return SQLITE_CORRUPT_PAGE(pPage); iEnd = iFreeBlk + get2byte(&data[iFreeBlk+2]); - if( iEnd > pPage->pBt->usableSize ){ + if( iEnd > (int)pPage->pBt->usableSize ){ return SQLITE_CORRUPT_PAGE(pPage); } iSize = iEnd - iStart; @@ -72761,7 +73516,7 @@ static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){ } } if( nFrag>data[hdr+7] ) return SQLITE_CORRUPT_PAGE(pPage); - data[hdr+7] -= nFrag; + data[hdr+7] -= (u8)nFrag; } pTmp = &data[hdr+5]; x = get2byte(pTmp); @@ -72782,7 +73537,8 @@ static int freeSpace(MemPage *pPage, u16 iStart, u16 iSize){ /* Insert the new freeblock into the freelist */ put2byte(&data[iPtr], iStart); put2byte(&data[iStart], iFreeBlk); - put2byte(&data[iStart+2], iSize); + assert( iSize>=0 && iSize<=0xffff ); + put2byte(&data[iStart+2], (u16)iSize); } pPage->nFree += iOrigSize; return SQLITE_OK; @@ -73008,7 +73764,7 @@ static int btreeInitPage(MemPage *pPage){ assert( pBt->pageSize>=512 && pBt->pageSize<=65536 ); pPage->maskPage = (u16)(pBt->pageSize - 1); pPage->nOverflow = 0; - pPage->cellOffset = pPage->hdrOffset + 8 + pPage->childPtrSize; + pPage->cellOffset = (u16)(pPage->hdrOffset + 8 + pPage->childPtrSize); pPage->aCellIdx = data + pPage->childPtrSize + 8; pPage->aDataEnd = pPage->aData + pBt->pageSize; pPage->aDataOfst = pPage->aData + pPage->childPtrSize; @@ -73042,8 +73798,8 @@ static int btreeInitPage(MemPage *pPage){ static void zeroPage(MemPage *pPage, int flags){ unsigned char *data = pPage->aData; BtShared *pBt = pPage->pBt; - u8 hdr = pPage->hdrOffset; - u16 first; + int hdr = pPage->hdrOffset; + int first; assert( sqlite3PagerPagenumber(pPage->pDbPage)==pPage->pgno || CORRUPT_DB ); assert( sqlite3PagerGetExtra(pPage->pDbPage) == (void*)pPage ); @@ -73060,7 +73816,7 @@ static void zeroPage(MemPage *pPage, int flags){ put2byte(&data[hdr+5], pBt->usableSize); pPage->nFree = (u16)(pBt->usableSize - first); decodeFlags(pPage, flags); - pPage->cellOffset = first; + pPage->cellOffset = (u16)first; pPage->aDataEnd = &data[pBt->pageSize]; pPage->aCellIdx = &data[first]; pPage->aDataOfst = &data[pPage->childPtrSize]; @@ -73846,7 +74602,7 @@ SQLITE_PRIVATE int sqlite3BtreeSetPageSize(Btree *p, int pageSize, int nReserve, BtShared *pBt = p->pBt; assert( nReserve>=0 && nReserve<=255 ); sqlite3BtreeEnter(p); - pBt->nReserveWanted = nReserve; + pBt->nReserveWanted = (u8)nReserve; x = pBt->pageSize - pBt->usableSize; if( nReserve<x ) nReserve = x; if( pBt->btsFlags & BTS_PAGESIZE_FIXED ){ @@ -73952,7 +74708,7 @@ SQLITE_PRIVATE int sqlite3BtreeSecureDelete(Btree *p, int newFlag){ assert( BTS_FAST_SECURE==(BTS_OVERWRITE|BTS_SECURE_DELETE) ); if( newFlag>=0 ){ p->pBt->btsFlags &= ~BTS_FAST_SECURE; - p->pBt->btsFlags |= BTS_SECURE_DELETE*newFlag; + p->pBt->btsFlags |= (u16)(BTS_SECURE_DELETE*newFlag); } b = (p->pBt->btsFlags & BTS_FAST_SECURE)/BTS_SECURE_DELETE; sqlite3BtreeLeave(p); @@ -76881,7 +77637,7 @@ bypass_moveto_root: rc = SQLITE_CORRUPT_PAGE(pPage); goto moveto_index_finish; } - pCellKey = sqlite3Malloc( nCell+nOverrun ); + pCellKey = sqlite3Malloc( (u64)nCell+(u64)nOverrun ); if( pCellKey==0 ){ rc = SQLITE_NOMEM_BKPT; goto moveto_index_finish; @@ -78400,7 +79156,8 @@ static int rebuildPage( } /* The pPg->nFree field is now set incorrectly. The caller will fix it. */ - pPg->nCell = nCell; + assert( nCell < 10922 ); + pPg->nCell = (u16)nCell; pPg->nOverflow = 0; put2byte(&aData[hdr+1], 0); @@ -78647,9 +79404,13 @@ static int editPage( if( pageInsertArray( pPg, pBegin, &pData, pCellptr, iNew+nCell, nNew-nCell, pCArray - ) ) goto editpage_fail; + ) + ){ + goto editpage_fail; + } - pPg->nCell = nNew; + assert( nNew < 10922 ); + pPg->nCell = (u16)nNew; pPg->nOverflow = 0; put2byte(&aData[hdr+3], pPg->nCell); @@ -78958,7 +79719,7 @@ static int balance_nonroot( int pageFlags; /* Value of pPage->aData[0] */ int iSpace1 = 0; /* First unused byte of aSpace1[] */ int iOvflSpace = 0; /* First unused byte of aOvflSpace[] */ - int szScratch; /* Size of scratch memory requested */ + u64 szScratch; /* Size of scratch memory requested */ MemPage *apOld[NB]; /* pPage and up to two siblings */ MemPage *apNew[NB+2]; /* pPage and up to NB siblings after balancing */ u8 *pRight; /* Location in parent of right-sibling pointer */ @@ -80243,7 +81004,7 @@ SQLITE_PRIVATE int sqlite3BtreeInsert( if( pCur->info.nKey==pX->nKey ){ BtreePayload x2; x2.pData = pX->pKey; - x2.nData = pX->nKey; + x2.nData = (int)pX->nKey; assert( pX->nKey<=0x7fffffff ); x2.nZero = 0; return btreeOverwriteCell(pCur, &x2); } @@ -80424,7 +81185,7 @@ SQLITE_PRIVATE int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 getCellInfo(pSrc); if( pSrc->info.nPayload<0x80 ){ - *(aOut++) = pSrc->info.nPayload; + *(aOut++) = (u8)pSrc->info.nPayload; }else{ aOut += sqlite3PutVarint(aOut, pSrc->info.nPayload); } @@ -80437,7 +81198,7 @@ SQLITE_PRIVATE int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 nRem = pSrc->info.nPayload; if( nIn==nRem && nIn<pDest->pPage->maxLocal ){ memcpy(aOut, aIn, nIn); - pBt->nPreformatSize = nIn + (aOut - pBt->pTmpSpace); + pBt->nPreformatSize = nIn + (int)(aOut - pBt->pTmpSpace); return SQLITE_OK; }else{ int rc = SQLITE_OK; @@ -80449,7 +81210,7 @@ SQLITE_PRIVATE int sqlite3BtreeTransferRow(BtCursor *pDest, BtCursor *pSrc, i64 u32 nOut; /* Size of output buffer aOut[] */ nOut = btreePayloadToLocal(pDest->pPage, pSrc->info.nPayload); - pBt->nPreformatSize = nOut + (aOut - pBt->pTmpSpace); + pBt->nPreformatSize = (int)nOut + (int)(aOut - pBt->pTmpSpace); if( nOut<pSrc->info.nPayload ){ pPgnoOut = &aOut[nOut]; pBt->nPreformatSize += 4; @@ -82070,6 +82831,7 @@ SQLITE_PRIVATE int sqlite3BtreeIsInBackup(Btree *p){ */ SQLITE_PRIVATE void *sqlite3BtreeSchema(Btree *p, int nBytes, void(*xFree)(void *)){ BtShared *pBt = p->pBt; + assert( nBytes==0 || nBytes==sizeof(Schema) ); sqlite3BtreeEnter(p); if( !pBt->pSchema && nBytes ){ pBt->pSchema = sqlite3DbMallocZero(0, nBytes); @@ -83186,7 +83948,7 @@ static void vdbeMemRenderNum(int sz, char *zBuf, Mem *p){ ** corresponding string value, then it is important that the string be ** derived from the numeric value, not the other way around, to ensure ** that the index and table are consistent. See ticket -** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for +** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for ** an example. ** ** This routine looks at pMem to verify that if it has both a numeric @@ -83372,7 +84134,7 @@ SQLITE_PRIVATE void sqlite3VdbeMemZeroTerminateIfAble(Mem *pMem){ return; } if( pMem->enc!=SQLITE_UTF8 ) return; - if( NEVER(pMem->z==0) ) return; + assert( pMem->z!=0 ); if( pMem->flags & MEM_Dyn ){ if( pMem->xDel==sqlite3_free && sqlite3_msize(pMem->z) >= (u64)(pMem->n+1) @@ -84485,7 +85247,7 @@ static sqlite3_value *valueNew(sqlite3 *db, struct ValueNewStat4Ctx *p){ if( pRec==0 ){ Index *pIdx = p->pIdx; /* Index being probed */ - int nByte; /* Bytes of space to allocate */ + i64 nByte; /* Bytes of space to allocate */ int i; /* Counter variable */ int nCol = pIdx->nColumn; /* Number of index columns including rowid */ @@ -84551,7 +85313,7 @@ static int valueFromFunction( ){ sqlite3_context ctx; /* Context object for function invocation */ sqlite3_value **apVal = 0; /* Function arguments */ - int nVal = 0; /* Size of apVal[] array */ + int nVal = 0; /* Number of function arguments */ FuncDef *pFunc = 0; /* Function definition */ sqlite3_value *pVal = 0; /* New value */ int rc = SQLITE_OK; /* Return code */ @@ -85549,12 +86311,10 @@ SQLITE_PRIVATE int sqlite3VdbeAddFunctionCall( int eCallCtx /* Calling context */ ){ Vdbe *v = pParse->pVdbe; - int nByte; int addr; sqlite3_context *pCtx; assert( v ); - nByte = sizeof(*pCtx) + (nArg-1)*sizeof(sqlite3_value*); - pCtx = sqlite3DbMallocRawNN(pParse->db, nByte); + pCtx = sqlite3DbMallocRawNN(pParse->db, SZ_CONTEXT(nArg)); if( pCtx==0 ){ assert( pParse->db->mallocFailed ); freeEphemeralFunction(pParse->db, (FuncDef*)pFunc); @@ -85830,7 +86590,7 @@ static Op *opIterNext(VdbeOpIter *p){ } if( pRet->p4type==P4_SUBPROGRAM ){ - int nByte = (p->nSub+1)*sizeof(SubProgram*); + i64 nByte = (1+(u64)p->nSub)*sizeof(SubProgram*); int j; for(j=0; j<p->nSub; j++){ if( p->apSub[j]==pRet->p4.pProgram ) break; @@ -85960,8 +86720,8 @@ SQLITE_PRIVATE void sqlite3VdbeAssertAbortable(Vdbe *p){ ** (1) For each jump instruction with a negative P2 value (a label) ** resolve the P2 value to an actual address. ** -** (2) Compute the maximum number of arguments used by any SQL function -** and store that value in *pMaxFuncArgs. +** (2) Compute the maximum number of arguments used by the xUpdate/xFilter +** methods of any virtual table and store that value in *pMaxVtabArgs. ** ** (3) Update the Vdbe.readOnly and Vdbe.bIsReader flags to accurately ** indicate what the prepared statement actually does. @@ -85974,8 +86734,8 @@ SQLITE_PRIVATE void sqlite3VdbeAssertAbortable(Vdbe *p){ ** script numbers the opcodes correctly. Changes to this routine must be ** coordinated with changes to mkopcodeh.tcl. */ -static void resolveP2Values(Vdbe *p, int *pMaxFuncArgs){ - int nMaxArgs = *pMaxFuncArgs; +static void resolveP2Values(Vdbe *p, int *pMaxVtabArgs){ + int nMaxVtabArgs = *pMaxVtabArgs; Op *pOp; Parse *pParse = p->pParse; int *aLabel = pParse->aLabel; @@ -86020,15 +86780,19 @@ static void resolveP2Values(Vdbe *p, int *pMaxFuncArgs){ } #ifndef SQLITE_OMIT_VIRTUALTABLE case OP_VUpdate: { - if( pOp->p2>nMaxArgs ) nMaxArgs = pOp->p2; + if( pOp->p2>nMaxVtabArgs ) nMaxVtabArgs = pOp->p2; break; } case OP_VFilter: { int n; + /* The instruction immediately prior to VFilter will be an + ** OP_Integer that sets the "argc" value for the VFilter. See + ** the code where OP_VFilter is generated at tag-20250207a. */ assert( (pOp - p->aOp) >= 3 ); assert( pOp[-1].opcode==OP_Integer ); + assert( pOp[-1].p2==pOp->p3+1 ); n = pOp[-1].p1; - if( n>nMaxArgs ) nMaxArgs = n; + if( n>nMaxVtabArgs ) nMaxVtabArgs = n; /* Fall through into the default case */ /* no break */ deliberate_fall_through } @@ -86069,7 +86833,7 @@ resolve_p2_values_loop_exit: pParse->aLabel = 0; } pParse->nLabel = 0; - *pMaxFuncArgs = nMaxArgs; + *pMaxVtabArgs = nMaxVtabArgs; assert( p->bIsReader!=0 || DbMaskAllZero(p->btreeMask) ); } @@ -86298,7 +87062,7 @@ SQLITE_PRIVATE void sqlite3VdbeScanStatus( const char *zName /* Name of table or index being scanned */ ){ if( IS_STMT_SCANSTATUS(p->db) ){ - sqlite3_int64 nByte = (p->nScan+1) * sizeof(ScanStatus); + i64 nByte = (1+(i64)p->nScan) * sizeof(ScanStatus); ScanStatus *aNew; aNew = (ScanStatus*)sqlite3DbRealloc(p->db, p->aScan, nByte); if( aNew ){ @@ -86408,6 +87172,9 @@ SQLITE_PRIVATE void sqlite3VdbeChangeP5(Vdbe *p, u16 p5){ */ SQLITE_PRIVATE void sqlite3VdbeTypeofColumn(Vdbe *p, int iDest){ VdbeOp *pOp = sqlite3VdbeGetLastOp(p); +#ifdef SQLITE_DEBUG + while( pOp->opcode==OP_ReleaseReg ) pOp--; +#endif if( pOp->p3==iDest && pOp->opcode==OP_Column ){ pOp->p5 |= OPFLAG_TYPEOFARG; } @@ -87747,7 +88514,7 @@ SQLITE_PRIVATE void sqlite3VdbeMakeReady( int nVar; /* Number of parameters */ int nMem; /* Number of VM memory registers */ int nCursor; /* Number of cursors required */ - int nArg; /* Number of arguments in subprograms */ + int nArg; /* Max number args to xFilter or xUpdate */ int n; /* Loop counter */ struct ReusableSpace x; /* Reusable bulk memory */ @@ -87819,6 +88586,9 @@ SQLITE_PRIVATE void sqlite3VdbeMakeReady( p->apCsr = allocSpace(&x, p->apCsr, nCursor*sizeof(VdbeCursor*)); } } +#ifdef SQLITE_DEBUG + p->napArg = nArg; +#endif if( db->mallocFailed ){ p->nVar = 0; @@ -89316,6 +90086,7 @@ SQLITE_PRIVATE UnpackedRecord *sqlite3VdbeAllocUnpackedRecord( ){ UnpackedRecord *p; /* Unpacked record to return */ int nByte; /* Number of bytes required for *p */ + assert( sizeof(UnpackedRecord) + sizeof(Mem)*65536 < 0x7fffffff ); nByte = ROUND8P(sizeof(UnpackedRecord)) + sizeof(Mem)*(pKeyInfo->nKeyField+1); p = (UnpackedRecord *)sqlite3DbMallocRaw(pKeyInfo->db, nByte); if( !p ) return 0; @@ -90622,10 +91393,11 @@ SQLITE_PRIVATE void sqlite3VdbePreUpdateHook( preupdate.pCsr = pCsr; preupdate.op = op; preupdate.iNewReg = iReg; - preupdate.keyinfo.db = db; - preupdate.keyinfo.enc = ENC(db); - preupdate.keyinfo.nKeyField = pTab->nCol; - preupdate.keyinfo.aSortFlags = (u8*)&fakeSortOrder; + preupdate.pKeyinfo = (KeyInfo*)&preupdate.keyinfoSpace; + preupdate.pKeyinfo->db = db; + preupdate.pKeyinfo->enc = ENC(db); + preupdate.pKeyinfo->nKeyField = pTab->nCol; + preupdate.pKeyinfo->aSortFlags = (u8*)&fakeSortOrder; preupdate.iKey1 = iKey1; preupdate.iKey2 = iKey2; preupdate.pTab = pTab; @@ -90635,8 +91407,8 @@ SQLITE_PRIVATE void sqlite3VdbePreUpdateHook( db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2); db->pPreUpdate = 0; sqlite3DbFree(db, preupdate.aRecord); - vdbeFreeUnpacked(db, preupdate.keyinfo.nKeyField+1, preupdate.pUnpacked); - vdbeFreeUnpacked(db, preupdate.keyinfo.nKeyField+1, preupdate.pNewUnpacked); + vdbeFreeUnpacked(db, preupdate.pKeyinfo->nKeyField+1,preupdate.pUnpacked); + vdbeFreeUnpacked(db, preupdate.pKeyinfo->nKeyField+1,preupdate.pNewUnpacked); sqlite3VdbeMemRelease(&preupdate.oldipk); if( preupdate.aNew ){ int i; @@ -92467,7 +93239,7 @@ SQLITE_API int sqlite3_bind_text64( assert( xDel!=SQLITE_DYNAMIC ); if( enc!=SQLITE_UTF8 ){ if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; - nData &= ~(u16)1; + nData &= ~(u64)1; } return bindText(pStmt, i, zData, nData, xDel, enc); } @@ -92875,7 +93647,7 @@ SQLITE_API int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppVa if( !aRec ) goto preupdate_old_out; rc = sqlite3BtreePayload(p->pCsr->uc.pCursor, 0, nRec, aRec); if( rc==SQLITE_OK ){ - p->pUnpacked = vdbeUnpackRecord(&p->keyinfo, nRec, aRec); + p->pUnpacked = vdbeUnpackRecord(p->pKeyinfo, nRec, aRec); if( !p->pUnpacked ) rc = SQLITE_NOMEM; } if( rc!=SQLITE_OK ){ @@ -92892,7 +93664,9 @@ SQLITE_API int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppVa Column *pCol = &p->pTab->aCol[iIdx]; if( pCol->iDflt>0 ){ if( p->apDflt==0 ){ - int nByte = sizeof(sqlite3_value*)*p->pTab->nCol; + int nByte; + assert( sizeof(sqlite3_value*)*UMXV(p->pTab->nCol) < 0x7fffffff ); + nByte = sizeof(sqlite3_value*)*p->pTab->nCol; p->apDflt = (sqlite3_value**)sqlite3DbMallocZero(db, nByte); if( p->apDflt==0 ) goto preupdate_old_out; } @@ -92938,7 +93712,7 @@ SQLITE_API int sqlite3_preupdate_count(sqlite3 *db){ #else p = db->pPreUpdate; #endif - return (p ? p->keyinfo.nKeyField : 0); + return (p ? p->pKeyinfo->nKeyField : 0); } #endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ @@ -93021,7 +93795,7 @@ SQLITE_API int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppVa Mem *pData = &p->v->aMem[p->iNewReg]; rc = ExpandBlob(pData); if( rc!=SQLITE_OK ) goto preupdate_new_out; - pUnpack = vdbeUnpackRecord(&p->keyinfo, pData->n, pData->z); + pUnpack = vdbeUnpackRecord(p->pKeyinfo, pData->n, pData->z); if( !pUnpack ){ rc = SQLITE_NOMEM; goto preupdate_new_out; @@ -93042,7 +93816,8 @@ SQLITE_API int sqlite3_preupdate_new(sqlite3 *db, int iIdx, sqlite3_value **ppVa */ assert( p->op==SQLITE_UPDATE ); if( !p->aNew ){ - p->aNew = (Mem *)sqlite3DbMallocZero(db, sizeof(Mem) * p->pCsr->nField); + assert( sizeof(Mem)*UMXV(p->pCsr->nField) < 0x7fffffff ); + p->aNew = (Mem *)sqlite3DbMallocZero(db, sizeof(Mem)*p->pCsr->nField); if( !p->aNew ){ rc = SQLITE_NOMEM; goto preupdate_new_out; @@ -93812,11 +94587,11 @@ static VdbeCursor *allocateCursor( */ Mem *pMem = iCur>0 ? &p->aMem[p->nMem-iCur] : p->aMem; - int nByte; + i64 nByte; VdbeCursor *pCx = 0; - nByte = - ROUND8P(sizeof(VdbeCursor)) + 2*sizeof(u32)*nField + - (eCurType==CURTYPE_BTREE?sqlite3BtreeCursorSize():0); + nByte = SZ_VDBECURSOR(nField); + assert( ROUND8(nByte)==nByte ); + if( eCurType==CURTYPE_BTREE ) nByte += sqlite3BtreeCursorSize(); assert( iCur>=0 && iCur<p->nCursor ); if( p->apCsr[iCur] ){ /*OPTIMIZATION-IF-FALSE*/ @@ -93840,7 +94615,7 @@ static VdbeCursor *allocateCursor( pMem->szMalloc = 0; return 0; } - pMem->szMalloc = nByte; + pMem->szMalloc = (int)nByte; } p->apCsr[iCur] = pCx = (VdbeCursor*)pMem->zMalloc; @@ -93849,8 +94624,8 @@ static VdbeCursor *allocateCursor( pCx->nField = nField; pCx->aOffset = &pCx->aType[nField]; if( eCurType==CURTYPE_BTREE ){ - pCx->uc.pCursor = (BtCursor*) - &pMem->z[ROUND8P(sizeof(VdbeCursor))+2*sizeof(u32)*nField]; + assert( ROUND8(SZ_VDBECURSOR(nField))==SZ_VDBECURSOR(nField) ); + pCx->uc.pCursor = (BtCursor*)&pMem->z[SZ_VDBECURSOR(nField)]; sqlite3BtreeCursorZero(pCx->uc.pCursor); } return pCx; @@ -94854,7 +95629,7 @@ case OP_Halt: { sqlite3VdbeError(p, "%s", pOp->p4.z); } pcx = (int)(pOp - aOp); - sqlite3_log(pOp->p1, "abort at %d in [%s]: %s", pcx, p->zSql, p->zErrMsg); + sqlite3_log(pOp->p1, "abort at %d: %s; [%s]", pcx, p->zErrMsg, p->zSql); } rc = sqlite3VdbeHalt(p); assert( rc==SQLITE_BUSY || rc==SQLITE_OK || rc==SQLITE_ERROR ); @@ -96180,7 +96955,7 @@ case OP_BitNot: { /* same as TK_BITNOT, in1, out2 */ break; } -/* Opcode: Once P1 P2 * * * +/* Opcode: Once P1 P2 P3 * * ** ** Fall through to the next instruction the first time this opcode is ** encountered on each invocation of the byte-code program. Jump to P2 @@ -96196,6 +96971,12 @@ case OP_BitNot: { /* same as TK_BITNOT, in1, out2 */ ** whether or not the jump should be taken. The bitmask is necessary ** because the self-altering code trick does not work for recursive ** triggers. +** +** The P3 operand is not used directly by this opcode. However P3 is +** used by the code generator as follows: If this opcode is the start +** of a subroutine and that subroutine uses a Bloom filter, then P3 will +** be the register that holds that Bloom filter. See tag-202407032019 +** in the source code for implementation details. */ case OP_Once: { /* jump */ u32 iAddr; /* Address of this instruction */ @@ -97241,6 +98022,7 @@ case OP_MakeRecord: { zHdr += sqlite3PutVarint(zHdr, serial_type); if( pRec->n ){ assert( pRec->z!=0 ); + assert( pRec->z!=(const char*)sqlite3CtypeMap ); memcpy(zPayload, pRec->z, pRec->n); zPayload += pRec->n; } @@ -99592,7 +100374,7 @@ case OP_RowData: { /* The OP_RowData opcodes always follow OP_NotExists or ** OP_SeekRowid or OP_Rewind/Op_Next with no intervening instructions ** that might invalidate the cursor. - ** If this where not the case, on of the following assert()s + ** If this were not the case, one of the following assert()s ** would fail. Should this ever change (because of changes in the code ** generator) then the fix would be to insert a call to ** sqlite3VdbeCursorMoveto(). @@ -100861,7 +101643,7 @@ case OP_RowSetTest: { /* jump, in1, in3 */ */ case OP_Program: { /* jump0 */ int nMem; /* Number of memory registers for sub-program */ - int nByte; /* Bytes of runtime space required for sub-program */ + i64 nByte; /* Bytes of runtime space required for sub-program */ Mem *pRt; /* Register to allocate runtime space */ Mem *pMem; /* Used to iterate through memory cells */ Mem *pEnd; /* Last memory cell in new array */ @@ -100912,7 +101694,7 @@ case OP_Program: { /* jump0 */ nByte = ROUND8(sizeof(VdbeFrame)) + nMem * sizeof(Mem) + pProgram->nCsr * sizeof(VdbeCursor*) - + (pProgram->nOp + 7)/8; + + (7 + (i64)pProgram->nOp)/8; pFrame = sqlite3DbMallocZero(db, nByte); if( !pFrame ){ goto no_mem; @@ -100920,7 +101702,7 @@ case OP_Program: { /* jump0 */ sqlite3VdbeMemRelease(pRt); pRt->flags = MEM_Blob|MEM_Dyn; pRt->z = (char*)pFrame; - pRt->n = nByte; + pRt->n = (int)nByte; pRt->xDel = sqlite3VdbeFrameMemDel; pFrame->v = p; @@ -101019,12 +101801,14 @@ case OP_Param: { /* out2 */ ** statement counter is incremented (immediate foreign key constraints). */ case OP_FkCounter: { - if( db->flags & SQLITE_DeferFKs ){ - db->nDeferredImmCons += pOp->p2; - }else if( pOp->p1 ){ + if( pOp->p1 ){ db->nDeferredCons += pOp->p2; }else{ - p->nFkConstraint += pOp->p2; + if( db->flags & SQLITE_DeferFKs ){ + db->nDeferredImmCons += pOp->p2; + }else{ + p->nFkConstraint += pOp->p2; + } } break; } @@ -101239,7 +102023,7 @@ case OP_AggStep: { ** ** Note: We could avoid this by using a regular memory cell from aMem[] for ** the accumulator, instead of allocating one here. */ - nAlloc = ROUND8P( sizeof(pCtx[0]) + (n-1)*sizeof(sqlite3_value*) ); + nAlloc = ROUND8P( SZ_CONTEXT(n) ); pCtx = sqlite3DbMallocRawNN(db, nAlloc + sizeof(Mem)); if( pCtx==0 ) goto no_mem; pCtx->pOut = (Mem*)((u8*)pCtx + nAlloc); @@ -101899,6 +102683,7 @@ case OP_VFilter: { /* jump, ncycle */ /* Invoke the xFilter method */ apArg = p->apArg; + assert( nArg<=p->napArg ); for(i = 0; i<nArg; i++){ apArg[i] = &pArgc[i+1]; } @@ -102109,6 +102894,7 @@ case OP_VUpdate: { u8 vtabOnConflict = db->vtabOnConflict; apArg = p->apArg; pX = &aMem[pOp->p3]; + assert( nArg<=p->napArg ); for(i=0; i<nArg; i++){ assert( memIsValid(pX) ); memAboutToChange(p, pX); @@ -102685,8 +103471,8 @@ abort_due_to_error: p->rc = rc; sqlite3SystemError(db, rc); testcase( sqlite3GlobalConfig.xLog!=0 ); - sqlite3_log(rc, "statement aborts at %d: [%s] %s", - (int)(pOp - aOp), p->zSql, p->zErrMsg); + sqlite3_log(rc, "statement aborts at %d: %s; [%s]", + (int)(pOp - aOp), p->zErrMsg, p->zSql); if( p->eVdbeState==VDBE_RUN_STATE ) sqlite3VdbeHalt(p); if( rc==SQLITE_IOERR_NOMEM ) sqlite3OomFault(db); if( rc==SQLITE_CORRUPT && db->autoCommit==0 ){ @@ -102895,6 +103681,7 @@ SQLITE_API int sqlite3_blob_open( char *zErr = 0; Table *pTab; Incrblob *pBlob = 0; + int iDb; Parse sParse; #ifdef SQLITE_ENABLE_API_ARMOR @@ -102940,7 +103727,10 @@ SQLITE_API int sqlite3_blob_open( sqlite3ErrorMsg(&sParse, "cannot open view: %s", zTable); } #endif - if( !pTab ){ + if( pTab==0 + || ((iDb = sqlite3SchemaToIndex(db, pTab->pSchema))==1 && + sqlite3OpenTempDatabase(&sParse)) + ){ if( sParse.zErrMsg ){ sqlite3DbFree(db, zErr); zErr = sParse.zErrMsg; @@ -102951,15 +103741,11 @@ SQLITE_API int sqlite3_blob_open( goto blob_open_out; } pBlob->pTab = pTab; - pBlob->zDb = db->aDb[sqlite3SchemaToIndex(db, pTab->pSchema)].zDbSName; + pBlob->zDb = db->aDb[iDb].zDbSName; /* Now search pTab for the exact column. */ - for(iCol=0; iCol<pTab->nCol; iCol++) { - if( sqlite3StrICmp(pTab->aCol[iCol].zCnName, zColumn)==0 ){ - break; - } - } - if( iCol==pTab->nCol ){ + iCol = sqlite3ColumnIndex(pTab, zColumn); + if( iCol<0 ){ sqlite3DbFree(db, zErr); zErr = sqlite3MPrintf(db, "no such column: \"%s\"", zColumn); rc = SQLITE_ERROR; @@ -103039,7 +103825,6 @@ SQLITE_API int sqlite3_blob_open( {OP_Halt, 0, 0, 0}, /* 5 */ }; Vdbe *v = (Vdbe *)pBlob->pStmt; - int iDb = sqlite3SchemaToIndex(db, pTab->pSchema); VdbeOp *aOp; sqlite3VdbeAddOp4Int(v, OP_Transaction, iDb, wrFlag, @@ -103617,9 +104402,12 @@ struct VdbeSorter { u8 iPrev; /* Previous thread used to flush PMA */ u8 nTask; /* Size of aTask[] array */ u8 typeMask; - SortSubtask aTask[1]; /* One or more subtasks */ + SortSubtask aTask[FLEXARRAY]; /* One or more subtasks */ }; +/* Size (in bytes) of a VdbeSorter object that works with N or fewer subtasks */ +#define SZ_VDBESORTER(N) (offsetof(VdbeSorter,aTask)+(N)*sizeof(SortSubtask)) + #define SORTER_TYPE_INTEGER 0x01 #define SORTER_TYPE_TEXT 0x02 @@ -104221,7 +105009,7 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( VdbeSorter *pSorter; /* The new sorter */ KeyInfo *pKeyInfo; /* Copy of pCsr->pKeyInfo with db==0 */ int szKeyInfo; /* Size of pCsr->pKeyInfo in bytes */ - int sz; /* Size of pSorter in bytes */ + i64 sz; /* Size of pSorter in bytes */ int rc = SQLITE_OK; #if SQLITE_MAX_WORKER_THREADS==0 # define nWorker 0 @@ -104249,8 +105037,10 @@ SQLITE_PRIVATE int sqlite3VdbeSorterInit( assert( pCsr->pKeyInfo ); assert( !pCsr->isEphemeral ); assert( pCsr->eCurType==CURTYPE_SORTER ); - szKeyInfo = sizeof(KeyInfo) + (pCsr->pKeyInfo->nKeyField-1)*sizeof(CollSeq*); - sz = sizeof(VdbeSorter) + nWorker * sizeof(SortSubtask); + assert( sizeof(KeyInfo) + UMXV(pCsr->pKeyInfo->nKeyField)*sizeof(CollSeq*) + < 0x7fffffff ); + szKeyInfo = SZ_KEYINFO(pCsr->pKeyInfo->nKeyField+1); + sz = SZ_VDBESORTER(nWorker+1); pSorter = (VdbeSorter*)sqlite3DbMallocZero(db, sz + szKeyInfo); pCsr->uc.pSorter = pSorter; @@ -104462,7 +105252,7 @@ static int vdbeSorterJoinAll(VdbeSorter *pSorter, int rcin){ */ static MergeEngine *vdbeMergeEngineNew(int nReader){ int N = 2; /* Smallest power of two >= nReader */ - int nByte; /* Total bytes of space to allocate */ + i64 nByte; /* Total bytes of space to allocate */ MergeEngine *pNew; /* Pointer to allocated object to return */ assert( nReader<=SORTER_MAX_MERGE_COUNT ); @@ -104714,6 +105504,10 @@ static int vdbeSorterSort(SortSubtask *pTask, SorterList *pList){ p->u.pNext = 0; for(i=0; aSlot[i]; i++){ p = vdbeSorterMerge(pTask, p, aSlot[i]); + /* ,--Each aSlot[] holds twice as much as the previous. So we cannot use + ** | up all 64 aSlots[] with only a 64-bit address space. + ** v */ + assert( i<ArraySize(aSlot) ); aSlot[i] = 0; } aSlot[i] = p; @@ -107505,7 +108299,6 @@ static int lookupName( Schema *pSchema = 0; /* Schema of the expression */ int eNewExprOp = TK_COLUMN; /* New value for pExpr->op on success */ Table *pTab = 0; /* Table holding the row */ - Column *pCol; /* A column of pTab */ ExprList *pFJMatch = 0; /* Matches for FULL JOIN .. USING */ const char *zCol = pRight->u.zToken; @@ -107556,7 +108349,6 @@ static int lookupName( if( pSrcList ){ for(i=0, pItem=pSrcList->a; i<pSrcList->nSrc; i++, pItem++){ - u8 hCol; pTab = pItem->pSTab; assert( pTab!=0 && pTab->zName!=0 ); assert( pTab->nCol>0 || pParse->nErr ); @@ -107644,43 +108436,38 @@ static int lookupName( sqlite3RenameTokenRemap(pParse, 0, (void*)&pExpr->y.pTab); } } - hCol = sqlite3StrIHash(zCol); - for(j=0, pCol=pTab->aCol; j<pTab->nCol; j++, pCol++){ - if( pCol->hName==hCol - && sqlite3StrICmp(pCol->zCnName, zCol)==0 - ){ - if( cnt>0 ){ - if( pItem->fg.isUsing==0 - || sqlite3IdListIndex(pItem->u3.pUsing, zCol)<0 - ){ - /* Two or more tables have the same column name which is - ** not joined by USING. This is an error. Signal as much - ** by clearing pFJMatch and letting cnt go above 1. */ - sqlite3ExprListDelete(db, pFJMatch); - pFJMatch = 0; - }else - if( (pItem->fg.jointype & JT_RIGHT)==0 ){ - /* An INNER or LEFT JOIN. Use the left-most table */ - continue; - }else - if( (pItem->fg.jointype & JT_LEFT)==0 ){ - /* A RIGHT JOIN. Use the right-most table */ - cnt = 0; - sqlite3ExprListDelete(db, pFJMatch); - pFJMatch = 0; - }else{ - /* For a FULL JOIN, we must construct a coalesce() func */ - extendFJMatch(pParse, &pFJMatch, pMatch, pExpr->iColumn); - } + j = sqlite3ColumnIndex(pTab, zCol); + if( j>=0 ){ + if( cnt>0 ){ + if( pItem->fg.isUsing==0 + || sqlite3IdListIndex(pItem->u3.pUsing, zCol)<0 + ){ + /* Two or more tables have the same column name which is + ** not joined by USING. This is an error. Signal as much + ** by clearing pFJMatch and letting cnt go above 1. */ + sqlite3ExprListDelete(db, pFJMatch); + pFJMatch = 0; + }else + if( (pItem->fg.jointype & JT_RIGHT)==0 ){ + /* An INNER or LEFT JOIN. Use the left-most table */ + continue; + }else + if( (pItem->fg.jointype & JT_LEFT)==0 ){ + /* A RIGHT JOIN. Use the right-most table */ + cnt = 0; + sqlite3ExprListDelete(db, pFJMatch); + pFJMatch = 0; + }else{ + /* For a FULL JOIN, we must construct a coalesce() func */ + extendFJMatch(pParse, &pFJMatch, pMatch, pExpr->iColumn); } - cnt++; - pMatch = pItem; - /* Substitute the rowid (column -1) for the INTEGER PRIMARY KEY */ - pExpr->iColumn = j==pTab->iPKey ? -1 : (i16)j; - if( pItem->fg.isNestedFrom ){ - sqlite3SrcItemColumnUsed(pItem, j); - } - break; + } + cnt++; + pMatch = pItem; + /* Substitute the rowid (column -1) for the INTEGER PRIMARY KEY */ + pExpr->iColumn = j==pTab->iPKey ? -1 : (i16)j; + if( pItem->fg.isNestedFrom ){ + sqlite3SrcItemColumnUsed(pItem, j); } } if( 0==cnt && VisibleRowid(pTab) ){ @@ -107770,23 +108557,18 @@ static int lookupName( if( pTab ){ int iCol; - u8 hCol = sqlite3StrIHash(zCol); pSchema = pTab->pSchema; cntTab++; - for(iCol=0, pCol=pTab->aCol; iCol<pTab->nCol; iCol++, pCol++){ - if( pCol->hName==hCol - && sqlite3StrICmp(pCol->zCnName, zCol)==0 - ){ - if( iCol==pTab->iPKey ){ - iCol = -1; - } - break; + iCol = sqlite3ColumnIndex(pTab, zCol); + if( iCol>=0 ){ + if( pTab->iPKey==iCol ) iCol = -1; + }else{ + if( sqlite3IsRowid(zCol) && VisibleRowid(pTab) ){ + iCol = -1; + }else{ + iCol = pTab->nCol; } } - if( iCol>=pTab->nCol && sqlite3IsRowid(zCol) && VisibleRowid(pTab) ){ - /* IMP: R-51414-32910 */ - iCol = -1; - } if( iCol<pTab->nCol ){ cnt++; pMatch = 0; @@ -108425,13 +109207,12 @@ static int resolveExprStep(Walker *pWalker, Expr *pExpr){ ** sqlite_version() that might change over time cannot be used ** in an index or generated column. Curiously, they can be used ** in a CHECK constraint. SQLServer, MySQL, and PostgreSQL all - ** all this. */ + ** allow this. */ sqlite3ResolveNotValid(pParse, pNC, "non-deterministic functions", NC_IdxExpr|NC_PartIdx|NC_GenCol, 0, pExpr); }else{ assert( (NC_SelfRef & 0xff)==NC_SelfRef ); /* Must fit in 8 bits */ pExpr->op2 = pNC->ncFlags & NC_SelfRef; - if( pNC->ncFlags & NC_FromDDL ) ExprSetProperty(pExpr, EP_FromDDL); } if( (pDef->funcFlags & SQLITE_FUNC_INTERNAL)!=0 && pParse->nested==0 @@ -108447,6 +109228,7 @@ static int resolveExprStep(Walker *pWalker, Expr *pExpr){ if( (pDef->funcFlags & (SQLITE_FUNC_DIRECT|SQLITE_FUNC_UNSAFE))!=0 && !IN_RENAME_OBJECT ){ + if( pNC->ncFlags & NC_FromDDL ) ExprSetProperty(pExpr, EP_FromDDL); sqlite3ExprFunctionUsable(pParse, pExpr, pDef); } } @@ -109500,20 +110282,22 @@ SQLITE_PRIVATE int sqlite3ResolveSelfReference( Expr *pExpr, /* Expression to resolve. May be NULL. */ ExprList *pList /* Expression list to resolve. May be NULL. */ ){ - SrcList sSrc; /* Fake SrcList for pParse->pNewTable */ + SrcList *pSrc; /* Fake SrcList for pParse->pNewTable */ NameContext sNC; /* Name context for pParse->pNewTable */ int rc; + u8 srcSpace[SZ_SRCLIST_1]; /* Memory space for the fake SrcList */ assert( type==0 || pTab!=0 ); assert( type==NC_IsCheck || type==NC_PartIdx || type==NC_IdxExpr || type==NC_GenCol || pTab==0 ); memset(&sNC, 0, sizeof(sNC)); - memset(&sSrc, 0, sizeof(sSrc)); + pSrc = (SrcList*)srcSpace; + memset(pSrc, 0, SZ_SRCLIST_1); if( pTab ){ - sSrc.nSrc = 1; - sSrc.a[0].zName = pTab->zName; - sSrc.a[0].pSTab = pTab; - sSrc.a[0].iCursor = -1; + pSrc->nSrc = 1; + pSrc->a[0].zName = pTab->zName; + pSrc->a[0].pSTab = pTab; + pSrc->a[0].iCursor = -1; if( pTab->pSchema!=pParse->db->aDb[1].pSchema ){ /* Cause EP_FromDDL to be set on TK_FUNCTION nodes of non-TEMP ** schema elements */ @@ -109521,7 +110305,7 @@ SQLITE_PRIVATE int sqlite3ResolveSelfReference( } } sNC.pParse = pParse; - sNC.pSrcList = &sSrc; + sNC.pSrcList = pSrc; sNC.ncFlags = type | NC_IsDDL; if( (rc = sqlite3ResolveExprNames(&sNC, pExpr))!=SQLITE_OK ) return rc; if( pList ) rc = sqlite3ResolveExprListNames(&sNC, pList); @@ -111270,7 +112054,7 @@ static Expr *exprDup( SQLITE_PRIVATE With *sqlite3WithDup(sqlite3 *db, With *p){ With *pRet = 0; if( p ){ - sqlite3_int64 nByte = sizeof(*p) + sizeof(p->a[0]) * (p->nCte-1); + sqlite3_int64 nByte = SZ_WITH(p->nCte); pRet = sqlite3DbMallocZero(db, nByte); if( pRet ){ int i; @@ -111381,7 +112165,6 @@ SQLITE_PRIVATE ExprList *sqlite3ExprListDup(sqlite3 *db, const ExprList *p, int } pItem->zEName = sqlite3DbStrDup(db, pOldItem->zEName); pItem->fg = pOldItem->fg; - pItem->fg.done = 0; pItem->u = pOldItem->u; } return pNew; @@ -111398,11 +112181,9 @@ SQLITE_PRIVATE ExprList *sqlite3ExprListDup(sqlite3 *db, const ExprList *p, int SQLITE_PRIVATE SrcList *sqlite3SrcListDup(sqlite3 *db, const SrcList *p, int flags){ SrcList *pNew; int i; - int nByte; assert( db!=0 ); if( p==0 ) return 0; - nByte = sizeof(*p) + (p->nSrc>0 ? sizeof(p->a[0]) * (p->nSrc-1) : 0); - pNew = sqlite3DbMallocRawNN(db, nByte ); + pNew = sqlite3DbMallocRawNN(db, SZ_SRCLIST(p->nSrc) ); if( pNew==0 ) return 0; pNew->nSrc = pNew->nAlloc = p->nSrc; for(i=0; i<p->nSrc; i++){ @@ -111464,7 +112245,7 @@ SQLITE_PRIVATE IdList *sqlite3IdListDup(sqlite3 *db, const IdList *p){ int i; assert( db!=0 ); if( p==0 ) return 0; - pNew = sqlite3DbMallocRawNN(db, sizeof(*pNew)+(p->nId-1)*sizeof(p->a[0]) ); + pNew = sqlite3DbMallocRawNN(db, SZ_IDLIST(p->nId)); if( pNew==0 ) return 0; pNew->nId = p->nId; for(i=0; i<p->nId; i++){ @@ -111496,7 +112277,7 @@ SQLITE_PRIVATE Select *sqlite3SelectDup(sqlite3 *db, const Select *pDup, int fla pNew->pLimit = sqlite3ExprDup(db, p->pLimit, flags); pNew->iLimit = 0; pNew->iOffset = 0; - pNew->selFlags = p->selFlags & ~SF_UsesEphemeral; + pNew->selFlags = p->selFlags & ~(u32)SF_UsesEphemeral; pNew->addrOpenEphm[0] = -1; pNew->addrOpenEphm[1] = -1; pNew->nSelectRow = p->nSelectRow; @@ -111548,7 +112329,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE ExprList *sqlite3ExprListAppendNew( struct ExprList_item *pItem; ExprList *pList; - pList = sqlite3DbMallocRawNN(db, sizeof(ExprList)+sizeof(pList->a[0])*4 ); + pList = sqlite3DbMallocRawNN(db, SZ_EXPRLIST(4)); if( pList==0 ){ sqlite3ExprDelete(db, pExpr); return 0; @@ -111568,8 +112349,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE ExprList *sqlite3ExprListAppendGrow( struct ExprList_item *pItem; ExprList *pNew; pList->nAlloc *= 2; - pNew = sqlite3DbRealloc(db, pList, - sizeof(*pList)+(pList->nAlloc-1)*sizeof(pList->a[0])); + pNew = sqlite3DbRealloc(db, pList, SZ_EXPRLIST(pList->nAlloc)); if( pNew==0 ){ sqlite3ExprListDelete(db, pList); sqlite3ExprDelete(db, pExpr); @@ -112498,13 +113278,7 @@ SQLITE_PRIVATE const char *sqlite3RowidAlias(Table *pTab){ int ii; assert( VisibleRowid(pTab) ); for(ii=0; ii<ArraySize(azOpt); ii++){ - int iCol; - for(iCol=0; iCol<pTab->nCol; iCol++){ - if( sqlite3_stricmp(azOpt[ii], pTab->aCol[iCol].zCnName)==0 ) break; - } - if( iCol==pTab->nCol ){ - return azOpt[ii]; - } + if( sqlite3ColumnIndex(pTab, azOpt[ii])<0 ) return azOpt[ii]; } return 0; } @@ -112908,7 +113682,7 @@ static char *exprINAffinity(Parse *pParse, const Expr *pExpr){ char *zRet; assert( pExpr->op==TK_IN ); - zRet = sqlite3DbMallocRaw(pParse->db, nVal+1); + zRet = sqlite3DbMallocRaw(pParse->db, 1+(i64)nVal); if( zRet ){ int i; for(i=0; i<nVal; i++){ @@ -113168,11 +113942,12 @@ SQLITE_PRIVATE void sqlite3CodeRhsOfIN( sqlite3SelectDelete(pParse->db, pCopy); sqlite3DbFree(pParse->db, dest.zAffSdst); if( addrBloom ){ + /* Remember that location of the Bloom filter in the P3 operand + ** of the OP_Once that began this subroutine. tag-202407032019 */ sqlite3VdbeGetOp(v, addrOnce)->p3 = dest.iSDParm2; if( dest.iSDParm2==0 ){ - sqlite3VdbeChangeToNoop(v, addrBloom); - }else{ - sqlite3VdbeGetOp(v, addrOnce)->p3 = dest.iSDParm2; + /* If the Bloom filter won't actually be used, keep it small */ + sqlite3VdbeGetOp(v, addrBloom)->p1 = 10; } } if( rc ){ @@ -113619,7 +114394,7 @@ static void sqlite3ExprCodeIN( if( ExprHasProperty(pExpr, EP_Subrtn) ){ const VdbeOp *pOp = sqlite3VdbeGetOp(v, pExpr->y.sub.iAddr); assert( pOp->opcode==OP_Once || pParse->nErr ); - if( pOp->opcode==OP_Once && pOp->p3>0 ){ + if( pOp->opcode==OP_Once && pOp->p3>0 ){ /* tag-202407032019 */ assert( OptimizationEnabled(pParse->db, SQLITE_BloomFilter) ); sqlite3VdbeAddOp4Int(v, OP_Filter, pOp->p3, destIfFalse, rLhs, nVector); VdbeCoverage(v); @@ -114211,7 +114986,7 @@ static SQLITE_NOINLINE int sqlite3IndexedExprLookup( /* -** Expresion pExpr is guaranteed to be a TK_COLUMN or equivalent. This +** Expression pExpr is guaranteed to be a TK_COLUMN or equivalent. This ** function checks the Parse.pIdxPartExpr list to see if this column ** can be replaced with a constant value. If so, it generates code to ** put the constant value in a register (ideally, but not necessarily, @@ -115468,11 +116243,11 @@ SQLITE_PRIVATE void sqlite3ExprIfTrue(Parse *pParse, Expr *pExpr, int dest, int assert( TK_ISNULL==OP_IsNull ); testcase( op==TK_ISNULL ); assert( TK_NOTNULL==OP_NotNull ); testcase( op==TK_NOTNULL ); r1 = sqlite3ExprCodeTemp(pParse, pExpr->pLeft, ®Free1); - sqlite3VdbeTypeofColumn(v, r1); + assert( regFree1==0 || regFree1==r1 ); + if( regFree1 ) sqlite3VdbeTypeofColumn(v, r1); sqlite3VdbeAddOp2(v, op, r1, dest); VdbeCoverageIf(v, op==TK_ISNULL); VdbeCoverageIf(v, op==TK_NOTNULL); - testcase( regFree1==0 ); break; } case TK_BETWEEN: { @@ -115643,11 +116418,11 @@ SQLITE_PRIVATE void sqlite3ExprIfFalse(Parse *pParse, Expr *pExpr, int dest, int case TK_ISNULL: case TK_NOTNULL: { r1 = sqlite3ExprCodeTemp(pParse, pExpr->pLeft, ®Free1); - sqlite3VdbeTypeofColumn(v, r1); + assert( regFree1==0 || regFree1==r1 ); + if( regFree1 ) sqlite3VdbeTypeofColumn(v, r1); sqlite3VdbeAddOp2(v, op, r1, dest); testcase( op==TK_ISNULL ); VdbeCoverageIf(v, op==TK_ISNULL); testcase( op==TK_NOTNULL ); VdbeCoverageIf(v, op==TK_NOTNULL); - testcase( regFree1==0 ); break; } case TK_BETWEEN: { @@ -117452,13 +118227,13 @@ SQLITE_PRIVATE void sqlite3AlterBeginAddColumn(Parse *pParse, SrcList *pSrc){ assert( pNew->nCol>0 ); nAlloc = (((pNew->nCol-1)/8)*8)+8; assert( nAlloc>=pNew->nCol && nAlloc%8==0 && nAlloc-pNew->nCol<8 ); - pNew->aCol = (Column*)sqlite3DbMallocZero(db, sizeof(Column)*nAlloc); + pNew->aCol = (Column*)sqlite3DbMallocZero(db, sizeof(Column)*(u32)nAlloc); pNew->zName = sqlite3MPrintf(db, "sqlite_altertab_%s", pTab->zName); if( !pNew->aCol || !pNew->zName ){ assert( db->mallocFailed ); goto exit_begin_add_column; } - memcpy(pNew->aCol, pTab->aCol, sizeof(Column)*pNew->nCol); + memcpy(pNew->aCol, pTab->aCol, sizeof(Column)*(size_t)pNew->nCol); for(i=0; i<pNew->nCol; i++){ Column *pCol = &pNew->aCol[i]; pCol->zCnName = sqlite3DbStrDup(db, pCol->zCnName); @@ -117553,10 +118328,8 @@ SQLITE_PRIVATE void sqlite3AlterRenameColumn( ** altered. Set iCol to be the index of the column being renamed */ zOld = sqlite3NameFromToken(db, pOld); if( !zOld ) goto exit_rename_column; - for(iCol=0; iCol<pTab->nCol; iCol++){ - if( 0==sqlite3StrICmp(pTab->aCol[iCol].zCnName, zOld) ) break; - } - if( iCol==pTab->nCol ){ + iCol = sqlite3ColumnIndex(pTab, zOld); + if( iCol<0 ){ sqlite3ErrorMsg(pParse, "no such column: \"%T\"", pOld); goto exit_rename_column; } @@ -118059,6 +118832,7 @@ static int renameParseSql( int bTemp /* True if SQL is from temp schema */ ){ int rc; + u64 flags; sqlite3ParseObjectInit(p, db); if( zSql==0 ){ @@ -118067,11 +118841,21 @@ static int renameParseSql( if( sqlite3StrNICmp(zSql,"CREATE ",7)!=0 ){ return SQLITE_CORRUPT_BKPT; } - db->init.iDb = bTemp ? 1 : sqlite3FindDbName(db, zDb); + if( bTemp ){ + db->init.iDb = 1; + }else{ + int iDb = sqlite3FindDbName(db, zDb); + assert( iDb>=0 && iDb<=0xff ); + db->init.iDb = (u8)iDb; + } p->eParseMode = PARSE_MODE_RENAME; p->db = db; p->nQueryLoop = 1; + flags = db->flags; + testcase( (db->flags & SQLITE_Comments)==0 && strstr(zSql," /* ")!=0 ); + db->flags |= SQLITE_Comments; rc = sqlite3RunParser(p, zSql); + db->flags = flags; if( db->mallocFailed ) rc = SQLITE_NOMEM; if( rc==SQLITE_OK && NEVER(p->pNewTable==0 && p->pNewIndex==0 && p->pNewTrigger==0) @@ -118134,10 +118918,11 @@ static int renameEditSql( nQuot = sqlite3Strlen30(zQuot)-1; } - assert( nQuot>=nNew ); - zOut = sqlite3DbMallocZero(db, nSql + pRename->nList*nQuot + 1); + assert( nQuot>=nNew && nSql>=0 && nNew>=0 ); + zOut = sqlite3DbMallocZero(db, (u64)nSql + pRename->nList*(u64)nQuot + 1); }else{ - zOut = (char*)sqlite3DbMallocZero(db, (nSql*2+1) * 3); + assert( nSql>0 ); + zOut = (char*)sqlite3DbMallocZero(db, (2*(u64)nSql + 1) * 3); if( zOut ){ zBuf1 = &zOut[nSql*2+1]; zBuf2 = &zOut[nSql*4+2]; @@ -118149,16 +118934,17 @@ static int renameEditSql( ** with the new column name, or with single-quoted versions of themselves. ** All that remains is to construct and return the edited SQL string. */ if( zOut ){ - int nOut = nSql; - memcpy(zOut, zSql, nSql); + i64 nOut = nSql; + assert( nSql>0 ); + memcpy(zOut, zSql, (size_t)nSql); while( pRename->pList ){ int iOff; /* Offset of token to replace in zOut */ - u32 nReplace; + i64 nReplace; const char *zReplace; RenameToken *pBest = renameColumnTokenNext(pRename); if( zNew ){ - if( bQuote==0 && sqlite3IsIdChar(*pBest->t.z) ){ + if( bQuote==0 && sqlite3IsIdChar(*(u8*)pBest->t.z) ){ nReplace = nNew; zReplace = zNew; }else{ @@ -118176,14 +118962,15 @@ static int renameEditSql( memcpy(zBuf1, pBest->t.z, pBest->t.n); zBuf1[pBest->t.n] = 0; sqlite3Dequote(zBuf1); - sqlite3_snprintf(nSql*2, zBuf2, "%Q%s", zBuf1, + assert( nSql < 0x15555554 /* otherwise malloc would have failed */ ); + sqlite3_snprintf((int)(nSql*2), zBuf2, "%Q%s", zBuf1, pBest->t.z[pBest->t.n]=='\'' ? " " : "" ); zReplace = zBuf2; nReplace = sqlite3Strlen30(zReplace); } - iOff = pBest->t.z - zSql; + iOff = (int)(pBest->t.z - zSql); if( pBest->t.n!=nReplace ){ memmove(&zOut[iOff + nReplace], &zOut[iOff + pBest->t.n], nOut - (iOff + pBest->t.n) @@ -118209,11 +118996,12 @@ static int renameEditSql( ** Set all pEList->a[].fg.eEName fields in the expression-list to val. */ static void renameSetENames(ExprList *pEList, int val){ + assert( val==ENAME_NAME || val==ENAME_TAB || val==ENAME_SPAN ); if( pEList ){ int i; for(i=0; i<pEList->nExpr; i++){ assert( val==ENAME_NAME || pEList->a[i].fg.eEName==ENAME_NAME ); - pEList->a[i].fg.eEName = val; + pEList->a[i].fg.eEName = val&0x3; } } } @@ -118470,7 +119258,7 @@ static void renameColumnFunc( if( sParse.pNewTable ){ if( IsView(sParse.pNewTable) ){ Select *pSelect = sParse.pNewTable->u.view.pSelect; - pSelect->selFlags &= ~SF_View; + pSelect->selFlags &= ~(u32)SF_View; sParse.rc = SQLITE_OK; sqlite3SelectPrep(&sParse, pSelect, 0); rc = (db->mallocFailed ? SQLITE_NOMEM : sParse.rc); @@ -118688,7 +119476,7 @@ static void renameTableFunc( sNC.pParse = &sParse; assert( pSelect->selFlags & SF_View ); - pSelect->selFlags &= ~SF_View; + pSelect->selFlags &= ~(u32)SF_View; sqlite3SelectPrep(&sParse, pTab->u.view.pSelect, &sNC); if( sParse.nErr ){ rc = sParse.rc; @@ -118861,7 +119649,7 @@ static void renameQuotefixFunc( if( sParse.pNewTable ){ if( IsView(sParse.pNewTable) ){ Select *pSelect = sParse.pNewTable->u.view.pSelect; - pSelect->selFlags &= ~SF_View; + pSelect->selFlags &= ~(u32)SF_View; sParse.rc = SQLITE_OK; sqlite3SelectPrep(&sParse, pSelect, 0); rc = (db->mallocFailed ? SQLITE_NOMEM : sParse.rc); @@ -118960,10 +119748,10 @@ static void renameTableTest( if( zDb && zInput ){ int rc; Parse sParse; - int flags = db->flags; + u64 flags = db->flags; if( bNoDQS ) db->flags &= ~(SQLITE_DqsDML|SQLITE_DqsDDL); rc = renameParseSql(&sParse, zDb, db, zInput, bTemp); - db->flags |= (flags & (SQLITE_DqsDML|SQLITE_DqsDDL)); + db->flags = flags; if( rc==SQLITE_OK ){ if( isLegacy==0 && sParse.pNewTable && IsView(sParse.pNewTable) ){ NameContext sNC; @@ -119455,7 +120243,8 @@ static void openStatTable( sqlite3NestedParse(pParse, "CREATE TABLE %Q.%s(%s)", pDb->zDbSName, zTab, aTable[i].zCols ); - aRoot[i] = (u32)pParse->regRoot; + assert( pParse->isCreate || pParse->nErr ); + aRoot[i] = (u32)pParse->u1.cr.regRoot; aCreateTbl[i] = OPFLAG_P2ISREG; } }else{ @@ -119646,7 +120435,7 @@ static void statInit( int nCol; /* Number of columns in index being sampled */ int nKeyCol; /* Number of key columns */ int nColUp; /* nCol rounded up for alignment */ - int n; /* Bytes of space to allocate */ + i64 n; /* Bytes of space to allocate */ sqlite3 *db = sqlite3_context_db_handle(context); /* Database connection */ #ifdef SQLITE_ENABLE_STAT4 /* Maximum number of samples. 0 if STAT4 data is not collected */ @@ -119682,7 +120471,7 @@ static void statInit( p->db = db; p->nEst = sqlite3_value_int64(argv[2]); p->nRow = 0; - p->nLimit = sqlite3_value_int64(argv[3]); + p->nLimit = sqlite3_value_int(argv[3]); p->nCol = nCol; p->nKeyCol = nKeyCol; p->nSkipAhead = 0; @@ -120815,16 +121604,6 @@ static void decodeIntArray( while( z[0]!=0 && z[0]!=' ' ) z++; while( z[0]==' ' ) z++; } - - /* Set the bLowQual flag if the peak number of rows obtained - ** from a full equality match is so large that a full table scan - ** seems likely to be faster than using the index. - */ - if( aLog[0] > 66 /* Index has more than 100 rows */ - && aLog[0] <= aLog[nOut-1] /* And only a single value seen */ - ){ - pIndex->bLowQual = 1; - } } } @@ -121420,7 +122199,7 @@ static void attachFunc( if( aNew==0 ) return; memcpy(aNew, db->aDb, sizeof(db->aDb[0])*2); }else{ - aNew = sqlite3DbRealloc(db, db->aDb, sizeof(db->aDb[0])*(db->nDb+1) ); + aNew = sqlite3DbRealloc(db, db->aDb, sizeof(db->aDb[0])*(1+(i64)db->nDb)); if( aNew==0 ) return; } db->aDb = aNew; @@ -121491,6 +122270,13 @@ static void attachFunc( sqlite3BtreeEnterAll(db); db->init.iDb = 0; db->mDbFlags &= ~(DBFLAG_SchemaKnownOk); +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + if( db->setlkFlags & SQLITE_SETLK_BLOCK_ON_CONNECT ){ + int val = 1; + sqlite3_file *fd = sqlite3PagerFile(sqlite3BtreePager(pNew->pBt)); + sqlite3OsFileControlHint(fd, SQLITE_FCNTL_BLOCK_ON_CONNECT, &val); + } +#endif if( !REOPEN_AS_MEMDB(db) ){ rc = sqlite3Init(db, &zErrDyn); } @@ -122213,6 +122999,7 @@ static SQLITE_NOINLINE void lockTable( } } + assert( pToplevel->nTableLock < 0x7fff0000 ); nBytes = sizeof(TableLock) * (pToplevel->nTableLock+1); pToplevel->aTableLock = sqlite3DbReallocOrFree(pToplevel->db, pToplevel->aTableLock, nBytes); @@ -122313,10 +123100,12 @@ SQLITE_PRIVATE void sqlite3FinishCoding(Parse *pParse){ || sqlite3VdbeAssertMayAbort(v, pParse->mayAbort)); if( v ){ if( pParse->bReturning ){ - Returning *pReturning = pParse->u1.pReturning; + Returning *pReturning; int addrRewind; int reg; + assert( !pParse->isCreate ); + pReturning = pParse->u1.d.pReturning; if( pReturning->nRetCol ){ sqlite3VdbeAddOp0(v, OP_FkCheck); addrRewind = @@ -122392,7 +123181,9 @@ SQLITE_PRIVATE void sqlite3FinishCoding(Parse *pParse){ } if( pParse->bReturning ){ - Returning *pRet = pParse->u1.pReturning; + Returning *pRet; + assert( !pParse->isCreate ); + pRet = pParse->u1.d.pReturning; if( pRet->nRetCol ){ sqlite3VdbeAddOp2(v, OP_OpenEphemeral, pRet->iRetCur, pRet->nRetCol); } @@ -123207,10 +123998,16 @@ SQLITE_PRIVATE Index *sqlite3PrimaryKeyIndex(Table *pTab){ ** find the (first) offset of that column in index pIdx. Or return -1 ** if column iCol is not used in index pIdx. */ -SQLITE_PRIVATE i16 sqlite3TableColumnToIndex(Index *pIdx, i16 iCol){ +SQLITE_PRIVATE int sqlite3TableColumnToIndex(Index *pIdx, int iCol){ int i; + i16 iCol16; + assert( iCol>=(-1) && iCol<=SQLITE_MAX_COLUMN ); + assert( pIdx->nColumn<=SQLITE_MAX_COLUMN+1 ); + iCol16 = iCol; for(i=0; i<pIdx->nColumn; i++){ - if( iCol==pIdx->aiColumn[i] ) return i; + if( iCol16==pIdx->aiColumn[i] ){ + return i; + } } return -1; } @@ -123464,8 +124261,9 @@ SQLITE_PRIVATE void sqlite3StartTable( /* If the file format and encoding in the database have not been set, ** set them now. */ - reg1 = pParse->regRowid = ++pParse->nMem; - reg2 = pParse->regRoot = ++pParse->nMem; + assert( pParse->isCreate ); + reg1 = pParse->u1.cr.regRowid = ++pParse->nMem; + reg2 = pParse->u1.cr.regRoot = ++pParse->nMem; reg3 = ++pParse->nMem; sqlite3VdbeAddOp3(v, OP_ReadCookie, iDb, reg3, BTREE_FILE_FORMAT); sqlite3VdbeUsesBtree(v, iDb); @@ -123480,8 +124278,8 @@ SQLITE_PRIVATE void sqlite3StartTable( ** The record created does not contain anything yet. It will be replaced ** by the real entry in code generated at sqlite3EndTable(). ** - ** The rowid for the new entry is left in register pParse->regRowid. - ** The root page number of the new table is left in reg pParse->regRoot. + ** The rowid for the new entry is left in register pParse->u1.cr.regRowid. + ** The root page of the new table is left in reg pParse->u1.cr.regRoot. ** The rowid and root page number values are needed by the code that ** sqlite3EndTable will generate. */ @@ -123492,7 +124290,7 @@ SQLITE_PRIVATE void sqlite3StartTable( #endif { assert( !pParse->bReturning ); - pParse->u1.addrCrTab = + pParse->u1.cr.addrCrTab = sqlite3VdbeAddOp3(v, OP_CreateBtree, iDb, reg2, BTREE_INTKEY); } sqlite3OpenSchemaTable(pParse, iDb); @@ -123570,7 +124368,8 @@ SQLITE_PRIVATE void sqlite3AddReturning(Parse *pParse, ExprList *pList){ sqlite3ExprListDelete(db, pList); return; } - pParse->u1.pReturning = pRet; + assert( !pParse->isCreate ); + pParse->u1.d.pReturning = pRet; pRet->pParse = pParse; pRet->pReturnEL = pList; sqlite3ParserAddCleanup(pParse, sqlite3DeleteReturning, pRet); @@ -123612,7 +124411,6 @@ SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token sName, Token sType){ char *zType; Column *pCol; sqlite3 *db = pParse->db; - u8 hName; Column *aNew; u8 eType = COLTYPE_CUSTOM; u8 szEst = 1; @@ -123666,13 +124464,10 @@ SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token sName, Token sType){ memcpy(z, sName.z, sName.n); z[sName.n] = 0; sqlite3Dequote(z); - hName = sqlite3StrIHash(z); - for(i=0; i<p->nCol; i++){ - if( p->aCol[i].hName==hName && sqlite3StrICmp(z, p->aCol[i].zCnName)==0 ){ - sqlite3ErrorMsg(pParse, "duplicate column name: %s", z); - sqlite3DbFree(db, z); - return; - } + if( p->nCol && sqlite3ColumnIndex(p, z)>=0 ){ + sqlite3ErrorMsg(pParse, "duplicate column name: %s", z); + sqlite3DbFree(db, z); + return; } aNew = sqlite3DbRealloc(db,p->aCol,((i64)p->nCol+1)*sizeof(p->aCol[0])); if( aNew==0 ){ @@ -123683,7 +124478,7 @@ SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token sName, Token sType){ pCol = &p->aCol[p->nCol]; memset(pCol, 0, sizeof(p->aCol[0])); pCol->zCnName = z; - pCol->hName = hName; + pCol->hName = sqlite3StrIHash(z); sqlite3ColumnPropertiesFromName(p, pCol); if( sType.n==0 ){ @@ -123707,9 +124502,14 @@ SQLITE_PRIVATE void sqlite3AddColumn(Parse *pParse, Token sName, Token sType){ pCol->affinity = sqlite3AffinityType(zType, pCol); pCol->colFlags |= COLFLAG_HASTYPE; } + if( p->nCol<=0xff ){ + u8 h = pCol->hName % sizeof(p->aHx); + p->aHx[h] = p->nCol; + } p->nCol++; p->nNVCol++; - pParse->constraintName.n = 0; + assert( pParse->isCreate ); + pParse->u1.cr.constraintName.n = 0; } /* @@ -123973,15 +124773,11 @@ SQLITE_PRIVATE void sqlite3AddPrimaryKey( assert( pCExpr!=0 ); sqlite3StringToId(pCExpr); if( pCExpr->op==TK_ID ){ - const char *zCName; assert( !ExprHasProperty(pCExpr, EP_IntValue) ); - zCName = pCExpr->u.zToken; - for(iCol=0; iCol<pTab->nCol; iCol++){ - if( sqlite3StrICmp(zCName, pTab->aCol[iCol].zCnName)==0 ){ - pCol = &pTab->aCol[iCol]; - makeColumnPartOfPrimaryKey(pParse, pCol); - break; - } + iCol = sqlite3ColumnIndex(pTab, pCExpr->u.zToken); + if( iCol>=0 ){ + pCol = &pTab->aCol[iCol]; + makeColumnPartOfPrimaryKey(pParse, pCol); } } } @@ -124033,8 +124829,10 @@ SQLITE_PRIVATE void sqlite3AddCheckConstraint( && !sqlite3BtreeIsReadonly(db->aDb[db->init.iDb].pBt) ){ pTab->pCheck = sqlite3ExprListAppend(pParse, pTab->pCheck, pCheckExpr); - if( pParse->constraintName.n ){ - sqlite3ExprListSetName(pParse, pTab->pCheck, &pParse->constraintName, 1); + assert( pParse->isCreate ); + if( pParse->u1.cr.constraintName.n ){ + sqlite3ExprListSetName(pParse, pTab->pCheck, + &pParse->u1.cr.constraintName, 1); }else{ Token t; for(zStart++; sqlite3Isspace(zStart[0]); zStart++){} @@ -124229,7 +125027,8 @@ static void identPut(char *z, int *pIdx, char *zSignedIdent){ ** from sqliteMalloc() and must be freed by the calling function. */ static char *createTableStmt(sqlite3 *db, Table *p){ - int i, k, n; + int i, k, len; + i64 n; char *zStmt; char *zSep, *zSep2, *zEnd; Column *pCol; @@ -124253,8 +125052,9 @@ static char *createTableStmt(sqlite3 *db, Table *p){ sqlite3OomFault(db); return 0; } - sqlite3_snprintf(n, zStmt, "CREATE TABLE "); - k = sqlite3Strlen30(zStmt); + assert( n>14 && n<=0x7fffffff ); + memcpy(zStmt, "CREATE TABLE ", 13); + k = 13; identPut(zStmt, &k, p->zName); zStmt[k++] = '('; for(pCol=p->aCol, i=0; i<p->nCol; i++, pCol++){ @@ -124266,13 +125066,15 @@ static char *createTableStmt(sqlite3 *db, Table *p){ /* SQLITE_AFF_REAL */ " REAL", /* SQLITE_AFF_FLEXNUM */ " NUM", }; - int len; const char *zType; - sqlite3_snprintf(n-k, &zStmt[k], zSep); - k += sqlite3Strlen30(&zStmt[k]); + len = sqlite3Strlen30(zSep); + assert( k+len<n ); + memcpy(&zStmt[k], zSep, len); + k += len; zSep = zSep2; identPut(zStmt, &k, pCol->zCnName); + assert( k<n ); assert( pCol->affinity-SQLITE_AFF_BLOB >= 0 ); assert( pCol->affinity-SQLITE_AFF_BLOB < ArraySize(azType) ); testcase( pCol->affinity==SQLITE_AFF_BLOB ); @@ -124287,11 +125089,14 @@ static char *createTableStmt(sqlite3 *db, Table *p){ assert( pCol->affinity==SQLITE_AFF_BLOB || pCol->affinity==SQLITE_AFF_FLEXNUM || pCol->affinity==sqlite3AffinityType(zType, 0) ); + assert( k+len<n ); memcpy(&zStmt[k], zType, len); k += len; assert( k<=n ); } - sqlite3_snprintf(n-k, &zStmt[k], "%s", zEnd); + len = sqlite3Strlen30(zEnd); + assert( k+len<n ); + memcpy(&zStmt[k], zEnd, len+1); return zStmt; } @@ -124299,12 +125104,17 @@ static char *createTableStmt(sqlite3 *db, Table *p){ ** Resize an Index object to hold N columns total. Return SQLITE_OK ** on success and SQLITE_NOMEM on an OOM error. */ -static int resizeIndexObject(sqlite3 *db, Index *pIdx, int N){ +static int resizeIndexObject(Parse *pParse, Index *pIdx, int N){ char *zExtra; - int nByte; + u64 nByte; + sqlite3 *db; if( pIdx->nColumn>=N ) return SQLITE_OK; + db = pParse->db; + assert( N>0 ); + assert( N <= SQLITE_MAX_COLUMN*2 /* tag-20250221-1 */ ); + testcase( N==2*pParse->db->aLimit[SQLITE_LIMIT_COLUMN] ); assert( pIdx->isResized==0 ); - nByte = (sizeof(char*) + sizeof(LogEst) + sizeof(i16) + 1)*N; + nByte = (sizeof(char*) + sizeof(LogEst) + sizeof(i16) + 1)*(u64)N; zExtra = sqlite3DbMallocZero(db, nByte); if( zExtra==0 ) return SQLITE_NOMEM_BKPT; memcpy(zExtra, pIdx->azColl, sizeof(char*)*pIdx->nColumn); @@ -124318,7 +125128,7 @@ static int resizeIndexObject(sqlite3 *db, Index *pIdx, int N){ zExtra += sizeof(i16)*N; memcpy(zExtra, pIdx->aSortOrder, pIdx->nColumn); pIdx->aSortOrder = (u8*)zExtra; - pIdx->nColumn = N; + pIdx->nColumn = (u16)N; /* See tag-20250221-1 above for proof of safety */ pIdx->isResized = 1; return SQLITE_OK; } @@ -124484,9 +125294,9 @@ static void convertToWithoutRowidTable(Parse *pParse, Table *pTab){ ** into BTREE_BLOBKEY. */ assert( !pParse->bReturning ); - if( pParse->u1.addrCrTab ){ + if( pParse->u1.cr.addrCrTab ){ assert( v ); - sqlite3VdbeChangeP3(v, pParse->u1.addrCrTab, BTREE_BLOBKEY); + sqlite3VdbeChangeP3(v, pParse->u1.cr.addrCrTab, BTREE_BLOBKEY); } /* Locate the PRIMARY KEY index. Or, if this table was originally @@ -124572,14 +125382,14 @@ static void convertToWithoutRowidTable(Parse *pParse, Table *pTab){ pIdx->nColumn = pIdx->nKeyCol; continue; } - if( resizeIndexObject(db, pIdx, pIdx->nKeyCol+n) ) return; + if( resizeIndexObject(pParse, pIdx, pIdx->nKeyCol+n) ) return; for(i=0, j=pIdx->nKeyCol; i<nPk; i++){ if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){ testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) ); pIdx->aiColumn[j] = pPk->aiColumn[i]; pIdx->azColl[j] = pPk->azColl[i]; if( pPk->aSortOrder[i] ){ - /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */ + /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */ pIdx->bAscKeyBug = 1; } j++; @@ -124596,7 +125406,7 @@ static void convertToWithoutRowidTable(Parse *pParse, Table *pTab){ if( !hasColumn(pPk->aiColumn, nPk, i) && (pTab->aCol[i].colFlags & COLFLAG_VIRTUAL)==0 ) nExtra++; } - if( resizeIndexObject(db, pPk, nPk+nExtra) ) return; + if( resizeIndexObject(pParse, pPk, nPk+nExtra) ) return; for(i=0, j=nPk; i<pTab->nCol; i++){ if( !hasColumn(pPk->aiColumn, j, i) && (pTab->aCol[i].colFlags & COLFLAG_VIRTUAL)==0 @@ -124926,7 +125736,7 @@ SQLITE_PRIVATE void sqlite3EndTable( /* If this is a CREATE TABLE xx AS SELECT ..., execute the SELECT ** statement to populate the new table. The root-page number for the - ** new table is in register pParse->regRoot. + ** new table is in register pParse->u1.cr.regRoot. ** ** Once the SELECT has been coded by sqlite3Select(), it is in a ** suitable state to query for the column names and types to be used @@ -124957,7 +125767,8 @@ SQLITE_PRIVATE void sqlite3EndTable( regRec = ++pParse->nMem; regRowid = ++pParse->nMem; sqlite3MayAbort(pParse); - sqlite3VdbeAddOp3(v, OP_OpenWrite, iCsr, pParse->regRoot, iDb); + assert( pParse->isCreate ); + sqlite3VdbeAddOp3(v, OP_OpenWrite, iCsr, pParse->u1.cr.regRoot, iDb); sqlite3VdbeChangeP5(v, OPFLAG_P2ISREG); addrTop = sqlite3VdbeCurrentAddr(v) + 1; sqlite3VdbeAddOp3(v, OP_InitCoroutine, regYield, 0, addrTop); @@ -125002,6 +125813,7 @@ SQLITE_PRIVATE void sqlite3EndTable( ** schema table. We just need to update that slot with all ** the information we've collected. */ + assert( pParse->isCreate ); sqlite3NestedParse(pParse, "UPDATE %Q." LEGACY_SCHEMA_TABLE " SET type='%s', name=%Q, tbl_name=%Q, rootpage=#%d, sql=%Q" @@ -125010,9 +125822,9 @@ SQLITE_PRIVATE void sqlite3EndTable( zType, p->zName, p->zName, - pParse->regRoot, + pParse->u1.cr.regRoot, zStmt, - pParse->regRowid + pParse->u1.cr.regRowid ); sqlite3DbFree(db, zStmt); sqlite3ChangeCookie(pParse, iDb); @@ -125752,7 +126564,7 @@ SQLITE_PRIVATE void sqlite3CreateForeignKey( }else{ nCol = pFromCol->nExpr; } - nByte = sizeof(*pFKey) + (nCol-1)*sizeof(pFKey->aCol[0]) + pTo->n + 1; + nByte = SZ_FKEY(nCol) + pTo->n + 1; if( pToCol ){ for(i=0; i<pToCol->nExpr; i++){ nByte += sqlite3Strlen30(pToCol->a[i].zEName) + 1; @@ -125954,7 +126766,7 @@ static void sqlite3RefillIndex(Parse *pParse, Index *pIndex, int memRootPage){ ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables ** with DESC primary keys, since those indexes have there keys in ** a different order from the main table. - ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf + ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf */ sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx); } @@ -125978,13 +126790,14 @@ static void sqlite3RefillIndex(Parse *pParse, Index *pIndex, int memRootPage){ */ SQLITE_PRIVATE Index *sqlite3AllocateIndexObject( sqlite3 *db, /* Database connection */ - i16 nCol, /* Total number of columns in the index */ + int nCol, /* Total number of columns in the index */ int nExtra, /* Number of bytes of extra space to alloc */ char **ppExtra /* Pointer to the "extra" space */ ){ Index *p; /* Allocated index object */ - int nByte; /* Bytes of space for Index object + arrays */ + i64 nByte; /* Bytes of space for Index object + arrays */ + assert( nCol <= 2*db->aLimit[SQLITE_LIMIT_COLUMN] ); nByte = ROUND8(sizeof(Index)) + /* Index structure */ ROUND8(sizeof(char*)*nCol) + /* Index.azColl */ ROUND8(sizeof(LogEst)*(nCol+1) + /* Index.aiRowLogEst */ @@ -125997,8 +126810,9 @@ SQLITE_PRIVATE Index *sqlite3AllocateIndexObject( p->aiRowLogEst = (LogEst*)pExtra; pExtra += sizeof(LogEst)*(nCol+1); p->aiColumn = (i16*)pExtra; pExtra += sizeof(i16)*nCol; p->aSortOrder = (u8*)pExtra; - p->nColumn = nCol; - p->nKeyCol = nCol - 1; + assert( nCol>0 ); + p->nColumn = (u16)nCol; + p->nKeyCol = (u16)(nCol - 1); *ppExtra = ((char*)p) + nByte; } return p; @@ -126336,6 +127150,7 @@ SQLITE_PRIVATE void sqlite3CreateIndex( assert( j<=0x7fff ); if( j<0 ){ j = pTab->iPKey; + pIndex->bIdxRowid = 1; }else{ if( pTab->aCol[j].notNull==0 ){ pIndex->uniqNotNull = 0; @@ -126809,12 +127624,11 @@ SQLITE_PRIVATE IdList *sqlite3IdListAppend(Parse *pParse, IdList *pList, Token * sqlite3 *db = pParse->db; int i; if( pList==0 ){ - pList = sqlite3DbMallocZero(db, sizeof(IdList) ); + pList = sqlite3DbMallocZero(db, SZ_IDLIST(1)); if( pList==0 ) return 0; }else{ IdList *pNew; - pNew = sqlite3DbRealloc(db, pList, - sizeof(IdList) + pList->nId*sizeof(pList->a)); + pNew = sqlite3DbRealloc(db, pList, SZ_IDLIST(pList->nId+1)); if( pNew==0 ){ sqlite3IdListDelete(db, pList); return 0; @@ -126913,8 +127727,7 @@ SQLITE_PRIVATE SrcList *sqlite3SrcListEnlarge( return 0; } if( nAlloc>SQLITE_MAX_SRCLIST ) nAlloc = SQLITE_MAX_SRCLIST; - pNew = sqlite3DbRealloc(db, pSrc, - sizeof(*pSrc) + (nAlloc-1)*sizeof(pSrc->a[0]) ); + pNew = sqlite3DbRealloc(db, pSrc, SZ_SRCLIST(nAlloc)); if( pNew==0 ){ assert( db->mallocFailed ); return 0; @@ -126989,7 +127802,7 @@ SQLITE_PRIVATE SrcList *sqlite3SrcListAppend( assert( pParse->db!=0 ); db = pParse->db; if( pList==0 ){ - pList = sqlite3DbMallocRawNN(pParse->db, sizeof(SrcList) ); + pList = sqlite3DbMallocRawNN(pParse->db, SZ_SRCLIST(1)); if( pList==0 ) return 0; pList->nAlloc = 1; pList->nSrc = 1; @@ -127875,10 +128688,9 @@ SQLITE_PRIVATE With *sqlite3WithAdd( } if( pWith ){ - sqlite3_int64 nByte = sizeof(*pWith) + (sizeof(pWith->a[1]) * pWith->nCte); - pNew = sqlite3DbRealloc(db, pWith, nByte); + pNew = sqlite3DbRealloc(db, pWith, SZ_WITH(pWith->nCte+1)); }else{ - pNew = sqlite3DbMallocZero(db, sizeof(*pWith)); + pNew = sqlite3DbMallocZero(db, SZ_WITH(1)); } assert( (pNew!=0 && zName!=0) || db->mallocFailed ); @@ -129852,11 +130664,6 @@ static void substrFunc( i64 p1, p2; assert( argc==3 || argc==2 ); - if( sqlite3_value_type(argv[1])==SQLITE_NULL - || (argc==3 && sqlite3_value_type(argv[2])==SQLITE_NULL) - ){ - return; - } p0type = sqlite3_value_type(argv[0]); p1 = sqlite3_value_int64(argv[1]); if( p0type==SQLITE_BLOB ){ @@ -129874,19 +130681,23 @@ static void substrFunc( } } } -#ifdef SQLITE_SUBSTR_COMPATIBILITY - /* If SUBSTR_COMPATIBILITY is defined then substr(X,0,N) work the same as - ** as substr(X,1,N) - it returns the first N characters of X. This - ** is essentially a back-out of the bug-fix in check-in [5fc125d362df4b8] - ** from 2009-02-02 for compatibility of applications that exploited the - ** old buggy behavior. */ - if( p1==0 ) p1 = 1; /* <rdar://problem/6778339> */ -#endif if( argc==3 ){ p2 = sqlite3_value_int64(argv[2]); + if( p2==0 && sqlite3_value_type(argv[2])==SQLITE_NULL ) return; }else{ p2 = sqlite3_context_db_handle(context)->aLimit[SQLITE_LIMIT_LENGTH]; } + if( p1==0 ){ +#ifdef SQLITE_SUBSTR_COMPATIBILITY + /* If SUBSTR_COMPATIBILITY is defined then substr(X,0,N) work the same as + ** as substr(X,1,N) - it returns the first N characters of X. This + ** is essentially a back-out of the bug-fix in check-in [5fc125d362df4b8] + ** from 2009-02-02 for compatibility of applications that exploited the + ** old buggy behavior. */ + p1 = 1; /* <rdar://problem/6778339> */ +#endif + if( sqlite3_value_type(argv[1])==SQLITE_NULL ) return; + } if( p1<0 ){ p1 += len; if( p1<0 ){ @@ -130587,7 +131398,7 @@ static const char hexdigits[] = { ** Append to pStr text that is the SQL literal representation of the ** value contained in pValue. */ -SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue){ +SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue, int bEscape){ /* As currently implemented, the string must be initially empty. ** we might relax this requirement in the future, but that will ** require enhancements to the implementation. */ @@ -130635,7 +131446,7 @@ SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue){ } case SQLITE_TEXT: { const unsigned char *zArg = sqlite3_value_text(pValue); - sqlite3_str_appendf(pStr, "%Q", zArg); + sqlite3_str_appendf(pStr, bEscape ? "%#Q" : "%Q", zArg); break; } default: { @@ -130646,6 +131457,105 @@ SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue){ } } +/* +** Return true if z[] begins with N hexadecimal digits, and write +** a decoding of those digits into *pVal. Or return false if any +** one of the first N characters in z[] is not a hexadecimal digit. +*/ +static int isNHex(const char *z, int N, u32 *pVal){ + int i; + int v = 0; + for(i=0; i<N; i++){ + if( !sqlite3Isxdigit(z[i]) ) return 0; + v = (v<<4) + sqlite3HexToInt(z[i]); + } + *pVal = v; + return 1; +} + +/* +** Implementation of the UNISTR() function. +** +** This is intended to be a work-alike of the UNISTR() function in +** PostgreSQL. Quoting from the PG documentation (PostgreSQL 17 - +** scraped on 2025-02-22): +** +** Evaluate escaped Unicode characters in the argument. Unicode +** characters can be specified as \XXXX (4 hexadecimal digits), +** \+XXXXXX (6 hexadecimal digits), \uXXXX (4 hexadecimal digits), +** or \UXXXXXXXX (8 hexadecimal digits). To specify a backslash, +** write two backslashes. All other characters are taken literally. +*/ +static void unistrFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + char *zOut; + const char *zIn; + int nIn; + int i, j, n; + u32 v; + + assert( argc==1 ); + UNUSED_PARAMETER( argc ); + zIn = (const char*)sqlite3_value_text(argv[0]); + if( zIn==0 ) return; + nIn = sqlite3_value_bytes(argv[0]); + zOut = sqlite3_malloc64(nIn+1); + if( zOut==0 ){ + sqlite3_result_error_nomem(context); + return; + } + i = j = 0; + while( i<nIn ){ + char *z = strchr(&zIn[i],'\\'); + if( z==0 ){ + n = nIn - i; + memmove(&zOut[j], &zIn[i], n); + j += n; + break; + } + n = z - &zIn[i]; + if( n>0 ){ + memmove(&zOut[j], &zIn[i], n); + j += n; + i += n; + } + if( zIn[i+1]=='\\' ){ + i += 2; + zOut[j++] = '\\'; + }else if( sqlite3Isxdigit(zIn[i+1]) ){ + if( !isNHex(&zIn[i+1], 4, &v) ) goto unistr_error; + i += 5; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else if( zIn[i+1]=='+' ){ + if( !isNHex(&zIn[i+2], 6, &v) ) goto unistr_error; + i += 8; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else if( zIn[i+1]=='u' ){ + if( !isNHex(&zIn[i+2], 4, &v) ) goto unistr_error; + i += 6; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else if( zIn[i+1]=='U' ){ + if( !isNHex(&zIn[i+2], 8, &v) ) goto unistr_error; + i += 10; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else{ + goto unistr_error; + } + } + zOut[j] = 0; + sqlite3_result_text64(context, zOut, j, sqlite3_free, SQLITE_UTF8); + return; + +unistr_error: + sqlite3_free(zOut); + sqlite3_result_error(context, "invalid Unicode escape", -1); + return; +} + + /* ** Implementation of the QUOTE() function. ** @@ -130655,6 +131565,10 @@ SQLITE_PRIVATE void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue){ ** as needed. BLOBs are encoded as hexadecimal literals. Strings with ** embedded NUL characters cannot be represented as string literals in SQL ** and hence the returned string literal is truncated prior to the first NUL. +** +** If sqlite3_user_data() is non-zero, then the UNISTR_QUOTE() function is +** implemented instead. The difference is that UNISTR_QUOTE() uses the +** UNISTR() function to escape control characters. */ static void quoteFunc(sqlite3_context *context, int argc, sqlite3_value **argv){ sqlite3_str str; @@ -130662,7 +131576,7 @@ static void quoteFunc(sqlite3_context *context, int argc, sqlite3_value **argv){ assert( argc==1 ); UNUSED_PARAMETER(argc); sqlite3StrAccumInit(&str, db, 0, 0, db->aLimit[SQLITE_LIMIT_LENGTH]); - sqlite3QuoteValue(&str,argv[0]); + sqlite3QuoteValue(&str,argv[0],SQLITE_PTR_TO_INT(sqlite3_user_data(context))); sqlite3_result_text(context, sqlite3StrAccumFinish(&str), str.nChar, SQLITE_DYNAMIC); if( str.accError!=SQLITE_OK ){ @@ -130917,7 +131831,7 @@ static void replaceFunc( assert( zRep==sqlite3_value_text(argv[2]) ); nOut = nStr + 1; assert( nOut<SQLITE_MAX_LENGTH ); - zOut = contextMalloc(context, (i64)nOut); + zOut = contextMalloc(context, nOut); if( zOut==0 ){ return; } @@ -131313,7 +132227,7 @@ static void kahanBabuskaNeumaierInit( ** that it returns NULL if it sums over no inputs. TOTAL returns ** 0.0 in that case. In addition, TOTAL always returns a float where ** SUM might return an integer if it never encounters a floating point -** value. TOTAL never fails, but SUM might through an exception if +** value. TOTAL never fails, but SUM might throw an exception if ** it overflows an integer. */ static void sumStep(sqlite3_context *context, int argc, sqlite3_value **argv){ @@ -132233,7 +133147,9 @@ SQLITE_PRIVATE void sqlite3RegisterBuiltinFunctions(void){ DFUNCTION(sqlite_version, 0, 0, 0, versionFunc ), DFUNCTION(sqlite_source_id, 0, 0, 0, sourceidFunc ), FUNCTION(sqlite_log, 2, 0, 0, errlogFunc ), + FUNCTION(unistr, 1, 0, 0, unistrFunc ), FUNCTION(quote, 1, 0, 0, quoteFunc ), + FUNCTION(unistr_quote, 1, 1, 0, quoteFunc ), VFUNCTION(last_insert_rowid, 0, 0, 0, last_insert_rowid), VFUNCTION(changes, 0, 0, 0, changes ), VFUNCTION(total_changes, 0, 0, 0, total_changes ), @@ -134520,7 +135436,7 @@ SQLITE_PRIVATE Select *sqlite3MultiValues(Parse *pParse, Select *pLeft, ExprList f = (f & pLeft->selFlags); } pSelect = sqlite3SelectNew(pParse, pRow, 0, 0, 0, 0, 0, f, 0); - pLeft->selFlags &= ~SF_MultiValue; + pLeft->selFlags &= ~(u32)SF_MultiValue; if( pSelect ){ pSelect->op = TK_ALL; pSelect->pPrior = pLeft; @@ -134902,28 +135818,22 @@ SQLITE_PRIVATE void sqlite3Insert( aTabColMap = sqlite3DbMallocZero(db, pTab->nCol*sizeof(int)); if( aTabColMap==0 ) goto insert_cleanup; for(i=0; i<pColumn->nId; i++){ - const char *zCName = pColumn->a[i].zName; - u8 hName = sqlite3StrIHash(zCName); - for(j=0; j<pTab->nCol; j++){ - if( pTab->aCol[j].hName!=hName ) continue; - if( sqlite3StrICmp(zCName, pTab->aCol[j].zCnName)==0 ){ - if( aTabColMap[j]==0 ) aTabColMap[j] = i+1; - if( i!=j ) bIdListInOrder = 0; - if( j==pTab->iPKey ){ - ipkColumn = i; assert( !withoutRowid ); - } -#ifndef SQLITE_OMIT_GENERATED_COLUMNS - if( pTab->aCol[j].colFlags & (COLFLAG_STORED|COLFLAG_VIRTUAL) ){ - sqlite3ErrorMsg(pParse, - "cannot INSERT into generated column \"%s\"", - pTab->aCol[j].zCnName); - goto insert_cleanup; - } -#endif - break; + j = sqlite3ColumnIndex(pTab, pColumn->a[i].zName); + if( j>=0 ){ + if( aTabColMap[j]==0 ) aTabColMap[j] = i+1; + if( i!=j ) bIdListInOrder = 0; + if( j==pTab->iPKey ){ + ipkColumn = i; assert( !withoutRowid ); } - } - if( j>=pTab->nCol ){ +#ifndef SQLITE_OMIT_GENERATED_COLUMNS + if( pTab->aCol[j].colFlags & (COLFLAG_STORED|COLFLAG_VIRTUAL) ){ + sqlite3ErrorMsg(pParse, + "cannot INSERT into generated column \"%s\"", + pTab->aCol[j].zCnName); + goto insert_cleanup; + } +#endif + }else{ if( sqlite3IsRowid(pColumn->a[i].zName) && !withoutRowid ){ ipkColumn = i; bIdListInOrder = 0; @@ -135221,7 +136131,7 @@ SQLITE_PRIVATE void sqlite3Insert( continue; }else if( pColumn==0 ){ /* Hidden columns that are not explicitly named in the INSERT - ** get there default value */ + ** get their default value */ sqlite3ExprCodeFactorable(pParse, sqlite3ColumnExpr(pTab, &pTab->aCol[i]), iRegStore); @@ -135946,7 +136856,7 @@ SQLITE_PRIVATE void sqlite3GenerateConstraintChecks( ** could happen in any order, but they are grouped up front for ** convenience. ** - ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43 + ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43 ** The order of constraints used to have OE_Update as (2) and OE_Abort ** and so forth as (1). But apparently PostgreSQL checks the OE_Update ** constraint before any others, so it had to be moved. @@ -137756,6 +138666,8 @@ struct sqlite3_api_routines { /* Version 3.44.0 and later */ void *(*get_clientdata)(sqlite3*,const char*); int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*)); + /* Version 3.50.0 and later */ + int (*setlk_timeout)(sqlite3*,int,int); }; /* @@ -138089,6 +139001,8 @@ typedef int (*sqlite3_loadext_entry)( /* Version 3.44.0 and later */ #define sqlite3_get_clientdata sqlite3_api->get_clientdata #define sqlite3_set_clientdata sqlite3_api->set_clientdata +/* Version 3.50.0 and later */ +#define sqlite3_setlk_timeout sqlite3_api->setlk_timeout #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */ #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) @@ -138610,7 +139524,9 @@ static const sqlite3_api_routines sqlite3Apis = { sqlite3_stmt_explain, /* Version 3.44.0 and later */ sqlite3_get_clientdata, - sqlite3_set_clientdata + sqlite3_set_clientdata, + /* Version 3.50.0 and later */ + sqlite3_setlk_timeout }; /* True if x is the directory separator character @@ -139132,48 +140048,48 @@ static const char *const pragCName[] = { /* 13 */ "pk", /* 14 */ "hidden", /* table_info reuses 8 */ - /* 15 */ "schema", /* Used by: table_list */ - /* 16 */ "name", + /* 15 */ "name", /* Used by: function_list */ + /* 16 */ "builtin", /* 17 */ "type", - /* 18 */ "ncol", - /* 19 */ "wr", - /* 20 */ "strict", - /* 21 */ "seqno", /* Used by: index_xinfo */ - /* 22 */ "cid", - /* 23 */ "name", - /* 24 */ "desc", - /* 25 */ "coll", - /* 26 */ "key", - /* 27 */ "name", /* Used by: function_list */ - /* 28 */ "builtin", - /* 29 */ "type", - /* 30 */ "enc", - /* 31 */ "narg", - /* 32 */ "flags", - /* 33 */ "tbl", /* Used by: stats */ - /* 34 */ "idx", - /* 35 */ "wdth", - /* 36 */ "hght", - /* 37 */ "flgs", - /* 38 */ "seq", /* Used by: index_list */ - /* 39 */ "name", - /* 40 */ "unique", - /* 41 */ "origin", - /* 42 */ "partial", + /* 18 */ "enc", + /* 19 */ "narg", + /* 20 */ "flags", + /* 21 */ "schema", /* Used by: table_list */ + /* 22 */ "name", + /* 23 */ "type", + /* 24 */ "ncol", + /* 25 */ "wr", + /* 26 */ "strict", + /* 27 */ "seqno", /* Used by: index_xinfo */ + /* 28 */ "cid", + /* 29 */ "name", + /* 30 */ "desc", + /* 31 */ "coll", + /* 32 */ "key", + /* 33 */ "seq", /* Used by: index_list */ + /* 34 */ "name", + /* 35 */ "unique", + /* 36 */ "origin", + /* 37 */ "partial", + /* 38 */ "tbl", /* Used by: stats */ + /* 39 */ "idx", + /* 40 */ "wdth", + /* 41 */ "hght", + /* 42 */ "flgs", /* 43 */ "table", /* Used by: foreign_key_check */ /* 44 */ "rowid", /* 45 */ "parent", /* 46 */ "fkid", - /* index_info reuses 21 */ - /* 47 */ "seq", /* Used by: database_list */ - /* 48 */ "name", - /* 49 */ "file", - /* 50 */ "busy", /* Used by: wal_checkpoint */ - /* 51 */ "log", - /* 52 */ "checkpointed", - /* collation_list reuses 38 */ + /* 47 */ "busy", /* Used by: wal_checkpoint */ + /* 48 */ "log", + /* 49 */ "checkpointed", + /* 50 */ "seq", /* Used by: database_list */ + /* 51 */ "name", + /* 52 */ "file", + /* index_info reuses 27 */ /* 53 */ "database", /* Used by: lock_status */ /* 54 */ "status", + /* collation_list reuses 33 */ /* 55 */ "cache_size", /* Used by: default_cache_size */ /* module_list pragma_list reuses 9 */ /* 56 */ "timeout", /* Used by: busy_timeout */ @@ -139266,7 +140182,7 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "collation_list", /* ePragTyp: */ PragTyp_COLLATION_LIST, /* ePragFlg: */ PragFlg_Result0, - /* ColNames: */ 38, 2, + /* ColNames: */ 33, 2, /* iArg: */ 0 }, #endif #if !defined(SQLITE_OMIT_COMPILEOPTION_DIAGS) @@ -139301,7 +140217,7 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "database_list", /* ePragTyp: */ PragTyp_DATABASE_LIST, /* ePragFlg: */ PragFlg_Result0, - /* ColNames: */ 47, 3, + /* ColNames: */ 50, 3, /* iArg: */ 0 }, #endif #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) && !defined(SQLITE_OMIT_DEPRECATED) @@ -139381,7 +140297,7 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "function_list", /* ePragTyp: */ PragTyp_FUNCTION_LIST, /* ePragFlg: */ PragFlg_Result0, - /* ColNames: */ 27, 6, + /* ColNames: */ 15, 6, /* iArg: */ 0 }, #endif #endif @@ -139410,17 +140326,17 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "index_info", /* ePragTyp: */ PragTyp_INDEX_INFO, /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, - /* ColNames: */ 21, 3, + /* ColNames: */ 27, 3, /* iArg: */ 0 }, {/* zName: */ "index_list", /* ePragTyp: */ PragTyp_INDEX_LIST, /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, - /* ColNames: */ 38, 5, + /* ColNames: */ 33, 5, /* iArg: */ 0 }, {/* zName: */ "index_xinfo", /* ePragTyp: */ PragTyp_INDEX_INFO, /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, - /* ColNames: */ 21, 6, + /* ColNames: */ 27, 6, /* iArg: */ 1 }, #endif #if !defined(SQLITE_OMIT_INTEGRITY_CHECK) @@ -139599,7 +140515,7 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "stats", /* ePragTyp: */ PragTyp_STATS, /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq, - /* ColNames: */ 33, 5, + /* ColNames: */ 38, 5, /* iArg: */ 0 }, #endif #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) @@ -139618,7 +140534,7 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "table_list", /* ePragTyp: */ PragTyp_TABLE_LIST, /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1, - /* ColNames: */ 15, 6, + /* ColNames: */ 21, 6, /* iArg: */ 0 }, {/* zName: */ "table_xinfo", /* ePragTyp: */ PragTyp_TABLE_INFO, @@ -139695,7 +140611,7 @@ static const PragmaName aPragmaName[] = { {/* zName: */ "wal_checkpoint", /* ePragTyp: */ PragTyp_WAL_CHECKPOINT, /* ePragFlg: */ PragFlg_NeedSchema, - /* ColNames: */ 50, 3, + /* ColNames: */ 47, 3, /* iArg: */ 0 }, #endif #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) @@ -139717,7 +140633,7 @@ static const PragmaName aPragmaName[] = { ** the following macro or to the actual analysis_limit if it is non-zero, ** in order to prevent PRAGMA optimize from running for too long. ** -** The value of 2000 is chosen emperically so that the worst-case run-time +** The value of 2000 is chosen empirically so that the worst-case run-time ** for PRAGMA optimize does not exceed 100 milliseconds against a variety ** of test databases on a RaspberryPI-4 compiled using -Os and without ** -DSQLITE_DEBUG. Of course, your mileage may vary. For the purpose of @@ -140834,7 +141750,10 @@ SQLITE_PRIVATE void sqlite3Pragma( } }else{ db->flags &= ~mask; - if( mask==SQLITE_DeferFKs ) db->nDeferredImmCons = 0; + if( mask==SQLITE_DeferFKs ){ + db->nDeferredImmCons = 0; + db->nDeferredCons = 0; + } if( (mask & SQLITE_WriteSchema)!=0 && sqlite3_stricmp(zRight, "reset")==0 ){ @@ -144003,7 +144922,7 @@ SQLITE_PRIVATE Select *sqlite3SelectNew( pNew->addrOpenEphm[0] = -1; pNew->addrOpenEphm[1] = -1; pNew->nSelectRow = 0; - if( pSrc==0 ) pSrc = sqlite3DbMallocZero(pParse->db, sizeof(*pSrc)); + if( pSrc==0 ) pSrc = sqlite3DbMallocZero(pParse->db, SZ_SRCLIST_1); pNew->pSrc = pSrc; pNew->pWhere = pWhere; pNew->pGroupBy = pGroupBy; @@ -144168,10 +145087,33 @@ SQLITE_PRIVATE int sqlite3JoinType(Parse *pParse, Token *pA, Token *pB, Token *p */ SQLITE_PRIVATE int sqlite3ColumnIndex(Table *pTab, const char *zCol){ int i; - u8 h = sqlite3StrIHash(zCol); - Column *pCol; - for(pCol=pTab->aCol, i=0; i<pTab->nCol; pCol++, i++){ - if( pCol->hName==h && sqlite3StrICmp(pCol->zCnName, zCol)==0 ) return i; + u8 h; + const Column *aCol; + int nCol; + + h = sqlite3StrIHash(zCol); + aCol = pTab->aCol; + nCol = pTab->nCol; + + /* See if the aHx gives us a lucky match */ + i = pTab->aHx[h % sizeof(pTab->aHx)]; + assert( i<nCol ); + if( aCol[i].hName==h + && sqlite3StrICmp(aCol[i].zCnName, zCol)==0 + ){ + return i; + } + + /* No lucky match from the hash table. Do a full search. */ + i = 0; + while( 1 /*exit-by-break*/ ){ + if( aCol[i].hName==h + && sqlite3StrICmp(aCol[i].zCnName, zCol)==0 + ){ + return i; + } + i++; + if( i>=nCol ) break; } return -1; } @@ -145363,8 +146305,8 @@ static void selectInnerLoop( ** X extra columns. */ SQLITE_PRIVATE KeyInfo *sqlite3KeyInfoAlloc(sqlite3 *db, int N, int X){ - int nExtra = (N+X)*(sizeof(CollSeq*)+1) - sizeof(CollSeq*); - KeyInfo *p = sqlite3DbMallocRawNN(db, sizeof(KeyInfo) + nExtra); + int nExtra = (N+X)*(sizeof(CollSeq*)+1); + KeyInfo *p = sqlite3DbMallocRawNN(db, SZ_KEYINFO(0) + nExtra); if( p ){ p->aSortFlags = (u8*)&p->aColl[N+X]; p->nKeyField = (u16)N; @@ -145372,7 +146314,7 @@ SQLITE_PRIVATE KeyInfo *sqlite3KeyInfoAlloc(sqlite3 *db, int N, int X){ p->enc = ENC(db); p->db = db; p->nRef = 1; - memset(&p[1], 0, nExtra); + memset(p->aColl, 0, nExtra); }else{ return (KeyInfo*)sqlite3OomFault(db); } @@ -147073,6 +148015,7 @@ static int multiSelect( multi_select_end: pDest->iSdst = dest.iSdst; pDest->nSdst = dest.nSdst; + pDest->iSDParm2 = dest.iSDParm2; if( pDelete ){ sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete); } @@ -148683,7 +149626,8 @@ static void constInsert( return; /* Already present. Return without doing anything. */ } } - if( sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){ + assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB ); + if( sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){ pConst->bHasAffBlob = 1; } @@ -148758,7 +149702,8 @@ static int propagateConstantExprRewriteOne( if( pColumn==pExpr ) continue; if( pColumn->iTable!=pExpr->iTable ) continue; if( pColumn->iColumn!=pExpr->iColumn ) continue; - if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){ + assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB ); + if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){ break; } /* A match is found. Add the EP_FixedCol property */ @@ -149411,7 +150356,7 @@ SQLITE_PRIVATE int sqlite3IndexedByLookup(Parse *pParse, SrcItem *pFrom){ ** above that generates the code for a compound SELECT with an ORDER BY clause ** uses a merge algorithm that requires the same collating sequence on the ** result columns as on the ORDER BY clause. See ticket -** http://www.sqlite.org/src/info/6709574d2a +** http://sqlite.org/src/info/6709574d2a ** ** This transformation is only needed for EXCEPT, INTERSECT, and UNION. ** The UNION ALL operator works fine with multiSelectOrderBy() even when @@ -149472,7 +150417,7 @@ static int convertCompoundSelectToSubquery(Walker *pWalker, Select *p){ #ifndef SQLITE_OMIT_WINDOWFUNC p->pWinDefn = 0; #endif - p->selFlags &= ~SF_Compound; + p->selFlags &= ~(u32)SF_Compound; assert( (p->selFlags & SF_Converted)==0 ); p->selFlags |= SF_Converted; assert( pNew->pPrior!=0 ); @@ -149888,7 +150833,7 @@ static int selectExpander(Walker *pWalker, Select *p){ pEList = p->pEList; if( pParse->pWith && (p->selFlags & SF_View) ){ if( p->pWith==0 ){ - p->pWith = (With*)sqlite3DbMallocZero(db, sizeof(With)); + p->pWith = (With*)sqlite3DbMallocZero(db, SZ_WITH(1) ); if( p->pWith==0 ){ return WRC_Abort; } @@ -151027,6 +151972,7 @@ static void agginfoFree(sqlite3 *db, void *pArg){ ** * There is no WHERE or GROUP BY or HAVING clauses on the subqueries ** * The outer query is a simple count(*) with no WHERE clause or other ** extraneous syntax. +** * None of the subqueries are DISTINCT (forumpost/a860f5fb2e 2025-03-10) ** ** Return TRUE if the optimization is undertaken. */ @@ -151059,7 +152005,11 @@ static int countOfViewOptimization(Parse *pParse, Select *p){ if( pSub->op!=TK_ALL && pSub->pPrior ) return 0; /* Must be UNION ALL */ if( pSub->pWhere ) return 0; /* No WHERE clause */ if( pSub->pLimit ) return 0; /* No LIMIT clause */ - if( pSub->selFlags & SF_Aggregate ) return 0; /* Not an aggregate */ + if( pSub->selFlags & (SF_Aggregate|SF_Distinct) ){ + testcase( pSub->selFlags & SF_Aggregate ); + testcase( pSub->selFlags & SF_Distinct ); + return 0; /* Not an aggregate nor DISTINCT */ + } assert( pSub->pHaving==0 ); /* Due to the previous */ pSub = pSub->pPrior; /* Repeat over compound */ }while( pSub ); @@ -151071,14 +152021,14 @@ static int countOfViewOptimization(Parse *pParse, Select *p){ pExpr = 0; pSub = sqlite3SubqueryDetach(db, pFrom); sqlite3SrcListDelete(db, p->pSrc); - p->pSrc = sqlite3DbMallocZero(pParse->db, sizeof(*p->pSrc)); + p->pSrc = sqlite3DbMallocZero(pParse->db, SZ_SRCLIST_1); while( pSub ){ Expr *pTerm; pPrior = pSub->pPrior; pSub->pPrior = 0; pSub->pNext = 0; pSub->selFlags |= SF_Aggregate; - pSub->selFlags &= ~SF_Compound; + pSub->selFlags &= ~(u32)SF_Compound; pSub->nSelectRow = 0; sqlite3ParserAddCleanup(pParse, sqlite3ExprListDeleteGeneric, pSub->pEList); pTerm = pPrior ? sqlite3ExprDup(db, pCount, 0) : pCount; @@ -151093,7 +152043,7 @@ static int countOfViewOptimization(Parse *pParse, Select *p){ pSub = pPrior; } p->pEList->a[0].pExpr = pExpr; - p->selFlags &= ~SF_Aggregate; + p->selFlags &= ~(u32)SF_Aggregate; #if TREETRACE_ENABLED if( sqlite3TreeTrace & 0x200 ){ @@ -151300,7 +152250,7 @@ SQLITE_PRIVATE int sqlite3Select( testcase( pParse->earlyCleanup ); p->pOrderBy = 0; } - p->selFlags &= ~SF_Distinct; + p->selFlags &= ~(u32)SF_Distinct; p->selFlags |= SF_NoopOrderBy; } sqlite3SelectPrep(pParse, p, 0); @@ -151339,7 +152289,7 @@ SQLITE_PRIVATE int sqlite3Select( ** and leaving this flag set can cause errors if a compound sub-query ** in p->pSrc is flattened into this query and this function called ** again as part of compound SELECT processing. */ - p->selFlags &= ~SF_UFSrcCheck; + p->selFlags &= ~(u32)SF_UFSrcCheck; } if( pDest->eDest==SRT_Output ){ @@ -151828,7 +152778,7 @@ SQLITE_PRIVATE int sqlite3Select( && p->pWin==0 #endif ){ - p->selFlags &= ~SF_Distinct; + p->selFlags &= ~(u32)SF_Distinct; pGroupBy = p->pGroupBy = sqlite3ExprListDup(db, pEList, 0); if( pGroupBy ){ for(i=0; i<pGroupBy->nExpr; i++){ @@ -151937,6 +152887,12 @@ SQLITE_PRIVATE int sqlite3Select( if( pWInfo==0 ) goto select_end; if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){ p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo); + if( pDest->eDest<=SRT_DistQueue && pDest->eDest>=SRT_DistFifo ){ + /* TUNING: For a UNION CTE, because UNION is implies DISTINCT, + ** reduce the estimated output row count by 8 (LogEst 30). + ** Search for tag-20250414a to see other cases */ + p->nSelectRow -= 30; + } } if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){ sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo); @@ -152310,6 +153266,10 @@ SQLITE_PRIVATE int sqlite3Select( if( iOrderByCol ){ Expr *pX = p->pEList->a[iOrderByCol-1].pExpr; Expr *pBase = sqlite3ExprSkipCollateAndLikely(pX); + while( ALWAYS(pBase!=0) && pBase->op==TK_IF_NULL_ROW ){ + pX = pBase->pLeft; + pBase = sqlite3ExprSkipCollateAndLikely(pX); + } if( ALWAYS(pBase!=0) && pBase->op!=TK_AGG_COLUMN && pBase->op!=TK_REGISTER @@ -152893,7 +153853,8 @@ SQLITE_PRIVATE Trigger *sqlite3TriggerList(Parse *pParse, Table *pTab){ assert( pParse->db->pVtabCtx==0 ); #endif assert( pParse->bReturning ); - assert( &(pParse->u1.pReturning->retTrig) == pTrig ); + assert( !pParse->isCreate ); + assert( &(pParse->u1.d.pReturning->retTrig) == pTrig ); pTrig->table = pTab->zName; pTrig->pTabSchema = pTab->pSchema; pTrig->pNext = pList; @@ -153861,7 +154822,8 @@ static void codeReturningTrigger( ExprList *pNew; Returning *pReturning; Select sSelect; - SrcList sFrom; + SrcList *pFrom; + u8 fromSpace[SZ_SRCLIST_1]; assert( v!=0 ); if( !pParse->bReturning ){ @@ -153870,19 +154832,21 @@ static void codeReturningTrigger( return; } assert( db->pParse==pParse ); - pReturning = pParse->u1.pReturning; + assert( !pParse->isCreate ); + pReturning = pParse->u1.d.pReturning; if( pTrigger != &(pReturning->retTrig) ){ /* This RETURNING trigger is for a different statement */ return; } memset(&sSelect, 0, sizeof(sSelect)); - memset(&sFrom, 0, sizeof(sFrom)); + pFrom = (SrcList*)fromSpace; + memset(pFrom, 0, SZ_SRCLIST_1); sSelect.pEList = sqlite3ExprListDup(db, pReturning->pReturnEL, 0); - sSelect.pSrc = &sFrom; - sFrom.nSrc = 1; - sFrom.a[0].pSTab = pTab; - sFrom.a[0].zName = pTab->zName; /* tag-20240424-1 */ - sFrom.a[0].iCursor = -1; + sSelect.pSrc = pFrom; + pFrom->nSrc = 1; + pFrom->a[0].pSTab = pTab; + pFrom->a[0].zName = pTab->zName; /* tag-20240424-1 */ + pFrom->a[0].iCursor = -1; sqlite3SelectPrep(pParse, &sSelect, 0); if( pParse->nErr==0 ){ assert( db->mallocFailed==0 ); @@ -154100,6 +155064,8 @@ static TriggerPrg *codeRowTrigger( sSubParse.eTriggerOp = pTrigger->op; sSubParse.nQueryLoop = pParse->nQueryLoop; sSubParse.prepFlags = pParse->prepFlags; + sSubParse.oldmask = 0; + sSubParse.newmask = 0; v = sqlite3GetVdbe(&sSubParse); if( v ){ @@ -154854,38 +155820,32 @@ SQLITE_PRIVATE void sqlite3Update( */ chngRowid = chngPk = 0; for(i=0; i<pChanges->nExpr; i++){ - u8 hCol = sqlite3StrIHash(pChanges->a[i].zEName); /* If this is an UPDATE with a FROM clause, do not resolve expressions ** here. The call to sqlite3Select() below will do that. */ if( nChangeFrom==0 && sqlite3ResolveExprNames(&sNC, pChanges->a[i].pExpr) ){ goto update_cleanup; } - for(j=0; j<pTab->nCol; j++){ - if( pTab->aCol[j].hName==hCol - && sqlite3StrICmp(pTab->aCol[j].zCnName, pChanges->a[i].zEName)==0 - ){ - if( j==pTab->iPKey ){ - chngRowid = 1; - pRowidExpr = pChanges->a[i].pExpr; - iRowidExpr = i; - }else if( pPk && (pTab->aCol[j].colFlags & COLFLAG_PRIMKEY)!=0 ){ - chngPk = 1; - } -#ifndef SQLITE_OMIT_GENERATED_COLUMNS - else if( pTab->aCol[j].colFlags & COLFLAG_GENERATED ){ - testcase( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ); - testcase( pTab->aCol[j].colFlags & COLFLAG_STORED ); - sqlite3ErrorMsg(pParse, - "cannot UPDATE generated column \"%s\"", - pTab->aCol[j].zCnName); - goto update_cleanup; - } -#endif - aXRef[j] = i; - break; + j = sqlite3ColumnIndex(pTab, pChanges->a[i].zEName); + if( j>=0 ){ + if( j==pTab->iPKey ){ + chngRowid = 1; + pRowidExpr = pChanges->a[i].pExpr; + iRowidExpr = i; + }else if( pPk && (pTab->aCol[j].colFlags & COLFLAG_PRIMKEY)!=0 ){ + chngPk = 1; } - } - if( j>=pTab->nCol ){ +#ifndef SQLITE_OMIT_GENERATED_COLUMNS + else if( pTab->aCol[j].colFlags & COLFLAG_GENERATED ){ + testcase( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ); + testcase( pTab->aCol[j].colFlags & COLFLAG_STORED ); + sqlite3ErrorMsg(pParse, + "cannot UPDATE generated column \"%s\"", + pTab->aCol[j].zCnName); + goto update_cleanup; + } +#endif + aXRef[j] = i; + }else{ if( pPk==0 && sqlite3IsRowid(pChanges->a[i].zEName) ){ j = -1; chngRowid = 1; @@ -156208,7 +157168,7 @@ SQLITE_PRIVATE void sqlite3Vacuum(Parse *pParse, Token *pNm, Expr *pInto){ #else /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments ** to VACUUM are silently ignored. This is a back-out of a bug fix that - ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270). + ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270). ** The buggy behavior is required for binary compatibility with some ** legacy applications. */ iDb = sqlite3FindDb(pParse->db, pNm); @@ -156287,7 +157247,7 @@ SQLITE_PRIVATE SQLITE_NOINLINE int sqlite3RunVacuum( saved_nChange = db->nChange; saved_nTotalChange = db->nTotalChange; saved_mTrace = db->mTrace; - db->flags |= SQLITE_WriteSchema | SQLITE_IgnoreChecks; + db->flags |= SQLITE_WriteSchema | SQLITE_IgnoreChecks | SQLITE_Comments; db->mDbFlags |= DBFLAG_PreferBuiltin | DBFLAG_Vacuum; db->flags &= ~(u64)(SQLITE_ForeignKeys | SQLITE_ReverseOrder | SQLITE_Defensive | SQLITE_CountRows); @@ -156992,11 +157952,12 @@ SQLITE_PRIVATE void sqlite3VtabFinishParse(Parse *pParse, Token *pEnd){ ** schema table. We just need to update that slot with all ** the information we've collected. ** - ** The VM register number pParse->regRowid holds the rowid of an + ** The VM register number pParse->u1.cr.regRowid holds the rowid of an ** entry in the sqlite_schema table that was created for this vtab ** by sqlite3StartTable(). */ iDb = sqlite3SchemaToIndex(db, pTab->pSchema); + assert( pParse->isCreate ); sqlite3NestedParse(pParse, "UPDATE %Q." LEGACY_SCHEMA_TABLE " " "SET type='table', name=%Q, tbl_name=%Q, rootpage=0, sql=%Q " @@ -157005,7 +157966,7 @@ SQLITE_PRIVATE void sqlite3VtabFinishParse(Parse *pParse, Token *pEnd){ pTab->zName, pTab->zName, zStmt, - pParse->regRowid + pParse->u1.cr.regRowid ); v = sqlite3GetVdbe(pParse); sqlite3ChangeCookie(pParse, iDb); @@ -158415,9 +159376,14 @@ struct WhereInfo { Bitmask revMask; /* Mask of ORDER BY terms that need reversing */ WhereClause sWC; /* Decomposition of the WHERE clause */ WhereMaskSet sMaskSet; /* Map cursor numbers to bitmasks */ - WhereLevel a[1]; /* Information about each nest loop in WHERE */ + WhereLevel a[FLEXARRAY]; /* Information about each nest loop in WHERE */ }; +/* +** The size (in bytes) of a WhereInfo object that holds N WhereLevels. +*/ +#define SZ_WHEREINFO(N) ROUND8(offsetof(WhereInfo,a)+(N)*sizeof(WhereLevel)) + /* ** Private interfaces - callable only by other where.c routines. ** @@ -159097,7 +160063,7 @@ static void adjustOrderByCol(ExprList *pOrderBy, ExprList *pEList){ /* ** pX is an expression of the form: (vector) IN (SELECT ...) ** In other words, it is a vector IN operator with a SELECT clause on the -** LHS. But not all terms in the vector are indexable and the terms might +** RHS. But not all terms in the vector are indexable and the terms might ** not be in the correct order for indexing. ** ** This routine makes a copy of the input pX expression and then adjusts @@ -160163,6 +161129,9 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( } sqlite3VdbeAddOp2(v, OP_Integer, pLoop->u.vtab.idxNum, iReg); sqlite3VdbeAddOp2(v, OP_Integer, nConstraint, iReg+1); + /* The instruction immediately prior to OP_VFilter must be an OP_Integer + ** that sets the "argc" value for xVFilter. This is necessary for + ** resolveP2() to work correctly. See tag-20250207a. */ sqlite3VdbeAddOp4(v, OP_VFilter, iCur, addrNotFound, iReg, pLoop->u.vtab.idxStr, pLoop->u.vtab.needFree ? P4_DYNAMIC : P4_STATIC); @@ -160865,8 +161834,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( int nNotReady; /* The number of notReady tables */ SrcItem *origSrc; /* Original list of tables */ nNotReady = pWInfo->nLevel - iLevel - 1; - pOrTab = sqlite3DbMallocRawNN(db, - sizeof(*pOrTab)+ nNotReady*sizeof(pOrTab->a[0])); + pOrTab = sqlite3DbMallocRawNN(db, SZ_SRCLIST(nNotReady+1)); if( pOrTab==0 ) return notReady; pOrTab->nAlloc = (u8)(nNotReady + 1); pOrTab->nSrc = pOrTab->nAlloc; @@ -160917,7 +161885,7 @@ SQLITE_PRIVATE Bitmask sqlite3WhereCodeOneLoopStart( ** ** This optimization also only applies if the (x1 OR x2 OR ...) term ** is not contained in the ON clause of a LEFT JOIN. - ** See ticket http://www.sqlite.org/src/info/f2369304e4 + ** See ticket http://sqlite.org/src/info/f2369304e4 ** ** 2022-02-04: Do not push down slices of a row-value comparison. ** In other words, "w" or "y" may not be a slice of a vector. Otherwise, @@ -161409,7 +162377,8 @@ SQLITE_PRIVATE SQLITE_NOINLINE void sqlite3WhereRightJoinLoop( WhereInfo *pSubWInfo; WhereLoop *pLoop = pLevel->pWLoop; SrcItem *pTabItem = &pWInfo->pTabList->a[pLevel->iFrom]; - SrcList sFrom; + SrcList *pFrom; + u8 fromSpace[SZ_SRCLIST_1]; Bitmask mAll = 0; int k; @@ -161453,13 +162422,14 @@ SQLITE_PRIVATE SQLITE_NOINLINE void sqlite3WhereRightJoinLoop( sqlite3ExprDup(pParse->db, pTerm->pExpr, 0)); } } - sFrom.nSrc = 1; - sFrom.nAlloc = 1; - memcpy(&sFrom.a[0], pTabItem, sizeof(SrcItem)); - sFrom.a[0].fg.jointype = 0; + pFrom = (SrcList*)fromSpace; + pFrom->nSrc = 1; + pFrom->nAlloc = 1; + memcpy(&pFrom->a[0], pTabItem, sizeof(SrcItem)); + pFrom->a[0].fg.jointype = 0; assert( pParse->withinRJSubrtn < 100 ); pParse->withinRJSubrtn++; - pSubWInfo = sqlite3WhereBegin(pParse, &sFrom, pSubWhere, 0, 0, 0, + pSubWInfo = sqlite3WhereBegin(pParse, pFrom, pSubWhere, 0, 0, 0, WHERE_RIGHT_JOIN, 0); if( pSubWInfo ){ int iCur = pLevel->iTabCur; @@ -163447,11 +164417,16 @@ struct HiddenIndexInfo { int eDistinct; /* Value to return from sqlite3_vtab_distinct() */ u32 mIn; /* Mask of terms that are <col> IN (...) */ u32 mHandleIn; /* Terms that vtab will handle as <col> IN (...) */ - sqlite3_value *aRhs[1]; /* RHS values for constraints. MUST BE LAST - ** because extra space is allocated to hold up - ** to nTerm such values */ + sqlite3_value *aRhs[FLEXARRAY]; /* RHS values for constraints. MUST BE LAST + ** Extra space is allocated to hold up + ** to nTerm such values */ }; +/* Size (in bytes) of a HiddenIndeInfo object sufficient to hold as +** many as N constraints */ +#define SZ_HIDDENINDEXINFO(N) \ + (offsetof(HiddenIndexInfo,aRhs) + (N)*sizeof(sqlite3_value*)) + /* Forward declaration of methods */ static int whereLoopResize(sqlite3*, WhereLoop*, int); @@ -164516,6 +165491,8 @@ static SQLITE_NOINLINE void constructAutomaticIndex( } /* Construct the Index object to describe this index */ + assert( nKeyCol <= pTable->nCol + MAX(0, pTable->nCol - BMS + 1) ); + /* ^-- This guarantees that the number of index columns will fit in the u16 */ pIdx = sqlite3AllocateIndexObject(pParse->db, nKeyCol+HasRowid(pTable), 0, &zNotUsed); if( pIdx==0 ) goto end_auto_index_create; @@ -164927,8 +165904,8 @@ static sqlite3_index_info *allocateIndexInfo( */ pIdxInfo = sqlite3DbMallocZero(pParse->db, sizeof(*pIdxInfo) + (sizeof(*pIdxCons) + sizeof(*pUsage))*nTerm - + sizeof(*pIdxOrderBy)*nOrderBy + sizeof(*pHidden) - + sizeof(sqlite3_value*)*nTerm ); + + sizeof(*pIdxOrderBy)*nOrderBy + + SZ_HIDDENINDEXINFO(nTerm) ); if( pIdxInfo==0 ){ sqlite3ErrorMsg(pParse, "out of memory"); return 0; @@ -166564,11 +167541,8 @@ static int whereLoopAddBtreeIndex( assert( pNew->u.btree.nBtm==0 ); opMask = WO_EQ|WO_IN|WO_GT|WO_GE|WO_LT|WO_LE|WO_ISNULL|WO_IS; } - if( pProbe->bUnordered || pProbe->bLowQual ){ - if( pProbe->bUnordered ) opMask &= ~(WO_GT|WO_GE|WO_LT|WO_LE); - if( pProbe->bLowQual && pSrc->fg.isIndexedBy==0 ){ - opMask &= ~(WO_EQ|WO_IN|WO_IS); - } + if( pProbe->bUnordered ){ + opMask &= ~(WO_GT|WO_GE|WO_LT|WO_LE); } assert( pNew->u.btree.nEq<pProbe->nColumn ); @@ -166881,7 +167855,7 @@ static int whereLoopAddBtreeIndex( if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0 && pNew->u.btree.nEq<pProbe->nColumn && (pNew->u.btree.nEq<pProbe->nKeyCol || - pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY) + (pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid)) ){ if( pNew->u.btree.nEq>3 ){ sqlite3ProgressCheck(pParse); @@ -167010,6 +167984,7 @@ static int whereUsablePartialIndex( if( (!ExprHasProperty(pExpr, EP_OuterON) || pExpr->w.iJoin==iTab) && ((jointype & JT_OUTER)==0 || ExprHasProperty(pExpr, EP_OuterON)) && sqlite3ExprImpliesExpr(pParse, pExpr, pWhere, iTab) + && !sqlite3ExprImpliesExpr(pParse, pExpr, pWhere, -1) && (pTerm->wtFlags & TERM_VNULL)==0 ){ return 1; @@ -167505,7 +168480,7 @@ static int whereLoopAddBtree( && (HasRowid(pTab) || pWInfo->pSelect!=0 || sqlite3FaultSim(700)) ){ WHERETRACE(0x200, - ("-> %s a covering index according to bitmasks\n", + ("-> %s is a covering index according to bitmasks\n", pProbe->zName, m==0 ? "is" : "is not")); pNew->wsFlags = WHERE_IDX_ONLY | WHERE_INDEXED; } @@ -170122,10 +171097,7 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( ** field (type Bitmask) it must be aligned on an 8-byte boundary on ** some architectures. Hence the ROUND8() below. */ - nByteWInfo = ROUND8P(sizeof(WhereInfo)); - if( nTabList>1 ){ - nByteWInfo = ROUND8P(nByteWInfo + (nTabList-1)*sizeof(WhereLevel)); - } + nByteWInfo = SZ_WHEREINFO(nTabList); pWInfo = sqlite3DbMallocRawNN(db, nByteWInfo + sizeof(WhereLoop)); if( db->mallocFailed ){ sqlite3DbFree(db, pWInfo); @@ -170342,7 +171314,8 @@ SQLITE_PRIVATE WhereInfo *sqlite3WhereBegin( } /* TUNING: Assume that a DISTINCT clause on a subquery reduces - ** the output size by a factor of 8 (LogEst -30). + ** the output size by a factor of 8 (LogEst -30). Search for + ** tag-20250414a to see other cases. */ if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){ WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n", @@ -172077,7 +173050,7 @@ SQLITE_PRIVATE int sqlite3WindowRewrite(Parse *pParse, Select *p){ p->pWhere = 0; p->pGroupBy = 0; p->pHaving = 0; - p->selFlags &= ~SF_Aggregate; + p->selFlags &= ~(u32)SF_Aggregate; p->selFlags |= SF_WinRewrite; /* Create the ORDER BY clause for the sub-select. This is the concatenation @@ -174217,6 +175190,11 @@ SQLITE_PRIVATE void sqlite3WindowCodeStep( /* #include "sqliteInt.h" */ +/* +** Verify that the pParse->isCreate field is set +*/ +#define ASSERT_IS_CREATE assert(pParse->isCreate) + /* ** Disable all error recovery processing in the parser push-down ** automaton. @@ -174280,6 +175258,10 @@ static void parserSyntaxError(Parse *pParse, Token *p){ static void disableLookaside(Parse *pParse){ sqlite3 *db = pParse->db; pParse->disableLookaside++; +#ifdef SQLITE_DEBUG + pParse->isCreate = 1; +#endif + memset(&pParse->u1.cr, 0, sizeof(pParse->u1.cr)); DisableLookaside; } @@ -177916,7 +178898,9 @@ static YYACTIONTYPE yy_reduce( } break; case 14: /* createkw ::= CREATE */ -{disableLookaside(pParse);} +{ + disableLookaside(pParse); +} break; case 15: /* ifnotexists ::= */ case 18: /* temp ::= */ yytestcase(yyruleno==18); @@ -178008,7 +178992,7 @@ static YYACTIONTYPE yy_reduce( break; case 32: /* ccons ::= CONSTRAINT nm */ case 67: /* tcons ::= CONSTRAINT nm */ yytestcase(yyruleno==67); -{pParse->constraintName = yymsp[0].minor.yy0;} +{ASSERT_IS_CREATE; pParse->u1.cr.constraintName = yymsp[0].minor.yy0;} break; case 33: /* ccons ::= DEFAULT scantok term */ {sqlite3AddDefaultValue(pParse,yymsp[0].minor.yy590,yymsp[-1].minor.yy0.z,&yymsp[-1].minor.yy0.z[yymsp[-1].minor.yy0.n]);} @@ -178118,7 +179102,7 @@ static YYACTIONTYPE yy_reduce( {yymsp[-1].minor.yy502 = 0;} break; case 66: /* tconscomma ::= COMMA */ -{pParse->constraintName.n = 0;} +{ASSERT_IS_CREATE; pParse->u1.cr.constraintName.n = 0;} break; case 68: /* tcons ::= PRIMARY KEY LP sortlist autoinc RP onconf */ {sqlite3AddPrimaryKey(pParse,yymsp[-3].minor.yy402,yymsp[0].minor.yy502,yymsp[-2].minor.yy502,0);} @@ -178205,8 +179189,8 @@ static YYACTIONTYPE yy_reduce( if( pRhs ){ pRhs->op = (u8)yymsp[-1].minor.yy502; pRhs->pPrior = pLhs; - if( ALWAYS(pLhs) ) pLhs->selFlags &= ~SF_MultiValue; - pRhs->selFlags &= ~SF_MultiValue; + if( ALWAYS(pLhs) ) pLhs->selFlags &= ~(u32)SF_MultiValue; + pRhs->selFlags &= ~(u32)SF_MultiValue; if( yymsp[-1].minor.yy502!=TK_ALL ) pParse->hasCompound = 1; }else{ sqlite3SelectDelete(pParse->db, pLhs); @@ -179011,6 +179995,10 @@ static YYACTIONTYPE yy_reduce( { sqlite3BeginTrigger(pParse, &yymsp[-7].minor.yy0, &yymsp[-6].minor.yy0, yymsp[-5].minor.yy502, yymsp[-4].minor.yy28.a, yymsp[-4].minor.yy28.b, yymsp[-2].minor.yy563, yymsp[0].minor.yy590, yymsp[-10].minor.yy502, yymsp[-8].minor.yy502); yymsp[-10].minor.yy0 = (yymsp[-6].minor.yy0.n==0?yymsp[-7].minor.yy0:yymsp[-6].minor.yy0); /*A-overwrites-T*/ +#ifdef SQLITE_DEBUG + assert( pParse->isCreate ); /* Set by createkw reduce action */ + pParse->isCreate = 0; /* But, should not be set for CREATE TRIGGER */ +#endif } break; case 262: /* trigger_time ::= BEFORE|AFTER */ @@ -180946,7 +181934,11 @@ SQLITE_PRIVATE int sqlite3RunParser(Parse *pParse, const char *zSql){ assert( n==6 ); tokenType = analyzeFilterKeyword((const u8*)&zSql[6], lastTokenParsed); #endif /* SQLITE_OMIT_WINDOWFUNC */ - }else if( tokenType==TK_COMMENT && (db->flags & SQLITE_Comments)!=0 ){ + }else if( tokenType==TK_COMMENT + && (db->init.busy || (db->flags & SQLITE_Comments)!=0) + ){ + /* Ignore SQL comments if either (1) we are reparsing the schema or + ** (2) SQLITE_DBCONFIG_ENABLE_COMMENTS is turned on (the default). */ zSql += n; continue; }else if( tokenType!=TK_QNUMBER ){ @@ -181841,6 +182833,14 @@ SQLITE_API int sqlite3_initialize(void){ if( rc==SQLITE_OK ){ sqlite3PCacheBufferSetup( sqlite3GlobalConfig.pPage, sqlite3GlobalConfig.szPage, sqlite3GlobalConfig.nPage); +#ifdef SQLITE_EXTRA_INIT_MUTEXED + { + int SQLITE_EXTRA_INIT_MUTEXED(const char*); + rc = SQLITE_EXTRA_INIT_MUTEXED(0); + } +#endif + } + if( rc==SQLITE_OK ){ sqlite3MemoryBarrier(); sqlite3GlobalConfig.isInit = 1; #ifdef SQLITE_EXTRA_INIT @@ -182297,17 +183297,22 @@ SQLITE_API int sqlite3_config(int op, ...){ ** If lookaside is already active, return SQLITE_BUSY. ** ** The sz parameter is the number of bytes in each lookaside slot. -** The cnt parameter is the number of slots. If pStart is NULL the -** space for the lookaside memory is obtained from sqlite3_malloc(). -** If pStart is not NULL then it is sz*cnt bytes of memory to use for -** the lookaside memory. +** The cnt parameter is the number of slots. If pBuf is NULL the +** space for the lookaside memory is obtained from sqlite3_malloc() +** or similar. If pBuf is not NULL then it is sz*cnt bytes of memory +** to use for the lookaside memory. */ -static int setupLookaside(sqlite3 *db, void *pBuf, int sz, int cnt){ +static int setupLookaside( + sqlite3 *db, /* Database connection being configured */ + void *pBuf, /* Memory to use for lookaside. May be NULL */ + int sz, /* Desired size of each lookaside memory slot */ + int cnt /* Number of slots to allocate */ +){ #ifndef SQLITE_OMIT_LOOKASIDE - void *pStart; - sqlite3_int64 szAlloc; - int nBig; /* Number of full-size slots */ - int nSm; /* Number smaller LOOKASIDE_SMALL-byte slots */ + void *pStart; /* Start of the lookaside buffer */ + sqlite3_int64 szAlloc; /* Total space set aside for lookaside memory */ + int nBig; /* Number of full-size slots */ + int nSm; /* Number smaller LOOKASIDE_SMALL-byte slots */ if( sqlite3LookasideUsed(db,0)>0 ){ return SQLITE_BUSY; @@ -182320,19 +183325,22 @@ static int setupLookaside(sqlite3 *db, void *pBuf, int sz, int cnt){ sqlite3_free(db->lookaside.pStart); } /* The size of a lookaside slot after ROUNDDOWN8 needs to be larger - ** than a pointer to be useful. + ** than a pointer and small enough to fit in a u16. */ - sz = ROUNDDOWN8(sz); /* IMP: R-33038-09382 */ + sz = ROUNDDOWN8(sz); if( sz<=(int)sizeof(LookasideSlot*) ) sz = 0; if( sz>65528 ) sz = 65528; - if( cnt<0 ) cnt = 0; + /* Count must be at least 1 to be useful, but not so large as to use + ** more than 0x7fff0000 total bytes for lookaside. */ + if( cnt<1 ) cnt = 0; + if( sz>0 && cnt>(0x7fff0000/sz) ) cnt = 0x7fff0000/sz; szAlloc = (i64)sz*(i64)cnt; - if( sz==0 || cnt==0 ){ + if( szAlloc==0 ){ sz = 0; pStart = 0; }else if( pBuf==0 ){ sqlite3BeginBenignMalloc(); - pStart = sqlite3Malloc( szAlloc ); /* IMP: R-61949-35727 */ + pStart = sqlite3Malloc( szAlloc ); sqlite3EndBenignMalloc(); if( pStart ) szAlloc = sqlite3MallocSize(pStart); }else{ @@ -183309,6 +184317,9 @@ SQLITE_API int sqlite3_busy_handler( db->busyHandler.pBusyArg = pArg; db->busyHandler.nBusy = 0; db->busyTimeout = 0; +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + db->setlkTimeout = 0; +#endif sqlite3_mutex_leave(db->mutex); return SQLITE_OK; } @@ -183358,12 +184369,47 @@ SQLITE_API int sqlite3_busy_timeout(sqlite3 *db, int ms){ sqlite3_busy_handler(db, (int(*)(void*,int))sqliteDefaultBusyCallback, (void*)db); db->busyTimeout = ms; +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + db->setlkTimeout = ms; +#endif }else{ sqlite3_busy_handler(db, 0, 0); } return SQLITE_OK; } +/* +** Set the setlk timeout value. +*/ +SQLITE_API int sqlite3_setlk_timeout(sqlite3 *db, int ms, int flags){ +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + int iDb; + int bBOC = ((flags & SQLITE_SETLK_BLOCK_ON_CONNECT) ? 1 : 0); +#endif +#ifdef SQLITE_ENABLE_API_ARMOR + if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; +#endif + if( ms<-1 ) return SQLITE_RANGE; +#ifdef SQLITE_ENABLE_SETLK_TIMEOUT + db->setlkTimeout = ms; + db->setlkFlags = flags; + sqlite3BtreeEnterAll(db); + for(iDb=0; iDb<db->nDb; iDb++){ + Btree *pBt = db->aDb[iDb].pBt; + if( pBt ){ + sqlite3_file *fd = sqlite3PagerFile(sqlite3BtreePager(pBt)); + sqlite3OsFileControlHint(fd, SQLITE_FCNTL_BLOCK_ON_CONNECT, (void*)&bBOC); + } + } + sqlite3BtreeLeaveAll(db); +#endif +#if !defined(SQLITE_ENABLE_API_ARMOR) && !defined(SQLITE_ENABLE_SETLK_TIMEOUT) + UNUSED_PARAMETER(db); + UNUSED_PARAMETER(flags); +#endif + return SQLITE_OK; +} + /* ** Cause any pending operation to stop at its earliest opportunity. */ @@ -185329,7 +186375,7 @@ SQLITE_API int sqlite3_set_clientdata( return SQLITE_OK; }else{ size_t n = strlen(zName); - p = sqlite3_malloc64( sizeof(DbClientData)+n+1 ); + p = sqlite3_malloc64( SZ_DBCLIENTDATA(n+1) ); if( p==0 ){ if( xDestructor ) xDestructor(pData); sqlite3_mutex_leave(db->mutex); @@ -185483,13 +186529,10 @@ SQLITE_API int sqlite3_table_column_metadata( if( zColumnName==0 ){ /* Query for existence of table only */ }else{ - for(iCol=0; iCol<pTab->nCol; iCol++){ + iCol = sqlite3ColumnIndex(pTab, zColumnName); + if( iCol>=0 ){ pCol = &pTab->aCol[iCol]; - if( 0==sqlite3StrICmp(pCol->zCnName, zColumnName) ){ - break; - } - } - if( iCol==pTab->nCol ){ + }else{ if( HasRowid(pTab) && sqlite3IsRowid(zColumnName) ){ iCol = pTab->iPKey; pCol = iCol>=0 ? &pTab->aCol[iCol] : 0; @@ -185698,8 +186741,8 @@ SQLITE_API int sqlite3_test_control(int op, ...){ /* sqlite3_test_control(SQLITE_TESTCTRL_FK_NO_ACTION, sqlite3 *db, int b); ** ** If b is true, then activate the SQLITE_FkNoAction setting. If b is - ** false then clearn that setting. If the SQLITE_FkNoAction setting is - ** abled, all foreign key ON DELETE and ON UPDATE actions behave as if + ** false then clear that setting. If the SQLITE_FkNoAction setting is + ** enabled, all foreign key ON DELETE and ON UPDATE actions behave as if ** they were NO ACTION, regardless of how they are defined. ** ** NB: One must usually run "PRAGMA writable_schema=RESET" after @@ -187046,7 +188089,7 @@ SQLITE_PRIVATE void sqlite3ConnectionClosed(sqlite3 *db){ ** Here, array { X } means zero or more occurrences of X, adjacent in ** memory. A "position" is an index of a token in the token stream ** generated by the tokenizer. Note that POS_END and POS_COLUMN occur -** in the same logical place as the position element, and act as sentinals +** in the same logical place as the position element, and act as sentinels ** ending a position list array. POS_END is 0. POS_COLUMN is 1. ** The positions numbers are not stored literally but rather as two more ** than the difference from the prior position, or the just the position plus @@ -187265,6 +188308,13 @@ SQLITE_PRIVATE void sqlite3ConnectionClosed(sqlite3 *db){ #ifndef _FTSINT_H #define _FTSINT_H +/* #include <assert.h> */ +/* #include <stdlib.h> */ +/* #include <stddef.h> */ +/* #include <stdio.h> */ +/* #include <string.h> */ +/* #include <stdarg.h> */ + #if !defined(NDEBUG) && !defined(SQLITE_DEBUG) # define NDEBUG 1 #endif @@ -187734,6 +188784,19 @@ typedef sqlite3_int64 i64; /* 8-byte signed integer */ #define deliberate_fall_through +/* +** Macros needed to provide flexible arrays in a portable way +*/ +#ifndef offsetof +# define offsetof(STRUCTURE,FIELD) ((size_t)((char*)&((STRUCTURE*)0)->FIELD)) +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define FLEXARRAY +#else +# define FLEXARRAY 1 +#endif + + #endif /* SQLITE_AMALGAMATION */ #ifdef SQLITE_DEBUG @@ -187838,7 +188901,7 @@ struct Fts3Table { #endif #if defined(SQLITE_DEBUG) || defined(SQLITE_TEST) - /* True to disable the incremental doclist optimization. This is controled + /* True to disable the incremental doclist optimization. This is controlled ** by special insert command 'test-no-incr-doclist'. */ int bNoIncrDoclist; @@ -187890,7 +188953,7 @@ struct Fts3Cursor { /* ** The Fts3Cursor.eSearch member is always set to one of the following. -** Actualy, Fts3Cursor.eSearch can be greater than or equal to +** Actually, Fts3Cursor.eSearch can be greater than or equal to ** FTS3_FULLTEXT_SEARCH. If so, then Fts3Cursor.eSearch - 2 is the index ** of the column to be searched. For example, in ** @@ -187963,9 +189026,13 @@ struct Fts3Phrase { */ int nToken; /* Number of tokens in the phrase */ int iColumn; /* Index of column this phrase must match */ - Fts3PhraseToken aToken[1]; /* One entry for each token in the phrase */ + Fts3PhraseToken aToken[FLEXARRAY]; /* One for each token in the phrase */ }; +/* Size (in bytes) of an Fts3Phrase object large enough to hold N tokens */ +#define SZ_FTS3PHRASE(N) \ + (offsetof(Fts3Phrase,aToken)+(N)*sizeof(Fts3PhraseToken)) + /* ** A tree of these objects forms the RHS of a MATCH operator. ** @@ -188199,12 +189266,6 @@ SQLITE_PRIVATE int sqlite3Fts3IntegrityCheck(Fts3Table *p, int *pbOk); # define SQLITE_CORE 1 #endif -/* #include <assert.h> */ -/* #include <stdlib.h> */ -/* #include <stddef.h> */ -/* #include <stdio.h> */ -/* #include <string.h> */ -/* #include <stdarg.h> */ /* #include "fts3.h" */ #ifndef SQLITE_CORE @@ -190543,7 +191604,7 @@ static int fts3DoclistOrMerge( ** sizes of the two inputs, plus enough space for exactly one of the input ** docids to grow. ** - ** A symetric argument may be made if the doclists are in descending + ** A symmetric argument may be made if the doclists are in descending ** order. */ aOut = sqlite3_malloc64((i64)n1+n2+FTS3_VARINT_MAX-1+FTS3_BUFFER_PADDING); @@ -192342,7 +193403,7 @@ static int fts3EvalDeferredPhrase(Fts3Cursor *pCsr, Fts3Phrase *pPhrase){ nDistance = iPrev - nMaxUndeferred; } - aOut = (char *)sqlite3Fts3MallocZero(nPoslist+FTS3_BUFFER_PADDING); + aOut = (char *)sqlite3Fts3MallocZero(((i64)nPoslist)+FTS3_BUFFER_PADDING); if( !aOut ){ sqlite3_free(aPoslist); return SQLITE_NOMEM; @@ -192641,7 +193702,7 @@ static int incrPhraseTokenNext( ** ** * does not contain any deferred tokens. ** -** Advance it to the next matching documnent in the database and populate +** Advance it to the next matching document in the database and populate ** the Fts3Doclist.pList and nList fields. ** ** If there is no "next" entry and no error occurs, then *pbEof is set to @@ -193648,7 +194709,7 @@ static int fts3EvalNext(Fts3Cursor *pCsr){ } /* -** Restart interation for expression pExpr so that the next call to +** Restart iteration for expression pExpr so that the next call to ** fts3EvalNext() visits the first row. Do not allow incremental ** loading or merging of phrase doclists for this iteration. ** @@ -194840,6 +195901,23 @@ SQLITE_PRIVATE int sqlite3Fts3OpenTokenizer( */ static int fts3ExprParse(ParseContext *, const char *, int, Fts3Expr **, int *); +/* +** Search buffer z[], size n, for a '"' character. Or, if enable_parenthesis +** is defined, search for '(' and ')' as well. Return the index of the first +** such character in the buffer. If there is no such character, return -1. +*/ +static int findBarredChar(const char *z, int n){ + int ii; + for(ii=0; ii<n; ii++){ + if( (z[ii]=='"') + || (sqlite3_fts3_enable_parentheses && (z[ii]=='(' || z[ii]==')')) + ){ + return ii; + } + } + return -1; +} + /* ** Extract the next token from buffer z (length n) using the tokenizer ** and other information (column names etc.) in pParse. Create an Fts3Expr @@ -194864,16 +195942,9 @@ static int getNextToken( int rc; sqlite3_tokenizer_cursor *pCursor; Fts3Expr *pRet = 0; - int i = 0; - /* Set variable i to the maximum number of bytes of input to tokenize. */ - for(i=0; i<n; i++){ - if( sqlite3_fts3_enable_parentheses && (z[i]=='(' || z[i]==')') ) break; - if( z[i]=='"' ) break; - } - - *pnConsumed = i; - rc = sqlite3Fts3OpenTokenizer(pTokenizer, pParse->iLangid, z, i, &pCursor); + *pnConsumed = n; + rc = sqlite3Fts3OpenTokenizer(pTokenizer, pParse->iLangid, z, n, &pCursor); if( rc==SQLITE_OK ){ const char *zToken; int nToken = 0, iStart = 0, iEnd = 0, iPosition = 0; @@ -194881,7 +195952,18 @@ static int getNextToken( rc = pModule->xNext(pCursor, &zToken, &nToken, &iStart, &iEnd, &iPosition); if( rc==SQLITE_OK ){ - nByte = sizeof(Fts3Expr) + sizeof(Fts3Phrase) + nToken; + /* Check that this tokenization did not gobble up any " characters. Or, + ** if enable_parenthesis is true, that it did not gobble up any + ** open or close parenthesis characters either. If it did, call + ** getNextToken() again, but pass only that part of the input buffer + ** up to the first such character. */ + int iBarred = findBarredChar(z, iEnd); + if( iBarred>=0 ){ + pModule->xClose(pCursor); + return getNextToken(pParse, iCol, z, iBarred, ppExpr, pnConsumed); + } + + nByte = sizeof(Fts3Expr) + SZ_FTS3PHRASE(1) + nToken; pRet = (Fts3Expr *)sqlite3Fts3MallocZero(nByte); if( !pRet ){ rc = SQLITE_NOMEM; @@ -194891,7 +195973,7 @@ static int getNextToken( pRet->pPhrase->nToken = 1; pRet->pPhrase->iColumn = iCol; pRet->pPhrase->aToken[0].n = nToken; - pRet->pPhrase->aToken[0].z = (char *)&pRet->pPhrase[1]; + pRet->pPhrase->aToken[0].z = (char*)&pRet->pPhrase->aToken[1]; memcpy(pRet->pPhrase->aToken[0].z, zToken, nToken); if( iEnd<n && z[iEnd]=='*' ){ @@ -194915,7 +195997,11 @@ static int getNextToken( } *pnConsumed = iEnd; - }else if( i && rc==SQLITE_DONE ){ + }else if( n && rc==SQLITE_DONE ){ + int iBarred = findBarredChar(z, n); + if( iBarred>=0 ){ + *pnConsumed = iBarred; + } rc = SQLITE_OK; } @@ -194962,9 +196048,9 @@ static int getNextString( Fts3Expr *p = 0; sqlite3_tokenizer_cursor *pCursor = 0; char *zTemp = 0; - int nTemp = 0; + i64 nTemp = 0; - const int nSpace = sizeof(Fts3Expr) + sizeof(Fts3Phrase); + const int nSpace = sizeof(Fts3Expr) + SZ_FTS3PHRASE(1); int nToken = 0; /* The final Fts3Expr data structure, including the Fts3Phrase, @@ -195336,7 +196422,7 @@ static int fts3ExprParse( /* The isRequirePhrase variable is set to true if a phrase or ** an expression contained in parenthesis is required. If a - ** binary operator (AND, OR, NOT or NEAR) is encounted when + ** binary operator (AND, OR, NOT or NEAR) is encountered when ** isRequirePhrase is set, this is a syntax error. */ if( !isPhrase && isRequirePhrase ){ @@ -195918,7 +197004,6 @@ static void fts3ExprTestCommon( } if( rc!=SQLITE_OK && rc!=SQLITE_NOMEM ){ - sqlite3Fts3ExprFree(pExpr); sqlite3_result_error(context, "Error parsing expression", -1); }else if( rc==SQLITE_NOMEM || !(zBuf = exprToString(pExpr, 0)) ){ sqlite3_result_error_nomem(context); @@ -196161,7 +197246,7 @@ static void fts3HashInsertElement( } -/* Resize the hash table so that it cantains "new_size" buckets. +/* Resize the hash table so that it contains "new_size" buckets. ** "new_size" must be a power of 2. The hash table might fail ** to resize if sqliteMalloc() fails. ** @@ -196616,7 +197701,7 @@ static int star_oh(const char *z){ /* ** If the word ends with zFrom and xCond() is true for the stem -** of the word that preceeds the zFrom ending, then change the +** of the word that precedes the zFrom ending, then change the ** ending to zTo. ** ** The input word *pz and zFrom are both in reverse order. zTo @@ -198127,7 +199212,7 @@ static int fts3tokFilterMethod( fts3tokResetCursor(pCsr); if( idxNum==1 ){ const char *zByte = (const char *)sqlite3_value_text(apVal[0]); - int nByte = sqlite3_value_bytes(apVal[0]); + sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]); pCsr->zInput = sqlite3_malloc64(nByte+1); if( pCsr->zInput==0 ){ rc = SQLITE_NOMEM; @@ -202199,7 +203284,7 @@ static int fts3IncrmergePush( ** ** It is assumed that the buffer associated with pNode is already large ** enough to accommodate the new entry. The buffer associated with pPrev -** is extended by this function if requrired. +** is extended by this function if required. ** ** If an error (i.e. OOM condition) occurs, an SQLite error code is ** returned. Otherwise, SQLITE_OK. @@ -203862,7 +204947,7 @@ SQLITE_PRIVATE int sqlite3Fts3DeferToken( /* ** SQLite value pRowid contains the rowid of a row that may or may not be ** present in the FTS3 table. If it is, delete it and adjust the contents -** of subsiduary data structures accordingly. +** of subsidiary data structures accordingly. */ static int fts3DeleteByRowid( Fts3Table *p, @@ -204188,9 +205273,13 @@ struct MatchinfoBuffer { int nElem; int bGlobal; /* Set if global data is loaded */ char *zMatchinfo; - u32 aMatchinfo[1]; + u32 aMI[FLEXARRAY]; }; +/* Size (in bytes) of a MatchinfoBuffer sufficient for N elements */ +#define SZ_MATCHINFOBUFFER(N) \ + (offsetof(MatchinfoBuffer,aMI)+(((N)+1)/2)*sizeof(u64)) + /* ** The snippet() and offsets() functions both return text values. An instance @@ -204215,13 +205304,13 @@ struct StrBuffer { static MatchinfoBuffer *fts3MIBufferNew(size_t nElem, const char *zMatchinfo){ MatchinfoBuffer *pRet; sqlite3_int64 nByte = sizeof(u32) * (2*(sqlite3_int64)nElem + 1) - + sizeof(MatchinfoBuffer); + + SZ_MATCHINFOBUFFER(1); sqlite3_int64 nStr = strlen(zMatchinfo); pRet = sqlite3Fts3MallocZero(nByte + nStr+1); if( pRet ){ - pRet->aMatchinfo[0] = (u8*)(&pRet->aMatchinfo[1]) - (u8*)pRet; - pRet->aMatchinfo[1+nElem] = pRet->aMatchinfo[0] + pRet->aMI[0] = (u8*)(&pRet->aMI[1]) - (u8*)pRet; + pRet->aMI[1+nElem] = pRet->aMI[0] + sizeof(u32)*((int)nElem+1); pRet->nElem = (int)nElem; pRet->zMatchinfo = ((char*)pRet) + nByte; @@ -204235,10 +205324,10 @@ static MatchinfoBuffer *fts3MIBufferNew(size_t nElem, const char *zMatchinfo){ static void fts3MIBufferFree(void *p){ MatchinfoBuffer *pBuf = (MatchinfoBuffer*)((u8*)p - ((u32*)p)[-1]); - assert( (u32*)p==&pBuf->aMatchinfo[1] - || (u32*)p==&pBuf->aMatchinfo[pBuf->nElem+2] + assert( (u32*)p==&pBuf->aMI[1] + || (u32*)p==&pBuf->aMI[pBuf->nElem+2] ); - if( (u32*)p==&pBuf->aMatchinfo[1] ){ + if( (u32*)p==&pBuf->aMI[1] ){ pBuf->aRef[1] = 0; }else{ pBuf->aRef[2] = 0; @@ -204255,18 +205344,18 @@ static void (*fts3MIBufferAlloc(MatchinfoBuffer *p, u32 **paOut))(void*){ if( p->aRef[1]==0 ){ p->aRef[1] = 1; - aOut = &p->aMatchinfo[1]; + aOut = &p->aMI[1]; xRet = fts3MIBufferFree; } else if( p->aRef[2]==0 ){ p->aRef[2] = 1; - aOut = &p->aMatchinfo[p->nElem+2]; + aOut = &p->aMI[p->nElem+2]; xRet = fts3MIBufferFree; }else{ aOut = (u32*)sqlite3_malloc64(p->nElem * sizeof(u32)); if( aOut ){ xRet = sqlite3_free; - if( p->bGlobal ) memcpy(aOut, &p->aMatchinfo[1], p->nElem*sizeof(u32)); + if( p->bGlobal ) memcpy(aOut, &p->aMI[1], p->nElem*sizeof(u32)); } } @@ -204276,7 +205365,7 @@ static void (*fts3MIBufferAlloc(MatchinfoBuffer *p, u32 **paOut))(void*){ static void fts3MIBufferSetGlobal(MatchinfoBuffer *p){ p->bGlobal = 1; - memcpy(&p->aMatchinfo[2+p->nElem], &p->aMatchinfo[1], p->nElem*sizeof(u32)); + memcpy(&p->aMI[2+p->nElem], &p->aMI[1], p->nElem*sizeof(u32)); } /* @@ -204691,7 +205780,7 @@ static int fts3StringAppend( } /* If there is insufficient space allocated at StrBuffer.z, use realloc() - ** to grow the buffer until so that it is big enough to accomadate the + ** to grow the buffer until so that it is big enough to accommodate the ** appended data. */ if( pStr->n+nAppend+1>=pStr->nAlloc ){ @@ -205103,16 +206192,16 @@ static size_t fts3MatchinfoSize(MatchInfo *pInfo, char cArg){ break; case FTS3_MATCHINFO_LHITS: - nVal = pInfo->nCol * pInfo->nPhrase; + nVal = (size_t)pInfo->nCol * pInfo->nPhrase; break; case FTS3_MATCHINFO_LHITS_BM: - nVal = pInfo->nPhrase * ((pInfo->nCol + 31) / 32); + nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); break; default: assert( cArg==FTS3_MATCHINFO_HITS ); - nVal = pInfo->nCol * pInfo->nPhrase * 3; + nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3; break; } @@ -206670,8 +207759,8 @@ SQLITE_PRIVATE int sqlite3FtsUnicodeFold(int c, int eRemoveDiacritic){ ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also ** accept BLOB values that have JSON encoded using a binary representation ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk -** format SQLite JSONB is completely different and incompatible with -** PostgreSQL JSONB. +** format for SQLite-JSONB is completely different and incompatible with +** PostgreSQL-JSONB. ** ** Decoding and interpreting JSONB is still O(N) where N is the size of ** the input, the same as text JSON. However, the constant of proportionality @@ -206728,7 +207817,7 @@ SQLITE_PRIVATE int sqlite3FtsUnicodeFold(int c, int eRemoveDiacritic){ ** ** The payload size need not be expressed in its minimal form. For example, ** if the payload size is 10, the size can be expressed in any of 5 different -** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte, +** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte, ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and ** a single byte of 0x0a. The shorter forms are preferred, of course, but @@ -206738,7 +207827,7 @@ SQLITE_PRIVATE int sqlite3FtsUnicodeFold(int c, int eRemoveDiacritic){ ** the size when it becomes known, resulting in a non-minimal encoding. ** ** The value (X>>4)==15 is not actually used in the current implementation -** (as SQLite is currently unable handle BLOBs larger than about 2GB) +** (as SQLite is currently unable to handle BLOBs larger than about 2GB) ** but is included in the design to allow for future enhancements. ** ** The payload follows the header. NULL, TRUE, and FALSE have no payload and @@ -206798,23 +207887,47 @@ static const char * const jsonbType[] = { ** increase for the text-JSON parser. (Ubuntu14.10 gcc 4.8.4 x64 with -Os). */ static const char jsonIsSpace[] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +#ifdef SQLITE_ASCII +/*0 1 2 3 4 5 6 7 8 9 a b c d e f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, /* 0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 1 */ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 2 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 3 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 4 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 5 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 6 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 7 */ + + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 8 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 9 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* a */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* b */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* c */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* d */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* e */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* f */ +#endif +#ifdef SQLITE_EBCDIC +/*0 1 2 3 4 5 6 7 8 9 a b c d e f */ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, /* 0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 1 */ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 2 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 3 */ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 4 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 5 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 6 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 7 */ + + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 8 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 9 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* a */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* b */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* c */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* d */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* e */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* f */ +#endif - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; #define jsonIsspace(x) (jsonIsSpace[(unsigned char)x]) @@ -206822,7 +207935,13 @@ static const char jsonIsSpace[] = { ** The set of all space characters recognized by jsonIsspace(). ** Useful as the second argument to strspn(). */ +#ifdef SQLITE_ASCII static const char jsonSpaces[] = "\011\012\015\040"; +#endif +#ifdef SQLITE_EBCDIC +static const char jsonSpaces[] = "\005\045\015\100"; +#endif + /* ** Characters that are special to JSON. Control characters, @@ -206831,23 +207950,46 @@ static const char jsonSpaces[] = "\011\012\015\040"; ** it in the set of special characters. */ static const char jsonIsOk[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +#ifdef SQLITE_ASCII +/*0 1 2 3 4 5 6 7 8 9 a b c d e f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 1 */ + 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, /* 2 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 3 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 4 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, /* 5 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 6 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 7 */ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 8 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 9 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* a */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* b */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* c */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* d */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* e */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 /* f */ +#endif +#ifdef SQLITE_EBCDIC +/*0 1 2 3 4 5 6 7 8 9 a b c d e f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 1 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 2 */ + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, /* 3 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 4 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 5 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 6 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, /* 7 */ + + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 8 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 9 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* a */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* b */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* c */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* d */ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* e */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 /* f */ +#endif }; /* Objects */ @@ -206992,7 +208134,7 @@ struct JsonParse { ** Forward references **************************************************************************/ static void jsonReturnStringAsBlob(JsonString*); -static int jsonFuncArgMightBeBinary(sqlite3_value *pJson); +static int jsonArgIsJsonb(sqlite3_value *pJson, JsonParse *p); static u32 jsonTranslateBlobToText(const JsonParse*,u32,JsonString*); static void jsonReturnParse(sqlite3_context*,JsonParse*); static JsonParse *jsonParseFuncArg(sqlite3_context*,sqlite3_value*,u32); @@ -207066,7 +208208,7 @@ static int jsonCacheInsert( ** most-recently used entry if it isn't so already. ** ** The JsonParse object returned still belongs to the Cache and might -** be deleted at any moment. If the caller whants the JsonParse to +** be deleted at any moment. If the caller wants the JsonParse to ** linger, it needs to increment the nPJRef reference counter. */ static JsonParse *jsonCacheSearch( @@ -207410,11 +208552,9 @@ static void jsonAppendSqlValue( break; } default: { - if( jsonFuncArgMightBeBinary(pValue) ){ - JsonParse px; - memset(&px, 0, sizeof(px)); - px.aBlob = (u8*)sqlite3_value_blob(pValue); - px.nBlob = sqlite3_value_bytes(pValue); + JsonParse px; + memset(&px, 0, sizeof(px)); + if( jsonArgIsJsonb(pValue, &px) ){ jsonTranslateBlobToText(&px, 0, p); }else if( p->eErr==0 ){ sqlite3_result_error(p->pCtx, "JSON cannot hold BLOB values", -1); @@ -207733,7 +208873,7 @@ static void jsonWrongNumArgs( */ static int jsonBlobExpand(JsonParse *pParse, u32 N){ u8 *aNew; - u32 t; + u64 t; assert( N>pParse->nBlobAlloc ); if( pParse->nBlobAlloc==0 ){ t = 100; @@ -207743,8 +208883,9 @@ static int jsonBlobExpand(JsonParse *pParse, u32 N){ if( t<N ) t = N+100; aNew = sqlite3DbRealloc(pParse->db, pParse->aBlob, t); if( aNew==0 ){ pParse->oom = 1; return 1; } + assert( t<0x7fffffff ); pParse->aBlob = aNew; - pParse->nBlobAlloc = t; + pParse->nBlobAlloc = (u32)t; return 0; } @@ -207811,7 +208952,7 @@ static SQLITE_NOINLINE void jsonBlobExpandAndAppendNode( } -/* Append an node type byte together with the payload size and +/* Append a node type byte together with the payload size and ** possibly also the payload. ** ** If aPayload is not NULL, then it is a pointer to the payload which @@ -208351,7 +209492,12 @@ json_parse_restart: || c=='n' || c=='r' || c=='t' || (c=='u' && jsonIs4Hex(&z[j+1])) ){ if( opcode==JSONB_TEXT ) opcode = JSONB_TEXTJ; - }else if( c=='\'' || c=='0' || c=='v' || c=='\n' + }else if( c=='\'' || c=='v' || c=='\n' +#ifdef SQLITE_BUG_COMPATIBLE_20250510 + || (c=='0') /* Legacy bug compatible */ +#else + || (c=='0' && !sqlite3Isdigit(z[j+1])) /* Correct implementation */ +#endif || (0xe2==(u8)c && 0x80==(u8)z[j+1] && (0xa8==(u8)z[j+2] || 0xa9==(u8)z[j+2])) || (c=='x' && jsonIs2Hex(&z[j+1])) ){ @@ -208701,10 +209847,7 @@ static u32 jsonbPayloadSize(const JsonParse *pParse, u32 i, u32 *pSz){ u8 x; u32 sz; u32 n; - if( NEVER(i>pParse->nBlob) ){ - *pSz = 0; - return 0; - } + assert( i<=pParse->nBlob ); x = pParse->aBlob[i]>>4; if( x<=11 ){ sz = x; @@ -208741,15 +209884,15 @@ static u32 jsonbPayloadSize(const JsonParse *pParse, u32 i, u32 *pSz){ *pSz = 0; return 0; } - sz = (pParse->aBlob[i+5]<<24) + (pParse->aBlob[i+6]<<16) + + sz = ((u32)pParse->aBlob[i+5]<<24) + (pParse->aBlob[i+6]<<16) + (pParse->aBlob[i+7]<<8) + pParse->aBlob[i+8]; n = 9; } if( (i64)i+sz+n > pParse->nBlob && (i64)i+sz+n > pParse->nBlob-pParse->delta ){ - sz = 0; - n = 0; + *pSz = 0; + return 0; } *pSz = sz; return n; @@ -208846,9 +209989,12 @@ static u32 jsonTranslateBlobToText( } case JSONB_TEXT: case JSONB_TEXTJ: { - jsonAppendChar(pOut, '"'); - jsonAppendRaw(pOut, (const char*)&pParse->aBlob[i+n], sz); - jsonAppendChar(pOut, '"'); + if( pOut->nUsed+sz+2<=pOut->nAlloc || jsonStringGrow(pOut, sz+2)==0 ){ + pOut->zBuf[pOut->nUsed] = '"'; + memcpy(pOut->zBuf+pOut->nUsed+1,(const char*)&pParse->aBlob[i+n],sz); + pOut->zBuf[pOut->nUsed+sz+1] = '"'; + pOut->nUsed += sz+2; + } break; } case JSONB_TEXT5: { @@ -209087,33 +210233,6 @@ static u32 jsonTranslateBlobToPrettyText( return i; } - -/* Return true if the input pJson -** -** For performance reasons, this routine does not do a detailed check of the -** input BLOB to ensure that it is well-formed. Hence, false positives are -** possible. False negatives should never occur, however. -*/ -static int jsonFuncArgMightBeBinary(sqlite3_value *pJson){ - u32 sz, n; - const u8 *aBlob; - int nBlob; - JsonParse s; - if( sqlite3_value_type(pJson)!=SQLITE_BLOB ) return 0; - aBlob = sqlite3_value_blob(pJson); - nBlob = sqlite3_value_bytes(pJson); - if( nBlob<1 ) return 0; - if( NEVER(aBlob==0) || (aBlob[0] & 0x0f)>JSONB_OBJECT ) return 0; - memset(&s, 0, sizeof(s)); - s.aBlob = (u8*)aBlob; - s.nBlob = nBlob; - n = jsonbPayloadSize(&s, 0, &sz); - if( n==0 ) return 0; - if( sz+n!=(u32)nBlob ) return 0; - if( (aBlob[0] & 0x0f)<=JSONB_FALSE && sz>0 ) return 0; - return sz+n==(u32)nBlob; -} - /* ** Given that a JSONB_ARRAY object starts at offset i, return ** the number of entries in that array. @@ -209146,6 +210265,82 @@ static void jsonAfterEditSizeAdjust(JsonParse *pParse, u32 iRoot){ pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz); } +/* +** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the +** size field) by d bytes, then write the expansion into aOut[] and +** return true. In this way, an overwrite happens without changing the +** size of the JSONB, which reduces memcpy() operations and also make it +** faster and easier to update the B-Tree entry that contains the JSONB +** in the database. +** +** If the expansion of aIns[] by d bytes cannot be (easily) accomplished +** then return false. +** +** The d parameter is guaranteed to be between 1 and 8. +** +** This routine is an optimization. A correct answer is obtained if it +** always leaves the output unchanged and returns false. +*/ +static int jsonBlobOverwrite( + u8 *aOut, /* Overwrite here */ + const u8 *aIns, /* New content */ + u32 nIns, /* Bytes of new content */ + u32 d /* Need to expand new content by this much */ +){ + u32 szPayload; /* Bytes of payload */ + u32 i; /* New header size, after expansion & a loop counter */ + u8 szHdr; /* Size of header before expansion */ + + /* Lookup table for finding the upper 4 bits of the first byte of the + ** expanded aIns[], based on the size of the expanded aIns[] header: + ** + ** 2 3 4 5 6 7 8 9 */ + static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 }; + + if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */ + switch( aIns[0]>>4 ){ + default: { /* aIns[] header size 1 */ + if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */ + i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */ + szHdr = 1; + break; + } + case 12: { /* aIns[] header size is 2 */ + if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */ + i = d + 2; /* New hdr sz: 2, 5, or 9 */ + szHdr = 2; + break; + } + case 13: { /* aIns[] header size is 3 */ + if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */ + i = d + 3; /* New hdr sz: 5 or 9 */ + szHdr = 3; + break; + } + case 14: { /* aIns[] header size is 5 */ + if( d!=4 ) return 0; /* d must be 4 */ + i = 9; /* New hdr sz: 9 */ + szHdr = 5; + break; + } + case 15: { /* aIns[] header size is 9 */ + return 0; /* No solution */ + } + } + assert( i>=2 && i<=9 && aType[i-2]!=0 ); + aOut[0] = (aIns[0] & 0x0f) | aType[i-2]; + memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr); + szPayload = nIns - szHdr; + while( 1/*edit-by-break*/ ){ + i--; + aOut[i] = szPayload & 0xff; + if( i==1 ) break; + szPayload >>= 8; + } + assert( (szPayload>>8)==0 ); + return 1; +} + /* ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of ** content beginning at iDel, and replacing them with nIns bytes of @@ -209167,6 +210362,11 @@ static void jsonBlobEdit( u32 nIns /* Bytes of content to insert */ ){ i64 d = (i64)nIns - (i64)nDel; + if( d<0 && d>=(-8) && aIns!=0 + && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d) + ){ + return; + } if( d!=0 ){ if( pParse->nBlob + d > pParse->nBlobAlloc ){ jsonBlobExpand(pParse, pParse->nBlob+d); @@ -209178,7 +210378,9 @@ static void jsonBlobEdit( pParse->nBlob += d; pParse->delta += d; } - if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns); + if( nIns && aIns ){ + memcpy(&pParse->aBlob[iDel], aIns, nIns); + } } /* @@ -209263,7 +210465,21 @@ static u32 jsonUnescapeOneChar(const char *z, u32 n, u32 *piOut){ case 'r': { *piOut = '\r'; return 2; } case 't': { *piOut = '\t'; return 2; } case 'v': { *piOut = '\v'; return 2; } - case '0': { *piOut = 0; return 2; } + case '0': { + /* JSON5 requires that the \0 escape not be followed by a digit. + ** But SQLite did not enforce this restriction in versions 3.42.0 + ** through 3.49.2. That was a bug. But some applications might have + ** come to depend on that bug. Use the SQLITE_BUG_COMPATIBLE_20250510 + ** option to restore the old buggy behavior. */ +#ifdef SQLITE_BUG_COMPATIBLE_20250510 + /* Legacy bug-compatible behavior */ + *piOut = 0; +#else + /* Correct behavior */ + *piOut = (n>2 && sqlite3Isdigit(z[2])) ? JSON_INVALID_CHAR : 0; +#endif + return 2; + } case '\'': case '"': case '/': @@ -209763,7 +210979,7 @@ static void jsonReturnFromBlob( char *zOut; u32 nOut = sz; z = (const char*)&pParse->aBlob[i+n]; - zOut = sqlite3DbMallocRaw(db, nOut+1); + zOut = sqlite3DbMallocRaw(db, ((u64)nOut)+1); if( zOut==0 ) goto returnfromblob_oom; for(iIn=iOut=0; iIn<sz; iIn++){ char c = z[iIn]; @@ -209858,10 +211074,7 @@ static int jsonFunctionArgToBlob( return 0; } case SQLITE_BLOB: { - if( jsonFuncArgMightBeBinary(pArg) ){ - pParse->aBlob = (u8*)sqlite3_value_blob(pArg); - pParse->nBlob = sqlite3_value_bytes(pArg); - }else{ + if( !jsonArgIsJsonb(pArg, pParse) ){ sqlite3_result_error(ctx, "JSON cannot hold BLOB values", -1); return 1; } @@ -209941,7 +211154,7 @@ static char *jsonBadPathError( } /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent -** arguments come in parse where each pair contains a JSON path and +** arguments come in pairs where each pair contains a JSON path and ** content to insert or set at that patch. Do the updates ** and return the result. ** @@ -210012,27 +211225,46 @@ jsonInsertIntoBlob_patherror: /* ** If pArg is a blob that seems like a JSONB blob, then initialize ** p to point to that JSONB and return TRUE. If pArg does not seem like -** a JSONB blob, then return FALSE; +** a JSONB blob, then return FALSE. ** -** This routine is only called if it is already known that pArg is a -** blob. The only open question is whether or not the blob appears -** to be a JSONB blob. +** For small BLOBs (having no more than 7 bytes of payload) a full +** validity check is done. So for small BLOBs this routine only returns +** true if the value is guaranteed to be a valid JSONB. For larger BLOBs +** (8 byte or more of payload) only the size of the outermost element is +** checked to verify that the BLOB is superficially valid JSONB. +** +** A full JSONB validation is done on smaller BLOBs because those BLOBs might +** also be text JSON that has been incorrectly cast into a BLOB. +** (See tag-20240123-a and https://sqlite.org/forum/forumpost/012136abd5) +** If the BLOB is 9 bytes are larger, then it is not possible for the +** superficial size check done here to pass if the input is really text +** JSON so we do not need to look deeper in that case. +** +** Why we only need to do full JSONB validation for smaller BLOBs: +** +** The first byte of valid JSON text must be one of: '{', '[', '"', ' ', '\n', +** '\r', '\t', '-', or a digit '0' through '9'. Of these, only a subset +** can also be the first byte of JSONB: '{', '[', and digits '3' +** through '9'. In every one of those cases, the payload size is 7 bytes +** or less. So if we do full JSONB validation for every BLOB where the +** payload is less than 7 bytes, we will never get a false positive for +** JSONB on an input that is really text JSON. */ static int jsonArgIsJsonb(sqlite3_value *pArg, JsonParse *p){ u32 n, sz = 0; + u8 c; + if( sqlite3_value_type(pArg)!=SQLITE_BLOB ) return 0; p->aBlob = (u8*)sqlite3_value_blob(pArg); p->nBlob = (u32)sqlite3_value_bytes(pArg); - if( p->nBlob==0 ){ - p->aBlob = 0; - return 0; - } - if( NEVER(p->aBlob==0) ){ - return 0; - } - if( (p->aBlob[0] & 0x0f)<=JSONB_OBJECT + if( p->nBlob>0 + && ALWAYS(p->aBlob!=0) + && ((c = p->aBlob[0]) & 0x0f)<=JSONB_OBJECT && (n = jsonbPayloadSize(p, 0, &sz))>0 && sz+n==p->nBlob - && ((p->aBlob[0] & 0x0f)>JSONB_FALSE || sz==0) + && ((c & 0x0f)>JSONB_FALSE || sz==0) + && (sz>7 + || (c!=0x7b && c!=0x5b && !sqlite3Isdigit(c)) + || jsonbValidityCheck(p, 0, p->nBlob, 1)==0) ){ return 1; } @@ -210110,7 +211342,7 @@ rebuild_from_cache: ** JSON functions were suppose to work. From the beginning, blob was ** reserved for expansion and a blob value should have raised an error. ** But it did not, due to a bug. And many applications came to depend - ** upon this buggy behavior, espeically when using the CLI and reading + ** upon this buggy behavior, especially when using the CLI and reading ** JSON text using readfile(), which returns a blob. For this reason ** we will continue to support the bug moving forward. ** See for example https://sqlite.org/forum/forumpost/012136abd5292b8d @@ -211125,21 +212357,17 @@ static void jsonValidFunc( return; } case SQLITE_BLOB: { - if( jsonFuncArgMightBeBinary(argv[0]) ){ + JsonParse py; + memset(&py, 0, sizeof(py)); + if( jsonArgIsJsonb(argv[0], &py) ){ if( flags & 0x04 ){ /* Superficial checking only - accomplished by the - ** jsonFuncArgMightBeBinary() call above. */ + ** jsonArgIsJsonb() call above. */ res = 1; }else if( flags & 0x08 ){ /* Strict checking. Check by translating BLOB->TEXT->BLOB. If ** no errors occur, call that a "strict check". */ - JsonParse px; - u32 iErr; - memset(&px, 0, sizeof(px)); - px.aBlob = (u8*)sqlite3_value_blob(argv[0]); - px.nBlob = sqlite3_value_bytes(argv[0]); - iErr = jsonbValidityCheck(&px, 0, px.nBlob, 1); - res = iErr==0; + res = 0==jsonbValidityCheck(&py, 0, py.nBlob, 1); } break; } @@ -211197,9 +212425,7 @@ static void jsonErrorFunc( UNUSED_PARAMETER(argc); memset(&s, 0, sizeof(s)); s.db = sqlite3_context_db_handle(ctx); - if( jsonFuncArgMightBeBinary(argv[0]) ){ - s.aBlob = (u8*)sqlite3_value_blob(argv[0]); - s.nBlob = sqlite3_value_bytes(argv[0]); + if( jsonArgIsJsonb(argv[0], &s) ){ iErrPos = (i64)jsonbValidityCheck(&s, 0, s.nBlob, 1); }else{ s.zJson = (char*)sqlite3_value_text(argv[0]); @@ -211360,18 +212586,20 @@ static void jsonObjectStep( UNUSED_PARAMETER(argc); pStr = (JsonString*)sqlite3_aggregate_context(ctx, sizeof(*pStr)); if( pStr ){ + z = (const char*)sqlite3_value_text(argv[0]); + n = sqlite3Strlen30(z); if( pStr->zBuf==0 ){ jsonStringInit(pStr, ctx); jsonAppendChar(pStr, '{'); - }else if( pStr->nUsed>1 ){ + }else if( pStr->nUsed>1 && z!=0 ){ jsonAppendChar(pStr, ','); } pStr->pCtx = ctx; - z = (const char*)sqlite3_value_text(argv[0]); - n = sqlite3Strlen30(z); - jsonAppendString(pStr, z, n); - jsonAppendChar(pStr, ':'); - jsonAppendSqlValue(pStr, argv[1]); + if( z!=0 ){ + jsonAppendString(pStr, z, n); + jsonAppendChar(pStr, ':'); + jsonAppendSqlValue(pStr, argv[1]); + } } } static void jsonObjectCompute(sqlite3_context *ctx, int isFinal){ @@ -211884,9 +213112,8 @@ static int jsonEachFilter( memset(&p->sParse, 0, sizeof(p->sParse)); p->sParse.nJPRef = 1; p->sParse.db = p->db; - if( jsonFuncArgMightBeBinary(argv[0]) ){ - p->sParse.nBlob = sqlite3_value_bytes(argv[0]); - p->sParse.aBlob = (u8*)sqlite3_value_blob(argv[0]); + if( jsonArgIsJsonb(argv[0], &p->sParse) ){ + /* We have JSONB */ }else{ p->sParse.zJson = (char*)sqlite3_value_text(argv[0]); p->sParse.nJson = sqlite3_value_bytes(argv[0]); @@ -212210,6 +213437,14 @@ typedef unsigned int u32; # define ALWAYS(X) (X) # define NEVER(X) (X) #endif +#ifndef offsetof +#define offsetof(STRUCTURE,FIELD) ((size_t)((char*)&((STRUCTURE*)0)->FIELD)) +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define FLEXARRAY +#else +# define FLEXARRAY 1 +#endif #endif /* !defined(SQLITE_AMALGAMATION) */ /* Macro to check for 4-byte alignment. Only used inside of assert() */ @@ -212530,9 +213765,13 @@ struct RtreeMatchArg { RtreeGeomCallback cb; /* Info about the callback functions */ int nParam; /* Number of parameters to the SQL function */ sqlite3_value **apSqlParam; /* Original SQL parameter values */ - RtreeDValue aParam[1]; /* Values for parameters to the SQL function */ + RtreeDValue aParam[FLEXARRAY]; /* Values for parameters to the SQL function */ }; +/* Size of an RtreeMatchArg object with N parameters */ +#define SZ_RTREEMATCHARG(N) \ + (offsetof(RtreeMatchArg,aParam)+(N)*sizeof(RtreeDValue)) + #ifndef MAX # define MAX(x,y) ((x) < (y) ? (y) : (x)) #endif @@ -214221,7 +215460,7 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){ } /* -** Return the N-dimensional volumn of the cell stored in *p. +** Return the N-dimensional volume of the cell stored in *p. */ static RtreeDValue cellArea(Rtree *pRtree, RtreeCell *p){ RtreeDValue area = (RtreeDValue)1; @@ -215987,7 +217226,7 @@ static sqlite3_stmt *rtreeCheckPrepare( /* ** The second and subsequent arguments to this function are a printf() ** style format string and arguments. This function formats the string and -** appends it to the report being accumuated in pCheck. +** appends it to the report being accumulated in pCheck. */ static void rtreeCheckAppendMsg(RtreeCheck *pCheck, const char *zFmt, ...){ va_list ap; @@ -217175,7 +218414,7 @@ static void geopolyBBoxFinal( ** Determine if point (x0,y0) is beneath line segment (x1,y1)->(x2,y2). ** Returns: ** -** +2 x0,y0 is on the line segement +** +2 x0,y0 is on the line segment ** ** +1 x0,y0 is beneath line segment ** @@ -217281,7 +218520,7 @@ static void geopolyWithinFunc( sqlite3_free(p2); } -/* Objects used by the overlap algorihm. */ +/* Objects used by the overlap algorithm. */ typedef struct GeoEvent GeoEvent; typedef struct GeoSegment GeoSegment; typedef struct GeoOverlap GeoOverlap; @@ -218328,8 +219567,7 @@ static void geomCallback(sqlite3_context *ctx, int nArg, sqlite3_value **aArg){ sqlite3_int64 nBlob; int memErr = 0; - nBlob = sizeof(RtreeMatchArg) + (nArg-1)*sizeof(RtreeDValue) - + nArg*sizeof(sqlite3_value*); + nBlob = SZ_RTREEMATCHARG(nArg) + nArg*sizeof(sqlite3_value*); pBlob = (RtreeMatchArg *)sqlite3_malloc64(nBlob); if( !pBlob ){ sqlite3_result_error_nomem(ctx); @@ -219424,7 +220662,7 @@ SQLITE_PRIVATE void sqlite3Fts3IcuTokenizerModule( ** ** "RBU" stands for "Resumable Bulk Update". As in a large database update ** transmitted via a wireless network to a mobile device. A transaction -** applied using this extension is hence refered to as an "RBU update". +** applied using this extension is hence referred to as an "RBU update". ** ** ** LIMITATIONS @@ -219721,7 +220959,7 @@ SQLITE_API sqlite3rbu *sqlite3rbu_open( ** the next call to sqlite3rbu_vacuum() opens a handle that starts a ** new RBU vacuum operation. ** -** As with sqlite3rbu_open(), Zipvfs users should rever to the comment +** As with sqlite3rbu_open(), Zipvfs users should refer to the comment ** describing the sqlite3rbu_create_vfs() API function below for ** a description of the complications associated with using RBU with ** zipvfs databases. @@ -219817,7 +221055,7 @@ SQLITE_API int sqlite3rbu_savestate(sqlite3rbu *pRbu); ** ** If the RBU update has been completely applied, mark the RBU database ** as fully applied. Otherwise, assuming no error has occurred, save the -** current state of the RBU update appliation to the RBU database. +** current state of the RBU update application to the RBU database. ** ** If an error has already occurred as part of an sqlite3rbu_step() ** or sqlite3rbu_open() call, or if one occurs within this function, an @@ -224743,7 +225981,7 @@ static int rbuVfsFileSize(sqlite3_file *pFile, sqlite_int64 *pSize){ /* If this is an RBU vacuum operation and this is the target database, ** pretend that it has at least one page. Otherwise, SQLite will not - ** check for the existance of a *-wal file. rbuVfsRead() contains + ** check for the existence of a *-wal file. rbuVfsRead() contains ** similar logic. */ if( rc==SQLITE_OK && *pSize==0 && p->pRbu && rbuIsVacuum(p->pRbu) @@ -226675,8 +227913,8 @@ static int dbpageUpdate( /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and ** all subsequent pages to be deleted. */ pTab->iDbTrunc = iDb; - pgno--; - pTab->pgnoTrunc = pgno; + pTab->pgnoTrunc = pgno-1; + pgno = 1; }else{ zErr = "bad page value"; goto update_fail; @@ -227973,7 +229211,7 @@ static int sessionTableInfo( /* ** This function is called to initialize the SessionTable.nCol, azCol[] ** abPK[] and azDflt[] members of SessionTable object pTab. If these -** fields are already initilialized, this function is a no-op. +** fields are already initialized, this function is a no-op. ** ** If an error occurs, an error code is stored in sqlite3_session.rc and ** non-zero returned. Or, if no error occurs but the table has no primary @@ -227992,6 +229230,8 @@ static int sessionInitTable( if( pTab->nCol==0 ){ u8 *abPK; assert( pTab->azCol==0 || pTab->abPK==0 ); + sqlite3_free(pTab->azCol); + pTab->abPK = 0; rc = sessionTableInfo(pSession, db, zDb, pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol, &pTab->azDflt, &pTab->aiIdx, &abPK, @@ -228999,7 +230239,9 @@ SQLITE_API int sqlite3session_diff( SessionTable *pTo; /* Table zTbl */ /* Locate and if necessary initialize the target table object */ + pSession->bAutoAttach++; rc = sessionFindTable(pSession, zTbl, &pTo); + pSession->bAutoAttach--; if( pTo==0 ) goto diff_out; if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){ rc = pSession->rc; @@ -229010,17 +230252,43 @@ SQLITE_API int sqlite3session_diff( if( rc==SQLITE_OK ){ int bHasPk = 0; int bMismatch = 0; - int nCol; /* Columns in zFrom.zTbl */ + int nCol = 0; /* Columns in zFrom.zTbl */ int bRowid = 0; - u8 *abPK; + u8 *abPK = 0; const char **azCol = 0; - rc = sessionTableInfo(0, db, zFrom, zTbl, - &nCol, 0, 0, &azCol, 0, 0, &abPK, - pSession->bImplicitPK ? &bRowid : 0 - ); + char *zDbExists = 0; + + /* Check that database zFrom is attached. */ + zDbExists = sqlite3_mprintf("SELECT * FROM %Q.sqlite_schema", zFrom); + if( zDbExists==0 ){ + rc = SQLITE_NOMEM; + }else{ + sqlite3_stmt *pDbExists = 0; + rc = sqlite3_prepare_v2(db, zDbExists, -1, &pDbExists, 0); + if( rc==SQLITE_ERROR ){ + rc = SQLITE_OK; + nCol = -1; + } + sqlite3_finalize(pDbExists); + sqlite3_free(zDbExists); + } + + if( rc==SQLITE_OK && nCol==0 ){ + rc = sessionTableInfo(0, db, zFrom, zTbl, + &nCol, 0, 0, &azCol, 0, 0, &abPK, + pSession->bImplicitPK ? &bRowid : 0 + ); + } if( rc==SQLITE_OK ){ if( pTo->nCol!=nCol ){ - bMismatch = 1; + if( nCol<=0 ){ + rc = SQLITE_SCHEMA; + if( pzErrMsg ){ + *pzErrMsg = sqlite3_mprintf("no such table: %s.%s", zFrom, zTbl); + } + }else{ + bMismatch = 1; + } }else{ int i; for(i=0; i<nCol; i++){ @@ -229796,7 +231064,7 @@ static int sessionGenerateChangeset( ){ sqlite3 *db = pSession->db; /* Source database handle */ SessionTable *pTab; /* Used to iterate through attached tables */ - SessionBuffer buf = {0,0,0}; /* Buffer in which to accumlate changeset */ + SessionBuffer buf = {0,0,0}; /* Buffer in which to accumulate changeset */ int rc; /* Return code */ assert( xOutput==0 || (pnChangeset==0 && ppChangeset==0) ); @@ -230149,14 +231417,15 @@ SQLITE_API int sqlite3changeset_start_v2_strm( ** object and the buffer is full, discard some data to free up space. */ static void sessionDiscardData(SessionInput *pIn){ - if( pIn->xInput && pIn->iNext>=sessions_strm_chunk_size ){ - int nMove = pIn->buf.nBuf - pIn->iNext; + if( pIn->xInput && pIn->iCurrent>=sessions_strm_chunk_size ){ + int nMove = pIn->buf.nBuf - pIn->iCurrent; assert( nMove>=0 ); if( nMove>0 ){ - memmove(pIn->buf.aBuf, &pIn->buf.aBuf[pIn->iNext], nMove); + memmove(pIn->buf.aBuf, &pIn->buf.aBuf[pIn->iCurrent], nMove); } - pIn->buf.nBuf -= pIn->iNext; - pIn->iNext = 0; + pIn->buf.nBuf -= pIn->iCurrent; + pIn->iNext -= pIn->iCurrent; + pIn->iCurrent = 0; pIn->nData = pIn->buf.nBuf; } } @@ -230510,8 +231779,8 @@ static int sessionChangesetNextOne( p->rc = sessionInputBuffer(&p->in, 2); if( p->rc!=SQLITE_OK ) return p->rc; - sessionDiscardData(&p->in); p->in.iCurrent = p->in.iNext; + sessionDiscardData(&p->in); /* If the iterator is already at the end of the changeset, return DONE. */ if( p->in.iNext>=p->in.nData ){ @@ -232870,14 +234139,19 @@ SQLITE_API int sqlite3changegroup_add_change( sqlite3_changegroup *pGrp, sqlite3_changeset_iter *pIter ){ + int rc = SQLITE_OK; + if( pIter->in.iCurrent==pIter->in.iNext || pIter->rc!=SQLITE_OK || pIter->bInvert ){ /* Iterator does not point to any valid entry or is an INVERT iterator. */ - return SQLITE_ERROR; + rc = SQLITE_ERROR; + }else{ + pIter->in.bNoDiscard = 1; + rc = sessionOneChangeToHash(pGrp, pIter, 0); } - return sessionOneChangeToHash(pGrp, pIter, 0); + return rc; } /* @@ -234230,6 +235504,18 @@ typedef sqlite3_uint64 u64; # define EIGHT_BYTE_ALIGNMENT(X) ((((uptr)(X) - (uptr)0)&7)==0) #endif +/* +** Macros needed to provide flexible arrays in a portable way +*/ +#ifndef offsetof +# define offsetof(STRUCTURE,FIELD) ((size_t)((char*)&((STRUCTURE*)0)->FIELD)) +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) +# define FLEXARRAY +#else +# define FLEXARRAY 1 +#endif + #endif /* Truncate very long tokens to this many bytes. Hard limit is @@ -234302,10 +235588,11 @@ typedef struct Fts5Colset Fts5Colset; */ struct Fts5Colset { int nCol; - int aiCol[1]; + int aiCol[FLEXARRAY]; }; - +/* Size (int bytes) of a complete Fts5Colset object with N columns. */ +#define SZ_FTS5COLSET(N) (sizeof(i64)*((N+2)/2)) /************************************************************************** ** Interface to code in fts5_config.c. fts5_config.c contains contains code @@ -235134,7 +236421,7 @@ static void sqlite3Fts5UnicodeAscii(u8*, u8*); ** ** The "lemon" program processes an LALR(1) input grammar file, then uses ** this template to construct a parser. The "lemon" program inserts text -** at each "%%" line. Also, any "P-a-r-s-e" identifer prefix (without the +** at each "%%" line. Also, any "P-a-r-s-e" identifier prefix (without the ** interstitial "-" characters) contained in this template is changed into ** the value of the %name directive from the grammar. Otherwise, the content ** of this template is copied straight through into the generate parser @@ -237288,7 +238575,7 @@ static int fts5Bm25GetData( ** under consideration. ** ** The problem with this is that if (N < 2*nHit), the IDF is - ** negative. Which is undesirable. So the mimimum allowable IDF is + ** negative. Which is undesirable. So the minimum allowable IDF is ** (1e-6) - roughly the same as a term that appears in just over ** half of set of 5,000,000 documents. */ double idf = log( (nRow - nHit + 0.5) / (nHit + 0.5) ); @@ -237751,7 +239038,7 @@ static char *sqlite3Fts5Strndup(int *pRc, const char *pIn, int nIn){ ** * The 52 upper and lower case ASCII characters, and ** * The 10 integer ASCII characters. ** * The underscore character "_" (0x5F). -** * The unicode "subsitute" character (0x1A). +** * The unicode "substitute" character (0x1A). */ static int sqlite3Fts5IsBareword(char t){ u8 aBareword[128] = { @@ -239069,9 +240356,13 @@ struct Fts5ExprNode { /* Child nodes. For a NOT node, this array always contains 2 entries. For ** AND or OR nodes, it contains 2 or more entries. */ int nChild; /* Number of child nodes */ - Fts5ExprNode *apChild[1]; /* Array of child nodes */ + Fts5ExprNode *apChild[FLEXARRAY]; /* Array of child nodes */ }; +/* Size (in bytes) of an Fts5ExprNode object that holds up to N children */ +#define SZ_FTS5EXPRNODE(N) \ + (offsetof(Fts5ExprNode,apChild) + (N)*sizeof(Fts5ExprNode*)) + #define Fts5NodeIsString(p) ((p)->eType==FTS5_TERM || (p)->eType==FTS5_STRING) /* @@ -239102,9 +240393,13 @@ struct Fts5ExprPhrase { Fts5ExprNode *pNode; /* FTS5_STRING node this phrase is part of */ Fts5Buffer poslist; /* Current position list */ int nTerm; /* Number of entries in aTerm[] */ - Fts5ExprTerm aTerm[1]; /* Terms that make up this phrase */ + Fts5ExprTerm aTerm[FLEXARRAY]; /* Terms that make up this phrase */ }; +/* Size (in bytes) of an Fts5ExprPhrase object that holds up to N terms */ +#define SZ_FTS5EXPRPHRASE(N) \ + (offsetof(Fts5ExprPhrase,aTerm) + (N)*sizeof(Fts5ExprTerm)) + /* ** One or more phrases that must appear within a certain token distance of ** each other within each matching document. @@ -239113,9 +240408,12 @@ struct Fts5ExprNearset { int nNear; /* NEAR parameter */ Fts5Colset *pColset; /* Columns to search (NULL -> all columns) */ int nPhrase; /* Number of entries in aPhrase[] array */ - Fts5ExprPhrase *apPhrase[1]; /* Array of phrase pointers */ + Fts5ExprPhrase *apPhrase[FLEXARRAY]; /* Array of phrase pointers */ }; +/* Size (in bytes) of an Fts5ExprNearset object covering up to N phrases */ +#define SZ_FTS5EXPRNEARSET(N) \ + (offsetof(Fts5ExprNearset,apPhrase)+(N)*sizeof(Fts5ExprPhrase*)) /* ** Parse context. @@ -239275,7 +240573,7 @@ static int sqlite3Fts5ExprNew( /* If the LHS of the MATCH expression was a user column, apply the ** implicit column-filter. */ if( sParse.rc==SQLITE_OK && iCol<pConfig->nCol ){ - int n = sizeof(Fts5Colset); + int n = SZ_FTS5COLSET(1); Fts5Colset *pColset = (Fts5Colset*)sqlite3Fts5MallocZero(&sParse.rc, n); if( pColset ){ pColset->nCol = 1; @@ -240633,7 +241931,7 @@ static Fts5ExprNearset *sqlite3Fts5ParseNearset( if( pParse->rc==SQLITE_OK ){ if( pNear==0 ){ sqlite3_int64 nByte; - nByte = sizeof(Fts5ExprNearset) + SZALLOC * sizeof(Fts5ExprPhrase*); + nByte = SZ_FTS5EXPRNEARSET(SZALLOC+1); pRet = sqlite3_malloc64(nByte); if( pRet==0 ){ pParse->rc = SQLITE_NOMEM; @@ -240644,7 +241942,7 @@ static Fts5ExprNearset *sqlite3Fts5ParseNearset( int nNew = pNear->nPhrase + SZALLOC; sqlite3_int64 nByte; - nByte = sizeof(Fts5ExprNearset) + nNew * sizeof(Fts5ExprPhrase*); + nByte = SZ_FTS5EXPRNEARSET(nNew+1); pRet = (Fts5ExprNearset*)sqlite3_realloc64(pNear, nByte); if( pRet==0 ){ pParse->rc = SQLITE_NOMEM; @@ -240735,12 +242033,12 @@ static int fts5ParseTokenize( int nNew = SZALLOC + (pPhrase ? pPhrase->nTerm : 0); pNew = (Fts5ExprPhrase*)sqlite3_realloc64(pPhrase, - sizeof(Fts5ExprPhrase) + sizeof(Fts5ExprTerm) * nNew + SZ_FTS5EXPRPHRASE(nNew+1) ); if( pNew==0 ){ rc = SQLITE_NOMEM; }else{ - if( pPhrase==0 ) memset(pNew, 0, sizeof(Fts5ExprPhrase)); + if( pPhrase==0 ) memset(pNew, 0, SZ_FTS5EXPRPHRASE(1)); pCtx->pPhrase = pPhrase = pNew; pNew->nTerm = nNew - SZALLOC; } @@ -240848,7 +242146,7 @@ static Fts5ExprPhrase *sqlite3Fts5ParseTerm( if( sCtx.pPhrase==0 ){ /* This happens when parsing a token or quoted phrase that contains ** no token characters at all. (e.g ... MATCH '""'). */ - sCtx.pPhrase = sqlite3Fts5MallocZero(&pParse->rc, sizeof(Fts5ExprPhrase)); + sCtx.pPhrase = sqlite3Fts5MallocZero(&pParse->rc, SZ_FTS5EXPRPHRASE(1)); }else if( sCtx.pPhrase->nTerm ){ sCtx.pPhrase->aTerm[sCtx.pPhrase->nTerm-1].bPrefix = (u8)bPrefix; } @@ -240883,19 +242181,18 @@ static int sqlite3Fts5ExprClonePhrase( sizeof(Fts5ExprPhrase*)); } if( rc==SQLITE_OK ){ - pNew->pRoot = (Fts5ExprNode*)sqlite3Fts5MallocZero(&rc, - sizeof(Fts5ExprNode)); + pNew->pRoot = (Fts5ExprNode*)sqlite3Fts5MallocZero(&rc, SZ_FTS5EXPRNODE(1)); } if( rc==SQLITE_OK ){ pNew->pRoot->pNear = (Fts5ExprNearset*)sqlite3Fts5MallocZero(&rc, - sizeof(Fts5ExprNearset) + sizeof(Fts5ExprPhrase*)); + SZ_FTS5EXPRNEARSET(2)); } if( rc==SQLITE_OK && ALWAYS(pOrig!=0) ){ Fts5Colset *pColsetOrig = pOrig->pNode->pNear->pColset; if( pColsetOrig ){ sqlite3_int64 nByte; Fts5Colset *pColset; - nByte = sizeof(Fts5Colset) + (pColsetOrig->nCol-1) * sizeof(int); + nByte = SZ_FTS5COLSET(pColsetOrig->nCol); pColset = (Fts5Colset*)sqlite3Fts5MallocZero(&rc, nByte); if( pColset ){ memcpy(pColset, pColsetOrig, (size_t)nByte); @@ -240923,7 +242220,7 @@ static int sqlite3Fts5ExprClonePhrase( }else{ /* This happens when parsing a token or quoted phrase that contains ** no token characters at all. (e.g ... MATCH '""'). */ - sCtx.pPhrase = sqlite3Fts5MallocZero(&rc, sizeof(Fts5ExprPhrase)); + sCtx.pPhrase = sqlite3Fts5MallocZero(&rc, SZ_FTS5EXPRPHRASE(1)); } } @@ -240988,7 +242285,8 @@ static void sqlite3Fts5ParseSetDistance( ); return; } - nNear = nNear * 10 + (p->p[i] - '0'); + if( nNear<214748363 ) nNear = nNear * 10 + (p->p[i] - '0'); + /* ^^^^^^^^^^^^^^^--- Prevent integer overflow */ } }else{ nNear = FTS5_DEFAULT_NEARDIST; @@ -241017,7 +242315,7 @@ static Fts5Colset *fts5ParseColset( assert( pParse->rc==SQLITE_OK ); assert( iCol>=0 && iCol<pParse->pConfig->nCol ); - pNew = sqlite3_realloc64(p, sizeof(Fts5Colset) + sizeof(int)*nCol); + pNew = sqlite3_realloc64(p, SZ_FTS5COLSET(nCol+1)); if( pNew==0 ){ pParse->rc = SQLITE_NOMEM; }else{ @@ -241052,7 +242350,7 @@ static Fts5Colset *sqlite3Fts5ParseColsetInvert(Fts5Parse *pParse, Fts5Colset *p int nCol = pParse->pConfig->nCol; pRet = (Fts5Colset*)sqlite3Fts5MallocZero(&pParse->rc, - sizeof(Fts5Colset) + sizeof(int)*nCol + SZ_FTS5COLSET(nCol+1) ); if( pRet ){ int i; @@ -241113,7 +242411,7 @@ static Fts5Colset *sqlite3Fts5ParseColset( static Fts5Colset *fts5CloneColset(int *pRc, Fts5Colset *pOrig){ Fts5Colset *pRet; if( pOrig ){ - sqlite3_int64 nByte = sizeof(Fts5Colset) + (pOrig->nCol-1) * sizeof(int); + sqlite3_int64 nByte = SZ_FTS5COLSET(pOrig->nCol); pRet = (Fts5Colset*)sqlite3Fts5MallocZero(pRc, nByte); if( pRet ){ memcpy(pRet, pOrig, (size_t)nByte); @@ -241281,7 +242579,7 @@ static Fts5ExprNode *fts5ParsePhraseToAnd( assert( pNear->nPhrase==1 ); assert( pParse->bPhraseToAnd ); - nByte = sizeof(Fts5ExprNode) + nTerm*sizeof(Fts5ExprNode*); + nByte = SZ_FTS5EXPRNODE(nTerm+1); pRet = (Fts5ExprNode*)sqlite3Fts5MallocZero(&pParse->rc, nByte); if( pRet ){ pRet->eType = FTS5_AND; @@ -241291,7 +242589,7 @@ static Fts5ExprNode *fts5ParsePhraseToAnd( pParse->nPhrase--; for(ii=0; ii<nTerm; ii++){ Fts5ExprPhrase *pPhrase = (Fts5ExprPhrase*)sqlite3Fts5MallocZero( - &pParse->rc, sizeof(Fts5ExprPhrase) + &pParse->rc, SZ_FTS5EXPRPHRASE(1) ); if( pPhrase ){ if( parseGrowPhraseArray(pParse) ){ @@ -241360,7 +242658,7 @@ static Fts5ExprNode *sqlite3Fts5ParseNode( if( pRight->eType==eType ) nChild += pRight->nChild-1; } - nByte = sizeof(Fts5ExprNode) + sizeof(Fts5ExprNode*)*(nChild-1); + nByte = SZ_FTS5EXPRNODE(nChild); pRet = (Fts5ExprNode*)sqlite3Fts5MallocZero(&pParse->rc, nByte); if( pRet ){ @@ -242235,7 +243533,7 @@ static int sqlite3Fts5ExprInstToken( } /* -** Clear the token mappings for all Fts5IndexIter objects mannaged by +** Clear the token mappings for all Fts5IndexIter objects managed by ** the expression passed as the only argument. */ static void sqlite3Fts5ExprClearTokens(Fts5Expr *pExpr){ @@ -242270,7 +243568,7 @@ typedef struct Fts5HashEntry Fts5HashEntry; /* ** This file contains the implementation of an in-memory hash table used -** to accumuluate "term -> doclist" content before it is flused to a level-0 +** to accumulate "term -> doclist" content before it is flushed to a level-0 ** segment. */ @@ -242327,7 +243625,7 @@ struct Fts5HashEntry { }; /* -** Eqivalent to: +** Equivalent to: ** ** char *fts5EntryKey(Fts5HashEntry *pEntry){ return zKey; } */ @@ -243263,9 +244561,13 @@ struct Fts5Structure { u64 nOriginCntr; /* Origin value for next top-level segment */ int nSegment; /* Total segments in this structure */ int nLevel; /* Number of levels in this index */ - Fts5StructureLevel aLevel[1]; /* Array of nLevel level objects */ + Fts5StructureLevel aLevel[FLEXARRAY]; /* Array of nLevel level objects */ }; +/* Size (in bytes) of an Fts5Structure object holding up to N levels */ +#define SZ_FTS5STRUCTURE(N) \ + (offsetof(Fts5Structure,aLevel) + (N)*sizeof(Fts5StructureLevel)) + /* ** An object of type Fts5SegWriter is used to write to segments. */ @@ -243395,11 +244697,15 @@ struct Fts5SegIter { ** Array of tombstone pages. Reference counted. */ struct Fts5TombstoneArray { - int nRef; /* Number of pointers to this object */ + int nRef; /* Number of pointers to this object */ int nTombstone; - Fts5Data *apTombstone[1]; /* Array of tombstone pages */ + Fts5Data *apTombstone[FLEXARRAY]; /* Array of tombstone pages */ }; +/* Size (in bytes) of an Fts5TombstoneArray holding up to N tombstones */ +#define SZ_FTS5TOMBSTONEARRAY(N) \ + (offsetof(Fts5TombstoneArray,apTombstone)+(N)*sizeof(Fts5Data*)) + /* ** Argument is a pointer to an Fts5Data structure that contains a ** leaf page. @@ -243468,9 +244774,12 @@ struct Fts5Iter { i64 iSwitchRowid; /* Firstest rowid of other than aFirst[1] */ Fts5CResult *aFirst; /* Current merge state (see above) */ - Fts5SegIter aSeg[1]; /* Array of segment iterators */ + Fts5SegIter aSeg[FLEXARRAY]; /* Array of segment iterators */ }; +/* Size (in bytes) of an Fts5Iter object holding up to N segment iterators */ +#define SZ_FTS5ITER(N) (offsetof(Fts5Iter,aSeg)+(N)*sizeof(Fts5SegIter)) + /* ** An instance of the following type is used to iterate through the contents ** of a doclist-index record. @@ -243497,9 +244806,13 @@ struct Fts5DlidxLvl { struct Fts5DlidxIter { int nLvl; int iSegid; - Fts5DlidxLvl aLvl[1]; + Fts5DlidxLvl aLvl[FLEXARRAY]; }; +/* Size (in bytes) of an Fts5DlidxIter object with up to N levels */ +#define SZ_FTS5DLIDXITER(N) \ + (offsetof(Fts5DlidxIter,aLvl)+(N)*sizeof(Fts5DlidxLvl)) + static void fts5PutU16(u8 *aOut, u16 iVal){ aOut[0] = (iVal>>8); aOut[1] = (iVal&0xFF); @@ -243867,7 +245180,7 @@ static int sqlite3Fts5StructureTest(Fts5Index *p, void *pStruct){ static void fts5StructureMakeWritable(int *pRc, Fts5Structure **pp){ Fts5Structure *p = *pp; if( *pRc==SQLITE_OK && p->nRef>1 ){ - i64 nByte = sizeof(Fts5Structure)+(p->nLevel-1)*sizeof(Fts5StructureLevel); + i64 nByte = SZ_FTS5STRUCTURE(p->nLevel); Fts5Structure *pNew; pNew = (Fts5Structure*)sqlite3Fts5MallocZero(pRc, nByte); if( pNew ){ @@ -243941,10 +245254,7 @@ static int fts5StructureDecode( ){ return FTS5_CORRUPT; } - nByte = ( - sizeof(Fts5Structure) + /* Main structure */ - sizeof(Fts5StructureLevel) * (nLevel-1) /* aLevel[] array */ - ); + nByte = SZ_FTS5STRUCTURE(nLevel); pRet = (Fts5Structure*)sqlite3Fts5MallocZero(&rc, nByte); if( pRet ){ @@ -244024,10 +245334,7 @@ static void fts5StructureAddLevel(int *pRc, Fts5Structure **ppStruct){ if( *pRc==SQLITE_OK ){ Fts5Structure *pStruct = *ppStruct; int nLevel = pStruct->nLevel; - sqlite3_int64 nByte = ( - sizeof(Fts5Structure) + /* Main structure */ - sizeof(Fts5StructureLevel) * (nLevel+1) /* aLevel[] array */ - ); + sqlite3_int64 nByte = SZ_FTS5STRUCTURE(nLevel+2); pStruct = sqlite3_realloc64(pStruct, nByte); if( pStruct ){ @@ -244566,7 +245873,7 @@ static Fts5DlidxIter *fts5DlidxIterInit( int bDone = 0; for(i=0; p->rc==SQLITE_OK && bDone==0; i++){ - sqlite3_int64 nByte = sizeof(Fts5DlidxIter) + i * sizeof(Fts5DlidxLvl); + sqlite3_int64 nByte = SZ_FTS5DLIDXITER(i+1); Fts5DlidxIter *pNew; pNew = (Fts5DlidxIter*)sqlite3_realloc64(pIter, nByte); @@ -244784,7 +246091,7 @@ static void fts5SegIterSetNext(Fts5Index *p, Fts5SegIter *pIter){ static void fts5SegIterAllocTombstone(Fts5Index *p, Fts5SegIter *pIter){ const int nTomb = pIter->pSeg->nPgTombstone; if( nTomb>0 ){ - int nByte = nTomb * sizeof(Fts5Data*) + sizeof(Fts5TombstoneArray); + int nByte = SZ_FTS5TOMBSTONEARRAY(nTomb+1); Fts5TombstoneArray *pNew; pNew = (Fts5TombstoneArray*)sqlite3Fts5MallocZero(&p->rc, nByte); if( pNew ){ @@ -246245,8 +247552,7 @@ static Fts5Iter *fts5MultiIterAlloc( for(nSlot=2; nSlot<nSeg; nSlot=nSlot*2); pNew = fts5IdxMalloc(p, - sizeof(Fts5Iter) + /* pNew */ - sizeof(Fts5SegIter) * (nSlot-1) + /* pNew->aSeg[] */ + SZ_FTS5ITER(nSlot) + /* pNew + pNew->aSeg[] */ sizeof(Fts5CResult) * nSlot /* pNew->aFirst[] */ ); if( pNew ){ @@ -248047,7 +249353,7 @@ static void fts5DoSecureDelete( int iDelKeyOff = 0; /* Offset of deleted key, if any */ nIdx = nPg-iPgIdx; - aIdx = sqlite3Fts5MallocZero(&p->rc, nIdx+16); + aIdx = sqlite3Fts5MallocZero(&p->rc, ((i64)nIdx)+16); if( p->rc ) return; memcpy(aIdx, &aPg[iPgIdx], nIdx); @@ -248612,7 +249918,7 @@ static Fts5Structure *fts5IndexOptimizeStruct( Fts5Structure *pStruct ){ Fts5Structure *pNew = 0; - sqlite3_int64 nByte = sizeof(Fts5Structure); + sqlite3_int64 nByte = SZ_FTS5STRUCTURE(1); int nSeg = pStruct->nSegment; int i; @@ -248641,7 +249947,8 @@ static Fts5Structure *fts5IndexOptimizeStruct( assert( pStruct->aLevel[i].nMerge<=nThis ); } - nByte += (pStruct->nLevel+1) * sizeof(Fts5StructureLevel); + nByte += (((i64)pStruct->nLevel)+1) * sizeof(Fts5StructureLevel); + assert( nByte==SZ_FTS5STRUCTURE(pStruct->nLevel+2) ); pNew = (Fts5Structure*)sqlite3Fts5MallocZero(&p->rc, nByte); if( pNew ){ @@ -249218,9 +250525,13 @@ struct Fts5TokenDataIter { int nIterAlloc; Fts5PoslistReader *aPoslistReader; int *aPoslistToIter; - Fts5Iter *apIter[1]; + Fts5Iter *apIter[FLEXARRAY]; }; +/* Size in bytes of an Fts5TokenDataIter object holding up to N iterators */ +#define SZ_FTS5TOKENDATAITER(N) \ + (offsetof(Fts5TokenDataIter,apIter) + (N)*sizeof(Fts5Iter)) + /* ** The two input arrays - a1[] and a2[] - are in sorted order. This function ** merges the two arrays together and writes the result to output array @@ -249292,7 +250603,7 @@ static void fts5TokendataIterAppendMap( /* ** Sort the contents of the pT->aMap[] array. ** -** The sorting algorithm requries a malloc(). If this fails, an error code +** The sorting algorithm requires a malloc(). If this fails, an error code ** is left in Fts5Index.rc before returning. */ static void fts5TokendataIterSortMap(Fts5Index *p, Fts5TokenDataIter *pT){ @@ -249483,7 +250794,7 @@ static void fts5SetupPrefixIter( && p->pConfig->bPrefixInsttoken ){ s.pTokendata = &s2; - s2.pT = (Fts5TokenDataIter*)fts5IdxMalloc(p, sizeof(*s2.pT)); + s2.pT = (Fts5TokenDataIter*)fts5IdxMalloc(p, SZ_FTS5TOKENDATAITER(1)); } if( p->pConfig->eDetail==FTS5_DETAIL_NONE ){ @@ -249529,7 +250840,8 @@ static void fts5SetupPrefixIter( } } - pData = fts5IdxMalloc(p, sizeof(*pData)+s.doclist.n+FTS5_DATA_ZERO_PADDING); + pData = fts5IdxMalloc(p, sizeof(*pData) + + ((i64)s.doclist.n)+FTS5_DATA_ZERO_PADDING); assert( pData!=0 || p->rc!=SQLITE_OK ); if( pData ){ pData->p = (u8*)&pData[1]; @@ -249610,15 +250922,17 @@ static int sqlite3Fts5IndexRollback(Fts5Index *p){ ** and the initial version of the "averages" record (a zero-byte blob). */ static int sqlite3Fts5IndexReinit(Fts5Index *p){ - Fts5Structure s; + Fts5Structure *pTmp; + u8 tmpSpace[SZ_FTS5STRUCTURE(1)]; fts5StructureInvalidate(p); fts5IndexDiscardData(p); - memset(&s, 0, sizeof(Fts5Structure)); + pTmp = (Fts5Structure*)tmpSpace; + memset(pTmp, 0, SZ_FTS5STRUCTURE(1)); if( p->pConfig->bContentlessDelete ){ - s.nOriginCntr = 1; + pTmp->nOriginCntr = 1; } fts5DataWrite(p, FTS5_AVERAGES_ROWID, (const u8*)"", 0); - fts5StructureWrite(p, &s); + fts5StructureWrite(p, pTmp); return fts5IndexReturn(p); } @@ -249826,7 +251140,7 @@ static Fts5TokenDataIter *fts5AppendTokendataIter( if( p->rc==SQLITE_OK ){ if( pIn==0 || pIn->nIter==pIn->nIterAlloc ){ int nAlloc = pIn ? pIn->nIterAlloc*2 : 16; - int nByte = nAlloc * sizeof(Fts5Iter*) + sizeof(Fts5TokenDataIter); + int nByte = SZ_FTS5TOKENDATAITER(nAlloc+1); Fts5TokenDataIter *pNew = (Fts5TokenDataIter*)sqlite3_realloc(pIn, nByte); if( pNew==0 ){ @@ -250342,7 +251656,8 @@ static int fts5SetupPrefixIterTokendata( fts5BufferGrow(&p->rc, &token, nToken+1); assert( token.p!=0 || p->rc!=SQLITE_OK ); - ctx.pT = (Fts5TokenDataIter*)sqlite3Fts5MallocZero(&p->rc, sizeof(*ctx.pT)); + ctx.pT = (Fts5TokenDataIter*)sqlite3Fts5MallocZero(&p->rc, + SZ_FTS5TOKENDATAITER(1)); if( p->rc==SQLITE_OK ){ @@ -250473,7 +251788,8 @@ static int sqlite3Fts5IndexIterWriteTokendata( if( pIter->nSeg>0 ){ /* This is a prefix term iterator. */ if( pT==0 ){ - pT = (Fts5TokenDataIter*)sqlite3Fts5MallocZero(&p->rc, sizeof(*pT)); + pT = (Fts5TokenDataIter*)sqlite3Fts5MallocZero(&p->rc, + SZ_FTS5TOKENDATAITER(1)); pIter->pTokenDataIter = pT; } if( pT ){ @@ -251507,7 +252823,7 @@ static void fts5DecodeRowid( #if defined(SQLITE_TEST) || defined(SQLITE_FTS5_DEBUG) static void fts5DebugRowid(int *pRc, Fts5Buffer *pBuf, i64 iKey){ - int iSegid, iHeight, iPgno, bDlidx, bTomb; /* Rowid compenents */ + int iSegid, iHeight, iPgno, bDlidx, bTomb; /* Rowid components */ fts5DecodeRowid(iKey, &bTomb, &iSegid, &bDlidx, &iHeight, &iPgno); if( iSegid==0 ){ @@ -251753,7 +253069,7 @@ static void fts5DecodeFunction( ** buffer overreads even if the record is corrupt. */ n = sqlite3_value_bytes(apVal[1]); aBlob = sqlite3_value_blob(apVal[1]); - nSpace = n + FTS5_DATA_ZERO_PADDING; + nSpace = ((i64)n) + FTS5_DATA_ZERO_PADDING; a = (u8*)sqlite3Fts5MallocZero(&rc, nSpace); if( a==0 ) goto decode_out; if( n>0 ) memcpy(a, aBlob, n); @@ -252468,9 +253784,11 @@ struct Fts5Sorter { i64 iRowid; /* Current rowid */ const u8 *aPoslist; /* Position lists for current row */ int nIdx; /* Number of entries in aIdx[] */ - int aIdx[1]; /* Offsets into aPoslist for current row */ + int aIdx[FLEXARRAY]; /* Offsets into aPoslist for current row */ }; +/* Size (int bytes) of an Fts5Sorter object with N indexes */ +#define SZ_FTS5SORTER(N) (offsetof(Fts5Sorter,nIdx)+((N+2)/2)*sizeof(i64)) /* ** Virtual-table cursor object. @@ -253348,7 +254666,7 @@ static int fts5CursorFirstSorted( const char *zRankArgs = pCsr->zRankArgs; nPhrase = sqlite3Fts5ExprPhraseCount(pCsr->pExpr); - nByte = sizeof(Fts5Sorter) + sizeof(int) * (nPhrase-1); + nByte = SZ_FTS5SORTER(nPhrase); pSorter = (Fts5Sorter*)sqlite3_malloc64(nByte); if( pSorter==0 ) return SQLITE_NOMEM; memset(pSorter, 0, (size_t)nByte); @@ -255874,7 +257192,7 @@ static void fts5SourceIdFunc( ){ assert( nArg==0 ); UNUSED_PARAM2(nArg, apUnused); - sqlite3_result_text(pCtx, "fts5: 2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70", -1, SQLITE_TRANSIENT); + sqlite3_result_text(pCtx, "fts5: 2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742", -1, SQLITE_TRANSIENT); } /* @@ -256099,8 +257417,8 @@ static int fts5Init(sqlite3 *db){ ** its entry point to enable the matchinfo() demo. */ #ifdef SQLITE_FTS5_ENABLE_TEST_MI if( rc==SQLITE_OK ){ - extern int sqlite3Fts5TestRegisterMatchinfo(sqlite3*); - rc = sqlite3Fts5TestRegisterMatchinfo(db); + extern int sqlite3Fts5TestRegisterMatchinfoAPI(fts5_api*); + rc = sqlite3Fts5TestRegisterMatchinfoAPI(&pGlobal->api); } #endif @@ -259938,7 +261256,6 @@ static void sqlite3Fts5UnicodeAscii(u8 *aArray, u8 *aAscii){ aAscii[0] = 0; /* 0x00 is never a token character */ } - /* ** 2015 May 30 ** @@ -260479,12 +261796,12 @@ static int fts5VocabInitVtab( *pzErr = sqlite3_mprintf("wrong number of vtable arguments"); rc = SQLITE_ERROR; }else{ - int nByte; /* Bytes of space to allocate */ + i64 nByte; /* Bytes of space to allocate */ const char *zDb = bDb ? argv[3] : argv[1]; const char *zTab = bDb ? argv[4] : argv[3]; const char *zType = bDb ? argv[5] : argv[4]; - int nDb = (int)strlen(zDb)+1; - int nTab = (int)strlen(zTab)+1; + i64 nDb = strlen(zDb)+1; + i64 nTab = strlen(zTab)+1; int eType = 0; rc = fts5VocabTableType(zType, pzErr, &eType); diff --git a/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.h b/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.h index 082a9f9dc4..f61a148575 100644 --- a/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.h +++ b/Telegram/SourceFiles/ayu/libs/sqlite/sqlite3.h @@ -133,7 +133,7 @@ extern "C" { ** ** Since [version 3.6.18] ([dateof:3.6.18]), ** SQLite source code has been stored in the -** <a href="http://www.fossil-scm.org/">Fossil configuration management +** <a href="http://fossil-scm.org/">Fossil configuration management ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to ** a string which identifies a particular check-in of SQLite ** within its configuration management system. ^The SQLITE_SOURCE_ID @@ -146,9 +146,9 @@ extern "C" { ** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite_version()] and [sqlite_source_id()]. */ -#define SQLITE_VERSION "3.49.1" -#define SQLITE_VERSION_NUMBER 3049001 -#define SQLITE_SOURCE_ID "2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70" +#define SQLITE_VERSION "3.50.0" +#define SQLITE_VERSION_NUMBER 3050000 +#define SQLITE_SOURCE_ID "2025-05-29 14:26:00 dfc790f998f450d9c35e3ba1c8c89c17466cb559f87b0239e4aab9d34e28f742" /* ** CAPI3REF: Run-Time Library Version Numbers @@ -1163,6 +1163,12 @@ struct sqlite3_io_methods { ** the value that M is to be set to. Before returning, the 32-bit signed ** integer is overwritten with the previous value of M. ** +** <li>[[SQLITE_FCNTL_BLOCK_ON_CONNECT]] +** The [SQLITE_FCNTL_BLOCK_ON_CONNECT] opcode is used to configure the +** VFS to block when taking a SHARED lock to connect to a wal mode database. +** This is used to implement the functionality associated with +** SQLITE_SETLK_BLOCK_ON_CONNECT. +** ** <li>[[SQLITE_FCNTL_DATA_VERSION]] ** The [SQLITE_FCNTL_DATA_VERSION] opcode is used to detect changes to ** a database file. The argument is a pointer to a 32-bit unsigned integer. @@ -1259,6 +1265,7 @@ struct sqlite3_io_methods { #define SQLITE_FCNTL_CKSM_FILE 41 #define SQLITE_FCNTL_RESET_CACHE 42 #define SQLITE_FCNTL_NULL_IO 43 +#define SQLITE_FCNTL_BLOCK_ON_CONNECT 44 /* deprecated names */ #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE @@ -1989,13 +1996,16 @@ struct sqlite3_mem_methods { ** ** [[SQLITE_CONFIG_LOOKASIDE]] <dt>SQLITE_CONFIG_LOOKASIDE</dt> ** <dd> ^(The SQLITE_CONFIG_LOOKASIDE option takes two arguments that determine -** the default size of lookaside memory on each [database connection]. +** the default size of [lookaside memory] on each [database connection]. ** The first argument is the -** size of each lookaside buffer slot and the second is the number of -** slots allocated to each database connection.)^ ^(SQLITE_CONFIG_LOOKASIDE -** sets the <i>default</i> lookaside size. The [SQLITE_DBCONFIG_LOOKASIDE] -** option to [sqlite3_db_config()] can be used to change the lookaside -** configuration on individual connections.)^ </dd> +** size of each lookaside buffer slot ("sz") and the second is the number of +** slots allocated to each database connection ("cnt").)^ +** ^(SQLITE_CONFIG_LOOKASIDE sets the <i>default</i> lookaside size. +** The [SQLITE_DBCONFIG_LOOKASIDE] option to [sqlite3_db_config()] can +** be used to change the lookaside configuration on individual connections.)^ +** The [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to change the +** default lookaside configuration at compile-time. +** </dd> ** ** [[SQLITE_CONFIG_PCACHE2]] <dt>SQLITE_CONFIG_PCACHE2</dt> ** <dd> ^(The SQLITE_CONFIG_PCACHE2 option takes a single argument which is @@ -2232,31 +2242,50 @@ struct sqlite3_mem_methods { ** [[SQLITE_DBCONFIG_LOOKASIDE]] ** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt> ** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the -** configuration of the lookaside memory allocator within a database +** configuration of the [lookaside memory allocator] within a database ** connection. ** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i> ** in the [DBCONFIG arguments|usual format]. ** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two, ** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE ** should have a total of five parameters. -** ^The first argument (the third parameter to [sqlite3_db_config()] is a +** <ol> +** <li><p>The first argument ("buf") is a ** pointer to a memory buffer to use for lookaside memory. -** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb -** may be NULL in which case SQLite will allocate the -** lookaside buffer itself using [sqlite3_malloc()]. ^The second argument is the -** size of each lookaside buffer slot. ^The third argument is the number of -** slots. The size of the buffer in the first argument must be greater than -** or equal to the product of the second and third arguments. The buffer -** must be aligned to an 8-byte boundary. ^If the second argument to -** SQLITE_DBCONFIG_LOOKASIDE is not a multiple of 8, it is internally -** rounded down to the next smaller multiple of 8. ^(The lookaside memory +** The first argument may be NULL in which case SQLite will allocate the +** lookaside buffer itself using [sqlite3_malloc()]. +** <li><P>The second argument ("sz") is the +** size of each lookaside buffer slot. Lookaside is disabled if "sz" +** is less than 8. The "sz" argument should be a multiple of 8 less than +** 65536. If "sz" does not meet this constraint, it is reduced in size until +** it does. +** <li><p>The third argument ("cnt") is the number of slots. Lookaside is disabled +** if "cnt"is less than 1. The "cnt" value will be reduced, if necessary, so +** that the product of "sz" and "cnt" does not exceed 2,147,418,112. The "cnt" +** parameter is usually chosen so that the product of "sz" and "cnt" is less +** than 1,000,000. +** </ol> +** <p>If the "buf" argument is not NULL, then it must +** point to a memory buffer with a size that is greater than +** or equal to the product of "sz" and "cnt". +** The buffer must be aligned to an 8-byte boundary. +** The lookaside memory ** configuration for a database connection can only be changed when that ** connection is not currently using lookaside memory, or in other words -** when the "current value" returned by -** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero. +** when the value returned by [SQLITE_DBSTATUS_LOOKASIDE_USED] is zero. ** Any attempt to change the lookaside memory configuration when lookaside ** memory is in use leaves the configuration unchanged and returns -** [SQLITE_BUSY].)^</dd> +** [SQLITE_BUSY]. +** If the "buf" argument is NULL and an attempt +** to allocate memory based on "sz" and "cnt" fails, then +** lookaside is silently disabled. +** <p> +** The [SQLITE_CONFIG_LOOKASIDE] configuration option can be used to set the +** default lookaside configuration at initialization. The +** [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to set the default lookaside +** configuration at compile-time. Typical values for lookaside are 1200 for +** "sz" and 40 to 100 for "cnt". +** </dd> ** ** [[SQLITE_DBCONFIG_ENABLE_FKEY]] ** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt> @@ -2993,6 +3022,44 @@ SQLITE_API int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*); */ SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms); +/* +** CAPI3REF: Set the Setlk Timeout +** METHOD: sqlite3 +** +** This routine is only useful in SQLITE_ENABLE_SETLK_TIMEOUT builds. If +** the VFS supports blocking locks, it sets the timeout in ms used by +** eligible locks taken on wal mode databases by the specified database +** handle. In non-SQLITE_ENABLE_SETLK_TIMEOUT builds, or if the VFS does +** not support blocking locks, this function is a no-op. +** +** Passing 0 to this function disables blocking locks altogether. Passing +** -1 to this function requests that the VFS blocks for a long time - +** indefinitely if possible. The results of passing any other negative value +** are undefined. +** +** Internally, each SQLite database handle store two timeout values - the +** busy-timeout (used for rollback mode databases, or if the VFS does not +** support blocking locks) and the setlk-timeout (used for blocking locks +** on wal-mode databases). The sqlite3_busy_timeout() method sets both +** values, this function sets only the setlk-timeout value. Therefore, +** to configure separate busy-timeout and setlk-timeout values for a single +** database handle, call sqlite3_busy_timeout() followed by this function. +** +** Whenever the number of connections to a wal mode database falls from +** 1 to 0, the last connection takes an exclusive lock on the database, +** then checkpoints and deletes the wal file. While it is doing this, any +** new connection that tries to read from the database fails with an +** SQLITE_BUSY error. Or, if the SQLITE_SETLK_BLOCK_ON_CONNECT flag is +** passed to this API, the new connection blocks until the exclusive lock +** has been released. +*/ +SQLITE_API int sqlite3_setlk_timeout(sqlite3*, int ms, int flags); + +/* +** CAPI3REF: Flags for sqlite3_setlk_timeout() +*/ +#define SQLITE_SETLK_BLOCK_ON_CONNECT 0x01 + /* ** CAPI3REF: Convenience Routines For Running Queries ** METHOD: sqlite3 @@ -5108,7 +5175,7 @@ SQLITE_API const void *sqlite3_column_decltype16(sqlite3_stmt*,int); ** other than [SQLITE_ROW] before any subsequent invocation of ** sqlite3_step(). Failure to reset the prepared statement using ** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from -** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1], +** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1]), ** sqlite3_step() began ** calling [sqlite3_reset()] automatically in this circumstance rather ** than returning [SQLITE_MISUSE]. This is not considered a compatibility @@ -7004,6 +7071,8 @@ SQLITE_API int sqlite3_autovacuum_pages( ** ** ^The second argument is a pointer to the function to invoke when a ** row is updated, inserted or deleted in a rowid table. +** ^The update hook is disabled by invoking sqlite3_update_hook() +** with a NULL pointer as the second parameter. ** ^The first argument to the callback is a copy of the third argument ** to sqlite3_update_hook(). ** ^The second callback argument is one of [SQLITE_INSERT], [SQLITE_DELETE], @@ -11486,9 +11555,10 @@ SQLITE_API void sqlite3session_table_filter( ** is inserted while a session object is enabled, then later deleted while ** the same session object is disabled, no INSERT record will appear in the ** changeset, even though the delete took place while the session was disabled. -** Or, if one field of a row is updated while a session is disabled, and -** another field of the same row is updated while the session is enabled, the -** resulting changeset will contain an UPDATE change that updates both fields. +** Or, if one field of a row is updated while a session is enabled, and +** then another field of the same row is updated while the session is disabled, +** the resulting changeset will contain an UPDATE change that updates both +** fields. */ SQLITE_API int sqlite3session_changeset( sqlite3_session *pSession, /* Session object */ @@ -11560,8 +11630,9 @@ SQLITE_API sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession ** database zFrom the contents of the two compatible tables would be ** identical. ** -** It an error if database zFrom does not exist or does not contain the -** required compatible table. +** Unless the call to this function is a no-op as described above, it is an +** error if database zFrom does not exist or does not contain the required +** compatible table. ** ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg @@ -11696,7 +11767,7 @@ SQLITE_API int sqlite3changeset_start_v2( ** The following flags may passed via the 4th parameter to ** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]: ** -** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd> +** <dt>SQLITE_CHANGESETSTART_INVERT <dd> ** Invert the changeset while iterating through it. This is equivalent to ** inverting a changeset using sqlite3changeset_invert() before applying it. ** It is an error to specify this flag with a patchset. @@ -12011,19 +12082,6 @@ SQLITE_API int sqlite3changeset_concat( void **ppOut /* OUT: Buffer containing output changeset */ ); - -/* -** CAPI3REF: Upgrade the Schema of a Changeset/Patchset -*/ -SQLITE_API int sqlite3changeset_upgrade( - sqlite3 *db, - const char *zDb, - int nIn, const void *pIn, /* Input changeset */ - int *pnOut, void **ppOut /* OUT: Inverse of input */ -); - - - /* ** CAPI3REF: Changegroup Handle ** From 20976ac9f94f02f08833fd3e89ddf476a69e8c21 Mon Sep 17 00:00:00 2001 From: AlexeyZavar <sltkval1@gmail.com> Date: Fri, 6 Jun 2025 21:27:39 +0300 Subject: [PATCH 161/340] fix: ttl messages destroying --- Telegram/SourceFiles/ayu/ui/ayu_logo.cpp | 2 +- Telegram/SourceFiles/ayu/ui/ayu_logo.h | 2 +- Telegram/SourceFiles/history/history_item.cpp | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp b/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp index 29b542f66c..45395f1ccf 100644 --- a/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp +++ b/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp @@ -43,7 +43,7 @@ void loadIcons() { } } -QImage loadPreview(QString name) { +QImage loadPreview(const QString& name) { return QImage(qsl(":/gui/art/ayu/%1/app_preview.png").arg(name)); } diff --git a/Telegram/SourceFiles/ayu/ui/ayu_logo.h b/Telegram/SourceFiles/ayu/ui/ayu_logo.h index 564beb8c05..736812d2bb 100644 --- a/Telegram/SourceFiles/ayu/ui/ayu_logo.h +++ b/Telegram/SourceFiles/ayu/ui/ayu_logo.h @@ -26,7 +26,7 @@ ICON(EXTERA2, "extera2"); void loadAppIco(); -QImage loadPreview(QString name); +QImage loadPreview(const QString& name); QString currentAppLogoName(); QImage currentAppLogo(); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 99250b6046..063551f996 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2182,6 +2182,12 @@ void HistoryItem::clearMediaAsExpired() { if (!media || !media->ttlSeconds()) { return; } + + const auto& settings = AyuSettings::getInstance(); + if (settings.saveDeletedMessages) { + return; + } + if (const auto document = media->document()) { applyEditionToHistoryCleared(); auto text = (document->isVideoFile() From 69420f5750c7cc6d659b2f86a5cf01f5d147dac3 Mon Sep 17 00:00:00 2001 From: AlexeyZavar <sltkval1@gmail.com> Date: Sun, 8 Jun 2025 11:00:04 +0300 Subject: [PATCH 162/340] chore: refactor & reformat settings --- Telegram/CMakeLists.txt | 10 +- Telegram/SourceFiles/api/api_polls.cpp | 2 +- .../SourceFiles/api/api_send_progress.cpp | 2 +- Telegram/SourceFiles/api/api_updates.cpp | 2 +- Telegram/SourceFiles/apiwrap.cpp | 10 +- Telegram/SourceFiles/ayu/ayu_infra.cpp | 2 +- Telegram/SourceFiles/ayu/ayu_settings.cpp | 14 +- Telegram/SourceFiles/ayu/ayu_settings.h | 8 +- Telegram/SourceFiles/ayu/ayu_worker.cpp | 2 +- Telegram/SourceFiles/ayu/ui/ayu_logo.cpp | 4 +- .../ayu/ui/boxes/edit_edited_mark.cpp | 93 ------- .../ayu/ui/boxes/edit_edited_mark.h | 28 -- ...dit_deleted_mark.cpp => edit_mark_box.cpp} | 42 +-- .../{edit_deleted_mark.h => edit_mark_box.h} | 12 +- .../ayu/ui/boxes/message_shot_box.cpp | 2 +- .../{settings => components}/icon_picker.cpp | 2 +- .../ui/{settings => components}/icon_picker.h | 0 .../ayu/ui/context_menu/context_menu.cpp | 8 +- .../ayu/ui/settings/settings_ayu.cpp | 259 ++++++++++-------- .../ayu/ui/utils/ayu_profile_values.cpp | 2 +- .../ayu/utils/telegram_helpers.cpp | 4 +- Telegram/SourceFiles/boxes/share_box.cpp | 2 +- .../chat_helpers/emoji_list_widget.cpp | 2 +- .../chat_helpers/field_autocomplete.cpp | 2 +- .../chat_helpers/gifs_list_widget.cpp | 2 +- .../chat_helpers/stickers_list_widget.cpp | 6 +- .../SourceFiles/chat_helpers/tabbed_panel.cpp | 2 +- .../chat_helpers/ttl_media_layer_widget.cpp | 2 +- .../data/components/promo_suggestions.cpp | 2 +- .../data/components/sponsored_messages.cpp | 4 +- .../SourceFiles/data/data_chat_filters.cpp | 6 +- Telegram/SourceFiles/data/data_histories.cpp | 2 +- .../data/data_message_reactions.cpp | 2 +- .../SourceFiles/data/data_peer_values.cpp | 2 +- .../SourceFiles/data/data_replies_list.cpp | 2 +- Telegram/SourceFiles/data/data_session.cpp | 6 +- Telegram/SourceFiles/data/data_stories.cpp | 10 +- Telegram/SourceFiles/data/data_user.cpp | 2 +- Telegram/SourceFiles/dialogs/dialogs_row.cpp | 2 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 6 +- Telegram/SourceFiles/history/history_item.cpp | 6 +- .../history/history_item_components.cpp | 2 +- .../history/history_item_helpers.cpp | 2 +- .../SourceFiles/history/history_widget.cpp | 26 +- .../history_view_voice_record_bar.cpp | 4 +- .../history/view/history_view_bottom_info.cpp | 2 +- .../view/history_view_context_menu.cpp | 2 +- .../history/view/history_view_reply.cpp | 2 +- .../history/view/history_view_send_action.cpp | 2 +- .../view/history_view_top_bar_widget.cpp | 8 +- .../view/media/history_view_document.cpp | 2 +- .../view/media/history_view_web_page.cpp | 2 +- .../history_view_reactions_selector.cpp | 6 +- Telegram/SourceFiles/info/info_top_bar.cpp | 2 +- .../info/profile/info_profile_actions.cpp | 2 +- .../profile/info_profile_inner_widget.cpp | 2 +- .../inline_bots/bot_attach_web_view.cpp | 2 +- Telegram/SourceFiles/main/main_session.cpp | 6 +- .../stories/media_stories_repost_view.cpp | 2 +- Telegram/SourceFiles/menu/menu_send.cpp | 2 +- .../platform/win/main_window_win.cpp | 2 +- .../SourceFiles/platform/win/tray_win.cpp | 2 +- .../SourceFiles/settings/settings_premium.cpp | 2 +- Telegram/SourceFiles/tray.cpp | 2 +- .../ui/chat/attach/attach_bot_webview.cpp | 2 +- Telegram/SourceFiles/ui/chat/chat_style.cpp | 2 +- .../window/notifications_manager.cpp | 2 +- .../SourceFiles/window/section_widget.cpp | 2 +- .../window/window_filters_menu.cpp | 8 +- .../SourceFiles/window/window_main_menu.cpp | 4 +- .../window/window_session_controller.cpp | 2 +- 71 files changed, 297 insertions(+), 389 deletions(-) delete mode 100644 Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.cpp delete mode 100644 Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.h rename Telegram/SourceFiles/ayu/ui/boxes/{edit_deleted_mark.cpp => edit_mark_box.cpp} (68%) rename Telegram/SourceFiles/ayu/ui/boxes/{edit_deleted_mark.h => edit_mark_box.h} (58%) rename Telegram/SourceFiles/ayu/ui/{settings => components}/icon_picker.cpp (98%) rename Telegram/SourceFiles/ayu/ui/{settings => components}/icon_picker.h (100%) diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 7412592163..7cc0af58ba 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -129,8 +129,6 @@ set(ayugram_files ayu/ui/ayu_logo.h ayu/ui/utils/ayu_profile_values.cpp ayu/ui/utils/ayu_profile_values.h - ayu/ui/settings/icon_picker.cpp - ayu/ui/settings/icon_picker.h ayu/ui/settings/settings_ayu.cpp ayu/ui/settings/settings_ayu.h ayu/ui/context_menu/context_menu.cpp @@ -143,10 +141,8 @@ set(ayugram_files ayu/ui/message_history/history_item.h ayu/ui/message_history/history_section.cpp ayu/ui/message_history/history_section.h - ayu/ui/boxes/edit_deleted_mark.cpp - ayu/ui/boxes/edit_deleted_mark.h - ayu/ui/boxes/edit_edited_mark.cpp - ayu/ui/boxes/edit_edited_mark.h + ayu/ui/boxes/edit_mark_box.cpp + ayu/ui/boxes/edit_mark_box.h ayu/ui/boxes/font_selector.cpp ayu/ui/boxes/font_selector.h ayu/ui/boxes/theme_selector_box.cpp @@ -155,6 +151,8 @@ set(ayugram_files ayu/ui/boxes/message_shot_box.h ayu/ui/components/image_view.cpp ayu/ui/components/image_view.h + ayu/ui/components/icon_picker.cpp + ayu/ui/components/icon_picker.h ayu/libs/json.hpp ayu/libs/json_ext.hpp ayu/libs/sqlite/sqlite3.c diff --git a/Telegram/SourceFiles/api/api_polls.cpp b/Telegram/SourceFiles/api/api_polls.cpp index 303dd8f4e4..9a68a71e01 100644 --- a/Telegram/SourceFiles/api/api_polls.cpp +++ b/Telegram/SourceFiles/api/api_polls.cpp @@ -171,7 +171,7 @@ void Polls::sendVotes( hideSending(); _session->updates().applyUpdates(result); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages && settings.markReadAfterAction && item) { readHistory(item); diff --git a/Telegram/SourceFiles/api/api_send_progress.cpp b/Telegram/SourceFiles/api/api_send_progress.cpp index 18cbbc3c64..2f3eeabc63 100644 --- a/Telegram/SourceFiles/api/api_send_progress.cpp +++ b/Telegram/SourceFiles/api/api_send_progress.cpp @@ -118,7 +118,7 @@ void SendProgressManager::send(const Key &key, int progress) { } // AyuGram sendUploadProgress - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendUploadProgress) { DEBUG_LOG(("[AyuGram] Don't send upload progress")); diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 2cdb347706..5bac3c0aab 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -998,7 +998,7 @@ void Updates::updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline) { }); // AyuGram sendOnlinePackets - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto& config = _session->serverConfig(); bool isOnlineOrig = Core::App().hasActiveWindow(&session()); bool isOnline = settings.sendOnlinePackets && isOnlineOrig; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index cf03883baa..44b5427fd6 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -426,7 +426,7 @@ void ApiWrap::toggleHistoryArchived( if (archived) { history->setFolder(_session->data().folder(archiveId)); } else { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideAllChatsFolder) { if (const auto window = Core::App().activeWindow()) { if (const auto controller = window->sessionController()) { @@ -1303,7 +1303,7 @@ void ApiWrap::migrateFail(not_null<PeerData*> peer, const QString &error) { void ApiWrap::markContentsRead( const base::flat_set<not_null<HistoryItem*>> &items) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto markedIds = QVector<MTPint>(); auto channelMarkedIds = base::flat_map< @@ -1349,7 +1349,7 @@ void ApiWrap::markContentsRead(not_null<HistoryItem*> item) { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages && !passthrough) { return; } @@ -1752,7 +1752,7 @@ void ApiWrap::joinChannel(not_null<ChannelData*> channel) { using Flag = ChannelDataFlag; chatParticipants().loadSimilarPeers(channel); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.collapseSimilarChannels) { channel->setFlags(channel->flags() | Flag::SimilarExpanded); } @@ -3380,7 +3380,7 @@ void ApiWrap::forwardMessages( shared->callback(); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages && settings.markReadAfterAction && history->lastMessage()) { readHistory(history->lastMessage()); diff --git a/Telegram/SourceFiles/ayu/ayu_infra.cpp b/Telegram/SourceFiles/ayu/ayu_infra.cpp index 5571b36ec6..71bb7804a4 100644 --- a/Telegram/SourceFiles/ayu/ayu_infra.cpp +++ b/Telegram/SourceFiles/ayu/ayu_infra.cpp @@ -28,7 +28,7 @@ void initLang() { } void initUiSettings() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); AyuUiSettings::setMonoFont(settings.monoFont); AyuUiSettings::setWideMultiplier(settings.wideMultiplier); diff --git a/Telegram/SourceFiles/ayu/ayu_settings.cpp b/Telegram/SourceFiles/ayu/ayu_settings.cpp index e3ea5e76ec..f38b86d612 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.cpp +++ b/Telegram/SourceFiles/ayu/ayu_settings.cpp @@ -417,8 +417,8 @@ void set_localPremium(bool val) { settings->localPremium = val; } -void set_appIcon(QString val) { - settings->appIcon = std::move(val); +void set_appIcon(const QString &val) { + settings->appIcon = val; } void set_simpleQuotesAndReplies(bool val) { @@ -429,13 +429,13 @@ void set_replaceBottomInfoWithIcons(bool val) { settings->replaceBottomInfoWithIcons = val; } -void set_deletedMark(QString val) { - settings->deletedMark = std::move(val); +void set_deletedMark(const QString &val) { + settings->deletedMark = val; deletedMarkReactive = settings->deletedMark; } -void set_editedMark(QString val) { - settings->editedMark = std::move(val); +void set_editedMark(const QString &val) { + settings->editedMark = val; editedMarkReactive = settings->editedMark; } @@ -522,7 +522,7 @@ void set_showStreamerToggleInTray(bool val) { settings->showStreamerToggleInTray = val; } -void set_monoFont(QString val) { +void set_monoFont(const QString &val) { settings->monoFont = val; } diff --git a/Telegram/SourceFiles/ayu/ayu_settings.h b/Telegram/SourceFiles/ayu/ayu_settings.h index 6bbf81c7c7..44939b05fb 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.h +++ b/Telegram/SourceFiles/ayu/ayu_settings.h @@ -132,11 +132,11 @@ void set_increaseWebviewWidth(bool val); void set_disableNotificationsDelay(bool val); void set_localPremium(bool val); -void set_appIcon(QString val); +void set_appIcon(const QString &val); void set_simpleQuotesAndReplies(bool val); void set_replaceBottomInfoWithIcons(bool val); -void set_deletedMark(QString val); -void set_editedMark(QString val); +void set_deletedMark(const QString &val); +void set_editedMark(const QString &val); void set_recentStickersCount(int val); void set_showReactionsPanelInContextMenu(int val); @@ -162,7 +162,7 @@ void set_showStreamerToggleInDrawer(bool val); void set_showGhostToggleInTray(bool val); void set_showStreamerToggleInTray(bool val); -void set_monoFont(QString val); +void set_monoFont(const QString &val); void set_hideNotificationCounters(bool val); void set_hideNotificationBadge(bool val); diff --git a/Telegram/SourceFiles/ayu/ayu_worker.cpp b/Telegram/SourceFiles/ayu/ayu_worker.cpp index 6ed8a19335..5cfc3faadc 100644 --- a/Telegram/SourceFiles/ayu/ayu_worker.cpp +++ b/Telegram/SourceFiles/ayu/ayu_worker.cpp @@ -42,7 +42,7 @@ void runOnce() { lateInit(); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendOfflinePacketAfterOnline) { return; } diff --git a/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp b/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp index 45395f1ccf..59d00abe2d 100644 --- a/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp +++ b/Telegram/SourceFiles/ayu/ui/ayu_logo.cpp @@ -14,7 +14,7 @@ static QImage LAST_LOADED_NO_MARGIN; namespace AyuAssets { void loadAppIco() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); QString appDataPath = QDir::fromNativeSeparators(qgetenv("APPDATA")); QString tempIconPath = appDataPath + "/AyuGram.ico"; @@ -30,7 +30,7 @@ void loadAppIco() { } void loadIcons() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (LAST_LOADED_NAME != settings.appIcon) { LAST_LOADED_NAME = settings.appIcon; diff --git a/Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.cpp b/Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.cpp deleted file mode 100644 index 1cc86f7aef..0000000000 --- a/Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// This is the source code of AyuGram for Desktop. -// -// We do not and cannot prevent the use of our code, -// but be respectful and credit the original author. -// -// Copyright @Radolyn, 2025 -#include "edit_edited_mark.h" - -#include "boxes/peer_list_controllers.h" -#include "lang/lang_keys.h" -#include "styles/style_boxes.h" -#include "styles/style_layers.h" -#include "styles/style_widgets.h" -#include "ui/widgets/popup_menu.h" -#include "ui/widgets/fields/input_field.h" -#include "ui/widgets/fields/special_fields.h" - -#include "ayu/ayu_settings.h" - -EditEditedMarkBox::EditEditedMarkBox(QWidget *) - : _text( - this, - st::defaultInputField, - tr::ayu_EditedMarkText(), - AyuSettings::getInstance().editedMark) { -} - -void EditEditedMarkBox::prepare() { - const auto defaultEditedMark = tr::lng_edited(tr::now); - auto newHeight = st::contactPadding.top() + _text->height(); - - setTitle(tr::ayu_EditedMarkText()); - - newHeight += st::boxPadding.bottom() + st::contactPadding.bottom(); - setDimensions(st::boxWidth, newHeight); - - addLeftButton(tr::ayu_BoxActionReset(), - [=] - { - _text->setText(defaultEditedMark); - }); - addButton(tr::lng_settings_save(), - [=] - { - save(); - }); - addButton(tr::lng_cancel(), - [=] - { - closeBox(); - }); - - const auto submitted = [=] - { - submit(); - }; - _text->submits( - ) | rpl::start_with_next(submitted, _text->lifetime()); -} - -void EditEditedMarkBox::setInnerFocus() { - _text->setFocusFast(); -} - -void EditEditedMarkBox::submit() { - if (_text->getLastText().trimmed().isEmpty()) { - _text->setFocus(); - _text->showError(); - } else { - save(); - } -} - -void EditEditedMarkBox::resizeEvent(QResizeEvent *e) { - BoxContent::resizeEvent(e); - - _text->resize( - width() - - st::boxPadding.left() - - st::newGroupInfoPadding.left() - - st::boxPadding.right(), - _text->height()); - - const auto left = st::boxPadding.left() + st::newGroupInfoPadding.left(); - _text->moveToLeft(left, st::contactPadding.top()); -} - -void EditEditedMarkBox::save() { - AyuSettings::set_editedMark(_text->getLastText()); - AyuSettings::save(); - - closeBox(); -} diff --git a/Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.h b/Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.h deleted file mode 100644 index d12e3bca2b..0000000000 --- a/Telegram/SourceFiles/ayu/ui/boxes/edit_edited_mark.h +++ /dev/null @@ -1,28 +0,0 @@ -// This is the source code of AyuGram for Desktop. -// -// We do not and cannot prevent the use of our code, -// but be respectful and credit the original author. -// -// Copyright @Radolyn, 2025 -#pragma once - -#include "base/timer.h" -#include "boxes/abstract_box.h" -#include "mtproto/sender.h" - -class EditEditedMarkBox : public Ui::BoxContent -{ -public: - EditEditedMarkBox(QWidget *); - -protected: - void setInnerFocus() override; - void prepare() override; - void resizeEvent(QResizeEvent *e) override; - -private: - void submit(); - void save(); - - object_ptr<Ui::InputField> _text; -}; diff --git a/Telegram/SourceFiles/ayu/ui/boxes/edit_deleted_mark.cpp b/Telegram/SourceFiles/ayu/ui/boxes/edit_mark_box.cpp similarity index 68% rename from Telegram/SourceFiles/ayu/ui/boxes/edit_deleted_mark.cpp rename to Telegram/SourceFiles/ayu/ui/boxes/edit_mark_box.cpp index c077b9aeef..225adbda6c 100644 --- a/Telegram/SourceFiles/ayu/ui/boxes/edit_deleted_mark.cpp +++ b/Telegram/SourceFiles/ayu/ui/boxes/edit_mark_box.cpp @@ -4,7 +4,9 @@ // but be respectful and credit the original author. // // Copyright @Radolyn, 2025 -#include "edit_deleted_mark.h" +#include "edit_mark_box.h" + +#include <utility> #include "boxes/peer_list_controllers.h" #include "lang/lang_keys.h" @@ -17,19 +19,25 @@ #include "ayu/ayu_settings.h" -EditDeletedMarkBox::EditDeletedMarkBox(QWidget *) - : _text( - this, - st::defaultInputField, - tr::ayu_DeletedMarkText(), - AyuSettings::getInstance().deletedMark) { +EditMarkBox::EditMarkBox(QWidget *, + rpl::producer<QString> title, + const QString ¤tValue, + QString defaultValue, + const Fn<void(const QString &)> &saveCallback) + : _title(title) + , _defaultValue(std::move(defaultValue)) + , _saveCallback(saveCallback) + , _text( + this, + st::defaultInputField, + title, + currentValue) { } -void EditDeletedMarkBox::prepare() { - const auto defaultDeletedMark = "🧹"; +void EditMarkBox::prepare() { auto newHeight = st::contactPadding.top() + _text->height(); - setTitle(tr::ayu_DeletedMarkText()); + setTitle(_title); newHeight += st::boxPadding.bottom() + st::contactPadding.bottom(); setDimensions(st::boxWidth, newHeight); @@ -37,7 +45,7 @@ void EditDeletedMarkBox::prepare() { addLeftButton(tr::ayu_BoxActionReset(), [=] { - _text->setText(defaultDeletedMark); + _text->setText(_defaultValue); }); addButton(tr::lng_settings_save(), @@ -59,11 +67,11 @@ void EditDeletedMarkBox::prepare() { ) | rpl::start_with_next(submitted, _text->lifetime()); } -void EditDeletedMarkBox::setInnerFocus() { +void EditMarkBox::setInnerFocus() { _text->setFocusFast(); } -void EditDeletedMarkBox::submit() { +void EditMarkBox::submit() { if (_text->getLastText().trimmed().isEmpty()) { _text->setFocus(); _text->showError(); @@ -72,7 +80,7 @@ void EditDeletedMarkBox::submit() { } } -void EditDeletedMarkBox::resizeEvent(QResizeEvent *e) { +void EditMarkBox::resizeEvent(QResizeEvent *e) { BoxContent::resizeEvent(e); _text->resize( @@ -86,9 +94,7 @@ void EditDeletedMarkBox::resizeEvent(QResizeEvent *e) { _text->moveToLeft(left, st::contactPadding.top()); } -void EditDeletedMarkBox::save() { - AyuSettings::set_deletedMark(_text->getLastText()); - AyuSettings::save(); - +void EditMarkBox::save() { + _saveCallback(_text->getLastText()); closeBox(); } diff --git a/Telegram/SourceFiles/ayu/ui/boxes/edit_deleted_mark.h b/Telegram/SourceFiles/ayu/ui/boxes/edit_mark_box.h similarity index 58% rename from Telegram/SourceFiles/ayu/ui/boxes/edit_deleted_mark.h rename to Telegram/SourceFiles/ayu/ui/boxes/edit_mark_box.h index f2479a8b49..c5201141ca 100644 --- a/Telegram/SourceFiles/ayu/ui/boxes/edit_deleted_mark.h +++ b/Telegram/SourceFiles/ayu/ui/boxes/edit_mark_box.h @@ -9,10 +9,14 @@ #include "base/timer.h" #include "boxes/abstract_box.h" -class EditDeletedMarkBox : public Ui::BoxContent +namespace Ui { +class InputField; +} + +class EditMarkBox : public Ui::BoxContent { public: - EditDeletedMarkBox(QWidget *); + EditMarkBox(QWidget *, rpl::producer<QString> title, const QString& currentValue, QString defaultValue, const Fn<void(const QString&)> &saveCallback); protected: void setInnerFocus() override; @@ -23,5 +27,9 @@ private: void submit(); void save(); + rpl::producer<QString> _title; + QString _defaultValue; + Fn<void(const QString&)> _saveCallback; + object_ptr<Ui::InputField> _text; }; diff --git a/Telegram/SourceFiles/ayu/ui/boxes/message_shot_box.cpp b/Telegram/SourceFiles/ayu/ui/boxes/message_shot_box.cpp index 9b69d02cd2..872affb742 100644 --- a/Telegram/SourceFiles/ayu/ui/boxes/message_shot_box.cpp +++ b/Telegram/SourceFiles/ayu/ui/boxes/message_shot_box.cpp @@ -38,7 +38,7 @@ void MessageShotBox::prepare() { void MessageShotBox::setupContent() { _selectedPalette = std::make_shared<style::palette>(); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto savedShowColorfulReplies = !settings.simpleQuotesAndReplies; using namespace Settings; diff --git a/Telegram/SourceFiles/ayu/ui/settings/icon_picker.cpp b/Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp similarity index 98% rename from Telegram/SourceFiles/ayu/ui/settings/icon_picker.cpp rename to Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp index 4be98b0355..64b8940dde 100644 --- a/Telegram/SourceFiles/ayu/ui/settings/icon_picker.cpp +++ b/Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp @@ -121,7 +121,7 @@ void IconPicker::paintEvent(QPaintEvent *e) { } void IconPicker::mousePressEvent(QMouseEvent *e) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto changed = false; auto x = e->pos().x(); diff --git a/Telegram/SourceFiles/ayu/ui/settings/icon_picker.h b/Telegram/SourceFiles/ayu/ui/components/icon_picker.h similarity index 100% rename from Telegram/SourceFiles/ayu/ui/settings/icon_picker.h rename to Telegram/SourceFiles/ayu/ui/components/icon_picker.h diff --git a/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp b/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp index 7216520af4..f4778ee9c7 100644 --- a/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp +++ b/Telegram/SourceFiles/ayu/ui/context_menu/context_menu.cpp @@ -197,7 +197,7 @@ void AddHistoryAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { } void AddHideMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!needToShowItem(settings.showHideMessageInContextMenu)) { return; } @@ -220,7 +220,7 @@ void AddHideMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { } void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!needToShowItem(settings.showUserMessagesInContextMenu)) { return; } @@ -245,7 +245,7 @@ void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { } void AddMessageDetailsAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!needToShowItem(settings.showMessageDetailsInContextMenu)) { return; } @@ -464,7 +464,7 @@ void AddReadUntilAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.sendReadMessages) { return; } diff --git a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp index 9ace785559..520ea113b9 100644 --- a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp +++ b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp @@ -7,8 +7,7 @@ #include "settings_ayu.h" #include "ayu/ayu_settings.h" -#include "ayu/ui/boxes/edit_deleted_mark.h" -#include "ayu/ui/boxes/edit_edited_mark.h" +#include "ayu/ui/boxes/edit_mark_box.h" #include "ayu/ui/boxes/font_selector.h" #include "lang_auto.h" @@ -27,7 +26,7 @@ #include "styles/style_settings.h" #include "styles/style_widgets.h" -#include "icon_picker.h" +#include "../components/icon_picker.h" #include "tray.h" #include "core/application.h" #include "main/main_domain.h" @@ -37,7 +36,6 @@ #include "ui/boxes/confirm_box.h" #include "ui/boxes/single_choice_box.h" #include "ui/text/text_utilities.h" -#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" @@ -464,41 +462,41 @@ Ayu::Ayu( } void SetupGhostModeToggle(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSubsectionTitle(container, tr::ayu_GhostEssentialsHeader()); std::vector checkboxes{ NestedEntry{ - tr::ayu_DontReadMessages(tr::now), !settings.sendReadMessages, [=](bool enabled) + tr::ayu_DontReadMessages(tr::now), !settings->sendReadMessages, [=](bool enabled) { AyuSettings::set_sendReadMessages(!enabled); AyuSettings::save(); } }, NestedEntry{ - tr::ayu_DontReadStories(tr::now), !settings.sendReadStories, [=](bool enabled) + tr::ayu_DontReadStories(tr::now), !settings->sendReadStories, [=](bool enabled) { AyuSettings::set_sendReadStories(!enabled); AyuSettings::save(); } }, NestedEntry{ - tr::ayu_DontSendOnlinePackets(tr::now), !settings.sendOnlinePackets, [=](bool enabled) + tr::ayu_DontSendOnlinePackets(tr::now), !settings->sendOnlinePackets, [=](bool enabled) { AyuSettings::set_sendOnlinePackets(!enabled); AyuSettings::save(); } }, NestedEntry{ - tr::ayu_DontSendUploadProgress(tr::now), !settings.sendUploadProgress, [=](bool enabled) + tr::ayu_DontSendUploadProgress(tr::now), !settings->sendUploadProgress, [=](bool enabled) { AyuSettings::set_sendUploadProgress(!enabled); AyuSettings::save(); } }, NestedEntry{ - tr::ayu_SendOfflinePacketAfterOnline(tr::now), settings.sendOfflinePacketAfterOnline, [=](bool enabled) + tr::ayu_SendOfflinePacketAfterOnline(tr::now), settings->sendOfflinePacketAfterOnline, [=](bool enabled) { AyuSettings::set_sendOfflinePacketAfterOnline(enabled); AyuSettings::save(); @@ -510,13 +508,14 @@ void SetupGhostModeToggle(not_null<Ui::VerticalLayout*> container) { } void SetupGhostEssentials(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); SetupGhostModeToggle(container); - auto markReadAfterActionVal = container->lifetime().make_state<rpl::variable<bool>>(settings.sendOfflinePacketAfterOnline); + auto markReadAfterActionVal = container->lifetime().make_state<rpl::variable<bool>>( + settings->markReadAfterAction); auto useScheduledMessagesVal = container->lifetime().make_state<rpl::variable< - bool>>(settings.useScheduledMessages); + bool>>(settings->useScheduledMessages); AddButtonWithIcon( container, @@ -528,7 +527,7 @@ void SetupGhostEssentials(not_null<Ui::VerticalLayout*> container) { ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.sendOfflinePacketAfterOnline); + return (enabled != settings->markReadAfterAction); }) | start_with_next( [=](bool enabled) { @@ -555,7 +554,7 @@ void SetupGhostEssentials(not_null<Ui::VerticalLayout*> container) { ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.useScheduledMessages); + return (enabled != settings->useScheduledMessages); }) | start_with_next( [=](bool enabled) { @@ -577,12 +576,12 @@ void SetupGhostEssentials(not_null<Ui::VerticalLayout*> container) { tr::ayu_SendWithoutSoundByDefault(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.sendWithoutSound) + rpl::single(settings->sendWithoutSound) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.sendWithoutSound); + return (enabled != settings->sendWithoutSound); }) | start_with_next( [=](bool enabled) { @@ -595,7 +594,7 @@ void SetupGhostEssentials(not_null<Ui::VerticalLayout*> container) { } void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSubsectionTitle(container, tr::ayu_SpyEssentialsHeader()); @@ -604,12 +603,12 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) { tr::ayu_SaveDeletedMessages(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.saveDeletedMessages) + rpl::single(settings->saveDeletedMessages) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.saveDeletedMessages); + return (enabled != settings->saveDeletedMessages); }) | start_with_next( [=](bool enabled) { @@ -623,12 +622,12 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) { tr::ayu_SaveMessagesHistory(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.saveMessagesHistory) + rpl::single(settings->saveMessagesHistory) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.saveMessagesHistory); + return (enabled != settings->saveMessagesHistory); }) | start_with_next( [=](bool enabled) { @@ -646,12 +645,12 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) { tr::ayu_MessageSavingSaveForBots(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.saveForBots) + rpl::single(settings->saveForBots) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.saveForBots); + return (enabled != settings->saveForBots); }) | start_with_next( [=](bool enabled) { @@ -662,7 +661,7 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) { } void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) { - auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSubsectionTitle(container, tr::ayu_RegexFilters()); @@ -671,12 +670,12 @@ void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) { tr::ayu_FiltersHideFromBlocked(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.hideFromBlocked) + rpl::single(settings->hideFromBlocked) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.hideFromBlocked); + return (enabled != settings->hideFromBlocked); }) | start_with_next( [=](bool enabled) { @@ -687,7 +686,7 @@ void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) { } void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSubsectionTitle(container, tr::ayu_QoLTogglesHeader()); @@ -696,12 +695,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_DisableAds(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.disableAds) + rpl::single(settings->disableAds) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.disableAds); + return (enabled != settings->disableAds); }) | start_with_next( [=](bool enabled) { @@ -715,12 +714,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_DisableStories(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.disableStories) + rpl::single(settings->disableStories) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.disableStories); + return (enabled != settings->disableStories); }) | start_with_next( [=](bool enabled) { @@ -734,12 +733,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_DisableCustomBackgrounds(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.disableCustomBackgrounds) + rpl::single(settings->disableCustomBackgrounds) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.disableCustomBackgrounds); + return (enabled != settings->disableCustomBackgrounds); }) | start_with_next( [=](bool enabled) { @@ -753,12 +752,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_SimpleQuotesAndReplies(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.simpleQuotesAndReplies) + rpl::single(settings->simpleQuotesAndReplies) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.simpleQuotesAndReplies); + return (enabled != settings->simpleQuotesAndReplies); }) | start_with_next( [=](bool enabled) { @@ -769,14 +768,14 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { std::vector checkboxes = { NestedEntry{ - tr::ayu_CollapseSimilarChannels(tr::now), settings.collapseSimilarChannels, [=](bool enabled) + tr::ayu_CollapseSimilarChannels(tr::now), settings->collapseSimilarChannels, [=](bool enabled) { AyuSettings::set_collapseSimilarChannels(enabled); AyuSettings::save(); } }, NestedEntry{ - tr::ayu_HideSimilarChannelsTab(tr::now), settings.hideSimilarChannels, [=](bool enabled) + tr::ayu_HideSimilarChannelsTab(tr::now), settings->hideSimilarChannels, [=](bool enabled) { AyuSettings::set_hideSimilarChannels(enabled); AyuSettings::save(); @@ -795,12 +794,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_DisableNotificationsDelay(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.disableNotificationsDelay) + rpl::single(settings->disableNotificationsDelay) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.disableNotificationsDelay); + return (enabled != settings->disableNotificationsDelay); }) | start_with_next( [=](bool enabled) { @@ -814,12 +813,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_ShowOnlyAddedEmojisAndStickers(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.showOnlyAddedEmojisAndStickers) + rpl::single(settings->showOnlyAddedEmojisAndStickers) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showOnlyAddedEmojisAndStickers); + return (enabled != settings->showOnlyAddedEmojisAndStickers); }) | start_with_next( [=](bool enabled) { @@ -833,12 +832,12 @@ void SetupQoLToggles(not_null<Ui::VerticalLayout*> container) { tr::ayu_LocalPremium(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.localPremium) + rpl::single(settings->localPremium) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.localPremium); + return (enabled != settings->localPremium); }) | start_with_next( [=](bool enabled) { @@ -856,7 +855,7 @@ void SetupAppIcon(not_null<Ui::VerticalLayout*> container) { void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSkip(container); AddSubsectionTitle(container, tr::ayu_ContextMenuElementsHeader()); @@ -870,7 +869,7 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, AddChooseButtonWithIconAndRightText( container, controller, - settings.showReactionsPanelInContextMenu, + settings->showReactionsPanelInContextMenu, options, tr::ayu_SettingsContextMenuReactionsPanel(), tr::ayu_SettingsContextMenuTitle(), @@ -883,7 +882,7 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, AddChooseButtonWithIconAndRightText( container, controller, - settings.showViewsPanelInContextMenu, + settings->showViewsPanelInContextMenu, options, tr::ayu_SettingsContextMenuViewsPanel(), tr::ayu_SettingsContextMenuTitle(), @@ -897,7 +896,7 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, AddChooseButtonWithIconAndRightText( container, controller, - settings.showHideMessageInContextMenu, + settings->showHideMessageInContextMenu, options, tr::ayu_ContextHideMessage(), tr::ayu_SettingsContextMenuTitle(), @@ -910,7 +909,7 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, AddChooseButtonWithIconAndRightText( container, controller, - settings.showUserMessagesInContextMenu, + settings->showUserMessagesInContextMenu, options, tr::ayu_UserMessagesMenuText(), tr::ayu_SettingsContextMenuTitle(), @@ -923,7 +922,7 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, AddChooseButtonWithIconAndRightText( container, controller, - settings.showMessageDetailsInContextMenu, + settings->showMessageDetailsInContextMenu, options, tr::ayu_MessageDetailsPC(), tr::ayu_SettingsContextMenuTitle(), @@ -939,7 +938,7 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container, } void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSkip(container); AddSubsectionTitle(container, tr::ayu_MessageFieldElementsHeader()); @@ -950,12 +949,12 @@ void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldAttachIcon} )->toggleOn( - rpl::single(settings.showAttachButtonInMessageField) + rpl::single(settings->showAttachButtonInMessageField) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showAttachButtonInMessageField); + return (enabled != settings->showAttachButtonInMessageField); }) | start_with_next( [=](bool enabled) { @@ -970,12 +969,12 @@ void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldCommandsIcon} )->toggleOn( - rpl::single(settings.showCommandsButtonInMessageField) + rpl::single(settings->showCommandsButtonInMessageField) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showCommandsButtonInMessageField); + return (enabled != settings->showCommandsButtonInMessageField); }) | start_with_next( [=](bool enabled) { @@ -990,12 +989,12 @@ void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldTTLIcon} )->toggleOn( - rpl::single(settings.showAutoDeleteButtonInMessageField) + rpl::single(settings->showAutoDeleteButtonInMessageField) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showAutoDeleteButtonInMessageField); + return (enabled != settings->showAutoDeleteButtonInMessageField); }) | start_with_next( [=](bool enabled) { @@ -1010,12 +1009,12 @@ void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldEmojiIcon} )->toggleOn( - rpl::single(settings.showEmojiButtonInMessageField) + rpl::single(settings->showEmojiButtonInMessageField) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showEmojiButtonInMessageField); + return (enabled != settings->showEmojiButtonInMessageField); }) | start_with_next( [=](bool enabled) { @@ -1030,12 +1029,12 @@ void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldVoiceIcon} )->toggleOn( - rpl::single(settings.showMicrophoneButtonInMessageField) + rpl::single(settings->showMicrophoneButtonInMessageField) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showMicrophoneButtonInMessageField); + return (enabled != settings->showMicrophoneButtonInMessageField); }) | start_with_next( [=](bool enabled) { @@ -1049,7 +1048,7 @@ void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) { } void SetupMessageFieldPopups(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSkip(container); AddSubsectionTitle(container, tr::ayu_MessageFieldPopupsHeader()); @@ -1060,12 +1059,12 @@ void SetupMessageFieldPopups(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldAttachIcon} )->toggleOn( - rpl::single(settings.showAttachPopup) + rpl::single(settings->showAttachPopup) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showAttachPopup); + return (enabled != settings->showAttachPopup); }) | start_with_next( [=](bool enabled) { @@ -1080,12 +1079,12 @@ void SetupMessageFieldPopups(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::messageFieldEmojiIcon} )->toggleOn( - rpl::single(settings.showEmojiPopup) + rpl::single(settings->showEmojiPopup) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showEmojiPopup); + return (enabled != settings->showEmojiPopup); }) | start_with_next( [=](bool enabled) { @@ -1099,7 +1098,7 @@ void SetupMessageFieldPopups(not_null<Ui::VerticalLayout*> container) { } void SetupDrawerElements(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSkip(container); AddSubsectionTitle(container, tr::ayu_DrawerElementsHeader()); @@ -1110,12 +1109,12 @@ void SetupDrawerElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::ayuLReadMenuIcon} )->toggleOn( - rpl::single(settings.showLReadToggleInDrawer) + rpl::single(settings->showLReadToggleInDrawer) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showLReadToggleInDrawer); + return (enabled != settings->showLReadToggleInDrawer); }) | start_with_next( [=](bool enabled) { @@ -1130,12 +1129,12 @@ void SetupDrawerElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::ayuSReadMenuIcon} )->toggleOn( - rpl::single(settings.showSReadToggleInDrawer) + rpl::single(settings->showSReadToggleInDrawer) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showSReadToggleInDrawer); + return (enabled != settings->showSReadToggleInDrawer); }) | start_with_next( [=](bool enabled) { @@ -1150,12 +1149,12 @@ void SetupDrawerElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::ayuGhostIcon} )->toggleOn( - rpl::single(settings.showGhostToggleInDrawer) + rpl::single(settings->showGhostToggleInDrawer) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showGhostToggleInDrawer); + return (enabled != settings->showGhostToggleInDrawer); }) | start_with_next( [=](bool enabled) { @@ -1171,12 +1170,12 @@ void SetupDrawerElements(not_null<Ui::VerticalLayout*> container) { st::settingsButton, {&st::ayuStreamerModeMenuIcon} )->toggleOn( - rpl::single(settings.showStreamerToggleInDrawer) + rpl::single(settings->showStreamerToggleInDrawer) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showStreamerToggleInDrawer); + return (enabled != settings->showStreamerToggleInDrawer); }) | start_with_next( [=](bool enabled) { @@ -1188,7 +1187,7 @@ void SetupDrawerElements(not_null<Ui::VerticalLayout*> container) { } void SetupTrayElements(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSkip(container); AddSubsectionTitle(container, tr::ayu_TrayElementsHeader()); @@ -1198,12 +1197,12 @@ void SetupTrayElements(not_null<Ui::VerticalLayout*> container) { tr::ayu_EnableGhostModeTray(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.showGhostToggleInTray) + rpl::single(settings->showGhostToggleInTray) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showGhostToggleInTray); + return (enabled != settings->showGhostToggleInTray); }) | start_with_next( [=](bool enabled) { @@ -1218,12 +1217,12 @@ void SetupTrayElements(not_null<Ui::VerticalLayout*> container) { tr::ayu_EnableStreamerModeTray(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.showStreamerToggleInTray) + rpl::single(settings->showStreamerToggleInTray) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showStreamerToggleInTray); + return (enabled != settings->showStreamerToggleInTray); }) | start_with_next( [=](bool enabled) { @@ -1236,7 +1235,7 @@ void SetupTrayElements(not_null<Ui::VerticalLayout*> container) { void SetupShowPeerId(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); const auto options = std::vector{ QString(tr::ayu_SettingsShowID_Hide(tr::now)), @@ -1270,7 +1269,7 @@ void SetupShowPeerId(not_null<Ui::VerticalLayout*> container, { .title = tr::ayu_SettingsShowID(), .options = options, - .initialSelection = settings.showPeerId, + .initialSelection = settings->showPeerId, .callback = save, }); })); @@ -1278,7 +1277,7 @@ void SetupShowPeerId(not_null<Ui::VerticalLayout*> container, } void SetupRecentStickersLimitSlider(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); container->add( object_ptr<Button>(container, @@ -1300,7 +1299,7 @@ void SetupRecentStickersLimitSlider(not_null<Ui::VerticalLayout*> container) { { label->setText(QString::number(amount)); }; - updateLabel(settings.recentStickersCount); + updateLabel(settings->recentStickersCount); slider->setPseudoDiscrete( 200 + 1, @@ -1309,7 +1308,7 @@ void SetupRecentStickersLimitSlider(not_null<Ui::VerticalLayout*> container) { { return amount; }, - settings.recentStickersCount, + settings->recentStickersCount, [=](int amount) { updateLabel(amount); @@ -1325,7 +1324,7 @@ void SetupRecentStickersLimitSlider(not_null<Ui::VerticalLayout*> container) { void SetupWideMultiplierSlider(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); container->add( object_ptr<Button>(container, @@ -1360,12 +1359,12 @@ void SetupWideMultiplierSlider(not_null<Ui::VerticalLayout*> container, return kMinSize + index * kStep; }; - updateLabel(settings.wideMultiplier); + updateLabel(settings->wideMultiplier); slider->setPseudoDiscrete( kSizeAmount, [=](int index) { return index; }, - valueToIndex(settings.wideMultiplier), + valueToIndex(settings->wideMultiplier), [=](int index) { updateLabel(indexToValue(index)); @@ -1394,13 +1393,13 @@ void SetupWideMultiplierSlider(not_null<Ui::VerticalLayout*> container, } void SetupFonts(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); const auto monoButton = AddButtonWithLabel( container, tr::ayu_MonospaceFont(), rpl::single( - settings.monoFont.isEmpty() ? tr::ayu_FontDefault(tr::now) : settings.monoFont + settings->monoFont.isEmpty() ? tr::ayu_FontDefault(tr::now) : settings->monoFont ), st::settingsButtonNoIcon); const auto monoGuard = Ui::CreateChild<base::binary_guard>(monoButton.get()); @@ -1419,7 +1418,7 @@ void SetupFonts(not_null<Ui::VerticalLayout*> container, not_null<Window::Sessio } void SetupSendConfirmations(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSubsectionTitle(container, tr::ayu_ConfirmationsTitle()); @@ -1428,12 +1427,12 @@ void SetupSendConfirmations(not_null<Ui::VerticalLayout*> container) { tr::ayu_StickerConfirmation(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.stickerConfirmation) + rpl::single(settings->stickerConfirmation) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.stickerConfirmation); + return (enabled != settings->stickerConfirmation); }) | start_with_next( [=](bool enabled) { @@ -1447,12 +1446,12 @@ void SetupSendConfirmations(not_null<Ui::VerticalLayout*> container) { tr::ayu_GIFConfirmation(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.gifConfirmation) + rpl::single(settings->gifConfirmation) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.gifConfirmation); + return (enabled != settings->gifConfirmation); }) | start_with_next( [=](bool enabled) { @@ -1466,12 +1465,12 @@ void SetupSendConfirmations(not_null<Ui::VerticalLayout*> container) { tr::ayu_VoiceConfirmation(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.voiceConfirmation) + rpl::single(settings->voiceConfirmation) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.voiceConfirmation); + return (enabled != settings->voiceConfirmation); }) | start_with_next( [=](bool enabled) { @@ -1482,19 +1481,19 @@ void SetupSendConfirmations(not_null<Ui::VerticalLayout*> container) { } void SetupMarks(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddButtonWithIcon( container, tr::ayu_ReplaceMarksWithIcons(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.replaceBottomInfoWithIcons) + rpl::single(settings->replaceBottomInfoWithIcons) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.replaceBottomInfoWithIcons); + return (enabled != settings->replaceBottomInfoWithIcons); }) | start_with_next( [=](bool enabled) { @@ -1511,7 +1510,16 @@ void SetupMarks(not_null<Ui::VerticalLayout*> container) { )->addClickHandler( [=]() { - auto box = Box<EditDeletedMarkBox>(); + auto box = Box<EditMarkBox>( + tr::ayu_DeletedMarkText(), + settings->deletedMark, + QString("🧹"), + [=](const QString &value) + { + AyuSettings::set_deletedMark(value); + AyuSettings::save(); + } + ); Ui::show(std::move(box)); }); @@ -1523,25 +1531,34 @@ void SetupMarks(not_null<Ui::VerticalLayout*> container) { )->addClickHandler( [=]() { - auto box = Box<EditEditedMarkBox>(); + auto box = Box<EditMarkBox>( + tr::ayu_EditedMarkText(), + settings->editedMark, + tr::lng_edited(tr::now), + [=](const QString &value) + { + AyuSettings::set_editedMark(value); + AyuSettings::save(); + } + ); Ui::show(std::move(box)); }); } void SetupFolderSettings(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddButtonWithIcon( container, tr::ayu_HideNotificationCounters(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.hideNotificationCounters) + rpl::single(settings->hideNotificationCounters) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.hideNotificationCounters); + return (enabled != settings->hideNotificationCounters); }) | start_with_next( [=](bool enabled) { @@ -1557,12 +1574,12 @@ void SetupFolderSettings(not_null<Ui::VerticalLayout*> container, not_null<Windo tr::ayu_HideNotificationBadge(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.hideNotificationBadge) + rpl::single(settings->hideNotificationBadge) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.hideNotificationBadge); + return (enabled != settings->hideNotificationBadge); }) | start_with_next( [=](bool enabled) { @@ -1581,12 +1598,12 @@ void SetupFolderSettings(not_null<Ui::VerticalLayout*> container, not_null<Windo tr::ayu_HideAllChats(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.hideAllChatsFolder) + rpl::single(settings->hideAllChatsFolder) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.hideAllChatsFolder); + return (enabled != settings->hideAllChatsFolder); }) | start_with_next( [=](bool enabled) { @@ -1597,7 +1614,7 @@ void SetupFolderSettings(not_null<Ui::VerticalLayout*> container, not_null<Windo } void SetupChannelSettings(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); const auto options = std::vector{ tr::ayu_ChannelBottomButtonHide(tr::now), @@ -1608,7 +1625,7 @@ void SetupChannelSettings(not_null<Ui::VerticalLayout*> container, not_null<Wind AddChooseButtonWithIconAndRightText( container, controller, - settings.channelBottomButton, + settings->channelBottomButton, options, tr::ayu_ChannelBottomButton(), tr::ayu_ChannelBottomButton(), @@ -1620,7 +1637,7 @@ void SetupChannelSettings(not_null<Ui::VerticalLayout*> container, not_null<Wind } void SetupNerdSettings(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); SetupShowPeerId(container, controller); @@ -1629,12 +1646,12 @@ void SetupNerdSettings(not_null<Ui::VerticalLayout*> container, not_null<Window: tr::ayu_SettingsShowMessageSeconds(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.showMessageSeconds) + rpl::single(settings->showMessageSeconds) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showMessageSeconds); + return (enabled != settings->showMessageSeconds); }) | start_with_next( [=](bool enabled) { @@ -1648,12 +1665,12 @@ void SetupNerdSettings(not_null<Ui::VerticalLayout*> container, not_null<Window: tr::ayu_SettingsShowMessageShot(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.showMessageShot) + rpl::single(settings->showMessageShot) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.showMessageShot); + return (enabled != settings->showMessageShot); }) | start_with_next( [=](bool enabled) { @@ -1664,7 +1681,7 @@ void SetupNerdSettings(not_null<Ui::VerticalLayout*> container, not_null<Window: } void SetupWebviewSettings(not_null<Ui::VerticalLayout*> container) { - const auto& settings = AyuSettings::getInstance(); + auto *settings = &AyuSettings::getInstance(); AddSubsectionTitle(container, rpl::single(QString("Webview"))); @@ -1673,12 +1690,12 @@ void SetupWebviewSettings(not_null<Ui::VerticalLayout*> container) { tr::ayu_SettingsSpoofWebviewAsAndroid(), st::settingsButtonNoIcon )->toggleOn( - rpl::single(settings.spoofWebviewAsAndroid) + rpl::single(settings->spoofWebviewAsAndroid) )->toggledValue( ) | rpl::filter( [=](bool enabled) { - return (enabled != settings.spoofWebviewAsAndroid); + return (enabled != settings->spoofWebviewAsAndroid); }) | start_with_next( [=](bool enabled) { @@ -1689,14 +1706,14 @@ void SetupWebviewSettings(not_null<Ui::VerticalLayout*> container) { std::vector checkboxes = { NestedEntry{ - tr::ayu_SettingsIncreaseWebviewHeight(tr::now), settings.increaseWebviewHeight, [=](bool enabled) + tr::ayu_SettingsIncreaseWebviewHeight(tr::now), settings->increaseWebviewHeight, [=](bool enabled) { AyuSettings::set_increaseWebviewHeight(enabled); AyuSettings::save(); } }, NestedEntry{ - tr::ayu_SettingsIncreaseWebviewWidth(tr::now), settings.increaseWebviewWidth, [=](bool enabled) + tr::ayu_SettingsIncreaseWebviewWidth(tr::now), settings->increaseWebviewWidth, [=](bool enabled) { AyuSettings::set_increaseWebviewWidth(enabled); AyuSettings::save(); diff --git a/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp b/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp index c5169b074d..d2fddc4a50 100644 --- a/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp +++ b/Telegram/SourceFiles/ayu/ui/utils/ayu_profile_values.cpp @@ -15,7 +15,7 @@ constexpr auto kMaxChannelId = -1000000000000; QString IDString(not_null<PeerData*> peer) { auto resultId = QString::number(getBareID(peer)); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.showPeerId == 2) { if (peer->isChannel()) { resultId = QString::number(peerToChannel(peer->id).bare - kMaxChannelId).prepend("-"); diff --git a/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp b/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp index b2c3dd3817..66632715ca 100644 --- a/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp +++ b/Telegram/SourceFiles/ayu/utils/telegram_helpers.cpp @@ -108,7 +108,7 @@ bool isMessageHidden(const not_null<HistoryItem*> item) { return true; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideFromBlocked) { if (item->from()->isUser() && item->from()->asUser()->isBlocked()) { @@ -513,7 +513,7 @@ int getScheduleTime(int64 sumSize) { } bool isMessageSavable(const not_null<HistoryItem *> item) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.saveDeletedMessages) { return false; diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 184c373647..fb02fa7a8f 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1731,7 +1731,7 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback( } } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages && settings.markReadAfterAction && history->lastMessage()) { readHistory(history->lastMessage()); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index e281ffa327..ab35c28a8c 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -2259,7 +2259,7 @@ void EmojiListWidget::refreshCustom() { && !_allowWithoutPremium; const auto owner = &session->data(); const auto &sets = owner->stickers().sets(); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto push = [&](uint64 setId, bool installed) { const auto megagroup = _megagroupSet && (setId == Data::Stickers::MegagroupSetId); diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 7fa8993757..0732be3700 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -408,7 +408,7 @@ bool FieldAutocomplete::clearFilteredBotCommands() { FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() { const auto data = &_session->data().stickers(); const auto list = data->getListByEmoji({ _emoji }, _stickersSeed); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto result = ranges::views::all( list ) | ranges::views::filter([&](not_null<DocumentData*> sticker) { diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index faee7841bb..1970176897 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -502,7 +502,7 @@ void GifsListWidget::selectInlineResult( return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (AyuSettings::isUseScheduledMessages()) { auto current = base::unixtime::now(); options.scheduled = current + 12; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 4d0d324935..e3f2c7da7b 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -763,7 +763,7 @@ void StickersListWidget::fillFilteredStickersRow() { } void StickersListWidget::addSearchRow(not_null<StickersSet*> set) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.showOnlyAddedEmojisAndStickers && !SetInMyList(set->flags)) { return; } @@ -1910,7 +1910,7 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) { && (e->modifiers() & Qt::ControlModifier)) { showStickerSetBox(document, set.id); } else { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto from = messageSentAnimationInfo( sticker->section, sticker->index, @@ -2339,7 +2339,7 @@ auto StickersListWidget::collectRecentStickers() -> std::vector<Sticker> { result.reserve(cloudCount + recent.size() + customCount); _custom.reserve(cloudCount + recent.size() + customCount); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto add = [&](not_null<DocumentData*> document, bool custom) { if (result.size() >= settings.recentStickersCount) { diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp index e2772b3aaa..c8291d1436 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp @@ -474,7 +474,7 @@ void TabbedPanel::showStarted() { } bool TabbedPanel::eventFilter(QObject *obj, QEvent *e) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (TabbedPanelShowOnClick.value() || !settings.showEmojiPopup) { return false; diff --git a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp index a8477291de..2be42e9c6e 100644 --- a/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp @@ -195,7 +195,7 @@ PreviewWrap::PreviewWrap( } }, lifetime()); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); { const auto close = Ui::CreateChild<Ui::RoundButton>( diff --git a/Telegram/SourceFiles/data/components/promo_suggestions.cpp b/Telegram/SourceFiles/data/components/promo_suggestions.cpp index 43cafca12b..ce027c6703 100644 --- a/Telegram/SourceFiles/data/components/promo_suggestions.cpp +++ b/Telegram/SourceFiles/data/components/promo_suggestions.cpp @@ -115,7 +115,7 @@ void PromoSuggestions::refreshTopPromotion() { |= _dismissedSuggestions.emplace(qs(suggestion)).second; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableAds) { setTopPromoted(nullptr, QString(), QString()); return; diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index 81a8d57e0b..fa0674124b 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -228,7 +228,7 @@ void SponsoredMessages::inject( } bool SponsoredMessages::canHaveFor(not_null<History*> history) const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableAds) { return false; } @@ -242,7 +242,7 @@ bool SponsoredMessages::canHaveFor(not_null<History*> history) const { } bool SponsoredMessages::isTopBarFor(not_null<History*> history) const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableAds) { return false; } diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index c4b3d28630..6d0ecd4022 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -484,7 +484,7 @@ void ChatFilters::requestToggleTags(bool value, Fn<void()> fail) { void ChatFilters::received(const QVector<MTPDialogFilter> &list) { // AyuGram hideAllChatsFolder - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto position = 0; auto changed = false; @@ -526,7 +526,7 @@ void ChatFilters::received(const QVector<MTPDialogFilter> &list) { void ChatFilters::apply(const MTPUpdate &update) { // AyuGram hideAllChatsFolder - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); update.match([&](const MTPDupdateDialogFilter &data) { if (const auto filter = data.vfilter()) { @@ -912,7 +912,7 @@ FilterId ChatFilters::lookupId(int index) const { return FilterId(); // AyuGram: fix crash when using `hideAllChatsFolder` } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (_owner->session().user()->isPremium() || !_list.front().id() || settings.hideAllChatsFolder) { return _list[index].id(); diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 59060f9612..6939217295 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -627,7 +627,7 @@ void Histories::sendReadRequests() { DEBUG_LOG(("Reading: send requests with count %1.").arg(_states.size())); // AyuGram sendReadMessages - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages) { DEBUG_LOG(("[AyuGram] Don't read messages")); _states.clear(); diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 4e87a05fb2..8d905461cd 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -1491,7 +1491,7 @@ void Reactions::send(not_null<HistoryItem*> item, bool addToRecent) { _sentRequests.remove(id); _owner->session().api().applyUpdates(result); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages && settings.markReadAfterAction && item) { readHistory(item); } diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp index 762cd86373..717a00bbc6 100644 --- a/Telegram/SourceFiles/data/data_peer_values.cpp +++ b/Telegram/SourceFiles/data/data_peer_values.cpp @@ -399,7 +399,7 @@ rpl::producer<bool> PeerPremiumValue(not_null<PeerData*> peer) { } rpl::producer<bool> AmPremiumValue(not_null<Main::Session*> session) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.localPremium) { return rpl::single(true); } diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp index 00884f67f2..d520913655 100644 --- a/Telegram/SourceFiles/data/data_replies_list.cpp +++ b/Telegram/SourceFiles/data/data_replies_list.cpp @@ -1005,7 +1005,7 @@ void RepliesList::sendReadTillRequest() { const auto api = &_history->session().api(); api->request(base::take(_readRequestId)).cancel(); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages) { return; } diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 4d32e35e57..c1a6047da9 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -318,7 +318,7 @@ Session::Session(not_null<Main::Session*> session) }, _lifetime); // AyuGram disableStories - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.disableStories) { _stories->loadMore(Data::StorySourcesList::NotHidden); } @@ -2486,7 +2486,7 @@ void Session::updateEditedMessage(const MTPMessage &data) { } // AyuGram saveMessagesHistory - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); HistoryMessageEdition edit; if (data.type() != mtpc_message) { @@ -2638,7 +2638,7 @@ void Session::unregisterMessageTTL( } void Session::checkTTLs() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); _ttlCheckTimer.cancel(); const auto now = base::unixtime::now(); diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index d327f49f18..ef8d55c959 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -1120,7 +1120,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadStories) { return; } @@ -1270,7 +1270,7 @@ void Stories::toggleHidden( void Stories::sendMarkAsReadRequest( not_null<PeerData*> peer, StoryId tillId) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadStories) { return; } @@ -1305,7 +1305,7 @@ void Stories::checkQuitPreventFinished() { void Stories::sendMarkAsReadRequests() { _markReadTimer.cancel(); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadStories) { return; } @@ -1329,7 +1329,7 @@ void Stories::sendIncrementViewsRequests() { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadStories) { return; } @@ -1941,7 +1941,7 @@ bool Stories::isQuitPrevent() { sendIncrementViewsRequests(); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadStories || _markReadRequests.empty() && _incrementViewsRequests.empty()) { return false; } diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 60b06e0b61..5cc7fe3084 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -490,7 +490,7 @@ bool UserData::isFake() const { bool UserData::isPremium() const { if (id) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.localPremium) { if (getSession(id.value)) { return true; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index 5ad217ebea..e8f745d139 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -553,7 +553,7 @@ void Row::paintUserpic( updateCornerBadgeShown(peer, nullptr, hasUnreadBadgesAbove); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto cornerBadgeShown = !_cornerBadgeUserpic ? _cornerBadgeShown diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 667171b95b..348808135e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1307,7 +1307,7 @@ void Widget::setupMainMenuToggle() { ? &st::dialogsMenuToggleUnread : &st::dialogsMenuToggleUnreadMuted; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideNotificationCounters) { icon = nullptr; } @@ -1318,7 +1318,7 @@ void Widget::setupMainMenuToggle() { void Widget::setupStories() { // AyuGram disableStories - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableStories) { return; } @@ -2277,7 +2277,7 @@ void Widget::updateStoriesVisibility() { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableStories) { _stories->setVisible(false); return; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 063551f996..18c0151f1f 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1253,7 +1253,7 @@ void HistoryItem::setCommentsItemId(FullMsgId id) { void HistoryItem::setServiceText(PreparedServiceText &&prepared) { auto text = std::move(prepared.text); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (date() > 0) { const auto timeString = QString(" (%1)").arg(QLocale().toString( base::unixtime::parse(_date), @@ -2183,7 +2183,7 @@ void HistoryItem::clearMediaAsExpired() { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.saveDeletedMessages) { return; } @@ -3189,7 +3189,7 @@ void HistoryItem::setDeleted() { _deleted = true; if (isService()) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); setAyuHint(settings.deletedMark); } else { history()->owner().requestItemViewRefresh(this); diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 65d43a511a..8ec2505a50 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -484,7 +484,7 @@ void HistoryMessageReply::updateData( && (asExternal || _fields.manualQuote); _multiline = !_fields.storyId && (asExternal || nonEmptyQuote); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto author = resolvedMessage ? resolvedMessage->from().get() : resolvedStory diff --git a/Telegram/SourceFiles/history/history_item_helpers.cpp b/Telegram/SourceFiles/history/history_item_helpers.cpp index 050dee1005..715c91095d 100644 --- a/Telegram/SourceFiles/history/history_item_helpers.cpp +++ b/Telegram/SourceFiles/history/history_item_helpers.cpp @@ -525,7 +525,7 @@ QString NewMessagePostAuthor(const Api::SendAction &action) { bool ShouldSendSilent( not_null<PeerData*> peer, const Api::SendOptions &options) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.sendWithoutSound) { return !options.silent; } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index b2113c07aa..0cb02c937b 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -510,7 +510,7 @@ HistoryWidget::HistoryWidget( _fieldCharsCountManager.limitExceeds( ) | rpl::start_with_next([=] { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto hide = _fieldCharsCountManager.isLimitExceeded(); if (_silent) { _silent->setVisible(!hide); @@ -1980,7 +1980,7 @@ void HistoryWidget::fileChosen(ChatHelpers::FileChosen &&data) { Data::InsertCustomEmoji(_field.data(), data.document); } } else if (_history) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.sendReadMessages && settings.markReadAfterAction) { if (const auto lastMessage = history()->lastMessage()) { readHistory(lastMessage); @@ -2789,7 +2789,7 @@ void HistoryWidget::setHistory(History *history) { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto was = _attachBotsMenu && _history && _history->peer->isUser(); const auto now = _attachBotsMenu && history && history->peer->isUser() && settings.showAttachPopup; @@ -2875,7 +2875,7 @@ void HistoryWidget::refreshAttachBotsMenu() { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); _attachBotsMenu = InlineBots::MakeAttachBotsMenu( this, @@ -3201,7 +3201,7 @@ bool HistoryWidget::canWriteMessage() const { } void HistoryWidget::updateControlsVisibility() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto fieldDisabledRemoved = (_fieldDisabled != nullptr); const auto hideExtraButtons = _fieldCharsCountManager.isLimitExceeded(); @@ -4583,7 +4583,7 @@ void HistoryWidget::sendVoice(const VoiceToSend &data) { } void HistoryWidget::send(Api::SendOptions options) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (AyuSettings::isUseScheduledMessages() && !options.scheduled) { auto current = base::unixtime::now(); options.scheduled = current + 12; @@ -4784,7 +4784,7 @@ void HistoryWidget::goToDiscussionGroup() { } bool HistoryWidget::hasDiscussionGroup() const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.channelBottomButton != 2) { return false; } @@ -5377,7 +5377,7 @@ bool HistoryWidget::isChoosingTheme() const { } bool HistoryWidget::isMuteUnmute() const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.channelBottomButton == 0) { return false; } @@ -5394,7 +5394,7 @@ bool HistoryWidget::isSearching() const { } bool HistoryWidget::showRecordButton() const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.showMicrophoneButtonInMessageField) { return false; } @@ -5637,7 +5637,7 @@ void HistoryWidget::showKeyboardHideButton() { } void HistoryWidget::toggleKeyboard(bool manual) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto fieldEnabled = canWriteMessage() && !_showAnimation; if (_kbShown || _kbReplyTo) { @@ -5865,7 +5865,7 @@ bool HistoryWidget::fieldOrDisabledShown() const { } void HistoryWidget::moveFieldControls() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto keyboardHeight = 0; auto bottom = height(); @@ -5966,7 +5966,7 @@ void HistoryWidget::moveFieldControls() { } void HistoryWidget::updateFieldSize() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto kbShowShown = _history && !_kbShown && _keyboard->hasMarkup(); auto fieldWidth = width() @@ -7105,7 +7105,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto wasVisible = _kbShown || _kbReplyTo; const auto wasMsgId = _keyboard->forMsgId(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 28b0860107..a1e14004e0 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -2129,7 +2129,7 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) { : 0), }; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (AyuSettings::isUseScheduledMessages()) { auto current = base::unixtime::now(); options.scheduled = current + 12 + 5; @@ -2221,7 +2221,7 @@ void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) { options.ttlSeconds = std::numeric_limits<int>::max(); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (AyuSettings::isUseScheduledMessages()) { auto current = base::unixtime::now(); options.scheduled = current + 12 + 5; diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp index 8225aa4a99..186c99e3d0 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp @@ -415,7 +415,7 @@ void BottomInfo::layout() { } void BottomInfo::layoutDateText() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.replaceBottomInfoWithIcons) { const auto deleted = (_data.flags & Data::Flag::AyuDeleted) diff --git a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp index efe1beea85..a03c9ba96c 100644 --- a/Telegram/SourceFiles/history/view/history_view_context_menu.cpp +++ b/Telegram/SourceFiles/history/view/history_view_context_menu.cpp @@ -1513,7 +1513,7 @@ void AddWhoReactedAction( not_null<QWidget*> context, not_null<HistoryItem*> item, not_null<Window::SessionController*> controller) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!AyuUi::needToShowItem(settings.showViewsPanelInContextMenu)) { return; } diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index 71f579fdf5..6c3f680bca 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -646,7 +646,7 @@ void Reply::paint( Ui::Text::ValidateQuotePaintCache(*cache, quoteSt); Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.simpleQuotesAndReplies && backgroundEmoji) { ValidateBackgroundEmoji( backgroundEmojiId, diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.cpp b/Telegram/SourceFiles/history/view/history_view_send_action.cpp index 8a1d1539b9..92b2296c16 100644 --- a/Telegram/SourceFiles/history/view/history_view_send_action.cpp +++ b/Telegram/SourceFiles/history/view/history_view_send_action.cpp @@ -67,7 +67,7 @@ bool SendActionPainter::updateNeedsAnimating( return false; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideFromBlocked) { if (user->isBlocked()) { return false; 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 549ff6e42d..20d8d5232b 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -788,7 +788,7 @@ void TopBarWidget::infoClicked() { void TopBarWidget::backClicked() { if (_activeChat.key.folder()) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideAllChatsFolder) { const auto filters = &_controller->session().data().chatsFilters(); const auto lookupId = filters->lookupId(_controller->session().premium() ? 0 : 1); @@ -1156,7 +1156,7 @@ void TopBarWidget::updateControlsVisibility() { return; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); _clear->show(); _delete->setVisible(_canDelete); @@ -1337,14 +1337,14 @@ void TopBarWidget::updateMembersShowArea() { } bool TopBarWidget::showSelectedState() const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); return (_selectedCount > 0) && (_canDelete || _canForward || _canSendNow || settings.showMessageShot); } void TopBarWidget::showSelected(SelectedState state) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto canDelete = (state.count > 0 && state.count == state.canDeleteCount); auto canForward = (state.count > 0 && state.count == state.canForwardCount); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 0cbd0a5470..52398fa7c0 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -325,7 +325,7 @@ Document::Document( const auto &data = &_parent->data()->history()->owner(); _parent->data()->removeFromSharedMediaIndex(); setDocumentLinks(_data, realParent, [=] { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.saveDeletedMessages) { _openl = nullptr; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp index 8ceee136d6..ca88904a62 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_web_page.cpp @@ -936,7 +936,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const { Ui::Text::ValidateQuotePaintCache(*cache, _st); Ui::Text::FillQuotePaint(p, outer, *cache, _st); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.simpleQuotesAndReplies && backgroundEmoji) { ValidateBackgroundEmoji( backgroundEmojiId, diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 97dcb6f375..c2eae3ffb2 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -1189,7 +1189,7 @@ bool AdjustMenuGeometryForSelector( not_null<Ui::PopupMenu*> menu, QPoint desiredPosition, not_null<Selector*> selector) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!AyuUi::needToShowItem(settings.showReactionsPanelInContextMenu)) { return false; } @@ -1357,7 +1357,7 @@ AttachSelectorResult AttachSelectorToMenu( Fn<void(ChosenReaction)> chosen, TextWithEntities about, IconFactory iconFactory) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!AyuUi::needToShowItem(settings.showReactionsPanelInContextMenu)) { return AttachSelectorResult::Skipped; } @@ -1409,7 +1409,7 @@ auto AttachSelectorToMenu( IconFactory iconFactory, Fn<bool()> paused) -> base::expected<not_null<Selector*>, AttachSelectorResult> { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!AyuUi::needToShowItem(settings.showReactionsPanelInContextMenu)) { return base::make_unexpected(AttachSelectorResult::Skipped); } diff --git a/Telegram/SourceFiles/info/info_top_bar.cpp b/Telegram/SourceFiles/info/info_top_bar.cpp index 6c66051178..4d689a7721 100644 --- a/Telegram/SourceFiles/info/info_top_bar.cpp +++ b/Telegram/SourceFiles/info/info_top_bar.cpp @@ -505,7 +505,7 @@ void TopBar::updateControlsVisibility(anim::type animated) { void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) { // AyuGram disableStories - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableStories) { return; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 843f7bff4b..acc48462c5 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1193,7 +1193,7 @@ bool SetClickContext( } object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto result = object_ptr<Ui::VerticalLayout>(_wrap); auto tracker = Ui::MultiSlideTracker(); diff --git a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp index 942c3b1b3a..80ceb966a1 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp @@ -140,7 +140,7 @@ object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia( using namespace rpl::mappers; using MediaType = Media::Type; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto content = object_ptr<Ui::VerticalLayout>(parent); auto tracker = Ui::MultiSlideTracker(); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 30279be011..992335f1a7 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -765,7 +765,7 @@ void BotAction::handleKeyPress(not_null<QKeyEvent*> e) { } QString WebviewPlatform() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); return settings.spoofWebviewAsAndroid ? "android" : "tdesktop"; } diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index b26195d4c5..b9987430ff 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -321,7 +321,7 @@ rpl::producer<> Session::downloaderTaskFinished() const { } bool Session::premium() const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.localPremium) { return true; } @@ -330,7 +330,7 @@ bool Session::premium() const { } bool Session::premiumPossible() const { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.localPremium) { return true; } @@ -353,7 +353,7 @@ rpl::producer<bool> Session::premiumPossibleValue() const { return _user->isPremium(); }); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.localPremium) { premium = rpl::single(true); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp index 4d5a744f59..0335bd9b74 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp @@ -100,7 +100,7 @@ void RepostView::draw(Painter &p, int x, int y, int availableWidth) { Ui::Text::ValidateQuotePaintCache(*cache, quoteSt); Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.simpleQuotesAndReplies && backgroundEmoji) { using namespace HistoryView; if (backgroundEmoji->firstFrameMask.isNull() diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 0eaad13aa3..57920d2f12 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -694,7 +694,7 @@ FillMenuResult FillSendMenu( : st::defaultComposeIcons; if (sending && type != Type::Reminder) { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); menu->addAction( settings.sendWithoutSound ? tr::ayu_SendWithSound(tr::now) : tr::lng_send_silent_message(tr::now), [=] { action({ Api::SendOptions{ .silent = true } }, details); }, diff --git a/Telegram/SourceFiles/platform/win/main_window_win.cpp b/Telegram/SourceFiles/platform/win/main_window_win.cpp index 917041a7a9..3fb263bbdf 100644 --- a/Telegram/SourceFiles/platform/win/main_window_win.cpp +++ b/Telegram/SourceFiles/platform/win/main_window_win.cpp @@ -512,7 +512,7 @@ void MainWindow::unreadCounterChangedHook() { } void MainWindow::updateTaskbarAndIconCounters() { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto counter = settings.hideNotificationBadge ? 0 : Core::App().unreadBadge(); const auto muted = settings.hideNotificationBadge ? 0 : Core::App().unreadBadgeMuted(); diff --git a/Telegram/SourceFiles/platform/win/tray_win.cpp b/Telegram/SourceFiles/platform/win/tray_win.cpp index bf307ed0c3..8dcf5347dd 100644 --- a/Telegram/SourceFiles/platform/win/tray_win.cpp +++ b/Telegram/SourceFiles/platform/win/tray_win.cpp @@ -142,7 +142,7 @@ bool DarkTasbarValueValid/* = false*/; ScaledLogoLight = base::flat_map<int, QImage>(); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideNotificationBadge) { args.count = 0; } diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 3a8087d9e0..9775fb7ed0 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -1042,7 +1042,7 @@ QPointer<Ui::RpWidget> Premium::createPinnedToTop( } } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.localPremium) { return tr::ayu_LocalPremiumNotice(Ui::Text::RichLangValue); } diff --git a/Telegram/SourceFiles/tray.cpp b/Telegram/SourceFiles/tray.cpp index a1f0034188..4af0e57ddc 100644 --- a/Telegram/SourceFiles/tray.cpp +++ b/Telegram/SourceFiles/tray.cpp @@ -102,7 +102,7 @@ void Tray::rebuildMenu() { [=] { toggleSoundNotifications(); }); } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.showGhostToggleInTray) { auto turnGhostModeText = _textUpdates.events( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 9fa366665c..1891e7fb9c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -386,7 +386,7 @@ Panel::Panel(Args &&args) , _allowClipboardRead(args.allowClipboardRead) { _widget->setWindowFlag(Qt::WindowStaysOnTopHint, false); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); auto size = QSize(st::botWebViewPanelSize); if (settings.increaseWebviewHeight) { size.setHeight(st::botWebViewPanelHeightIncreased); diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp index 8eac60bd3d..e8a2f4906b 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp @@ -48,7 +48,7 @@ void EnsureBlockquoteCache( cache->outlines = colors.outlines; cache->icon = colors.name; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.simpleQuotesAndReplies) { cache->bg = QColor(0, 0, 0, 0); } diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index 2c62ba1cad..04bd0d31b3 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -340,7 +340,7 @@ System::Timing System::countTiming( delay = config.notifyDefaultDelay; } - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.disableNotificationsDelay) { delay = minimalDelay; } diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index e2ff30b077..a2d4cdfd35 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -482,7 +482,7 @@ auto ChatThemeValueFromPeer( peer ) | rpl::map([=](ResolvedTheme resolved) -> rpl::producer<std::shared_ptr<Ui::ChatTheme>> { - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); // this check ensures that background is not a pattern wallpaper in a private chat if (settings.disableCustomBackgrounds && resolved.paper && resolved.paper->media) { resolved.paper = std::nullopt; diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp index 0681286baf..52590c897d 100644 --- a/Telegram/SourceFiles/window/window_filters_menu.cpp +++ b/Telegram/SourceFiles/window/window_filters_menu.cpp @@ -140,7 +140,7 @@ void FiltersMenu::setupMainMenuIcon() { ? &st::windowFiltersMainMenuUnread : &st::windowFiltersMainMenuUnreadMuted; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideNotificationCounters) { icon = nullptr; } @@ -177,7 +177,7 @@ void FiltersMenu::scrollToButton(not_null<Ui::RpWidget*> widget) { void FiltersMenu::refresh() { // AyuGram hideAllChatsFolder - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto filters = &_session->session().data().chatsFilters(); if (!filters->has() || _ignoreRefresh) { @@ -310,7 +310,7 @@ base::unique_qptr<Ui::SideBarButton> FiltersMenu::prepareButton( auto count = (chats + state.marks) - (includeMuted ? 0 : muted); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.hideNotificationCounters) { count = 0; muted = 0; @@ -457,7 +457,7 @@ void FiltersMenu::applyReorder( } // AyuGram hideAllChatsFolder - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto filters = &_session->session().data().chatsFilters(); const auto &list = filters->list(); diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 8552e9b56b..361951d477 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -639,7 +639,7 @@ void MainMenu::showFinished() { void MainMenu::setupMenu() { using namespace Settings; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); const auto controller = _controller; const auto addAction = [&]( @@ -706,7 +706,7 @@ void MainMenu::setupMenu() { controller->showPeerHistory(controller->session().user()); }); - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (settings.showLReadToggleInDrawer) { addAction( diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index bf8d90d619..6f9d6b7df5 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1794,7 +1794,7 @@ void SessionController::activateFirstChatsFilter() { } _filtersActivated = true; - const auto& settings = AyuSettings::getInstance(); + const auto &settings = AyuSettings::getInstance(); if (!settings.hideAllChatsFolder) { setActiveChatsFilter(session().data().chatsFilters().defaultId()); } 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 163/340] 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 164/340] 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 165/340] 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 166/340] 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 167/340] 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 168/340] 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 169/340] 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 170/340] 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 171/340] 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 172/340] 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 717d197998f5e9aa5bcd3b965d72d0488a4aae66 Mon Sep 17 00:00:00 2001 From: Neurotoxin001 <39812401+Neurotoxin001@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:36:25 +0300 Subject: [PATCH 173/340] fix: image order when downloading albums --- .../SourceFiles/menu/menu_item_download_files.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Telegram/SourceFiles/menu/menu_item_download_files.cpp b/Telegram/SourceFiles/menu/menu_item_download_files.cpp index dd510b5369..2f6be600bd 100644 --- a/Telegram/SourceFiles/menu/menu_item_download_files.cpp +++ b/Telegram/SourceFiles/menu/menu_item_download_files.cpp @@ -226,6 +226,12 @@ void AddDownloadFilesAction( return; } } + std::sort(docs.begin(), docs.end(), [](const auto &a, const auto &b) { + return a.second < b.second; + }); + std::sort(photos.begin(), photos.end(), [](const auto &a, const auto &b) { + return a.second < b.second; + }); const auto done = [weak = Ui::MakeWeak(list)] { if (const auto strong = weak.data()) { strong->cancelSelection(); @@ -249,6 +255,12 @@ void AddDownloadFilesAction( return; } } + std::sort(docs.begin(), docs.end(), [](const auto &a, const auto &b) { + return a.second < b.second; + }); + std::sort(photos.begin(), photos.end(), [](const auto &a, const auto &b) { + return a.second < b.second; + }); const auto done = [weak = Ui::MakeWeak(list)] { if (const auto strong = weak.data()) { strong->clearSelected(); From e27b1840c64cacaddf121e3f476110ba80e60780 Mon Sep 17 00:00:00 2001 From: AlexeyZavar <sltkval1@gmail.com> Date: Thu, 26 Jun 2025 18:57:24 +0300 Subject: [PATCH 174/340] chore: update README Co-authored-by: Max Balashov <rsg245@yandex.com> --- README-RU.md | 14 +++++++++++++- README.md | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README-RU.md b/README-RU.md index 5fd1220f2d..985b46ee3f 100644 --- a/README-RU.md +++ b/README-RU.md @@ -67,12 +67,24 @@ brew install --cask ayugram ### Arch Linux -Вы можете установить `ayugram-desktop` из [AUR](https://aur.archlinux.org/packages?O=0&K=ayugram). +#### Из исходников (рекомендованный способ) + +Установите `ayugram-desktop` из [AUR](https://aur.archlinux.org/packages/ayugram-desktop). + +#### Готовые бинарники + +Установите `ayugram-desktop-bin` из [AUR](https://aur.archlinux.org/packages/ayugram-desktop-bin). + +Примечание: данный пакет собирается не нами. ### NixOS Попробуйте [этот репозиторий](https://github.com/ayugram-port/ayugram-desktop). +### ALT Linux + +[Sisyphus](https://packages.altlinux.org/en/sisyphus/srpms/ayugram-desktop/) + ### Любой другой Линукс дистрибутив Следуйте [официальному руководству](https://github.com/AyuGram/AyuGramDesktop/blob/dev/docs/building-linux.md). diff --git a/README.md b/README.md index c3a1373d17..41b5d1aab5 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,24 @@ brew install --cask ayugram ### Arch Linux -You can install `ayugram-desktop` from [AUR](https://aur.archlinux.org/packages?O=0&K=ayugram). +#### From source (recommended) + +Install `ayugram-desktop` from [AUR](https://aur.archlinux.org/packages/ayugram-desktop). + +#### Prebuilt binaries + +Install `ayugram-desktop-bin` from [AUR](https://aur.archlinux.org/packages/ayugram-desktop-bin). + +Note: these binaries aren't officially maintained by us. ### NixOS See [this repository](https://github.com/ayugram-port/ayugram-desktop) for installation manual. +### ALT Linux + +[Sisyphus](https://packages.altlinux.org/en/sisyphus/srpms/ayugram-desktop/) + ### Any other Linux distro Follow the [official guide](https://github.com/AyuGram/AyuGramDesktop/blob/dev/docs/building-linux.md). From 2250fe75c4d6de08ad775c7026c639afe1455778 Mon Sep 17 00:00:00 2001 From: Neurotoxin001 <39812401+Neurotoxin001@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:08:41 +0300 Subject: [PATCH 175/340] fix: use custom serialization for settings The error occurred when adding a new fields, for example showForwards, to the AyuGramSettings class. The NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT macro was used for serialization, which works correctly only when the number of parameters is up to 64. After extending the structure, the macro failed to expand and the compiler started generating messages about undefined identifiers like NLOHMANN_JSON_TO. The fix was to replace the problematic macro with manual implementation of to_json and from_json functions. For each field, it is now explicitly specified how to serialize and deserialize it, which eliminates the limitation on the number of arguments. Additionally, auxiliary macros NLOHMANN_JSON_TO, NLOHMANN_JSON_FROM and NLOHMANN_JSON_FROM_WITH_DEFAULT have been declared in case they are absent in the header file used. More info: https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/#notes --- Telegram/SourceFiles/ayu/ayu_settings.h | 213 +++++++++++++++++------- 1 file changed, 151 insertions(+), 62 deletions(-) diff --git a/Telegram/SourceFiles/ayu/ayu_settings.h b/Telegram/SourceFiles/ayu/ayu_settings.h index 44939b05fb..017cf313a1 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.h +++ b/Telegram/SourceFiles/ayu/ayu_settings.h @@ -8,6 +8,33 @@ #include "ayu/libs/json.hpp" #include "ayu/libs/json_ext.hpp" + +// json.hpp in some build environments may not provide helper macros. +// To ensure successful compilation, define them here when missing. +#ifndef NLOHMANN_JSON_TO +#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; +#endif +#ifndef NLOHMANN_JSON_FROM +#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); +#endif +#ifndef NLOHMANN_JSON_FROM_WITH_DEFAULT +#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) \ + nlohmann_json_t.v1 = nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1); +#endif +#ifndef NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, \ + const Type& nlohmann_json_t) { \ + NLOHMANN_JSON_EXPAND( \ + NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) \ + } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, \ + Type& nlohmann_json_t) { \ + const Type nlohmann_json_default_obj{}; \ + NLOHMANN_JSON_EXPAND( \ + NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) \ + } +#endif #include "rpl/producer.h" namespace AyuSettings { @@ -178,68 +205,130 @@ void set_stickerConfirmation(bool val); void set_gifConfirmation(bool val); void set_voiceConfirmation(bool val); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( - AyuGramSettings, - sendReadMessages, - sendReadStories, - sendOnlinePackets, - sendUploadProgress, - sendOfflinePacketAfterOnline, - markReadAfterAction, - useScheduledMessages, - sendWithoutSound, - saveDeletedMessages, - saveMessagesHistory, - saveForBots, - hideFromBlocked, - disableAds, - disableStories, - disableCustomBackgrounds, - showOnlyAddedEmojisAndStickers, - collapseSimilarChannels, - hideSimilarChannels, - wideMultiplier, - spoofWebviewAsAndroid, - increaseWebviewHeight, - increaseWebviewWidth, - disableNotificationsDelay, - localPremium, - appIcon, - simpleQuotesAndReplies, - replaceBottomInfoWithIcons, - deletedMark, - editedMark, - recentStickersCount, - showReactionsPanelInContextMenu, - showViewsPanelInContextMenu, - showHideMessageInContextMenu, - showUserMessagesInContextMenu, - showMessageDetailsInContextMenu, - showAttachButtonInMessageField, - showCommandsButtonInMessageField, - showEmojiButtonInMessageField, - showMicrophoneButtonInMessageField, - showAutoDeleteButtonInMessageField, - showAttachPopup, - showEmojiPopup, - showLReadToggleInDrawer, - showSReadToggleInDrawer, - showGhostToggleInDrawer, - showStreamerToggleInDrawer, - showGhostToggleInTray, - showStreamerToggleInTray, - monoFont, - hideNotificationCounters, - hideNotificationBadge, - hideAllChatsFolder, - channelBottomButton, - showPeerId, - showMessageSeconds, - showMessageShot, - stickerConfirmation, - gifConfirmation, - voiceConfirmation -); +inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nlohmann_json_t) { + NLOHMANN_JSON_TO(sendReadMessages) + NLOHMANN_JSON_TO(sendReadStories) + NLOHMANN_JSON_TO(sendOnlinePackets) + NLOHMANN_JSON_TO(sendUploadProgress) + NLOHMANN_JSON_TO(sendOfflinePacketAfterOnline) + NLOHMANN_JSON_TO(markReadAfterAction) + NLOHMANN_JSON_TO(useScheduledMessages) + NLOHMANN_JSON_TO(sendWithoutSound) + NLOHMANN_JSON_TO(saveDeletedMessages) + NLOHMANN_JSON_TO(saveMessagesHistory) + NLOHMANN_JSON_TO(saveForBots) + NLOHMANN_JSON_TO(hideFromBlocked) + NLOHMANN_JSON_TO(disableAds) + NLOHMANN_JSON_TO(disableStories) + NLOHMANN_JSON_TO(disableCustomBackgrounds) + NLOHMANN_JSON_TO(showOnlyAddedEmojisAndStickers) + NLOHMANN_JSON_TO(collapseSimilarChannels) + NLOHMANN_JSON_TO(hideSimilarChannels) + NLOHMANN_JSON_TO(wideMultiplier) + NLOHMANN_JSON_TO(spoofWebviewAsAndroid) + NLOHMANN_JSON_TO(increaseWebviewHeight) + NLOHMANN_JSON_TO(increaseWebviewWidth) + NLOHMANN_JSON_TO(disableNotificationsDelay) + NLOHMANN_JSON_TO(localPremium) + NLOHMANN_JSON_TO(appIcon) + NLOHMANN_JSON_TO(simpleQuotesAndReplies) + NLOHMANN_JSON_TO(replaceBottomInfoWithIcons) + NLOHMANN_JSON_TO(deletedMark) + NLOHMANN_JSON_TO(editedMark) + NLOHMANN_JSON_TO(recentStickersCount) + NLOHMANN_JSON_TO(showReactionsPanelInContextMenu) + NLOHMANN_JSON_TO(showViewsPanelInContextMenu) + NLOHMANN_JSON_TO(showHideMessageInContextMenu) + NLOHMANN_JSON_TO(showUserMessagesInContextMenu) + NLOHMANN_JSON_TO(showMessageDetailsInContextMenu) + NLOHMANN_JSON_TO(showAttachButtonInMessageField) + NLOHMANN_JSON_TO(showCommandsButtonInMessageField) + NLOHMANN_JSON_TO(showEmojiButtonInMessageField) + NLOHMANN_JSON_TO(showMicrophoneButtonInMessageField) + NLOHMANN_JSON_TO(showAutoDeleteButtonInMessageField) + NLOHMANN_JSON_TO(showAttachPopup) + NLOHMANN_JSON_TO(showEmojiPopup) + NLOHMANN_JSON_TO(showLReadToggleInDrawer) + NLOHMANN_JSON_TO(showSReadToggleInDrawer) + NLOHMANN_JSON_TO(showGhostToggleInDrawer) + NLOHMANN_JSON_TO(showStreamerToggleInDrawer) + NLOHMANN_JSON_TO(showGhostToggleInTray) + NLOHMANN_JSON_TO(showStreamerToggleInTray) + NLOHMANN_JSON_TO(monoFont) + NLOHMANN_JSON_TO(hideNotificationCounters) + NLOHMANN_JSON_TO(hideNotificationBadge) + NLOHMANN_JSON_TO(hideAllChatsFolder) + NLOHMANN_JSON_TO(channelBottomButton) + NLOHMANN_JSON_TO(showPeerId) + NLOHMANN_JSON_TO(showMessageSeconds) + NLOHMANN_JSON_TO(showMessageShot) + NLOHMANN_JSON_TO(stickerConfirmation) + NLOHMANN_JSON_TO(gifConfirmation) + NLOHMANN_JSON_TO(voiceConfirmation) +} + +inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nlohmann_json_t) { + const AyuGramSettings nlohmann_json_default_obj{}; + NLOHMANN_JSON_FROM_WITH_DEFAULT(sendReadMessages) + NLOHMANN_JSON_FROM_WITH_DEFAULT(sendReadStories) + NLOHMANN_JSON_FROM_WITH_DEFAULT(sendOnlinePackets) + NLOHMANN_JSON_FROM_WITH_DEFAULT(sendUploadProgress) + NLOHMANN_JSON_FROM_WITH_DEFAULT(sendOfflinePacketAfterOnline) + NLOHMANN_JSON_FROM_WITH_DEFAULT(markReadAfterAction) + NLOHMANN_JSON_FROM_WITH_DEFAULT(useScheduledMessages) + NLOHMANN_JSON_FROM_WITH_DEFAULT(sendWithoutSound) + NLOHMANN_JSON_FROM_WITH_DEFAULT(saveDeletedMessages) + NLOHMANN_JSON_FROM_WITH_DEFAULT(saveMessagesHistory) + NLOHMANN_JSON_FROM_WITH_DEFAULT(saveForBots) + NLOHMANN_JSON_FROM_WITH_DEFAULT(hideFromBlocked) + NLOHMANN_JSON_FROM_WITH_DEFAULT(disableAds) + NLOHMANN_JSON_FROM_WITH_DEFAULT(disableStories) + NLOHMANN_JSON_FROM_WITH_DEFAULT(disableCustomBackgrounds) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showOnlyAddedEmojisAndStickers) + NLOHMANN_JSON_FROM_WITH_DEFAULT(collapseSimilarChannels) + NLOHMANN_JSON_FROM_WITH_DEFAULT(hideSimilarChannels) + NLOHMANN_JSON_FROM_WITH_DEFAULT(wideMultiplier) + NLOHMANN_JSON_FROM_WITH_DEFAULT(spoofWebviewAsAndroid) + NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewHeight) + NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewWidth) + NLOHMANN_JSON_FROM_WITH_DEFAULT(disableNotificationsDelay) + NLOHMANN_JSON_FROM_WITH_DEFAULT(localPremium) + NLOHMANN_JSON_FROM_WITH_DEFAULT(appIcon) + NLOHMANN_JSON_FROM_WITH_DEFAULT(simpleQuotesAndReplies) + NLOHMANN_JSON_FROM_WITH_DEFAULT(replaceBottomInfoWithIcons) + NLOHMANN_JSON_FROM_WITH_DEFAULT(deletedMark) + NLOHMANN_JSON_FROM_WITH_DEFAULT(editedMark) + NLOHMANN_JSON_FROM_WITH_DEFAULT(recentStickersCount) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showReactionsPanelInContextMenu) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showViewsPanelInContextMenu) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showHideMessageInContextMenu) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showUserMessagesInContextMenu) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageDetailsInContextMenu) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachButtonInMessageField) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showCommandsButtonInMessageField) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showEmojiButtonInMessageField) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showMicrophoneButtonInMessageField) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showAutoDeleteButtonInMessageField) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachPopup) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showEmojiPopup) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showLReadToggleInDrawer) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showSReadToggleInDrawer) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInDrawer) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInDrawer) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInTray) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInTray) + NLOHMANN_JSON_FROM_WITH_DEFAULT(monoFont) + NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationCounters) + NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationBadge) + NLOHMANN_JSON_FROM_WITH_DEFAULT(hideAllChatsFolder) + NLOHMANN_JSON_FROM_WITH_DEFAULT(channelBottomButton) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showPeerId) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageSeconds) + NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageShot) + NLOHMANN_JSON_FROM_WITH_DEFAULT(stickerConfirmation) + NLOHMANN_JSON_FROM_WITH_DEFAULT(gifConfirmation) + NLOHMANN_JSON_FROM_WITH_DEFAULT(voiceConfirmation) +} AyuGramSettings &getInstance(); 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 176/340] 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 177/340] 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 178/340] 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 179/340] 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 180/340] 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 181/340] 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 182/340] 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 183/340] 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 184/340] 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 185/340] 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 186/340] 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 187/340] 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 188/340] 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 189/340] 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 190/340] 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 191/340] 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 192/340] 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 193/340] 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 194/340] 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 195/340] 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 196/340] 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 197/340] 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 198/340] 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 199/340] 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 200/340] 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 201/340] 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 202/340] 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 203/340] 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 204/340] 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 205/340] 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 zcmV)CK*GNtiwFP!000021MI!+j$BuECHN`=e`hAn_ecHf>1p>2V9X3`|Cm8RFytz# zL>DCrB+G82p@*3_nn#+m*4q1KL}sLvWL`2u;Y4>?<Yi>!z4z?1&;D3@t^fJz<6l0! z`a`^W^>45K@T%U_oAm1H)%WjT{h@`cS8slP^@sJJck4fKtUvtwt3TA3u3r6M|NZI5 z^+W&lAO4?z{HOo&_kX{B?$3YzGk@T_4<BBCTwnXg|M}|8y7KBz?|%IGpTqjV+wcD4 z^?Ut;fA-B!f9CK1_#dzSQ0y!J@rUnze*Mkg<HO&?^<y92^CN#<Klwjj{fz(f9e?(( z>zm@JS2?$<@a-?Ztl#{O|L_4{px=Y<ef5|3>s#J@)E)h#U(A2_@WU(r-tY7~^ch_E zAM0Z`Bj>AE|6zYxzu?{WFR^v|m-VOhFYDXm`hpMs1%LcwST}Wb@Be<wFZq>!{kLEJ z*FU}b!^ii3d;QC|zg&IxeWrADpVy__lsfIV*x%Nlk1pF)zxz}FcJ;tF-9FRx2sd^8 z8~$S-vMbki>FyKaZs+mcwsF&Me@BfsGvXVLF56YV+f)B`^}shB-0dvuUt%iT&GN#v zUpczhb+<C!|LePtKhSD_Sa$aPcW*zu=5x^6KEC3Q9NF&`|KGR1X4}8S5Psp7J8hbO z<Vt_brL{jFEc?2WZ~ygMEqm8{3`eH3KG|BgPsTgn5x0NQpUuphz9?@i)V6QNN8bL4 z_xtu=`JW%(a%2C?k9;ug?~m`_{q^+&FX<!p>YYu#_W1Q|`0nqH{{frg7yk02cN)Ug z`XBz~{n5XK2j23Bzux=UhwtuP;=TXV`|p1G@cp~@Kfe0qTf6k%-oAbF^9MbTJ?8CW zeJeNG<wkS8f)(H+{3M?G$P=bxPx$uDPs@+(>;XT;CF*zFb7=RSm~We!)>0+y>5OHI zwomjr9<fVK8Gx<xIj+7At8!}XufFiR9{uXN!6o4j{XfNf&d>3lV_ffdRvL8*<EGl* z{xiJr^el#r19qLW9k#7MkMf`5b;n<O-KXM*aMN4pqvW1zD_7xWhB-o>BbLy=Ok{C7 zDrgTx7v<Api>$9H9$VycFSbaEEz+k16+gbZUEfnr{HZa}Q7m;3Qqy4Ynb$l1ve@qY zww&+$ws-f;>_`4;GqyKBeuItj;Zs7cVs0+g6FcV4zdrBI`S0)F{q5&pjlRMoqpy4% zef{14{TrcIisL5qewF6D2`Qg+E@@1CCT^d3_Cz(@%~Nk?$KkeqIE?kT5E}m$;??!K zMnAm%(D`q%KQf2C#)k*Kp-=mQWy$N}(3huvW&gDOOUirYxc$x}_I#5Po~o_=H`4lL zCF>#kdaUb~Zen84TE|UWDQCRNkA2Mb(G?%mxl%6Xhc&P7j92X@6uh{!9_woRSNq3! z(`Mc4Sz|eR*!2rXyNXc(%jh>VuXRi}r6;DZYmp0*+nY2qF3hoR=Qdehe_lU6r+&6% z+Q8(`)|)sRf3)jIds-e~y~|YZzK$5yBbH{VeOk-gZZa2u@8kBL?O)eV)p9cZZ@(8~ zeM|}9`?sH50+9bXJnH`L&D*!1;L3Bs+bCCY8DT7En?}oxr{8wtso!zq+B;jNF+RT1 z`1;R(QHXow<&U4O;im68{@UP9u2RX%o6OTuB|mY!-2RC1Jl+0@{qU&;%U|=0WnNw9 zX!X4W7nCwXdYpu7uPsm1u%p%oZwd+rX{_)yo70MjFqUs{7gG86K*x0O-G^DhlbmS( z%5Nu6_|EIJoN2u+E3PNIDFbh5{X34Pbkj24LCov*1-zu?IV)aLUME#sKe4_b`WLjk zzM<ZXqPKMPJ9c|h<7?YdUPsb;u+$Vf<54^B3~#H`E3cOkXE}IV@g~BbueTNTw)*nr z>F{mg!r<##0fCI__!sT=!sb_gVQK9r)GT6K`)V*If$<>DS@wVnc|GCsO1H0Too6W5 zrQpU@Z^{Y`w%1kT3Kw{;uQSA6;PG$S>usg4?WWeo>ezS;*7)c3vYNdFII8U}!9USY z1vjcJKV@%e?XxoZ*J^I+_Ph6bJ^5?Do|N?n<Awq9^0`;#CXc~T4j!u9#LYwH)OqB| zjYoewBU{AQ`8V@UN8h~L`-xxs{VcDWA|Im%d8ZXM^R-kqkizmR%LTw!Eeqm{%6Nf% zQF%7|OzPT8qw`Bvm>l@FW|*6IKlnX+y|3~N+pW~~WNWL9$VXeAcpyW*3DuoZj(F0w zyQ*H;N~V}q)SJlwAI@i%qASh6WxeEn(~9rB!(X%8>-yT}1zSO|lwcvur`GEkE9G0! z6I~WQ(Hq}Z4SHLB?Mqx)|K;1jM^el*uNaDbNr%C8WP^HP$G>Q|7nZ;F3tLXRO$I$O zFPwEu=;?NNY~d)32uJyL$~xO>W~@+hH%AZ0)a=JAlHe6!<g7P!^c(hiQT3~b=PNX7 zYYVN7BX@qivYHArLF$6n5aUfRiJNLFP)BIVpGb1>D;((DhWUmUuIq}X8eUedO)=H+ zZ@Krj@Z|II6O(f-4xDjE<k59qV3kl1xUS`al0bTVN#Otd+edWW+0p&g+HcFt0k7b& z=asPij`RDgFSg;XK6z_KRvgz3ti$TDC|dDK3{#0R+%KGS#rvld3gM<Q@on9Hs-ozL z9~$dJO0V0BDf01^;}68p{c8q3tKE#`aOk&1Imy_jT?-9=;I)Y(Vg2m-#VR8+J6*BI zx_7$8tJu5!rr4ssAn{>$s{&uZ>NmX@e~WRM!R)s9MMuzD{l2Y8W4sd(IU>MrD-#V| z|EcH(%UhY-uau=mUFckqhNCR8vwRb*B(N4VgeD76Bwq?bP`9@3>z^8aG9b?K<*zx* zY@=C8TSvyA4K#(MU%>{_1=zp}=uwGSu}Egq0Y;$wX&FIM42OJx=|EcV-4m$VxO8jB zvLb{x>^pv4RZf+sU1>u;VEt(pSwU*%2UZqe_*D}XBDaH-aSwI<W7bK(jAW?QvHnmM zL{C^yp&`uC-4I5UAq-vm3Wks`!Vs|PE1;~w?I0kMgdKzs%?@(qVX#8BDQB=eapmf* zt=sT}YmJNcK2kw7cS<%psDq2SR-u|1sA&bKGG>w$lwP*_V8nM<x_h^R5|i#~=FJL9 z&+G}hVJs`?sEw@bD#mo$3i{qwP%gj<uo>|xix=V6;n0F@yH4j0GiW8t3|2T!Ggzq% z6O&~IiA%a1L*jxCR0fJlS!M-A$X;}5htNhh&GMJ95=ZXxWe20_oG1wmP}zY;04w&G z6IQm8reJlTWd{>pqoHTG*+I*)18*09d2diJzz(v72HB#(Wd@O68h|`A0<B3{exGKs z{v{OV@rVNOvKb%_*#MRjp}U0%S@|16k8EMVU^(h~&Kt%cedT&(xB-Mlzk{%{xdG?| zit8VBLd>;_%Ov1jM)u{exa+n2Rgn3wu>9=;mOrh<1<E4%tEx>N6RWIXF?S*fkb<wM zQJKM9ubWPM{Tvhc<*p}JZlJcoSkX><*nmo5@a}^L#`=SfgSmc5Dl&qWt5-IFyQUG$ zvKc{SoDi3v@58NNpsUp2cbc0)!MTMB_$XiLH0T##1~}#$%^+|`At<o)u&=dBW(O-* zkR7PN2batbtrt5BE_I+svdn<t-1?bdR*<xN*YB9}73<(o%WI-9NGphhf6xk+K?FvI zYybCcmoI-0D?n5!BUm{lPMd}*>-MS4%GgRk;(Z&y_o^4UzIrbk7#Cmz0gjpTI*Gv> zUKIndmC<dUpmPHk^hy{OMq~<ACJ-3=_m!BHC=&?W(#u@r8W5vtSE5Q@Wdp*C0rVJY zDa!^*jNWD+gPs6g=q>+CiJpLQhZ{kmCs<}-MThmVfIe#i9IM^4f)$a}C>t4lH*nni z{aZl?tOjXf@brLtn|6RknLL4K3<Wq%zR<NJ1JCys3=r5?gvs-L02#>7tSxGG5Y$2_ z!+USl!qDr96I^D{ZONAzL`4NFAyj=B>1b#25fLdwH-m<h0TJo##|9!;U_ko)^nhMY zVM7aXw~EmP6b$<ifOGkJh1dx80Pws<NuanH<iZ1?TRo28NUkwLe%AUs)~Cu0vbmAv zV6uY6DW6@|xsA>b?7)zQbD7<wKUg8<uvXfzm;%Mil6-{|5i7B;yV(qZ{gveeV&6Vi z*^(%pIvyKBSHhbT(Uf}`!Ufb0*E{O)w`;3T^+g+eHX}rWU(6C3Pl2_N+O=4Yz%mib zOpFbd6GzhrK4$Wp5CB-_IM~6*Xlssi8q0rc56`gHI?52L6%=t0(ae^&3D%fi&cD+> z<1$M-1(qR<4nyz)HVq-8{*l&slkb;}06=W&S%$n@cn~k3fY_~wT(%Yh-l_`r(Trfy znHPmtFaoT-Vz|co0hWH64K(=`^gN4<psNwQvcSkf@G^p=;_z4t$qK?U0$~?u1c9+8 zq36Sm09WF?v5FB&S}=gI6{A~KReKpgi6;!8BEcYrr0*9W#tSGM_KN6g89?`PF&a8t z->@Dv$hXh6oJ<xgs>^Kp6ECiOjum8N8(B7xn5@UCUN&r?sj*W4KsQHFRM;C`K#JH` ztY5U7^Rf_2KyoxspO-Vi*RS7e3k)-WFGAtjVufWW1IYGe1(+DPX9d7kH6qedZeM7i z;P+_->-C}>Q<`~M6!0oCwTb3^F(Xi1P_S!h15pSFY_G{c4}~Y?e82#31QgKHDJ!Uj zo?vb9QM;_5sra`d1Ppr63VKcs9I5;mV%6N-3^HQ{qEE7$LBiHwcRXu$JE#m0*M0SJ z$_^^C#g(w^7$9Ci;jkx_T8hSDSir$J?{Wz#nh^*sg&q%=6Trj}t;|nvJkW$_GJ;B< zf{%D`SX326XENAvFoImc`bq|sE6Iq_Afbhez?xKT<_nUHH74Gz8$ljGv|*-ma|Vg{ z8^Cz;DI<vZ78vLXC-*Xf3n(9=UW46JkQ(YVhi?a0!Ac2)C|q_>*#knCN4;hw<!mF@ z<ph{4F5e!qc>|RZQne!UNY;C}&RIoX7=kSw4h*)aJ2q-_Lx`*#HDC+68$xDqm{6MD z41xVm+;ZK*2h0+}5NbYgD&T188Wkw}oeCFFKm>LJo3D5q0dX4IHnVvrIb&3hIl}s< zCVv3G9o27EqPF5ZHW}ADZ*(KcgS?TvHJDLatY!x4F06<m@>9zU#>^Y;a#)vQcx(`X zW#vSOU0Fte(n3$Sp8@Ps&S1ooHc;R`mPd&jO7ZD7Z~+CxhFU&sVCyBoJCnGiqK{}s z5ZTo&NVI7K>oq5}mpWa@#BgAj4JeHON>KU0+Np}-)AZDF*$0!!>yDlTe{RL8!?DVW zF&fYX)lGmu2;0kBlF&OG0gSh}5iq9VFv~n;1Q9-M89^QUA&oAeen?aXjerJFg$|p= zF=i`|V`R8IXfFl{4&Ay-X<~6F048(-02t$Y@;a)MHJ1cJ(ZVztKvaRZqf@N)_i?5i z4X@S?fCa&>!O6ERFq~NKzD+SL1E7HcUfb_O%_`CFEF6Zq+rV+UfWjfFLx|RZPga?r z8){=_1oK`lYGwvOt)zsWQc^8(aK3weJn~9s#UEw>q)(9<XqFG<yeY`ZN=gMbz^~&u zv;GilI|G%^co8Tj>nj69!f(S8&n-_Z@J7Afc9s!B$`g!{PZ>fP>NwW@rtH%N6b{!8 zVgyiv^^tL=W3496fA<Cq@zzJOp%8;g_^2Ue77~~1Xhf8*HR=2@1p3}!O3K|aUN-DF z$Z8595@4?i_Ha8{R|RK`<{;2fp#7v_My8t4@+|zh>NZeOqe`eO_ER=MczVJUmoLO7 z`1`bhq^dh&KMvWjuP|i<L0E!i1T5DeUo^A8F>m#QNhH#WA|pdS94L(Zb5s2$D)gff z1Tsv}2s8<YLQyhTU~L61H0ygrj>!nRfGBBhhXh(lu#2_G*@35Pm9vasoH`LOH^lxz zY5U<$FQ9DLkTow8FgOt^z?|&B<>}qTC<BO=G6I#7lroA;W1`@nEWl^ke*IK$QGKOp zMZM)ty2S*BKSUQ%-UA?(x(z5Z>uB9azgU{2?JOG@bAuMJGL624<rEEHhx`yVgi|I^ zY<>x7h`ns!0*Z#fjU!FZNbC5nux%0Kt!O`uwt)+<R5%rekJnA60tgpr1_=9y%plW! zfP*8+3c3}7K!{=?K~t0fgc&OWID+IX5dV*CRxr%-19nJHfOsKoC?j0z@D0(8fcYm( z<+LM+Ia+^Vw<1BhfRbTFyWk3ge_aEqDD9w@JElcoFoMc%E-Js4GOo})n}3+Y2U7$e zSq@%fyepk-hBc_>=nLHrte_94rET2zOcb`+Uya%i5A!1tu`nr2o7L_{kYT@YUTG^O z1b7S#)t)LGgVm<f3dY#a7hFKi5U4)nUu#|MT>&VJE5i||=;RL>L2^7g_iy$IBJ`fK z44@DqhzcZ|(tsIQs-FNekqKDn-cT*5Dl=Ln$BJO6vVm!vz5>9`@k`VV4?xF*T+7{f zEzo$xae&F;Hc#1rMx_Br+sy<npjx>6EaLuU0ZEABku}r<mz{mqIx=+2o~82=(ddAs z*KE@%LoLA-F>nT9<qdV39Yi&efG@8ygQ<lB7=$GNM56{DKs}Gg59bo>JV+W*=A5@7 zicru5atx&iLoEaHgkcvHCQq9|szRN%-C9WL0t$u|JOp@e=)o}t7&z9~T8##vp$CUL zl2`hdwoAkiz=H%IPRZ54LX|z=f=TIVU=!iZ*FnH|afo~jkkL5|ix8`2h+shz961P& z#;A@zxNg%!0E{ESSkS4osH5^|pb?CoPZ<HnXOKO#-Fisr0xE{6pkRg99vi})VnhHl zCKy>Wz##_6wFLGVTp>t2V;pJNXvz?-0CS620)QBaChCkoxhNQ*mlYU}9PsgEAF7J# zBf%ds1Fr+sV5~g22hR{tX<YXOi2K;gfGrIi9Q}UxpkQ)o@Wfb0*lts47f?4`?{^NF zL9!IVipQ81Xx>tu&DJ1Pw~Wa}N7qk37lvWZSMtY2r-j)@uT4E=JM#ys0X*MZP@Rts z!?0q2FR?n*5!2gr0)UO1L)z?sQ;cX!Tg4HafQTSavpfMjEoeJ+9T|1EE>gaLvSDG3 zE+;?rf%kxy#=8|Dxgg<hb}9>Sf=k;$t%HMOgTvNN!|TOH8v5J-f;Y*mz&GpkarUsy zssmUW%?cc6R)$^62Tg>c)&~YG&45r|#3l41PkRFfi0D)878&Gp5zZjNaWnkZnb5dY z@U+FrWd>OV=xMuO5@S!YVd6--BY1JH<w`x&eFWx+B^|ucVtzpI0gRh?U8xwMRz&I9 zvB);f3>YM@!2%46Jb^sG!PPu2EUbDI=Qy|<62z|6FZ<;OIbX(EkhJ`X)mJQk$LR}H z(9mKh)?1mSv%JqM$rE>#6OaN{8#bQAkFnO9Wda&!Oom!TTqx|=N>7G6fofn8OdP2I ziSd@?CJ^|eFab{wF!4we@Zt~)$FPc1fRdn)1rOTxGl5H}7$UvW<_JaMK-Y<i#7oct zf=#-W>q7BMJjHQI!^oHzj5cVj7GX8F3mf44K@~`tg#tB(YPr$a8VZ40?PW`?7u6gL zDpI(RW&nmIn~BV|NM*a{hgq{Kxf-*|y@Fvy;~I}TcWWW$dJ(AsC&#kmK3LPOs==7g zFIajj+_&u#o4w$&Q+)x&Kza+*02v=NtI~2EiY65|KtY>M?zsg6ju4C6&t-EmGJ|PH zfsHI-dWFUcFEh8n1LT&dUZi%aiO63D$U@pL7-$z!CLls&y&10W7^0v>pdOt*t<lY+ zgO~kE^DE*U!?X#{`sPDx99}jXD9j7yRx_?@2_S=P)?b(=Q-<MZgB*swIf5CjY+bx& z9Jk`OGZ$f!mQ9dT>_vD?D<JgU5GA=^#QO8d#<8BJSwI~s#|KV+N0$Aj`uI`$a-|uc z1zY<H2QV#QJ)c5{0$`Y&I=8>36q6`R`i;VYGCUlJ#>mRy_)raSSj*-HlyPMKK}KK{ zS~F}Z2VIaFqUj{Okc~(@Y~L?2yo72YNPv^2%nAzGld}dbm%JN57?2))2A8U%eAHD$ zKBrOD26Q;AVC4mZ4z4iM=2$RH=<qVG`T8bOkr8%c@DZ?~M8Oh5DqB#<YiLjlt&SVz zECO7D8B3><AdwNFrBjgel4S-Z?8ja-FQRuq#%&OhYs139;z;hC58|Q^FwYpoFWHq! zSC1=77ceF`%?t!EXD3)zATEJ*WT-R<l&J#zf})gT(5#gliXaUzgBr?hdT6kgVg&*m zTyPLG`5Zh-3KLKlOuC;a5MUV1%0wyeb|{o^5oR!xdM-g7!5M_vx@8!3oNrUtn3slK zvLRLDAtf%5X$HDK8c?hld)I(sjXr*2q<7gE4F{4>*#Ihv*?!JS#s$U2CVX46t~4;~ zxNm^qEVb+yg2)8QwQ+JFkx2%gbid9)i5JmHZrJ!}ziO#4D~zHHAi&Wx>`Vv=2EUn) z>5`2B$b%%19dv^U6J3O&VYB6|Dn29X%QlLp1umqDc3AR8Yc-lV5fkrav5>7rlqqeL z9c9g6pjc}valSGVIa^Q7C0F1~@vL*V^q^cqtq^B@;+tt|1WiMDcSoI9=Nbbg4Dlko z!~>ozG1+Gn*<C<T$nA=$kZjmffz>ei({1)dQ|+8ob!TAdJpd|JGHzQaSj2r@z>j2b zwW3iHpyMsB#OHCmU_2tmlQacE&)VaDbawe7utfn)fgFrI2KiUOtK^($_7t1rAc+~} zcB%U%A<;ol7&SMfVN%5SDkMXot7}S;cs7`UjUofEBx>Ac27(#Pt$oxtsv0n!Fn%b> zXywI?gaij{r`B1-JY>^6RXmj?bTz{8wEJ}qO1TI-;08s5Rl^3bgUJam%@-lX$}9l` z@=>=d#je<>D%wq1R-iV(+M22%f!KyK1j)!{U<J`M1aR<}6&UPbHmgzsd}wrL;7_8W z2LP@{ZiN9iv;#nL=_kSzzNlntSKMQ-X-PJZ=EYuCP%i>+nvp)C0%$fs;%LBug&0AC zCAESTS}S|xrD>Ox1!72y^mGj~3orr<q&F-6aB>Cm7|BM|C7H)=AM1fwQX?@biu0Yj z4mE`$gnO{YGK!3xphg-q{Sw^_=ICobMPilgA<J&1g3>Nz`HK*`b{GWBjC6qae6@u! zhv-n+u0*?{G9m`TMOmDj&S)Bu0qVazw6i1<NnIpDnanwHTmNaKR@QdRwcL7uAq!~X zcDuv4<}KUVuyC?$V1s)WKte;nO7=5>ei<fUn?ns8mg1twABG?Tgds-J#e}?FB6S5W z$x|#Fn4+(b2K*f6O<)7G6LxNj(U=Kff|*s8l)#AEjL@o>5%e%`gMluxl;}QxC~@T! z((sdE8s&zy9PS%|tiA6>SX0J@*nnP4%T^<h(kj_XyDj)+0XU*)TenNeE~%{u=q5`; zXd5w-BW0C=&4f5dm%kgYr8xBg{OwMQYtv<8qR=1^iVqvHI8|i^4k_x!my5~N+&x`j zq5-zG7EhZ&)YI;l8qA9@148oBwB8IVh69;4Izd(wXg4ek#kFDz0C9yJ6}Y4@F%63u z+Hxgy(9GbBn@k1ZZ2VzFuVouiRdE4EU~~oOGaC}gDT+`epbsg{CSo+f%4jlxR<(QK zAd+_lL@Vheu_fgZCQFff5nYGNm_gD}nTEFyDz>7?t8LBMwIz*Vt=lELl3eF1W}_Vo z3=<LlXQQvJ$s|NYJ~TGRzEX=cjN>ZHu@9SS06Nvi&QUTPW(AsmQNF<<CA6LURz?II zs!S__-!lS{;*)Y?HzSA_VFYvPNKgqLR**y$X|o6`w`Nrapn)uzH(oN{;cZ-fu>S`v zNi;q1&L1O(anxv9QD#s;dS@f5p*dH+>Oz3LN9!;<Y!J<ZZo}{rUZIB>qTd8NmIVeF zl<6dE4fT{2O!Iqjzru07gt2763#a7AiZzr6wt4gMVpdq_j!8Ja6<t`y#U;(3<X|O3 z&XG&$td!!I!=cGEGKmn?ZVc7^#yvqbQZr{!2I#PF!J&xsx~as|v?K(IZiCOfB3_gY z%oB8m4Io3!9nAfhN@~6cBVeGNFatCkR)BW}63Mx-pv(P7)1A>J)1$B$65Y8WBT#!6 z<pvufi8h>8T@9xh@!VD@0;_2=%{vu}2G?N26jTu{8zKcTHZdQEvIPn}$_lx=Z8q($ z5#%X*2c^+oMo=!o2v}i*Fy(Y~5%C|W`xzBJ4})Mr2h`4DhY%W#Ho6S1;%q#p?B-AC z6IX!_)h7TPWtad)aXw>kTo~=8DuVzf52rO4x9~D{FACHUhl5B6hQ#DkbYi<8wU&x+ zn<t6yW$tai;q+QB!UjkLgYZ5zLm9FX1p11;Mb#!o)PzXJ!9PtksVPf@D0MWtTQw7= zmlVl7nlzWzn*{~g2nnKa;AFtnrM{8kI=Wf00CN<~5IJ$-Fyd4wtXc4YIs+aDGpr~0 z%bHcf@U;6e-`92-L={NX(y%EWLP2o_x*;-z=CrgJGD>b6bNVGEiaDDQ7SqZs0uwRj zH!vb=G;K&ctK39&kW2)fW*}%l2mZv&z-P_?L*gkxhW-sw7K@p|a0IG}hF6G+=TlNB z!5m-SkJz;Ki!g(nwLw$>1ho{^C7{Wh%nGDe7P{4Sc!@O}Tv9&)1tE#3RY;yWi;2OI zrDX*%lx&&^Q(=HJk)Xn)Vle<Br8yO*tf1W}?u@1wEF++?Kt9n&lCoe708N#XdN?W{ z4`#r78Ns*+BdAsMZz7l=aHmOnPQ?NeKqT|MqPWDR=-zmk4GfkSD>4hCFv(J+@3w!z zh8xWWtjcpxGMd5_f~2&0tl$_kREm<UU>;Zj`<4OCK_j#(lzoh$ow5MuIh=L37Gj+j zVFBcsPMG5`10cf$;=kz2EwPJ${+wT7cL-O?rm8zk;j&9WAsBTC5#d}nh^_@!&{SO@ zX=~C1)cZv`;o;&mkU50IrwCMA*yJ<Cpd99gM#N|$lf(wy*Hc!&IyIhk+;1U#G%4uc zJsBCF(C11`xL7dZ73qm&W@Xk=DC8v-U$fIqWEVTGx5LKCM2n$7r`aOWNPyHtWDpxZ zD0{`fja$*=14U$0U-m0lNd92F1&`0VDfy1m4t+jAHSrjs^ba5J3#G4D2#CwFys*N` zGOvJAY^j%;2B^^=lu-ZoqibVbdHW|S>Aw9}{^w9!{U1NRfA`nd|Jmf#qsy!32jtcB z<K@-=w>N+J;qCh0#}E0g!20ncHr)Hq4=bZLbLa8M$!B-}M~dhLHI#mZG8pJn*N%Jj z@txK|e|fY0#r%k$42WzGfAE(RtC^q*bo8O$of7=xy_$Qe>q;(SWP&ORAs4vHH{A+_ z<V4LDh+a1;Yao-9oEB1vE6A=2pNgJV=FsDFQI%iZ3m9Mi1!SYGa$%B8GWx5Oo!T77 z&^j1!LGmd?w}L{wOMQ2}p=7wkKr-{lsrl!kss8kLU_+M0#(J5*tjHRQKTSdG3Wry^ zm77t?qm~AEHYWAiE+3Mk3&8B5>|%5N<o|dsw&qWN4^70fKzmMm4{as-*|QSjf?(t5 z6HNmk#=c%mGVbGOXjaG~Pfj>?be0NV({r&EfBK6+R>Mf!g<BwBzT#t`Zq*V+U1U8O z2n0<nVm)f+t4ZqO70ul>BLa;i@WRUlCFOGwwtmLmKySHFpeh**1ij*UCH*a%SX|Z) zu5}w!0IOBw1sVZp57k>*N$C)*y(bFmt#>w`jjiz0Uj|u!ih=*Dzl^$E1;)XLzl_$d z2-Bb2+bFn@_%;fhgapBa--lue-Ugro_4$bVKI3gbM>mpg#;U=58wGD;Zi<^=Si{>; zqlj-K@@*s|&}%_MHEJ^#5gTJHxjY|n*{8n@JQ2C4%X;>_y^P6czuhM@;X<^!OCZvY z7GVTpDaDPVWxWhkWGmk=NGP6*tKl==M%UXY9Bi_;fe9pV@vBV$>`A7V=xQ&l*RWO| zfA2<}qL+aI=4QNFd>NAy56?we@aZoDTyChZVUEux@e7G0!Re);9tZ*OU8Ixm0v%P9 zWFcTcCyeu`a&9N>ry7|1bG!;Pz1FJ$ZAvvcM%qQcXBUAT5YQ;_U1YZlj6e&uzMolq z@~GNpc?_PcnLOeaO_i0MM^cCyn6IyQ0kt?ycL3eD{tm^<Br~90;4GHIFim1?n*Gv{ z^5xSI2suVkb|=H6qV|OBDv2dmFj+LlP1Kv>Sk#1{qFa-@=w=pZV3PO%jm+|Fc%;wR zBVD}ix32;T=hZcWaXLnh{z7Vo&y8~v*%+hF&1p6>i0W^l?MV_}zkJ+TETu}_cL&On z=!+S7cb6t#FpmiOjH_U;p`fx{_`?D$7WoMLeZOr{zx>gGNSWDrHd|QsPOeh31+YX_ zz7*rEOb|qdtqd-IayHpy_JA+cZ0bpS_?&LanLpKID$(D+y<drrSNwku)tgoFJgh4J z-J7>>KcS|adZFf%6&&}iFK?B{gDM(Iq(89IdTa52H0&2F+x~oD^?yW~<Nx^WaY?F$ zZ|{5&psM_6yz?DYzp>wdKOaXMdX@)nDgM^&I0T^G)_ljE$Nlz8*4VEut|AORvvp!b zP@gg8AOf%=ST$x5N?NZhuNdDME<jxa(h0-LL@Oc0S9HsK)_1NTFG4A2w{Ug5yo>E! ze0iS@81am(agO3*H!-6o8iq4l$3F4o&HkE7WR7F>MdL;Xb~nf|%8F<>vMaq;wfM`& zXJGh8jk7shCucL2$r>aYuZs2KRz1H1XHX{H0*mHlGFoB!XN3KMZT9EV{{EcZVbGG` z(v~>UQbzg5#BnO7&KfR+;#Ja;RZ%wXB+Q;(f$eT6v?U8CIo6*`{rYqE1_*Z;&)DEJ zI2_iku*_vIf|c|MLJi|@K!F4WWlaDy<AF0~&%yU}4I#1gd+MZQu?k?4xf`YsvV0&I z(cn2S1E_gn3v5glov%t(1aufNOfI&Qvb|kk9ezhm1iXVF@_plOfwbhx><Yr!W||On z#X+PJ<|mN1ub)8;M7nd`BtfQSLF=H&RYE^`KSuxb<-H~au#71~V~HK}#mf@1%c8Xc z+Iw7ZdJA-88{UT6O(3(u5wgPlOxFu{Vo0Jg=I^Mh0#UfEN^jD>!0=_DDNHeRG}L|) z-iDYWS>$OIpJ=W*Lb_U3gN7x!Hh@*)v+2Qo)<)s{Ai9Whm)Iz?;Mt1~5==_vgTF(( zuVif;Bil#0nG=Xf`-bXn$gOtlJbwA31cS6ix)et7jBPR^p4}wFS|wdM_Khi&WAL0l zR<y;SZJN>w>uNYv%=44EKK`EiONpT~S#I2acKHVp<F(Eq=HkK(3@@~aDt!z_@@F>1 zgQD>`BGxiEx1LQL>9ZP-dKD0*vXPCdrlJ7&gm}|RYH*{do!+9;nPS|asy`JNf<Wyw z786D*zoSmu{ddt6e|HC`V}K&^6z#XodASSq^BYAxB?2_{E;yl&cai3^>EwJ?3pO&w zK)fi|DFJCDqy}9;?~p-9(_Ckkqzo<;^;2Tgvj=uy(2})EddTD1#9}_<Wq`;=^1tHV zLb{$p0z9S5GHCR)136a)<cu&h{@yt0%5u5<1^U-&LZ;H5O*7^5+R-2;L&s2kHLB39 zT>_PmY)wcojIDK7CyuRI+J%0i6>IMU(o)r)jx-D~Ta;&05cw>_$dtjt@O|-G#d<d2 zvkjmw`aTE&Y03#Dp2=KR58s*;z}RX_dK&>8TS#qo^cnSQ_|JNDtb&DT8U2c?HEV)h z(w1c)E|dQRG-dhEoJ}*kDKKZRmKRXevPR^dFXJn;B7)d(85}vm<~lL2rVZdyadjmf z;PW@NPEOw<E7iLQW*E&b<Y~$ag`mjiV$l68s8>w+P-8ZOvT(TwS1JX2=}I}U`jWcD zsT%DZhLjOsMDZ7a@j%7(d%lSLHJZ_!vBbGynoNK=E$3-Ue`quzk$1X`#Azu^))plr ziZ1Mg1qjB(W$LWYMx6LrC>}S6m{h3@lOq{i|8<f8C@fMk<nHOpV~Cn;Q-BIU=V<Cm z(Do~Om-aQ@02vvQf@C6l0g*)!L%sMomT$13_)aNj_ldwO!3@%EEJ2N-1lp}<>b!oX z=Q-P+tN7cmsN0d-m+QHP`z_aX^(l?m(@oU9nz3FQep)luH{@+Iw)B%P!YzIG9&P&V zIVk>a&Owz2=Ag>s=b--K_4n_;`_soa?|v%3J_nV@FZZpn5;DD_&_MFHqmiT6-*4ka z_1-VbfA~f_>l@52+=Oq}FaK$M`*NN?{<J>%U;fws<-dLN_jrVF{_DG+KE8j4z4qpR z@a|s!r2pK0fArnNpLq4)cU*@M;_Vmx+wl(l#r)V~Za6;rhSPc6aC-C&=kvJX{OB7l z=W)a3(KlSr<A&>_Z@8Vu4Yx<%a6gY5?vK9VaUM539(}{}JZ^YC{@@5_a&mmaS$8gn z-Q!QYcqYePeEfMA&*Z?1k3aF^7e4a7{oM?VpQob6z<3;HaJxuty7Y3<*Y-hcaNB{y z!Z?W`Z<5&nE;rM7Y(cS#5=1QyPa^q!ljcRO8Zv<J2C0pRV~fU|J}AjuhLd4~YEc?~ zn<94=B>DCVbsNj!l~Hd(aZXLaonm3hjK5BL&|qu<CC*$=w;!i|ya+=`XjpFR_h>^% zny!qt#LdPOH;bYnBoJNBzNuy<$fC`|m4S+8<*FA=W@?I!LkLu8(9C0DiIi6YzYBt2 zB4do8!4YF~CJ`6L;#?h8#BQ6}fxD@L0A`ZPf46?@MJR4j!w6cX^Aqd<P(<RS;;kSq zma{g8nLrO`;#1SP^rXy0#(cJyvyteF9<i{eZoM-T?@CUunM~aQc0lSY&}32E0W?iq zV+j3Hsij~TvbjvKMBOpbXLgXL?y2Yfx@;Grzc+y`gmSUQL>_4fQANsX{hQXM$Bv2A zR^1L7Rv2t)QJ+dllZyByO^Xg5eF-d1Zc$Ay_GYWzWG!HBEn0PYgKYfXV80_-E7+eo zy;7QKZomo()xgoVZ%*nsVHi;L+7AL+F2W3ejD?h%7;+zJ1ti3zJZE<^7;yNQkh7Bl zKq5N)0Nk#Al3md(f!!oZJBaa-8Wr=}77bvA$Lv6T!(d`v+aUnny<tTDicugBqWGYi z6M-Q#3QGf^WT#$UzirYGa@Z|Es28Eh+Oh5x<cXhhFa&ln>di)AN8~l)4gZAVzYsTy z{6zU*lUY_wI4YU^NsKxi<sV3&-Lkd8z@6_}KI{wvT2o{L>Y-7lCrX$}b^;riByWWa zFcZ(>ome&y_UoUui?9LiYw+6|8SZ0j00fproh*fVlt~8(2j~_pfdK57o;xH@4Jh5p ztxsHt6cyGgF<_t%$VLvs)B}bfHvb~d4;X^{PjxC~jP}!dCtAu)mY{<IS}>w&hQwP> zPWp_fctx&b>=qsLi_mK)jIFw^1w(72p^wr#J!uK#MdrLha>9Ems$*osDbb;z-DL@` zs2iv6(s?sXR6*)mAj#F&lfGlh4l=ztCs+?dLTIN{6Zs+e)XXGE;bA062be)o(wIn? zX+a_s2UjKwF2-k?s5?h90HI~P5~D`9{2EYk%m^3~?xYzs3OE8u+T9GOg$~X^TXJgY zR#9{gGmQxBwTW(LS=j*1@suG{b3tn@yQzEfia}+>={f|@O+5Ii8-&}O)x-b^;jLx} zU0GwYgcMIl?ED0qP<pO$!gDl)B=syX;LoEyLpF_vPX^}qgub~QB<yqYbZ>T`D6x1f z5aR${k<4H;0de6VqR}Pmlz{=s={v_OMw%=@D7*#4kJFWYxWp({DMG+8RLl;hl130| z`LRbj1W@npI8Av81;r?;bf_FD=f-J@-3LMT+#fUq9w)b61C{`e0hISlJ{yb8fG?09 z&MhITSP)It1u7ej22(NZ<GADL9$}~s7>EU+DM+vjy5QMNA@V*!=+1)x-z`h%9+yT7 zcNn1}0GcS5>}Ls=P*Ge#H(8TFure%~f`~TswfjRy1F|)Zm?bYDa2Su4V7N$psxnIq z>#SB@v6hDV3J5ln%WPzt1~`wZL9WHI4e@UUc#vi~tQIs^j6`9aAAwXSnWFJa<=Uc6 z{6rGd@q3(R4$?3=KHGTp8Ja<v3ZYSivkpU)_+kR@$b8TaI-N#$x>t(rpy{v{ue(r! zlctA$W2eP()>2YKKA}PbhGJPrs>x{sfgYyJ4~HP5jYf;PAEFDmD^U11Q+}$V2uGoU z$3><>oY_KaRkq;YL?yo~bT;~q<HQ$GQ-sh}rYQugwgCGmmk=zgKV=Hs?BK5t%}t?U zx3$7*9TCIpQa5RYWGN-4yct1a0%iE5cvCSc-`ET={Jxn&Hg!;rNJ|fvDJG)R;38nU z^AOECvIO{K3i>%t71C2+>IVnL45a3NF1CSlGzBn7MQUHGN0Wm>v1JOjZ%>*6(~Wei zng@yy1O*sbwOTT#jG{CWf;kUV`r1*R2ObH93E2u-M36JPfL*B+)LkMzN`w&-<J5{G z>4b{}sP>4dFiRnak|0=u;u+SwacPRQ__^2?&e0M8a3-OkZ5o`a6gbVM<$c-`==l?+ z(%ceQ6mHGA4Y49>A%wZrna~Sl?67{J#tC{f&9`L<LoG|p@~Yuf{DfcIoP~m|p4=0s zbfgO>OIT+JLxru00ZUMK5{g_(C(sthfe1Cp{f4FE1(X*tE{Jzrvdi&dpn-zNE17&K ziNsl)rF~aSO*L@1AC@rNW(lSSAFS|8z1M;a6~9;IA*ir)Rh5cC^r@|>o>MUaw-l_S z1kEU}DJW^WX~QLx4gt1b9p_?^!jL?9q#O?~t)~G0#PtAwWPUat|8q12hyYBQLa2T| z1QwQx)$&i9f~z35<faheT^eMwlAA)Hmj_4qG*f`ZMfHZPMveT_rF~3l4}6BfOkwa> zt`N3Ht0y)td@tfljpQR%)QDiSsp+hQl;ZPDKU25#lcHbvD##PoXHzsdM^jkucxdp` zl-fCP%*s-z4;n%-wZG9fr$8y_;ir8RVH6A@i9-u!khuM^F^jAD1l5(OGC(t^g0sL3 zl#!-#+g>R2IK63jS0aI2bL1$jJ(Ck)dc?@^A~maxATFv%K$jUnhWNR-h|bXtqTb*7 zZ?)tdMLUgGmHx0LL@X9*THFvaCW*1ZtrJb|*D3L5J3~+z=u@x(HI;CjbVZESd(#M3 zvqg-P%ZyR4t<%I0bui*inL<z*nfj9PsKtOhlJcU<6!de<mf$2tBO%Xo30s__DInLz zKy+}uKJ_0pX;&U7GI}(K^sF663OFY8nPv?J)B~)d0NJ-LOE7)s22x-X{;R4*q-cOP zP!w`WG3_2LOo+UvKIFLYV@<(eK;tB<qNtmKB@~vjFzKf#5=baK-KZ2ocrH?|GqeP< zG!0sURqN^U%VCf4q$O0+dSUW-t0=&tdN67Vp4NqlLM&|l(iVbgH)2*-wdJ+*gQ`n) z`_XA27O+#2WNoU<K{;6($5{7U-Ipl9!FPLAG5PVp778pus-r+Zz?VR7en+jF3n(*! zf{K&~(`xle+-|S~JIYVm0bej!I^uBys>iLzKn8~^W{!qO<hE61sk<~|OBCy&n~xrS zTGP5!(~d`x!wo^6YeXseGQO~*AX=>g)dNGU^&8fo5d6wo2*Vk)g!qgKH|J;xZ7owL zIfm8iA%pBa>Gn|S0MIxs!OIB)y1rLn__0niI$10VMCYE6EHncE5~vrWLt<}oi~Ozu zEK&Pu{XSShGC5&@9(Y4QGe}K%<)l_kTpyzGK<?2DqnS)B;!T}<=xrdsf!5@BJ{G!j zG=r+j3kWZjhe7i>mBXIGNi&c@PTZCl83XJ9T$@%*NLC%fwlrZ@ME9&eBWkk@5;dxH zm<mFZDX9a62*Ma9liNb&cLCvKGNhIzKx}j<G)z`YaA-oxZsaIq;^|;Y&&a`dj+Ri= zwFSgnZxpSBsxG5Rf1I=gDGZ@Xut67L8y!t#D>X-p_CYu;JAiy&v2m7Z(=37^KnK{W z)@fN_k{QTYK!v7wV4Xi)Pyil-iN^Nm<1AT3{KcA+1S_ei1P~-IRSF{l7qcH?EnPr$ zaqU_VdP-oini;SFxEw>84_W~<C`rw8JJ>)ZGf9W-Q^9J4DEFE_sMdr4G)jjK#X+VF z!MfJLpbD@9k@(=B<{=6J@Mqx?pmmv#rWlxh`q4m;1%dRcH-Kjfzit`Ajwsf20mVgZ zX}lSrx%938Hy9!`eXz{D;z(mDs^w_1p;s?Xvq;I49_<3_NQYv*=7TwgCb$bMVdwx^ z(S}-2)rbRGxR~lWO4b_#5$Zw$39&&|RX|E6Hhypka?xf86JVtg_;k-j!+DN|fVf|z zoXI_cIQO=M;G`k2ek6ag%{t+PA^o<_0dTklvxH%Ag^+cmAdM5in9^jbDGdwhuF<rx zU{N>+R5?^q9(M4Isrm<Dce5XX)WFsp)7?v!V=lu>ywS<D<+)f$&(IXSfRu}AO(NQ& zQzu+yJZKBSG#SnBqezJ519`6YKe7)Ni-2agGbE&Knn9ulh~9$Xj%?sn9-;xm9L1$u z6c-N50cQ|H6s*8N*a$-uAq0xy+9kFqwqFk`==j0DJj;F@@A(1>i)hW^Wg%p=GQx`8 z(eU5g{s|+fLZgD-)?)<7W-XVC+OOr42sXmeZsf>IiUwNE$p!jiKice2_YH0e{IXJH zt8Oea3T1+?>DFo#I0eq9J0yXL;;lcz;lmNBK3L&?F6P;DG=nCv28O}h7gR<Jm|V;c zngQ#D$moh&0ig!#1v{8AH8H|%r!%MDq6$Zly>2$&Hp)1R0<S6HAR}N}QBZA^31k~& z2wdOIln9n5G;Iq;jxO0CG<YNtRlrpDLoViY5zc^081CE;FO>3dn8<-tjFwv22)RDe z47gj*{~EPv56obWn{f<UE>j38ri3M%a?Ln86V?qmT2({Vm}iMLENMXe6znKjE<m?^ zbV-kfKy>)2^;}fL=V=2>@N@RoJ+2s;d3p%4xYd<cFmIOt8DQ~X8j3{aoNPug6gE!q z%vYH~+osz_H(Ywwlw<Isvc-qPib1s-@ixX1)O`+yio=pm8V}-un(tkr5f}%Ag|T~? z!37i(fh{WX{}yT8yYes`1qnmktbhQ65pJ0<kgv64D`*Bo!0f$oM6d7A5$|O}7NHJH z2Vltd@<OI%&T9QeV6as}hIAtf&0Fd6;L-|s)t^R)*>5x3=OWua&oqIV-j@9FN>;28 z;&^bpB?RO64~AyXD*a%5%`V+qLh_%)M%J2U=aI+c^0e4M+TPW`XIo7fy`f|k$H2Rs zO_0i4ZWa8Js<u|!e8j;eRlo5|La}1kehFi47hwsli*{O38?QuFh{l6+4N3hX^cj4v zVVM#_yAi!SL`xv}1S1mq*gDv&qJix7gpxh~n;bNtDsS(H{YF%Zsx>9`ML1!sO<}Wa zC?bZV{w6MELRN4|l_m_xA=I$q_j75WpJx$?7*+C*JAgT{^^|TNns>unRF5)yztQ-f z(Z88@tN?25qJU4T-=N5EMi9lRbK6lyuz!F(Sf9*F7E!mNGZ-Ahf(axO04`Q*+@KD} zmlXNnH=qt>ov^)3;4<on!-S7&@nMEMjf4K7;nzwrMj#fvv53|&&WVwYjzmG?um(>T zTx|xMW?V~a;bjTdh6cdI22^~&(UEB&@KCbgIJCJA1AYbq-iEDQpPq;KrxRBi#7^SY z8GkPIiSukMaI%eqj`48TEhk^m6Nb<f3P4E53(6R87tSa4aVKRgKIuB9Va~wIp`!zM zsB%bf0BozcFdVZCjJnc=Y6I0QK@6kLi?;v4xR(zgkw;gg2~X5BY{S4Zd$A^ki)c0y zcMj@+<WwRm6(>jqJzxn#MI3BrgQg<uOR|KSw&H=nM*u+U^ccfxglPJ8mpm=a;D;Jf zHCl^pjsa1=23V>OE`>;T#9OE%D_>I>Y2#44e+9!)C^rH;x1-)fya+QO!4wzY&7hTb z*ib%U22<5zDVfL&hKMr_0cvJYInqK!h-8#$8g016B4!M~=0iL(8idoQ+-PO+6^t9# zk3}<s$*sPDpE;;N7ZF{s>eMA1U@UcoJ<;ps67Y$}f{>#p*(@OCgDWR20fk`feh}wa z9T@H#r@x`X%@P^|0-O=bJYXl&0)Jyp!k{ac3quO(5Dt+yarBiQ9qXrjp?GT?<BhW& z&O}hogK<NQhOs?$2~=#THDi8%ztpkhi?9Xa_`oMpoN<h7w%e6cwvf~;!aR&?V`0E~ zY{MdE3ql*AeW`NNDBfjij%h_)>Rv$uD{)#C2qmg{M2)gR5<?N08}2Ga$K40{fM7Ei zr!Ik<5t*?K-|S@y<sz_Y1p5R2pPiUia<YEqlqqDsk86rK+oh@y4j_@TnZoD>AXV&% z+_E155;5o+ZCpij(J87QbW4o@@8A$+akO%}QT3!k+7m7N%r<MQ0lia~U<t?_FpsyR zQ@9M2;<V$yYHtTu@<WwHfJ8for=2$ki-ZR97#n~cu7EWHKoL|n7+_dTN1#LuP6P`W zHDHA@P%MXhDd;&ISD`rFSZto!Mj1W(1v+|I9&~WY5>znvc1`J`fc9J}WoK>*##(;Y zI8K>DRq%%`X}tyb38KU2hGE~F!f?e!V1Y_HgeAgWcx9VvMW)flA%RlsMsa}3wAsNJ zB3KyHQs2a0bLa+OJ9P<MorpNlD{uP|@Aiwbf@mYqcdwkbg2{-nx->Q6L8My_TcPPT zN<axH8f{jr)-<p}6INX_ZGo&tSAV4Sm`70Zha+9N!v;zW$UKWo!2(T3T;ktCdgv0O zPlOyOU+*{OR>noyf+R6{#Y`bQlssTZHqz}FH!Hv)<jkF6gRtKw57YfdZ=!%FtUwln z5_CVLJivHWGD<b^c7T7Qjw4WF06UwJG5J9NxrQO?u3+cf!6ntXNy3D02PxZrb8cl` z#8?Znca@wE88-n!e`xd-dm5W<nZoouGKiml#tp{4RTR*`(8qeUo+NNaG9u#F%odcC zq61=wLuSa)qxT$e#wRlcIu10zjN2n=s1?Da%|#zxvQdp@(0XJ2+KZ1nT+o11*l23+ zUO8n6L#(P8T(s#l;ztU(>#`ASZ7{Ws;SOD&I@%%r6mzb5TCtgDK6dgjCT1NgJ$ch1 zdUq_KqG^<kHpQaexi~&uY~bKhJghYv4d7me5HG?Ii1s9J(lA3H*b`SCVo*ZU7~y^a z7lF~~p?}=SWf|&m(;=JKGMYZ3+}p+yFy^`ymPCyND)nqk%u$``;!OvqYbGa?D7l=v zGYVKI%HNRty-V&9aKVT{_p*a@5q8jJ2F|x>Y{fY^P2PPoz_!BXFQBm--j52XDjB8e zQ5O<Xo6^M|jId&xSi2^Gz%>_m2>`fh0zQ*|q*ozn4*-f7d@t>g&3CJikqq8!`tZ_l zmk@Ca;Beiq{Z#Wsn8LKd_N-ll0mESu;q4(?h{Bj)P{{ymNG20`j-9D{LnWZ6p>+p` zdpB&FPsDjy$$0a&xwcKQqjyc?-VVc{c1-93X#UiNbafs!rW6_;UFzu&n1I5?ev%#K zB1Yk3)l_@3pvzeLw=1X2ASr(Xfv=^ICF>jjOZqL6J#?eVZ#KEq#n8$HvEU+lQOUV9 zA>~j_+)eabEAuRzblnEzU<uvXjNN09Cc)b-__l4^wr$(y)0nnBPusRRZQIj5ZFAbj zv~8dM?{37|_r$w<&eyD}tc;4fGb%FbcO_@poJ_U}cGll5`L3U^R6ONWERX6Bl@DYJ zfs2_%s_g0{IRx7Oz~{*Jc<k&EFm<SJ`!KjT-uSF4Wy>kD{YP+<9J*$f7v$yUQuZPu zs~f>z(u8WHU^=r8C}1*&!Ss+H&;&L?nO&#K>I!U>5ZMh@r{LBsoy-%!O!=na-$Q6d zo3y^_F;QHi>>M1VOoDLmvwH9#Xs!5HWM}l_t><dPwNrAO@KXb=)s#B$$)ifMX|ETi z5C&t|5y*Omx)Y9RU+ytSYRTL;>-r4dBr`<Cc_S5kYPT8<y{%VJ4a!#r3g$fQq1uAM zEk|rm8Lcf&P^^Wrn>FKWvs06c5?=#@nlf|B^z`s4<#4fFz`P_J1XcoLugPZ%{fyaD z)-O^oDHdeBZ|rj1)|iEGuuY-QFW@`uJVe;`FUD#4b$*sKlBbyytoOG|Hen~*ZAv`W z(*B-D3JyHmYoSOtrI9GkuBD$zQe6^jCGWPqkK-COtbUvWn)30aIn<fJ)n$EKxWMJL znq`$E$$G0$y0k$T`b&Uz%=EtyQV!L8P%}SxJ3x_jV<iBF68EEGC>ey{(7XM7c}!lG zX$lM!wQLK^@UmP_Wsx>)2%4plN_<pHoCDq{%%q^?O`F9|_?M4>a!NYs5*Cx0sye$? zwD??>v3bY!jXPb>-+jtle3qW-iyw5;&g~YiHxKNDbvv4c)+`BZj@}R4zl<8T3T`<J z<CNJLbXpV>vt|NUhfkHoLbr7l-c#MqpIQ4zUUO@DmNeA%(O7=EKb(dO|9%~K+V1(d z&RQn|+r5z_d2dEVjDbXkQssIHlSN5;8|YpYb0Me0n{#KgHD4`TS9bQl7ANN?XD09$ z>UDVsb%VP2cCBaSFI)<Jq;byl(58qbfaUttY^09^3c?fu-6y5so~WL6$E3L^WVo;Q zBDmg~fQ9IrHfv2|#(+#C-P84`@@pKLX@5yiHJz`*X}#rJEq*07U&XVaAQ%NscFDoO z?cyez8bl`eW21ydCUR+O2VVSSy|JFR+hk5yz>ip^`u=1T7s#|4KT7;|VB@U_V}p@w zj9@0}B9A3Rg+d)YE?fb?nZVq{-kc)~ES+onMm1c%QE0bwULqu@V^r4y5`T=W;Ow1F z1m2PJuxYL-y&JVTb_3DVf<iIkGw2>rCXdP;J6(Fv8D!j#aw~}=rZKA#ydEeuN7{^5 zv>JHNBm!U!H-!{;kF$%kvqU$?k@;lQ8U;YRE6iX{BFMQ;=a9||#F6>fsTG56rFVJK z)39b9OK(*d6zD{Dnrx2pc2>mT?0@;wAN%U}LoTL-PbHc>Gw=t?z_(ubJZwxz$%9}y z<(M!Ifeag{_4&0IfU6lQ{N>D!x@n_DMnnyxW4Go)MJGXA{pPBY#8rRo&@x-NcVBpm zNlr-J+Q1(r@l5L+pcyBS6r6AAy@KIsXcjDywLm);U}cR3BT`|c3OGf}W<^N~Gb}tQ zWs$%KN%12rIB5p{gxK9@ZtAf0c^h_YxHito%7(K!U>;Q|b`1Z6DnO@kaF<`KSY)Xt zf4gmZ$>72)|MscaDzZjroDKFW2<OvuQt`5ot@^2pfY{u;niFGrzH{(aQoU)O@+WyA z@Z87xzyiWZQEnm}95=h+^kf|(5n{e&MOv=x$8m;i)Iqy>Bq2FjPDfD~ze%J(x(UKn z`V`|`j&`6Nn+`w~DEh<1g1OOh!2*FYld$|nKPQKfiC4eFB#q3p#S)nfJj}H~o}E5{ zXrB|cR9q`Tj-xbX7^Xy8#lC|>2YxtM?X7j<Z@ko@3Vj@{R}L3Q&94jfgG)t`h3z)b zR&h#m<F{y-=Nd$aicW1h=eLk{6+iPkh+KRf1IcN}yed%$oHx%HDXC16oivZuMp1$; z*5Ze6hDtes4nn_T0Zuurh(6hG>+JB7AKA@v=|o=e4rP|qtNJ9Xi^LP`F#|Vi64#hG zcNY!z41he|D(ZxZjM{N>c<>9p!%*>^SZxTVJ;2;mvEkUr?g2WX|3G-g6Ao^`TosJ~ z>je+6qYnC`!d9Y@DVm>+W}%@0;?WWtTO$t}N9g<?d0LpLvntG`;86Yeb|T8T^#vi; z_e=&7U;+(JC=nFVc_V3&mI=E{?*O%zPP=*PY?j4J^ghYwI`aD6As4rqA0~{@LDK|Z z+j<IN48>!$bqJ=Q{aQl87yWR&>iE*I-1J`l1OTSwHYChqTS!U;p3AR?7#C<49SF{o zy%zoX6iCXDKO>7#KN-aYCe#K0C*2LsBBDiGG3&>2QYHgm7cFB%F)d3$9FH1YPsKMj zjUlPg*>)<}#Ezx@buDm`;SoE1v}FUZ7H!${zfgLEz%R-LOTOLCQI9ba4*PRnGMM7* zLWB<nRkO=NR#?Rhx6>A8B6-l*f^w~;SsxF8Agg48tI4TdYSFCf6GgKbYR`Iy^HPY4 zDX{VeS4g7xw(ZVdCJ+tdFaSdQik~bwr&^3UkK{%gRXB=M!{T3cj?!RNYa*S4=up5V zbRszW#80v}e-jYQIZP|0YJ-?Cq-E?$cF|tKkdt9{m0upIL4(*bI)adhMPFA7uv2BQ z%xVu7IGAcM|6n-$!!nO22SJl&O0S^r*PS5787F1U8~d?w03gO8f0~BilnydM>}#KD zK-_d;5Lm<(j4*y9SA&2!#1`YG)Fn<N<sOzWB5o?F9}pJf@mt1ft)dq)!6}cwq^%cR zRL@8}l0!YWH8yM`fXUDHtKB$i>`NJQ&)*HN8W8?ub9H4O9fIMNpLT$%kYjhjn*-~! zmy8N-N>}$#$z&8`d6&F;n?*{JV_Z3M+Yc;(7#&gPAU`e<k&Yk1kw$se&&=QQDuA7{ zfJhaBB)-|HFQ<!`wy57X32AmVz2*UGJ-PX}CECmM-<;7Xb1th_{EY=h^__)Tt>lQ} zQE}09wf}%X-39}{IFUSk>w_s;%pncSbIX#ZH7$_H?pzJrDL%d}Uydp1#%FghZ{&fw zOC~Lk0ajRIWu}vsKZy`|reb6>6YhqphpE_~B36I(kEC_$!qBx@=<%DgLp%yQPq@Pq z%RI!Pj{EIt^v%uNz|=Hj7hdU$7hD6VQX`Ep_c)sE0e4Dl_1oj9ED|ZP(MrjcDe@$8 zC`r&d({X=*2ilkiu?IDxC^iWa#DId!KO|_X02aFySPjKuq9np&Ka{7u!d(*x8peCK zx6JB7AB3@yWG)fG(v)LwBaB{0dytlQ1pv9P_Vu);IMgt2sG3r<>TQle<^YEVpCQaM z;tKG1b3P(uOH57DnI;N)H&AWl(j1vk5cYE;N<EzZop!hn#HW|?h(mWPtBs=2919LL zv@NyC3@>9lqbvr<0AHX-v@f~!{qX94SVM3C>D*Qr;#k4&jR%Tqy85a@Jo{|~i%$yb zw?kr*F^wh%sedrRh7^U`umQ`#9Ix=0k4cu#n9ixY_4q-sAiAKaw_E$1pl-~4(=seg zOh(S03IWII4N*Yc^;fdf?-yKCV9`WS<G+Z}uuJHy7Mw7R^7)6GQTklWpHsh(D}OPt z)9|f`{-FP#V01w=LEm7uMtZ@}#<}=lv5iu?W*%ug!R^nIt`rQdGK-XuC5&bHzK(jp z+`SM>NHt6W#2NX%!ie5ftQ+yehHY|Dm>2@xZgE(Ou1t=~E#BJF-e1XrE5y>04gVL@ zzn?1PD(WzaAikRQ&{go@kjy;hcKD=aNl+2er5%4`@FX>&G;tGD3#1_TYPOVDMHpP5 zT+^_5ufXwTDmz^-<5fvs;nby2LMRKyqX6JtnUHbbRm2KpiB#>5l`$mjw`C&nxPgd; zB_;yn1Gx1nG;;sos%!c}0sir$4f=YR*r(I_^spWcSiJn#7z&VVLC&WVI^2Y-#w&@Z z|Ik9ZSt%{}wm3i*jp$&1nB{LN#;^gcX~|V+WogY_dV-ZI1nqt_{6*NW$3GKjE823Q z-ZTRnAoj=?xy;FOf^F-oE*ABy;2lHjBB&2!<+cW;UpUrp9|iAKa@Bk(fvt}Y6|YFa z#=O$UFQ@pGWE6%3Zl@37O~0mGY`+s!*wAODUW03G43FKUx|xjKBQDM>uh~ScE7~Te z0*)r3Wx~!NlF>j0Bp-!dFYk3f6ziMrT<W|zyl*ayajF>Lmf!~amv19<0Am)nWNgh0 zw*{>Q^Z*S_;X?&7e9KUHGjV(vsFk{PnH?@vg2tC{UOyJe>%_wyYE?<d_h+_X+qhn9 zE_ag;J0{Ctuc}%sZK&m?uuHq9LWekwiHD18n=owW*wbfemci_^CVM=ZJXFkZH74CK zUc|bh>YtEw=sonO76{I{uLd}tyourX+o6gG>vn2p#dX2j<u<n&&&Gcc82&=%cMO4J zb&Bt5I}c)w$?}wqLzS|#f%h2=MkS7b9($?W{}{@`KT|@tTDQJSK~&MjyO+O^?0D;g zpxX~P=EM*7UQ$6e@OZT8ADw0NO`Db<0>CLQkra^dS2IrHhJfjUH-R@07%QV@3?P(Z zA(^10fuO|V`GGtL5DDkVXDD^(9{Svd(TIsATU3{WB6A0GNWSGNXM>g%i|!B@yLBN? zf+QE9tyXUgf`nV~JkI5`5%m|wS=5`@rw;<O3QHUYXglz9${UaLM^zpEqd*Jez}ZE@ z`vEEcSQx~r(@1Mg8sXyzVr;Qh34c0mmWG4iNC!0(sN1MYAI^!8;|S)nfyK>`_^!m0 zJfTr^sfv>B>W0bW>Gt=<z)CG4IJFnml~C5t>_mCV!G-_M?c|w&86;633#-NCQqyoF z$g1vhIsnE~zo}?y5W{{Z;Cd03qbZs^?cjpZ#YT!Ld)oo}(x8yy0)G1fcWunibfs6K z0Z)Or)w$djP-AKk^hD-*1>W-C|A7XpY_iEzOw{J9W+D&dh|gHd3Dvo+AOF?hIG-dg zhjJrlH~BC2b6N54FMA}LreF&`<uSUr1V17*Bs^+hnPpThw)Aj-g;hGqnxRfHcxbhf z|Bm3!uNwzQ877(M>^lw2-_3-JR9<W~liNKgQ>g0e$4DsMvH-|l-Lm+maC<GnzIkh{ z9iAq(b3GwzGK>}>784sNi3iV$D4`uh9FijChoV*gbA%)tCjdV>UOHS}tMyo+y!oIz z+~JVKx|{64;6Z8$_L1HaU{l&8uC%!w+7$X4oN8@Lu@HT)myR6l??$$HVy?0|42Bpy z3&Sl#ku~wu{+~aU(#~<gU8JHL5{GNi22VHt0iRSV!N7#5i7`Im1G4zE|A%`%#X2)+ z{5RSY$D(ZmRs%fRWKRZCCZ=uUwEISSYy^-<N=0M?W<+4No1u66kOIqM(I4FUNcPv? z1dJjp?I0@zUi$y7Jt>kS{(J7p3Cf58w}=x&BHz?lgy~1pFSFK&#ARWnN3@x&F_5IF zZY!}lb~jM1n`#iNduY^O-`Tn0mv|zigvi62M)=g9<)Dc;>=%G~#tZ*s-!%wubOAGn z%A40OfEs-+&`nJ{Q{kv+2ZhUmioZVFMCK7=a1c(P8XH9@C4}T^3^~EXqwk*FfI?2t zFY-xTtDqwLiAw@c+biQnoC+I(5?G06542#R8-_DQ;v((zu55ox{inUwJXbQ<)cZ1v z7oI)^HP|B9mgTG|4}xpa^!e@3fCDp7ya@iT5bS!ZonBKs4ny`<OlX<5m(;7#*BM$O zE2?S#QJ$XaigT3`QDzkcj!EbYZ;B5$`wSkkepq%F^aOp)zz07$Zq;V8vgwOD+98VZ zq<~_l0t95}BH0RO9w7~If~D`^9ZQ1Dp9!cqp-Tw*<D%FlVb*-G%ogNm$m>VZeE+G2 zUs+>n0LV~vy>7c%m$5F;5YUr3E2j#+acM)z16Mpvdy((~I6Mr!Q1oN2kswpC@N`+< z!sw`&di3CUAR;AP8j9L#kXS{XiIm<Y=m+Bl_J4$@bT*khSNFH@bfv2o^WuxacD^j$ z0;QxOu`EP{hmI!ia21M(M#2+UpV7#KIW+0?F;MLUvi92mNPxWj{f%JGu)rY|4am(h zRMAEp!eBtnfeM5T5O-1K+mZ*jV9=xN>;wg8j2-Q$s$~E>P3Au&0zn3>wW#DedTCsV zGy4@zJ0RR4tWW^*W&XP%^-jHthUP|i9%Eq|Wp4bD@D^Etm+?>-^ut@61v<s?`8X$r zTv7vp2kKnFD6`i5rX<!3hb~$(iZeOzro57QcE<mecuL#!c%kMVq)CPeH)~{IN~uDj z>>T5Iq^Xf*!I^hin1kGsz*<0Wn*3QUscZIc_@(KWM`bI?COK6BcKeqwt$R){7N9iP z(X7glQch;O(TJVG860R<Q!)55E^}Zf$eC^+&Dmf7Og83k0%^Dhw%CpbfJ7MVES|1% zjSWPT5~E8Ii|~*|f?$t2XCk3F@EFpN;X1GAv(0!v;qoo2@KCDkWUs#`0>PxmZDYh3 zM5Uln=jNf~Suu$a8PPY0oZ0v4_mYUAwt)-EYZ<vA8}<)jgj4;)V-_#^DcR{W9SKlw zpiCc&ap_w8NJ=;+p^2eN&yF1{sZ20XKnHzSn*>eAU7=wEkuMrH1dwF(Z>&lC7gvJr z%W^>WR0?r)B!*?wRJmiZBCm2NV=7z18pg6jNep3AUXuTx$W9SQEpf00L@Su2#ECw* zqIco%^<Uk<ek77wRr|aO++5t}WQuOY6US&IMHyU*^Z+u2-DtU1k0HUwthB5OkQQvr zOY&~oPy`iF4tx@4j-5WS7?OUn-ym)*se<3UP<|YYl_D()K413I7RFl5&c*UDm3=j$ zQ)hDZ!dwtV-j;$&(z(}lxv*3v#0hyxM=CK9eNo1|)VL_SpTPg3Or|<X&?1w0eBCo` z``-DS1ttsk1mx<)7T&x!NEkEHn{o{^6_pi*X?*X%O==+mLFMbP5OJ5XxE_ryx59?o z2;X_DhGu3ht|Lt!nZ*K>al_$>?k}VjKR%XH8ls)k448~HjLw)};-(N6=sJPzPCW*; z<fjSQ@P9R(B2tF;4|Bl$fo@nk5i@I@BQp*FRfy;zg1Lmfl(pt0j88^ezX-OjoLjzd zO1lf3&GQ&btjmBN(o<|f#_>$_#v>vAY_!d)Eku$bmA*|IWIh9kG=}||2kk0l3nu6$ zL9H*B;<5|_y9fcBuE_t^bMkYGGNYZc;~f)`Y;c3i0(-3Ow^_e6XL-~@jTujZO)<I! zXCS5<Vgsy4GU33YZJRtC`a*q1AY7_=k(4D4cP3ol>hnN^(F9jf<sMi}-!WWgWC0Vo zQxq8n?6J69QzVXJeJ9%<%jJ)P=+Q1VgNs9=2FnCJ1i3_Jv2|&789us&>qpTcEe!9L z8s4tf64q+>E(`5NKe?SGwzY;n7S!FJT(4{6e^@h7v#a|E-sBkPS>ou?<?*=wg1Fgk zOLRX}%-elRzCcB%fDVP%#K7=i90ZJv)r*uomuz^Ut_raQ@w!u#Lm<=36h$@bhOB9( zf?AWbH5&d;#|hg5eiT#ciX$CcifIW2S>nl6gqP)dSj7Y}>f4OeGypXxaLk2ZtT2Gz zLNn(WL^uHkjxg?))Dq@14p;ip({8-`j7HJQS}*zt1!1$KQpy4NUmd4<J)9QX0Re9{ zh+x?NzvFx@on7lMSdR_*Yh{GWJP!2_mQOt5hv)z8IGIEZ@Q)SP{7j&%d8$FK;3$$@ z%|+AS;!F99<x8baSt&YQ#ia@!nZ^;hB+%@6YC7?h*!MbWR3Nm}JDmV;T%8;-4M@?B zr<q|gpfQS*BZYQ>VDXJ&mk!yAT|Xea$G1dn?#XKWts$!GYq-r5R7nCsaYQy}%CEg~ zAkg^=Jh1pfP+LL(P%OU^=Ro;`AWK?gGz-KZU_qpDser0EGb@W|{br@(B*3u&8^mG8 z?wCz@x)UVmL==R`v9jYq4^dl2%xx}ZwC#zF^?-qs^n!}mv3OFm@T9Cl1YKA#f6474 zTXb@^NKJzG1&!pa#=&kZLA=eHEZ7D4C<`J^K?OQ?m&K|7Z_2s2b6P%_%c|3la0dI? z2uNiSxCoehN-wBX<XxZUF8*&NXENOsRme~?TXdg9%nM@7D4rrd+6*0cLq_>yH5UHb zpwE9VIc?RdK{S`qyCrdqxku8U$hmqTH0@{kE$mS+K|~oP-6KJ?yqmyIQbQfw360DU zq0<+YMMP!DY%!Cprp?S)ENpZ|5h$~%{%GLrC{UEMROlc8Wo(o}Y3Z%Gqxknr7dH%S zux}%Ul2RbeL98ot76(*cyjZ{k5*LLyg7hT6sDfa@JhMuUG#E%gCD^v<qvQ>uE@Q)2 z`%JtW$eLK-k{Mn$9Nya0W>+}WQ;`W$VoIGtNoehf=y`?_Qi~|)GoZEldM!m<XJKk6 z(g?-*EkUPUd4BlL%?mYx+$vB{I%j|;n1bCH-yf!}NQ(TNuB+z{p|@23OIYf(N?Cvs z!xs|aREsaM97JyoP@h*CP!#DgE<q<^QUcFRrxTJK^fOJK!3cKAz{f)^j#NOycQaPj z1wC8zTAE|ll<Be&elw_m#fGQqCKF7Yte(_^3+*o1`{B_Ud2TlH;IYD!Y0Zjp%%M+# z2b+);fnYWOZCUr+=1iaER}f)$IuQT~KSmtWwsWJ)O}Xv7RyYG)P8E09v|#vRHxBsu z^@#a1q;gfXa`g<e7N<2nX&=r@3k0ujuk8|~HGT+XspUyS{xhA}JN5VZ)mFE^uW?oO z@ALgP_m`u6W34*2-nTB_HeeNTTwSfPcgyEfO~6(EcV_YB<$&>(|M`ID6VpxW*U5*c zu>bSt<{Po`$8C+_>;IM5Yb6vVxP5_M6W{%kxx7{L#gQVroYo<g7?MA<{@dr_vFpq0 z!^rpeKS|cYN>jHKQ@3`%kG`JQeazXX9-@Vg{pCj9H+%Sd?k?vWqeG61Pu(}YBRLKJ zh8tOQ&3`TZ-h<vg`R71P8&_Zj_bH+6Nkn!=>bLrJc#)m|8Flz^`EizClK=ks@bvZe zx>F_G^LBIe6Uj6G@<Uwh;wJ)`>k#bSi2lIB@4*Y>`=k9lO{XKyY8BSt*8Um(lRv)N z2YjLQ$Hx0Zbw9nRlm8lT+5K$DKEWb1obi#=i=ywU;y;6=v8F?=UePoEyz(0QB;<U3 zsQg*~Wd?b}Rv+RA{pg~^Y|2|q>E@ArQPc8nnKlcnajY<IzC61?z##fL$(gfj8#Yo+ zvIY4ew*<F$Ri<5^HwQQD=ZQW18QN-k?+x|7+aTYrr2V0y|KmAReq~MbN4&0BF9P4Z z!+L_YQk@xpnWo!j(fZTBz68w8%dRM<IM<%7N56AAJ|H2Y-k`e2tN)%@t)Jb;*9Zf} z7BqHJUH|s%N$9y%b3)UWq4W{LnlJaXM;GpT@$#Ik(t0uSkg+k10P`}f{y`9YS-MTm z99t-V)q(hXSC<0+Z=x&akAiL~NqPa>?2lJCUF$LG_m)u?BIcPh;jhC7?vrd}$L)`| zItI1wE*VerL(F<f_NKRZm37R|rxoe)e+zZ%;xizkv#$OyM6%MNLUjVyBsVd{6lq=I zc7D&I^Dja&4@VJ?1P{9-?a7`!Z|ft6y+%KukG8!M?it>Sg`aT_bBOmBk-V>G;~%Pt z4+n|7pH<g*g~vY(Z?A(lA_oJ!?$U|3dSA++UyhJ}`rqx>2`GM6>JR;pM*ipJob%BH z(&^#(y97GlD(~ygPE?p^pPNe&&gx5`ji35!=_Tnh=jrSE_3dff-|y~{T4<X4_bts~ zw^ByR^qZ*ANsR2vB=J9WrA&?KH&dbA99fS?Y~PgltAIG@r<t)%<Bn~-S{CS(^%Hz7 zYPOy4REBtmw*lnsk9WOXNDr;gpy~1KcsZ1x^N6>^Lhsk(Kc60l@>k~G7KL&>w;LZC zPq`?NHG+4VCH<fm^lV<VPEh@R-$h}}Xn2>8{LB}3+dT+p_jXn~8WGD)42D`!eL4#a z!!WoDHy-;3JKmf_`13O)rgyM7;MFR1kLa~jdOOX3;ri75=c}OZOU+1468m}N4{iUQ z2W+20_cm<3nd{Kjn;rO`ZJ75&$7Q^9$AEVzuFqXia$}-%qiAlUA>*^t&$o2l{`zIG zt+%2z;@2WS->z*U^|H3`Sd!0D(EAS}+KX$2IZiRN4lBv;Xo@%LyfVkH)Gyxim9PDg zr>oD`S9G68oRAp43@JW;C&I6{yT_X^)bK;@7Y*XC)5G`oqN5`#tpQiB#^6>%%yo6{ z<NI3E_O>Ruw+rwq*KLu_GsKd{b^P)t<#LxW>cwO|mi!ao0#W$53_l^AYj+N<@xFpA zv(@;e)i|vcu%Rl!N^W9MynAoMoL+Dkbo@xV9oA4j-?5R}Imtu2f$Lc9y7soZB98SG z8&dI;iF<+^nDRT8PvN3{=k~6v?dvA}dkVYz`uKg*?r^`tHkbT^PI<WagKzq^Ft^t< zr=K4=$=hTon%$gQsW4$mf@1~eEdx&urXql{|8%?86QSC@(WPWHW~k?QD;xGTX-pUg z0>3-fSlZ&L?TS#ND3$~4?;tIlm+2rup>DzhN-)p`;p6@>l|%IB+tNB)>Ys|ETGF;8 zR=jf2C)!v5${`wYylS#eZ&K8*<2FCd12r@!Cpog-(4gvu3?b6~Lyott8Is&@V`;Yw z)+eRF*H&25WFimSW&?u1uzu*W?D>NJl%eI@y?3N93fJ@xrgU~_icu<?3{u&DX=cOc z9zJk>+^KU{ra%<lHjm9fBVB}<s{16G-!2?;k83CGc}?b8A)GAQX8j_NCJna-(#f%k z!ReuN2Ykt$g2s2ao|>p$v9IbF)Ns*iOH4r)3Lx<^l@Uw5${8HRcY2D*(`SelZ(7rH z`Q*3K{^p9Ac0E5@mG`;rNxnb`Sys}XPg0eJh;3xTpAX@~X#Vjjr_mVi%$foqy$W?4 zFKTE#SWw#N)Xf>^m#ayCJn&WKp_gZu^_iDRBB7I5NG)RkW(ZzqPiPli_)ZpT6a+OF zVJ90Yu2Sg?u;nQNRVm6dn|X~REzVg35yrgwGOKYIBxwW9&}VFRMNPaC_*?&IlS#f_ z=o<$zWmv&AD<)N_Biw!kYMAJ0qy<$d6bjwB>lhHBzffIklR}^vk4L%kJ|U6mPw6rJ z*h66pGaU#}3xBA<=M<HU2~rRrBsQ^NMfzuF`rc_?N|O0Uz}QCWlhvCDgbW28{&5NX zpL9B@<(wK&KB*Aofbz*T?jkZ914370a5AeAF=5U2pl`-f?+@Lh1&q<;hr@fMzFUI3 zx?atA^)jiPYJMu?s=qt}OXRd;-CMxkCj_!3rslgwOgj>36pR9#W44SMnL7m1YGd{f z8|96BoD8A`8vG{P=mR0$C1XWM3{;=g?ghCQkFGmvhnYaSo>A~Y0r*JhEZ%#HDtL9- zWHbB4kCfh5^e3xu9-`5NoGHJaA8<s0?C!6*={B#lzs_4l-_W_Op?6O)kG0@+kwqHe z!i>s86uGfrfBsT<9O?NUFn@C}JlY1tSh*w(`4$aPrldULJTb031^29x=jR~eT4@S~ zNX87lF#y?!;Fwy@1Xh4^PG?DelTqXRd&nq$!C6RD!r9G)R502kHyj@raDOBE_5(;K zy!IYLEPlD2IFzU)Q%b`L=`?7I7sPNZnq+QFk;YLwD5XTv420uZCrx9WxtS;+Pr;WC zw;6cy3yz>aQAIIU?`e(7?Dkd_C94{8l>-}p=@oLH{~P_!LAVwRf?yAs<UM;(L6T?a zA$tVdc?W{B9i+{(>HKzHSwA@|&jCK|Uq*B-+p&d&Hc4>5V6Ot|@bi^7a!~Ge{y`|x zJ8W23ia@FtTTb^MOSrCI^?qMRDm1#>ammzF77eVr9ma$pS#Tti3FjJiC}qoU7o=?q z=Js2#+3!;>=7Lt!Bov`<gRgEX=wA1Lw?9Jnq^2=fjT{io9KuwL3|FK9iPxfG0q!|$ zG3sP~-!*|lWD3!Jou>EA=-0T=d8}2%Z9{99aU`VSY1w8+q?<?|yKHlCXB^5Eywd*K z!?@~d&D-V+`V%$84^ms>^B8DRN<nW9Ufj|gqos?t%_nqED+qO7Iq@?C1vN$T0m^p! zn%|IowPuo=AIBB_fcp~ukvwsFp9?8gAF&p4RnIM+eDuk3U0F|Qb??1L1UJ94Rs}5t zXLzEJlzApcZrqikye?y0*5NXHWsE%P49MzYoWWoOrE@d1+>-TNl()?pmA!@5e-drp zK+@P1>Sh6G$Ii?&p27ag%J!NI&t>KH3=MV%?SzKXQp!PC_vJ=hh{lyNo#giYu7I`D zlF`#{F#7yK_maN2b*=H%4Mee&vDe)I&yQxp)k0|WIuaoo7wxF-zUu6#2$T3cx{c}0 zr0{160#u86Ewo76_Irg?$o}Y2iKSJgT4xl4t0VEe2ok*x+%-Fp8l0g3plFOW6*$#* z7)g;u*S^i2#I;JU0!cKiQ~^!mMTP>LLRuA5pPB-N-5Vj7vIJ6L?(Z!sG*dR!Qieh3 zlsQ~{rTPr%v>)pMFbU5RvnZ4v|1>QH;o3H}57scD=bcE#Vw#|{0{MwJl?UNTb&Wrf zl8W7a()-1dB!&)?tnL*03wkY^3depAxEf_X_<<=q8n@mP*L!(>3eqkJO~CTEe!!ZC zM6rc|=m&jfw`-wyU+6w=!W6cq)sw(NJ`A$lY~biwYga@8DSf7fZ^R<S2}v!$2J3dA zYx6<<ErZ*8=yVDL7xd|v2T?Rw!ND%#k_?Ck@p|F0t}F>@>5<#F#elSBi7#L?u9#xS z<6bJtDzFg1{n1<lGl+ks>oee<3D@J^fQ6X?o3X!0Xi2fJ9w<UzcX15H<|P(V$*&={ z8ntGpqKp5=n?MZ)tcCZzYt-4`>T~FpZRp~D1Zm^`y>2LMPv7{ey`+`X!Yj=QbCiyX zJsvw}(itOg*@l@P@hY0a4G4ut{qi2^ppnL=Z3~@>q=69nb>4=b5Az~AQ*G6KMTPl{ zbeGZX7%41Dz;)7VdeOD%>AmOgsdIO0H{TExWI1m$>da<qc+*VqHWoEVP3B)dntOzq zcv;%UWwWu6XP$ctp{JhiD17xKk%26tS#A`3FPLp+0h)lq*SeTF-luod>M0>SNFC%? zu1z%2d0`=z9wn$ngmT<gm_D?^iRRU^ZZGUCuNC4$kIe2)8Btb1Aiz2XP`gUG4x(8I zh=_fb2tocOxYr(pM?kKHAp(~?q?BV9q*IU@{WL&1L|x=dQ^}E7<4KDPRuW0~<a1$Y zbJe`Ue0d6zA8`?AU>9{m=d1B2i@xM1RT1g~HKI2fuHA+EzH-Rpj)mQiISA;3SZ!Hp zF%7W~*<-wlQ$Ej_vb>1%KpG-B|9Wg%^AOVOzDBp68z@bO&8L8ioo5LXWF?0Xm9x^M zgS9<27>tNQaLTdv?83&ijfTy82@T}9+f=`r_15g^Tb~7YwmS5+)y|6#j^I6XR>6CG z8K|IbsssvJS#f`j<t1CXhY<85q$@L_8zww<Jr0wSRlKe$ZgFYLj9GoTe?)6_70m)> zug?oK$V8=dFh8VI-J5ErV@?kF@Fk3GL;sOnf0}nR^YNF02nSA_elojBAXRFf;V+!S zj`OdpcxJUcR#AT@>2LXP%q=t1{@P^_<%UJpl?pz<+Q^|A<EKkkeoM9Vt)KM1pZ#~M zt$hr6gM~Ld*={K%PmiuJ(Iard{dht8CPy=Y!2S@xkDR|0FYuyRs?x?yi&~IC8jQ(8 z)}~-F9MhZgg*9CqH^WxJ%tcJBbbFE;T0ZMI^QUYCdEhBLZ1?TT4d=6>7>b69NjrzV z825W~5&RCamX%V_ALP)epraLCL-)$LoFWyHO;A}_;`>pXSLfv6XT$mpHo+a+<wxq_ zh$L>gJa#IOSR)l>y1@jTRG=pl6}rKMM?v{~wm_<OjrksT$<3pa9)=Ciy%npV!6gUP z5A?I6>Qng+W>!McJGx3S75jE`lSZjvC|BX(kmp5Fa1LnaBl?$PTjRsRB!y*b6bA!! zS`nzLbPyr;=>i;s-z<JzujC^8A>K$P&*#(T*?1f>zG+_hUPmZzlLQ>)3~6Mdu$^}p z8^lOp`|1bnBM!1p=%!cVCZiZJme#BO<geg&mFpUE%LStLZ<@S$eAX)3fJ#yDmh88+ z5FGWu=zOJFHc1RNR_^X~)cZwQ@Wp=#ZEnd3lBR?K7MtD-Wf_*!AyTDcb6H?r+^H&h zZfrqoQk=O(sl4b&nR<PDeY(M;+#BPHd8h4@8%hb|Ik3S9UYd4`1?bOPK<&9$=KO#t zwSNlF=Jih?=22tA{3I)RA%4U?`D7`)=#Lrh`r>UZN`-}7+EhV)Jtqmuvtv21-U>ts zqDG)m?&7H0fuV7XnE_D>{}eLJT|jdPo1juHAN~lH0}$7EBq?2~23hHGkewn32Yilu zt78IL{)WJuV<b*yUJp~$lL0p)MHf47Y-8`-Ic(manBpfh6NamCEJ)6D<8n?f=sOq& zq!CYP(K6XhV|1a-GIBUfprLdn7oDUXPB4NeGiQf@b4b)at)NuEjbsM_xt%vA|H(|J zAqbi%Z3H}sWXd+!wZkqe-XTI)DhXCl^4~DXjz#UEaFza0s6s>du0#~9u8?)MXcQjZ zB7n+aRcx~_B+E(f>w!rT-6xQHwm6^Q$HFZCD29Im8Kx|&(P-S+)7I|4Ou2JMV6!sr zc0zE=x8kc(Phk{OhCYM3T2~{?!!qs)f=?iCpu!MB{I0a*V4<*tsKR-XsWMu*R>sG* zn#VJ_AQ}l64k9!KMG;+z1X%1lT3WJ~Ppm$A+|6EPllKhuzIz5%&=`WPsEu7k(!s^# zjQH45)0j>NfBiB`i@k#+z>?^%nWlM;&3R%aob-134TQ2riou2vMq(Q~*D3SKhx;es zWh{5lq2uV)S70gj2){FaTys_5mjit$KRk&6fY%9wjMb(SJ6$;2Bn9#!x288I>2EZ# zv{3kE`^@b_JWP0BcT<qi1LqwX9F5a$Sx8gjm9KkqV8`UGkm_5_m+bl>Y~wu{4K@hp z)7eeEM5%x`$+TtKtgxuMa`JQoMC`pe(7z`-(?L|ODIkF>?&yv_PNf$}>l5AyT_Be% zwO(XF_>jNY5`Hx}dWZiz6yPzer3WWl34?-PQp6Iy2Y=cnj7q=TaU~@j@3Wq#3Ati4 zkC1$;ZCLOO-#HXmF_?Xp@mr7G0qvv4)d-fb+pzNFMP|ZAFb%`ZH}i|--gKx$tb$DX z+$i+mMJB?AlEv)hv+=PnxW)Hg4~A^wA7nb}-vh&&NJBY83_h~TmIlK<ZC5^XAtGSH zv4LP}(ZkLtW)sZ-Z9%$B==`o2c$m~>Z^PJ|l(y(}e%1Fb<zpABbH-&(7q5WKs4&Pt zbx0R0GnI<%GS}~#!6Mk><+kYTS<&g2P<JdE)w^8i$bt2QZzdUr5c;iPc^nvQ-|jdk zLgf10><WK$0X6#~Zhq3|2YJs$)%p8SD19OmrA>t0w%lT=Djsw8Wa`0qj*d9-`)qf3 zs^vX6UsN{Ae7a+ca1o&9<=l6(H+*mx5`}ZsLKz&)Stk9i>BUFJ?4iQ3z7tLB*Y6%? z*`#xs#x6MIon5oLdWyO}$`b7<J&x5B+X^O9?QZ@HLCv?_0c8+a^(PJrvpKc$YOIoB zWOa7JEg<6Lv!|9NR!J4MSMMSfauQQEO*;uFKBqz*ld@4DT@EkhvidS5aVJxDc{|Jv zsC=FCoz3ySzIrv<TQ-`~pdr_s%WhbMDHX01S5%(-H5pMLp;3y`o#?(Lx@0=?`0J=x zW3)Zdqv`71MEuVJ+x6nv393e>$%&ADn2mmlSr*$NEsG0%%beSS$HvI^=p3iTT9mj1 za<E}G#gq-d1R_pju0*dy@t`W-=De(WP_zlSsRY&Y;@b%-Ic5kq{w3;ShIDpfY0Kjv z5HmAgbl|GQT__W5qMUsSUFT@N2HzBEdT7yW0c2C;Jv^c4%1goQe340WX`UB!9sG;5 zWQHP!i8U~}%H&!s@r)|&j0*NZDTItT)Rb#3@P&jV*<_)dtmxfx5!PM7PT}golq)f~ zkX0Y!jQVh77BhTa+Y4;4>sXZ<fNqGxN>;K7CC6NWY$7c|K$qTlBf$p14#hc+Y667I zbhQ#U>?gzL@Qo|I8}S103o|w}m^-7VIbAapTu4--7H^SXS=UF0FseDmD6I8}DUj<l zKzHOdUkXT@qAd_sAnt8o18;Zn1QgYE2`1<t3MiVQoe*RQo}Y?bG>E=ndM(TUhS^l! zl~XrG17qsE^g?S>Fke8K8|WzVYwxywbu$e$Ed2g9vJr4n1U=ayl3Fm_LLl$4bw!Ll z^Eup+9dzpn7kie_rt(5{EN84rszC+t;hY7c($0(X+zWD$qIr0C5TyV-P}2fqQBPU8 z2&9MMKC%5}1roDYB@LlsrWKIvH8v1~v6rz6GJY-kq2#VgnnOvfjlO`=?Fww`h-l7f zxZA4|rC52OW`X`(okOoTdWeNQBEe{wLx1_8e#tak1nk_Womd_3#TYW-*S?)*f=w|+ zvF~&*nEMf=RmPu}M+wHdlU0OUiE(t%%=8f^lycg**sdh;fK54P>}8ToF@bRqvDn*< zs;OZs2i#H3#rsHErW0A%7=5Qt{2YY=F&^(5oKgWp>~PL=6~ZigY0%>!KG`(m*d;XM zc)8HyxVTW`_zutH5b|%sh<!^cYDg}6`H<td(3j&lRIZe0@*>{1gvl6=V`?nny6_(2 ztc_hTNG%cNmc&I`)qQ%gtlACx<Jx?+smy;=8%#IDhR56(QGDR0=n+wfg)L5OYWk{; zVx6b7*b5ujdsr(#AwASeV6$(A>1*`(muj@r;(;A9JWo78_%of&+c;Fo8@DJ9;BHVg z;qVV9HKlxf&(7O7HL;zcm8BMKVmIbjEg?lav+w9a1mP*RcjGZH;L&JqD&r|-9DHA7 zibMRN=HCg=fy~5StrF53&Ar)0BY35Aqijcld@)q>K=tt9D1C}#pE_DC$rFmEhokKK zRAAD+8jU8}V6~aC;sR9M6XcI13WR>1abMD2u9_ZJ#|Qyd66j1xqjd74Vf03FJnyV% z7aR%2JU-EBuezw*XuqqohGNGt64y7`@~h4RN3p@^0)^`;>WTF^{@(<CeV5eYo#OG( zxTYf77-I`M5bq=M+bZE>HDz?zA;83maAYSa#-JavK367{!1JLV$MT_rTbFBr$&@HM zAHve=N|0jBdHG~(1EcpXmgmlQwnXA+35_s+wF|NE4#4tA<!E(Tw!@}@aV-oUA;W%) zJS+GcxF)m{<0M%izXU=x!r@l8G1Fk*^N@nvxbd+YB(AVKCwy7{duZ1e02b4*?kD_+ z4ahq-qlYXtxE9BwE~+{LeDOa(M5lq`Qet98=O6Et8LDDU*5?Ir4j0ir>>__hEFZ(m z$DKUrYV~AW?7Jm~gySWv<HpyR4cxV%-i!*82{RMiU200d$FN_8JF`97yKR$HbUv+3 zfR*1v3ic*;&ilzp?HB-O6p(vi<y4U}`~n;VN=j^WOeiTG>b^jowYVD+UL!W=_ahCz z)HXP>Uuhmb(UEdgx(8wE5xo0Gc74=IJ8YViu&l%5O%?;-O&R4NS^Ku(JSb;OZne4u z@ZvY>C`~@A9NBppLjpDA^s1#HMSQl;Ylb`qq(o|Yk_YNtyTusTP0eC>g=n&<Ua{h3 zyyzUN60u+-kmmRvYhgHZLlmJup_rT{Bym_pEE6{S=NYFKlD;#7in!nZMf2+Q^`Pt| z>_<cFxF*dSerf!fCM#IW-qkjqoMR)(CIi+?{`y{Rqr&^^mw$!2fRmbe_<)A(+C2CJ zBr+$#t#XNqRV(bd$_>AOL(TFp_XI1>Xw#3RJVSS2;W4nuNVFAW6}0S6hHeNxD^zQU z78Ae*s!sqF`&9Q1ORpMLk~WEf#@qZasS{`Olw7oKR&eXQETaNk7@i@7Nj$C-gKk*g zn93wSBf#cQHZxj%B+u~%Qk%%%ub>3FD=A{yXltxjv$Q|e3P#z^jfuZ_D<&ykW7$$Q z<P%Aq<UMq=u$weghz39858{xNQz)+OZ7aAkLG<;~4u>cpO+nR^$ockA=TtFn8qj43 zS~RK}nGqx>t_N!*k<!4_TqHu9Se05}pu$&6r9krBKhF>hhwsdyg6=TJ7;j7RB@q@S zvrKF&u$XK%XlM~-W4u{~3k&00tW~-6LT<}v54-G$N3Kd>nOTex!I`61BQFSsJJoym zMQ+<Sabk<%56RDDPFmpoSovA4`#I4>OQd&ntznELBxiwYL}29G*08OHlT*%`D#tvN z?Sf&8f9mXvj(T*%{sM*ZrFt{6RTeah91f_NmW@IUDsC%_BjN1|bFGLnBzx0xcN~(% zIm$4uaWsS+GJ?0Hs`Z~d+Pn<q6*EdqSWlZJ7VXgxFXjnu)czD?s?nV4nf=L#4+sh( z#kHb(9S~Fr?;_~ds7%e0>`6;giTshEyK@I~xOs_OB}I7Vq2yjwc;{d^A)})trt-JS zW1~euf6`^r75u2fDr*u7Wg74J*-fT%%NOcYCynnF)`d+Nr-S$BN*gG5Jl9PNR}i}V zr_eFL>1JwW3S|J_4nqD_=^T(Vu*rO03hIFl`^w*+{h_}z^-c}x$TV{UJR6yB=4-#o ziM@d6$uv;A2W$Wmpe3zDAm9gMr~i3GZDHV$o`;Te6$_-;WWc4MTE|!=^M^VX)lm8^ zs7#o<XK>4bFGK#u9bEqB>`?}yq!a<VqhOltOk{^POO5HbuyB$q#^b6AV->lMaL+gG zP9D}2wLZT8t|XFUa0WW?9bPtmMxqp_$o~4viLGB9fm&1u#2GJbvPxWVXN^T>O7UYN zFU$*W|6ml2MTtMY<gjB4rgsOg|5Zc<Dc)5ebD08+7`V^TO>@VW(o~4u_c3+yw!orX zY7zOJQJcAIn7rJOgP4pMQIex@cMu*)N|(U?Xb<Jb!)_wg_ocMqKn&&M5(R=onXrH& zq*4U>%#AW~LX-T$v8Ya9bs4qKVE~)%PGB{V<7hxyIMURSx5twSs*^O>Jynj(;?L&B zju@sS<txVrvXacBG%<F~Cm5t0@aGISf7n-z8YUZxMoE0f<zeCwXQoSdSm}veNYXj! z6cv=ku1nzqM8sX<bI*893Cdp$2RiTowCwl9G4iH#B-maK=GA7i@){~}sv)uAh9wBh zTJ{gHJqDLlVT2EgN210c9_9KbC|3~>HuB2K8*ho!N9Wrzpu)T*$S8ww*0BHm`c<)W zm!|@_Bb23MHdI=hGWZ*QX&|fL6Gy$jBKSpV;N<K!H&V={{(j#*H6e7TyA9&vn3FJb z_)=*~7HSkxWi{)o!AiiK-c+rX?%!5Wxs18^yw;Qo*R_=+F`}`mtvdC5=kS}t+>>LU zd!4AlA2#YlQ2d|pnEgfX0-&8Wbj=3m9@CbBk6{2h-(UJ_;`B{B3bR(|vSo7!i<M#% z9t9VzM1J8wT0nJpxyvsC%pKRtXraz}yk^&{Fh0Qa&O(ziv7ktm`fQd7jnZwd^+OC2 zk82)mg{63Xj_5R2LaXavqujpd@gU^#i&CGV%VbZxh+2+zr&)j~dvh7>zL5pW-&)ow z1}p@s<}^gp*`b}<kwjojH5HBIdbWbG`d?@gBgu(@xr_}uA8<jsTN*hLU4fP|K1xtF zxiY?d<_;FpHaqG;tc^|_9q_8gm?O=<N)5cE6Zmb`!W6C6R?I#V0OOylzd@`CotWUT zH0;M~-E|)qD-w*#9_{udhUlJ~YXhYjv%YuoU<&su0c7yePQ6tyN@JSamaky)PO!P) zu77{<{&Sf>p5XLm=AXtwwiHm!1n%8Niu4Ifj$3PvCgKdjA(-V13yI}VCeR(Lo7X?n z7QDCXdi_Sxhq=-+d*h+-F<u+^`K+un)|K@fZ?v^B_%r48mFkl`pp9TX9WT2B5C8W` z9oO}zKBC|I53Xw<L$I<JqEKGzshQMvXNJdHtNhhvpBXj7_Oj1fX*6+<Bbs;f02ISI zDd&l{iJZ&UuV_Ypx*+E%k4C)-fXu62@Y(u|o||)NozXkn1;<%>Y(gIjw2y74tiZ$3 zp3-*m$_<Y=f1V{V#VkxgdU=uF%v49lqHjY8Vf+x+rcZg17BYE^(|ks>s=wPF+z&Zn zLw8iQsh!#^y&3xsoTV}v3~@)<58YV8R)mz!uG5;&+2ksF!PrZ(l-;(IdC=><8xWzg z9FK#A9;1!yPQr2L4ryf&7_@Sb@=+UMCaOm?fos`-Y$U>G)JM~B%?)$@TBAqN>a*P3 zi3^wDy#D;il8z&<rRb`v9&Byj0nuj{Po!8Kgk-@Gf(aXHrhkv5(emAe?vF0Q!g1o{ zST$7^lde}xSWig5bG2khx#0p<jq1H_q(TNq%3Re^mMdzX!P<#6eTL(c^QYpb8hcD+ zi@tNIgG;m#4=#Ps?#MxP2d;@){J^pP+U2yKGr>Uc6U~FrD1W-;qW#KX2)?QO2c573 z2jHG+>@e9?Q_qR_$!vhWvB#@R%wQ970l~x%YxSVXvvlo`n)58VQ=R3Uff2fm3kONW zX=o)=lik#f%3;ceUxE;#JFL)qM+m?pJ{(Sluy;=3^xt^&XkdOL(hOV%W!psBCl~eU zf0k)1FwfVa!Nj^?$(q;-Vj;bgjEQ&2)K-kvjEM*PdwBd2!~h&`*X`;&XMjl)V#0bR zI;q9F^fQvUTGagI8N&}tdgrlx(AZ!S?UJH8b;L)!ETzNdlekoRp&AP|c;^uuWNFo} zy2-V{=_bf0KqzQKwcm!is0360NP;3G?%HqZ*zXK26#}<S&e<Ma?{p6~H5u?qlqfMf zBs)##73_|AXh|qI+MJ|JCL_T;*b#7tKm^w0(T@#2L$%1jA~=tVZjcn6Mf7s=opx~? zcQ#UeW#oZ%5Z4-UIA_;=7os}wdg&x!mUvPx!;*d8@$kVT#706QI8zwNGg?%-ypFCZ z_>||cQSCvv)7V79L02J<kUPRtXdtV?s02B{L++sA6t0`IY54n`aixccuk7ZRdk*3w z!fI}Up4M`gswtmyxfobRD$EcF?tE87ds!A$4t;$I^juR|B>Sz>ug&aSRykG>x4BF~ z=||b9V>u&K!w$lq|B~iM5n8G%L5n65sbE2m_vNk1lpx(NrH7~VECB?ay7mT$Y;9o7 zSrhJm=2W141B)McXFzwGMpsspD$DT>7$xsrvx%~riLfAOGhpwiZY4AG|2pvfwqt>j z(ZCkbPTE2aMqW-;!dJ_@0*Wm5z0~8+>GL4)HLFk>3>>YGpE!pN`)2I#PdD_0w`S)S zCxRA4rz`biP$-I|d%5ipX&!UXBtv+cJ^;9ZwottVxmH#NEKwczM!8J4UIyY>saDpC zfYC6sM8+EI=hM(s^V|p~Y|J^%^_tRT!t=!B+~O3_BKm@I27kVBzMZS4C@mT;_ES)F zWBH#@$9ngzX6fnmCUr<?uxM}rbD|)Im+TmLvd)NSr}0p3ga$`Xpq)Pc(0NtOZ37)< z{@giLJ)#9CzPP%^Aq>7O*x$2PP{*IPK7(Cm*ndHlbI+b;?NmixBxh0Dk<49W9VGDi zOwOmQphz*4Si_^H`Uu)#uF}6tY?2`dj!A-jX%9Ul94#}-Nsh-~kiya++Cc{PxFH>4 zky!q@1R8*fg+O7EogYK3w<a*!nW=Z1NRBci(ytHOo1{XH$C=bbWGeIk8ALEw$dVhE zsF+OU3$%_`@C}@#O;kX<HK|phGhH2F=im|@l2@kpKg*p?BL;CObj=+ir-MF1&&W@6 z5`8)0kD+Q(s{gcwSjKWYrVQ=VQv9ei%UHv0{vQESIjzRr)LWvl@@S!Os4IO7#=9QT z$Tb{_ZBbb6>PF^GFPD;Vw#BSNu#$V?uQLBSoDN(c7=%rq4q2~m3cP}{iDxF_5W{Zw z@aU);L#`^#u5j()9A~i9)u2Px!Fz(O;JHw7w1sJqnn;Gio?~N})p!}*XgV|1n?}^| zdhd){VGnyEtbq3Itewavh+XirFy`RTu_?A{lziD`6sny0$E>b7e&Vh7L{%joYV-tz zB}I?%l$dh3eeqOzeAQ8jLLv)M1J0&>iw^zwL{eqYh%<U!E*ZnPKZ1nqbK4d}RRWw& zFL2&Qs|y3#BI^GF)Z7z2g_>y`?4ZyoY*sK<Xl^bU_k>MV3_>m-n$UoAT+rggq138P z;Zoxzf#dTy$25LKjPk8(3L68Z=*EU|xM-GNL(`}~1qwDqNR4+7tAY<F{zD&tgk*_Z zu?u^mqsILpZtT}6fmfZ1jK5uF3HC%rjn7zsr^EVK7oDO<iaW66tzl6P>p+lXbsbCF zFnMx<-x?9+fI7|~$p+bk2W*(cOey-ZH5|$-IU^tA;3N)z7zZnMbXP2thw?G>;_>-) z!b(6UO4*RN2121xgWndq$4QEHS(+ZNm-?nCD0qR>^KrbHlNKBBjYsHuJ8lYr8a+ct z&yHecPF!P`kFF5)UGYzpBPT2Y*41<F3Sk`1k=xejCkP-Xaca%Nz2o?`Gx!7ey(#!9 z!M3uKDcc0Hn8#;^G(07O+Sbq~V2mkroF_yM16h1#kRE*78u$cg8Qnk7Yo<AvZ8DgR z6ZyR<@G0XskFem=K6$Yb>{%Fb&wTw2*wY`4eVHo@b0kl$Y%X(1>yyluixR6q@Sz$O zp5bCv(R2f@Y(~BYIPU4Bmc{!h;^QZ;%8>@?Mp0lFx&c!bO*BL_oY!eB?Gai-=XCJd zN)zgAvXgG>225E}9s%HB><yQ`XY5mF6!cSiYCuHBuDR~3xmq2bP%>q!G<@S-Ds+y7 ziBFrsi%%H1z{T#v8!%-%#9>BR76lACPP%sjJ<w$ZgC|2{{D{eJcL+SzV)U8AFM=Zk zfr+T;?*N+25j?5;(7`cub#(?@*@A^^1c;*QG`B*#2Q&?l(BPq`N>zY#v!S@I0a><z z$TLdb7$z~cW2ZQN3a1(gAS8t%^x4=Azr6un76%%p;#$TyM`Hd6UiDEEDa^v3t#%-? z(bTNdWiw{}SiKz5k}k_+x__!ubf3hd!Wo(;Eo(dh(v6xDOik5ufG!nt7QPxsc@TWL zsGg7AvG)diSz7lYKzGc1E9{?e{!=<Av|TB*rPzUSe6k=jG*V`}V*f|Cm!Tlj$N!`& z1N?53MB5E*WtQchz$8J3nnnQ-4kiiMYf$(5<dH=7;Al(?&E73^nH|;e(lyU1a1qNh z6g%~x(F*yxjc(U2`e4ACZ7}tN0Yb|Mg^J1!4mvF1pwg0Df&$+zdIRnq_oOgDsO6xq z$=4q*SU=`Bv8nYL&XZaE2J|~x4y8|(wg81fl~T+~Y6F!v2$R-u=I4Wf3xb~}2nTJm z=}@>vKZ&$nXv`6)FT3asShGQ05azc>yAo7VQJ*YS(5!V*GOaVCv-tQ~d=MsUc9dfz zp1S%uFGg)2g-P2DT`r?R?si-$FhclLD3sBMgI2{srWK!Em?IcADbXNz`M|;pL1T@O zLDe~NQSL0nth~}`gWV9&M+5R~Q}S%dZBAQ#iz>5N=VGwuD16#z=<pg1o?Y*SJe$(t z9YApz=qx5b-{Q=x6AlT^&<xvxJzIo58~h;?Qt7Wtsl~%0!HW;Mr^AC;{04pq<m{Jm zt0=Z3Lb}3Fxy3`ICDU3x2LhPQ;<w<>Vh{yOgmehxh+GJ_%yBjWA`~@ViE1}k^U07x ztB`w?W>v)h>LC)01t&MmInn0u%+oMgkZ7xrXk$Kv2wm4$6laB+pm1V?7c2aD60LhX zi8j84MEiCQZFsk>VSa<FVf$VR?U6@c99laLt$fs9CDF!n677F&e@PDQGimmbRXbkp z3edo4>3d6ikZ7N2vk$adJlE*zBYL(yIJ8fs*+)_>7+F~V1NUO*oxN5#JdtJ}NVOh3 zg)W;HJKo+}t^Y)teIV6xXMs*0IQrH%;m|&jW*<nk)Kt~|VyT;OXrD;452RX%*iw@Y z>&{v>;m|&jW*<nkzG|xub!NjNek~QBNV5;5T3?!MIHB)tw_0ADcR!J4A4s)+G=v;W zK~g=pO*piVtl4``Ezlv2UFv97?11u%)9pvr>^-NJa2$$N6?kQA!l8X)&EAu0(S;U{ zXfn--_u$YzlV<NpwU`~LjwmQMn{a5KIkOL>TB;8wA|%GoRX5?#K67RtNVQmUG@6hA zs?|2(&^~i!A4s*B9#!~806$ST;m|&FW*<nknD{2(gc`i7H=)ozQ)VAHwb+L_+<mk) z-Go8=%$R+k)N&JpF<H3=yFJq4Ao`gw`@pCrzXoj%Y<1Em1lni9>;t2gBV5cMfy zKKo3Uec;n#SRO!yi<Z!y#YyusU-p4d>&C^6J<RAM%P#EMC${V(n-<6hu7$4NE)^HK zvrlB%2QF<91c@7G#BV~JeIm*}5NSK%NEm_I;i(r%vrjD9dlD_gT`--7d%`Y+*(ZYR z1B12+u@D;IeHXIq6F2seJzFtY2qWlfx7A#>5P0InK2m3~Z5ut=s9o(;8J0P+&z#tM z)@&Ia!btQ)Cy-6(u_t8MN5brAa6`gm(vUi}%8WhX!9H?jp&i{(^%*K#t&(C-D6o$- z*-`ejj>aI>unQmdjQ#q+kcA7?coj#(o_C?bJ`-Obxv>#MHFl0=7Z&UZ=k<XWJ37aV zpi^qQ@L$i!u8)k^abW7mddzalF4Wf(p6e4C7Nf|3@j^3P!z||G3B~n^2kX%XV6@up zuD?~n>j}H{i2^&Ca);S_x05nhrMjN*TA#?TKrXaP-G$(KLT7!VykeYPctH9t+}0B! z>oeQc!zu++;g;eqyw+zH>l4v+TqJ?tyVx!JS2?XG4Av)>t7@YZ^f;T)Sx?BTPXt$# zS{e6tu+#QirLvyzR-ehOpfm*(fX!~Cx=Ld`;jBIrTN7G$1a7(7U8S%-^HrZYt#GQt zYTdPc*sCX0)n`8Ic!g@fh`6|Yn5!o=)n_g%Caw`<Vpr2Xtkoxu>JyE%U?+x|Ycho3 zFEdt8_^D4MR!m;U9|XW2_hGA^P*a}?tTj^0BESH1AExRFE%lkaiaG44nmwrz+pttm zD5=lHRf5R@7cPd*0^ULq?VgYNOj;d(Qox>Za0%XrpL#||edMfmbVl5VxV#TH^^A!6 z%vbI3lm=d4+@5(KYU&ve^@Oep^*o?_Mp={UKE%{B7U~IE6+AcDSoRY4;iaC@P@lM} zBc&5S1R3M=eORey9Mor~DpCWD2V`{HKAhAu0_qu0H3~Aig2S{ABlV1adcsnLWDAf! z4La`o@KMjmr{^41piDo;|5Nw-uu)H_r)LD!GOi{5hDN#2_aUR6Fi%hTsQ?630ptZk z&7Wm1>KXC$grACHfrwL#fy+Kr)HB}c2|YCdFiQ~#OWw;m6ZMRCdcsc4tZz8_jg~~< z<-|Q<ot}_WbC#QE{C_pWo+KHlkS0n21L!nM`*FbW1Xoh`=Qy}gL1a8|cG2f$8PZX( zC$78tRclP_cyV+%D3gn{V1G&;K{G5Ca##;O;~izGtPd7OHaIzzTw#eGjd|i|I2tm@ zSH<@cext@HX4dK*T&s-FV5lR(H~!_AW*h3Hts;tmvF1&Vuv(n+$%Hp-{0LolAQi8y z42g3vJoRlode4q;1Qr$YQsdR(eyRQNkFHu}22;}pzl<_M_%4fbVNtC#%;USDho}D7 zI76-o6|Yvq@D0+oQ%^cny5#8^q<|I%YsI-?kij3u#&!a{z8GX}30|cOMKc?_r%H@# zx<-X;G{=Gt!rz9HMa#ncfV*J$2Lya6X>R-oF2UwT4nE-iB+Ev^ea<u=iov^y@{lkQ z&A0JJ3Xg`cW{G5cQO*ZYALk#f_%JiqUBkHQQgY+}IBHB`JUn3(x4s4nC1lsz=&#eL z7uU2H((zZ0ljVFb<I5-f9i@cv*NmzI8%1EN!?@LZ2yMKGYk#HpZxET71J(D5Oyu-t zk%`)0FEVkg1Ad>%#2@`zQu(7wC%1Qn14O3y@#2oa5MylPw_bA@zY9qlp73#o$3tu- zZ1JD}{JH_YZh)^F;HSC){_owNh4Cl<{;!k}`S<_H|G2#y{`)`w@%R7rx4)X~4ZrxW z%HE`J$llbyr0fmQd94e?T_6)!)kZQme0)^F*+TOMlRV!f<Z+Qq#qV)_VV@?f;#(D* zEktiHL`p>nqw3lthy*N?$B$DUb#S&&y#YS2Dq(bT<+0!-Z6n0~sDrbG><y7(gd2{~ z(!tvBm@gF_wcaV=?4f&uu9KfSjL?+-0PRQ2X2ug2AC++S5WWHICxSSD35E}cOB@C; z7z~F;Eu1}+Z@}9~QREc7&W%P#9x3spTlzaOoIRv($lQP{GvH#npT~$GaF1~1g?*=n zvxoML5H}LTo|CVrf8@icgP-(p_E5h8a6sb522VbB0+uj}rU}#ZRuX3q0UYu+Ab`|_ zOsBjJ&K@&y5BH)tdr07rw&8`gQM??I>_jvPT0QYW6=x3(94tyh77E1Z4<~a2T>({* zN_o`9*+T^fD@Lb!R81jO8Qh*I3RGI6+$!Vjp@W0{TqOshswe>`xdin<@JmP2x7s*+ zDB*Co)kSPgeQJtQV8=VJ(Ahh2oISK~uo>#uv$t_#bZJxo>QOa2HH*E~$Js*;hf+z@ z9s-<Qzc?_DSvGT2VY`>e*+UVBIf)fechC@-^o<0V4-{OaUb$1r*+Ucu0SA;Pz-wyU zX*6c(B24P=q?5CUEDjh1#Fs*nRG}D9nJl`7Yw1ZUXAfx{fY}Q+&HXU?u22IgvVqIx zNiAm&aU2X8h|_`Cu3m;s9Raa?OYKoGXAgZGf|4s1wtW;*pVSQ=dKBXMSv6-5ksQKe z3;G1;;Y_qt0%?uZjnKlQbj}`9IUJKD)UN##8apn?SUrgw(B^m)(Ah&VhrA6^9eNw5 zyiMK)TI1v<gZ)-UXA|8V%y=rSdq$Q9MbR}?!bn(=_a`x(P1JJ$1q=k$IZX2>AhY2@ zOqi?1XGxuHG;|u!p+ee7pIBJT3_*D8C%QVj2<ZSIM{CG&J7&GkN#I26mBiRbb)8M* zbiA^qIYF)p;T@q@0AADh*gEyR*Vx%aR0l9F5$ymO=94PZF`t0qK}{!0JDW)Ba7su3 zDXdo7C*=V|f*g+?>0WPV6M-F28Cn&(M(79EgvjZf(HotJ?rb8n0|->EMc5kZI)f7r zv58zJhu~g%XA`j<a4>vPsz%izd5642B2{A|yY3};HWA%{6=dNI4g$UYVC7Y%YNY8| zc(25>iS!P5CLJw5s2ZX6gAEylsu4cT;BWPKHWA?AN9{qF8gps{riK%yhEufqgDTG^ zGCXu{ZB2+8O&e<xfD4HlwfQq`o=wzvz|#oCdt-0=$-Ks4f(nRka32(UHqqt5nnI{; zm?)}p-3?14oN4uJBF;n0S>J@BQOhSq1Cl0n+(E9NDfVol&_jZWBU~Ds>wIuD8Yvnz zgcI4GO=Nlydn-*C8c9Dh#;7rop^;Qf>t4HO6R{p(rGxXr&rq#bOv4(uB*4~dy>qYN zvx#C4g69;;V>Dg*WN2j29zZh5@l3{N6WJa{U)x2^XAd@p2k^f3>9X*s<+F=+55wmI zDHK}%>X**kGvUJVXNo?%NciyJW5z+E@+1k2Gt<-wp!G9dpIvl(aOrd!E`y=bj|~$& zA==2&e5UQQjh0X1oLv|i)gPK{jjO(VEuSyIjl$0vL&F(7o1MnPF;Gul-^hY70fe=b zwQ;Z5^P@dE!}|f+$9?fegm!%gLi;17cSE#4%9|nD{tXcA{0@Y6TLNgipNFFHbp?D~ z0bf_ZkG}%GI2ymj(Qv=X(a7K6Xq3MyN26(H;y`&g=HBSPq30%j?j3Ps6Q)LRf>*|7 zFqO|G4<kpHRZi#|dk{7_{%|VgHUX(Uh}|-z-1Nt}XL0O7+radrR@HH9$9}5iDdZMU zxEy<sH#j?>&;WGYbc5NU@~1*>?S#;=34;TJgPe=XxXmTW`0kkULB?$^*Y0>7n-DqB zcn2c#0l1l=7p`OL)%7!O$0lSBEc_amQ;=|r$9act1SQ;hI$?NhLg~O>kYb$>aBGKn z9?uaJ-1-^KV-sEnwt{j;LclE_pq0wy4j`-HjO?)qy93{phMA(@RzKlFN%Y%vXYxS# z*o5SPt)OaHfut8FOsimuR?4Ua+6nVx6RroAf>bqwcAG9pD1Q<~yRG*V{>LVa4+_r_ z{Q>1R-5fWt+X8ojIi1o#HsO6B7mJ_5ZBxS=#x$}O3<}nIAcJf|{=k@60Dcf|o6BU> zmRL?f$|{^OLN;N5Q2eJS*$W2M6>Xz8?pzC2m(FM*o6tZ+Gf&onSszqrRbea+YD+6; z)R0Z6Al&VU=-g;E6#k55Cij93+?6wa$R>OcL?3z82e8f3r%Ia$x0TPm;4MjH6H*AH zpSo>X_c)NqtD@T$)e`1`DzXVRgwkQG&mh~TOSIBtMGn`uUe5R;oA5)3M2J<Ef+2kh zL-N}vND`c}MmAxIa4G0zC#XK`)a2}R$~t3>oGb*B1&oFw#pp6L(?(O#q5KOHqWX0` z&_I6lLa_aSZ9w?}l1=d~>wsfB|IU5DyB7i<$-jOh@X_9|5eO?Af%FsY1ix;9uUp{j z7TDny_*x78##%7_=2ozLV=LJIq+3BhH}HS3H1(;-{sxt4VF!!xrE7szsLW%1DdYHy zK#8FND<G+BvDd3pLFPy-H|BpKu#!(~RM3p4EbzZ*D5D<Ya^y)=)TbjQrhgv2c=U=e zW-DH*T2&i<^}HN=<vtett}G`;1>mEjp(bL-_ZCSnZ>S+P{@&HtTlF$Vm`BD5EHgA! ztiePI4?iqtO-0iBdh9gT3Q2uc5UEcX){MNQ<1n?;8^=+Gw6&u&S~)V@t%Z2yLWGV7 z#Bb;EHu9^FlY%RzR!ftQxSppusLw?7MYFQtM!}CFqGQZlj9>HB&?6(Rr>W0bxG$X( zGhBw;>;yPa<au9$qFG@gXBT>1ev_Dz7|`ntGhU!v#78CPpsy5nv*Y&>4A;nU{XKOd z$Z8~VjqD*L3Yr~#<0$vm$FBslze;F}-3oj)O-C}48tccy%8#7;xN|c1Poi~%l{g=z z(kz8haf_{m4hamdVY6yTj=pgG0umR0zDVrx+Hl&jUx;=OYFf?I2y-1-!X_?ZeWtAq z{jw^DmcHk!>z-WAPz=BOYO0Rx3%dUHln;W}7kOS-h&%c2AZ-GQfF-ycXd<nqsuLq( zkg0_di(@1p5ZsWGsz?QB2SCul&;e;NRe^?R0{F-hDCA&>Fu;Fm<dtFq;G!5h2rXtR zw)R!|_mO}JIt$`#kDn>ZVTZn5AnwN7$xYxnTnoL7wnEWOA~ykkPY^6qlwx3nPB$Lh zK6avNJ}>5JOxMy?DWl14ILh}HX$0!I4n*h}qkUv3mD*JUc`;4LCvH)}d>VNK!~noO z7mmo$l8kH9?rJQnDT=X>;3vg7R?!lGdG>f57UKHgwrO{{l*JT<>lL?EZoEuV2tdt% ztH}|l*U%&CoYaZyGYfq&MX?^{!|AYvWa#6baWGs9N9YyZ5@m&gp)p)pOwrNS1;IJ{ zd9KINacZwki35ZVQ*vlHr*7BZmBkbd;{m1AS0~09C~~@e;_1`^AfRU&l}<H89@fPS z9R)e}?YKTjHAaD0J6+_pV$7T;!FJ6MB3u{q6Z#gE;j0`BU48*%uV4<X6TA-KOr%7d zFLw5Ibul?{iHLe&<#BG~dVx}4@=05Z0+ht45R&C7S<FjF?PQT_tUS(`4}}YSJ>@#_ zC<aE@<F|4G31$e3`Q=atRZ3k}x)D_25CljrX)_#AFhODGQETb#zybfs3VYYa@MzoL zw=ldh=6R*lQ_t^S7lt`Bd)vA&<o5b?Vd(x1`@$IV!oo1lcKpwe8~C~ezAk~UOJIvj z;0reJE!Y4rDe{R4a{5)|K>Y@Cz`H*wbYM|!7@LmBwMO(R=!G&gu?US>HXl`X+6@EX zzKPZ_0;kVC@XS>vGp$||B<<p~lLxpwGyyo5u!+<#c~~5+V_7W#2LKx&L>+M#%lcYD z#8}Fgw29C#5f4cr@Lm=)2oU`sVC9M$D5Gz!MD(ZTH<20U5?Ju>qXU%LIzR(O)YD3g zx{Bw{653dHV2$e5zk2V28Axej1{`>l0jxR6TCfv{83=a5_cbg>9-~4AyvzrHDT!nS zC4~$C=F>?X8ZYT43d2Mt$#H?sSe{6pC@PIjCSs5y!weFH6}t^bE$fSS9|SXybn)JW zQ3<wL)2p&@ca3Xi6KP>?-Lq2qyl&lLx|g|iugBvQg`hY(j!k5Rv2)kOHOP_-d>j%o zR<Hz{117wp<5yAJE{eifmC^tKyjrhVftY3A-sFOZ(gh}C1?0F*w1ly3FT#}v^oX~4 z&WdOozl&BCDbR5z0{CdB$kEF3vRx?fKI)_$lE*aG!k_dxvUUV$q^lh)rmf~D1lGH7 z{0rTYJf&NBO<qgVB>{!AOR&_7?`|0+;8buu3UrME-<~Q8F=*gaMJdh@M75|Nj5T{n ziV*OE4Fm~|zXYe6lvpPvl<*l?s=kSOFhvP^mNk1Ttga&b922bs^pw|8?l#JbM7wfp zQ92l5ig>L<UC2{CnxjDJT`+AYwjqhfLpF?jF+pK0AWQaw0UfvO@W|wrJ;h_?TPUDY z?S$=D6@&4Ct9*0p>X@OxH-aJ$vyw2sc^BQHUbj&TX8$Pw0hgGe5QCt#(>2H?*{LE- zfPmjdCKxSTF6z}IY*yUs8J-PMtp$;wMHRXX6LeK07%g1%>X0N5Sfr}2qIs4l;;T{= zEjPonvY4RbO!us=RN8XFDG4-YuSPd`h$71O1V2hU*wmt=6`Kj9W1sjgK0Da5$2zf% z5rmUbnmI#z??p8$WMhXH#z?|MIKZf9L&xAYm9A^{0Js2zA(CO-i|L8NmJ92uE&&Y~ z#&Yqh@`viy!5(H51j(*%_KPZ3xI<8}bj}qKHdN;NWnAXIYac`m_^1zRvU`9Q1+1tS z2DA%KF*S!O<P<%Y^Ao7vvnM{l86pA~m8<a37jQ?$A?75pV(-!}@hV882EReiVQ#P_ z!7S#agJOEVh}$a8Ht^6fjj$C6QB*-8Sc<RsMXf4a+9`@TZ056*JG;;X@*U$&0_?&V z*YFBl)T!!KG)hM1iH6*${d$98u(%!3Oa(cM5pFgNdo3ja<7%GX$BhrpC_=*MeDWM> zQL3cM_}0$6-Y+Usbtm_qQ$uXD`x&;<jcQ<+jyJ%*IJq@sysV`pRLsT|7l>FKcU)~S zd-pO&siU+7XALu3Lk#+&4i)Fdg*NK)up{f*h&suQUHyo^%edCionuje3Y!J7kEpcv zgmdFK9^;EUp}ID)O$&Zhfp$T!rDBCsaAwp_PI14+QG39hT&TrKWLZ5tMGVzI*HW>D z(TF83E+bV-L1sz$GJXnKJ(O`HaxMfIv|Xvas5CW>0K2e;MHSw3yfz40`UB7@IOoIy z)R058mW^<*!Y&OoO1h+v&I~a(nazTXAhX$23^`kCxmX;WUS&HhCPg$MzZ|E0H0+fW z#zaQ6S=ejYn2S(?SZ#>Hq=O(|0gi{WO|U~mWEHkuW)zUMYy_=?QfYCNtRgm+L72cT zA`|=+qz>`^YN$`JXgi)j_mHq<D1xiGMttt#ggw;855^JDxXSI|{EMdJ*zAZdotSN5 z+=Fi&MV25Y+2ps;9ot<Zi-zL~n7_cNlej@b5;^t@mI^Y69VeYs=kY5r<ngT~Bs@js zTMH(FZuA`mb`eAbL?qFMVHjoi%q&8G;Ejty6xF||zk)aE(*@jA9&B*V3svD^AZ-@* zT0WxMX9eYzGl{Wk>Z1vhgHGr7%<*e-c9-8G=qE(5Aw=bpU?ors(JPL|To%T-^b$?c zqus93uL6F$7JhzSekhqbI`!)LwOx^7CD?xL)lH>H7Q<>#91hCpfxW-PS&%p{VZ&z8 z?rX0CecUyBku2$~p&*M;#;-6ylM|!M(cC}{0vJ@g=3o)zqlU@|n(Gd#=#G{K2ZeMj zU{2Bn2y(lJW)<E;({_Z+T@xPnNa|JR)~rD^9Y2AKh9NI%ph5#$4+qOUG`__FFwT%i zsS4;l#(#DpFDj;vLbV7G(8&wa$Gw3r!Kokuz+mI)96wrp1?7s>P0)Kl0w`=;v26*D z46Gl^w<>QJXl!qnXY{VWgJ==UyNMRZee_1pwl|ozg&5wV&bT7a7}`&x()x7;d|d%w zSHK2Wz!%r*TU@K+emB*seT!=Km$YsaB|k4js~`e$4THl%p;PJiK(%TRLmdrBYMXdB zBJz@yY1PAA(AOwTE8!Es#e>5nE`s(jZX`IF3MO#lQ+?Jn{wZ)n4^>bhT6I|4HPgXM zS&lqVqY4HtGO%rcEm7dK3y`xGflMMp?8I5ya^Nv_VOe!i!Q;lWi5HCwUuqW|`RG}F z3!8X0GIXKkffFGsWFRLR^-W8iND1njVnvrWF>FNS5dR^|DkzaHDOV|J=SycVWHImu zmb)wiIovEXtELGFW>4haKFeOH73KVZ>#>VnBUyP>SXQIv(A_<O<D8q%8=eZ$uxQ%E zsuA14&iyfjljJHX(7qLxm11l9sGV?iR<PgvCO(Y_Jx~o7piZGf6l^boc<U(+)sJ?i zQkK=(;W<|!Z_xUZ(6I#Sy2y`C7Wtz+P)KL?BFh@=aBr)yH*6B+z+`>xa}roMtRz+S zDT%Ti6n^cLpF>$@i6yF3f>>4OO1~-9svg82m&E6@=DLYHBPCfW1PW%AoEIc+qTKvJ zaH|K|s@st{;yT%?3r!57U~{oAKg@WI+EniYxNoE{?2-4dWuzW!@k+PSh4ZM<DcUu) z*?R;2oouUUH)@Y*U60j!Wm_rMDPeAe4Zna@Jp}#4!`YCs6w9(2Ya|cPk!mt`{A5*w zv#fzfRFGhJ+G!!gwk*b)^CVlvx#v%y5|mknbv&q@Y^$Dk@nQ7T$4HZGHCO!@{C(u9 z|BT21YRF+^g5Si05l5qlB|O+xLD&7RbKS2p)qT`5g=8x`b?}sywOHxLNv@S-qxVwF z#Dm7!fM6ikD%)}LQ(2Z`4b)!;)k@BaD_q)rgz?1>G9>Er1g@dkWu8{KR*(Tr%C!<Y zo5L!aTYs<lIcj(yn@5S$?Y?<-%R;OPb0Op)xhRlcm$hjSmVSqkLM7MA+m%~+m1@;Q ziyBlb0u$kokTHi9vsX~WQZ1xfMZ4=Tr)42ldTey?HYvo4c0XMvREG>PF!CtZZg!{S zvaG`zVZMTuHY?eN%|nBvskF^KTr2(`VkN-KD(uCy1jt2bRvabPgnH#u1GRfXaLKc> z`_`)}Sq=^|g*2;3XfPb9Mlm(|3!(VlX=ZJrkOj%|A|xwBJg&o$;0eYiCt_KfXk$6J zKnGH+5<7*ANhr{Lu+n{2%GyI03j}Wj1#TldvkY7b8CKE&J&0uOA&CVYVjgP3;6YU? zBNrP8RyCaHW9^}a)j^C&WGl(Q5VqVS%6wIorh8?qJ%q4YRX`$u!8jlhG*d9FWH@^- zinWIX76!&y_*Ic(!f|*rVooAY{ve07hxiqaER+FY$K4TT0OAZxr6OAJ_exlM2w&BL zX{H!6cG-}Oauf`M7X7md)*hNy6{V+V&?<9*egOQtE_dF&0@faSSL3(OsTzgba2^T} z1Bkh|6ZNY-#I8yfiOvSXF6vWsw(U|+M6dRcxGI3MM=pa?CcZYae8SP_Cwf<V$Xn%6 z5Mi)4-f9IhLmR4ro7qIA^Iq&~4{586wRK|DiM=5<A%P#e2&Uj(>1q#QtJGDOHL83i zj2Pk10QT;Q%GDm4Rw-jDOrnoGF4)GmjEg$B6NRfi^sJ^q8UO@bGoei>0d-!J`Rcv8 z)gD?_;Gp9s=Ej}h8sxBosC6rRuWYr6kQEdwydciyfx%F0yzO~f>fWnbZK7cn&}1n) zi8FX;Y~fuQ2Su3D_nKClNLU3<)fu)=LaKt^Y(bM-)3cz}F7j0!1Xy8m5LSZyUF%>c zC0X6vt6A+LUWFqoteHktJgli>13Re+Xy9JTY8T-uY$Js?Q-xC%M{UP383*xqGFF?2 zR*kotl?brIyhLgTQVJ$ZTzcF|SZyL$g^=V>7oprWI_3fYOtj;jeAOmmRajvFo`ay- zc%eZrXb+*(=O5LpHqom3fSg5^6J3PW1mZ6D8ZJLbS8XCxMMKk|w}+gMK1w{b#bYAg zN>^<nRE4(6L6HCivMIFyqA>}tou%rnbk!z8RhSb*eHepOA=EdMbz|p_<4(G26QL@) zu|r@B&H+)H8MrB9Hj>p%@lLyH6QwE)NC^wYZNc##!F3MxE0lAd#H%)us>1Bw6SY0= zH&Kbe)|3G0(L~lK`KnFCs<0f2oIQnWOwy`{rlu1Vn0e5!+C;GmEllD$!PPA1Wc&&u z<tbI0?N-EU6Ui!U$4ihIgHIZEkv<326`%Jz9ji@bs{o|{DVmZqPR>b!)wY1>f-08Z zNLg(nTty3HzB}nEX_=;gQq;ckPReQ%;i?ZR6O~&*C8Fxd!AQz+xmB~;M7s)_@1=_3 zmdadkL`i!g6kGL9&uSC(DtZHC1})IU4@`pzorepBNG#n7T5Td<#kLW$GHtGsJ^+Q5 zabFiT{yRmhP4ufE#^Hl#_l;+gQI<f{9iZnzdy=%;M8OKYiN<TNszdSz5dZX`3PBAK z?p3We(Xc|_n}X~hbCoj=fkX`9G5m>-!d9E;SmA-EauOVUvwU$d<g##yatKe_R+~s! zF)4xf>-d*SDpoBC;rT@&)_Z-cP2{Wy2nfVrIK9JRL{@||S8IDxxY|U}3IH7sa6pGD zO_IoEvIdk(N{<>>n@Cy_c$Ns&sa1-r(cGiwVOtTOM6Nc`v?8n$#{yzB+8tw~Lq2O9 zXDC1ETx}w2Mfh+AKe$6}Q@=SdXK3uly&}EPoz~SR(pJQpC-CL^xHWW937IA0Dy#5{ z?&YpFk+&kHf}nw3K0kT|h!NLdd{VsHMB<9{8;3D+uj8D{y)xcW%)ZfxR(U<|l&^M? zx&pS9B7eQ9P}Bzn_<<R~KtlfLM*nIT!7GXnqDJI!Q|=akpdr_q5<$;TDp;@3y~4gO zQMneH^-jJZkaHA9%#>n#RKxnQ@>g${z6o!azDe&;zNzmQzVUAtzKL%TzB%5F*9zab zCE=U!(`a9P<^Nayf93y=&i^mrt8WQk{eIo6ey)4<-@N;i3SYIn2n;Es!2pS_tAPcG znVBnrBEEy2TJ^v=S2MU%tHx17c|LLj6Ghl)bh30ZE2CKnc5!#xxPd=p+*9l#Q+1)) zed+FvBbL45JR9}uX+O>HZx(a3jTvrs+~TG<qsD&+d(G}U8|Pt+3V6o{RvtIi-gB{2 zE9%B+2)M%3;fUD1WVuDG!Zpr5FuJG~GHL$^yNa%YVG4R@oP%H*Wx#!psF{S>TPC4* z##x9ZGro4bxjgvPfNx;<v5gba>(df$!sa1K)lfFf8E2x)a$8IbUW`X^MpCv+$U`sK zENuPQsN?4|E@vc~W~8^xNI*PM4<4EuNu$QD#V~5yj3k_v{fnNFsAt4kmU`aXCL~rW z<J)?cbt0s|rzSSbLT@yCrco279Q8XpUf;3U9%<WD#9kR&Dml=gM$;EKn;z(3n}!7d zWxb^vv{|_`OolFN!8QX+U;zRyx5_8*A&<PZ&0@CAzYO+myv*VQc>HKu{lh=eG+@uC zFwc(+m8wkKf$7PxO+P47#&*f`I9ILy1HZl<hSN3yqr50nH6asF;|=SLXq$ggh8<-N z4Qh}VFqjSve6-C#ps5Scy!rx`u2BcmG>o=sIHC*0-pQwi7HJW5UbYO{xF9VoHVQfy z{?{nQhVQ1IIvF>JP;sjB?ao~%de!t$C)-p^yp=zLF8ZO1>HU#xGZDM_+n|FyEk0Sk z-^n%)KPq36s+tMS>)6yX;%@wvDf1xpM$*>G+g^6r8;2}kw#@fCIsMYk^!?7ZNeGRj z!dsf(I_e$WO|R%|oP&U2#&#)lP#XErd~gD37qoE_Mu??fj=>{1YB(O%w7ts4d6+yW zcv((I(6`Xg57jsc;f>M|uinqT$^Fbhq+Qg;Ihf_UCMMm5Qj)y!Y$ssFI16***&t~P zbOzfAdNEGFB9s3(M>0?>E<$&<JQ>FMHwt}x;~=d!bd?RkmBzVOxi7v<8RD*o*qN4d z)j0JUCv#rgK|okJAGIu-t8w170z3+$jXD>#oN83#yn}!&T2-f?xKirl<ntQfY@h$~ z*;B|Q2==lg=fiVB_I7;j1aW*n&gc)r!VZv@m%k(RBxk)k{4F`bF$@4Z{;%DGw6F-- z%Ab%e{vG4jllfBtG?<Em?ENZYD;z&p1TwId*c?;j7K?TZ(kg%K)9;7T{5nHF%NhE@ z`F#uLhco{BfPTq+1L)WPq&UA-8vQe5j<7zT4^WiA{EBG@y2zqG!}%#Vac<hPv~(re zPOzFe&TmewKJgfgJD66Ni!AyRw4cW9Z?JPjzZBy^RxpOoN$rk#+ku~hEz~aL`R9Ps z#QW*00ajM@gR*RxvT;5}#p~2hC>rr|*|ZDE#@QGqHh7L$;O@h?ZTEh~I2&PIDR{-Z zkL^t}u^4Bf=cMx^9T$k4Yqtwi{yAVTnRpC`y6jsaSY(-t#W)w;+^F7M)tSCemM>#5 z&PC@XYJa`QDJI*gi)|+MB1gm>7f8M;-p?`>t8FUMoCAG{0_T!W!);;1)ixcQFeLm@ z@X449^nT0zPPI)&io=dSn9riu5q>@0q#2OgpWu6$zw2<Fh{GT%6hD5}bu=>X8&uTx z?m<O}=?#O5*1m605kvY(hFxEW=%+bEU(>E{O}lu=f9JT%e`DO0{-onBXT191Mp`BT z)LLQ9NymCKPjO=ak&BZqb%Y%R15BqIJNI1cxD`{DZsV3hvs+>UigDaNvM-h5m_aw~ zt_=xO%{pj>g---C<CYmgv}C6lhaD7WU9S%xJDV2u)+rgP3<8=WuRo7=uC{n!Yy6~_ zrl~95OzskLMw=~8$2xG52s{}acUO0DraVULyo_C^8gm%)F<D<F*yUxg&dbC*aNLAb zGP28EUXtZQX`Plys9ruFr$muE*k-x&e%qMHHG=3$xXsFNU=+MAN_j?=y}j-JIA;HE zYneU{hmZI5OdqAZzGwRI;X4rmKTF^Jb&7tTQ}oq%|Aw3tzxS_p-QgQucl_$QAG_|t zWo5M7puM`|E)jJ_v|Q=Dy4lXccu1CQ)Jq%f9H5(S7woGWY}uv?nf%YuT_@qYXVZP^ zt2^r`EWd0@$9Q#D9i#=YWvsrqqgE1^Ek@ww-Lwa?p;)pKU))N2FTx0xLyMR9(fNkL zY8A6}4rYZ!d7-R2<8ZCBFDuG#9AJU@eZ1l$TjyU^cB=2kG;5w`!xW6xDOkKJx4HST z_1-YN>!NiMmZU;2$K)`?tM#iAt<$jR`%M?nk05GMrmuIjPQ{|K7$MJTeq<$j$)<<e zIv2rO6SpVXuE8?#Vk)wetn;v{@MCCmG@5vtZ7cs|orzTy#Drd{Fk9Q!Dp}`Y?TTGJ zmfczK`q5C4Bw43o(?VM0n8<XNEvD(XAX%qkixX8RC{TJQlJjhMfn@7!Z0W!v>dv@E zVLclz0<(28wlZO#j95A?Z^>24*16c~A*U&aT9qqJ)itwuF3yQkn5VjF_vP#sm)SfO zby1npJk&iY0m$-e7wcr~Dn^*f++Fr5=4LoyD%RQ9y>L3}fhTZD4oS2OJK2XV8QE2q zX~eA$rsaaWSmz}CJ+oJ=jMoOi6e?4idB_@NoPa7XY^7q!46DvQZ1ypEY6g9HX}><I z<)dC-kdVCpayc?G==0MIE5G`JjEri<Ff%fW$ka!->>g{ezqkb%<BpL?EQ$c}hIdJA z_JIp1CCtPe^@+r7TDLZ5|HR8CM$9r(IvigN>(*vow>DbmXila%NITsh;qQ<3MX1oV z%aLG7D-54^fF<>}<4MX7fRnr)O_IKo#?gKnNYd8{`YBG(7aZv~;7IK^;YjftaHQ}B zM|waY!3RJw!ZGcMN4iBHA>1OSA(nFi+iLy}dxVJRNWfG;(YBC3;75p8jbuWz>^yAi z_gnZ8U=TX?3e}W1W1Dw(03-yho`J#Eipn<o?f^&#S{L^rZA|pJ10W%2f0a%0t^}Vu z^bzb8nto-!DHiDlc!Y4(uSanadqR=!phpN=M@4>DAX2_p&d=>|g2<b~klanQDyrg5 zjk2t9NROY`smhz>me?GHbPG8WNj*V+1yekaIr!l~9No+58NMRc7^GXk(T$W|w?u1L zBarSOM|X01mS7&^G`s^GQB4nf2FufIoP#%DBbCv^LdUQnG0wg_oRJ9V$(qFSa<&E@ z-2sh6Ku=^JOjRF@^Y0dBBpP~z#+Zg|oPBo~Bhk;3{?9aM;|#om7>RtIYT8>uVT{x8 z7GOm2JoNww+v1Jx07fHZL7AIc)mh_>?$AY-Us_fLx{bj`x5y%D=80sT<w-V9zk5{C z<(DdB0#n(?V$Q<6K@?qnDP3=B!<N+?U~~s3;w%)OIc<w9x`Ppo7PjH}OP1G#IjHC! zM}$$Rj_C|vzw!b)<|l}V<4~E><!uo~ckrPRu^8ZFOVC1n0UPrZI&}FZW;H=I1Y_41 zkTGvyL!5<Xn&hU9QAGEUA<jejp$+Ha<^ZAxxDaQeo&JlKG;VV`<~>{py`Y*XTkd*Z zz{tG83ypV>mq#^=*PM;1(&>JBQ%gx_7qB@W69>6X>~+3o;{##N#B7KEw4(F9*UQ;3 z`}zx*m?BN@4_IxL8KdD%*Iz)zEK0>xhG2+#X_&p;{RK=+`P7I0W@u`1Z}@z7>qM+h zhc6Q=8IzWyhJ{jp0U1-6jaB<WOh}_`MG82Qanw@JeT1$vQkk4v?zP<u$e4oGaWfY) zVM?|(5$*+iOg`W-OkPs1>h5d$xnID@B-$wHptNyfVECb5z{Qk-!Ms0?L8+y@sku~7 z%Q7dglSNPo___pB4H^FeHl}1_pN~xCKN-8vnk)7w9aGgH08qQw>d2bQ^-R`73X?>t z(>z-vW&bLg$>_VX4o;<$F}?S~3;38^(5FLCyHA-2yP#h{{`ypf*XgOi0!`(!!VAb> zte(=REVb)Wa!1?9%L~|zRL}MYtHvwfz))lD1)NN!WGW-GD;3Pt@-w<uee6S<Kj>p$ zEminpc+v4zZFmOGp{nH*8PiKH6P1PMj8b9I-SnZ1<wZBQ3d8GkHPZpq@-B%lcqzR) z!A>S=a);iXr5Eoj7@6cZrh`hFRP?T#(;I!V=T}aRbqQe3$MTQnHOBgGR$6>FDJ{p> zlhVAa-^WVxDgG?P#;+sva~z>BX5()#8|8!lDz!0vgWA~sq|`>^X6li5Aoi|Jng5nL zxyGi&_tXP10G0<m?aM`Y<Q|CEvV^^>9O@(YKw_2?;!U+hjs3&#=?7{cPK};!3Y9lb z$a?|;!tQJZ_oXA;&<_}{RMI(Lx8)<;G7u!>q}F$a9iMUA^uR&54LfYiN_b!)NZ2x# zaT_b5-V+hz@(`iGcHLDE^?`^WL90lhp@^$-D&7+j<aNoM{~P9_F)`tei6FIy_`*h0 z47zP5-f<BSH1EJ<3-q_m!aFX4l%`(Shn9p2+cdnRBOvH-RoPraq?Iv0;f{`gn7w}J zVv6cE)@8XRBtVCwtJH5Ym2NdxXAwpM0<MSXcG*__^p=u<m=yyQO{e3<TAk&Fl7OJ2 zx(xJ<wNGz22?*IK`KDT)4P%zV9Vr2TZXK$JvMEL3j*)<XvsO%|S4K5gXZd6#An2fC zC#LMNYOc_7FcKghqIYxHSmX4LkARrVq0(X~6l&cS-mno6bC@D6hTqwko-oM>nBwWN z@U|onW0e-=A|T$(TFz(7>!O*9wB*kNkrhnf)3)4%TOtDHjrxdq)2gW3>$8NL`|Y3! zdCLl;+Y7Wz8Un_4ics^;vdFh4Cfu<Q5OtP?MKdL?*bB6Lk`NHIx7UL2_8Kkmv;GW< z)HUTnno|<)I0%S3yujnYoRo0OK)|R`rMA;`V@ARq{QyCSNwl|w6FYmUmdQZCsIe+9 z%Qog$DYfX`;eu{xEsuw{S8DmBA6$Ns8Lp{ekvSRRj(IRb7Pz9JS&=yn;f8l``K8s1 z?TN70Xu07VjCe(cFKkOaxMvz%eyLawUV>roXHGl#<QR-_ov2h>rd)V|Jj)HiV6=jy zF{OQ}2KT&z%P(2}dosKw!V3gilu<C^O_z;{1@}aP%P)$;&9+GQ7bvrQvIs`dQVE$7 zj_pYWpA3S_FR`eMt7REvPAItN4~(e2C4b(YNN~p*xcm~bYHFLZA6_8Ja>E$F=1+yS zEe~^gfhfx-T>$MhFT83Ou(=+~C%s_2)coYtRuA4>ktJ{{tBZG*ZpIPYOS14#)Wa)U z#>}=szd)BoW^jG-Mr9gjD<|X4&47yzS_G6PKZ?%j499Zb+zwa@POQ#UcO!V+QyI2W z=8nLnsq@7wL$MZ?ouFm8pI;!(A~Gj3RD@5rye^hGY3>PJwZ0qQSoNKvkm+rGN6bBe zt0Lfo%I^U~Co<uNXWLw{v$g|JqBO<~vG8PikC^LrHm#dtI5fZn=bB6>&*tKtE#Q%i z(;W&Bemw2b_GCAE>v=gNQ}O<zh!>ElPPjMM@N9*+KRF;Z$N<c=xiy#YY@FOiDm*Ei z$D1CGnqFl*f0QCEs-m@J2~}U9&>|`pQxEe4&T;{9`){zIUt#JiF{D`!b>enD+|$_e z?M -ppdn?cFTaV?*##8f*DZf6m~37K_)fBlMFTp)V%%x0uj4;-o?oSoo_{=q!Zf zsQ>Q&{cr!{<3IlPkN^CSzy0e!|Ly<$?Z5n*+3Vk*B7WpZ3;RV#(of$qyG#DY_e`VS z7)K$E%5Iz)e%eot^)1!=w|~C!{PX?e@Bh5}x$q5s;|Kf4uiQQDGc)+l_j%)wjFNx8 z^P>Hz{+b?h@Q?ia{6vtBQohZ6i?f*=_X+-*e|i25-TV%|_1pQQAHM$T!CE|4f&7E- wJXN4Zj*AxZ!K<gLot~=OGgW`snd;T_MCH+{{E!p%FaPiV162hE$fs5X0BxS@djJ3c 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 206/340] 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 207/340] 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 208/340] 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 209/340] 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 210/340] 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