From bbd865ddecdd1db88ce13414b350a31eed78b620 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 16 Jan 2026 15:55:23 +0400 Subject: [PATCH 001/348] Stop calling hdiutil in macOS packaged action It periodically fails for some reason --- .github/workflows/mac_packaged.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 638080f6f8..864aa9b0aa 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -172,21 +172,15 @@ jobs: $DEFINE cmake --build build --parallel - - cd build - macdeployqt Telegram.app - codesign --remove-signature Telegram.app - - mkdir dmgsrc - mv Telegram.app dmgsrc - hdiutil create -volname Telegram -srcfolder dmgsrc -ov -format UDZO Telegram.dmg + macdeployqt build/Telegram.app + codesign --remove-signature build/Telegram.app - name: Move artifact. if: env.UPLOAD_ARTIFACT == 'true' run: | cd $REPO_NAME/build mkdir artifact - mv Telegram.dmg artifact/ + mv Telegram.app artifact/ - uses: actions/upload-artifact@v6 if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. From fb90685a7c1df2636ef0eb4b61adb6957b9a4e74 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 16 Jan 2026 15:58:32 +0400 Subject: [PATCH 002/348] Add tde2e version to cache key in macOS packaged action --- .github/workflows/mac_packaged.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 864aa9b0aa..82f2b8490c 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -50,6 +50,7 @@ jobs: env: GIT: "https://github.com" CMAKE_PREFIX_PATH: "/opt/homebrew/opt/ffmpeg@6:/opt/homebrew/opt/openal-soft" + TDE2E: "51743dfd01dff6179e2d8f7095729caa4e2222e9" UPLOAD_ARTIFACT: "true" ONLY_CACHE: "false" MANUAL_CACHING: "1" @@ -125,7 +126,7 @@ jobs: uses: actions/cache@v5 with: path: ${{ env.LibrariesPath }}/tde2e - key: ${{ runner.OS }}-tde2e-${{ env.CACHE_KEY }} + key: ${{ runner.OS }}-tde2e-${{ env.TDE2E }}-${{ env.CACHE_KEY }} - name: TDE2E. if: steps.cache-tde2e.outputs.cache-hit != 'true' run: | @@ -134,7 +135,7 @@ jobs: git init tde2e cd tde2e git remote add origin $GIT/tdlib/td.git - git fetch --depth=1 origin 51743dfd01dff6179e2d8f7095729caa4e2222e9 + git fetch --depth=1 origin $TDE2E git reset --hard FETCH_HEAD cmake -Bbuild -GNinja . \ From d3f2270f22f20b2cc44ac484777ad5ede19908dd Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 16 Jan 2026 15:59:05 +0400 Subject: [PATCH 003/348] Switch rnnoise to GitHub in 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 82f2b8490c..ac7c8145ba 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -92,7 +92,7 @@ jobs: run: | cd $LibrariesPath - git clone --depth=1 https://gitlab.xiph.org/xiph/rnnoise.git + git clone --depth=1 $GIT/xiph/rnnoise.git cd rnnoise ./autogen.sh ./configure --disable-examples --disable-doc From 82c6e50753fd09b62bef15feea62d37bd63973e5 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 16 Jan 2026 16:07:08 +0400 Subject: [PATCH 004/348] Cache only installed files in macOS packaged action --- .github/workflows/mac_packaged.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index ac7c8145ba..5c0ba84aa1 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -95,15 +95,15 @@ jobs: git clone --depth=1 $GIT/xiph/rnnoise.git cd rnnoise ./autogen.sh - ./configure --disable-examples --disable-doc + ./configure --prefix=$LibrariesPath/local/rnnoise --disable-examples --disable-doc make -j$(sysctl -n hw.logicalcpu) - sudo make install + make install - name: WebRTC cache. id: cache-webrtc uses: actions/cache@v5 with: - path: ${{ env.LibrariesPath }}/tg_owt + path: ${{ env.LibrariesPath }}/local/tg_owt key: ${{ runner.OS }}-webrtc-${{ env.WEBRTC }}-${{ env.CACHE_KEY }} - name: WebRTC. if: steps.cache-webrtc.outputs.cache-hit != 'true' @@ -115,17 +115,19 @@ jobs: cmake -Bbuild -GNinja . \ -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tg_owt \ -DCMAKE_C_FLAGS_DEBUG="" \ -DCMAKE_CXX_FLAGS_DEBUG="" \ -DCMAKE_DISABLE_FIND_PACKAGE_absl=ON cmake --build build --parallel + cmake --install build - name: TDE2E cache. id: cache-tde2e uses: actions/cache@v5 with: - path: ${{ env.LibrariesPath }}/tde2e + path: ${{ env.LibrariesPath }}/local/tde2e key: ${{ runner.OS }}-tde2e-${{ env.TDE2E }}-${{ env.CACHE_KEY }} - name: TDE2E. if: steps.cache-tde2e.outputs.cache-hit != 'true' @@ -140,7 +142,7 @@ jobs: cmake -Bbuild -GNinja . \ -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX=$PWD/build/prefix \ + -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tde2e \ -DCMAKE_C_FLAGS_DEBUG="" \ -DCMAKE_CXX_FLAGS_DEBUG="" \ -DTD_E2E_ONLY=ON @@ -150,11 +152,9 @@ jobs: - name: Telegram Desktop build. if: env.ONLY_CACHE == 'false' - env: - tg_owt_DIR: ${{ env.LibrariesPath }}/tg_owt/build - tde2e_DIR: ${{ env.LibrariesPath }}/tde2e/build/prefix run: | cd $REPO_NAME + export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH$(find $LibrariesPath/local -mindepth 1 -maxdepth 1 -type d -exec printf ':%s' {} +)" DEFINE="" if [ -n "${{ matrix.defines }}" ]; then From 70bddf76fa569592d5b369e554179c39c3dfd169 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 16 Jan 2026 16:03:59 +0400 Subject: [PATCH 005/348] Cache rnnoise in macOS packaged action --- .github/workflows/mac_packaged.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 5c0ba84aa1..e31b9f482a 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -86,9 +86,17 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=$(grep 'set(QT_SUPPORTED_MIN_MACOS_VERSION' /opt/homebrew/Cellar/qtbase/*/lib/cmake/Qt6/Qt6ConfigExtras.cmake | sed -E 's/^.*"(.*)"\)$/\1/')" >> $GITHUB_ENV echo "LibrariesPath=`pwd`" >> $GITHUB_ENV + echo "RNNOISE=`curl -sSL --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/repos/xiph/rnnoise/git/refs/heads/master | jq -r .object.sha`" >> $GITHUB_ENV echo "WEBRTC=`curl -sSL --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/repos/desktop-app/tg_owt/git/refs/heads/master | jq -r .object.sha`" >> $GITHUB_ENV + - name: RNNoise cache. + id: cache-rnnoise + uses: actions/cache@v5 + with: + path: ${{ env.LibrariesPath }}/local/rnnoise + key: ${{ runner.OS }}-rnnoise-${{ env.RNNOISE }}-${{ env.CACHE_KEY }} - name: RNNoise. + if: steps.cache-rnnoise.outputs.cache-hit != 'true' run: | cd $LibrariesPath From a86c84b1a15865fe63d1b0a263444b024efd1bf2 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Fri, 16 Jan 2026 17:04:03 +0400 Subject: [PATCH 006/348] Set more cmake variables globally in macOS packaged action --- .github/workflows/mac_packaged.yml | 34 +++++++++--------------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index e31b9f482a..e331d3d596 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -49,6 +49,9 @@ jobs: env: GIT: "https://github.com" + CMAKE_GENERATOR: "Ninja" + CMAKE_BUILD_TYPE: "Debug" + CMAKE_BUILD_PARALLEL_LEVEL: "" CMAKE_PREFIX_PATH: "/opt/homebrew/opt/ffmpeg@6:/opt/homebrew/opt/openal-soft" TDE2E: "51743dfd01dff6179e2d8f7095729caa4e2222e9" UPLOAD_ARTIFACT: "true" @@ -72,6 +75,7 @@ jobs: brew upgrade || true brew install ada-url autoconf automake boost cmake ffmpeg@6 jpeg-xl libavif libheif libtool openal-soft openh264 openssl opus ninja pkg-config python qtbase qtimageformats qtsvg xz sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + sudo sed -i '' '/CMAKE_${lang}_FLAGS_DEBUG_INIT/s/ -g//' /opt/homebrew/share/cmake/Modules/Compiler/GNU.cmake xcodebuild -version > CACHE_KEY.txt brew list --versions >> CACHE_KEY.txt @@ -121,14 +125,8 @@ jobs: git clone --depth=1 --recursive --shallow-submodules $GIT/desktop-app/tg_owt.git cd tg_owt - cmake -Bbuild -GNinja . \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tg_owt \ - -DCMAKE_C_FLAGS_DEBUG="" \ - -DCMAKE_CXX_FLAGS_DEBUG="" \ - -DCMAKE_DISABLE_FIND_PACKAGE_absl=ON - - cmake --build build --parallel + cmake -Bbuild . -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tg_owt + cmake --build build cmake --install build - name: TDE2E cache. @@ -148,14 +146,8 @@ jobs: git fetch --depth=1 origin $TDE2E git reset --hard FETCH_HEAD - cmake -Bbuild -GNinja . \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tde2e \ - -DCMAKE_C_FLAGS_DEBUG="" \ - -DCMAKE_CXX_FLAGS_DEBUG="" \ - -DTD_E2E_ONLY=ON - - cmake --build build --parallel + cmake -Bbuild . -DCMAKE_INSTALL_PREFIX=$LibrariesPath/local/tde2e -DTD_E2E_ONLY=ON + cmake --build build cmake --install build - name: Telegram Desktop build. @@ -173,14 +165,8 @@ jobs: echo "ARTIFACT_NAME=Telegram" >> $GITHUB_ENV fi - cmake -Bbuild -GNinja . \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_C_FLAGS_DEBUG="" \ - -DCMAKE_CXX_FLAGS_DEBUG="" \ - -DTDESKTOP_API_TEST=ON \ - $DEFINE - - cmake --build build --parallel + cmake -Bbuild . -DTDESKTOP_API_TEST=ON $DEFINE + cmake --build build macdeployqt build/Telegram.app codesign --remove-signature build/Telegram.app From 6022c79cbbd349f94c43a10fc2ca84f11f7ce318 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 21 Jan 2026 15:20:40 +0400 Subject: [PATCH 007/348] Ensure curl is present when building snap --- snap/snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index f1ac677907..91ed22fc53 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -323,6 +323,7 @@ parts: source-tag: v0.11.1 plugin: cmake build-packages: + - curl - libhwy-dev cmake-generator: Ninja cmake-parameters: From 18acef841b992b70c07b543bb893bf6e4beb0a28 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Sun, 12 Oct 2025 16:41:36 +0400 Subject: [PATCH 008/348] Add experimental option for QT_WIDGETS_HIGHDPI_DOWNSCALE --- Telegram/SourceFiles/core/launcher.cpp | 25 +++++++++++++++++-- Telegram/SourceFiles/core/launcher.h | 1 + .../settings/settings_experimental.cpp | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/core/launcher.cpp b/Telegram/SourceFiles/core/launcher.cpp index a5e98dc11e..d56d1d50b6 100644 --- a/Telegram/SourceFiles/core/launcher.cpp +++ b/Telegram/SourceFiles/core/launcher.cpp @@ -21,12 +21,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +#include namespace Core { namespace { uint64 InstallationTag = 0; +base::options::toggle OptionHighDpiDownscale({ + .id = kOptionHighDpiDownscale, + .name = "High DPI downscale", + .description = "Follow system interface scale settings exactly" + " (another approach, likely better quality).", + .scope = [] { + return !Platform::IsMac() + && QLibraryInfo::version() >= QVersionNumber(6, 4); + }, + .restartRequired = true, +}); + base::options::toggle OptionFreeType({ .id = kOptionFreeType, .name = "FreeType font engine", @@ -62,7 +75,7 @@ FilteredCommandLineArguments::FilteredCommandLineArguments( } #if defined Q_OS_WIN || defined Q_OS_MAC - if (OptionFreeType.value()) { + if (OptionFreeType.value() || OptionHighDpiDownscale.value()) { pushArgument("-platform"); #ifdef Q_OS_WIN pushArgument("windows:fontengine=freetype"); @@ -294,6 +307,7 @@ base::options::toggle OptionFractionalScalingEnabled({ } // namespace const char kOptionFractionalScalingEnabled[] = "fractional-scaling-enabled"; +const char kOptionHighDpiDownscale[] = "high-dpi-downscale"; const char kOptionFreeType[] = "freetype"; Launcher *Launcher::InstanceSetter::Instance = nullptr; @@ -345,7 +359,14 @@ void Launcher::initHighDpi() { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); #endif // Qt < 6.0.0 - if (OptionFractionalScalingEnabled.value()) { + if (OptionHighDpiDownscale.value()) { + qputenv("QT_WIDGETS_HIGHDPI_DOWNSCALE", "1"); + qputenv("QT_WIDGETS_RHI", "1"); + qputenv("QT_WIDGETS_RHI_BACKEND", "opengl"); + } + + if (OptionFractionalScalingEnabled.value() + || OptionHighDpiDownscale.value()) { QApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); } else { diff --git a/Telegram/SourceFiles/core/launcher.h b/Telegram/SourceFiles/core/launcher.h index e4de3c31d2..a0eb1e8d30 100644 --- a/Telegram/SourceFiles/core/launcher.h +++ b/Telegram/SourceFiles/core/launcher.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Core { extern const char kOptionFractionalScalingEnabled[]; +extern const char kOptionHighDpiDownscale[]; extern const char kOptionFreeType[]; class Launcher { diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index d271ddd989..f65f2c77af 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -150,6 +150,7 @@ void SetupExperimental( addToggle(ChatHelpers::kOptionTabbedPanelShowOnClick); addToggle(Dialogs::kOptionForumHideChatsList); addToggle(Core::kOptionFractionalScalingEnabled); + addToggle(Core::kOptionHighDpiDownscale); addToggle(Window::kOptionViewProfileInChatsListContextMenu); addToggle(Info::Profile::kOptionShowPeerIdBelowAbout); addToggle(Info::Profile::kOptionShowChannelJoinedBelowAbout); From 5a425dc5be4dbc48c003f44eac6258f965bc1a4e Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Mon, 19 Jan 2026 21:34:04 +0400 Subject: [PATCH 009/348] Update patches in prepare.py --- Telegram/build/prepare/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index 7071973730..1f0e897d59 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -454,7 +454,7 @@ if customRunCommand: stage('patches', """ git clone https://github.com/desktop-app/patches.git cd patches - git checkout 859556cab51d17585ff76d3db62ff1c7502bc850 + git checkout bc4c036311d07656c400c3f94877bdcf16d91688 """) stage('msys64', """ From 49b27d6b0d0b3c2730e496f4a7dd731b844e138f Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 21 Jan 2026 21:12:22 +0300 Subject: [PATCH 010/348] Removed notifications action buttom from profile top bar for self. --- Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index 6b9c7afa26..9fc34ecfe8 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -777,7 +777,7 @@ void TopBar::setupActions(not_null controller) { buttons.push_back(message); _actions->add(message); } - { + if (!peer->isSelf()) { const auto notifications = Ui::CreateChild( this, tr::lng_profile_action_short_mute(tr::now), From 68f65ea0004c3437366175e29c21333826f96a9b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 21 Jan 2026 21:25:50 +0300 Subject: [PATCH 011/348] Slightly improved position of context menu from top button in proxy box. --- Telegram/SourceFiles/boxes/connection_box.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/boxes/connection_box.cpp b/Telegram/SourceFiles/boxes/connection_box.cpp index c26d572b48..1198f1e501 100644 --- a/Telegram/SourceFiles/boxes/connection_box.cpp +++ b/Telegram/SourceFiles/boxes/connection_box.cpp @@ -790,7 +790,8 @@ void ProxiesBox::setupTopButton() { *menu = base::make_unique_q( top, st::popupMenuWithIcons); - const auto addAction = Ui::Menu::CreateAddActionCallback(*menu); + const auto raw = menu->get(); + const auto addAction = Ui::Menu::CreateAddActionCallback(raw); addAction({ .text = tr::lng_proxy_add_from_clipboard(tr::now), .handler = [=] { AddProxyFromClipboard(_controller, uiShow()); }, @@ -804,7 +805,16 @@ void ProxiesBox::setupTopButton() { .isAttention = true, }); } - (*menu)->popup(QCursor::pos()); + raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + top->setForceRippled(true); + raw->setDestroyedCallback([=] { + if (const auto strong = top.data()) { + strong->setForceRippled(false); + } + }); + raw->popup( + top->mapToGlobal( + QPoint(top->width(), top->height() - st::lineWidth * 3))); return true; }); } From b28bb112fe64367fe5afaeeb7d6fe5bd9c7e7b04 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 21 Jan 2026 21:29:48 +0300 Subject: [PATCH 012/348] Fixed display of compose search when edit media caption. --- Telegram/SourceFiles/history/history_widget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 8ff2088c7d..588dcf02a7 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -8821,7 +8821,8 @@ void HistoryWidget::editMessage( Window::PeerMenuEditTodoList(controller(), item); return; } - } else if (_composeSearch) { + } + if (_composeSearch) { _composeSearch->hideAnimated(); } From 8343178219b17946c026c55c6d1a287fce98397b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 20 Jan 2026 13:09:30 +0300 Subject: [PATCH 013/348] Improved forwarded info in service messages for staked dice. --- .../history/view/media/history_view_dice.cpp | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/history/view/media/history_view_dice.cpp b/Telegram/SourceFiles/history/view/media/history_view_dice.cpp index 93eed91e1f..17120a6fe7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_dice.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_dice.cpp @@ -63,8 +63,18 @@ void Dice::updateOutcomeMessage() { return; } const auto item = _parent->data(); - const auto from = item->from(); - const auto out = item->out() || from->isSelf(); + const auto forwarded = item->Get(); + const auto originalSender = forwarded + ? forwarded->originalSender + : nullptr; + const auto from = originalSender ? originalSender : item->from().get(); + const auto originalPostAuthor = item->originalPostAuthor(); + const auto fromName = !originalPostAuthor.isEmpty() + ? originalPostAuthor + : forwarded && forwarded->originalHiddenSenderInfo + ? forwarded->originalHiddenSenderInfo->name + : from->name(); + const auto out = (item->out() || from->isSelf()) && !forwarded; const auto won = (_outcomeNanoTon - _outcomeStakeNanoTon); const auto amount = tr::marked(QString::fromUtf8("\xf0\x9f\x92\x8e") + " " @@ -82,13 +92,15 @@ void Dice::updateOutcomeMessage() { : tr::lng_action_stake_game_lost)( tr::now, lt_from, - tr::link(st::wrap_rtl(from->name()), 1), + tr::link(st::wrap_rtl(fromName), 1), lt_amount, amount, tr::marked); auto prepared = PreparedServiceText{ text }; if (!out) { - prepared.links.push_back(from->createOpenLink()); + if (const auto link = forwarded ? originalSender : from) { + prepared.links.push_back(link->createOpenLink()); + } } _parent->setServicePostMessage(prepared, _link); } From 1f072631e55ebef9fecb6c17194e1e6cf420603e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 20 Jan 2026 17:11:13 +0300 Subject: [PATCH 014/348] Fixed crash when forward message from web bot to forum topic. --- Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 00d31d321d..de814d87f4 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -1954,7 +1954,7 @@ void WebViewInstance::botSendPreparedMessage( if (!checked) { return; } - state->send = nullptr; + [[maybe_unused]] const auto ongoing = base::take(state->send); send({ strong }, options); }; state->send({}); From 8585a00051a811184fbb6f530a55b778d24dc40e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 20 Jan 2026 17:38:27 +0300 Subject: [PATCH 015/348] Fixed position of freshly pinned to top stories. --- Telegram/SourceFiles/data/data_stories.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index fb1f1ec7a4..67442345fc 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -2247,11 +2247,6 @@ void Stories::togglePinnedList( auto &saved = _saved[peerId]; auto list = QVector(); list.reserve(maxPinnedCount()); - for (const auto &id : saved.ids.pinnedToTop) { - if (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) { - list.push_back(MTP_int(id)); - } - } if (pin) { auto copy = ids; ranges::sort(copy, ranges::greater()); @@ -2262,6 +2257,11 @@ void Stories::togglePinnedList( } } } + for (const auto &id : saved.ids.pinnedToTop) { + if (pin || !ranges::contains(ids, FullStoryId{ peerId, id })) { + list.push_back(MTP_int(id)); + } + } const auto api = &_owner->session().api(); const auto peer = session().data().peer(peerId); api->request(MTPstories_TogglePinnedToTop( From e6369eaae18157c4a54b092a575b9e6005bc39e5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 10 Jan 2026 21:34:40 +0300 Subject: [PATCH 016/348] Added chat filters to feature to kick participants from common groups. --- .../SourceFiles/boxes/choose_filter_box.cpp | 105 ++++++++++++++ .../SourceFiles/boxes/choose_filter_box.h | 8 ++ .../boxes/moderate_messages_box.cpp | 131 +++++++++++++++--- Telegram/SourceFiles/info/info.style | 1 + .../main/main_session_settings.cpp | 14 +- .../SourceFiles/main/main_session_settings.h | 6 +- 6 files changed, 237 insertions(+), 28 deletions(-) diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index af83512057..93230d4857 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -15,6 +15,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat_filters.h" #include "data/data_premium_limits.h" #include "data/data_session.h" +#include "data/data_channel.h" +#include "data/data_user.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -29,6 +31,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/popup_menu.h" #include "window/window_controller.h" #include "window/window_session_controller.h" +#include "main/main_session_settings.h" #include "styles/style_dialogs.h" #include "styles/style_media_player.h" // mediaPlayerMenuCheck #include "styles/style_menu_icons.h" @@ -348,3 +351,105 @@ void FillChooseFilterMenu( menu->hideMenu(); }, menu->lifetime()); } + +void FillChooseFilterWithAdminedGroupsMenu( + not_null controller, + not_null menu, + not_null user, + std::shared_ptr> listUpdates, + std::vector> common, + std::shared_ptr> collectCommon) { + const auto weak = base::make_weak(controller); + const auto session = &controller->session(); + const auto &list = session->data().chatsFilters().list(); + const auto showColors = session->data().chatsFilters().tagsEnabled(); + for (const auto &filter : list) { + const auto id = filter.id(); + if (!id) { + continue; + } + auto canRestrictList = std::vector>(); + const auto maybeAppend = [&](not_null chat) { + const auto channel = chat->peer->asChannel(); + if (channel && channel->canRestrictParticipant(user)) { + if (channel->isGroupAdmin(user) && !channel->amCreator()) { + return; + } + canRestrictList.push_back(chat->peer); + } + }; + for (const auto &chat : filter.always()) { + maybeAppend(chat); + } + for (const auto &chat : filter.pinned()) { + maybeAppend(chat); + } + if (canRestrictList.empty()) { + continue; + } + + const auto checked = std::make_shared(false); + + const auto contains = false; + const auto title = filter.title(); + auto item = base::make_unique_q( + menu->menu(), + menu->st().menu, + new QAction( + Ui::Text::FixAmpersandInAction(title.text.text), + menu.get()), + contains ? &st::mediaPlayerMenuCheck : nullptr, + contains ? &st::mediaPlayerMenuCheck : nullptr); + const auto triggered = [=, raw = item.get()] { + *checked = !*checked; + if (*checked) { + for (const auto &peer : canRestrictList) { + if (ranges::contains(common, peer)) { + collectCommon->push_back(peer->id); + } + } + } else { + for (const auto &peer : canRestrictList) { + if (const auto i = ranges::find(*collectCommon, peer->id); + i != collectCommon->end()) { + collectCommon->erase(i); + } + } + } + raw->Ui::Menu::Action::setIcon( + *checked ? &st::mediaPlayerMenuCheck : nullptr, + *checked ? &st::mediaPlayerMenuCheck : nullptr); + listUpdates->fire({}); + }; + item->setActionTriggered([=] { + triggered(); + + auto groups = session->settings().moderateCommonGroups(); + if (*checked && !ranges::contains(groups, id)) { + groups.push_back(id); + } else if (!*checked) { + groups.erase(ranges::remove(groups, id), groups.end()); + } + session->settings().setModerateCommonGroups(groups); + session->saveSettingsDelayed(); + }); + if (ranges::contains( + session->settings().moderateCommonGroups(), + id)) { + triggered(); + } + item->setPreventClose(true); + item->setMarkedText(title.text, QString(), Core::TextContext({ + .session = session, + .repaint = [raw = item.get()] { raw->update(); }, + .customEmojiLoopLimit = title.isStatic ? -1 : 0, + })); + + item->setIcon(Icon(showColors ? filter : filter.withColorIndex({}))); + } + + session->data().chatsFilters().changed( + ) | rpl::on_next([=] { + menu->hideMenu(); + }, menu->lifetime()); +} diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index e6c5ad3353..ff29b3c259 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -43,3 +43,11 @@ void FillChooseFilterMenu( not_null controller, not_null menu, not_null history); + +void FillChooseFilterWithAdminedGroupsMenu( + not_null controller, + not_null menu, + not_null user, + std::shared_ptr> listUpdates, + std::vector> common, + std::shared_ptr> collectCommon); diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index a95b0436b9..f44787862d 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -53,16 +53,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_window.h" +#include "window/window_session_controller.h" +#include "window/window_controller.h" +#include "boxes/choose_filter_box.h" #include "boxes/peer_list_box.h" #include "main/main_session_settings.h" #include "ui/painter.h" #include "ui/rect.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_action.h" +#include "base/qt/qt_key_modifiers.h" #include "ui/effects/round_checkbox.h" #include "styles/style_chat.h" +#include "styles/style_menu_icons.h" #include "styles/style_info.h" +#include "styles/style_media_player.h" // mediaPlayerMenuCheck base::options::toggle ModerateCommonGroups({ .id = kModerateCommonGroups, @@ -160,7 +168,9 @@ using CollectCommon = std::shared_ptr>; void FillMenuModerateCommonGroups( not_null menu, CommonGroups common, - CollectCommon collectCommon) { + CollectCommon collectCommon, + not_null user, + Fn onDestroyedCallback) { const auto resultList = menu->lifetime().make_state>(); const auto rememberCheckbox = Ui::CreateChild( @@ -172,18 +182,15 @@ void FillMenuModerateCommonGroups( st::historyHasCustomEmoji, st::historyHasCustomEmojiPosition, tr::lng_restrict_users_kick_from_common_group(tr::now, tr::rich)); - multiline->setDisabled(true); multiline->setAttribute(Qt::WA_TransparentForMouseEvents); menu->addAction(std::move(multiline)); const auto session = &common.front()->session(); + const auto settingsOnStart = session->settings().moderateCommonGroups(); + const auto checkboxesUpdate = std::make_shared>(); const auto save = [=] { auto result = std::vector( resultList->begin(), resultList->end()); - if (rememberCheckbox->checked()) { - session->settings().setModerateCommonGroups(result); - session->saveSettingsDelayed(); - } *collectCommon = std::move(result); }; for (const auto &group : common) { @@ -201,17 +208,22 @@ void FillMenuModerateCommonGroups( nullptr, nullptr); const auto state = item->lifetime().make_state(); - item->AbstractButton::setDisabled(true); - item->setActionTriggered([=, peerId = group->id] { - state->checkbox->setChecked(!state->checkbox->checked()); + const auto setChecked = [=, peerId = group->id](bool checked) { + state->checkbox->setChecked(checked); if (state->checkbox->checked()) { resultList->insert(peerId); } else { resultList->erase(peerId); } save(); + }; + item->setActionTriggered([=] { + setChecked(!state->checkbox->checked()); }); const auto raw = item.get(); + checkboxesUpdate->events() | rpl::on_next([=, peerId = group->id] { + setChecked(ranges::contains(*collectCommon, peerId)); + }, raw->lifetime()); state->checkboxWidget = Ui::CreateChild(raw); state->checkboxWidget->setAttribute(Qt::WA_TransparentForMouseEvents); state->checkboxWidget->resize(item->width() * 2, item->height()); @@ -224,10 +236,10 @@ void FillMenuModerateCommonGroups( ? int(size * Ui::ForumUserpicRadiusMultiplier()) : std::optional(); }); state->checkbox->setChecked( - ranges::contains( + /*ranges::contains( session->settings().moderateCommonGroups(), group->id) - || (collectCommon + || */(collectCommon && ranges::contains(*collectCommon, group->id)), anim::type::instant); state->checkboxWidget->paintOn([=](QPainter &p) { @@ -241,6 +253,24 @@ void FillMenuModerateCommonGroups( menu->addAction(std::move(item)); } menu->addSeparator(); + if (const auto window = Core::App().findWindow(menu->parentWidget())) { + Ui::Menu::CreateAddActionCallback(menu)(Ui::Menu::MenuCallback::Args{ + .text = tr::lng_restrict_users_kick_from_common_group(tr::now), + .handler = nullptr, + .icon = &st::menuIconAddToFolder, + .fillSubmenu = [&](not_null menu) { + FillChooseFilterWithAdminedGroupsMenu( + window->sessionController(), + menu, + user, + checkboxesUpdate, + common, + collectCommon); + }, + .submenuSt = &st::foldersMenu, + }); + } + menu->addSeparator(); { auto item = base::make_unique_q( menu->menu(), @@ -251,7 +281,7 @@ void FillMenuModerateCommonGroups( [] {}), nullptr, nullptr); - item->AbstractButton::setDisabled(true); + item->setPreventClose(true); item->setActionTriggered([=] { rememberCheckbox->setChecked(!rememberCheckbox->checked()); }); @@ -261,11 +291,18 @@ void FillMenuModerateCommonGroups( rememberCheckbox->show(); menu->addAction(std::move(item)); } + menu->setDestroyedCallback([=] { + onDestroyedCallback(); + if (!rememberCheckbox->checked()) { + session->settings().setModerateCommonGroups(settingsOnStart); + session->saveSettingsDelayed(); + } + }); } void ProccessCommonGroups( const HistoryItemsList &items, - Fn processHas) { + Fn)> processHas) { const auto moderateOptions = CalculateModerateOptions(items); if (moderateOptions.participants.size() != 1 || !moderateOptions.allCanBan) { @@ -298,7 +335,7 @@ void ProccessCommonGroups( } if (!filtered.empty()) { - processHas(filtered); + processHas(filtered, user); } }); } @@ -342,24 +379,79 @@ void CreateModerateMessagesBox( if (ModerateCommonGroups.value() || session->supportMode()) { ProccessCommonGroups( items, - crl::guard(box, [=](CommonGroups groups) { + crl::guard(box, [=](CommonGroups groups, not_null user) { using namespace Ui; const auto top = box->addTopButton(st::infoTopBarMenu); auto &lifetime = top->lifetime(); const auto menu = lifetime.make_state>(); + { + const auto was = collectCommon->size(); + *menu = base::make_unique_q( + top, + st::popupMenuExpandedSeparator); + FillMenuModerateCommonGroups( + *menu, + groups, + collectCommon, + user, + []{}); + *menu = nullptr; + if (was != collectCommon->size()) { + top->setIconOverride( + &st::infoTopBarMenuActive, + &st::infoTopBarMenuActive); + const auto minicheck = Ui::CreateChild(top); + minicheck->paintRequest() | rpl::on_next([=] { + auto p = Painter(minicheck); + const auto rect = minicheck->rect(); + const auto iconSize = QSize( + st::mediaPlayerMenuCheck.width(), + st::mediaPlayerMenuCheck.height()); + const auto scale = std::min( + rect.width() / float64(iconSize.width()), + rect.height() / float64(iconSize.height())); + if (scale < 1.0) { + p.save(); + p.translate(rect.center()); + p.scale(scale, scale); + p.translate(-rect.center()); + } + st::mediaPlayerMenuCheck.paintInCenter( + p, + rect, + st::windowActiveTextFg->c); + if (scale < 1.0) { + p.restore(); + } + }, minicheck->lifetime()); + minicheck->resize( + st::mediaPlayerMenuCheck.width() / 1.5, + st::mediaPlayerMenuCheck.width() / 1.5); + minicheck->show(); + minicheck->moveToLeft( + top->width() - st::lineWidth * 26, + top->height() - st::lineWidth * 29); + } + } + top->setClickedCallback([=] { top->setForceRippled(true); *menu = base::make_unique_q( top, st::popupMenuExpandedSeparator); - (*menu)->setDestroyedCallback([=, weak = top] { + const auto onDestroyedCallback = [=, weak = top] { if (const auto strong = weak.data()) { strong->setForceRippled(false); } - }); - FillMenuModerateCommonGroups(*menu, groups, collectCommon); + }; + FillMenuModerateCommonGroups( + *menu, + groups, + collectCommon, + user, + onDestroyedCallback); (*menu)->setForcedOrigin(PanelAnimation::Origin::TopRight); const auto point = QPoint(top->width(), top->height()); (*menu)->popup(top->mapToGlobal(point)); @@ -752,6 +844,9 @@ void CreateModerateMessagesBox( const auto request = [=]( not_null peer, not_null channel) { + if (base::IsAltPressed() || base::IsCtrlPressed()) { + return; + } if (!kick) { Api::ChatParticipants::Restrict( channel, diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index a3a04c9ac9..53dc0ff6f6 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -169,6 +169,7 @@ infoTopBarMenu: IconButton(infoTopBarBack) { iconPosition: point(18px, -1px); rippleAreaPosition: point(1px, 6px); } +infoTopBarMenuActive: icon {{ "title_menu_dots", windowActiveTextFg, point(0px, 0px) }}; infoTopBarCall: IconButton(infoTopBarMenu) { width: 42px; icon: icon {{ "top_bar_call", boxTitleCloseFg }}; diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp index 0be24c19c7..9169e20702 100644 --- a/Telegram/SourceFiles/main/main_session_settings.cpp +++ b/Telegram/SourceFiles/main/main_session_settings.cpp @@ -67,7 +67,7 @@ QByteArray SessionSettings::serialize() const { } size += sizeof(qint32); // _setupEmailState size += sizeof(qint32) // _moderateCommonGroups size - + (_moderateCommonGroups.size() * sizeof(quint64)); + + (_moderateCommonGroups.size() * sizeof(qint32)); auto result = QByteArray(); result.reserve(size); @@ -150,8 +150,8 @@ QByteArray SessionSettings::serialize() const { } stream << qint32(static_cast(_setupEmailState)); stream << qint32(_moderateCommonGroups.size()); - for (const auto &peerId : _moderateCommonGroups) { - stream << SerializePeerId(peerId); + for (const auto &filterId : _moderateCommonGroups) { + stream << qint32(filterId); } } @@ -225,7 +225,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { base::flat_set ratedTranscriptions; std::vector unreviewed; qint32 setupEmailState = 0; - std::vector moderateCommonGroups; + std::vector moderateCommonGroups; stream >> versionTag; if (versionTag == kVersionTag) { @@ -647,15 +647,15 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) { stream >> count; if (stream.status() == QDataStream::Ok) { for (auto i = 0; i != count; ++i) { - quint64 peerId; - stream >> peerId; + qint32 filterId; + stream >> filterId; if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for SessionSettings::addFromSerialized()" "with moderateCommonGroups")); return; } - moderateCommonGroups.emplace_back(DeserializePeerId(peerId)); + moderateCommonGroups.emplace_back(filterId); } } } diff --git a/Telegram/SourceFiles/main/main_session_settings.h b/Telegram/SourceFiles/main/main_session_settings.h index 16e4f07ab2..56982c7902 100644 --- a/Telegram/SourceFiles/main/main_session_settings.h +++ b/Telegram/SourceFiles/main/main_session_settings.h @@ -176,10 +176,10 @@ public: void setSetupEmailState(Data::SetupEmailState state); [[nodiscard]] Data::SetupEmailState setupEmailState() const; - void setModerateCommonGroups(std::vector groups) { + void setModerateCommonGroups(std::vector groups) { _moderateCommonGroups = std::move(groups); } - [[nodiscard]] const std::vector &moderateCommonGroups() const { + [[nodiscard]] const std::vector &moderateCommonGroups() const { return _moderateCommonGroups; } @@ -229,7 +229,7 @@ private: Data::SetupEmailState _setupEmailState; - std::vector _moderateCommonGroups; + std::vector _moderateCommonGroups; }; From 7134363905623dc1deadc2bbc83a5b8ec9109711 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 07:18:52 +0300 Subject: [PATCH 017/348] Slightly improved padding of emoji status in accounts list. --- Telegram/SourceFiles/settings/settings_information.cpp | 9 +++++---- Telegram/SourceFiles/settings/settings_information.h | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 695f1bae16..08f8067b10 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -693,7 +693,7 @@ void SetupAccountsWrap( const auto raw = result.data(); { - const auto container = Badge::AddRight(raw); + const auto container = Badge::AddRight(raw, st::mainMenuAccountLine); const auto composedBadge = Ui::CreateChild( container.get(), raw, @@ -1110,7 +1110,8 @@ Ui::UnreadBadgeStyle Style() { } not_null AddRight( - not_null button) { + not_null button, + int rightPadding) { const auto widget = Ui::CreateChild(button.get()); rpl::combine( @@ -1121,10 +1122,10 @@ not_null AddRight( auto padding = button->st().padding; if (shown) { widget->moveToRight( - padding.right(), + padding.right() + rightPadding, (outer.height() - inner.height()) / 2, outer.width()); - padding.setRight(padding.right() + inner.width()); + padding.setRight(padding.right() + inner.width() + rightPadding); } button->setPaddingOverride(padding); button->update(); diff --git a/Telegram/SourceFiles/settings/settings_information.h b/Telegram/SourceFiles/settings/settings_information.h index 3489d5ca1d..d83b571de9 100644 --- a/Telegram/SourceFiles/settings/settings_information.h +++ b/Telegram/SourceFiles/settings/settings_information.h @@ -52,7 +52,8 @@ struct UnreadBadge { bool muted = false; }; [[nodiscard]] not_null AddRight( - not_null button); + not_null button, + int rightPadding = 0); [[nodiscard]] not_null CreateUnread( not_null container, rpl::producer value); From 97a1d5c5927f256dd033437c01d446f156a40b8a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 08:47:34 +0300 Subject: [PATCH 018/348] Fixed stale state of media edit manager in history widget. Fixed #30147. --- Telegram/SourceFiles/history/history_widget.cpp | 14 ++++++++++++++ .../history_view_compose_media_edit_manager.cpp | 2 ++ 2 files changed, 16 insertions(+) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 588dcf02a7..74b938e567 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -2605,6 +2605,7 @@ void HistoryWidget::showHistory( updateReplaceMediaButton(); _fieldBarCancel->hide(); + _mediaEditManager.cancel(); _membersDropdownShowTimer.cancel(); _scroll->takeWidget().destroy(); @@ -8793,6 +8794,19 @@ void HistoryWidget::setReplyFieldsFromProcessing() { } else { _replyEditMsg = item; _replyTo = id; + if (_replyTo) { + if (const auto i = session().data().message(_replyTo.messageId)) { + if (const auto media = i->media()) { + using namespace SendMenu; + const auto type = media->hasSpoiler() + ? Action{ .type = Action::Type::SpoilerOn } + : Action{ .type = Action::Type::SpoilerOff }; + _mediaEditManager.apply(type); + } + } + } else { + _mediaEditManager.cancel(); + } cancelSuggestPost(); updateReplyEditText(_replyEditMsg); updateCanSendMessage(); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp index 2897317dff..66659ad75a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_media_edit_manager.cpp @@ -60,6 +60,8 @@ void MediaEditManager::apply(SendMenu::Action action) { void MediaEditManager::cancel() { _menu = nullptr; _item = nullptr; + _spoilered = false; + _invertCaption = false; _lifetime.destroy(); } From 2c68df96963ecdf7dfd6159ffaabd0629eaca019 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 09:05:40 +0300 Subject: [PATCH 019/348] Fixed column bounds check in ShareBox for mouse to prevent cross-rows. Fixed #30155. --- Telegram/SourceFiles/boxes/share_box.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Telegram/SourceFiles/boxes/share_box.cpp b/Telegram/SourceFiles/boxes/share_box.cpp index 2caf3ad3e8..754cf4bd51 100644 --- a/Telegram/SourceFiles/boxes/share_box.cpp +++ b/Telegram/SourceFiles/boxes/share_box.cpp @@ -1313,6 +1313,12 @@ void ShareBox::Inner::updateUpon(const QPoint &pos) { auto x = pos.x(), y = pos.y(); auto row = (y - _rowsTop) / _rowHeight; auto column = qFloor((x - _rowsLeft) / _rowWidthReal); + + if (column < 0 || column >= _columnCount) { + _upon = -1; + return; + } + auto left = _rowsLeft + qFloor(column * _rowWidthReal) + st::shareColumnSkip / 2; auto top = _rowsTop + row * _rowHeight + st::sharePhotoTop; auto xupon = (x >= left) && (x < left + (_rowWidth - st::shareColumnSkip)); From a05d85673ab4a49334fb9a364ecf540eec2b4d8a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 09:38:34 +0300 Subject: [PATCH 020/348] Improved process of replying via hotkeys for sending messages. Fixed #30160. --- .../SourceFiles/history/history_widget.cpp | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 74b938e567..5d6f938711 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -7736,9 +7736,10 @@ bool HistoryWidget::replyToPreviousMessage() { || (_replyTo && _replyTo.messageId.peer != _history->peer->id)) { return false; } + const auto isFieldVisible = _field->isVisible(); const auto fullId = FullMsgId( _history->peer->id, - (_field->isVisible() + ((isFieldVisible && _replyTo.messageId.msg) ? _replyTo.messageId.msg : _highlighter.latestSingleHighlightedMsgId())); if (const auto item = session().data().message(fullId)) { @@ -7746,8 +7747,12 @@ bool HistoryWidget::replyToPreviousMessage() { if (const auto previousView = view->previousDisplayedInBlocks()) { const auto previous = previousView->data(); controller()->showMessage(previous); - if (_field->isVisible()) { - replyToMessage(previous); + if (isFieldVisible) { + if (previous->isLocal()) { + cancelReply(); + } else { + replyToMessage(previous); + } } return true; } @@ -7755,8 +7760,12 @@ bool HistoryWidget::replyToPreviousMessage() { } else if (const auto previousView = _history->findLastDisplayed()) { const auto previous = previousView->data(); controller()->showMessage(previous); - if (_field->isVisible()) { - replyToMessage(previous); + if (isFieldVisible) { + if (previous->isLocal()) { + cancelReply(); + } else { + replyToMessage(previous); + } } return true; } @@ -7770,9 +7779,10 @@ bool HistoryWidget::replyToNextMessage() { || (_replyTo && _replyTo.messageId.peer != _history->peer->id)) { return false; } + const auto isFieldVisible = _field->isVisible(); const auto fullId = FullMsgId( _history->peer->id, - (_field->isVisible() + ((isFieldVisible && _replyTo.messageId.msg) ? _replyTo.messageId.msg : _highlighter.latestSingleHighlightedMsgId())); if (const auto item = session().data().message(fullId)) { @@ -7780,8 +7790,12 @@ bool HistoryWidget::replyToNextMessage() { if (const auto nextView = view->nextDisplayedInBlocks()) { const auto next = nextView->data(); controller()->showMessage(next); - if (_field->isVisible()) { - replyToMessage(next); + if (isFieldVisible) { + if (next->isLocal()) { + cancelReply(); + } else { + replyToMessage(next); + } } } else { _highlighter.clear(); From 2781e6c304dcbc1004bc491c12d3172d55e8df13 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 12:17:54 +0300 Subject: [PATCH 021/348] Attempted to fix rare visual glitch when pinned bar is not resized. Fixed #30188. --- Telegram/SourceFiles/ui/chat/pinned_bar.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Telegram/SourceFiles/ui/chat/pinned_bar.cpp b/Telegram/SourceFiles/ui/chat/pinned_bar.cpp index cbd80030a4..b93ac93f70 100644 --- a/Telegram/SourceFiles/ui/chat/pinned_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/pinned_bar.cpp @@ -247,6 +247,9 @@ void PinnedBar::move(int x, int y) { void PinnedBar::resizeToWidth(int width) { _wrap.entity()->resizeToWidth(width); + if (!_wrap.width()) { + _wrap.resizeToWidth(width); + } } int PinnedBar::height() const { From 481de1e3e3801aa3700c91d6807f33e0aa53b403 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 21:52:15 +0300 Subject: [PATCH 022/348] Added bullet separator between name and date in media viewer overlay. --- .../SourceFiles/media/view/media_view.style | 1 + .../media/view/media_view_overlay_widget.cpp | 31 +++++++++++++++++-- .../media/view/media_view_overlay_widget.h | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 2fa68f81b2..fa3d245c58 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -215,6 +215,7 @@ mediaviewTextStyle: defaultTextStyle; mediaviewTextLeft: 14px; mediaviewTextSkip: 10px; +mediaviewTextSkipHalf: 5px; mediaviewHeaderTop: 47px; mediaviewTextTop: 26px; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 7c1f88ebfd..bbc870bbb4 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1497,13 +1497,27 @@ void OverlayWidget::updateControls() { height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height); + const auto separatorText = QString::fromUtf8("\xE2\x80\xA2"); + const auto separatorWidth = st::mediaviewFont->width(separatorText); + _separatorNav = QRect( + st::mediaviewTextLeft + + _nameNav.width() + + st::mediaviewTextSkipHalf, + height() - st::mediaviewTextTop, + separatorWidth, + st::mediaviewFont->height); _dateNav = QRect( - st::mediaviewTextLeft + _nameNav.width() + st::mediaviewTextSkip, + st::mediaviewTextLeft + + _nameNav.width() + + st::mediaviewTextSkipHalf + + separatorWidth + + st::mediaviewTextSkipHalf, height() - st::mediaviewTextTop, st::mediaviewFont->width(_dateText), st::mediaviewFont->height); } else { _nameNav = QRect(); + _separatorNav = QRect(); _dateNav = QRect( st::mediaviewTextLeft, height() - st::mediaviewTextTop, @@ -1983,6 +1997,7 @@ bool OverlayWidget::updateControlsAnimation(crl::time now) { : QRect()) + _headerNav + _nameNav + + _separatorNav + _dateNav + _captionRect.marginsAdded(st::mediaviewCaptionPadding) + _groupThumbsRect @@ -5613,6 +5628,18 @@ void OverlayWidget::paintFooterContent( } } + // separator + if (_separatorNav.isValid()) { + const auto separator = _separatorNav.translated(shift); + if (separator.intersects(clip)) { + p.setOpacity(controlOpacity(0.) * opacity); + p.drawText( + separator.left(), + separator.top() + st::mediaviewFont->ascent, + QString::fromUtf8("\xE2\x80\xA2")); + } + } + // date if (date.intersects(clip)) { float64 o = overLevel(Over::Date); @@ -5627,7 +5654,7 @@ void OverlayWidget::paintFooterContent( } QRect OverlayWidget::footerGeometry() const { - return _headerNav.united(_nameNav).united(_dateNav); + return _headerNav.united(_nameNav).united(_separatorNav).united(_dateNav); } void OverlayWidget::paintCaptionContent( diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index f52d368d9f..0ffe6deb62 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -590,7 +590,7 @@ private: QRect _leftNav, _leftNavOver, _leftNavIcon; QRect _rightNav, _rightNavOver, _rightNavIcon; - QRect _headerNav, _nameNav, _dateNav; + QRect _headerNav, _nameNav, _dateNav, _separatorNav; QRect _rotateNav, _rotateNavOver, _rotateNavIcon; QRect _shareNav, _shareNavOver, _shareNavIcon; QRect _recognizeNav, _recognizeNavOver, _recognizeNavIcon; From 9fb25bfada19997b294e74ddc30735cabc9d4ede Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 12 Jan 2026 22:02:10 +0300 Subject: [PATCH 023/348] Added special bullet char to global access. --- .../SourceFiles/boxes/peers/choose_peer_box.cpp | 2 +- .../boxes/peers/edit_peer_invite_links.cpp | 2 +- .../boxes/peers/edit_peer_permissions_box.cpp | 2 +- .../SourceFiles/calls/group/calls_group_panel.cpp | 2 +- Telegram/SourceFiles/core/changelogs.cpp | 2 +- .../info/profile/info_profile_actions.cpp | 14 ++++++++------ .../media/stories/media_stories_header.cpp | 14 +++++++++----- .../media/stories/media_stories_recent_views.cpp | 4 ++-- .../media/view/media_view_overlay_widget.cpp | 5 ++--- .../settings/settings_active_sessions.cpp | 2 +- Telegram/SourceFiles/settings/settings_folders.cpp | 2 +- .../SourceFiles/settings/settings_websites.cpp | 2 +- Telegram/lib_ui | 2 +- 13 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp index 3f5815771f..908579b221 100644 --- a/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/choose_peer_box.cpp @@ -352,7 +352,7 @@ void ChoosePeerBoxController::prepareRestrictions() { tr::lng_request_peer_requirements(), { 0, st::membersMarginTop, 0, 0 }); const auto skip = st::defaultSubsectionTitlePadding.left(); - auto separator = QString::fromUtf8("\n\xE2\x80\xA2 "); + auto separator = '\n' + Ui::kQBullet + ' '; raw->add( object_ptr( raw, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index cf6303ea70..eacaa448ca 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -173,7 +173,7 @@ private: link.usageLimit) : tr::lng_group_invite_no_joined(tr::now); const auto add = [&](const QString &text) { - result += QString::fromUtf8(" \xE2\x80\xA2 ") + text; + result += ' ' + Ui::kQBullet + ' ' + text; }; if (revoked) { return result; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp index 7a20770d2f..88eb87ecd9 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_permissions_box.cpp @@ -1310,7 +1310,7 @@ Fn AboutGigagroupCallback( box->setTitle(tr::lng_gigagroup_convert_title()); const auto addFeature = [&](rpl::producer text) { using namespace rpl::mappers; - const auto prefix = QString::fromUtf8("\xE2\x80\xA2 "); + const auto prefix = Ui::kQBullet + ' '; box->addRow( object_ptr( box, diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 3f809df881..02329beaeb 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -2740,7 +2740,7 @@ void Panel::refreshTitle() { if (_call->rtmp()) { _titleSeparator.create( widget(), - rpl::single(QString::fromUtf8("\xE2\x80\xA2")), + rpl::single(Ui::kQBullet), st::groupCallTitleLabel); _titleSeparator->show(); _titleSeparator->setAttribute(Qt::WA_TransparentForMouseEvents); diff --git a/Telegram/SourceFiles/core/changelogs.cpp b/Telegram/SourceFiles/core/changelogs.cpp index b0a9a66eb2..9b1f21fb3b 100644 --- a/Telegram/SourceFiles/core/changelogs.cpp +++ b/Telegram/SourceFiles/core/changelogs.cpp @@ -135,7 +135,7 @@ void Changelogs::addBetaLog(int changeVersion, const char *changes) { } const auto text = [&] { static const auto simple = u"\n- "_q; - static const auto separator = QString::fromUtf8("\n\xE2\x80\xA2 "); + static const auto separator = '\n' + Ui::kQBullet + ' '; auto result = QString::fromUtf8(changes).trimmed(); if (result.startsWith(base::StringViewMid(simple, 1))) { result = separator.mid(1) + result.mid(simple.size() - 1); diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index f55eda96b0..a83831a4b9 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -1802,12 +1802,14 @@ object_ptr DetailsFiller::setupPersonalChannel( ) | rpl::map([](TextWithEntities &&text, ChannelData *channel) { const auto count = channel ? channel->membersCount() : 0; if (count > 1) { - text.append( - QString::fromUtf8(" \xE2\x80\xA2 ") - ).append(tr::lng_chat_status_subscribers( - tr::now, - lt_count_decimal, - count)); + text.append(' ') + .append(Ui::kQBullet) + .append(' ') + .append( + tr::lng_chat_status_subscribers( + tr::now, + lt_count_decimal, + count)); } return text; }); diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index 17a1d55eff..f1113de0eb 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -249,19 +249,23 @@ struct MadePrivacyBadge { const auto index = data.fullIndex + 1; const auto count = data.fullCount; return count - ? QString::fromUtf8(" \xE2\x80\xA2 %1/%2").arg(index).arg(count) + ? QString::fromUtf8(" %3 %1/%2") + .arg(index) + .arg(count) + .arg(Ui::kQBullet) : QString(); } [[nodiscard]] Timestamp ComposeDetails(HeaderData data, TimeId now) { auto result = ComposeTimestamp(data.date, now); if (data.edited) { - result.text.append( - QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now)); + result.text.append(' ' + + Ui::kQBullet + + ' ' + + tr::lng_edited(tr::now)); } if (data.fromPeer || !data.repostFrom.isEmpty()) { - result.text = QString::fromUtf8("\xE2\x80\xA2 ") - + result.text; + result.text = Ui::kQBullet + ' ' + result.text; } return result; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp index c40e80be6e..ac07131c63 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -129,9 +129,9 @@ constexpr auto kLoadViewsPages = 2; const QString &date, not_null repost) { return date + (repost->repostModified() - ? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now)) + ? (' ' + Ui::kQBullet + ' ' + tr::lng_edited(tr::now)) : !repost->caption().empty() - ? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_commented(tr::now)) + ? (' ' + Ui::kQBullet + ' ' + tr::lng_commented(tr::now)) : QString()); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index bbc870bbb4..f9eb0ac99d 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1497,8 +1497,7 @@ void OverlayWidget::updateControls() { height() - st::mediaviewTextTop, qMin(_fromNameLabel.maxWidth(), width() / 3), st::mediaviewFont->height); - const auto separatorText = QString::fromUtf8("\xE2\x80\xA2"); - const auto separatorWidth = st::mediaviewFont->width(separatorText); + const auto separatorWidth = st::mediaviewFont->width(Ui::kQBullet); _separatorNav = QRect( st::mediaviewTextLeft + _nameNav.width() @@ -5636,7 +5635,7 @@ void OverlayWidget::paintFooterContent( p.drawText( separator.left(), separator.top() + st::mediaviewFont->ascent, - QString::fromUtf8("\xE2\x80\xA2")); + Ui::kQBullet); } } diff --git a/Telegram/SourceFiles/settings/settings_active_sessions.cpp b/Telegram/SourceFiles/settings/settings_active_sessions.cpp index 9d078a21cc..ceb16ee804 100644 --- a/Telegram/SourceFiles/settings/settings_active_sessions.cpp +++ b/Telegram/SourceFiles/settings/settings_active_sessions.cpp @@ -153,7 +153,7 @@ void RenameBox(not_null box) { [[nodiscard]] QString LocationAndDate(const EntryData &entry) { return (entry.location.isEmpty() ? entry.ip : entry.location) + (entry.hash - ? (QString::fromUtf8(" \xE2\x80\xA2 ") + entry.active) + ? (' ' + Ui::kQBullet + ' ' + entry.active) : QString()); } diff --git a/Telegram/SourceFiles/settings/settings_folders.cpp b/Telegram/SourceFiles/settings/settings_folders.cpp index 5bd22679fd..b453f9cd6b 100644 --- a/Telegram/SourceFiles/settings/settings_folders.cpp +++ b/Telegram/SourceFiles/settings/settings_folders.cpp @@ -162,7 +162,7 @@ struct FilterRow { : tr::lng_filters_no_chats(tr::now); return filter.chatlist() ? (result - + QString::fromUtf8(" \xE2\x80\xA2 ") + + (' ' + Ui::kQBullet + ' ') + tr::lng_filters_shareable_status(tr::now)) : result; } diff --git a/Telegram/SourceFiles/settings/settings_websites.cpp b/Telegram/SourceFiles/settings/settings_websites.cpp index 5fd6e078c3..56d8ae50d3 100644 --- a/Telegram/SourceFiles/settings/settings_websites.cpp +++ b/Telegram/SourceFiles/settings/settings_websites.cpp @@ -96,7 +96,7 @@ private: [[nodiscard]] QString LocationAndDate(const EntryData &entry) { return (entry.location.isEmpty() ? entry.ip : entry.location) + (entry.hash - ? (QString::fromUtf8(" \xE2\x80\xA2 ") + entry.active) + ? ((' ' + Ui::kQBullet + ' ') + entry.active) : QString()); } diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 50490486d1..d9516e9f26 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 50490486d12b486fb27a09e844ba3adbaca66a3e +Subproject commit d9516e9f26ecbe267d24b4bbb5fa6fb15939e628 From 3ea8d14521740a62c2426bd7c5bbaf4e1579116b Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 13 Jan 2026 09:38:30 +0300 Subject: [PATCH 024/348] Extracted strip selector from reactions to make dedicated popup. --- .../ui/controls/popup_selector.cpp | 300 ++++++++++++++++++ .../SourceFiles/ui/controls/popup_selector.h | 62 ++++ Telegram/cmake/td_ui.cmake | 2 + 3 files changed, 364 insertions(+) create mode 100644 Telegram/SourceFiles/ui/controls/popup_selector.cpp create mode 100644 Telegram/SourceFiles/ui/controls/popup_selector.h diff --git a/Telegram/SourceFiles/ui/controls/popup_selector.cpp b/Telegram/SourceFiles/ui/controls/popup_selector.cpp new file mode 100644 index 0000000000..a79e319bb6 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/popup_selector.cpp @@ -0,0 +1,300 @@ +// 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/popup_selector.h" + +#include "ui/painter.h" +#include "ui/style/style_core.h" +#include "ui/effects/animation_value.h" +#include "ui/platform/ui_platform_utility.h" +#include "styles/style_chat.h" +#include "styles/style_widgets.h" + +#include +#include +#include + +namespace Ui { + +PopupSelector::PopupSelector( + not_null parent, + QSize size, + PopupAppearType appearType) +: RpWidget(parent) +, _innerSize(size) +, _cachedRound( + size, + st::reactionCornerShadow, + std::min(size.width(), size.height())) +, _appearType(appearType) { + _useTransparency = Platform::TranslucentWindowsSupported(); + const auto margins = marginsForShadow(); + resize(size + QSize( + margins.left() + margins.right(), + margins.top() + margins.bottom())); + + setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) + | Qt::BypassWindowManagerHint + | Qt::Popup + | Qt::NoDropShadowWindowHint); + setMouseTracking(true); + setAttribute(Qt::WA_NoSystemBackground, true); + + if (_useTransparency) { + setAttribute(Qt::WA_TranslucentBackground, true); + } else { + setAttribute(Qt::WA_TranslucentBackground, false); + setAttribute(Qt::WA_OpaquePaintEvent, true); + } + + installEventFilter(this); + + hide(); +} + +void PopupSelector::popup(const QPoint &p) { + const auto screen = QGuiApplication::screenAt(p); + createWinId(); + windowHandle()->removeEventFilter(this); + windowHandle()->installEventFilter(this); + if (screen) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + setScreen(screen); +#else + windowHandle()->setScreen(screen); +#endif + } + + auto w = p; + const auto r = screen ? screen->availableGeometry() : QRect(); + if (!r.isNull()) { + if (w.x() + width() > r.x() + r.width()) { + w.setX(r.x() + r.width() - width()); + } + if (w.x() < r.x()) { + w.setX(r.x()); + } + if (w.y() + height() > r.y() + r.height()) { + w.setY(r.y() + r.height() - height()); + } + if (w.y() < r.y()) { + w.setY(r.y()); + } + } + move(w); + show(); + Platform::ShowOverAll(this); + raise(); + activateWindow(); +} + +QMargins PopupSelector::marginsForShadow() const { + const auto line = st::lineWidth; + return _useTransparency + ? st::reactionCornerShadow + : QMargins(line, line, line, line); +} + +void PopupSelector::updateShowState( + float64 progress, + float64 opacity, + bool appearing) { + if (_appearing && !appearing && !_paintBuffer.isNull()) { + paintBackgroundToBuffer(); + } + _appearing = appearing; + _appearProgress = progress; + _appearOpacity = opacity; + if (_appearing && isHidden()) { + show(); + } else if (!_appearing && _appearOpacity == 0. && !isHidden()) { + hide(); + } + update(); +} + +void PopupSelector::hideAnimated() { + if (!isHidden() && !_hiding) { + _hiding = true; + _hideAnimation.start([=] { + const auto progress = 1. - _hideAnimation.value(0.); + updateShowState(progress, progress, false); + if (!_hideAnimation.animating()) { + _hiding = false; + } + }, 1., 0., st::defaultPopupMenu.duration); + } +} + +void PopupSelector::paintBackgroundToBuffer() { + const auto factor = style::DevicePixelRatio(); + if (_paintBuffer.size() != size() * factor) { + _paintBuffer = _cachedRound.PrepareImage(size()); + } + _paintBuffer.fill(Qt::transparent); + + _cachedRound.setBackgroundColor(st::boxBg->c); + _cachedRound.setShadowColor(st::shadowFg->c); + + auto p = QPainter(&_paintBuffer); + const auto radius = std::min(_innerSize.width(), _innerSize.height()) / 2.; + const auto frame = _cachedRound.validateFrame(0, 1., radius); + const auto fill = _cachedRound.FillWithImage(p, rect(), frame); + if (!fill.isEmpty()) { + p.fillRect(fill, st::boxBg); + } +} + +void PopupSelector::paintAppearing(QPainter &p) { + p.setOpacity(_appearOpacity); + const auto factor = style::DevicePixelRatio(); + if (_paintBuffer.size() != size() * factor) { + _paintBuffer = _cachedRound.PrepareImage(size()); + } + _paintBuffer.fill(st::boxBg->c); + + auto q = QPainter(&_paintBuffer); + const auto margins = marginsForShadow(); + + if (_appearType == PopupAppearType::CenterExpand) { + const auto appearedWidth = anim::interpolate( + _innerSize.height(), + _innerSize.width(), + _appearProgress); + const auto fullWidth = margins.left() + appearedWidth + margins.right(); + const auto drawSize = QSize(fullWidth, height()); + const auto offsetX = (width() - fullWidth) / 2; + + _cachedRound.setBackgroundColor(st::boxBg->c); + _cachedRound.setShadowColor(st::shadowFg->c); + const auto radius + = std::min(_innerSize.width(), _innerSize.height()) / 2.; + _cachedRound.overlayExpandedBorder( + q, + drawSize, + _appearProgress, + radius, + radius, + 1.); + q.end(); + + p.drawImage( + QRect(QPoint(offsetX, 0), drawSize), + _paintBuffer, + QRect(QPoint(), drawSize * factor)); + } else if (_appearType == PopupAppearType::RightToLeft) { + const auto appearedWidth = anim::interpolate( + _innerSize.height(), + _innerSize.width(), + _appearProgress); + const auto fullWidth = margins.left() + + appearedWidth + + margins.right(); + const auto drawSize = QSize(fullWidth, height()); + const auto offsetX = width() - fullWidth; + + _cachedRound.setBackgroundColor(st::boxBg->c); + _cachedRound.setShadowColor(st::shadowFg->c); + const auto radius + = std::min(_innerSize.width(), _innerSize.height()) / 2.; + _cachedRound.overlayExpandedBorder( + q, + drawSize, + _appearProgress, + radius, + radius, + 1.); + q.end(); + + p.drawImage( + QRect(QPoint(offsetX, 0), drawSize), + _paintBuffer, + QRect(QPoint(), drawSize * factor)); + } else { + const auto appearedWidth = anim::interpolate( + _innerSize.height(), + _innerSize.width(), + _appearProgress); + const auto fullWidth = margins.left() + appearedWidth + margins.right(); + const auto drawSize = QSize(fullWidth, height()); + + _cachedRound.setBackgroundColor(st::boxBg->c); + _cachedRound.setShadowColor(st::shadowFg->c); + const auto radius + = std::min(_innerSize.width(), _innerSize.height()) / 2.; + _cachedRound.overlayExpandedBorder( + q, + drawSize, + _appearProgress, + radius, + radius, + 1.); + q.end(); + + } +} + +void PopupSelector::paintCollapsed(QPainter &p) { + if (_paintBuffer.isNull()) { + paintBackgroundToBuffer(); + } + p.drawImage(0, 0, _paintBuffer); +} + +void PopupSelector::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + if (!_useTransparency) { + p.fillRect(rect(), st::boxBg); + } + if (_appearing) { + paintAppearing(p); + } else { + paintCollapsed(p); + } +} + +bool PopupSelector::eventFilter(QObject *obj, QEvent *e) { + const auto type = e->type(); + if (type == QEvent::TouchBegin + || type == QEvent::TouchUpdate + || type == QEvent::TouchEnd) { + if (obj == windowHandle() && isActiveWindow()) { + const auto event = static_cast(e); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + e->setAccepted( + QApplicationPrivate::translateRawTouchEvent( + this, + event->device(), + event->touchPoints(), + event->timestamp())); +#elif QT_VERSION < QT_VERSION_CHECK(6, 2, 0) + e->setAccepted( + QApplicationPrivate::translateRawTouchEvent( + this, + event->pointingDevice(), + const_cast &>(event->points()), + event->timestamp())); +#else + e->setAccepted( + QApplicationPrivate::translateRawTouchEvent(this, event)); +#endif + return e->isAccepted(); + } + } + return RpWidget::eventFilter(obj, e); +} + +void PopupSelector::mousePressEvent(QMouseEvent *e) { + if (!rect().contains(e->pos())) { + hideAnimated(); + } +} + +void PopupSelector::focusOutEvent(QFocusEvent *e) { + hideAnimated(); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/popup_selector.h b/Telegram/SourceFiles/ui/controls/popup_selector.h new file mode 100644 index 0000000000..46a5162371 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/popup_selector.h @@ -0,0 +1,62 @@ +// This file is part of Telegram Desktop, +// the official desktop application for the Telegram messaging service. +// +// For license and copyright information please follow this link: +// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" +#include "ui/effects/animations.h" +#include "ui/effects/round_area_with_shadow.h" + +namespace Ui { + +enum class PopupAppearType { + LeftToRight, + RightToLeft, + CenterExpand, +}; + +class PopupSelector final : public RpWidget { +public: + PopupSelector( + not_null parent, + QSize size, + PopupAppearType appearType = PopupAppearType::CenterExpand); + + void updateShowState( + float64 progress, + float64 opacity, + bool appearing); + void hideAnimated(); + void popup(const QPoint &p); + + [[nodiscard]] QMargins marginsForShadow() const; + +protected: + void paintEvent(QPaintEvent *e) override; + bool eventFilter(QObject *obj, QEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void focusOutEvent(QFocusEvent *e) override; + +private: + void paintBackgroundToBuffer(); + void paintAppearing(QPainter &p); + void paintCollapsed(QPainter &p); + + const QSize _innerSize; + RoundAreaWithShadow _cachedRound; + QImage _paintBuffer; + PopupAppearType _appearType = PopupAppearType::CenterExpand; + + float64 _appearProgress = 0.; + float64 _appearOpacity = 0.; + bool _appearing = false; + bool _useTransparency = false; + bool _hiding = false; + Animations::Simple _hideAnimation; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index aa5b0cba6b..6227d3d0f9 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -417,6 +417,8 @@ PRIVATE ui/controls/invite_link_label.h ui/controls/peer_list_dummy.cpp ui/controls/peer_list_dummy.h + ui/controls/popup_selector.cpp + ui/controls/popup_selector.h ui/controls/round_video_recorder_data.h ui/controls/round_video_recorder.cpp ui/controls/round_video_recorder.h From b45896b294a34cb47d1f9bfdf545842c96f8d88e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 14 Jan 2026 10:11:56 +0300 Subject: [PATCH 025/348] Added initial implementation of userpics strip for popup selector. --- .../chat_helpers/chat_helpers.style | 2 + .../ui/controls/dynamic_images_strip.cpp | 160 ++++++++++++++++++ .../ui/controls/dynamic_images_strip.h | 50 ++++++ Telegram/cmake/td_ui.cmake | 2 + 4 files changed, 214 insertions(+) create mode 100644 Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp create mode 100644 Telegram/SourceFiles/ui/controls/dynamic_images_strip.h diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 07ed20f701..02ecf89ba7 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1746,3 +1746,5 @@ menuTranscribeDummyButton: IconButton(defaultIconButton) { } roundVideoFont: font(14px semibold); + +topPeersSelectorUserpicExpand: 0.1; diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp new file mode 100644 index 0000000000..538b10f85e --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp @@ -0,0 +1,160 @@ +// 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/dynamic_images_strip.h" + +#include "ui/dynamic_image.h" +#include "ui/painter.h" +#include "styles/style_chat_helpers.h" + +namespace Ui { +namespace { + +constexpr auto kDimmedAlpha = 0.6; +constexpr auto kHoverScaleIncrease = st::topPeersSelectorUserpicExpand; + +} // namespace + +DynamicImagesStrip::DynamicImagesStrip( + QWidget *parent, + std::vector> thumbnails, + int userpicSize, + int gap) +: RpWidget(parent) +, _thumbnails(std::move(thumbnails)) +, _userpicSize(userpicSize) +, _gap(gap) +, _scales(_thumbnails.size(), 0.) +, _alphas(_thumbnails.size(), 1.) +, _scaleTargets(_thumbnails.size(), 0.) +, _alphaTargets(_thumbnails.size(), 1.) +, _animation([=](crl::time now) { + const auto dt = (now - _animation.started()) + / float64(st::universalDuration); + auto finished = true; + for (auto i = 0; i < int(_thumbnails.size()); ++i) { + const auto progress = std::clamp(dt, 0., 1.); + _scales[i] += (_scaleTargets[i] - _scales[i]) * progress; + _alphas[i] += (_alphaTargets[i] - _alphas[i]) * progress; + if (std::abs(_scales[i] - _scaleTargets[i]) > 0.01 + || std::abs(_alphas[i] - _alphaTargets[i]) > 0.01) { + finished = false; + } + } + update(); + return !finished; +}) { + for (const auto &thumbnail : _thumbnails) { + thumbnail->subscribeToUpdates([=] { update(); }); + } + setMouseTracking(true); +} + +void DynamicImagesStrip::setProgress(float64 progress) { + _progress = progress; + update(); +} + +void DynamicImagesStrip::setClickCallback(Fn callback) { + _clickCallback = std::move(callback); +} + +void DynamicImagesStrip::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + { + const auto shift = (height() - _userpicSize) / 2; + p.translate(shift, shift); + } + auto x = 0; + const auto count = int(_thumbnails.size()); + const auto duration = 0.25; + const auto overlap = 0.15; + auto hq = PainterHighQualityEnabler(p); + for (auto i = 0; i < count; ++i) { + const auto center = count / 2; + const auto offset = (i <= center) + ? (center - i) + : (i - center); + const auto start = offset * (duration - overlap); + const auto scale = std::clamp( + (_progress - start) / duration, + 0., + 1.); + if (scale > 0.) { + const auto hoverScale = 1. + kHoverScaleIncrease * _scales[i]; + const auto alpha = _alphas[i]; + const auto cx = x + _userpicSize / 2; + const auto cy = height() / 2; + p.save(); + p.setOpacity(alpha); + p.translate(cx, cy); + p.scale(scale * hoverScale, scale * hoverScale); + p.translate(-cx, -cy); + const auto image = _thumbnails[i]->image(_userpicSize); + p.drawImage(x, 0, image); + p.restore(); + } + x += _userpicSize + _gap; + } +} + +void DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) { + const auto pos = e->pos().x(); + const auto step = _userpicSize + _gap; + const auto count = int(_thumbnails.size()); + auto newIndex = -1; + for (auto i = 0; i < count; ++i) { + const auto start = i * step; + const auto end = start + _userpicSize + _gap / 2; + if (pos >= start && pos < end) { + newIndex = i; + break; + } + } + if (_hoveredIndex != newIndex) { + const auto prev = _hoveredIndex; + _hoveredIndex = newIndex; + if (prev >= 0) { + _scaleTargets[prev] = 0.; + _alphaTargets[prev] = kDimmedAlpha; + } + if (newIndex >= 0) { + _scaleTargets[newIndex] = 1.; + _alphaTargets[newIndex] = 1.; + for (auto i = 0; i < count; ++i) { + if (i != newIndex && _alphaTargets[i] > kDimmedAlpha) { + _alphaTargets[i] = kDimmedAlpha; + } + } + } + startAnimation(); + } +} + +void DynamicImagesStrip::mousePressEvent(QMouseEvent *e) { + if (_hoveredIndex >= 0 && _clickCallback) { + _clickCallback(_hoveredIndex); + } +} + +void DynamicImagesStrip::leaveEventHook(QEvent *e) { + if (_hoveredIndex >= 0) { + _hoveredIndex = -1; + for (auto i = 0; i < int(_thumbnails.size()); ++i) { + _scaleTargets[i] = 0.; + _alphaTargets[i] = 1.; + } + startAnimation(); + } +} + +void DynamicImagesStrip::startAnimation() { + if (!_animation.animating()) { + _animation.start(); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h new file mode 100644 index 0000000000..49d6fe53b7 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h @@ -0,0 +1,50 @@ +// This file is part of Telegram Desktop, +// the official desktop application for the Telegram messaging service. +// +// For license and copyright information please follow this link: +// https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" +#include "ui/effects/animations.h" + +namespace Ui { + +class DynamicImage; + +class DynamicImagesStrip final : public RpWidget { +public: + DynamicImagesStrip( + QWidget *parent, + std::vector> thumbnails, + int userpicSize, + int gap); + + void setProgress(float64 progress); + void setClickCallback(Fn callback); + +protected: + void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void leaveEventHook(QEvent *e) override; + +private: + void startAnimation(); + + std::vector> _thumbnails; + int _userpicSize = 0; + int _gap = 0; + float64 _progress = 0.; + int _hoveredIndex = -1; + std::vector _scales; + std::vector _alphas; + std::vector _scaleTargets; + std::vector _alphaTargets; + Animations::Basic _animation; + Fn _clickCallback; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 6227d3d0f9..32598631f5 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -403,6 +403,8 @@ PRIVATE ui/controls/delete_message_context_action.h ui/controls/download_bar.cpp ui/controls/download_bar.h + ui/controls/dynamic_images_strip.cpp + ui/controls/dynamic_images_strip.h ui/controls/emoji_button.cpp ui/controls/emoji_button.h ui/controls/feature_list.cpp From e2ed9baacb9665ebd718687d7a2a3970ee3904e1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 13 Jan 2026 07:26:00 +0300 Subject: [PATCH 026/348] Added initial popup selector for top peers. --- Telegram/CMakeLists.txt | 2 + .../chat_helpers/chat_helpers.style | 3 + .../view/history_view_top_peers_selector.cpp | 124 ++++++++++++++++++ .../view/history_view_top_peers_selector.h | 24 ++++ 4 files changed, 153 insertions(+) create mode 100644 Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_top_peers_selector.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0c4e4c2162..d3e757a5d6 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -880,6 +880,8 @@ PRIVATE history/view/reactions/history_view_reactions_strip.h history/view/reactions/history_view_reactions_tabs.cpp history/view/reactions/history_view_reactions_tabs.h + history/view/history_view_top_peers_selector.cpp + history/view/history_view_top_peers_selector.h history/view/history_view_about_view.cpp history/view/history_view_about_view.h history/view/history_view_bottom_info.cpp diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 02ecf89ba7..1354506f5a 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1747,4 +1747,7 @@ menuTranscribeDummyButton: IconButton(defaultIconButton) { roundVideoFont: font(14px semibold); +topPeersSelectorUserpicSize: 30px; +topPeersSelectorUserpicGap: 8px; +topPeersSelectorPadding: 6px; topPeersSelectorUserpicExpand: 0.1; diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp new file mode 100644 index 0000000000..a599aa3af0 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -0,0 +1,124 @@ +// 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_top_peers_selector.h" + +#include "apiwrap.h" +#include "main/session/session_show.h" +#include "chat_helpers/share_message_phrase_factory.h" +#include "ui/controls/dynamic_images_strip.h" +#include "ui/controls/popup_selector.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/effects/animations.h" +#include "ui/rect.h" +#include "data/components/top_peers.h" +#include "history/history.h" +#include "data/data_peer.h" +#include "data/data_user.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "styles/style_chat_helpers.h" + +namespace HistoryView { +namespace { + +constexpr auto kMaxPeers = 5; + +[[nodiscard]] std::vector> CollectPeers( + not_null session) { + const auto user = session->user(); + auto topPeers = session->topPeers().list(); + const auto it = ranges::find(topPeers, user); + if (it != topPeers.end()) { + topPeers.erase(it); + } + auto result = std::vector>(); + result.push_back(user); + for (const auto &peer : topPeers | ranges::views::take(kMaxPeers - 1)) { + result.push_back(peer); + } + return result; +} + +} // namespace + +void ShowTopPeersSelector( + not_null parent, + std::shared_ptr show, + FullMsgId fullId) { + const auto session = &show->session(); + const auto peers = CollectPeers(session); + auto thumbnails = std::vector>(); + thumbnails.reserve(peers.size()); + for (const auto &peer : peers) { + thumbnails.push_back(peer->isSelf() + ? Ui::MakeSavedMessagesThumbnail() + : Ui::MakeUserpicThumbnail(peer)); + } + + const auto send = [=](not_null peer) { + if (const auto item = session->data().message(fullId)) { + session->api().forwardMessages( + Data::ResolvedForwardDraft{ .items = { item } }, + Api::SendAction(session->data().history(peer)), + [=] { + using namespace ChatHelpers; + auto text = rpl::variable( + ForwardedMessagePhrase({ + .toCount = 1, + .singleMessage = 1, + .to1 = peer, + })).current(); + show->showToast(std::move(text)); + }); + } + }; + + const auto contentWidth = peers.size() * st::topPeersSelectorUserpicSize + + (peers.size() - 1) * st::topPeersSelectorUserpicGap; + const auto contentHeight = int( + st::topPeersSelectorUserpicSize + * (1. + st::topPeersSelectorUserpicExpand)); + const auto selectorWidth = contentWidth + + 2 * st::topPeersSelectorPadding; + const auto selectorHeight = contentHeight + + 2 * st::topPeersSelectorPadding; + const auto selector = Ui::CreateChild( + parent, + QSize(selectorWidth, selectorHeight)); + const auto userpicsWidget = Ui::CreateChild( + selector, + std::move(thumbnails), + st::topPeersSelectorUserpicSize, + st::topPeersSelectorUserpicGap); + const auto margins = selector->marginsForShadow(); + const auto x = (selectorWidth - contentWidth) / 2 + margins.left(); + const auto y = (selectorHeight - contentHeight) / 2 + margins.top(); + userpicsWidget->setGeometry( + QRect(x, y, contentWidth, contentHeight) + + Margins(int(st::topPeersSelectorUserpicSize + * st::topPeersSelectorUserpicExpand))); + userpicsWidget->setClickCallback([=](int index) { + send(peers[index]); + selector->hideAnimated(); + }); + userpicsWidget->setCursor(style::cur_pointer); + selector->updateShowState(0, 0, true); + selector->popup(QCursor::pos()); + + auto animation + = selector->lifetime().make_state(); + constexpr auto kShift = 0.15; + animation->start([=](float64 value) { + const auto userpicsProgress = std::clamp((value - kShift), 0., 1.); + userpicsWidget->setProgress(anim::easeInQuint(1, userpicsProgress)); + value = std::clamp(value, 0., 1.); + selector->updateShowState(value, value, true); + }, 0., 1. + kShift, st::fadeWrapDuration * 3, anim::easeOutQuint); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h new file mode 100644 index 0000000000..f1bd9588a5 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h @@ -0,0 +1,24 @@ +// 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 Ui { +class RpWidget; +} // namespace Ui + +namespace Main { +class SessionShow; +} // namespace Main + +namespace HistoryView { + +void ShowTopPeersSelector( + not_null parent, + std::shared_ptr show, + FullMsgId fullId); + +} // namespace HistoryView From 2f6e8bbef97c548c89876ecedae035262ce0b14c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 14 Jan 2026 09:26:12 +0300 Subject: [PATCH 027/348] Added ability to show popup selector for top peers from fast share. --- .../chat_helpers/chat_helpers.style | 1 + .../SourceFiles/core/click_handler_types.h | 1 + .../history/history_inner_widget.cpp | 19 ++++++++++++++++ .../history/view/history_view_list_widget.cpp | 22 +++++++++++++++++++ .../history/view/history_view_message.cpp | 4 +++- .../view/history_view_top_peers_selector.cpp | 7 ++++-- .../view/history_view_top_peers_selector.h | 3 ++- 7 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 1354506f5a..2bf06ae912 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1751,3 +1751,4 @@ topPeersSelectorUserpicSize: 30px; topPeersSelectorUserpicGap: 8px; topPeersSelectorPadding: 6px; topPeersSelectorUserpicExpand: 0.1; +topPeersSelectorSkip: point(11px, -5px); diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 520b1282a1..a1cd3bd35b 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -18,6 +18,7 @@ constexpr auto kReactionsCountEmojiProperty = 0x05; constexpr auto kDocumentFilenameTooltipProperty = 0x06; constexpr auto kPhoneNumberLinkProperty = 0x07; constexpr auto kTodoListItemIdProperty = 0x08; +constexpr auto kFastShareProperty = 0x09; namespace Ui { class Show; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 333b8d106e..41946075c8 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_reaction_preview.h" #include "history/view/history_view_quick_action.h" #include "history/view/history_view_emoji_interactions.h" +#include "history/view/history_view_top_peers_selector.h" #include "history/history_item_components.h" #include "history/history_item_text.h" #include "payments/payments_reaction_process.h" @@ -2307,6 +2308,24 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { e, session().data().reactions().favoriteId())) { return; + } else if (link && link->property(kFastShareProperty).value()) { + if (const auto item = _dragStateItem) { + const auto view = viewByItem(item); + const auto rightSize = view->rightActionSize().value_or(QSize()); + const auto top = itemTop(view) + + view->height() + - _visibleAreaTop + - rightSize.height(); + const auto right = rect::right(view->innerGeometry()) + - st::historyFastShareLeft + - rightSize.width(); + HistoryView::ShowTopPeersSelector( + this, + _controller->uiShow(), + item->fullId(), + parentWidget()->mapToGlobal(QPoint(right, top))); + return; + } } auto selectedState = getSelectionState(); diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 5127738984..2e396ce649 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_service_message.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_translate_tracker.h" +#include "history/view/history_view_top_peers_selector.h" #include "history/view/history_view_quick_action.h" #include "chat_helpers/message_field.h" #include "mainwindow.h" @@ -55,6 +56,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_theme.h" #include "ui/chat/chat_style.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" #include "boxes/delete_messages_box.h" @@ -2849,6 +2851,26 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { : _overElement ? _overElement->data().get() : nullptr; + if (link + && link->property(kFastShareProperty).value() + && overItem) { + if (const auto view = viewForItem(overItem)) { + const auto rightSize = view->rightActionSize().value_or(QSize()); + const auto top = itemTop(view) + + view->height() + - _visibleTop + - rightSize.height(); + const auto right = rect::right(view->innerGeometry()) + - st::historyFastShareLeft + - rightSize.width(); + ShowTopPeersSelector( + this, + controller()->uiShow(), + overItem->fullId(), + parentWidget()->mapToGlobal(QPoint(right, top))); + return; + } + } const auto clickedReaction = Reactions::ReactionIdOfLink(link); const auto linkPhoneNumber = link ? link->property(kPhoneNumberLinkProperty).toString() diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 712f3f0fb9..a5263e3092 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -4017,7 +4017,7 @@ ClickHandlerPtr Message::prepareRightActionLink() const { } }; }; - return std::make_shared([=]( + const auto result = std::make_shared([=]( ClickContext context) { const auto controller = ExtractController(context); if (!controller || controller->session().uniqueId() != sessionId) { @@ -4039,6 +4039,8 @@ ClickHandlerPtr Message::prepareRightActionLink() const { } } }); + result->setProperty(kFastShareProperty, QVariant::fromValue(true)); + return result; } ClickHandlerPtr Message::fastReplyLink() const { diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp index a599aa3af0..5c14913a0b 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -49,7 +49,8 @@ constexpr auto kMaxPeers = 5; void ShowTopPeersSelector( not_null parent, std::shared_ptr show, - FullMsgId fullId) { + FullMsgId fullId, + QPoint globalPos) { const auto session = &show->session(); const auto peers = CollectPeers(session); auto thumbnails = std::vector>(); @@ -108,7 +109,9 @@ void ShowTopPeersSelector( }); userpicsWidget->setCursor(style::cur_pointer); selector->updateShowState(0, 0, true); - selector->popup(QCursor::pos()); + selector->popup((!globalPos.isNull() ? globalPos : QCursor::pos()) + - QPoint(selector->width() / 2, selector->height()) + + st::topPeersSelectorSkip); auto animation = selector->lifetime().make_state(); diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h index f1bd9588a5..6de3b03f8e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.h @@ -19,6 +19,7 @@ namespace HistoryView { void ShowTopPeersSelector( not_null parent, std::shared_ptr show, - FullMsgId fullId); + FullMsgId fullId, + QPoint globalPos); } // namespace HistoryView From 70604b6e8b8f79a04f8aad9dee9231cedf076914 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 14 Jan 2026 15:57:52 +0300 Subject: [PATCH 028/348] Added simple tooltip to peers in popup selector for top peers. --- .../chat_helpers/chat_helpers.style | 10 +++ .../view/history_view_top_peers_selector.cpp | 71 ++++++++++++++++--- .../ui/controls/dynamic_images_strip.cpp | 26 +++++++ .../ui/controls/dynamic_images_strip.h | 8 +++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 2bf06ae912..fa2c412507 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1752,3 +1752,13 @@ topPeersSelectorUserpicGap: 8px; topPeersSelectorPadding: 6px; topPeersSelectorUserpicExpand: 0.1; topPeersSelectorSkip: point(11px, -5px); + +topPeersSelectorImportantTooltip: ImportantTooltip(defaultImportantTooltip) { + bg: scrollBarBgOver; + radius: 9px; +} +topPeersSelectorImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { + style: TextStyle(defaultTextStyle) { + font: font(semibold 12px); + } +} diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp index 5c14913a0b..5a174a1afe 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -7,20 +7,26 @@ #include "history/view/history_view_top_peers_selector.h" #include "apiwrap.h" -#include "main/session/session_show.h" +#include "base/unique_qptr.h" #include "chat_helpers/share_message_phrase_factory.h" +#include "data/components/top_peers.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "history/history.h" +#include "info/profile/info_profile_values.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "main/session/session_show.h" #include "ui/controls/dynamic_images_strip.h" #include "ui/controls/popup_selector.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "ui/effects/animations.h" #include "ui/rect.h" -#include "data/components/top_peers.h" -#include "history/history.h" -#include "data/data_peer.h" -#include "data/data_user.h" -#include "data/data_session.h" -#include "main/main_session.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/tooltip.h" +#include "ui/wrap/padding_wrap.h" #include "styles/style_chat_helpers.h" namespace HistoryView { @@ -108,15 +114,60 @@ void ShowTopPeersSelector( selector->hideAnimated(); }); userpicsWidget->setCursor(style::cur_pointer); + + struct State { + base::unique_qptr tooltip; + Ui::Animations::Simple animation; + }; + const auto state = selector->lifetime().make_state(); + + userpicsWidget->hoveredItemValue( + ) | rpl::on_next([=](Ui::HoveredItemInfo info) { + if (info.index < 0) { + state->tooltip = nullptr; + return; + } + state->tooltip = base::make_unique_q( + parent, + object_ptr>( + selector, + Ui::MakeNiceTooltipLabel( + parent, + Info::Profile::NameValue(peers[info.index]) | rpl::map(tr::rich), + userpicsWidget->width(), + st::topPeersSelectorImportantTooltipLabel), + st::topPeersSelectorImportantTooltip.padding), + st::topPeersSelectorImportantTooltip); + state->tooltip->setWindowFlags(Qt::WindowFlags(Qt::ToolTip) + | Qt::BypassWindowManagerHint + | Qt::NoDropShadowWindowHint + | Qt::FramelessWindowHint); + state->tooltip->setAttribute(Qt::WA_NoSystemBackground, true); + state->tooltip->setAttribute(Qt::WA_TranslucentBackground, true); + state->tooltip->setAttribute(Qt::WA_TransparentForMouseEvents, true); + const auto step = st::topPeersSelectorUserpicSize + + st::topPeersSelectorUserpicGap; + const auto shift = (userpicsWidget->height() + - st::topPeersSelectorUserpicSize) / 2; + const auto localX = info.index * step + shift; + const auto avatarRect = QRect( + localX, + -shift, + st::topPeersSelectorUserpicSize, + st::topPeersSelectorUserpicSize); + const auto globalRect = QRect( + userpicsWidget->mapToGlobal(avatarRect.topLeft()), + avatarRect.size()); + state->tooltip->pointAt(globalRect, RectPart::Top); + state->tooltip->toggleAnimated(true); + }, selector->lifetime()); selector->updateShowState(0, 0, true); selector->popup((!globalPos.isNull() ? globalPos : QCursor::pos()) - QPoint(selector->width() / 2, selector->height()) + st::topPeersSelectorSkip); - auto animation - = selector->lifetime().make_state(); constexpr auto kShift = 0.15; - animation->start([=](float64 value) { + state->animation.start([=](float64 value) { const auto userpicsProgress = std::clamp((value - kShift), 0., 1.); userpicsWidget->setProgress(anim::easeInQuint(1, userpicsProgress)); value = std::clamp(value, 0., 1.); diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp index 538b10f85e..841fc9582f 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp @@ -9,6 +9,7 @@ #include "ui/dynamic_image.h" #include "ui/painter.h" #include "styles/style_chat_helpers.h" +#include "styles/style_widgets.h" namespace Ui { namespace { @@ -62,6 +63,10 @@ void DynamicImagesStrip::setClickCallback(Fn callback) { _clickCallback = std::move(callback); } +rpl::producer DynamicImagesStrip::hoveredItemValue() const { + return _hoveredItem.events(); +} + void DynamicImagesStrip::paintEvent(QPaintEvent *e) { auto p = QPainter(this); { @@ -129,6 +134,9 @@ void DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) { _alphaTargets[i] = kDimmedAlpha; } } + updateHoveredItem(newIndex); + } else { + updateHoveredItem(-1); } startAnimation(); } @@ -147,6 +155,7 @@ void DynamicImagesStrip::leaveEventHook(QEvent *e) { _scaleTargets[i] = 0.; _alphaTargets[i] = 1.; } + updateHoveredItem(-1); startAnimation(); } } @@ -157,4 +166,21 @@ void DynamicImagesStrip::startAnimation() { } } +void DynamicImagesStrip::updateHoveredItem(int index) { + if (index < 0) { + _hoveredItem.fire({ .index = -1, .globalPos = {} }); + return; + } + const auto step = _userpicSize + _gap; + const auto shift = (height() - _userpicSize) / 2; + const auto x = index * step + shift; + const auto avatarRect = QRect(x, shift, _userpicSize, _userpicSize); + const auto globalRect = QRect( + mapToGlobal(avatarRect.topLeft()), + avatarRect.size()); + _hoveredItem.fire({ + .index = index, + .globalPos = globalRect.center() }); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h index 49d6fe53b7..0a1e43b196 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h @@ -13,6 +13,11 @@ namespace Ui { class DynamicImage; +struct HoveredItemInfo { + int index = -1; + QPoint globalPos; +}; + class DynamicImagesStrip final : public RpWidget { public: DynamicImagesStrip( @@ -23,6 +28,7 @@ public: void setProgress(float64 progress); void setClickCallback(Fn callback); + [[nodiscard]] rpl::producer hoveredItemValue() const; protected: void paintEvent(QPaintEvent *e) override; @@ -32,6 +38,7 @@ protected: private: void startAnimation(); + void updateHoveredItem(int index); std::vector> _thumbnails; int _userpicSize = 0; @@ -44,6 +51,7 @@ private: std::vector _alphaTargets; Animations::Basic _animation; Fn _clickCallback; + rpl::event_stream _hoveredItem; }; From 03d7736b99a4933000bbcf069706dc9cfa8565e5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 10:13:23 +0300 Subject: [PATCH 029/348] Added keyboard support to popup selector for top peers. --- .../view/history_view_top_peers_selector.cpp | 38 +++++++- .../ui/controls/dynamic_images_strip.cpp | 93 ++++++++++++------- .../ui/controls/dynamic_images_strip.h | 4 + 3 files changed, 99 insertions(+), 36 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp index 5a174a1afe..51e4f6e770 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -109,31 +109,45 @@ void ShowTopPeersSelector( QRect(x, y, contentWidth, contentHeight) + Margins(int(st::topPeersSelectorUserpicSize * st::topPeersSelectorUserpicExpand))); - userpicsWidget->setClickCallback([=](int index) { - send(peers[index]); - selector->hideAnimated(); - }); userpicsWidget->setCursor(style::cur_pointer); struct State { base::unique_qptr tooltip; Ui::Animations::Simple animation; + bool finishing = false; }; const auto state = selector->lifetime().make_state(); + const auto hideAll = [=] { + state->finishing = true; + selector->setAttribute(Qt::WA_TransparentForMouseEvents, true); + state->tooltip->toggleAnimated(false); + selector->hideAnimated(); + }; + + userpicsWidget->setClickCallback([=](int index) { + if (state->finishing) { + return; + } + send(peers[index]); + hideAll(); + }); userpicsWidget->hoveredItemValue( ) | rpl::on_next([=](Ui::HoveredItemInfo info) { if (info.index < 0) { state->tooltip = nullptr; return; } + using namespace Info::Profile; state->tooltip = base::make_unique_q( parent, object_ptr>( selector, Ui::MakeNiceTooltipLabel( parent, - Info::Profile::NameValue(peers[info.index]) | rpl::map(tr::rich), + peers[info.index]->isSelf() + ? tr::lng_saved_messages(tr::rich) + : NameValue(peers[info.index]) | rpl::map(tr::rich), userpicsWidget->width(), st::topPeersSelectorImportantTooltipLabel), st::topPeersSelectorImportantTooltip.padding), @@ -165,6 +179,20 @@ void ShowTopPeersSelector( selector->popup((!globalPos.isNull() ? globalPos : QCursor::pos()) - QPoint(selector->width() / 2, selector->height()) + st::topPeersSelectorSkip); + selector->events( + ) | rpl::on_next([=](not_null e) { + if (e->type() == QEvent::KeyPress) { + const auto key = static_cast(e.get()); + if (key->key() == Qt::Key_Escape) { + hideAll(); + } else { + userpicsWidget->handleKeyPressEvent(key); + } + } + }, selector->lifetime()); + crl::on_main(selector, [=] { + selector->setFocus(); + }); constexpr auto kShift = 0.15; state->animation.start([=](float64 value) { diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp index 841fc9582f..e23ea6d1d0 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp @@ -67,6 +67,10 @@ rpl::producer DynamicImagesStrip::hoveredItemValue() const { return _hoveredItem.events(); } +void DynamicImagesStrip::handleKeyPressEvent(QKeyEvent *e) { + keyPressEvent(e); +} + void DynamicImagesStrip::paintEvent(QPaintEvent *e) { auto p = QPainter(this); { @@ -119,27 +123,7 @@ void DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) { break; } } - if (_hoveredIndex != newIndex) { - const auto prev = _hoveredIndex; - _hoveredIndex = newIndex; - if (prev >= 0) { - _scaleTargets[prev] = 0.; - _alphaTargets[prev] = kDimmedAlpha; - } - if (newIndex >= 0) { - _scaleTargets[newIndex] = 1.; - _alphaTargets[newIndex] = 1.; - for (auto i = 0; i < count; ++i) { - if (i != newIndex && _alphaTargets[i] > kDimmedAlpha) { - _alphaTargets[i] = kDimmedAlpha; - } - } - updateHoveredItem(newIndex); - } else { - updateHoveredItem(-1); - } - startAnimation(); - } + setSelectedIndex(newIndex); } void DynamicImagesStrip::mousePressEvent(QMouseEvent *e) { @@ -149,15 +133,7 @@ void DynamicImagesStrip::mousePressEvent(QMouseEvent *e) { } void DynamicImagesStrip::leaveEventHook(QEvent *e) { - if (_hoveredIndex >= 0) { - _hoveredIndex = -1; - for (auto i = 0; i < int(_thumbnails.size()); ++i) { - _scaleTargets[i] = 0.; - _alphaTargets[i] = 1.; - } - updateHoveredItem(-1); - startAnimation(); - } + setSelectedIndex(-1); } void DynamicImagesStrip::startAnimation() { @@ -180,7 +156,62 @@ void DynamicImagesStrip::updateHoveredItem(int index) { avatarRect.size()); _hoveredItem.fire({ .index = index, - .globalPos = globalRect.center() }); + .globalPos = globalRect.center(), + }); +} + +void DynamicImagesStrip::keyPressEvent(QKeyEvent *e) { + const auto count = int(_thumbnails.size()); + if (count == 0) { + return; + } + const auto key = e->key(); + if (key == Qt::Key_Left || key == Qt::Key_Up) { + const auto newIndex = (_hoveredIndex < 0) + ? (count - 1) + : ((_hoveredIndex - 1 + count) % count); + setSelectedIndex(newIndex); + } else if (key == Qt::Key_Right || key == Qt::Key_Down) { + const auto newIndex = (_hoveredIndex < 0) + ? 0 + : ((_hoveredIndex + 1) % count); + setSelectedIndex(newIndex); + } else if ((key == Qt::Key_Return + || key == Qt::Key_Enter + || key == Qt::Key_Space) + && _hoveredIndex >= 0 && _clickCallback) { + _clickCallback(_hoveredIndex); + } +} + +void DynamicImagesStrip::setSelectedIndex(int index) { + if (_hoveredIndex == index) { + return; + } + const auto prev = _hoveredIndex; + const auto count = int(_thumbnails.size()); + _hoveredIndex = index; + if (prev >= 0) { + _scaleTargets[prev] = 0.; + _alphaTargets[prev] = kDimmedAlpha; + } + if (index >= 0) { + _scaleTargets[index] = 1.; + _alphaTargets[index] = 1.; + for (auto i = 0; i < count; ++i) { + if (i != index && _alphaTargets[i] > kDimmedAlpha) { + _alphaTargets[i] = kDimmedAlpha; + } + } + updateHoveredItem(index); + } else { + for (auto i = 0; i < count; ++i) { + _scaleTargets[i] = 0.; + _alphaTargets[i] = 1.; + } + updateHoveredItem(-1); + } + startAnimation(); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h index 0a1e43b196..e492467ad4 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h @@ -30,15 +30,19 @@ public: void setClickCallback(Fn callback); [[nodiscard]] rpl::producer hoveredItemValue() const; + void handleKeyPressEvent(QKeyEvent *e); + protected: void paintEvent(QPaintEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void leaveEventHook(QEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; private: void startAnimation(); void updateHoveredItem(int index); + void setSelectedIndex(int index); std::vector> _thumbnails; int _userpicSize = 0; From d0448569b7fb7af65beb035375c0e14db8967831 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 11:27:17 +0300 Subject: [PATCH 030/348] Fixed position of popup selector for messages with inline reactions. --- Telegram/SourceFiles/chat_helpers/chat_helpers.style | 4 ++-- Telegram/SourceFiles/history/history_inner_widget.cpp | 4 ++++ .../SourceFiles/history/view/history_view_list_widget.cpp | 4 ++++ Telegram/SourceFiles/history/view/history_view_message.cpp | 1 + .../history/view/history_view_top_peers_selector.cpp | 4 +++- .../history/view/media/history_view_media_unwrapped.cpp | 5 ++++- .../history/view/reactions/history_view_reactions_button.h | 1 + Telegram/SourceFiles/ui/chat/chat.style | 2 +- 8 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index fa2c412507..08b4c44f3f 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1747,11 +1747,11 @@ menuTranscribeDummyButton: IconButton(defaultIconButton) { roundVideoFont: font(14px semibold); -topPeersSelectorUserpicSize: 30px; +topPeersSelectorUserpicSize: 31px; topPeersSelectorUserpicGap: 8px; topPeersSelectorPadding: 6px; topPeersSelectorUserpicExpand: 0.1; -topPeersSelectorSkip: point(11px, -5px); +topPeersSelectorSkip: point(9px, -5px); topPeersSelectorImportantTooltip: ImportantTooltip(defaultImportantTooltip) { bg: scrollBarBgOver; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 41946075c8..c6db0d0bf2 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -2312,8 +2312,12 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (const auto item = _dragStateItem) { const auto view = viewByItem(item); const auto rightSize = view->rightActionSize().value_or(QSize()); + const auto reactionsSkip = view->embedReactionsInBubble() + ? 0 + : view->reactionButtonParameters({}, {}).reactionsHeight; const auto top = itemTop(view) + view->height() + - reactionsSkip - _visibleAreaTop - rightSize.height(); const auto right = rect::right(view->innerGeometry()) diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index 2e396ce649..a8fd3ea847 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -2856,8 +2856,12 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { && overItem) { if (const auto view = viewForItem(overItem)) { const auto rightSize = view->rightActionSize().value_or(QSize()); + const auto reactionsSkip = view->embedReactionsInBubble() + ? 0 + : view->reactionButtonParameters({}, {}).reactionsHeight; const auto top = itemTop(view) + view->height() + - reactionsSkip - _visibleTop - rightSize.height(); const auto right = rect::right(view->innerGeometry()) diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index a5263e3092..a24b8ae9e4 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -3249,6 +3249,7 @@ Reactions::ButtonParameters Message::reactionButtonParameters( const auto reactionsHeight = (_reactions && !embedReactionsInBubble()) ? (st::mediaInBubbleSkip + _reactions->height()) : 0; + result.reactionsHeight = reactionsHeight; const auto innerHeight = geometry.height() - keyboardHeight - reactionsHeight; diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp index 51e4f6e770..ae94edeabf 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -120,8 +120,10 @@ void ShowTopPeersSelector( const auto hideAll = [=] { state->finishing = true; + if (state->tooltip) { + state->tooltip->toggleAnimated(false); + } selector->setAttribute(Qt::WA_TransparentForMouseEvents, true); - state->tooltip->toggleAnimated(false); selector->hideAnimated(); }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp index 49ebdbe1ec..eba5bb2c49 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_unwrapped.cpp @@ -580,7 +580,10 @@ QPoint UnwrappedMedia::resolveCustomInfoRightBottom() const { const auto fullRight = calculateFullRight(inner); const auto skipx = st::msgDateImgPadding.x(); const auto skipy = st::msgDateImgPadding.y(); - return QPoint(fullRight - skipx, fullBottom - skipy); + const auto infoWidth = _parent->infoWidth() + + st::msgDateImgPadding.x() * 2 + + st::msgReplyPadding.left(); + return QPoint(fullRight - skipx - infoWidth, fullBottom - skipy); } std::unique_ptr UnwrappedMedia::stickerTakePlayer( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h index 4a1842d995..75e6d45384 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.h @@ -55,6 +55,7 @@ struct ButtonParameters { QPoint center; QPoint pointer; QPoint globalPointer; + int reactionsHeight = 0; int reactionsCount = 1; int visibleTop = 0; int visibleBottom = 0; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 8f05aacdcd..0b0f5a0178 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -589,7 +589,7 @@ historyAdminLogWhatIsThis: IconButton(historyAttach) { iconOver: icon {{ "menu/faq", windowActiveTextFg }}; } -historyFastShareSize: 31px; +historyFastShareSize: topPeersSelectorUserpicSize; historyFastShareLeft: 13px; historyFastShareBottom: 5px; historyFastShareIcon: icon {{ "fast_share", msgServiceFg }}; From 4c655a6b9d8f03f30e848917e8f15b016315a844 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 12:05:53 +0300 Subject: [PATCH 031/348] Added support of right press-move-release to popup selector for peers. --- Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp | 7 +++++++ Telegram/SourceFiles/ui/controls/dynamic_images_strip.h | 2 ++ 2 files changed, 9 insertions(+) diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp index e23ea6d1d0..27b696f679 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp @@ -127,11 +127,18 @@ void DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) { } void DynamicImagesStrip::mousePressEvent(QMouseEvent *e) { + _pressed = true; if (_hoveredIndex >= 0 && _clickCallback) { _clickCallback(_hoveredIndex); } } +void DynamicImagesStrip::mouseReleaseEvent(QMouseEvent *e) { + if (!_pressed && _hoveredIndex >= 0 && _clickCallback) { + _clickCallback(_hoveredIndex); + } +} + void DynamicImagesStrip::leaveEventHook(QEvent *e) { setSelectedIndex(-1); } diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h index e492467ad4..9b78ddf7a7 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h @@ -36,6 +36,7 @@ protected: void paintEvent(QPaintEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; void leaveEventHook(QEvent *e) override; void keyPressEvent(QKeyEvent *e) override; @@ -56,6 +57,7 @@ private: Animations::Basic _animation; Fn _clickCallback; rpl::event_stream _hoveredItem; + bool _pressed = false; }; From 5865891797abeeeac884b9f62585bc10f7220b56 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 12:46:33 +0300 Subject: [PATCH 032/348] Fixed memory clearing for popup selector. --- .../view/history_view_top_peers_selector.cpp | 25 +++++++++++++------ .../ui/controls/popup_selector.cpp | 11 ++++++-- .../SourceFiles/ui/controls/popup_selector.h | 2 ++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp index ae94edeabf..50644eea78 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_peers_selector.cpp @@ -94,9 +94,25 @@ void ShowTopPeersSelector( + 2 * st::topPeersSelectorPadding; const auto selectorHeight = contentHeight + 2 * st::topPeersSelectorPadding; - const auto selector = Ui::CreateChild( + + struct State { + base::unique_qptr selector; + base::unique_qptr tooltip; + Ui::Animations::Simple animation; + bool finishing = false; + }; + const auto state = std::make_shared(); + + state->selector = base::make_unique_q( parent, QSize(selectorWidth, selectorHeight)); + const auto selector = state->selector.get(); + selector->setHideFinishedCallback([=, state = std::weak_ptr(state)] { + if (const auto s = state.lock()) { + s->selector = nullptr; + s->tooltip = nullptr; + } + }); const auto userpicsWidget = Ui::CreateChild( selector, std::move(thumbnails), @@ -111,13 +127,6 @@ void ShowTopPeersSelector( * st::topPeersSelectorUserpicExpand))); userpicsWidget->setCursor(style::cur_pointer); - struct State { - base::unique_qptr tooltip; - Ui::Animations::Simple animation; - bool finishing = false; - }; - const auto state = selector->lifetime().make_state(); - const auto hideAll = [=] { state->finishing = true; if (state->tooltip) { diff --git a/Telegram/SourceFiles/ui/controls/popup_selector.cpp b/Telegram/SourceFiles/ui/controls/popup_selector.cpp index a79e319bb6..a80823ddb3 100644 --- a/Telegram/SourceFiles/ui/controls/popup_selector.cpp +++ b/Telegram/SourceFiles/ui/controls/popup_selector.cpp @@ -117,18 +117,25 @@ void PopupSelector::updateShowState( } void PopupSelector::hideAnimated() { - if (!isHidden() && !_hiding) { + if (/*!isHidden() && */!_hiding) { _hiding = true; _hideAnimation.start([=] { const auto progress = 1. - _hideAnimation.value(0.); updateShowState(progress, progress, false); if (!_hideAnimation.animating()) { _hiding = false; + if (_hideFinishedCallback) { + _hideFinishedCallback(); + } } - }, 1., 0., st::defaultPopupMenu.duration); + }, _appearProgress, 0., st::defaultPopupMenu.duration); } } +void PopupSelector::setHideFinishedCallback(Fn callback) { + _hideFinishedCallback = std::move(callback); +} + void PopupSelector::paintBackgroundToBuffer() { const auto factor = style::DevicePixelRatio(); if (_paintBuffer.size() != size() * factor) { diff --git a/Telegram/SourceFiles/ui/controls/popup_selector.h b/Telegram/SourceFiles/ui/controls/popup_selector.h index 46a5162371..8ea2d3aa6f 100644 --- a/Telegram/SourceFiles/ui/controls/popup_selector.h +++ b/Telegram/SourceFiles/ui/controls/popup_selector.h @@ -31,6 +31,7 @@ public: bool appearing); void hideAnimated(); void popup(const QPoint &p); + void setHideFinishedCallback(Fn callback); [[nodiscard]] QMargins marginsForShadow() const; @@ -56,6 +57,7 @@ private: bool _useTransparency = false; bool _hiding = false; Animations::Simple _hideAnimation; + Fn _hideFinishedCallback; }; From 8b3498aaf12eba360b0b7c13c6cd4c54abf164a4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 20 Jan 2026 12:10:40 +0300 Subject: [PATCH 033/348] Fixed ability to drag'n'drop to non-primary window chat while searching. --- Telegram/SourceFiles/history/history_widget.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 5d6f938711..38ce973592 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -6550,7 +6550,11 @@ bool HistoryWidget::confirmSendingFiles( std::optional overrideSendImagesAsPhotos, const QString &insertTextOnCancel) { if (!canWriteMessage()) { - return false; + if (_composeSearch) { + _composeSearch->hideAnimated(); + } else { + return false; + } } const auto hasImage = data->hasImage(); From 1696e4495acff5c3c0d7144e995aba8ef8efadde Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Dec 2025 16:43:09 +0400 Subject: [PATCH 034/348] Use /DEBUG:FASTLINK for debug builds. --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index ba366ade3e..ece5903560 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit ba366ade3e70c64d1dd854382abd74f532ba41a1 +Subproject commit ece5903560c617ce1fea685e236097715ce3b3e0 From fa190a74dac897aef801642aba6712e9e42bb0d4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 14 Jan 2026 10:39:20 +0400 Subject: [PATCH 035/348] Enable LTO by an option. --- Telegram/build/build.bat | 2 +- Telegram/build/build.sh | 2 +- Telegram/build/docker/build.sh | 2 +- cmake | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram/build/build.bat b/Telegram/build/build.bat index a8062abd16..5c26c72373 100644 --- a/Telegram/build/build.bat +++ b/Telegram/build/build.bat @@ -186,7 +186,7 @@ if %AlphaVersion% neq 0 ( cd "%HomePath%" -call configure.bat +call configure.bat -DDESKTOP_APP_ENABLE_LTO=ON if %errorlevel% neq 0 goto error cd "%SolutionPath%" diff --git a/Telegram/build/build.sh b/Telegram/build/build.sh index aa2dcc1ecf..b5cf854c39 100755 --- a/Telegram/build/build.sh +++ b/Telegram/build/build.sh @@ -255,7 +255,7 @@ if [ "$BuildTarget" == "mac" ] || [ "$BuildTarget" == "macstore" ]; then rm -rf "$ReleasePath/$BinaryName.app/Contents/_CodeSignature" rm -rf "$ReleasePath/Updater" - ./configure.sh -D DESKTOP_APP_MAC_ARCH="arm64;x86_64" + ./configure.sh -D DESKTOP_APP_MAC_ARCH="arm64;x86_64" -DDESKTOP_APP_ENABLE_LTO=ON cd $ProjectPath cmake --build . --config Release --target Telegram diff --git a/Telegram/build/docker/build.sh b/Telegram/build/docker/build.sh index 4d84f0da69..3b90b15537 100755 --- a/Telegram/build/docker/build.sh +++ b/Telegram/build/docker/build.sh @@ -26,7 +26,7 @@ if [ ! -f "/usr/bin/cmake" ]; then ln -s cmake3 /usr/bin/cmake fi -./configure.sh +./configure.sh -DDESKTOP_APP_ENABLE_LTO=ON cd $ProjectPath cmake --build . --config Release --target Telegram diff --git a/cmake b/cmake index ece5903560..ee891d4361 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit ece5903560c617ce1fea685e236097715ce3b3e0 +Subproject commit ee891d4361cac9437b7df8844e22ac1b25cec5b3 From 1ea7d979a87c88471ab2888f7244fe0af80093e1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 15 Jan 2026 18:36:23 +0400 Subject: [PATCH 036/348] Use FASTLINK only in x64 builds. --- cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake b/cmake index ee891d4361..9513cecbca 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit ee891d4361cac9437b7df8844e22ac1b25cec5b3 +Subproject commit 9513cecbca8ee41c07c7d946625bda356e00e2f6 From 9541e983d29922d95a5ec139c08a9095f422ebf5 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 13:35:35 +0300 Subject: [PATCH 037/348] Removed forum shape from bot userpics. --- Telegram/SourceFiles/data/data_peer.cpp | 2 +- Telegram/SourceFiles/ui/controls/userpic_button.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 34079a0236..bd595e2887 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -463,7 +463,7 @@ void PeerData::paintUserpic( const auto cloud = userpicCloudImage(view); const auto ratio = style::DevicePixelRatio(); if (context.shape == Ui::PeerUserpicShape::Auto) { - context.shape = isForum() + context.shape = (isForum() && !isBot()) ? Ui::PeerUserpicShape::Forum : isMonoforum() ? Ui::PeerUserpicShape::Monoforum diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index f3b64f94e7..99d5c24682 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -96,7 +96,7 @@ void CameraBox( done(std::move(image)); }; const auto useForumShape = forceForumShape - || (peer && peer->isForum()); + || (peer && peer->isForum() && !peer->isBot()); PrepareProfilePhoto( box, controller, @@ -929,7 +929,10 @@ void UserpicButton::processNewPeerPhoto() { bool UserpicButton::useForumShape() const { return (_shape == PeerUserpicShape::Forum) - || (_peer && _peer->isForum() && _shape == PeerUserpicShape::Auto); + || (_peer + && _peer->isForum() + && _shape == PeerUserpicShape::Auto + && !_peer->isBot()); } void UserpicButton::grabOldUserpic() { From c7dd173858cbaf9936218dde17be86b677e1e859 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 13:59:07 +0300 Subject: [PATCH 038/348] Returned about to bots with forum. --- Telegram/SourceFiles/history/view/history_view_about_view.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 1ee5a2ae65..95ee5294c1 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -683,7 +683,9 @@ bool AboutView::refresh() { } _version = 0; return false; - } else if (_history->peer->isForum()) { + } else if (_history->peer->isForum() + && info->startToken.isEmpty() + && (!_history->isEmpty() || _history->lastMessage())) { if (_item) { return false; } From 6bdbf85279d8e07c864fcf10887adddf4d5b5b05 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 13:59:15 +0300 Subject: [PATCH 039/348] Fixed display of tabs in forum bots on receiving full user. --- Telegram/SourceFiles/history/history_widget.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 38ce973592..4fa82cde06 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -9128,6 +9128,11 @@ void HistoryWidget::fullInfoUpdated() { } else if (!_scroll->isHidden() && _unblock->isHidden() == isBlocked()) { refresh = true; } + if (_history + && HistoryView::SubsectionTabs::UsedFor(_history) + && !_subsectionTabs) { + validateSubsectionTabs(); + } if (refresh) { updateControlsVisibility(); updateControlsGeometry(); From a7def1ee8736ccd31acf750be4b84e187e25ab79 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 14:34:40 +0300 Subject: [PATCH 040/348] Removed enforced empty general forum topic creation for bots. --- Telegram/SourceFiles/history/history_item.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index f642f7ff94..c175af307d 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -1947,7 +1947,9 @@ void HistoryItem::destroy() { not_null HistoryItem::notificationThread() const { if (const auto rootId = topicRootId()) { if (const auto forum = _history->asForum()) { - return forum->enforceTopicFor(rootId); + if (!_history->peer->isBot()) { + return forum->enforceTopicFor(rootId); + } } } return _history; From ce0f10f7645c9c5c5c5c490556802992929907c1 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 14:49:22 +0300 Subject: [PATCH 041/348] Renamed default tab for all bot forums. --- .../view/history_view_subsection_tabs.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index f625531583..1899f9e6e6 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -453,15 +453,15 @@ void SubsectionTabs::startFillingSlider( ).append(' ').append(peer->shortName()), }); } - } else if (item.thread->peer()->isBot()) { - sections.push_back({ - .text = { tr::lng_bot_new_chat(tr::now) }, - }); - if (vertical) { - auto &last = sections.back(); - last.userpic = Ui::MakeNewChatSubsectionsThumbnail( - textFg); - } + // } else if (item.thread->peer()->isBot()) { + // sections.push_back({ + // .text = { tr::lng_bot_new_chat(tr::now) }, + // }); + // if (vertical) { + // auto &last = sections.back(); + // last.userpic = Ui::MakeNewChatSubsectionsThumbnail( + // textFg); + // } } else { sections.push_back({ .text = { tr::lng_filters_all_short(tr::now) }, From 160075c22b5f1042af9638628d9af827d357ce66 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 15:37:37 +0300 Subject: [PATCH 042/348] More removed forum shape from bot userpics. --- Telegram/SourceFiles/data/data_peer.cpp | 2 +- Telegram/SourceFiles/ui/controls/userpic_button.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index bd595e2887..69b65a1298 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1292,7 +1292,7 @@ not_null PeerData::userpicPaintingPeer() const { } Ui::PeerUserpicShape PeerData::userpicShape() const { - return isForum() + return isForum() && !isBot() ? Ui::PeerUserpicShape::Forum : isMonoforum() ? Ui::PeerUserpicShape::Monoforum diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index 99d5c24682..07c4e557cd 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -360,7 +360,7 @@ void UserpicButton::choosePhotoLocally() { ? Api::PeerPhoto::EmojiListType::Profile : Api::PeerPhoto::EmojiListType::Group), done, - _peer ? _peer->isForum() : false); + _peer ? (_peer->isForum() && !_peer->isBot()) : false); }; const auto addFromClipboard = [=](ChosenType type, tr::phrase<> text) { if (const auto data = QGuiApplication::clipboard()->mimeData()) { From f40064333e33a35f8e96de07248666792998e5f2 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Fri, 16 Jan 2026 17:32:31 +0300 Subject: [PATCH 043/348] Fixed ability to delete chat as forum with all topics. --- Telegram/SourceFiles/history/history.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index a8e0cc6d3c..7d935c9a25 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -3990,6 +3990,11 @@ void History::clear(ClearType type, bool markEmpty) { } else if (const auto channel = peer->asMegagroup()) { channel->mgInfo->markupSenders.clear(); } + if (const auto forum = peer->forum()) { + forum->enumerateTopics([&](not_null topic) { + destroyMessagesByTopic(topic->rootId()); + }); + } owner().notifyHistoryChangeDelayed(this); owner().sendHistoryChangeNotifications(); From 6dce4e360fa18fafb8bb9b0a13bfee5d828f57c7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 19 Jan 2026 18:13:04 +0400 Subject: [PATCH 044/348] Use lottie for voice/round message icon. --- .../animations/chat/video_to_voice.tgs | Bin 0 -> 903 bytes .../animations/chat/voice_to_video.tgs | Bin 0 -> 914 bytes .../Resources/qrc/telegram/animations.qrc | 2 + .../chat_helpers/chat_helpers.style | 18 +-- .../history_view_voice_record_button.cpp | 53 ++++++--- .../history_view_voice_record_button.h | 6 + .../SourceFiles/media/view/media_view.style | 8 -- .../SourceFiles/ui/controls/send_button.cpp | 108 ++++++++++++++++-- .../SourceFiles/ui/controls/send_button.h | 15 +++ .../window/themes/window_theme_preview.cpp | 9 +- Telegram/lib_lottie | 2 +- 11 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 Telegram/Resources/animations/chat/video_to_voice.tgs create mode 100644 Telegram/Resources/animations/chat/voice_to_video.tgs diff --git a/Telegram/Resources/animations/chat/video_to_voice.tgs b/Telegram/Resources/animations/chat/video_to_voice.tgs new file mode 100644 index 0000000000000000000000000000000000000000..854a6c734ce31b4a325a512c078b1737fbc7bcc7 GIT binary patch literal 903 zcmV;219{#PPD8)Tb6 z(!6cj*4Ig$_JYvF61uR25CP3rMfu-%cQytSvUO6UZdFq_?mqi|aQ<>RvIASN(4YH~ zaduy^MI<=O_iO>T5^hp)_Jb|vQ_j}4s}JA4J}3lFyBTyZP&!HCx!CmEWli1VXm#xs>VPr zCLw?Elkxlyo?*X(jxPXhAZgc53}gsc6&g_np|J-_@7;-=-LtD;_a$32)kAhlPW*^v zk%PL-B44Sg8=Tg4Rgq$l;oKK88m-94c5>Q>Q_$h4KU<5N$iIODCL;h?4`YHX9|ra1j}}*x$irLU2JvMUkk!g_;*cJZnE*vZ>w~_&X*9oMvb=v(%c#O z0;|ffSJU(?z25shquM^v2A5brx?GeQZn_y;gypK!F-v%|FEriN_hnTuYzk7*VW1lA zAkzTemgxXfNkir%1-x{K{_e{`2Wt4NV=9-IjE49HUqBS07NuDz3>}yt{#`P-k;CID zu0;jsu815i5`p+nSl~G+d8(hIt}ZXgDjjLO7DiFOWfXNqMtO#DbKVU%p&fOsGP?0a zFdU%f?v!OQ-oSu6+EEEc8Z#5ZsAHx_l-fZkBtcHCPK)9xqoIzV_3FgTW5+m}kAoQI zG-7N$w7G#s(c}x0EDp@Vh*}a{8@*2DS>V;iyDLv4@@1wG`mOP*xFay!p+>nQcoKIc dXFvCAea>koG#6>L(ZA!9(=Q)q&bcrU005JWxPJfu literal 0 HcmV?d00001 diff --git a/Telegram/Resources/animations/chat/voice_to_video.tgs b/Telegram/Resources/animations/chat/voice_to_video.tgs new file mode 100644 index 0000000000000000000000000000000000000000..de864f07d9cf6f0f4e150bf31b3e31e7ee54a802 GIT binary patch literal 914 zcmV;D18w{tiwFpOiQQoU19op|V`X1-Z(nw4WMywIYIARH0Nq#LZ<{a>{#PPD8)Tc{ zB)o0f*4Ig$_JYvF61uR25CP3rMfu-%XB&e9Sv##!x2h=|cb|PfIDfeu$$>;9^yfY& zl-yM$l8lmkPa?RLaN~lKA0(PjDOu~=u7DMXGGAt2n)1sbUu7jMU9DDXOPlHQu;55ayrgQ$2Gk6f{se)aD5)G22Mb>U});Y|2^7PyfbWmlRJkc)B4^EjS9;@K972cQkO+O-n{ z4oPwm8j-6)pgj<#_Ezldo?YeCr9B2GbL>8*&3b~TzSzOjn|E*DPd;bMrmQBn*u@iX za#NM}djPDVaqrS?ovGSl`!)sXPRF2aW9ZvNHEGO~Uh9Hh8a+##_7&2fpT9X~L&o;c zp3m7V@cFqjJkQ}yt z{#`O)=ZAB_uSEsw*k5!uBm(xIu)yDxM(R4qL1j9Id_h|2SmU)&iuf(1h$~XcGt8TE z;j=)tQ;mMIbB<$lY%Oa!h%=lwVr)IM90wYqrX!ekaHwKN)DolG=yfX30-rE5&byH| zv_Z@Vf%X%cvkS?b8E{8C`$0rzu!Ato2#8WU{)Res1^@7MaAtDVqXq}4|MkWWs(06bgi^)V0t0D5P-`Tzg` literal 0 HcmV?d00001 diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index a33a9ff016..3902bfff8b 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -17,6 +17,8 @@ ../../animations/stats_earn.tgs ../../animations/voice_ttl_idle.tgs ../../animations/voice_ttl_start.tgs + ../../animations/chat/voice_to_video.tgs + ../../animations/chat/video_to_voice.tgs ../../animations/palette.tgs ../../animations/sleep.tgs ../../animations/greeting.tgs diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 08b4c44f3f..e1b1db8b3b 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -153,10 +153,7 @@ SendButton { sendIconFillPadding: pixels; inner: IconButton; stars: RoundButton; - record: icon; - recordOver: icon; - round: icon; - roundOver: icon; + recordSize: size; sendDisabledFg: color; } @@ -1287,18 +1284,11 @@ historyRecordVoiceFgOver: historyComposeIconFgOver; historyRecordVoiceFgInactive: attentionButtonFg; historyRecordVoiceFgActive: windowBgActive; historyRecordVoiceFgActiveIcon: windowFgActive; -historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }}; -historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }}; historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }}; historyRecordVoiceOnceBgOver: icon {{ "voice_lock/audio_once_bg", historySendIconFgOver }}; historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }}; historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }}; -historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }}; -historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }}; -historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }}; -historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }}; -historyRecordRoundIconPosition: point(0px, 0px); historyRecordSendIconPosition: point(2px, 0px); historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordSignalRadius: 5px; @@ -1411,12 +1401,10 @@ historySend: SendButton { textTop: 5px; width: -8px; } - record: historyRecordVoice; - recordOver: historyRecordVoiceOver; - round: historyRecordRound; - roundOver: historyRecordRoundOver; + recordSize: size(26px, 26px); sendDisabledFg: historyComposeIconFg; } +historyRecordFrameIndex: 30; defaultComposeFilesMenu: IconButton(defaultIconButton) { width: 48px; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp index b796d27858..abca0b803a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/controls/history_view_voice_record_button.h" +#include "lottie/lottie_icon.h" #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/style_chat.h" @@ -24,6 +25,8 @@ constexpr auto kBlobAlpha = 76. / 255.; constexpr auto kBlobMaxSpeed = 5.0; constexpr auto kLevelDuration = 100. + 500. * 0.33; constexpr auto kBlobsScaleEnterDuration = crl::time(250); +constexpr auto kVoiceIconIndex = 0; +constexpr auto kRoundIconIndex = 1; auto Blobs() { return std::vector{ @@ -135,22 +138,28 @@ void VoiceRecordButton::init() { p.scale(scale, scale); } const auto state = *currentState; - const auto icon = (state == Type::Send) - ? st::historySendIcon - : (state == Type::Record) - ? st::historyRecordVoiceActive - : st::historyRecordRoundActive; - const auto position = (state == Type::Send) - ? st::historyRecordSendIconPosition - : (state == Type::Record) - ? QPoint(0, 0) - : st::historyRecordRoundIconPosition; - icon.paint( - p, - -icon.width() / 2 + position.x(), - -icon.height() / 2 + position.y(), - 0, - st::historyRecordVoiceFgActiveIcon->c); + if (state == Type::Send) { + const auto icon = st::historySendIcon; + const auto position = st::historyRecordSendIconPosition; + icon.paint( + p, + -icon.width() / 2 + position.x(), + -icon.height() / 2 + position.y(), + 0, + st::historyRecordVoiceFgActiveIcon->c); + } else { + const auto index = (state == Type::Record) + ? kVoiceIconIndex + : kRoundIconIndex; + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } + icon->paintInCenter( + p, + rect().translated(-_center, -_center), + st::historyRecordVoiceFgActiveIcon->c); + } } }, lifetime()); @@ -205,6 +214,18 @@ void VoiceRecordButton::init() { }, lifetime()); } +void VoiceRecordButton::initVoiceRoundIcon(int index) { + Expects(index >= 0 && index < 2); + + _voiceRoundIcons[index] = Lottie::MakeIcon({ + .path = ((index == kVoiceIconIndex) + ? u":/animations/chat/voice_to_video.tgs"_q + : u":/animations/chat/video_to_voice.tgs"_q), + .sizeOverride = st::historySend.recordSize, + .colorizeUsingAlpha = true, + }); +} + rpl::producer VoiceRecordButton::actives() const { return events( ) | rpl::filter([=](not_null e) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h index e8e2dd7b65..4465a95925 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_button.h @@ -15,6 +15,10 @@ namespace style { struct RecordBar; } // namespace style +namespace Lottie { +class Icon; +} // namespace Lottie + namespace Ui::Paint { class Blobs; } // namespace Ui::Paint @@ -47,8 +51,10 @@ public: private: void init(); + void initVoiceRoundIcon(int index); std::unique_ptr _blobs; + std::array, 2> _voiceRoundIcons; crl::time _lastUpdateTime = 0; crl::time _blobsHideLastTime = 0; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index fa3d245c58..442277256c 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -493,10 +493,6 @@ storiesLike: IconButton(storiesAttach) { icon: icon {{ "chat/input_like", storiesComposeGrayIcon }}; iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }}; } -storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }}; -storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }}; -storiesRecordRound: icon {{ "chat/input_video", storiesComposeGrayIcon }}; -storiesRecordRoundOver: icon {{ "chat/input_video", storiesComposeGrayIcon }}; storiesEditStars: IconButton(storiesAttach) { icon: icon{{ "chat/input_paid", storiesComposeGrayIcon }}; iconOver: icon{{ "chat/input_paid", storiesComposeGrayIcon }}; @@ -727,10 +723,6 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { rippleAreaSize: 40px; rippleAreaPosition: point(1px, 1px); } - record: storiesRecordVoice; - recordOver: storiesRecordVoiceOver; - round: storiesRecordRound; - roundOver: storiesRecordRoundOver; sendDisabledFg: storiesComposeGrayText; } attach: storiesAttach; diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp index 7cf772cc2d..0dd88a0f57 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.cpp +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/send_button.h" #include "lang/lang_tag.h" +#include "lottie/lottie_icon.h" #include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/painter.h" @@ -19,7 +20,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { -constexpr int kWideScale = 5; +constexpr auto kWideScale = 5; +constexpr auto kVoiceToRoundIndex = 0; +constexpr auto kRoundToVoiceIndex = 1; } // namespace @@ -29,19 +32,34 @@ SendButton::SendButton(QWidget *parent, const style::SendButton &st) updateSize(); } +SendButton::~SendButton() = default; + void SendButton::setState(State state) { if (_state == state) { return; } + + const auto previousType = _state.type; + const auto newType = state.type; + const auto voiceRoundTransition = isVoiceRoundTransition( + previousType, + newType); + const auto hasSlowmode = (_state.slowmodeDelay > 0); const auto hasSlowmodeChanged = hasSlowmode != (state.slowmodeDelay > 0); auto withSameSlowmode = state; withSameSlowmode.slowmodeDelay = _state.slowmodeDelay; const auto animate = hasSlowmodeChanged || (!hasSlowmode && withSameSlowmode != _state); - if (animate) { + + if (animate && !voiceRoundTransition) { _contentFrom = grabContent(); } + + if (_voiceRoundAnimating && !voiceRoundTransition) { + _voiceRoundAnimating = false; + } + if (_state.slowmodeDelay != state.slowmodeDelay) { const auto seconds = state.slowmodeDelay; const auto minutes = seconds / 60; @@ -60,7 +78,29 @@ void SendButton::setState(State state) { kMarkupTextOptions); } _state = state; - if (animate) { + + if (voiceRoundTransition) { + _voiceRoundAnimating = true; + + const auto toRound = (newType == Type::Round); + const auto index = toRound ? kVoiceToRoundIndex : kRoundToVoiceIndex; + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } + icon->animate([=, raw = icon.get()] { + update(); + if (!raw->animating()) { + _voiceRoundAnimating = false; + } + }, 0, icon->framesCount() - 1); + auto &after = _voiceRoundIcons[1 - index]; + if (!after) { + initVoiceRoundIcon(1 - index); + } else if (after->frameIndex() != 0) { + after->jumpTo(0, nullptr); + } + } else if (animate) { _stateChangeFromWidth = width(); _stateChangeAnimation.stop(); updateSize(); @@ -86,6 +126,12 @@ void SendButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); auto over = (isDown() || isOver()); + + if (_voiceRoundAnimating) { + paintVoiceRoundIcon(p, over); + return; + } + auto changed = _stateChangeAnimation.value(1.); if (changed < 1.) { PainterHighQualityEnabler hq(p); @@ -153,11 +199,7 @@ void SendButton::paintRecord(QPainter &p, bool over) { (width() - _st.inner.rippleAreaSize) / 2, _st.inner.rippleAreaPosition.y()); } - - const auto &icon = (isDisabled() || !over) - ? _st.record - : _st.recordOver; - icon.paintInCenter(p, rect()); + paintLottieIcon(p, kVoiceToRoundIndex, over); } void SendButton::paintRound(QPainter &p, bool over) { @@ -167,11 +209,20 @@ void SendButton::paintRound(QPainter &p, bool over) { (width() - _st.inner.rippleAreaSize) / 2, _st.inner.rippleAreaPosition.y()); } + paintLottieIcon(p, kRoundToVoiceIndex, over); +} - const auto &icon = (isDisabled() || !over) - ? _st.round - : _st.roundOver; - icon.paintInCenter(p, rect()); +void SendButton::paintLottieIcon(QPainter &p, int index, bool over) { + auto &icon = _voiceRoundIcons[index]; + if (!icon) { + initVoiceRoundIcon(index); + } else if (!_voiceRoundAnimating && icon->frameIndex() != 0) { + icon->jumpTo(0, [=] { update(); }); + } + const auto color = (isDisabled() || !over) + ? st::historyRecordVoiceFg->c + : st::historyRecordVoiceFgOver->c; + icon->paintInCenter(p, rect(), color); } void SendButton::paintSave(QPainter &p, bool over) { @@ -330,6 +381,39 @@ QPoint SendButton::prepareRippleStartPosition() const { return real - QPoint((width() - size) / 2, y); } +void SendButton::initVoiceRoundIcon(int index) { + Expects(index >= 0 && index < 2); + + _voiceRoundIcons[index] = Lottie::MakeIcon({ + .path = ((index == kVoiceToRoundIndex) + ? u":/animations/chat/voice_to_video.tgs"_q + : u":/animations/chat/video_to_voice.tgs"_q), + .sizeOverride = _st.recordSize, + .colorizeUsingAlpha = true, + }); +} + +void SendButton::paintVoiceRoundIcon(QPainter &p, bool over) { + if (!isDisabled()) { + paintRipple( + p, + (width() - _st.inner.rippleAreaSize) / 2, + _st.inner.rippleAreaPosition.y()); + } + + const auto color = (isDisabled() || !over) + ? st::historyRecordVoiceFg->c + : st::historyRecordVoiceFgOver->c; + const auto toVideo = (_state.type == Type::Round); + const auto index = toVideo ? kVoiceToRoundIndex : kRoundToVoiceIndex; + _voiceRoundIcons[index]->paintInCenter(p, rect(), color); +} + +bool SendButton::isVoiceRoundTransition(Type from, Type to) { + return (from == Type::Record && to == Type::Round) + || (from == Type::Round && to == Type::Record); +} + SendStarButton::SendStarButton( QWidget *parent, const style::IconButton &st, diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h index cd9fe21b72..04529b9b95 100644 --- a/Telegram/SourceFiles/ui/controls/send_button.h +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -9,6 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" +#include + +namespace Lottie { +class Icon; +} // namespace Lottie + namespace style { struct SendButton; struct IconButton; @@ -20,6 +26,7 @@ namespace Ui { class SendButton final : public RippleButton { public: SendButton(QWidget *parent, const style::SendButton &st); + ~SendButton(); static constexpr auto kSlowmodeDelayLimit = 100 * 60; @@ -76,6 +83,11 @@ private: void paintSlowmode(QPainter &p); void paintStarsToSend(QPainter &p, bool over); + void initVoiceRoundIcon(int index); + void paintVoiceRoundIcon(QPainter &p, bool over); + [[nodiscard]] static bool isVoiceRoundTransition(Type from, Type to); + void paintLottieIcon(QPainter &p, int index, bool over); + const style::SendButton &_st; State _state; @@ -87,6 +99,9 @@ private: QString _slowmodeDelayText; Ui::Text::String _starsToSendText; + std::array, 2> _voiceRoundIcons; + bool _voiceRoundAnimating = false; + }; struct SendStarButtonState { diff --git a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp index 9b05d2fafd..0130e318a0 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_preview.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_preview.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/dialogs_three_state_icon.h" #include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" #include "platform/platform_window_title.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" @@ -565,7 +566,13 @@ void Generator::paintComposeArea() { : st::historyAttach.iconPosition.y(); st::historyAttach.icon[_palette].paint(*_p, _composeArea.x() + attachIconLeft, controlsTop + attachIconTop, _rect.width()); auto right = st::historySendRight + st::historySendSize.width(); - st::historyRecordVoice[_palette].paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height())); + + const auto recordIcon = Lottie::MakeIcon({ + .path = u":/animations/chat/voice_to_video.tgs"_q, + .sizeOverride = st::historySend.recordSize, + .colorizeUsingAlpha = true, + }); + recordIcon->paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height()), st::historyRecordVoiceFg[_palette]->c); const auto &emojiButton = st::historyAttachEmoji.inner; const auto emojiIconLeft = (emojiButton.iconPosition.x() < 0) diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 99d8cc4d18..cba5d113eb 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 99d8cc4d1820960fbcdafc574f1894073b14b75a +Subproject commit cba5d113eb941c3eec20c4d171ff4ff9d1a92382 From 786b0e77b8f999dec1f7e65e2e905f711d485bf8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 19 Jan 2026 22:37:41 +0400 Subject: [PATCH 045/348] Add nice animations to stories on focus search. --- .../SourceFiles/dialogs/dialogs_widget.cpp | 55 ++++++++++++------- .../dialogs/ui/dialogs_stories_list.cpp | 43 ++++++++++++++- .../dialogs/ui/dialogs_stories_list.h | 6 ++ 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 332c589669..c1a263ebb9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -403,7 +403,6 @@ Widget::Widget( _scrollToTop->raise(); _lockUnlock->toggle(false, anim::type::instant); - _inner->updated( ) | rpl::on_next([=] { listScrollUpdated(); @@ -1598,7 +1597,9 @@ void Widget::updateControlsVisibility(bool fast) { _forumReportBar->show(); } } else { - updateLockUnlockVisibility(); + updateLockUnlockVisibility(fast + ? anim::type::instant + : anim::type::normal); updateJumpToDateVisibility(fast); updateSearchFromVisibility(fast); } @@ -2335,21 +2336,28 @@ void Widget::stopWidthAnimation() { } void Widget::updateStoriesVisibility() { - updateLockUnlockVisibility(); + updateLockUnlockVisibility(anim::type::normal); if (!_stories) { return; } - const auto hidden = (_showAnimation != nullptr) + const auto widthAnimation = !_widthAnimationCache.isNull(); + const auto suggestionsAnimation = widthAnimation + && (!_suggestions || !_hidingSuggestions.empty()); + const auto hiddenInstant = _showAnimation || _openedForum - || !_widthAnimationCache.isNull() + || (widthAnimation && !suggestionsAnimation) || _childList - || _searchHasFocus + || _stories->empty() + || (_scroll->position().overscroll < -st::dialogsFilterSkip); + const auto hiddenAnimated = _searchHasFocus || _searchSuggestionsLocked || !_searchState.query.isEmpty() || _searchState.inChat - || _stories->empty(); - if (_stories->isHidden() != hidden) { - _stories->setVisible(!hidden); + || suggestionsAnimation; + const auto hidden = hiddenInstant || hiddenAnimated; + const auto changed = (_stories->toggledHidden() != hidden); + _stories->setToggledHidden(hiddenInstant, hiddenAnimated); + if (changed) { using Type = Ui::ElasticScroll::OverscrollType; if (hidden) { _scroll->setOverscrollDefaults(0, 0); @@ -2414,7 +2422,7 @@ void Widget::startSlideAnimation( Window::SlideDirection direction) { _scroll->hide(); if (_stories) { - _stories->hide(); + _stories->setToggledHidden(true, false); } _searchControls->hide(); if (_subsectionTopBar) { @@ -3787,20 +3795,27 @@ void Widget::updateLockUnlockVisibility(anim::type animated) { if (_showAnimation) { return; } - const auto hidden = !session().domain().local().hasLocalPasscode() - || _showAnimation + const auto widthAnimation = !_widthAnimationCache.isNull(); + const auto suggestionsAnimation = widthAnimation + && (!_suggestions || !_hidingSuggestions.empty()); + const auto hiddenInstant = _showAnimation || _openedForum - || !_widthAnimationCache.isNull() + || (widthAnimation && !suggestionsAnimation) || _childList - || _searchHasFocus + || !session().domain().local().hasLocalPasscode() + || (!_stories->empty() + && _scroll->position().overscroll < -st::dialogsFilterSkip); + const auto hiddenAnimated = _searchHasFocus || _searchSuggestionsLocked + || !_searchState.query.isEmpty() || _searchState.inChat - || !_searchState.query.isEmpty(); - if (_lockUnlock->toggled() == hidden) { - const auto stories = _stories && !_stories->empty(); - _lockUnlock->toggle( - !hidden, - stories ? anim::type::instant : animated); + || suggestionsAnimation; + const auto hidden = hiddenInstant || hiddenAnimated; + const auto changed = (_lockUnlock->toggled() == hidden); + _lockUnlock->toggle( + !hidden, + hiddenInstant ? anim::type::instant : animated); + if (changed) { if (!hidden) { updateLockUnlockPosition(); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index 24d924e5f6..a6869c14c1 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -199,6 +199,38 @@ rpl::producer> List::verticalScrollEvents() const { return _verticalScrollEvents.events(); } +bool List::toggledHidden() const { + return _hiddenInstant || _hiddenAnimated; +} + +void List::setToggledHidden(bool hiddenInstant, bool hiddenAnimated) { + const auto hidden = (hiddenInstant || hiddenAnimated); + const auto hiddenChanged = (hidden != toggledHidden()); + const auto hiddenInstantChanged = (_hiddenInstant != hiddenInstant); + const auto hiddenAnimatedChanged = (_hiddenAnimated != hiddenAnimated); + _hiddenInstant = hiddenInstant; + _hiddenAnimated = hiddenAnimated; + if (hiddenChanged) { + if (_hiddenInstant || !hiddenAnimatedChanged) { + _hiddenAnimation.stop(); + setVisible(!toggledHidden()); + } else { + const auto from = hidden ? 0. : 1.; + const auto till = hidden ? 1. : 0.; + _hiddenAnimation.start([=] { + if (!_hiddenAnimation.animating()) { + setVisible(!toggledHidden()); + } + update(); + }, from, till, st::fadeWrapDuration, anim::linear); + show(); + } + } else if (hiddenInstantChanged && _hiddenInstant) { + _hiddenAnimation.stop(); + setVisible(!toggledHidden()); + } +} + void List::requestExpanded(bool expanded) { if (_expanded != expanded) { _expanded = expanded; @@ -341,6 +373,10 @@ List::Layout List::computeLayout(float64 expanded) const { } void List::paintEvent(QPaintEvent *e) { + const auto hidden = _hiddenAnimation.value(toggledHidden() ? 1. : 0.); + if (hidden >= 1.) { + return; + } const auto &st = _st.small; const auto &full = _st.full; const auto layout = computeLayout(); @@ -354,13 +390,18 @@ void List::paintEvent(QPaintEvent *e) { }; const auto line = elerp(st.lineTwice, full.lineTwice) / 2.; const auto photo = lerp(st.photo, full.photo); - const auto layered = layout.single < (photo + 4 * line); + const auto layered = (layout.single < (photo + 4 * line)) + || (hidden > 0.); auto p = QPainter(this); if (layered) { ensureLayer(); auto q = QPainter(&_layer); paint(q, layout, photo, line, true); q.end(); + + if (hidden > 0.) { + p.setOpacity(1. - hidden); + } p.drawImage(0, 0, _layer); } else { paint(p, layout, photo, line, false); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index f55151ca4f..64ac522bb0 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -101,6 +101,9 @@ public: [[nodiscard]] auto verticalScrollEvents() const -> rpl::producer>; + [[nodiscard]] bool toggledHidden() const; + void setToggledHidden(bool hiddenInstant, bool hiddenAnimated); + private: struct Layout; enum class State { @@ -196,8 +199,11 @@ private: Ui::Animations::Simple _expandedAnimation; Ui::Animations::Simple _expandCatchUpAnimation; + Ui::Animations::Simple _hiddenAnimation; float64 _lastRatio = 0.; int _lastExpandedHeight = 0; + bool _hiddenAnimated : 1 = false; + bool _hiddenInstant : 1 = false; bool _expandIgnored : 1 = false; bool _expanded : 1 = false; From a5db6cd3636fa3cf5329ac2aa90d444b1cd29336 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 12:04:07 +0300 Subject: [PATCH 046/348] Fixed crash from feature to kick participants from common groups. --- Telegram/SourceFiles/boxes/choose_filter_box.cpp | 7 ++++++- Telegram/SourceFiles/boxes/choose_filter_box.h | 2 +- Telegram/SourceFiles/boxes/moderate_messages_box.cpp | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.cpp b/Telegram/SourceFiles/boxes/choose_filter_box.cpp index 93230d4857..da0be24a9a 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/choose_filter_box.cpp @@ -352,7 +352,7 @@ void FillChooseFilterMenu( }, menu->lifetime()); } -void FillChooseFilterWithAdminedGroupsMenu( +bool FillChooseFilterWithAdminedGroupsMenu( not_null controller, not_null menu, not_null user, @@ -363,6 +363,7 @@ void FillChooseFilterWithAdminedGroupsMenu( const auto session = &controller->session(); const auto &list = session->data().chatsFilters().list(); const auto showColors = session->data().chatsFilters().tagsEnabled(); + auto added = 0; for (const auto &filter : list) { const auto id = filter.id(); if (!id) { @@ -446,10 +447,14 @@ void FillChooseFilterWithAdminedGroupsMenu( })); item->setIcon(Icon(showColors ? filter : filter.withColorIndex({}))); + menu->addAction(std::move(item)); + added++; } session->data().chatsFilters().changed( ) | rpl::on_next([=] { menu->hideMenu(); }, menu->lifetime()); + + return added; } diff --git a/Telegram/SourceFiles/boxes/choose_filter_box.h b/Telegram/SourceFiles/boxes/choose_filter_box.h index ff29b3c259..84417d6ec7 100644 --- a/Telegram/SourceFiles/boxes/choose_filter_box.h +++ b/Telegram/SourceFiles/boxes/choose_filter_box.h @@ -44,7 +44,7 @@ void FillChooseFilterMenu( not_null menu, not_null history); -void FillChooseFilterWithAdminedGroupsMenu( +bool FillChooseFilterWithAdminedGroupsMenu( not_null controller, not_null menu, not_null user, diff --git a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp index f44787862d..f0ad87172e 100644 --- a/Telegram/SourceFiles/boxes/moderate_messages_box.cpp +++ b/Telegram/SourceFiles/boxes/moderate_messages_box.cpp @@ -254,12 +254,13 @@ void FillMenuModerateCommonGroups( } menu->addSeparator(); if (const auto window = Core::App().findWindow(menu->parentWidget())) { + auto hasActions = false; Ui::Menu::CreateAddActionCallback(menu)(Ui::Menu::MenuCallback::Args{ .text = tr::lng_restrict_users_kick_from_common_group(tr::now), .handler = nullptr, .icon = &st::menuIconAddToFolder, .fillSubmenu = [&](not_null menu) { - FillChooseFilterWithAdminedGroupsMenu( + hasActions = FillChooseFilterWithAdminedGroupsMenu( window->sessionController(), menu, user, @@ -269,6 +270,10 @@ void FillMenuModerateCommonGroups( }, .submenuSt = &st::foldersMenu, }); + if (!hasActions) { + menu->removeAction(menu->actions().size() - 1); + menu->removeAction(menu->actions().size() - 1); // Separator. + } } menu->addSeparator(); { From da3b62341f86fbff7b6b3170413cd0b754268a65 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 12:28:53 +0300 Subject: [PATCH 047/348] Added tooltip to fast share about popup selector for top peers. --- Telegram/Resources/langs/lang.strings | 1 + .../history/view/history_view_message.cpp | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d0d0c79e29..14b514d8d7 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2713,6 +2713,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_badge" = "channel"; "lng_topic_author_badge" = "Topic Creator"; "lng_fast_reply" = "Reply"; +"lng_fast_share_tooltip" = "Right-click to select a Recent Contact."; "lng_cancel_edit_post_sure" = "Cancel editing?"; "lng_cancel_edit_post_yes" = "Yes"; "lng_cancel_edit_post_no" = "No"; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index a24b8ae9e4..bfee67cf7e 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -4018,7 +4018,17 @@ ClickHandlerPtr Message::prepareRightActionLink() const { } }; }; - const auto result = std::make_shared([=]( + + class FastShareClickHandler : public LambdaClickHandler { + public: + FastShareClickHandler(Fn handler) + : LambdaClickHandler(std::move(handler)) {} + QString tooltip() const override { + return tr::lng_fast_share_tooltip(tr::now); + } + }; + + const auto result = std::make_shared([=]( ClickContext context) { const auto controller = ExtractController(context); if (!controller || controller->session().uniqueId() != sessionId) { From 61e0e3964102300dd9f92e4b58ad58d180debaba Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 12:36:19 +0300 Subject: [PATCH 048/348] Particularly ported QMenu::hasMouseMoved to top peers popup selector. --- .../ui/controls/dynamic_images_strip.cpp | 18 ++++++++++++++++++ .../ui/controls/dynamic_images_strip.h | 4 ++++ Telegram/lib_ui | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp index 27b696f679..6966199060 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.cpp @@ -63,6 +63,14 @@ void DynamicImagesStrip::setClickCallback(Fn callback) { _clickCallback = std::move(callback); } +bool DynamicImagesStrip::hasMouseMoved() const { + return _motions > 6; +} + +void DynamicImagesStrip::mouseMoved() { + _motions++; +} + rpl::producer DynamicImagesStrip::hoveredItemValue() const { return _hoveredItem.events(); } @@ -111,6 +119,10 @@ void DynamicImagesStrip::paintEvent(QPaintEvent *e) { } void DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) { + mouseMoved(); + if (!hasMouseMoved()) { + return; + } const auto pos = e->pos().x(); const auto step = _userpicSize + _gap; const auto count = int(_thumbnails.size()); @@ -127,6 +139,9 @@ void DynamicImagesStrip::mouseMoveEvent(QMouseEvent *e) { } void DynamicImagesStrip::mousePressEvent(QMouseEvent *e) { + if (!hasMouseMoved()) { + return; + } _pressed = true; if (_hoveredIndex >= 0 && _clickCallback) { _clickCallback(_hoveredIndex); @@ -134,6 +149,9 @@ void DynamicImagesStrip::mousePressEvent(QMouseEvent *e) { } void DynamicImagesStrip::mouseReleaseEvent(QMouseEvent *e) { + if (!hasMouseMoved()) { + return; + } if (!_pressed && _hoveredIndex >= 0 && _clickCallback) { _clickCallback(_hoveredIndex); } diff --git a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h index 9b78ddf7a7..363f3ba946 100644 --- a/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h +++ b/Telegram/SourceFiles/ui/controls/dynamic_images_strip.h @@ -45,11 +45,15 @@ private: void updateHoveredItem(int index); void setSelectedIndex(int index); + [[nodiscard]] bool hasMouseMoved() const; + void mouseMoved(); + std::vector> _thumbnails; int _userpicSize = 0; int _gap = 0; float64 _progress = 0.; int _hoveredIndex = -1; + int _motions = 0; std::vector _scales; std::vector _alphas; std::vector _scaleTargets; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index d9516e9f26..43508216d6 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit d9516e9f26ecbe267d24b4bbb5fa6fb15939e628 +Subproject commit 43508216d607025af8de8565637b75888500c04f From c7459af4eccd59e79faebec312780017761d6f6d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 12:46:01 +0300 Subject: [PATCH 049/348] Changed bg of important tooltip for top peers selector. --- Telegram/SourceFiles/chat_helpers/chat_helpers.style | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index e1b1db8b3b..8dc53ebdf0 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1742,7 +1742,7 @@ topPeersSelectorUserpicExpand: 0.1; topPeersSelectorSkip: point(9px, -5px); topPeersSelectorImportantTooltip: ImportantTooltip(defaultImportantTooltip) { - bg: scrollBarBgOver; + bg: msgServiceBg; radius: 9px; } topPeersSelectorImportantTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { From 3fdfd76f6e93cdd11b185ff79a5157107f84399c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 12:54:52 +0300 Subject: [PATCH 050/348] Fixed false drag state while copying recognized text from media view. --- Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index f9eb0ac99d..b2ae7d28c8 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -6564,6 +6564,7 @@ void OverlayWidget::handleMouseRelease( } if (_recognitionResult.success + && !_dragging && !_recognitionResult.items.empty() && _showRecognitionResults && button == Qt::LeftButton) { @@ -6573,6 +6574,9 @@ void OverlayWidget::handleMouseRelease( QString(), { tr::lng_text_copied(tr::now) }, 1000); + _over = _down = Over::None; + _pressed = false; + _dragging = 0; return; } } From ebc2190779967c2ef87710055737f3f37ed06eb3 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 13:14:32 +0300 Subject: [PATCH 051/348] Removed optimization of ripple animation in replies for unwrapped media. --- Telegram/SourceFiles/history/view/history_view_reply.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp index bc9c84e0ef..04a4cae4ff 100644 --- a/Telegram/SourceFiles/history/view/history_view_reply.cpp +++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp @@ -822,7 +822,7 @@ void Reply::paint( } if (_ripple.animation) { - _ripple.lastPaintedPoint = { x, y }; + _ripple.lastPaintedPoint = inBubble ? QPoint(x, y) : QPoint(); _ripple.animation->paint(p, x, y, w, &rippleColor); if (_ripple.animation->empty()) { _ripple.animation.reset(); @@ -988,7 +988,11 @@ void Reply::createRippleAnimation( Ui::RippleAnimation::RoundRectMask( size, st::messageQuoteStyle.radius), - [=] { view->repaint(QRect(_ripple.lastPaintedPoint, size)); }); + [=] { + view->repaint(_ripple.lastPaintedPoint.isNull() + ? QRect() + : QRect(_ripple.lastPaintedPoint, size)); + }); } void Reply::saveRipplePoint(QPoint point) const { From d1abbae1b3f2384167669d9ae0728813cee6e73f Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 13:17:48 +0300 Subject: [PATCH 052/348] Fixed position of popup menu from More button in profile top bar. --- Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index 9fc34ecfe8..c50e8e0c7b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -1962,7 +1962,7 @@ void TopBar::showTopBarMenu( _peerMenu->popup(_actionMore ? _actionMore->mapToGlobal( QPoint( - _actionMore->width(), + _actionMore->width() + _peerMenu->st().shadow.extend.right(), _actionMore->height() + st::infoProfileTopBarActionMenuSkip)) : QCursor::pos()); } From b97322c198f2ba5a3f1b1cb88f026d4dfe5b5c26 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 13:25:45 +0300 Subject: [PATCH 053/348] Removed report button from profile top bar actions for joined groups. --- Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index c50e8e0c7b..beff2e6a29 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -754,7 +754,8 @@ void TopBar::setupActions(not_null controller) { buttons.push_back(message); _actions->add(message); } - if (!topic && channel && !channel->amIn()) { + const auto canJoin = (!topic && channel && !channel->amIn()); + if (canJoin) { const auto join = Ui::CreateChild( this, tr::lng_profile_action_short_join(tr::now), @@ -942,6 +943,7 @@ void TopBar::setupActions(not_null controller) { return; } if (!topic + && canJoin && ((chat && !chat->amCreator() && !chat->hasAdminRights()) || (channel && !channel->amCreator() From 5146c05ec3c19afbfcdc4776d3fd9f09aecb388d Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 14:37:42 +0300 Subject: [PATCH 054/348] Added ability to start outgoing call without user confirmation. --- .../SourceFiles/calls/calls_box_controller.cpp | 2 +- Telegram/SourceFiles/calls/calls_instance.cpp | 14 ++++++++------ Telegram/SourceFiles/calls/calls_instance.h | 12 ++++++++++-- .../calls/group/calls_group_invite_controller.cpp | 2 +- .../history/view/history_view_top_bar_widget.cpp | 6 +++--- .../history/view/history_view_top_bar_widget.h | 6 +++++- .../history/view/media/history_view_call.cpp | 2 +- Telegram/SourceFiles/info/info_wrap_widget.cpp | 2 +- .../info/profile/info_profile_top_bar.cpp | 2 +- 9 files changed, 31 insertions(+), 17 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls_box_controller.cpp b/Telegram/SourceFiles/calls/calls_box_controller.cpp index db9a550624..d7062d5225 100644 --- a/Telegram/SourceFiles/calls/calls_box_controller.cpp +++ b/Telegram/SourceFiles/calls/calls_box_controller.cpp @@ -612,7 +612,7 @@ void BoxController::rowRightActionClicked(not_null row) { auto user = row->peer()->asUser(); Assert(user != nullptr); - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, {}); } void BoxController::receivedCalls(const QVector &result) { diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 6af96a3012..42c7806086 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -196,7 +196,9 @@ Instance::~Instance() { } } -void Instance::startOutgoingCall(not_null user, bool video) { +void Instance::startOutgoingCall( + not_null user, + StartOutgoingCallArgs args) { if (activateCurrentCall()) { return; } @@ -210,8 +212,8 @@ void Instance::startOutgoingCall(not_null user, bool video) { return; } requestPermissionsOrFail(crl::guard(this, [=] { - createCall(user, Call::Type::Outgoing, video); - }), video); + createCall(user, Call::Type::Outgoing, args); + }), args.video); } void Instance::startOrJoinGroupCall( @@ -413,7 +415,7 @@ void Instance::destroyCall(not_null call) { void Instance::createCall( not_null user, CallType type, - bool isVideo) { + StartOutgoingCallArgs args) { struct Performer final { explicit Performer(Fn callback) : callback(std::move(callback)) { @@ -455,7 +457,7 @@ void Instance::createCall( } _currentCallChanges.fire_copy(raw); }); - performer.callback(isVideo, false, performer); + performer.callback(args.video, args.isConfirmed, performer); } void Instance::destroyGroupCall(not_null call) { @@ -702,7 +704,7 @@ void Instance::handleCallUpdate( < base::unixtime::now()) { LOG(("Ignoring too old call.")); } else { - createCall(user, Call::Type::Incoming, phoneCall.is_video()); + createCall(user, Call::Type::Incoming, { phoneCall.is_video() }); _currentCall->handleUpdate(call); } } else if (!_currentCall diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index 651cdded99..4742827c69 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -55,6 +55,11 @@ struct DhConfig; struct InviteRequest; struct StartConferenceInfo; +struct StartOutgoingCallArgs { + bool video = false; + bool isConfirmed = false; +}; + struct StartGroupCallArgs { enum class JoinConfirm { None, @@ -80,7 +85,7 @@ public: Instance(); ~Instance(); - void startOutgoingCall(not_null user, bool video); + void startOutgoingCall(not_null user, StartOutgoingCallArgs); void startOrJoinGroupCall( std::shared_ptr show, not_null peer, @@ -159,7 +164,10 @@ private: not_null ensureSoundLoaded(const QString &key); void playSoundOnce(const QString &key); - void createCall(not_null user, CallType type, bool isVideo); + void createCall( + not_null user, + CallType type, + StartOutgoingCallArgs); void destroyCall(not_null call); void finishConferenceInvitations(const StartConferenceInfo &args); diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index 4b1c8b2fdf..5a81a58a20 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -1191,7 +1191,7 @@ object_ptr PrepareCreateCallBox( const auto &invite = selected.front(); Core::App().calls().startOutgoingCall( invite.user, - invite.video); + { invite.video }); } finished(true); }; 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 02aed3ddc1..bc6dded67d 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -154,7 +154,7 @@ TopBarWidget::TopBarWidget( _delete->setClickedCallback([=] { _deleteSelection.fire({}); }); _delete->setWidthChangedCallback([=] { updateControlsGeometry(); }); _clear->setClickedCallback([=] { _clearSelection.fire({}); }); - _call->setClickedCallback([=] { call(); }); + _call->setClickedCallback([=] { call({}); }); _groupCall->setClickedCallback([=] { groupCall(); }); _menuToggle->setClickedCallback([=] { showPeerMenu(); }); _infoToggle->setClickedCallback([=] { toggleInfoSection(); }); @@ -288,12 +288,12 @@ void TopBarWidget::refreshLang() { InvokeQueued(this, [this] { updateControlsGeometry(); }); } -void TopBarWidget::call() { +void TopBarWidget::call(Calls::StartOutgoingCallArgs args) { if (_controller->showFrozenError()) { return; } else if (const auto peer = _activeChat.key.peer()) { if (const auto user = peer->asUser()) { - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, std::move(args)); } } } diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index 5bbd5182d5..e45ba8a22d 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -36,6 +36,10 @@ namespace Window { class SessionController; } // namespace Window +namespace Calls { +struct StartOutgoingCallArgs; +} // namespace Calls + namespace HistoryView { class SendActionPainter; @@ -141,7 +145,7 @@ private: void updateInfoToggleActive(); void setupDragOnBackButton(); - void call(); + void call(Calls::StartOutgoingCallArgs); void groupCall(); void showGroupCallMenu(not_null peer); void toggleInfoSection(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_call.cpp b/Telegram/SourceFiles/history/view/media/history_view_call.cpp index 6cfd8c938b..0e58229f31 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_call.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_call.cpp @@ -76,7 +76,7 @@ QSize Call::countOptimalSize() { strong->resolveConferenceCall(id, contextId); } } else if (user) { - Core::App().calls().startOutgoingCall(user, video); + Core::App().calls().startOutgoingCall(user, { video }); } }); auto maxWidth = st::historyCallWidth; diff --git a/Telegram/SourceFiles/info/info_wrap_widget.cpp b/Telegram/SourceFiles/info/info_wrap_widget.cpp index 4a59252659..57bf5a15f3 100644 --- a/Telegram/SourceFiles/info/info_wrap_widget.cpp +++ b/Telegram/SourceFiles/info/info_wrap_widget.cpp @@ -549,7 +549,7 @@ void WrapWidget::addProfileCallsButton() { ? st::infoLayerTopBarCall : st::infoTopBarCall)) )->addClickHandler([=] { - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, {}); }); }, _topBar->lifetime()); diff --git a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp index beff2e6a29..846c721981 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp @@ -861,7 +861,7 @@ void TopBar::setupActions(not_null controller) { tr::lng_profile_action_short_call(tr::now), st::infoProfileTopBarActionCall); call->setClickedCallback([=] { - Core::App().calls().startOutgoingCall(user, false); + Core::App().calls().startOutgoingCall(user, {}); }); buttons.push_back(call); _actions->add(call); From ff3f434acce6751e867275a3101a34d4d5884d2c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 14:38:28 +0300 Subject: [PATCH 055/348] Added popup menu to call button from history view top bar for users. --- .../view/history_view_top_bar_widget.cpp | 39 ++++++++++++++++++- .../view/history_view_top_bar_widget.h | 5 ++- 2 files changed, 41 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 bc6dded67d..7547a91f5e 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_send_action.h" #include "dialogs/dialogs_main_list.h" #include "chat_helpers/emoji_interactions.h" +#include "base/call_delayed.h" #include "base/unixtime.h" #include "support/support_helper.h" #include "apiwrap.h" @@ -155,6 +156,12 @@ TopBarWidget::TopBarWidget( _delete->setWidthChangedCallback([=] { updateControlsGeometry(); }); _clear->setClickedCallback([=] { _clearSelection.fire({}); }); _call->setClickedCallback([=] { call({}); }); + _call->setAcceptBoth(); + _call->addClickHandler([=](Qt::MouseButton button) { + if (button == Qt::RightButton) { + showCallMenu(); + } + }); _groupCall->setClickedCallback([=] { groupCall(); }); _menuToggle->setClickedCallback([=] { showPeerMenu(); }); _infoToggle->setClickedCallback([=] { toggleInfoSection(); }); @@ -342,13 +349,17 @@ void TopBarWidget::setChooseForReportReason( : style::cur_default); } -bool TopBarWidget::createMenu(not_null button) { +bool TopBarWidget::createMenu( + not_null button, + bool withIcons) { if (!_activeChat.key || _menu) { return false; } _menu = base::make_unique_q( this, - st::popupMenuExpandedSeparator); + withIcons + ? st::popupMenuExpandedSeparator + : st::defaultPopupMenu); _menu->setDestroyedCallback([ weak = base::make_weak(this), weakButton = base::make_weak(button), @@ -393,6 +404,30 @@ void TopBarWidget::showGroupCallMenu(not_null peer) { st::topBarMenuPosition.y()))); } +void TopBarWidget::showCallMenu() { + const auto created = createMenu(_call, false); + if (!created) { + return; + } + const auto perform = [&](bool video) { + return [=] { + base::call_delayed(st::defaultPopupMenu.showDuration, this, [=] { + call({ .video = video, .isConfirmed = true }); + }); + }; + }; + _menu->addAction( + tr::lng_profile_action_short_call(tr::now), + perform(false)); + _menu->addAction( + tr::lng_call_start_video(tr::now), + perform(true)); + _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + _menu->popup(mapToGlobal(QPoint( + _call->x() + _call->width() + st::topBarMenuGroupCallSkip, + st::topBarMenuPosition.y()))); +} + void TopBarWidget::toggleInfoSection() { const auto isThreeColumn = _controller->adaptive().isThreeColumn(); if (isThreeColumn diff --git a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h index e45ba8a22d..fab04e6309 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.h @@ -148,9 +148,12 @@ private: void call(Calls::StartOutgoingCallArgs); void groupCall(); void showGroupCallMenu(not_null peer); + void showCallMenu(); void toggleInfoSection(); - [[nodiscard]] bool createMenu(not_null button); + [[nodiscard]] bool createMenu( + not_null button, + bool withIcons = true); void handleEmojiInteractionSeen(const QString &emoticon); bool paintSendAction( From 7c675a378c5c5267802ae0535f6b509fab9dfba8 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 15:07:52 +0300 Subject: [PATCH 056/348] Added experimental option to force embedded search in chat. --- Telegram/SourceFiles/history/history_widget.cpp | 4 +++- Telegram/SourceFiles/mainwidget.cpp | 12 +++++++++++- Telegram/SourceFiles/mainwidget.h | 2 ++ .../SourceFiles/settings/settings_experimental.cpp | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4fa82cde06..955d41eba6 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -5709,7 +5709,9 @@ bool HistoryWidget::searchInChatEmbedded( Dialogs::Key chat, PeerData *searchFrom) { const auto peer = chat.peer(); // windows todo - if (!peer || Window::SeparateId(peer) != controller()->windowId()) { + if (!peer + || ((Window::SeparateId(peer) != controller()->windowId()) + && !controller()->isPrimary())) { return false; } else if (_peer != peer) { const auto weak = base::make_weak(this); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 1451e257e5..13e91cf2d3 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -70,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/player/media_player_dropdown.h" #include "media/player/media_player_instance.h" #include "base/qthelp_regex.h" +#include "base/options.h" #include "mtproto/mtproto_dc_options.h" #include "core/update_checker.h" #include "core/shortcuts.h" @@ -105,8 +106,16 @@ void ClearBotStartToken(PeerData *peer) { } } +base::options::toggle ForceComposeSearchOneColumn({ + .id = kForceComposeSearchOneColumn, + .name = "Force embedded search in chats", + .description = "Force in one-column mode the embedded search in chats.", +}); + } // namespace +const char kForceComposeSearchOneColumn[] = "force-compose-search-one-column"; + enum StackItemType { HistoryStackItem, SectionStackItem, @@ -782,7 +791,8 @@ void MainWidget::searchMessages( return; } auto tags = Data::SearchTagsFromQuery(query); - if (_dialogs) { + if (_dialogs + && (!ForceComposeSearchOneColumn.value() || !isOneColumn())) { auto state = Dialogs::SearchState{ .inChat = ((tags.empty() || inChat.sublist()) ? inChat diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index fc9281f9fe..198d2fffee 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -92,6 +92,8 @@ namespace Core { class Changelogs; } // namespace Core +extern const char kForceComposeSearchOneColumn[]; + class MainWidget final : public Ui::RpWidget , private Media::Player::FloatDelegate { diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp index f65f2c77af..6a23ef9dd6 100644 --- a/Telegram/SourceFiles/settings/settings_experimental.cpp +++ b/Telegram/SourceFiles/settings/settings_experimental.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_actions.h" #include "lang/lang_keys.h" #include "mainwindow.h" +#include "mainwidget.h" #include "media/player/media_player_instance.h" #include "mtproto/session_private.h" #include "webview/webview_embed.h" @@ -175,6 +176,7 @@ void SetupExperimental( addToggle(Window::kOptionDisableTouchbar); addToggle(Info::kAlternativeScrollProcessing); addToggle(kModerateCommonGroups); + addToggle(kForceComposeSearchOneColumn); } } // namespace From 67818a2c2821770ed5c4a063caad813687dcc1cf Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 15:12:49 +0300 Subject: [PATCH 057/348] Fixed position of popup menu from More button in history view top bar. --- .../SourceFiles/history/view/history_view_top_bar_widget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 7547a91f5e..c14e08bd7f 100644 --- a/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_top_bar_widget.cpp @@ -386,7 +386,9 @@ void TopBarWidget::showPeerMenu() { } else { _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); _menu->popup(mapToGlobal(QPoint( - width() + st::topBarMenuPosition.x(), + width() + + st::topBarMenuPosition.x() + + _menu->st().shadow.extend.right(), st::topBarMenuPosition.y()))); } } From ae851b87333058009e76b876d86515d1a655719c Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 26 Jan 2026 16:29:59 +0300 Subject: [PATCH 058/348] Fixed display of credits payment form even with small credits balance. --- .../SourceFiles/boxes/send_credits_box.cpp | 35 ++++++- Telegram/SourceFiles/boxes/send_credits_box.h | 10 +- .../payments/payments_non_panel_process.cpp | 97 ++++++++++--------- 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/Telegram/SourceFiles/boxes/send_credits_box.cpp b/Telegram/SourceFiles/boxes/send_credits_box.cpp index b4c9e5f450..0aac8371c6 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/send_credits_box.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "lang/lang_keys.h" +#include "main/session/session_show.h" #include "main/main_session.h" #include "payments/payments_checkout_process.h" #include "payments/payments_form.h" @@ -301,7 +302,7 @@ void AddTerms( void SendCreditsBox( not_null box, std::shared_ptr form, - Fn sent) { + Fn sent) { if (!form) { return; } @@ -391,7 +392,7 @@ void SendCreditsBox( Ui::AddSkip(content); Ui::AddSkip(content); - const auto button = box->addButton(rpl::single(QString()), [=] { + const auto sendStars = [=] { if (state->confirmButtonBusy.current()) { return; } @@ -411,7 +412,7 @@ void SendCreditsBox( state->confirmButtonBusy = false; box->closeBox(); } - sent(); + sent(Settings::SmallBalanceResult::Success); }).fail([=](const MTP::Error &error) { if (weak) { state->confirmButtonBusy = false; @@ -433,6 +434,22 @@ void SendCreditsBox( show->showToast(id); } }).send(); + }; + + const auto button = box->addButton(rpl::single(QString()), [=] { + const auto weak = base::make_weak(box.get()); + Settings::MaybeRequestBalanceIncrease( + Main::MakeSessionShow(box->uiShow(), session), + form->invoice.credits, + SmallBalanceSourceFromForm(form), + [=](Settings::SmallBalanceResult result) { + if (result == Settings::SmallBalanceResult::Cancelled) { + } else if (result == Settings::SmallBalanceResult::Success) { + sendStars(); + } else { + sent(result); + } + }); }); if (form->invoice.subscriptionPeriod) { AddTerms(box, button, stBox); @@ -573,4 +590,16 @@ void SendStarsForm( }).send(); } +Settings::SmallBalanceSource SmallBalanceSourceFromForm( + std::shared_ptr form) { + using namespace Payments; + using namespace Settings; + const auto starGift = std::get_if(&form->id.value); + return !starGift + ? SmallBalanceSource(SmallBalanceBot{ .botId = form->botId }) + : SmallBalanceSource(SmallBalanceStarGift{ + .recipientId = starGift->recipient->id, + }); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_credits_box.h b/Telegram/SourceFiles/boxes/send_credits_box.h index 43fb75b033..d2bef3e54f 100644 --- a/Telegram/SourceFiles/boxes/send_credits_box.h +++ b/Telegram/SourceFiles/boxes/send_credits_box.h @@ -21,6 +21,11 @@ namespace Payments { struct CreditsFormData; } // namespace Payments +namespace Settings { +enum class SmallBalanceResult; +struct SmallBalanceSource; +} // namespace Settings + namespace Ui { class RpWidget; @@ -30,7 +35,7 @@ class FlatLabel; void SendCreditsBox( not_null box, std::shared_ptr data, - Fn sent); + Fn sent); [[nodiscard]] TextWithEntities CreditsEmoji(); @@ -55,4 +60,7 @@ void SendStarsForm( std::shared_ptr data, Fn)> done); +[[nodiscard]] Settings::SmallBalanceSource SmallBalanceSourceFromForm( + std::shared_ptr form); + } // namespace Ui diff --git a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp index e704bd3389..fa517bf9c2 100644 --- a/Telegram/SourceFiles/payments/payments_non_panel_process.cpp +++ b/Telegram/SourceFiles/payments/payments_non_panel_process.cpp @@ -46,64 +46,77 @@ void ProcessCreditsPayment( QPointer fireworks, std::shared_ptr form, Fn maybeReturnToBot) { - const auto done = [=](Settings::SmallBalanceResult result) { + const auto hasBadResult = [=](Settings::SmallBalanceResult result) { if (result == Settings::SmallBalanceResult::Blocked) { if (const auto onstack = maybeReturnToBot) { onstack(CheckoutResult::Failed); } - return; + return true; } else if (result == Settings::SmallBalanceResult::Cancelled) { if (const auto onstack = maybeReturnToBot) { onstack(CheckoutResult::Cancelled); } + return true; + } + return false; + }; + const auto done = [=](Settings::SmallBalanceResult result) { + if (hasBadResult(result)) { return; - } else if (form->starGiftForm - || IsPremiumForStarsInvoice(form->id)) { - const auto done = [=](std::optional error) { - const auto onstack = maybeReturnToBot; - if (error) { - if (*error == u"STARGIFT_USAGE_LIMITED"_q) { - if (form->starGiftLimitedCount) { - show->showToast({ - .title = tr::lng_gift_sold_out_title( - tr::now), - .text = tr::lng_gift_sold_out_text( - tr::now, - lt_count_decimal, - form->starGiftLimitedCount, - tr::rich), - }); - } else { - show->showToast( - tr::lng_gift_sold_out_title(tr::now)); - } - } else if (*error == u"STARGIFT_USER_USAGE_LIMITED"_q) { + } + const auto done = [=](std::optional error) { + const auto onstack = maybeReturnToBot; + if (error) { + if (*error == u"STARGIFT_USAGE_LIMITED"_q) { + if (form->starGiftLimitedCount) { show->showToast({ - .text = tr::lng_gift_sent_finished( + .title = tr::lng_gift_sold_out_title( + tr::now), + .text = tr::lng_gift_sold_out_text( tr::now, - lt_count, - std::max(form->starGiftPerUserLimit, 1), + lt_count_decimal, + form->starGiftLimitedCount, tr::rich), }); } else { - show->showToast(*error); + show->showToast( + tr::lng_gift_sold_out_title(tr::now)); } - if (onstack) { - onstack(CheckoutResult::Failed); - } - } else if (onstack) { - onstack(CheckoutResult::Paid); + } else if (*error == u"STARGIFT_USER_USAGE_LIMITED"_q) { + show->showToast({ + .text = tr::lng_gift_sent_finished( + tr::now, + lt_count, + std::max(form->starGiftPerUserLimit, 1), + tr::rich), + }); + } else { + show->showToast(*error); } - }; - Ui::SendStarsForm(&show->session(), form, done); - return; - } + if (onstack) { + onstack(CheckoutResult::Failed); + } + } else if (onstack) { + onstack(CheckoutResult::Paid); + } + }; + Ui::SendStarsForm(&show->session(), form, done); + }; + using namespace Settings; + auto source = Ui::SmallBalanceSourceFromForm(form); + if (form->starGiftForm || IsPremiumForStarsInvoice(form->id)) { + const auto credits = form->invoice.credits; + MaybeRequestBalanceIncrease(show, credits, source, done); + } else { const auto unsuccessful = std::make_shared(true); const auto box = show->show(Box( Ui::SendCreditsBox, form, - [=] { + [=](Settings::SmallBalanceResult result) { *unsuccessful = false; + if (hasBadResult(result)) { + return; + } if (const auto widget = fireworks.data()) { Ui::StartFireworks(widget); } @@ -120,15 +133,7 @@ void ProcessCreditsPayment( } }); }, box->lifetime()); - }; - using namespace Settings; - const auto starGift = std::get_if(&form->id.value); - auto source = !starGift - ? SmallBalanceSource(SmallBalanceBot{ .botId = form->botId }) - : SmallBalanceSource(SmallBalanceStarGift{ - .recipientId = starGift->recipient->id, - }); - MaybeRequestBalanceIncrease(show, form->invoice.credits, source, done); + } } void ProcessCreditsReceipt( From 4cd0e2c9d76b6f6c6e7cfadd83f9acedb5d69e23 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 28 Jan 2026 09:20:42 +0300 Subject: [PATCH 059/348] Added simple icon to button for bot privacy policy from profile actions. --- Telegram/SourceFiles/info/info.style | 1 + Telegram/SourceFiles/info/profile/info_profile_actions.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 53dc0ff6f6..1cec7810ac 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -668,6 +668,7 @@ infoIconReport: icon {{ "info/info_report", attentionButtonFg }}; infoIconLeave: icon {{ "info/info_leave", infoIconFg }}; infoIconBlock: icon {{ "info/info_block", attentionButtonFg }}; infoIconMembers: icon {{ "info/info_members", infoIconFg }}; +infoIconPrivacyPolicy: icon {{ "menu/2sv_off", infoIconFg, point(4px, 4px) }}; infoInformationIconPosition: point(25px, 12px); infoNotificationsIconPosition: point(20px, 5px); infoSharedMediaButtonIconPosition: point(20px, 3px); diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index a83831a4b9..16bffadeda 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -2634,7 +2634,7 @@ void ActionsFiller::addBotCommandActions(not_null user) { tr::lng_profile_bot_privacy(), rpl::single(true), openPrivacyPolicy, - nullptr); + &st::infoIconPrivacyPolicy); } void ActionsFiller::addReportAction() { From 156942f8fb8fccf62f385b28dd9e8b7e54578664 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 28 Jan 2026 09:31:10 +0300 Subject: [PATCH 060/348] Added saving of geometry for call panel. --- Telegram/SourceFiles/calls/calls_panel.cpp | 70 ++++++++++++++++++++- Telegram/SourceFiles/calls/calls_panel.h | 3 + Telegram/SourceFiles/core/core_settings.cpp | 15 ++++- Telegram/SourceFiles/core/core_settings.h | 8 +++ 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index b01c1f1d80..2c8b1e308e 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -49,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/rect.h" #include "ui/integration.h" #include "core/application.h" +#include "core/core_settings.h" #include "lang/lang_keys.h" #include "main/session/session_show.h" #include "main/main_session.h" @@ -59,6 +60,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/power_save_blocker.h" #include "media/streaming/media_streaming_utility.h" #include "window/main_window.h" +#include "window/window_controller.h" #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" #include "styles/style_calls.h" @@ -169,6 +171,30 @@ bool Panel::isActive() const { return window()->isActiveWindow() && isVisible(); } +QRect Panel::panelGeometry() const { + const auto saved = Core::App().settings().callPanelPosition(); + const auto adjusted = Core::AdjustToScale(saved, u"Call"_q); + const auto center = Core::App().getPointForCallPanelCenter(); + const auto simple = QRect(0, 0, st::callWidth, st::callHeight); + const auto initial = simple.translated(center - simple.center()); + auto result = initial; + if (const auto window = Core::App().activeWindow()) { + const auto initialPosition = Core::WindowPosition{ + .moncrc = 0, + .scale = cScale(), + .x = initial.x(), + .y = initial.y(), + .w = initial.width(), + .h = initial.height(), + }; + result = window->widget()->countInitialGeometry( + adjusted, + initialPosition, + { st::callWidthMin, st::callHeightMin }); + } + return result; +} + ConferencePanelMigration Panel::migrationInfo() const { return ConferencePanelMigration{ .window = _window }; } @@ -207,6 +233,41 @@ void Panel::replaceCall(not_null call) { updateControlsGeometry(); } +void Panel::savePanelGeometry() { + if (!window()->windowHandle()) { + return; + } + const auto state = window()->windowHandle()->windowState(); + if (state == Qt::WindowMinimized) { + return; + } + const auto &savedPosition = Core::App().settings().callPanelPosition(); + auto realPosition = savedPosition; + if (state == Qt::WindowMaximized) { + realPosition.maximized = 1; + realPosition.moncrc = 0; + } else { + auto r = window()->body()->mapToGlobal(window()->body()->rect()); + realPosition.x = r.x(); + realPosition.y = r.y(); + realPosition.w = r.width(); + realPosition.h = r.height(); + realPosition.scale = cScale(); + realPosition.maximized = 0; + realPosition.moncrc = 0; + } + realPosition = ::Window::PositionWithScreen( + realPosition, + window(), + { st::callWidthMin, st::callHeightMin }); + if (realPosition.w >= st::callWidthMin + && realPosition.h >= st::callHeightMin + && realPosition != savedPosition) { + Core::App().settings().setCallPanelPosition(realPosition); + Core::App().saveSettingsDelayed(); + } +} + void Panel::initWindow() { window()->setAttribute(Qt::WA_OpaquePaintEvent); window()->setAttribute(Qt::WA_NoSystemBackground); @@ -946,12 +1007,15 @@ rpl::lifetime &Panel::lifetime() { } void Panel::initGeometry() { - const auto center = Core::App().getPointForCallPanelCenter(); - const auto initRect = QRect(0, 0, st::callWidth, st::callHeight); - window()->setGeometry(initRect.translated(center - initRect.center())); + window()->setGeometry(panelGeometry()); window()->setMinimumSize({ st::callWidthMin, st::callHeightMin }); window()->show(); updateControlsGeometry(); + + _geometryLifetime = window()->geometryValue( + ) | rpl::skip(1) | rpl::on_next([=](QRect r) { + savePanelGeometry(); + }); } void Panel::initMediaDeviceToggles() { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index 66716a76be..aaed848f7f 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -79,6 +79,7 @@ public: [[nodiscard]] not_null user() const; [[nodiscard]] bool isVisible() const; [[nodiscard]] bool isActive() const; + [[nodiscard]] QRect panelGeometry() const; [[nodiscard]] ConferencePanelMigration migrationInfo() const; @@ -86,6 +87,7 @@ public: void minimize(); void toggleFullScreen(); void replaceCall(not_null call); + void savePanelGeometry(); void closeBeforeDestroy(bool windowIsReused = false); QWidget *chooseSourceParent() override; @@ -218,6 +220,7 @@ private: std::unique_ptr _background; + rpl::lifetime _geometryLifetime; rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index e60324915b..0a9cc941c4 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -155,6 +155,8 @@ QByteArray Settings::serialize() const { LogPosition(_mediaViewPosition, u"Viewer"_q); const auto ivPosition = Serialize(_ivPosition); LogPosition(_ivPosition, u"IV"_q); + const auto callPanelPosition = Serialize(_callPanelPosition); + LogPosition(_callPanelPosition, u"CallPanel"_q); const auto proxy = _proxy.serialize(); const auto skipLanguages = _skipTranslationLanguages.current(); @@ -242,7 +244,8 @@ QByteArray Settings::serialize() const { + Serialize::bytearraySize(_tonsiteStorageToken) + sizeof(qint32) * 8 + sizeof(ushort) - + sizeof(qint32); // _notificationsDisplayChecksum + + sizeof(qint32) // _notificationsDisplayChecksum + + Serialize::bytearraySize(callPanelPosition); auto result = QByteArray(); result.reserve(size); @@ -406,7 +409,8 @@ QByteArray Settings::serialize() const { << qint32(_systemDarkModeEnabled.current() ? 1 : 0) << qint32(_quickDialogAction) << _notificationsVolume - << _notificationsDisplayChecksum; + << _notificationsDisplayChecksum + << callPanelPosition; } Ensures(result.size() == size); @@ -527,6 +531,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 trayIconMonochrome = (_trayIconMonochrome.current() ? 1 : 0); qint32 ttlVoiceClickTooltipHidden = _ttlVoiceClickTooltipHidden.current() ? 1 : 0; QByteArray ivPosition; + QByteArray callPanelPosition; QString customFontFamily = _customFontFamily; qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0; qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2; @@ -875,6 +880,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> notificationsDisplayChecksum; } + if (!stream.atEnd()) { + stream >> callPanelPosition; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -1086,6 +1094,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!ivPosition.isEmpty()) { _ivPosition = Deserialize(ivPosition); } + if (!callPanelPosition.isEmpty()) { + _callPanelPosition = Deserialize(callPanelPosition); + } _customFontFamily = customFontFamily; _systemUnlockEnabled = (systemUnlockEnabled == 1); _weatherInCelsius = !weatherInCelsius diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 0a78d2c22c..8c57e359cb 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -903,6 +903,13 @@ public: _ivPosition = position; } + [[nodiscard]] const WindowPosition &callPanelPosition() const { + return _callPanelPosition; + } + void setCallPanelPosition(const WindowPosition &position) { + _callPanelPosition = position; + } + [[nodiscard]] QString customFontFamily() const { return _customFontFamily; } @@ -1085,6 +1092,7 @@ private: rpl::variable _storiesClickTooltipHidden = false; rpl::variable _ttlVoiceClickTooltipHidden = false; WindowPosition _ivPosition; + WindowPosition _callPanelPosition; QString _customFontFamily; bool _systemUnlockEnabled = false; std::optional _weatherInCelsius; From 375a8cea12e4f9b05fc9213300681776766736b4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 Dec 2025 17:26:21 +0400 Subject: [PATCH 061/348] Initial settings builder experiment. --- Telegram/CMakeLists.txt | 4 + .../settings/builder/settings_builder.cpp | 125 +++++++++++++++ .../settings/builder/settings_builder.h | 91 +++++++++++ .../builder/settings_main_builder.cpp | 145 ++++++++++++++++++ .../settings/builder/settings_main_builder.h | 33 ++++ .../SourceFiles/settings/settings_main.cpp | 32 +++- Telegram/SourceFiles/settings/settings_main.h | 1 + 7 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 Telegram/SourceFiles/settings/builder/settings_builder.cpp create mode 100644 Telegram/SourceFiles/settings/builder/settings_builder.h create mode 100644 Telegram/SourceFiles/settings/builder/settings_main_builder.cpp create mode 100644 Telegram/SourceFiles/settings/builder/settings_main_builder.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index d3e757a5d6..5ac4f287e4 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1567,6 +1567,10 @@ PRIVATE settings/settings_local_passcode.h settings/settings_main.cpp settings/settings_main.h + settings/builder/settings_builder.cpp + settings/builder/settings_builder.h + settings/builder/settings_main_builder.cpp + settings/builder/settings_main_builder.h settings/settings_notifications.cpp settings/settings_notifications.h settings/settings_notifications_type.cpp diff --git a/Telegram/SourceFiles/settings/builder/settings_builder.cpp b/Telegram/SourceFiles/settings/builder/settings_builder.cpp new file mode 100644 index 0000000000..0717eae1bd --- /dev/null +++ b/Telegram/SourceFiles/settings/builder/settings_builder.cpp @@ -0,0 +1,125 @@ +/* +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/builder/settings_builder.h" + +#include "settings/settings_common.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_settings.h" + +namespace Settings::Builder { +namespace { + +[[nodiscard]] QString ResolveTitle(rpl::producer title) { + auto result = QString(); + auto lifetime = rpl::lifetime(); + std::move(title).start( + [&](QString value) { result = std::move(value); }, + [](auto&&) {}, + [] {}, + lifetime); + return result; +} + +} // namespace + +SectionBuilder::SectionBuilder(BuildContext context) +: _context(std::move(context)) { +} + +Ui::RpWidget *SectionBuilder::addControl(ControlArgs &&args) { + return v::match(_context, [&](const WidgetContext &ctx) { + if (!args.factory) { + return static_cast(nullptr); + } + auto widget = args.factory(ctx.container); + const auto raw = widget.data(); + ctx.container->add(std::move(widget), args.margin); + return raw; + }, [&](const SearchContext &ctx) { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return static_cast(nullptr); + }); +} + +Ui::SettingsButton *SectionBuilder::addSettingsButton(ButtonArgs &&args) { + const auto &st = args.st ? *args.st : st::settingsButton; + const auto button = v::match(_context, [&](const WidgetContext &ctx) -> Ui::SettingsButton* { + if (args.label) { + return AddButtonWithLabel( + ctx.container, + rpl::duplicate(args.title), + std::move(args.label), + st, + std::move(args.icon)); + } else { + return ctx.container->add(CreateButtonWithIcon( + ctx.container, + rpl::duplicate(args.title), + st, + std::move(args.icon))); + } + }, [&](const SearchContext &ctx) -> Ui::SettingsButton* { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return nullptr; + }); + if (button && args.onClick) { + button->addClickHandler(std::move(args.onClick)); + } + return button; +} + +Ui::SettingsButton *SectionBuilder::addLabeledButton(ButtonArgs &&args) { + return addSettingsButton(std::move(args)); +} + +Ui::SettingsButton *SectionBuilder::addSectionButton(SectionArgs &&args) { + const auto button = addSettingsButton({ + .id = std::move(args.id), + .title = std::move(args.title), + .icon = std::move(args.icon), + .keywords = std::move(args.keywords), + }); + if (button) { + const auto showOther = std::get(_context).showOther; + const auto target = args.targetSection; + button->addClickHandler([=] { + showOther(target); + }); + } + return button; +} + +void SectionBuilder::addDivider() { + v::match(_context, [&](const WidgetContext &ctx) { + Ui::AddDivider(ctx.container); + }, [](const SearchContext &) { + }); +} + +void SectionBuilder::addSkip() { + v::match(_context, [&](const WidgetContext &ctx) { + Ui::AddSkip(ctx.container); + }, [](const SearchContext &) { + }); +} + +} // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/builder/settings_builder.h b/Telegram/SourceFiles/settings/builder/settings_builder.h new file mode 100644 index 0000000000..0379b56f69 --- /dev/null +++ b/Telegram/SourceFiles/settings/builder/settings_builder.h @@ -0,0 +1,91 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" +#include "settings/settings_common.h" +#include "settings/settings_type.h" + +#include + +namespace Ui { +class RpWidget; +class VerticalLayout; +class SettingsButton; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace style { +struct SettingsButton; +} // namespace style + +namespace Settings::Builder { + +struct SearchEntry { + QString id; + QString title; + QStringList keywords; +}; + +struct WidgetContext { + not_null container; + not_null controller; + Fn showOther; +}; + +struct SearchContext { + not_null*> entries; +}; + +using BuildContext = std::variant; + +class SectionBuilder { +public: + explicit SectionBuilder(BuildContext context); + + struct ControlArgs { + Fn(not_null)> factory; + QString id; + rpl::producer title; + QStringList keywords; + style::margins margin; + }; + Ui::RpWidget *addControl(ControlArgs &&args); + + struct ButtonArgs { + QString id; + rpl::producer title; + const style::SettingsButton *st = nullptr; + IconDescriptor icon; + rpl::producer label; + Fn onClick; + QStringList keywords; + }; + Ui::SettingsButton *addSettingsButton(ButtonArgs &&args); + Ui::SettingsButton *addLabeledButton(ButtonArgs &&args); + + struct SectionArgs { + QString id; + rpl::producer title; + Type targetSection; + IconDescriptor icon; + QStringList keywords; + }; + Ui::SettingsButton *addSectionButton(SectionArgs &&args); + + void addDivider(); + void addSkip(); + +private: + BuildContext _context; +}; + +} // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp b/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp new file mode 100644 index 0000000000..ec58216333 --- /dev/null +++ b/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp @@ -0,0 +1,145 @@ +/* +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/builder/settings_main_builder.h" + +#include "ui/layers/generic_box.h" +#include "settings/builder/settings_builder.h" +#include "settings/settings_advanced.h" +#include "settings/settings_calls.h" +#include "settings/settings_chat.h" +#include "settings/settings_folders.h" +#include "settings/settings_information.h" +#include "settings/settings_notifications.h" +#include "settings/settings_power_saving.h" +#include "settings/settings_privacy_security.h" +#include "boxes/language_box.h" +#include "lang/lang_instance.h" +#include "lang/lang_keys.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +namespace Settings::Builder { +namespace { + +void BuildMainSectionContent( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addDivider(); + builder.addSkip(); + + builder.addSectionButton({ + .id = u"main/account"_q, + .title = tr::lng_settings_my_account(), + .targetSection = Information::Id(), + .icon = { &st::menuIconProfile }, + .keywords = { u"profile"_q, u"edit"_q, u"information"_q }, + }); + + builder.addSectionButton({ + .id = u"main/notifications"_q, + .title = tr::lng_settings_section_notify(), + .targetSection = Notifications::Id(), + .icon = { &st::menuIconNotifications }, + .keywords = { u"alerts"_q, u"sounds"_q, u"badge"_q }, + }); + + builder.addSectionButton({ + .id = u"main/privacy"_q, + .title = tr::lng_settings_section_privacy(), + .targetSection = PrivacySecurity::Id(), + .icon = { &st::menuIconLock }, + .keywords = { u"security"_q, u"passcode"_q, u"password"_q, u"2fa"_q }, + }); + + builder.addSectionButton({ + .id = u"main/chat"_q, + .title = tr::lng_settings_section_chat_settings(), + .targetSection = Chat::Id(), + .icon = { &st::menuIconChatBubble }, + .keywords = { u"themes"_q, u"appearance"_q, u"stickers"_q }, + }); + + builder.addSectionButton({ + .id = u"main/folders"_q, + .title = tr::lng_settings_section_filters(), + .targetSection = Folders::Id(), + .icon = { &st::menuIconShowInFolder }, + .keywords = { u"filters"_q, u"tabs"_q }, + }); + + builder.addSectionButton({ + .id = u"main/advanced"_q, + .title = tr::lng_settings_advanced(), + .targetSection = Advanced::Id(), + .icon = { &st::menuIconManage }, + .keywords = { u"performance"_q, u"proxy"_q, u"experimental"_q }, + }); + + builder.addSectionButton({ + .id = u"main/devices"_q, + .title = tr::lng_settings_section_devices(), + .targetSection = Calls::Id(), + .icon = { &st::menuIconUnmute }, + .keywords = { u"sessions"_q, u"calls"_q }, + }); + + const auto window = controller ? &controller->window() : nullptr; + builder.addSettingsButton({ + .id = u"main/power"_q, + .title = tr::lng_settings_power_menu(), + .icon = { &st::menuIconPowerUsage }, + .onClick = window + ? [=] { window->show(Box(PowerSavingBox)); } + : Fn(nullptr), + .keywords = { u"battery"_q, u"animations"_q, u"power"_q, u"saving"_q }, + }); + + builder.addLabeledButton({ + .id = u"main/language"_q, + .title = tr::lng_settings_language(), + .icon = { &st::menuIconTranslate }, + .label = rpl::single( + Lang::GetInstance().id() + ) | rpl::then( + Lang::GetInstance().idChanges() + ) | rpl::map([] { return Lang::GetInstance().nativeName(); }), + .onClick = controller + ? [=] { LanguageBox::Show(controller); } + : Fn(nullptr), + .keywords = { u"translate"_q, u"localization"_q }, + }); + + builder.addSkip(); +} + +} // namespace + +void BuildMainSection( + not_null container, + not_null controller, + Fn showOther) { + auto builder = SectionBuilder(WidgetContext{ + .container = container, + .controller = controller, + .showOther = std::move(showOther), + }); + BuildMainSectionContent(builder, controller); +} + +std::vector BuildMainSectionForSearch() { + auto entries = std::vector(); + auto builder = SectionBuilder(SearchContext{ + .entries = &entries, + }); + BuildMainSectionContent(builder, nullptr); + return entries; +} + +} // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/builder/settings_main_builder.h b/Telegram/SourceFiles/settings/builder/settings_main_builder.h new file mode 100644 index 0000000000..9c5273cf5f --- /dev/null +++ b/Telegram/SourceFiles/settings/builder/settings_main_builder.h @@ -0,0 +1,33 @@ +/* +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 "settings/settings_type.h" + +#include + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings::Builder { + +struct SearchEntry; + +void BuildMainSection( + not_null container, + not_null controller, + Fn showOther); + +[[nodiscard]] std::vector BuildMainSectionForSearch(); + +} // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 5f24dcf3d1..1b7f6c5ee7 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/builder/settings_main_builder.h" #include "api/api_credits.h" #include "core/application.h" #include "core/click_handler_types.h" @@ -1034,7 +1035,7 @@ Main::Main( not_null controller) : Section(parent) , _controller(controller) { - setupContent(controller); + setupContentWithBuilder(controller); _controller->session().api().premium().reload(); } @@ -1099,6 +1100,35 @@ void Main::setupContent(not_null controller) { controller->session().data().cloudThemes().refresh(); } +void Main::setupContentWithBuilder( + not_null controller) { + const auto content = Ui::CreateChild(this); + + content->add(object_ptr( + content, + controller, + controller->session().user())); + + Builder::BuildMainSection(content, controller, showOtherMethod()); + + if (HasInterfaceScale()) { + Ui::AddDivider(content); + Ui::AddSkip(content); + SetupInterfaceScale(&controller->window(), content); + Ui::AddSkip(content); + } + SetupPremium(controller, content, showOtherMethod()); + SetupHelp(controller, content); + + Ui::ResizeFitChild(this, content); + + controller->session().api().cloudPassword().reload(); + controller->session().api().reloadContactSignupSilent(); + controller->session().api().sensitiveContent().reload(); + controller->session().api().globalPrivacy().reload(); + controller->session().data().cloudThemes().refresh(); +} + void OpenFaq(base::weak_ptr weak) { UrlClickHandler::Open( tr::lng_settings_faq_link(tr::now), diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index a0f7e15ca6..9da6d278c3 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -45,6 +45,7 @@ protected: private: void setupContent(not_null controller); + void setupContentWithBuilder(not_null controller); const not_null _controller; From 74f204e8e8d65ff4179cbb9023c7b051763c3d08 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Dec 2025 16:58:09 +0400 Subject: [PATCH 062/348] Migrate all settings main section to builder. --- Telegram/SourceFiles/boxes/language_box.h | 3 +- .../settings/builder/settings_builder.cpp | 144 +++++ .../settings/builder/settings_builder.h | 45 ++ .../builder/settings_main_builder.cpp | 329 ++++++++++-- .../SourceFiles/settings/settings_common.cpp | 83 +++ .../SourceFiles/settings/settings_common.h | 5 + .../SourceFiles/settings/settings_main.cpp | 498 ++---------------- Telegram/SourceFiles/settings/settings_main.h | 10 +- 8 files changed, 618 insertions(+), 499 deletions(-) diff --git a/Telegram/SourceFiles/boxes/language_box.h b/Telegram/SourceFiles/boxes/language_box.h index 350b570a68..e6998474df 100644 --- a/Telegram/SourceFiles/boxes/language_box.h +++ b/Telegram/SourceFiles/boxes/language_box.h @@ -28,7 +28,8 @@ public: void setInnerFocus() override; - static base::binary_guard Show(Window::SessionController *controller); + [[nodiscard]] static base::binary_guard Show( + Window::SessionController *controller); protected: void prepare() override; diff --git a/Telegram/SourceFiles/settings/builder/settings_builder.cpp b/Telegram/SourceFiles/settings/builder/settings_builder.cpp index 0717eae1bd..3290e56e6c 100644 --- a/Telegram/SourceFiles/settings/builder/settings_builder.cpp +++ b/Telegram/SourceFiles/settings/builder/settings_builder.cpp @@ -10,7 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" +#include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" #include "styles/style_settings.h" namespace Settings::Builder { @@ -122,4 +124,146 @@ void SectionBuilder::addSkip() { }); } +void SectionBuilder::addSkip(int height) { + v::match(_context, [&](const WidgetContext &ctx) { + Ui::AddSkip(ctx.container, height); + }, [](const SearchContext &) { + }); +} + +void SectionBuilder::addDividerText(rpl::producer text) { + v::match(_context, [&](const WidgetContext &ctx) { + Ui::AddDividerText(ctx.container, std::move(text)); + }, [](const SearchContext &) { + }); +} + +Ui::SlideWrap *SectionBuilder::addSlideButton( + SlideButtonArgs &&args) { + return v::match(_context, [&](const WidgetContext &ctx) + -> Ui::SlideWrap* { + const auto &st = args.st ? *args.st : st::settingsButton; + const auto wrap = ctx.container->add( + object_ptr>( + ctx.container, + CreateButtonWithIcon( + ctx.container, + std::move(args.title), + st, + std::move(args.icon)))); + if (args.shown) { + wrap->toggleOn(std::move(args.shown)); + } + const auto button = wrap->entity(); + if (args.onClick) { + button->addClickHandler(std::move(args.onClick)); + } + return wrap; + }, [&](const SearchContext &ctx) -> Ui::SlideWrap* { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return nullptr; + }); +} + +Ui::SlideWrap *SectionBuilder::addSlideLabeledButton( + SlideLabeledButtonArgs &&args) { + return v::match(_context, [&](const WidgetContext &ctx) + -> Ui::SlideWrap* { + const auto &st = args.st ? *args.st : st::settingsButton; + auto button = object_ptr( + ctx.container, + rpl::duplicate(args.title), + st); + const auto raw = button.data(); + if (args.icon) { + AddButtonIcon(raw, st, std::move(args.icon)); + } + if (args.label) { + CreateRightLabel( + raw, + std::move(args.label), + st, + rpl::duplicate(args.title)); + } + const auto wrap = ctx.container->add( + object_ptr>( + ctx.container, + std::move(button))); + if (args.shown) { + wrap->toggleOn(std::move(args.shown)); + } + if (args.onClick) { + raw->addClickHandler(std::move(args.onClick)); + } + return wrap; + }, [&](const SearchContext &ctx) -> Ui::SlideWrap* { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return nullptr; + }); +} + +Ui::SettingsButton *SectionBuilder::addPremiumButton(PremiumButtonArgs &&args) { + return v::match(_context, [&](const WidgetContext &ctx) + -> Ui::SettingsButton* { + Ui::SettingsButton *button = nullptr; + if (args.label) { + button = AddButtonWithLabel( + ctx.container, + rpl::duplicate(args.title), + std::move(args.label), + st::settingsButton); + } else { + button = AddButtonWithIcon( + ctx.container, + rpl::duplicate(args.title), + st::settingsButton); + } + [[maybe_unused]] const auto decorated = AddPremiumStar( + button, + args.credits, + ctx.isPaused); + if (args.onClick) { + button->addClickHandler(std::move(args.onClick)); + } + return button; + }, [&](const SearchContext &ctx) -> Ui::SettingsButton* { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return nullptr; + }); +} + +Window::SessionController *SectionBuilder::controller() const { + return v::match(_context, [](const WidgetContext &ctx) { + return ctx.controller.get(); + }, [](const SearchContext &) -> Window::SessionController* { + return nullptr; + }); +} + +Fn SectionBuilder::showOther() const { + return v::match(_context, [](const WidgetContext &ctx) { + return ctx.showOther; + }, [](const SearchContext &) -> Fn { + return nullptr; + }); +} + } // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/builder/settings_builder.h b/Telegram/SourceFiles/settings/builder/settings_builder.h index 0379b56f69..965ca900b9 100644 --- a/Telegram/SourceFiles/settings/builder/settings_builder.h +++ b/Telegram/SourceFiles/settings/builder/settings_builder.h @@ -27,6 +27,11 @@ namespace style { struct SettingsButton; } // namespace style +namespace Ui { +template +class SlideWrap; +} // namespace Ui + namespace Settings::Builder { struct SearchEntry { @@ -39,6 +44,7 @@ struct WidgetContext { not_null container; not_null controller; Fn showOther; + Fn isPaused; }; struct SearchContext { @@ -81,8 +87,47 @@ public: }; Ui::SettingsButton *addSectionButton(SectionArgs &&args); + struct SlideButtonArgs { + QString id; + rpl::producer title; + const style::SettingsButton *st = nullptr; + IconDescriptor icon; + rpl::producer shown; + Fn onClick; + QStringList keywords; + }; + Ui::SlideWrap *addSlideButton(SlideButtonArgs &&args); + + struct SlideLabeledButtonArgs { + QString id; + rpl::producer title; + const style::SettingsButton *st = nullptr; + IconDescriptor icon; + rpl::producer label; + rpl::producer shown; + Fn onClick; + QStringList keywords; + }; + Ui::SlideWrap *addSlideLabeledButton( + SlideLabeledButtonArgs &&args); + + struct PremiumButtonArgs { + QString id; + rpl::producer title; + rpl::producer label; + bool credits = false; + Fn onClick; + QStringList keywords; + }; + Ui::SettingsButton *addPremiumButton(PremiumButtonArgs &&args); + void addDivider(); + void addDividerText(rpl::producer text); void addSkip(); + void addSkip(int height); + + [[nodiscard]] Window::SessionController *controller() const; + [[nodiscard]] Fn showOther() const; private: BuildContext _context; diff --git a/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp b/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp index ec58216333..efec26f190 100644 --- a/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp +++ b/Telegram/SourceFiles/settings/builder/settings_main_builder.cpp @@ -7,19 +7,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/builder/settings_main_builder.h" -#include "ui/layers/generic_box.h" -#include "settings/builder/settings_builder.h" -#include "settings/settings_advanced.h" -#include "settings/settings_calls.h" -#include "settings/settings_chat.h" -#include "settings/settings_folders.h" -#include "settings/settings_information.h" -#include "settings/settings_notifications.h" -#include "settings/settings_power_saving.h" -#include "settings/settings_privacy_security.h" +#include "api/api_credits.h" #include "boxes/language_box.h" +#include "boxes/star_gift_box.h" +#include "core/click_handler_types.h" +#include "data/components/credits.h" +#include "data/data_chat_filters.h" +#include "data/data_session.h" #include "lang/lang_instance.h" #include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "main/main_session_settings.h" +#include "settings/builder/settings_builder.h" +#include "settings/settings_advanced.h" +#include "settings/settings_business.h" +#include "settings/settings_calls.h" +#include "settings/settings_chat.h" +#include "settings/settings_credits.h" +#include "settings/settings_folders.h" +#include "settings/settings_information.h" +#include "settings/settings_main.h" +#include "settings/settings_notifications.h" +#include "settings/settings_power_saving.h" +#include "settings/settings_premium.h" +#include "settings/settings_privacy_security.h" +#include "ui/basic_click_handlers.h" +#include "ui/layers/generic_box.h" +#include "ui/new_badges.h" +#include "ui/text/format_values.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_menu_icons.h" @@ -28,19 +45,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Settings::Builder { namespace { -void BuildMainSectionContent( +void BuildSectionButtons( SectionBuilder &builder, Window::SessionController *controller) { - builder.addDivider(); - builder.addSkip(); - - builder.addSectionButton({ - .id = u"main/account"_q, - .title = tr::lng_settings_my_account(), - .targetSection = Information::Id(), - .icon = { &st::menuIconProfile }, - .keywords = { u"profile"_q, u"edit"_q, u"information"_q }, - }); + if (!controller || !controller->session().supportMode()) { + builder.addSectionButton({ + .id = u"main/account"_q, + .title = tr::lng_settings_my_account(), + .targetSection = Information::Id(), + .icon = { &st::menuIconProfile }, + .keywords = { u"profile"_q, u"edit"_q, u"information"_q }, + }); + } builder.addSectionButton({ .id = u"main/notifications"_q, @@ -66,13 +82,50 @@ void BuildMainSectionContent( .keywords = { u"themes"_q, u"appearance"_q, u"stickers"_q }, }); - builder.addSectionButton({ - .id = u"main/folders"_q, - .title = tr::lng_settings_section_filters(), - .targetSection = Folders::Id(), - .icon = { &st::menuIconShowInFolder }, - .keywords = { u"filters"_q, u"tabs"_q }, - }); + const auto showOther = builder.showOther(); + if (controller) { + const auto account = &controller->session().account(); + const auto preload = [=] { + controller->session().data().chatsFilters().requestSuggested(); + }; + const auto hasFilters = controller->session().data().chatsFilters().has() + || controller->session().settings().dialogsFiltersEnabled(); + + auto shownProducer = hasFilters + ? rpl::single(true) + : (rpl::single(rpl::empty) | rpl::then( + account->appConfig().refreshed() + ) | rpl::map([=] { + const auto enabled = account->appConfig().get( + u"dialog_filters_enabled"_q, + false); + if (enabled) { + preload(); + } + return enabled; + })); + + if (hasFilters) { + preload(); + } + + builder.addSlideButton({ + .id = u"main/folders"_q, + .title = tr::lng_settings_section_filters(), + .icon = { &st::menuIconShowInFolder }, + .shown = std::move(shownProducer), + .onClick = [=] { showOther(Folders::Id()); }, + .keywords = { u"filters"_q, u"tabs"_q }, + }); + } else { + builder.addSlideButton({ + .id = u"main/folders"_q, + .title = tr::lng_settings_section_filters(), + .icon = { &st::menuIconShowInFolder }, + .shown = rpl::single(true), + .keywords = { u"filters"_q, u"tabs"_q }, + }); + } builder.addSectionButton({ .id = u"main/advanced"_q, @@ -96,11 +149,15 @@ void BuildMainSectionContent( .title = tr::lng_settings_power_menu(), .icon = { &st::menuIconPowerUsage }, .onClick = window - ? [=] { window->show(Box(PowerSavingBox)); } + ? Fn([=] { window->show(Box(PowerSavingBox)); }) : Fn(nullptr), .keywords = { u"battery"_q, u"animations"_q, u"power"_q, u"saving"_q }, }); + const auto languageClick = controller ? [=] { + static auto Guard = base::binary_guard(); + Guard = LanguageBox::Show(controller); + } : Fn(); builder.addLabeledButton({ .id = u"main/language"_q, .title = tr::lng_settings_language(), @@ -110,25 +167,231 @@ void BuildMainSectionContent( ) | rpl::then( Lang::GetInstance().idChanges() ) | rpl::map([] { return Lang::GetInstance().nativeName(); }), - .onClick = controller - ? [=] { LanguageBox::Show(controller); } - : Fn(nullptr), - .keywords = { u"translate"_q, u"localization"_q }, + .onClick = languageClick, + .keywords = { u"translate"_q, u"localization"_q, u"language"_q }, + }); +} + +void BuildInterfaceScale( + SectionBuilder &builder, + Window::SessionController *controller) { + if (!HasInterfaceScale()) { + return; + } + + builder.addDivider(); + builder.addSkip(); + + const auto window = controller ? &controller->window() : nullptr; + builder.addControl({ + .factory = window ? [=](not_null container) { + auto wrap = object_ptr(container); + SetupInterfaceScale(window, wrap.data()); + return wrap; + } : Fn(not_null)>(nullptr), + .id = u"main/scale"_q, + .title = tr::lng_settings_default_scale(), + .keywords = { u"zoom"_q, u"size"_q, u"interface"_q, u"ui"_q }, }); builder.addSkip(); } +void BuildPremiumSection( + SectionBuilder &builder, + Window::SessionController *controller) { + if (controller && !controller->session().premiumPossible()) { + return; + } + + builder.addDivider(); + builder.addSkip(); + + const auto showOther = builder.showOther(); + + builder.addPremiumButton({ + .id = u"main/premium"_q, + .title = tr::lng_premium_summary_title(), + .onClick = controller ? Fn([=] { + controller->setPremiumRef("settings"); + showOther(PremiumId()); + }) : Fn(nullptr), + .keywords = { u"subscription"_q }, + }); + + if (controller) { + controller->session().credits().load(); + } + builder.addPremiumButton({ + .id = u"main/credits"_q, + .title = tr::lng_settings_credits(), + .label = controller + ? (controller->session().credits().balanceValue( + ) | rpl::map([](CreditsAmount c) { + return c + ? Lang::FormatCreditsAmountToShort(c).string + : QString(); + })) + : rpl::single(QString()), + .credits = true, + .onClick = controller ? Fn([=] { + controller->setPremiumRef("settings"); + showOther(CreditsId()); + }) : Fn(nullptr), + .keywords = { u"stars"_q, u"balance"_q }, + }); + + if (controller) { + controller->session().credits().tonLoad(); + } + builder.addSlideLabeledButton({ + .id = u"main/currency"_q, + .title = tr::lng_settings_currency(), + .icon = { &st::menuIconTon }, + .label = controller + ? (controller->session().credits().tonBalanceValue( + ) | rpl::map([](CreditsAmount c) { + return c + ? Lang::FormatCreditsAmountToShort(c).string + : QString(); + })) + : rpl::single(QString()), + .shown = controller + ? (controller->session().credits().tonBalanceValue( + ) | rpl::map([](CreditsAmount c) { return !c.empty(); })) + : rpl::single(false), + .onClick = controller ? Fn([=] { + controller->setPremiumRef("settings"); + showOther(CurrencyId()); + }) : Fn(nullptr), + .keywords = { u"ton"_q, u"crypto"_q, u"wallet"_q }, + }); + + builder.addSettingsButton({ + .id = u"main/business"_q, + .title = tr::lng_business_title(), + .icon = { .icon = &st::menuIconShop }, + .onClick = showOther + ? Fn([=] { showOther(BusinessId()); }) + : Fn(nullptr), + .keywords = { u"work"_q, u"company"_q }, + }); + + if (!controller || controller->session().premiumCanBuy()) { + builder.addSettingsButton({ + .id = u"main/gift"_q, + .title = tr::lng_settings_gift_premium(), + .icon = { .icon = &st::menuIconGiftPremium, .newBadge = true }, + .onClick = controller + ? Fn([=] { Ui::ChooseStarGiftRecipient(controller); }) + : Fn(nullptr), + .keywords = { u"present"_q, u"send"_q }, + }); + } + + builder.addSkip(); +} + +void BuildHelpSection( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addDivider(); + builder.addSkip(); + + const auto faqClick = controller + ? Fn([=] { + UrlClickHandler::Open( + tr::lng_settings_faq_link(tr::now), + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = base::make_weak(controller), + })); + }) + : Fn(nullptr); + builder.addSettingsButton({ + .id = u"main/faq"_q, + .title = tr::lng_settings_faq(), + .icon = { &st::menuIconFaq }, + .onClick = faqClick, + .keywords = { u"help"_q, u"support"_q, u"questions"_q }, + }); + + builder.addSettingsButton({ + .id = u"main/features"_q, + .title = tr::lng_settings_features(), + .icon = { &st::menuIconEmojiObjects }, + .onClick = [] { + UrlClickHandler::Open(tr::lng_telegram_features_url(tr::now)); + }, + .keywords = { u"tips"_q, u"tutorial"_q }, + }); + + builder.addSettingsButton({ + .id = u"main/ask"_q, + .title = tr::lng_settings_ask_question(), + .icon = { &st::menuIconDiscussion }, + .keywords = { u"contact"_q, u"feedback"_q }, + }); + + builder.addSkip(); +} + +void BuildValidationSuggestions( + SectionBuilder &builder, + Window::SessionController *controller) { + if (!controller) { + return; + } + + const auto showOther = builder.showOther(); + + builder.addControl({ + .factory = [=](not_null container) { + auto wrap = object_ptr(container); + SetupValidatePhoneNumberSuggestion(controller, wrap.data(), showOther); + return wrap; + }, + }); + + builder.addControl({ + .factory = [=](not_null container) { + auto wrap = object_ptr(container); + SetupValidatePasswordSuggestion(controller, wrap.data(), showOther); + return wrap; + }, + }); +} + +void BuildMainSectionContent( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addDivider(); + builder.addSkip(); + + BuildValidationSuggestions(builder, controller); + + BuildSectionButtons(builder, controller); + + builder.addSkip(); + + BuildInterfaceScale(builder, controller); + BuildPremiumSection(builder, controller); + BuildHelpSection(builder, controller); +} + } // namespace void BuildMainSection( not_null container, not_null controller, Fn showOther) { + const auto isPaused = Window::PausedIn( + controller, + Window::GifPauseReason::Layer); auto builder = SectionBuilder(WidgetContext{ .container = container, .controller = controller, .showOther = std::move(showOther), + .isPaused = isPaused, }); BuildMainSectionContent(builder, controller); } diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index dcb96bc0ae..e8a406f8ac 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -8,11 +8,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common.h" #include "lottie/lottie_icon.h" +#include "ui/effects/premium_graphics.h" +#include "ui/effects/premium_top_bar.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/widgets/buttons.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" +#include "styles/style_menu_icons.h" #include "styles/style_settings.h" #include @@ -354,4 +358,83 @@ void AddLottieIconWithCircle( }, circle->lifetime()); } +not_null AddPremiumStar( + not_null button, + bool credits, + Fn isPaused) { + const auto stops = credits + ? Ui::Premium::CreditsIconGradientStops() + : Ui::Premium::ButtonGradientStops(); + + const auto ministarsContainer = Ui::CreateChild(button); + const auto &buttonSt = button->st(); + const auto fullHeight = buttonSt.height + + rect::m::sum::v(buttonSt.padding); + using MiniStars = Ui::Premium::ColoredMiniStars; + const auto ministars = button->lifetime().make_state( + ministarsContainer, + false); + ministars->setColorOverride(stops); + + const auto isPausedValue + = button->lifetime().make_state>(isPaused()); + isPausedValue->value() | rpl::on_next([=](bool value) { + ministars->setPaused(value); + }, ministarsContainer->lifetime()); + + ministarsContainer->paintRequest( + ) | rpl::on_next([=] { + (*isPausedValue) = isPaused(); + auto p = QPainter(ministarsContainer); + { + constexpr auto kScale = 0.35; + const auto r = ministarsContainer->rect(); + p.translate(r.center()); + p.scale(kScale, kScale); + p.translate(-r.center()); + } + ministars->paint(p); + }, ministarsContainer->lifetime()); + + const auto badge = Ui::CreateChild(button.get()); + + auto star = [&] { + const auto factor = style::DevicePixelRatio(); + const auto size = Size(st::settingsButtonNoIcon.style.font->ascent); + auto image = QImage( + size * factor, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(factor); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto star = QSvgRenderer(Ui::Premium::ColorizedSvg(stops)); + star.render(&p, Rect(size)); + } + return image; + }(); + badge->resize(star.size() / style::DevicePixelRatio()); + badge->paintRequest( + ) | rpl::on_next([=] { + auto p = QPainter(badge); + p.drawImage(0, 0, star); + }, badge->lifetime()); + + button->sizeValue( + ) | rpl::on_next([=](const QSize &s) { + badge->moveToLeft( + button->st().iconLeft + + (st::menuIconShop.width() - badge->width()) / 2, + (s.height() - badge->height()) / 2); + ministarsContainer->moveToLeft( + badge->x() - (fullHeight - badge->height()) / 2, + 0); + }, badge->lifetime()); + + ministarsContainer->resize(fullHeight, fullHeight); + ministars->setCenter(ministarsContainer->rect()); + + return button; +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_common.h b/Telegram/SourceFiles/settings/settings_common.h index 7dbe555eb8..84f0a5125c 100644 --- a/Telegram/SourceFiles/settings/settings_common.h +++ b/Telegram/SourceFiles/settings/settings_common.h @@ -226,4 +226,9 @@ void AddLottieIconWithCircle( QMargins iconPadding, QSize circleSize); +[[nodiscard]] not_null AddPremiumStar( + not_null button, + bool credits, + Fn isPaused); + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 1b7f6c5ee7..f9c32ed1d2 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -8,77 +8,53 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_main.h" #include "settings/builder/settings_main_builder.h" -#include "api/api_credits.h" -#include "core/application.h" -#include "core/click_handler_types.h" -#include "settings/cloud_password/settings_cloud_password_input.h" -#include "settings/settings_advanced.h" -#include "settings/settings_business.h" -#include "settings/settings_calls.h" -#include "settings/settings_chat.h" -#include "settings/settings_codes.h" -#include "settings/settings_credits.h" -#include "settings/settings_folders.h" -#include "settings/settings_information.h" -#include "settings/settings_notifications.h" -#include "settings/settings_power_saving.h" -#include "settings/settings_premium.h" -#include "settings/settings_privacy_security.h" -#include "settings/settings_scale_preview.h" +#include "apiwrap.h" +#include "api/api_cloud_password.h" +#include "api/api_global_privacy.h" +#include "api/api_peer_photo.h" +#include "api/api_premium.h" +#include "api/api_sensitive_content.h" +#include "base/call_delayed.h" +#include "base/platform/base_platform_info.h" #include "boxes/language_box.h" #include "boxes/username_box.h" -#include "boxes/about_box.h" -#include "boxes/star_gift_box.h" -#include "ui/basic_click_handlers.h" -#include "ui/boxes/confirm_box.h" -#include "ui/controls/userpic_button.h" -#include "ui/effects/premium_graphics.h" -#include "ui/effects/premium_top_bar.h" // Ui::Premium::ColorizedSvg. -#include "ui/wrap/slide_wrap.h" -#include "ui/widgets/menu/menu_add_action_callback.h" -#include "ui/widgets/continuous_sliders.h" -#include "ui/widgets/popup_menu.h" -#include "ui/text/format_values.h" -#include "ui/text/text_utilities.h" -#include "ui/toast/toast.h" -#include "ui/new_badges.h" -#include "ui/rect.h" -#include "ui/vertical_list.h" -#include "info/channel_statistics/earn/earn_icons.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "data/components/promo_suggestions.h" +#include "data/data_cloud_themes.h" +#include "data/data_session.h" +#include "data/data_user.h" #include "info/profile/info_profile_badge.h" #include "info/profile/info_profile_emoji_status_panel.h" -#include "data/components/credits.h" -#include "data/components/promo_suggestions.h" -#include "data/data_user.h" -#include "data/data_session.h" -#include "data/data_cloud_themes.h" -#include "data/data_chat_filters.h" +#include "info/profile/info_profile_values.h" #include "lang/lang_cloud_manager.h" #include "lang/lang_instance.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" -#include "storage/localstorage.h" -#include "main/main_session.h" -#include "main/main_session_settings.h" -#include "main/main_account.h" #include "main/main_domain.h" -#include "main/main_app_config.h" -#include "apiwrap.h" -#include "api/api_peer_photo.h" -#include "api/api_cloud_password.h" -#include "api/api_global_privacy.h" -#include "api/api_sensitive_content.h" -#include "api/api_premium.h" -#include "info/profile/info_profile_values.h" +#include "main/main_session.h" +#include "settings/cloud_password/settings_cloud_password_input.h" +#include "settings/settings_codes.h" +#include "settings/settings_information.h" +#include "settings/settings_scale_preview.h" +#include "storage/localstorage.h" +#include "ui/basic_click_handlers.h" +#include "ui/boxes/confirm_box.h" +#include "ui/controls/userpic_button.h" +#include "ui/rect.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/vertical_list.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/widgets/popup_menu.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_controller.h" #include "window/window_session_controller.h" -#include "base/call_delayed.h" -#include "base/platform/base_platform_info.h" -#include "styles/style_chat.h" -#include "styles/style_settings.h" #include "styles/style_info.h" -#include "styles/style_layers.h" // boxLabel +#include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_settings.h" #include #include @@ -290,100 +266,8 @@ void Cover::refreshUsernameGeometry(int newWidth) { _username->moveToLeft(usernameLeft, usernameTop, newWidth); } -[[nodiscard]] not_null AddPremiumStar( - not_null button, - bool credits, - Fn isPaused) { - const auto stops = credits - ? Ui::Premium::CreditsIconGradientStops() - : Ui::Premium::ButtonGradientStops(); - - const auto ministarsContainer = Ui::CreateChild(button); - const auto &buttonSt = button->st(); - const auto fullHeight = buttonSt.height - + rect::m::sum::v(buttonSt.padding); - using MiniStars = Ui::Premium::ColoredMiniStars; - const auto ministars = button->lifetime().make_state( - ministarsContainer, - false); - ministars->setColorOverride(stops); - - const auto isPausedValue - = button->lifetime().make_state>(isPaused()); - isPausedValue->value() | rpl::on_next([=](bool value) { - ministars->setPaused(value); - }, ministarsContainer->lifetime()); - - ministarsContainer->paintRequest( - ) | rpl::on_next([=] { - (*isPausedValue) = isPaused(); - auto p = QPainter(ministarsContainer); - { - constexpr auto kScale = 0.35; - const auto r = ministarsContainer->rect(); - p.translate(r.center()); - p.scale(kScale, kScale); - p.translate(-r.center()); - } - ministars->paint(p); - }, ministarsContainer->lifetime()); - - const auto badge = Ui::CreateChild(button.get()); - - auto star = [&] { - const auto factor = style::DevicePixelRatio(); - const auto size = Size(st::settingsButtonNoIcon.style.font->ascent); - auto image = QImage( - size * factor, - QImage::Format_ARGB32_Premultiplied); - image.setDevicePixelRatio(factor); - image.fill(Qt::transparent); - { - auto p = QPainter(&image); - auto star = QSvgRenderer(Ui::Premium::ColorizedSvg(stops)); - star.render(&p, Rect(size)); - } - return image; - }(); - badge->resize(star.size() / style::DevicePixelRatio()); - badge->paintRequest( - ) | rpl::on_next([=] { - auto p = QPainter(badge); - p.drawImage(0, 0, star); - }, badge->lifetime()); - - button->sizeValue( - ) | rpl::on_next([=](const QSize &s) { - badge->moveToLeft( - button->st().iconLeft - + (st::menuIconShop.width() - badge->width()) / 2, - (s.height() - badge->height()) / 2); - ministarsContainer->moveToLeft( - badge->x() - (fullHeight - badge->height()) / 2, - 0); - }, badge->lifetime()); - - ministarsContainer->resize(fullHeight, fullHeight); - ministars->setCenter(ministarsContainer->rect()); - - return button; -} - } // namespace -void SetupPowerSavingButton( - not_null window, - not_null container) { - const auto button = AddButtonWithIcon( - container, - tr::lng_settings_power_menu(), - st::settingsButton, - { &st::menuIconPowerUsage }); - button->setClickedCallback([=] { - window->show(Box(PowerSavingBox)); - }); -} - void SetupLanguageButton( not_null window, not_null container) { @@ -596,214 +480,6 @@ void SetupValidatePasswordSuggestion( Ui::AddSkip(content); } -void SetupSections( - not_null controller, - not_null container, - Fn showOther) { - Ui::AddDivider(container); - Ui::AddSkip(container); - - SetupValidatePhoneNumberSuggestion( - controller, - container, - showOther); - SetupValidatePasswordSuggestion( - controller, - container, - showOther); - - const auto addSection = [&]( - rpl::producer label, - Type type, - IconDescriptor &&descriptor) { - AddButtonWithIcon( - container, - std::move(label), - st::settingsButton, - std::move(descriptor) - )->addClickHandler([=] { - showOther(type); - }); - }; - if (controller->session().supportMode()) { - SetupSupport(controller, container); - - Ui::AddDivider(container); - Ui::AddSkip(container); - } else { - addSection( - tr::lng_settings_my_account(), - Information::Id(), - { &st::menuIconProfile }); - } - - addSection( - tr::lng_settings_section_notify(), - Notifications::Id(), - { &st::menuIconNotifications }); - addSection( - tr::lng_settings_section_privacy(), - PrivacySecurity::Id(), - { &st::menuIconLock }); - addSection( - tr::lng_settings_section_chat_settings(), - Chat::Id(), - { &st::menuIconChatBubble }); - - const auto preload = [=] { - controller->session().data().chatsFilters().requestSuggested(); - }; - const auto account = &controller->session().account(); - const auto slided = container->add( - object_ptr>( - container, - CreateButtonWithIcon( - container, - tr::lng_settings_section_filters(), - st::settingsButton, - { &st::menuIconShowInFolder })) - )->setDuration(0); - if (controller->session().data().chatsFilters().has() - || controller->session().settings().dialogsFiltersEnabled()) { - slided->show(anim::type::instant); - preload(); - } else { - const auto enabled = [=] { - const auto result = account->appConfig().get( - u"dialog_filters_enabled"_q, - false); - if (result) { - preload(); - } - return result; - }; - const auto preloadIfEnabled = [=](bool enabled) { - if (enabled) { - preload(); - } - }; - slided->toggleOn( - rpl::single(rpl::empty) | rpl::then( - account->appConfig().refreshed() - ) | rpl::map( - enabled - ) | rpl::before_next(preloadIfEnabled)); - } - slided->entity()->setClickedCallback([=] { - showOther(Folders::Id()); - }); - - addSection( - tr::lng_settings_advanced(), - Advanced::Id(), - { &st::menuIconManage }); - addSection( - tr::lng_settings_section_devices(), - Calls::Id(), - { &st::menuIconUnmute }); - - SetupPowerSavingButton(&controller->window(), container); - SetupLanguageButton(&controller->window(), container); - - Ui::AddSkip(container); -} - -void SetupPremium( - not_null controller, - not_null container, - Fn showOther) { - if (!controller->session().premiumPossible()) { - return; - } - Ui::AddDivider(container); - Ui::AddSkip(container); - - const auto isPaused = Window::PausedIn( - controller, - Window::GifPauseReason::Any); - - AddPremiumStar( - AddButtonWithIcon( - container, - tr::lng_premium_summary_title(), - st::settingsButton), - false, - isPaused - )->addClickHandler([=] { - controller->setPremiumRef("settings"); - showOther(PremiumId()); - }); - { - controller->session().credits().load(); - AddPremiumStar( - AddButtonWithLabel( - container, - tr::lng_settings_credits(), - controller->session().credits().balanceValue( - ) | rpl::map([=](CreditsAmount c) { - return c - ? Lang::FormatCreditsAmountToShort(c).string - : QString(); - }), - st::settingsButton), - true, - isPaused - )->addClickHandler([=] { - controller->setPremiumRef("settings"); - showOther(CreditsId()); - }); - } - { - const auto wrap = container->add( - object_ptr>( - container, - object_ptr(container))); - wrap->toggleOn( - controller->session().credits().tonBalanceValue( - ) | rpl::map([](CreditsAmount c) -> bool { return !c.empty(); })); - wrap->finishAnimating(); - controller->session().credits().tonLoad(); - const auto button = AddButtonWithLabel( - wrap->entity(), - tr::lng_settings_currency(), - controller->session().credits().tonBalanceValue( - ) | rpl::map([=](CreditsAmount c) { - return c - ? Lang::FormatCreditsAmountToShort(c).string - : QString(); - }), - st::settingsButton, - { &st::menuIconTon }); - button->addClickHandler([=] { - controller->setPremiumRef("settings"); - showOther(CurrencyId()); - }); - } - const auto button = AddButtonWithIcon( - container, - tr::lng_business_title(), - st::settingsButton, - { .icon = &st::menuIconShop }); - button->addClickHandler([=] { - showOther(BusinessId()); - }); - - if (controller->session().premiumCanBuy()) { - const auto button = AddButtonWithIcon( - container, - tr::lng_settings_gift_premium(), - st::settingsButton, - { .icon = &st::menuIconGiftPremium } - ); - Ui::NewBadge::AddToRight(button); - - button->addClickHandler([=] { - Ui::ChooseStarGiftRecipient(controller); - }); - } - Ui::AddSkip(container); -} - bool HasInterfaceScale() { return true; } @@ -961,75 +637,6 @@ void SetupInterfaceScale( } } -void SetupHelp( - not_null controller, - not_null container) { - Ui::AddDivider(container); - Ui::AddSkip(container); - - AddButtonWithIcon( - container, - tr::lng_settings_faq(), - st::settingsButton, - { &st::menuIconFaq } - )->addClickHandler([=] { - OpenFaq(controller); - }); - - AddButtonWithIcon( - container, - tr::lng_settings_features(), - st::settingsButton, - { &st::menuIconEmojiObjects } - )->setClickedCallback([=] { - UrlClickHandler::Open(tr::lng_telegram_features_url(tr::now)); - }); - - const auto button = AddButtonWithIcon( - container, - tr::lng_settings_ask_question(), - st::settingsButton, - { &st::menuIconDiscussion }); - const auto requestId = button->lifetime().make_state(); - button->lifetime().add([=] { - if (*requestId) { - controller->session().api().request(*requestId).cancel(); - } - }); - button->addClickHandler([=] { - const auto sure = crl::guard(button, [=] { - if (*requestId) { - return; - } - *requestId = controller->session().api().request( - MTPhelp_GetSupport() - ).done([=](const MTPhelp_Support &result) { - *requestId = 0; - result.match([&](const MTPDhelp_support &data) { - auto &owner = controller->session().data(); - if (const auto user = owner.processUser(data.vuser())) { - controller->showPeerHistory(user); - } - }); - }).fail([=] { - *requestId = 0; - }).send(); - }); - auto box = Ui::MakeConfirmBox({ - .text = tr::lng_settings_ask_sure(), - .confirmed = sure, - .cancelled = [=](Fn close) { - OpenFaq(controller); - close(); - }, - .confirmText = tr::lng_settings_ask_ok(), - .cancelText = tr::lng_settings_faq_button(), - .strictCancel = true, - }); - controller->show(std::move(box)); - }); -} - Main::Main( QWidget *parent, not_null controller) @@ -1072,34 +679,6 @@ void Main::keyPressEvent(QKeyEvent *e) { return Section::keyPressEvent(e); } -void Main::setupContent(not_null controller) { - const auto content = Ui::CreateChild(this); - - content->add(object_ptr( - content, - controller, - controller->session().user())); - - SetupSections(controller, content, showOtherMethod()); - if (HasInterfaceScale()) { - Ui::AddDivider(content); - Ui::AddSkip(content); - SetupInterfaceScale(&controller->window(), content); - Ui::AddSkip(content); - } - SetupPremium(controller, content, showOtherMethod()); - SetupHelp(controller, content); - - Ui::ResizeFitChild(this, content); - - // If we load this in advance it won't jump when we open its' section. - controller->session().api().cloudPassword().reload(); - controller->session().api().reloadContactSignupSilent(); - controller->session().api().sensitiveContent().reload(); - controller->session().api().globalPrivacy().reload(); - controller->session().data().cloudThemes().refresh(); -} - void Main::setupContentWithBuilder( not_null controller) { const auto content = Ui::CreateChild(this); @@ -1111,15 +690,6 @@ void Main::setupContentWithBuilder( Builder::BuildMainSection(content, controller, showOtherMethod()); - if (HasInterfaceScale()) { - Ui::AddDivider(content); - Ui::AddSkip(content); - SetupInterfaceScale(&controller->window(), content); - Ui::AddSkip(content); - } - SetupPremium(controller, content, showOtherMethod()); - SetupHelp(controller, content); - Ui::ResizeFitChild(this, content); controller->session().api().cloudPassword().reload(); diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index 9da6d278c3..4037af4cb7 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -29,6 +29,15 @@ void SetupInterfaceScale( not_null container, bool icon = true); +void SetupValidatePhoneNumberSuggestion( + not_null controller, + not_null container, + Fn showOther); +void SetupValidatePasswordSuggestion( + not_null controller, + not_null container, + Fn showOther); + void OpenFaq(base::weak_ptr weak); class Main : public Section
{ @@ -44,7 +53,6 @@ protected: void keyPressEvent(QKeyEvent *e) override; private: - void setupContent(not_null controller); void setupContentWithBuilder(not_null controller); const not_null _controller; From 0c5c5d3c42f931f6e387dc03e599433920e045cf Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 2 Jan 2026 17:19:03 +0400 Subject: [PATCH 063/348] Migrate all notification settings to builder. --- Telegram/CMakeLists.txt | 2 + .../settings/builder/settings_builder.cpp | 69 ++ .../settings/builder/settings_builder.h | 23 + .../settings_notifications_builder.cpp | 770 ++++++++++++++++++ .../builder/settings_notifications_builder.h | 29 + .../settings/settings_notifications.cpp | 654 +-------------- .../settings/settings_notifications.h | 4 + .../settings/settings_notifications_common.h | 110 +++ 8 files changed, 1029 insertions(+), 632 deletions(-) create mode 100644 Telegram/SourceFiles/settings/builder/settings_notifications_builder.cpp create mode 100644 Telegram/SourceFiles/settings/builder/settings_notifications_builder.h create mode 100644 Telegram/SourceFiles/settings/settings_notifications_common.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5ac4f287e4..99073446fc 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1571,6 +1571,8 @@ PRIVATE settings/builder/settings_builder.h settings/builder/settings_main_builder.cpp settings/builder/settings_main_builder.h + settings/builder/settings_notifications_builder.cpp + settings/builder/settings_notifications_builder.h settings/settings_notifications.cpp settings/settings_notifications.h settings/settings_notifications_type.cpp diff --git a/Telegram/SourceFiles/settings/builder/settings_builder.cpp b/Telegram/SourceFiles/settings/builder/settings_builder.cpp index 3290e56e6c..108f2ea49d 100644 --- a/Telegram/SourceFiles/settings/builder/settings_builder.cpp +++ b/Telegram/SourceFiles/settings/builder/settings_builder.cpp @@ -250,6 +250,75 @@ Ui::SettingsButton *SectionBuilder::addPremiumButton(PremiumButtonArgs &&args) { }); } +Ui::SettingsButton *SectionBuilder::addToggle(ToggleArgs &&args) { + return v::match(_context, [&](const WidgetContext &ctx) + -> Ui::SettingsButton* { + const auto &st = args.st ? *args.st : st::settingsButton; + const auto button = ctx.container->add(CreateButtonWithIcon( + ctx.container, + rpl::duplicate(args.title), + st, + std::move(args.icon))); + button->toggleOn(std::move(args.toggled)); + return button; + }, [&](const SearchContext &ctx) -> Ui::SettingsButton* { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return nullptr; + }); +} + +Ui::SlideWrap *SectionBuilder::addSlideToggle( + SlideToggleArgs &&args) { + return v::match(_context, [&](const WidgetContext &ctx) + -> Ui::SlideWrap* { + const auto &st = args.st ? *args.st : st::settingsButton; + const auto wrap = ctx.container->add( + object_ptr>( + ctx.container, + CreateButtonWithIcon( + ctx.container, + rpl::duplicate(args.title), + st, + std::move(args.icon)))); + if (args.shown) { + wrap->toggleOn(std::move(args.shown)); + } + const auto button = wrap->entity(); + button->toggleOn(std::move(args.toggled)); + return wrap; + }, [&](const SearchContext &ctx) -> Ui::SlideWrap* { + if (!args.id.isEmpty()) { + ctx.entries->push_back({ + .id = std::move(args.id), + .title = ResolveTitle(std::move(args.title)), + .keywords = std::move(args.keywords), + }); + } + return nullptr; + }); +} + +void SectionBuilder::addSubsectionTitle(rpl::producer text) { + v::match(_context, [&](const WidgetContext &ctx) { + AddSubsectionTitle(ctx.container, std::move(text)); + }, [](const SearchContext &) { + }); +} + +Ui::VerticalLayout *SectionBuilder::container() const { + return v::match(_context, [](const WidgetContext &ctx) { + return ctx.container.get(); + }, [](const SearchContext &) -> Ui::VerticalLayout* { + return nullptr; + }); +} + Window::SessionController *SectionBuilder::controller() const { return v::match(_context, [](const WidgetContext &ctx) { return ctx.controller.get(); diff --git a/Telegram/SourceFiles/settings/builder/settings_builder.h b/Telegram/SourceFiles/settings/builder/settings_builder.h index 965ca900b9..2e2b7071a1 100644 --- a/Telegram/SourceFiles/settings/builder/settings_builder.h +++ b/Telegram/SourceFiles/settings/builder/settings_builder.h @@ -121,11 +121,34 @@ public: }; Ui::SettingsButton *addPremiumButton(PremiumButtonArgs &&args); + struct ToggleArgs { + QString id; + rpl::producer title; + const style::SettingsButton *st = nullptr; + IconDescriptor icon; + rpl::producer toggled; + QStringList keywords; + }; + Ui::SettingsButton *addToggle(ToggleArgs &&args); + + struct SlideToggleArgs { + QString id; + rpl::producer title; + const style::SettingsButton *st = nullptr; + IconDescriptor icon; + rpl::producer toggled; + rpl::producer shown; + QStringList keywords; + }; + Ui::SlideWrap *addSlideToggle(SlideToggleArgs &&args); + + void addSubsectionTitle(rpl::producer text); void addDivider(); void addDividerText(rpl::producer text); void addSkip(); void addSkip(int height); + [[nodiscard]] Ui::VerticalLayout *container() const; [[nodiscard]] Window::SessionController *controller() const; [[nodiscard]] Fn showOther() const; diff --git a/Telegram/SourceFiles/settings/builder/settings_notifications_builder.cpp b/Telegram/SourceFiles/settings/builder/settings_notifications_builder.cpp new file mode 100644 index 0000000000..9eba3937bf --- /dev/null +++ b/Telegram/SourceFiles/settings/builder/settings_notifications_builder.cpp @@ -0,0 +1,770 @@ +/* +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/builder/settings_notifications_builder.h" + +#include "api/api_authorizations.h" +#include "api/api_ringtones.h" +#include "apiwrap.h" +#include "base/platform/base_platform_info.h" +#include "boxes/ringtones_box.h" +#include "core/application.h" +#include "data/data_chat_filters.h" +#include "data/data_session.h" +#include "data/notify/data_notify_settings.h" +#include "data/notify/data_peer_notify_volume.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_domain.h" +#include "main/main_session.h" +#include "mainwindow.h" +#include "platform/platform_notifications_manager.h" +#include "platform/platform_specific.h" +#include "settings/builder/settings_builder.h" +#include "settings/settings_notifications_common.h" +#include "settings/settings_notifications_type.h" +#include "ui/vertical_list.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/continuous_sliders.h" +#include "ui/widgets/discrete_sliders.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/notifications_manager.h" +#include "window/window_session_controller.h" +#include "styles/style_settings.h" +#include "styles/style_menu_icons.h" + +#include +#include + +namespace Settings::Builder { +namespace { + +constexpr auto kDefaultDisplayIndex = -1; + +using NotifyView = Core::Settings::NotifyView; +using ChangeType = Window::Notifications::ChangeType; + +void BuildMultiAccountSection( + SectionBuilder &builder, + Window::SessionController *controller) { + if (Core::App().domain().accounts().size() < 2) { + return; + } + + builder.addSubsectionTitle(tr::lng_settings_show_from()); + + const auto container = builder.container(); + if (!container) { + builder.addToggle({ + .id = u"notifications/multi_account"_q, + .title = tr::lng_settings_notify_all(), + .toggled = rpl::single(Core::App().settings().notifyFromAll()), + .keywords = { u"all accounts"_q, u"multiple"_q }, + }); + builder.addSkip(); + builder.addDividerText(tr::lng_settings_notify_all_about()); + builder.addSkip(); + return; + } + + const auto fromAll = builder.addToggle({ + .id = u"notifications/multi_account"_q, + .title = tr::lng_settings_notify_all(), + .st = &st::settingsButtonNoIcon, + .toggled = rpl::single(Core::App().settings().notifyFromAll()), + .keywords = { u"all accounts"_q, u"multiple"_q }, + }); + + if (fromAll) { + fromAll->toggledChanges( + ) | rpl::filter([](bool checked) { + return (checked != Core::App().settings().notifyFromAll()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setNotifyFromAll(checked); + Core::App().saveSettingsDelayed(); + if (!checked) { + auto ¬ifications = Core::App().notifications(); + const auto &list = Core::App().domain().accounts(); + for (const auto &[index, account] : list) { + if (account.get() == &Core::App().domain().active()) { + continue; + } else if (const auto session = account->maybeSession()) { + notifications.clearFromSession(session); + } + } + } + }, fromAll->lifetime()); + } + + builder.addSkip(); + builder.addDividerText(tr::lng_settings_notify_all_about()); + builder.addSkip(); +} + +void BuildGlobalNotificationsSection( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addSubsectionTitle(tr::lng_settings_notify_global()); + + const auto container = builder.container(); + const auto &settings = Core::App().settings(); + + const auto desktopToggles = container + ? container->lifetime().make_state>() + : nullptr; + const auto desktop = builder.addToggle({ + .id = u"notifications/desktop"_q, + .title = tr::lng_settings_desktop_notify(), + .icon = { &st::menuIconNotifications }, + .toggled = desktopToggles + ? desktopToggles->events_starting_with(settings.desktopNotify()) + : rpl::single(settings.desktopNotify()), + .keywords = { u"desktop"_q, u"popup"_q, u"show"_q }, + }); + + const auto flashbounceToggles = container + ? container->lifetime().make_state>() + : nullptr; + const auto flashbounce = builder.addToggle({ + .id = u"notifications/flash"_q, + .title = (Platform::IsWindows() + ? tr::lng_settings_alert_windows + : Platform::IsMac() + ? tr::lng_settings_alert_mac + : tr::lng_settings_alert_linux)(), + .icon = { &st::menuIconDockBounce }, + .toggled = flashbounceToggles + ? flashbounceToggles->events_starting_with(settings.flashBounceNotify()) + : rpl::single(settings.flashBounceNotify()), + .keywords = { u"flash"_q, u"bounce"_q, u"taskbar"_q }, + }); + + const auto soundAllowed = container + ? container->lifetime().make_state>() + : nullptr; + const auto allowed = [=] { + return Core::App().settings().soundNotify(); + }; + const auto sound = builder.addToggle({ + .id = u"notifications/sound"_q, + .title = tr::lng_settings_sound_allowed(), + .icon = { &st::menuIconUnmute }, + .toggled = soundAllowed + ? soundAllowed->events_starting_with(allowed()) + : rpl::single(allowed()), + .keywords = { u"sound"_q, u"audio"_q, u"mute"_q }, + }); + + if (container && controller) { + const auto session = &controller->session(); + Ui::AddRingtonesVolumeSlider( + container, + rpl::single(true), + tr::lng_settings_master_volume_notifications(), + Data::VolumeController{ + .volume = []() -> ushort { + const auto volume + = Core::App().settings().notificationsVolume(); + return volume ? volume : 100; + }, + .saveVolume = [=](ushort volume) { + Core::App().notifications().playSound( + session, + 0, + volume / 100.); + Core::App().settings().setNotificationsVolume(volume); + Core::App().saveSettingsDelayed(); + }}); + } + + builder.addSkip(); + + if (desktop) { + const auto changed = [=](ChangeType change) { + Core::App().saveSettingsDelayed(); + Core::App().notifications().notifySettingsChanged(change); + }; + + desktop->toggledChanges( + ) | rpl::filter([](bool checked) { + return (checked != Core::App().settings().desktopNotify()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setDesktopNotify(checked); + changed(ChangeType::DesktopEnabled); + }, desktop->lifetime()); + + if (sound) { + sound->toggledChanges( + ) | rpl::filter([](bool checked) { + return (checked != Core::App().settings().soundNotify()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setSoundNotify(checked); + changed(ChangeType::SoundEnabled); + }, sound->lifetime()); + } + + if (flashbounce) { + flashbounce->toggledChanges( + ) | rpl::filter([](bool checked) { + return (checked != Core::App().settings().flashBounceNotify()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setFlashBounceNotify(checked); + changed(ChangeType::FlashBounceEnabled); + }, flashbounce->lifetime()); + } + + Core::App().notifications().settingsChanged( + ) | rpl::on_next([=](ChangeType change) { + if (change == ChangeType::DesktopEnabled) { + desktopToggles->fire(Core::App().settings().desktopNotify()); + } else if (change == ChangeType::SoundEnabled) { + soundAllowed->fire(allowed()); + } else if (change == ChangeType::FlashBounceEnabled) { + flashbounceToggles->fire( + Core::App().settings().flashBounceNotify()); + } + }, desktop->lifetime()); + } +} + +void BuildNotifyViewSection( + SectionBuilder &builder, + Window::SessionController *controller, + rpl::lifetime &lifetime) { + const auto container = builder.container(); + if (!container || !controller) { + return; + } + + const auto &settings = Core::App().settings(); + const auto checkboxes = SetupNotifyViewOptions( + controller, + container, + (settings.notifyView() <= NotifyView::ShowName), + (settings.notifyView() <= NotifyView::ShowPreview)); + const auto name = checkboxes.name; + const auto preview = checkboxes.preview; + const auto previewWrap = checkboxes.wrap; + + const auto previewDivider = container->add( + object_ptr>( + container, + object_ptr(container))); + previewWrap->toggle(settings.desktopNotify(), anim::type::instant); + previewDivider->toggle(!settings.desktopNotify(), anim::type::instant); + + const auto changed = [=](ChangeType change) { + Core::App().saveSettingsDelayed(); + Core::App().notifications().notifySettingsChanged(change); + }; + + name->checkedChanges( + ) | rpl::map([=](bool checked) { + if (!checked) { + preview->setChecked(false); + return NotifyView::ShowNothing; + } else if (!preview->checked()) { + return NotifyView::ShowName; + } + return NotifyView::ShowPreview; + }) | rpl::filter([=](NotifyView value) { + return (value != Core::App().settings().notifyView()); + }) | rpl::on_next([=](NotifyView value) { + Core::App().settings().setNotifyView(value); + changed(ChangeType::ViewParams); + }, name->lifetime()); + + preview->checkedChanges( + ) | rpl::map([=](bool checked) { + if (checked) { + name->setChecked(true); + return NotifyView::ShowPreview; + } else if (name->checked()) { + return NotifyView::ShowName; + } + return NotifyView::ShowNothing; + }) | rpl::filter([=](NotifyView value) { + return (value != Core::App().settings().notifyView()); + }) | rpl::on_next([=](NotifyView value) { + Core::App().settings().setNotifyView(value); + changed(ChangeType::ViewParams); + }, preview->lifetime()); + + Core::App().notifications().settingsChanged( + ) | rpl::on_next([=](ChangeType change) { + if (change == ChangeType::DesktopEnabled) { + previewWrap->toggle( + Core::App().settings().desktopNotify(), + anim::type::normal); + previewDivider->toggle( + !Core::App().settings().desktopNotify(), + anim::type::normal); + } + }, lifetime); +} + +void BuildNotifyTypeSection( + SectionBuilder &builder, + Window::SessionController *controller) { + const auto container = builder.container(); + const auto showOther = builder.showOther(); + + builder.addSkip(st::notifyPreviewBottomSkip); + builder.addSubsectionTitle(tr::lng_settings_notify_title()); + + if (container && controller && showOther) { + controller->session().data().notifySettings().loadExceptions(); + AddTypeButton(container, controller, Data::DefaultNotify::User, showOther); + AddTypeButton(container, controller, Data::DefaultNotify::Group, showOther); + AddTypeButton(container, controller, Data::DefaultNotify::Broadcast, showOther); + } else { + builder.addSettingsButton({ + .id = u"notifications/private"_q, + .title = tr::lng_notification_private_chats(), + .icon = { &st::menuIconProfile }, + .keywords = { u"private"_q, u"chats"_q, u"direct"_q }, + }); + builder.addSettingsButton({ + .id = u"notifications/groups"_q, + .title = tr::lng_notification_groups(), + .icon = { &st::menuIconGroups }, + .keywords = { u"groups"_q, u"chats"_q }, + }); + builder.addSettingsButton({ + .id = u"notifications/channels"_q, + .title = tr::lng_notification_channels(), + .icon = { &st::menuIconChannel }, + .keywords = { u"channels"_q, u"broadcast"_q }, + }); + } +} + +void BuildEventNotificationsSection( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addSkip(st::settingsCheckboxesSkip); + builder.addDivider(); + builder.addSkip(st::settingsCheckboxesSkip); + builder.addSubsectionTitle(tr::lng_settings_events_title()); + + if (!controller) { + builder.addToggle({ + .id = u"notifications/events/joined"_q, + .title = tr::lng_settings_events_joined(), + .icon = { &st::menuIconInvite }, + .toggled = rpl::single(false), + .keywords = { u"joined"_q, u"contacts"_q, u"signup"_q }, + }); + builder.addToggle({ + .id = u"notifications/events/pinned"_q, + .title = tr::lng_settings_events_pinned(), + .icon = { &st::menuIconPin }, + .toggled = rpl::single(Core::App().settings().notifyAboutPinned()), + .keywords = { u"pinned"_q, u"message"_q }, + }); + return; + } + + const auto session = &controller->session(); + const auto &settings = Core::App().settings(); + + auto joinSilent = rpl::single( + session->api().contactSignupSilentCurrent().value_or(false) + ) | rpl::then(session->api().contactSignupSilent()); + + const auto joined = builder.addToggle({ + .id = u"notifications/events/joined"_q, + .title = tr::lng_settings_events_joined(), + .icon = { &st::menuIconInvite }, + .toggled = std::move(joinSilent) | rpl::map([](bool s) { return !s; }), + .keywords = { u"joined"_q, u"contacts"_q, u"signup"_q }, + }); + if (joined) { + joined->toggledChanges( + ) | rpl::filter([=](bool enabled) { + const auto silent = session->api().contactSignupSilentCurrent(); + return (enabled == silent.value_or(false)); + }) | rpl::on_next([=](bool enabled) { + session->api().saveContactSignupSilent(!enabled); + }, joined->lifetime()); + } + + const auto pinned = builder.addToggle({ + .id = u"notifications/events/pinned"_q, + .title = tr::lng_settings_events_pinned(), + .icon = { &st::menuIconPin }, + .toggled = rpl::single( + settings.notifyAboutPinned() + ) | rpl::then(settings.notifyAboutPinnedChanges()), + .keywords = { u"pinned"_q, u"message"_q }, + }); + if (pinned) { + pinned->toggledChanges( + ) | rpl::filter([=](bool notify) { + return (notify != Core::App().settings().notifyAboutPinned()); + }) | rpl::on_next([=](bool notify) { + Core::App().settings().setNotifyAboutPinned(notify); + Core::App().saveSettingsDelayed(); + }, pinned->lifetime()); + } +} + +void BuildCallNotificationsSection( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addSkip(st::settingsCheckboxesSkip); + builder.addDivider(); + builder.addSkip(st::settingsCheckboxesSkip); + builder.addSubsectionTitle(tr::lng_settings_notifications_calls_title()); + + if (!controller) { + builder.addToggle({ + .id = u"notifications/calls/accept"_q, + .title = tr::lng_settings_call_accept_calls(), + .icon = { &st::menuIconCallsReceive }, + .toggled = rpl::single(true), + .keywords = { u"calls"_q, u"receive"_q, u"incoming"_q }, + }); + return; + } + + const auto session = &controller->session(); + const auto authorizations = &session->api().authorizations(); + authorizations->reload(); + + const auto acceptCalls = builder.addToggle({ + .id = u"notifications/calls/accept"_q, + .title = tr::lng_settings_call_accept_calls(), + .icon = { &st::menuIconCallsReceive }, + .toggled = authorizations->callsDisabledHereValue() + | rpl::map([](bool disabled) { return !disabled; }), + .keywords = { u"calls"_q, u"receive"_q, u"incoming"_q }, + }); + if (acceptCalls) { + const auto container = builder.container(); + acceptCalls->toggledChanges( + ) | rpl::filter([=](bool toggled) { + return (toggled == authorizations->callsDisabledHere()); + }) | rpl::on_next([=](bool toggled) { + authorizations->toggleCallsDisabledHere(!toggled); + }, container->lifetime()); + } +} + +void BuildBadgeCounterSection( + SectionBuilder &builder, + Window::SessionController *controller) { + builder.addSkip(st::settingsCheckboxesSkip); + builder.addDivider(); + builder.addSkip(st::settingsCheckboxesSkip); + builder.addSubsectionTitle(tr::lng_settings_badge_title()); + + const auto &settings = Core::App().settings(); + + const auto muted = builder.addToggle({ + .id = u"notifications/badge/muted"_q, + .title = tr::lng_settings_include_muted(), + .st = &st::settingsButtonNoIcon, + .toggled = rpl::single(settings.includeMutedCounter()), + .keywords = { u"muted"_q, u"badge"_q, u"counter"_q }, + }); + + const auto hasFolders = controller + && controller->session().data().chatsFilters().has(); + const auto mutedFolders = hasFolders ? builder.addToggle({ + .id = u"notifications/badge/muted_folders"_q, + .title = tr::lng_settings_include_muted_folders(), + .st = &st::settingsButtonNoIcon, + .toggled = rpl::single(settings.includeMutedCounterFolders()), + .keywords = { u"muted"_q, u"folders"_q }, + }) : nullptr; + + const auto count = builder.addToggle({ + .id = u"notifications/badge/count"_q, + .title = tr::lng_settings_count_unread(), + .st = &st::settingsButtonNoIcon, + .toggled = rpl::single(settings.countUnreadMessages()), + .keywords = { u"unread"_q, u"messages"_q, u"count"_q }, + }); + + const auto changed = [=](ChangeType change) { + Core::App().saveSettingsDelayed(); + Core::App().notifications().notifySettingsChanged(change); + }; + + if (muted) { + muted->toggledChanges( + ) | rpl::filter([=](bool checked) { + return (checked != Core::App().settings().includeMutedCounter()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setIncludeMutedCounter(checked); + changed(ChangeType::IncludeMuted); + }, muted->lifetime()); + } + + if (mutedFolders) { + mutedFolders->toggledChanges( + ) | rpl::filter([=](bool checked) { + return (checked + != Core::App().settings().includeMutedCounterFolders()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setIncludeMutedCounterFolders(checked); + changed(ChangeType::IncludeMuted); + }, mutedFolders->lifetime()); + } + + if (count) { + count->toggledChanges( + ) | rpl::filter([=](bool checked) { + return (checked != Core::App().settings().countUnreadMessages()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setCountUnreadMessages(checked); + changed(ChangeType::CountMessages); + }, count->lifetime()); + } +} + +void BuildSystemIntegrationAndAdvancedSection( + SectionBuilder &builder, + Window::SessionController *controller) { + const auto container = builder.container(); + + auto nativeText = [&]() -> rpl::producer { + if (!Platform::Notifications::Supported() + || Core::App().notifications().nativeEnforced()) { + return rpl::producer(); + } else if (Platform::IsWindows()) { + return tr::lng_settings_use_windows(); + } + return tr::lng_settings_use_native_notifications(); + }(); + + if (nativeText) { + builder.addSkip(st::settingsCheckboxesSkip); + builder.addDivider(); + builder.addSkip(st::settingsCheckboxesSkip); + builder.addSubsectionTitle(tr::lng_settings_native_title()); + } + + const auto &settings = Core::App().settings(); + const auto native = nativeText ? builder.addToggle({ + .id = u"notifications/native"_q, + .title = std::move(nativeText), + .st = &st::settingsButtonNoIcon, + .toggled = rpl::single(settings.nativeNotifications()), + .keywords = { u"native"_q, u"system"_q, u"windows"_q }, + }) : nullptr; + + if (Core::App().notifications().nativeEnforced()) { + return; + } + if (!container || !controller) { + return; + } + + const auto advancedSlide = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto advancedWrap = advancedSlide->entity(); + + if (native) { + native->toggledChanges( + ) | rpl::filter([](bool checked) { + return (checked != Core::App().settings().nativeNotifications()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setNativeNotifications(checked); + Core::App().saveSettingsDelayed(); + Core::App().notifications().createManager(); + advancedSlide->toggle(!checked, anim::type::normal); + }, native->lifetime()); + } + + if (Platform::IsWindows()) { + const auto skipInFocus = advancedWrap->add(object_ptr( + advancedWrap, + tr::lng_settings_skip_in_focus(), + st::settingsButtonNoIcon + ))->toggleOn(rpl::single(Core::App().settings().skipToastsInFocus())); + + skipInFocus->toggledChanges( + ) | rpl::filter([](bool checked) { + return (checked != Core::App().settings().skipToastsInFocus()); + }) | rpl::on_next([=](bool checked) { + Core::App().settings().setSkipToastsInFocus(checked); + Core::App().saveSettingsDelayed(); + if (checked && Platform::Notifications::SkipToastForCustom()) { + Core::App().notifications().notifySettingsChanged( + ChangeType::DesktopEnabled); + } + }, skipInFocus->lifetime()); + } + + const auto screens = QGuiApplication::screens(); + if (screens.size() > 1) { + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + Ui::AddDivider(advancedWrap); + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + Ui::AddSubsectionTitle( + advancedWrap, + tr::lng_settings_notifications_display()); + + const auto currentChecksum + = Core::App().settings().notificationsDisplayChecksum(); + auto currentIndex = (currentChecksum == 0) + ? kDefaultDisplayIndex + : 0; + for (auto i = 0; i < screens.size(); ++i) { + if (Platform::ScreenNameChecksum(screens[i]) == currentChecksum) { + currentIndex = i; + break; + } + } + + const auto group = std::make_shared( + currentIndex); + + advancedWrap->add( + object_ptr( + advancedWrap, + group, + kDefaultDisplayIndex, + tr::lng_settings_notifications_display_default(tr::now), + st::settingsSendType), + st::settingsSendTypePadding); + + for (auto i = 0; i < screens.size(); ++i) { + const auto &screen = screens[i]; + const auto name = Platform::ScreenDisplayLabel(screen); + const auto geometry = screen->geometry(); + const auto resolution = QString::number(geometry.width()) + + QChar(0x00D7) + + QString::number(geometry.height()); + const auto label = name.isEmpty() + ? QString("Display (%1)").arg(resolution) + : QString("%1 (%2)").arg(name).arg(resolution); + advancedWrap->add( + object_ptr( + advancedWrap, + group, + i, + label, + st::settingsSendType), + st::settingsSendTypePadding); + } + group->setChangedCallback([=](int selectedIndex) { + if (selectedIndex == kDefaultDisplayIndex) { + Core::App().settings().setNotificationsDisplayChecksum(0); + Core::App().saveSettings(); + Core::App().notifications().notifySettingsChanged( + ChangeType::Corner); + } else { + const auto screens = QGuiApplication::screens(); + if (selectedIndex >= 0 && selectedIndex < screens.size()) { + const auto checksum = Platform::ScreenNameChecksum( + screens[selectedIndex]); + Core::App().settings().setNotificationsDisplayChecksum( + checksum); + Core::App().saveSettings(); + Core::App().notifications().notifySettingsChanged( + ChangeType::Corner); + } + } + }); + } + + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + Ui::AddDivider(advancedWrap); + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + Ui::AddSubsectionTitle( + advancedWrap, + tr::lng_settings_notifications_position()); + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + + const auto position = advancedWrap->add( + object_ptr(advancedWrap, controller)); + + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + Ui::AddSubsectionTitle(advancedWrap, tr::lng_settings_notifications_count()); + + const auto countSlider = advancedWrap->add( + object_ptr(advancedWrap, st::settingsSlider), + st::settingsBigScalePadding); + for (int i = 0; i != kMaxNotificationsCount; ++i) { + countSlider->addSection(QString::number(i + 1)); + } + countSlider->setActiveSectionFast(CurrentNotificationsCount() - 1); + countSlider->sectionActivated( + ) | rpl::on_next([=](int section) { + position->setCount(section + 1); + }, countSlider->lifetime()); + Ui::AddSkip(advancedWrap, st::settingsCheckboxesSkip); + + if (Core::App().settings().nativeNotifications()) { + advancedSlide->hide(anim::type::instant); + } + + Core::App().notifications().settingsChanged( + ) | rpl::on_next([=](ChangeType change) { + if (change == ChangeType::DesktopEnabled) { + const auto native = Core::App().settings().nativeNotifications(); + advancedSlide->toggle(!native, anim::type::normal); + } + }, advancedSlide->lifetime()); +} + +void BuildNotificationsSectionContent( + SectionBuilder &builder, + Window::SessionController *controller) { + const auto container = builder.container(); + + builder.addSkip(st::settingsPrivacySkip); + + BuildMultiAccountSection(builder, controller); + BuildGlobalNotificationsSection(builder, controller); + + if (container && controller) { + BuildNotifyViewSection(builder, controller, container->lifetime()); + } + + BuildNotifyTypeSection(builder, controller); + BuildEventNotificationsSection(builder, controller); + BuildCallNotificationsSection(builder, controller); + BuildBadgeCounterSection(builder, controller); + BuildSystemIntegrationAndAdvancedSection(builder, controller); +} + +} // namespace + +void BuildNotificationsSection( + not_null container, + not_null controller, + Fn showOther) { + const auto isPaused = Window::PausedIn( + controller, + Window::GifPauseReason::Layer); + auto builder = SectionBuilder(WidgetContext{ + .container = container, + .controller = controller, + .showOther = std::move(showOther), + .isPaused = isPaused, + }); + BuildNotificationsSectionContent(builder, controller); +} + +std::vector BuildNotificationsSectionForSearch() { + auto entries = std::vector(); + auto builder = SectionBuilder(SearchContext{ + .entries = &entries, + }); + BuildNotificationsSectionContent(builder, nullptr); + return entries; +} + +} // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/builder/settings_notifications_builder.h b/Telegram/SourceFiles/settings/builder/settings_notifications_builder.h new file mode 100644 index 0000000000..a6238a1441 --- /dev/null +++ b/Telegram/SourceFiles/settings/builder/settings_notifications_builder.h @@ -0,0 +1,29 @@ +/* +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 "settings/builder/settings_builder.h" + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace Settings::Builder { + +void BuildNotificationsSection( + not_null container, + not_null controller, + Fn showOther); + +[[nodiscard]] std::vector BuildNotificationsSectionForSearch(); + +} // namespace Settings::Builder diff --git a/Telegram/SourceFiles/settings/settings_notifications.cpp b/Telegram/SourceFiles/settings/settings_notifications.cpp index 109dc62c33..cd595b235c 100644 --- a/Telegram/SourceFiles/settings/settings_notifications.cpp +++ b/Telegram/SourceFiles/settings/settings_notifications.cpp @@ -7,124 +7,50 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_notifications.h" -#include "ui/widgets/continuous_sliders.h" +#include "settings/builder/settings_notifications_builder.h" +#include "settings/settings_notifications_common.h" #include "settings/settings_notifications_type.h" #include "ui/boxes/confirm_box.h" +#include "ui/chat/chat_theme.h" #include "ui/controls/chat_service_checkbox.h" #include "ui/effects/animations.h" -#include "data/notify/data_peer_notify_volume.h" -#include "ui/chat/chat_theme.h" -#include "ui/text/text_utilities.h" -#include "ui/wrap/vertical_layout.h" -#include "ui/wrap/slide_wrap.h" -#include "ui/widgets/box_content_divider.h" -#include "ui/widgets/checkbox.h" -#include "ui/widgets/buttons.h" -#include "ui/widgets/discrete_sliders.h" #include "ui/painter.h" -#include "ui/vertical_list.h" #include "ui/ui_utility.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "core/application.h" +#include "data/data_session.h" +#include "data/notify/data_notify_settings.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mainwindow.h" #include "window/notifications_manager.h" #include "window/section_widget.h" #include "window/themes/window_theme.h" #include "window/window_session_controller.h" -#include "platform/platform_specific.h" -#include "platform/platform_notifications_manager.h" -#include "base/platform/base_platform_info.h" -#include "mainwindow.h" -#include "core/application.h" -#include "main/main_session.h" -#include "main/main_account.h" -#include "main/main_domain.h" -#include "api/api_authorizations.h" -#include "api/api_ringtones.h" -#include "data/data_chat_filters.h" -#include "data/data_session.h" -#include "data/data_document.h" -#include "data/notify/data_notify_settings.h" -#include "boxes/ringtones_box.h" -#include "apiwrap.h" -#include "styles/style_settings.h" #include "styles/style_boxes.h" +#include "styles/style_chat.h" +#include "styles/style_dialogs.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" -#include "styles/style_chat.h" +#include "styles/style_settings.h" #include "styles/style_window.h" -#include "styles/style_dialogs.h" #include -#include -#include namespace Settings { -namespace { -constexpr auto kMaxNotificationsCount = 5; -constexpr auto kDefaultDisplayIndex = -1; +using ChangeType = Window::Notifications::ChangeType; -[[nodiscard]] int CurrentCount() { +int CurrentNotificationsCount() { return std::clamp( Core::App().settings().notificationsCount(), 1, kMaxNotificationsCount); } -using ChangeType = Window::Notifications::ChangeType; - -class NotificationsCount : public Ui::RpWidget { -public: - NotificationsCount( - QWidget *parent, - not_null controller); - - void setCount(int count); - - ~NotificationsCount(); - -protected: - void paintEvent(QPaintEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void leaveEventHook(QEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - - int resizeGetHeight(int newWidth) override; - -private: - using ScreenCorner = Core::Settings::ScreenCorner; - void setOverCorner(ScreenCorner corner); - void clearOverCorner(); - - class SampleWidget; - void removeSample(SampleWidget *widget); - - QRect getScreenRect() const; - QRect getScreenRect(int width) const; - int getContentLeft() const; - void prepareNotificationSampleSmall(); - void prepareNotificationSampleLarge(); - void prepareNotificationSampleUserpic(); - - const not_null _controller; - - QPixmap _notificationSampleUserpic; - QPixmap _notificationSampleSmall; - QPixmap _notificationSampleLarge; - ScreenCorner _chosenCorner; - std::vector _sampleOpacities; - - bool _isOverCorner = false; - ScreenCorner _overCorner = ScreenCorner::TopLeft; - bool _isDownCorner = false; - ScreenCorner _downCorner = ScreenCorner::TopLeft; - - int _oldCount; - - std::vector _cornerSamples[4]; - -}; - class NotificationsCount::SampleWidget : public QWidget { public: SampleWidget(NotificationsCount *owner, const QPixmap &cache); @@ -306,7 +232,7 @@ NotificationsCount::NotificationsCount( not_null controller) : _controller(controller) , _chosenCorner(Core::App().settings().notificationsCorner()) -, _oldCount(CurrentCount()) { +, _oldCount(CurrentNotificationsCount()) { setMouseTracking(true); _sampleOpacities.resize(kMaxNotificationsCount); @@ -780,12 +706,6 @@ void NotifyPreview::paint(Painter &p, int x, int y) { 2); } -struct NotifyViewCheckboxes { - not_null*> wrap; - not_null name; - not_null preview; -}; - NotifyViewCheckboxes SetupNotifyViewOptions( not_null controller, not_null container, @@ -873,539 +793,6 @@ NotifyViewCheckboxes SetupNotifyViewOptions( }; } -void SetupAdvancedNotifications( - not_null controller, - not_null container) { - if (Platform::IsWindows()) { - const auto skipInFocus = container->add(object_ptr